diff --git a/.gitignore b/.gitignore index b32b49f..bd3907a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,8 @@ *.swo *.pyc config.json +dist +build +imgurpython.egg-info +runner.py +examples/auth.ini diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..cd12a9d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +language: python +script: + - python -m compileall +deploy: + provider: pypi + user: imgurops + password: + secure: DBBXzMOm037T4XUmfo0Gu9mAytw2DCYJT8i0KgihKYxS+uslF+dwHf2clBEWDLUE0xkXhqXetq+sNgfshovGKIqZanASYZ/6Zf5ikg10ApgaBidObv2XMYNyuQxL8Gqv9l2tdlWqdUoOJzRBMV2Nh0B3BJ9hG7V5NFMDcfG/qyo= + on: + tags: true + repo: Imgur/imgurpython diff --git a/EXAMPLES.md b/EXAMPLES.md new file mode 100644 index 0000000..2ec2cb7 --- /dev/null +++ b/EXAMPLES.md @@ -0,0 +1,85 @@ +Examples +------------ + +## Anonymous Usage without user authorization + +### Print links in a Gallery +Output links from gallery could be a GalleyImage or GalleryAlbum + +#### Default +By default, this will return links to items on the first page (0) with section 'hot' sorted by 'viral', date range is 'day' and show_viral is set to True + +```python +items = client.gallery() +for item in items: + print(item.link) + +``` + +**Output** + + + http://i.imgur.com/dRMIpvS.png + http://imgur.com/a/uxKYS + http://i.imgur.com/jYvaxQ1.jpg + http://i.imgur.com/ZWQJSXp.jpg + http://i.imgur.com/arP5ZwL.jpg + http://i.imgur.com/BejpKnz.jpg + http://i.imgur.com/4FJF0Vt.jpg + http://i.imgur.com/MZSBjTP.jpg + http://i.imgur.com/EbeztS2.jpg + http://i.imgur.com/DuwnhKO.jpg + ... + +#### With Specific Parameters +In this example, return links to items on the fourth page (3) with section 'top' sorted by 'time', date range is 'week' and show_viral is set to False + +```python +items = client.gallery(section='top', sort='time', page=3, window='week', show_viral=False) +for item in items: + print(item.link) + +``` + +**Output** + + + http://i.imgur.com/ls7OPx7.gif + http://i.imgur.com/FI7yPWo.png + http://imgur.com/a/8QKvH + http://i.imgur.com/h4IDMyK.gif + http://i.imgur.com/t4NpfCT.jpg + http://i.imgur.com/kyCP6q9.jpg + http://imgur.com/a/CU11w + http://i.imgur.com/q4rJFbR.jpg + http://i.imgur.com/gWaNC22.jpg + http://i.imgur.com/YEQomCd.gif + ... + +#### Getting the authenticated user's albums + +For endpoints that require usernames, once a user is authenticated we can use the keyword 'me' to pull their information. Here's how to pull one of their albums: + +```python +for album in client.get_account_albums('me'): +album_title = album.title if album.title else 'Untitled' +print('Album: {0} ({1})'.format(album_title, album.id)) + +for image in client.get_album_images(album.id): + image_title = image.title if image.title else 'Untitled' + print('\t{0}: {1}'.format(image_title, image.link)) + +# Save some API credits by not getting all albums +break +``` + +***Output*** + + + Album: Qittens! (LPNnY) + Untitled: http://i.imgur.com/b9rL7ew.jpg + Untitled: http://i.imgur.com/Ymg3obW.jpg + Untitled: http://i.imgur.com/kMzbu0S.jpg + ... + + diff --git a/Imgur/Auth/AccessToken.py b/Imgur/Auth/AccessToken.py deleted file mode 100644 index ec5973a..0000000 --- a/Imgur/Auth/AccessToken.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python3 - -from .Base import Base as AuthBase -import time as dt - -class AccessToken(AuthBase): - def __init__(self, access, refresh, expire_time): - self.access = access - self.refresh = refresh - self.expire_time = expire_time - - def need_to_authorize(self, time): - return (self.expire_time <= time) - - def add_authorization_header(self, request): - request.add_header('Authorization', 'Bearer ' + self.access) - return request - - def get_access_token(self): - return self.access - - def get_refresh_token(self): - return self.refresh diff --git a/Imgur/Auth/Anonymous.py b/Imgur/Auth/Anonymous.py deleted file mode 100644 index 8f14bd3..0000000 --- a/Imgur/Auth/Anonymous.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python3 - -class Anonymous: - def __init__(self, client_id): - self.client_id = client_id - - def need_to_authorize(self): - return False - - def authorize(self): - pass - - def add_authorization_header(self, request): - request.add_header('Authorization', 'Client-ID ' + self.client_id) - return request diff --git a/Imgur/Auth/Base.py b/Imgur/Auth/Base.py deleted file mode 100644 index 1547561..0000000 --- a/Imgur/Auth/Base.py +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env python3 - -class Base: - def need_to_authorize(self, time): - '''Do we need to refresh our authorization token?''' - pass - def authorize(self, api, requestfactory): - '''Refresh our access token''' - pass - def add_authorization_header(self, request): - pass - diff --git a/Imgur/Auth/Expired.py b/Imgur/Auth/Expired.py deleted file mode 100644 index 9407393..0000000 --- a/Imgur/Auth/Expired.py +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env python3 - -class Expired(BaseException): - def __str__(self): - return "Access token invalid or expired." - diff --git a/Imgur/Factory.py b/Imgur/Factory.py deleted file mode 100644 index 58c5bd2..0000000 --- a/Imgur/Factory.py +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/env python3 - -import base64, os.path, time as dt - -try: - from urllib.request import Request as UrlLibRequest - from urllib.parse import urlencode as UrlLibEncode -except ImportError: - from urllib2 import Request as UrlLibRequest - from urllib import urlencode as UrlLibEncode - -from .Imgur import Imgur -from .RateLimit import RateLimit -from .Auth.AccessToken import AccessToken -from .Auth.Anonymous import Anonymous - -class Factory: - - API_URL = "https://api.imgur.com/" - - def __init__(self, config): - self.config = config - if 'api' in self.config: - self.API_URL = self.config['api'] - - def get_api_url(self): - return self.API_URL - - def build_api(self, auth = None, ratelimit = None): - if auth is None: - auth = self.build_anonymous_auth() - if ratelimit is None: - ratelimit = self.build_rate_limit() - return Imgur(self.config['client_id'], self.config['secret'], auth, ratelimit) - - def build_anonymous_auth(self): - return Anonymous(self.config['client_id']) - - def build_oauth(self, access, refresh, expire_time = None): - now = int(dt.time()) - if expire_time is None: - return AccessToken(access, refresh, now) - else: - return AccessToken(access, refresh, expire_time) - - def build_request(self, endpoint, data = None): - '''Expects an endpoint like 'image.json' or a tuple like ('gallery', 'hot', 'viral', '0'). - - Prepends 3/ and appends \.json to the tuple-form, not the endpoint form.''' - if isinstance(endpoint, str): - url = self.API_URL + endpoint - else: - url = self.API_URL + '3/' + ('/'.join(endpoint)) + ".json" - - req = UrlLibRequest(url) - if data is not None: - req.add_data(UrlLibEncode(data).encode('utf-8')) - return req - - def build_rate_limit(self, limits = None): - '''If none, defaults to fresh rate limits. Else expects keys "client_limit", "user_limit", "user_reset"''' - if limits is not None: - return RateLimit(limits['client_limit'], limits['user_limit'], limits['user_reset']) - else: - return RateLimit() - - def build_rate_limits_from_server(self, api): - '''Get the rate limits for this application and build a rate limit model from it.''' - req = self.build_request('credits') - res = api.retrieve(req) - return RateLimit(res['ClientRemaining'], res['UserRemaining'], res['UserReset']) - - - def build_request_upload_from_path(self, path, params = dict()): - fd = open(path, 'rb') - contents = fd.read() - b64 = base64.b64encode(contents) - data = { - 'image': b64, - 'type': 'base64', - 'name': os.path.basename(path) - } - data.update(params) - return self.build_request(('upload',), data) - - def build_request_oauth_token_swap(self, grant_type, token): - data = { - 'client_id': self.config['client_id'], - 'client_secret': self.config['secret'], - 'grant_type': grant_type - } - - if grant_type == 'authorization_code': - data['code'] = token - if grant_type == 'pin': - data['pin'] = token - - return self.build_request('oauth2/token', data) - - def build_request_oauth_refresh(self, refresh_token): - data = { - 'refresh_token': refresh_token, - 'client_id': self.config['client_id'], - 'client_secret': self.config['secret'], - 'grant_type': 'refresh_token' - } - return self.build_request('oauth2/token', data) diff --git a/Imgur/Imgur.py b/Imgur/Imgur.py deleted file mode 100644 index 774f1e1..0000000 --- a/Imgur/Imgur.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env python3 - -import json - -try: - from urllib.request import urlopen as UrlLibOpen - from urllib.request import HTTPError -except ImportError: - from urllib2 import urlopen as UrlLibOpen - from urllib2 import HTTPError - -from .Auth.Expired import Expired - -class Imgur: - - def __init__(self, client_id, secret, auth, ratelimit): - self.client_id = client_id - self.secret = secret - self.auth = auth - self.ratelimit = ratelimit - - def retrieve_raw(self, request): - request = self.auth.add_authorization_header(request) - req = UrlLibOpen(request) - res = json.loads(req.read().decode('utf-8')) - return (req, res) - - def retrieve(self, request): - try: - (req, res) = self.retrieve_raw(request) - except HTTPError as e: - if e.code == 403: - raise Expired() - else: - print("Error %d\n%s\n" % (e.code, e.read())) - raise e - - self.ratelimit.update(req.info()) - if res['success'] is not True: - raise Exception(res['data']['error']['message']) - - return res['data'] - - def get_rate_limit(self): - return self.ratelimit - - def get_auth(self): - return self.auth - - def get_client_id(self): - return self.client_id diff --git a/Imgur/RateLimit.py b/Imgur/RateLimit.py deleted file mode 100644 index 1e62dde..0000000 --- a/Imgur/RateLimit.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python3 - -import time as dt - -class RateLimit: - - def __init__(self, client_remaining = 12500, user_remaining = 500, user_reset = None): - self.client_remaining = client_remaining - self.user_remaining = user_remaining - self.user_reset = user_reset - - def update(self, headers): - '''Update the rate limit state with a fresh API response''' - - if 'X-RateLimit-ClientRemaining' in headers: - self.client_remaining = int(headers['X-RateLimit-ClientRemaining']) - self.user_remaining = int(headers['X-RateLimit-UserRemaining']) - self.user_reset = int(headers['X-RateLimit-UserReset']) - - def is_over(self, time): - return self.would_be_over(0, time) - - def would_be_over(self, cost, time): - return self.client_remaining < cost or (self.user_reset is not None and self.user_reset > time and self.user_remaining < cost) - - def __str__(self, time = None): - # can't ask for time by DI when doing str(x). - if time is None: - time = dt.time() - - exp = int(self.user_reset) - int(time) - return "" % (self.client_remaining, self.user_remaining, exp) diff --git a/LICENSE b/LICENSE.txt similarity index 98% rename from LICENSE rename to LICENSE.txt index 57b39f8..b10a2fe 100644 --- a/LICENSE +++ b/LICENSE.txt @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index a9d5f28..c89b846 100644 --- a/README.md +++ b/README.md @@ -1,89 +1,236 @@ -Imgur API Example Usage -======================= - -This is a demo application of the [Imgur API](http://api.imgur.com/). It can be used to interrogate the Imgur API and -examine its responses, as a simple command line utility, and it can be used as a library for working with the Imgur API. - -You must [register](http://api.imgur.com/oauth2/addclient) your client with the Imgur API, and provide the client ID to -do *any* request to the API. If you want to perform actions on accounts, the user will have to authorize it through OAuth. -The **secret** field is required for OAuth. - -Usage ------ - -> Usage: python main.py (action) [options...] -> -> ### OAuth Actions -> -> **credits** -> View the rate limit information for this client -> -> **authorize** -> Start the authorization process -> -> **authorize [pin]** -> Get an access token after starting authorization -> -> **refresh [refresh-token]** -> Return a new OAuth access token after it's expired -> -> ### Unauthorized Actions -> -> **upload [file]** -> Anonymously upload a file -> -> **list-comments [hash]** -> Get the comments (raw JSON) for a gallery post -> -> **get-album [id]** -> Get information (raw JSON) about an album -> -> **get-comment [id]** -> Get a particular comment (raw JSON) for a gallery comment -> -> **get-gallery [hash]** -> Get information (raw JSON) about a gallery post -> -> ### Authorized Actions -> -> **upload-auth [access-token]** -> Upload a file to your account -> -> **comment [access-token] [hash] [text ...]** -> Comment on a gallery post -> -> **vote-gallery [token] [hash] [direction]** -> Vote on a gallery post. Direction either 'up', 'down', or 'veto' -> -> **vote-comment [token] [id] [direction]** -> Vote on a gallery comment. Direction either 'up', 'down', or 'veto' - -Config ------- - -Configuration is done through the **config.json** file in JSON format. The contents of the file should be a JSON -object with the following properties: - -### client_id - -**Key**: 'client_id' - -**Type**: String [16 characters] - -**Description**: The client ID you got when you registered. Required for any API call. - -### secret - -**Key**: 'secret' - -**Type**: String [40 characters] - -**Description**: The client secret you got when you registered, if you want to do OAuth authentication. - -### token_store - -**Key**: 'token_store' - -**Type**: Object - -**Description**: Future configuration to control where the tokens are stored for persistent **insecure** storage of refresh tokens. +# The imgurpython project is no longer supported. + +imgurpython +=========== + +A Python client for the [Imgur API](http://api.imgur.com/). It can be used to +interact with the Imgur API in your projects. + +You must [register](http://api.imgur.com/oauth2/addclient) your client with the Imgur API, and provide the Client-ID to +make *any* request to the API (see the [Authentication](https://api.imgur.com/#authentication) note). If you want to +perform actions on accounts, the user will have to authorize your application through OAuth2. + +Requirements +------------ + +- Python >= 2.7 +- [requests](http://docs.python-requests.org/en/latest/user/install/) + +Imgur API Documentation +----------------------- + +Our developer documentation can be found [here](https://api.imgur.com/). + +Community +--------- + +The best way to reach out to Imgur for API support is emailing us at api@imgur.com. + +Installation +------------ + + pip install imgurpython + +Library Usage +------------ + +Using imgurpython in your application takes just a couple quick steps. + +To use the client from a strictly anonymous context (no actions on behalf of a user) + +```python + +from imgurpython import ImgurClient + +client_id = 'YOUR CLIENT ID' +client_secret = 'YOUR CLIENT SECRET' + +client = ImgurClient(client_id, client_secret) + +# Example request +items = client.gallery() +for item in items: + print(item.link) + +``` + +To initialize a client that takes actions on behalf of a user + +```python +from imgurpython import ImgurClient + +client_id = 'YOUR CLIENT ID' +client_secret = 'YOUR CLIENT SECRET' + +client = ImgurClient(client_id, client_secret) + +# Authorization flow, pin example (see docs for other auth types) +authorization_url = client.get_auth_url('pin') + +# ... redirect user to `authorization_url`, obtain pin (or code or token) ... + +credentials = client.authorize('PIN OBTAINED FROM AUTHORIZATION', 'pin') +client.set_user_auth(credentials['access_token'], credentials['refresh_token']) +``` + +or if you already have an access/refresh token pair you can simply do + +```python +from imgurpython import ImgurClient + +# If you already have an access/refresh pair in hand +client_id = 'YOUR CLIENT ID' +client_secret = 'YOUR CLIENT SECRET' +access_token = 'USER ACCESS TOKEN' +refresh_token = 'USER REFRESH TOKEN' + +# Note since access tokens expire after an hour, only the refresh token is required (library handles autorefresh) +client = ImgurClient(client_id, client_secret, access_token, refresh_token) +``` + +### Error Handling +Error types +* ImgurClientError - General error handler, access message and status code via + +```python +from imgurpython.helpers.error import ImgurClientError + +try + ... +except ImgurClientError as e + print(e.error_message) + print(e.status_code) +``` + +* ImgurClientRateLimitError - Rate limit error + +### Credits + +To view client and user credit information, use the `credits` attribute of `ImgurClient`. +`credits` holds a dictionary with the following keys: +* UserLimit +* UserRemaining +* UserReset +* ClientLimit +* ClientRemaining + +For more information about rate-limiting, please see the note in our [docs](http://api.imgur.com/#limits)! + +Examples +------------ +Examples can be found [here](EXAMPLES.md) + +## ImgurClient Functions + +### Account + +* `get_account(username)` +* `get_gallery_favorites(username)` +* `get_account_favorites(username)` +* `get_account_submissions(username, page=0)` +* `get_account_settings(username)` +* `change_account_settings(username, fields)` +* `get_email_verification_status(username)` +* `send_verification_email(username)` +* `get_account_albums(username, page=0)` +* `get_account_album_ids(username, page=0)` +* `get_account_album_count(username)` +* `get_account_comments(username, sort='newest', page=0)` +* `get_account_comment_ids(username, sort='newest', page=0)` +* `get_account_comment_count(username)` +* `get_account_images(username, page=0)` +* `get_account_image_ids(username, page=0)` +* `get_account_album_count(username)` + +### Album +* `get_album(album_id)` +* `get_album_images(album_id)` +* `create_album(fields)` +* `update_album(album_id, fields)` +* `album_delete(album_id)` +* `album_favorite(album_id)` +* `album_set_images(album_id, ids)` +* `album_add_images(album_id, ids)` +* `album_remove_images(album_id, ids)` + +### Comment +* `get_comment(comment_id)` +* `delete_comment(comment_id)` +* `get_comment_replies(comment_id)` +* `post_comment_reply(comment_id, image_id, comment)` +* `comment_vote(comment_id, vote='up')` +* `comment_report(comment_id)` + +### Custom Gallery + +* `get_custom_gallery(gallery_id, sort='viral', window='week', page=0)` +* `get_user_galleries()` +* `create_custom_gallery(name, tags=None)` +* `custom_gallery_update(gallery_id, name)` +* `custom_gallery_add_tags(gallery_id, tags)` +* `custom_gallery_remove_tags(gallery_id, tags)` +* `custom_gallery_delete(gallery_id)` +* `filtered_out_tags()` +* `block_tag(tag)` +* `unblock_tag(tag)` + +### Gallery + +* `gallery(section='hot', sort='viral', page=0, window='day', show_viral=True)` +* `memes_subgallery(sort='viral', page=0, window='week')` +* `memes_subgallery_image(item_id)` +* `subreddit_gallery(subreddit, sort='time', window='week', page=0)` +* `subreddit_image(subreddit, image_id)` +* `gallery_tag(tag, sort='viral', page=0, window='week')` +* `gallery_tag_image(tag, item_id)` +* `gallery_item_tags(item_id)` +* `gallery_tag_vote(item_id, tag, vote)` +* `gallery_search(q, advanced=None, sort='time', window='all', page=0)` +* `gallery_random(page=0)` +* `share_on_imgur(item_id, title, terms=0)` +* `remove_from_gallery(item_id)` +* `gallery_item(item_id)` +* `report_gallery_item(item_id)` +* `gallery_item_vote(item_id, vote='up')` +* `gallery_item_comments(item_id, sort='best')` +* `gallery_comment(item_id, comment)` +* `gallery_comment_ids(item_id)` +* `gallery_comment_count(item_id)` + +### Image + +* `get_image(image_id)` +* `upload_from_path(path, config=None, anon=True)` +* `upload_from_url(url, config=None, anon=True)` +* `delete_image(image_id)` +* `favorite_image(image_id)` + +### Conversation + +* `conversation_list()` +* `get_conversation(conversation_id, page=1, offset=0)` +* `create_message(recipient, body)` +* `delete_conversation(conversation_id)` +* `report_sender(username)` +* `block_sender(username)` + +### Notification + +* `get_notifications(new=True)` +* `get_notification(notification_id)` +* `mark_notifications_as_read(notification_ids)` + +### Memegen + +* `default_memes()` + +Imgur entry points +================== +| entry point | content | +|-------------------------------------|--------------------------------| +| imgur.com/{image_id} | image | +| imgur.com/{image_id}.extension | direct link to image (no html) | +| imgur.com/a/{album_id} | album | +| imgur.com/a/{album_id}#{image_id} | single image from an album | +| imgur.com/gallery/{gallery_post_id} | gallery | + diff --git a/config.json.sample b/config.json.sample deleted file mode 100644 index aaaf445..0000000 --- a/config.json.sample +++ /dev/null @@ -1,5 +0,0 @@ -{ - "client_id": "", - "secret": "", - "token_store": {} -} diff --git a/examples/auth.ini b/examples/auth.ini new file mode 100644 index 0000000..8eb90dc --- /dev/null +++ b/examples/auth.ini @@ -0,0 +1,4 @@ +[credentials] +client_id=YOUR ID HERE +client_secret=YOUR SECRET HERE +refresh_token= \ No newline at end of file diff --git a/examples/auth.py b/examples/auth.py new file mode 100755 index 0000000..2f0b1f4 --- /dev/null +++ b/examples/auth.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 + +''' + Here's how you go about authenticating yourself! The important thing to + note here is that this script will be used in the other examples so + set up a test user with API credentials and set them up in auth.ini. +''' + +from imgurpython import ImgurClient +from helpers import get_input, get_config + +def authenticate(): + # Get client ID and secret from auth.ini + config = get_config() + config.read('auth.ini') + client_id = config.get('credentials', 'client_id') + client_secret = config.get('credentials', 'client_secret') + + client = ImgurClient(client_id, client_secret) + + # Authorization flow, pin example (see docs for other auth types) + authorization_url = client.get_auth_url('pin') + + print("Go to the following URL: {0}".format(authorization_url)) + + # Read in the pin, handle Python 2 or 3 here. + pin = get_input("Enter pin code: ") + + # ... redirect user to `authorization_url`, obtain pin (or code or token) ... + credentials = client.authorize(pin, 'pin') + client.set_user_auth(credentials['access_token'], credentials['refresh_token']) + + print("Authentication successful! Here are the details:") + print(" Access token: {0}".format(credentials['access_token'])) + print(" Refresh token: {0}".format(credentials['refresh_token'])) + + return client + +# If you want to run this as a standalone script, so be it! +if __name__ == "__main__": + authenticate() \ No newline at end of file diff --git a/examples/helpers.py b/examples/helpers.py new file mode 100644 index 0000000..7c9a0e8 --- /dev/null +++ b/examples/helpers.py @@ -0,0 +1,20 @@ +''' + These functions have nothing to do with the API, they just help ease + issues between Python 2 and 3 +''' + +def get_input(string): + ''' Get input from console regardless of python 2 or 3 ''' + try: + return raw_input(string) + except: + return input(string) + +def get_config(): + ''' Create a config parser for reading INI files ''' + try: + import ConfigParser + return ConfigParser.ConfigParser() + except: + import configparser + return configparser.ConfigParser() \ No newline at end of file diff --git a/examples/upload.py b/examples/upload.py new file mode 100755 index 0000000..3014d6f --- /dev/null +++ b/examples/upload.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 + +''' + Here's how you upload an image. For this example, put the cutest picture + of a kitten you can find in this script's folder and name it 'Kitten.jpg' + + For more details about images and the API see here: + https://api.imgur.com/endpoints/image +''' + +# Pull authentication from the auth example (see auth.py) +from auth import authenticate + +from datetime import datetime + +album = None # You can also enter an album ID here +image_path = 'Kitten.jpg' + +def upload_kitten(client): + ''' + Upload a picture of a kitten. We don't ship one, so get creative! + ''' + + # Here's the metadata for the upload. All of these are optional, including + # this config dict itself. + config = { + 'album': album, + 'name': 'Catastrophe!', + 'title': 'Catastrophe!', + 'description': 'Cute kitten being cute on {0}'.format(datetime.now()) + } + + print("Uploading image... ") + image = client.upload_from_path(image_path, config=config, anon=False) + print("Done") + print() + + return image + + +# If you want to run this as a standalone script +if __name__ == "__main__": + client = authenticate() + image = upload_kitten(client) + + print("Image was posted! Go check your images you sexy beast!") + print("You can find it here: {0}".format(image['link'])) \ No newline at end of file diff --git a/imgurpython/__init__.py b/imgurpython/__init__.py new file mode 100644 index 0000000..651dc85 --- /dev/null +++ b/imgurpython/__init__.py @@ -0,0 +1 @@ +from .client import ImgurClient \ No newline at end of file diff --git a/imgurpython/client.py b/imgurpython/client.py new file mode 100644 index 0000000..9c41ea6 --- /dev/null +++ b/imgurpython/client.py @@ -0,0 +1,683 @@ +import base64 +import requests +from .imgur.models.tag import Tag +from .imgur.models.album import Album +from .imgur.models.image import Image +from .imgur.models.account import Account +from .imgur.models.comment import Comment +from .imgur.models.tag_vote import TagVote +from .helpers.error import ImgurClientError +from .helpers.format import build_notification +from .helpers.format import format_comment_tree +from .helpers.format import build_notifications +from .imgur.models.conversation import Conversation +from .helpers.error import ImgurClientRateLimitError +from .helpers.format import build_gallery_images_and_albums +from .imgur.models.custom_gallery import CustomGallery +from .imgur.models.account_settings import AccountSettings + +API_URL = 'https://api.imgur.com/' +MASHAPE_URL = 'https://imgur-apiv3.p.mashape.com/' + + +class AuthWrapper(object): + def __init__(self, access_token, refresh_token, client_id, client_secret): + self.current_access_token = access_token + + if refresh_token is None: + raise TypeError('A refresh token must be provided') + + self.refresh_token = refresh_token + self.client_id = client_id + self.client_secret = client_secret + + def get_refresh_token(self): + return self.refresh_token + + def get_current_access_token(self): + return self.current_access_token + + def refresh(self): + data = { + 'refresh_token': self.refresh_token, + 'client_id': self.client_id, + 'client_secret': self.client_secret, + 'grant_type': 'refresh_token' + } + + url = API_URL + 'oauth2/token' + + response = requests.post(url, data=data) + + if response.status_code != 200: + raise ImgurClientError('Error refreshing access token!', response.status_code) + + response_data = response.json() + self.current_access_token = response_data['access_token'] + + +class ImgurClient(object): + allowed_album_fields = { + 'ids', 'title', 'description', 'privacy', 'layout', 'cover' + } + + allowed_advanced_search_fields = { + 'q_all', 'q_any', 'q_exactly', 'q_not', 'q_type', 'q_size_px' + } + + allowed_account_fields = { + 'bio', 'public_images', 'messaging_enabled', 'album_privacy', 'accepted_gallery_terms', 'username' + } + + allowed_image_fields = { + 'album', 'name', 'title', 'description' + } + + def __init__(self, client_id, client_secret, access_token=None, refresh_token=None, mashape_key=None): + self.client_id = client_id + self.client_secret = client_secret + self.auth = None + self.mashape_key = mashape_key + + if refresh_token is not None: + self.auth = AuthWrapper(access_token, refresh_token, client_id, client_secret) + + self.credits = self.get_credits() + + def set_user_auth(self, access_token, refresh_token): + self.auth = AuthWrapper(access_token, refresh_token, self.client_id, self.client_secret) + + def get_client_id(self): + return self.client_id + + def get_credits(self): + return self.make_request('GET', 'credits', None, True) + + def get_auth_url(self, response_type='pin'): + return '%soauth2/authorize?client_id=%s&response_type=%s' % (API_URL, self.client_id, response_type) + + def authorize(self, response, grant_type='pin'): + return self.make_request('POST', 'oauth2/token', { + 'client_id': self.client_id, + 'client_secret': self.client_secret, + 'grant_type': grant_type, + 'code' if grant_type == 'authorization_code' else grant_type: response + }, True) + + def prepare_headers(self, force_anon=False): + headers = {} + if force_anon or self.auth is None: + if self.client_id is None: + raise ImgurClientError('Client credentials not found!') + else: + headers['Authorization'] = 'Client-ID %s' % self.get_client_id() + else: + headers['Authorization'] = 'Bearer %s' % self.auth.get_current_access_token() + + if self.mashape_key is not None: + headers['X-Mashape-Key'] = self.mashape_key + + return headers + + + def make_request(self, method, route, data=None, force_anon=False): + method = method.lower() + method_to_call = getattr(requests, method) + + header = self.prepare_headers(force_anon) + url = (MASHAPE_URL if self.mashape_key is not None else API_URL) + ('3/%s' % route if 'oauth2' not in route else route) + + if method in ('delete', 'get'): + response = method_to_call(url, headers=header, params=data, data=data) + else: + response = method_to_call(url, headers=header, data=data) + + if response.status_code == 403 and self.auth is not None: + self.auth.refresh() + header = self.prepare_headers() + if method in ('delete', 'get'): + response = method_to_call(url, headers=header, params=data, data=data) + else: + response = method_to_call(url, headers=header, data=data) + + self.credits = { + 'UserLimit': response.headers.get('X-RateLimit-UserLimit'), + 'UserRemaining': response.headers.get('X-RateLimit-UserRemaining'), + 'UserReset': response.headers.get('X-RateLimit-UserReset'), + 'ClientLimit': response.headers.get('X-RateLimit-ClientLimit'), + 'ClientRemaining': response.headers.get('X-RateLimit-ClientRemaining') + } + + # Rate-limit check + if response.status_code == 429: + raise ImgurClientRateLimitError() + + try: + response_data = response.json() + except: + raise ImgurClientError('JSON decoding of response failed.') + + if 'data' in response_data and isinstance(response_data['data'], dict) and 'error' in response_data['data']: + raise ImgurClientError(response_data['data']['error'], response.status_code) + + return response_data['data'] if 'data' in response_data else response_data + + def validate_user_context(self, username): + if username == 'me' and self.auth is None: + raise ImgurClientError('\'me\' can only be used in the authenticated context.') + + def logged_in(self): + if self.auth is None: + raise ImgurClientError('Must be logged in to complete request.') + + # Account-related endpoints + def get_account(self, username): + self.validate_user_context(username) + account_data = self.make_request('GET', 'account/%s' % username) + + return Account( + account_data['id'], + account_data['url'], + account_data['bio'], + account_data['reputation'], + account_data['created'], + account_data['pro_expiration'], + ) + + def get_gallery_favorites(self, username, page=0): + self.validate_user_context(username) + gallery_favorites = self.make_request('GET', 'account/%s/gallery_favorites/%d' % (username, page)) + + return build_gallery_images_and_albums(gallery_favorites) + + def get_account_favorites(self, username, page=0): + self.validate_user_context(username) + favorites = self.make_request('GET', 'account/%s/favorites/%d' % (username, page)) + + return build_gallery_images_and_albums(favorites) + + def get_account_submissions(self, username, page=0): + self.validate_user_context(username) + submissions = self.make_request('GET', 'account/%s/submissions/%d' % (username, page)) + + return build_gallery_images_and_albums(submissions) + + def get_account_settings(self, username): + self.logged_in() + settings = self.make_request('GET', 'account/%s/settings' % username) + + return AccountSettings( + settings['email'], + settings['high_quality'], + settings['public_images'], + settings['album_privacy'], + settings['pro_expiration'], + settings['accepted_gallery_terms'], + settings['active_emails'], + settings['messaging_enabled'], + settings['blocked_users'] + ) + + def change_account_settings(self, username, fields): + post_data = {setting: fields[setting] for setting in set(self.allowed_account_fields).intersection(fields.keys())} + return self.make_request('POST', 'account/%s/settings' % username, post_data) + + def get_email_verification_status(self, username): + self.logged_in() + self.validate_user_context(username) + return self.make_request('GET', 'account/%s/verifyemail' % username) + + def send_verification_email(self, username): + self.logged_in() + self.validate_user_context(username) + return self.make_request('POST', 'account/%s/verifyemail' % username) + + def get_account_albums(self, username, page=0): + self.validate_user_context(username) + + albums = self.make_request('GET', 'account/%s/albums/%d' % (username, page)) + return [Album(album) for album in albums] + + def get_account_album_ids(self, username, page=0): + self.validate_user_context(username) + return self.make_request('GET', 'account/%s/albums/ids/%d' % (username, page)) + + def get_account_album_count(self, username): + self.validate_user_context(username) + return self.make_request('GET', 'account/%s/albums/count' % username) + + def get_account_comments(self, username, sort='newest', page=0): + self.validate_user_context(username) + comments = self.make_request('GET', 'account/%s/comments/%s/%s' % (username, sort, page)) + + return [Comment(comment) for comment in comments] + + def get_account_comment_ids(self, username, sort='newest', page=0): + self.validate_user_context(username) + return self.make_request('GET', 'account/%s/comments/ids/%s/%s' % (username, sort, page)) + + def get_account_comment_count(self, username): + self.validate_user_context(username) + return self.make_request('GET', 'account/%s/comments/count' % username) + + def get_account_images(self, username, page=0): + self.validate_user_context(username) + images = self.make_request('GET', 'account/%s/images/%d' % (username, page)) + + return [Image(image) for image in images] + + def get_account_image_ids(self, username, page=0): + self.validate_user_context(username) + return self.make_request('GET', 'account/%s/images/ids/%d' % (username, page)) + + def get_account_images_count(self, username): + self.validate_user_context(username) + return self.make_request('GET', 'account/%s/images/count' % username) + + # Album-related endpoints + def get_album(self, album_id): + album = self.make_request('GET', 'album/%s' % album_id) + return Album(album) + + def get_album_images(self, album_id): + images = self.make_request('GET', 'album/%s/images' % album_id) + return [Image(image) for image in images] + + def create_album(self, fields): + post_data = {field: fields[field] for field in set(self.allowed_album_fields).intersection(fields.keys())} + + if 'ids' in post_data: + self.logged_in() + + return self.make_request('POST', 'album', data=post_data) + + def update_album(self, album_id, fields): + post_data = {field: fields[field] for field in set(self.allowed_album_fields).intersection(fields.keys())} + + if isinstance(post_data['ids'], list): + post_data['ids'] = ','.join(post_data['ids']) + + return self.make_request('POST', 'album/%s' % album_id, data=post_data) + + def album_delete(self, album_id): + return self.make_request('DELETE', 'album/%s' % album_id) + + def album_favorite(self, album_id): + self.logged_in() + return self.make_request('POST', 'album/%s/favorite' % album_id) + + def album_set_images(self, album_id, ids): + if isinstance(ids, list): + ids = ','.join(ids) + + return self.make_request('POST', 'album/%s/' % album_id, {'ids': ids}) + + def album_add_images(self, album_id, ids): + if isinstance(ids, list): + ids = ','.join(ids) + + return self.make_request('POST', 'album/%s/add' % album_id, {'ids': ids}) + + def album_remove_images(self, album_id, ids): + if isinstance(ids, list): + ids = ','.join(ids) + + return self.make_request('DELETE', 'album/%s/remove_images' % album_id, {'ids': ids}) + + # Comment-related endpoints + def get_comment(self, comment_id): + comment = self.make_request('GET', 'comment/%d' % comment_id) + return Comment(comment) + + def delete_comment(self, comment_id): + self.logged_in() + return self.make_request('DELETE', 'comment/%d' % comment_id) + + def get_comment_replies(self, comment_id): + replies = self.make_request('GET', 'comment/%d/replies' % comment_id) + return format_comment_tree(replies) + + def post_comment_reply(self, comment_id, image_id, comment): + self.logged_in() + data = { + 'image_id': image_id, + 'comment': comment + } + + return self.make_request('POST', 'comment/%d' % comment_id, data) + + def comment_vote(self, comment_id, vote='up'): + self.logged_in() + return self.make_request('POST', 'comment/%d/vote/%s' % (comment_id, vote)) + + def comment_report(self, comment_id): + self.logged_in() + return self.make_request('POST', 'comment/%d/report' % comment_id) + + # Custom Gallery Endpoints + def get_custom_gallery(self, gallery_id, sort='viral', window='week', page=0): + gallery = self.make_request('GET', 'g/%s/%s/%s/%s' % (gallery_id, sort, window, page)) + return CustomGallery( + gallery['id'], + gallery['name'], + gallery['datetime'], + gallery['account_url'], + gallery['link'], + gallery['tags'], + gallery['item_count'], + gallery['items'] + ) + + def get_user_galleries(self): + self.logged_in() + galleries = self.make_request('GET', 'g') + + return [CustomGallery( + gallery['id'], + gallery['name'], + gallery['datetime'], + gallery['account_url'], + gallery['link'], + gallery['tags'] + ) for gallery in galleries] + + def create_custom_gallery(self, name, tags=None): + self.logged_in() + data = {'name': name} + + if tags: + data['tags'] = ','.join(tags) + + gallery = self.make_request('POST', 'g', data) + + return CustomGallery( + gallery['id'], + gallery['name'], + gallery['datetime'], + gallery['account_url'], + gallery['link'], + gallery['tags'] + ) + + def custom_gallery_update(self, gallery_id, name): + self.logged_in() + data = { + 'id': gallery_id, + 'name': name + } + + gallery = self.make_request('POST', 'g/%s' % gallery_id, data) + + return CustomGallery( + gallery['id'], + gallery['name'], + gallery['datetime'], + gallery['account_url'], + gallery['link'], + gallery['tags'] + ) + + def custom_gallery_add_tags(self, gallery_id, tags): + self.logged_in() + + if tags: + data = {'tags': ','.join(tags)} + else: + raise ImgurClientError('tags must not be empty!') + + return self.make_request('PUT', 'g/%s/add_tags' % gallery_id, data) + + def custom_gallery_remove_tags(self, gallery_id, tags): + self.logged_in() + + if tags: + data = {'tags': ','.join(tags)} + else: + raise ImgurClientError('tags must not be empty!') + + return self.make_request('DELETE', 'g/%s/remove_tags' % gallery_id, data) + + def custom_gallery_delete(self, gallery_id): + self.logged_in() + return self.make_request('DELETE', 'g/%s' % gallery_id) + + def filtered_out_tags(self): + self.logged_in() + return self.make_request('GET', 'g/filtered_out') + + def block_tag(self, tag): + self.logged_in() + return self.make_request('POST', 'g/block_tag', data={'tag': tag}) + + def unblock_tag(self, tag): + self.logged_in() + return self.make_request('POST', 'g/unblock_tag', data={'tag': tag}) + + # Gallery-related endpoints + def gallery(self, section='hot', sort='viral', page=0, window='day', show_viral=True): + if section == 'top': + response = self.make_request('GET', 'gallery/%s/%s/%s/%d?showViral=%s' + % (section, sort, window, page, str(show_viral).lower())) + else: + response = self.make_request('GET', 'gallery/%s/%s/%d?showViral=%s' + % (section, sort, page, str(show_viral).lower())) + + return build_gallery_images_and_albums(response) + + def memes_subgallery(self, sort='viral', page=0, window='week'): + if sort == 'top': + response = self.make_request('GET', 'g/memes/%s/%s/%d' % (sort, window, page)) + else: + response = self.make_request('GET', 'g/memes/%s/%d' % (sort, page)) + + return build_gallery_images_and_albums(response) + + def memes_subgallery_image(self, item_id): + item = self.make_request('GET', 'g/memes/%s' % item_id) + return build_gallery_images_and_albums(item) + + def subreddit_gallery(self, subreddit, sort='time', window='week', page=0): + if sort == 'top': + response = self.make_request('GET', 'gallery/r/%s/%s/%s/%d' % (subreddit, sort, window, page)) + else: + response = self.make_request('GET', 'gallery/r/%s/%s/%d' % (subreddit, sort, page)) + + return build_gallery_images_and_albums(response) + + def subreddit_image(self, subreddit, image_id): + item = self.make_request('GET', 'gallery/r/%s/%s' % (subreddit, image_id)) + return build_gallery_images_and_albums(item) + + def gallery_tag(self, tag, sort='viral', page=0, window='week'): + if sort == 'top': + response = self.make_request('GET', 'gallery/t/%s/%s/%s/%d' % (tag, sort, window, page)) + else: + response = self.make_request('GET', 'gallery/t/%s/%s/%d' % (tag, sort, page)) + + return Tag( + response['name'], + response['followers'], + response['total_items'], + response['following'], + response['items'] + ) + + def gallery_tag_image(self, tag, item_id): + item = self.make_request('GET', 'gallery/t/%s/%s' % (tag, item_id)) + return build_gallery_images_and_albums(item) + + def gallery_item_tags(self, item_id): + response = self.make_request('GET', 'gallery/%s/tags' % item_id) + + return [TagVote( + item['ups'], + item['downs'], + item['name'], + item['author'] + ) for item in response['tags']] + + def gallery_tag_vote(self, item_id, tag, vote): + self.logged_in() + response = self.make_request('POST', 'gallery/%s/vote/tag/%s/%s' % (item_id, tag, vote)) + return response + + def gallery_search(self, q, advanced=None, sort='time', window='all', page=0): + if advanced: + data = {field: advanced[field] + for field in set(self.allowed_advanced_search_fields).intersection(advanced.keys())} + else: + data = {'q': q} + + response = self.make_request('GET', 'gallery/search/%s/%s/%s' % (sort, window, page), data) + return build_gallery_images_and_albums(response) + + def gallery_random(self, page=0): + response = self.make_request('GET', 'gallery/random/random/%d' % page) + return build_gallery_images_and_albums(response) + + def share_on_imgur(self, item_id, title, terms=0): + self.logged_in() + data = { + 'title': title, + 'terms': terms + } + + return self.make_request('POST', 'gallery/%s' % item_id, data) + + def remove_from_gallery(self, item_id): + self.logged_in() + return self.make_request('DELETE', 'gallery/%s' % item_id) + + def gallery_item(self, item_id): + response = self.make_request('GET', 'gallery/%s' % item_id) + return build_gallery_images_and_albums(response) + + def report_gallery_item(self, item_id): + self.logged_in() + return self.make_request('POST', 'gallery/%s/report' % item_id) + + def gallery_item_vote(self, item_id, vote='up'): + self.logged_in() + return self.make_request('POST', 'gallery/%s/vote/%s' % (item_id, vote)) + + def gallery_item_comments(self, item_id, sort='best'): + response = self.make_request('GET', 'gallery/%s/comments/%s' % (item_id, sort)) + return format_comment_tree(response) + + def gallery_comment(self, item_id, comment): + self.logged_in() + return self.make_request('POST', 'gallery/%s/comment' % item_id, {'comment': comment}) + + def gallery_comment_ids(self, item_id): + return self.make_request('GET', 'gallery/%s/comments/ids' % item_id) + + def gallery_comment_count(self, item_id): + return self.make_request('GET', 'gallery/%s/comments/count' % item_id) + + # Image-related endpoints + def get_image(self, image_id): + image = self.make_request('GET', 'image/%s' % image_id) + return Image(image) + + def upload_from_path(self, path, config=None, anon=True): + with open(path, 'rb') as fd: + self.upload(fd, config, anon) + + def upload(self, fd, config=None, anon=True): + if not config: + config = dict() + + contents = fd.read() + b64 = base64.b64encode(contents) + data = { + 'image': b64, + 'type': 'base64', + } + data.update({meta: config[meta] for meta in set(self.allowed_image_fields).intersection(config.keys())}) + + return self.make_request('POST', 'upload', data, anon) + + def upload_from_url(self, url, config=None, anon=True): + if not config: + config = dict() + + data = { + 'image': url, + 'type': 'url', + } + + data.update({meta: config[meta] for meta in set(self.allowed_image_fields).intersection(config.keys())}) + return self.make_request('POST', 'upload', data, anon) + + def delete_image(self, image_id): + return self.make_request('DELETE', 'image/%s' % image_id) + + def favorite_image(self, image_id): + self.logged_in() + return self.make_request('POST', 'image/%s/favorite' % image_id) + + # Conversation-related endpoints + def conversation_list(self): + self.logged_in() + + conversations = self.make_request('GET', 'conversations') + return [Conversation( + conversation['id'], + conversation['last_message_preview'], + conversation['datetime'], + conversation['with_account_id'], + conversation['with_account'], + conversation['message_count'], + ) for conversation in conversations] + + def get_conversation(self, conversation_id, page=1, offset=0): + self.logged_in() + + conversation = self.make_request('GET', 'conversations/%d/%d/%d' % (conversation_id, page, offset)) + return Conversation( + conversation['id'], + conversation['last_message_preview'], + conversation['datetime'], + conversation['with_account_id'], + conversation['with_account'], + conversation['message_count'], + conversation['messages'], + conversation['done'], + conversation['page'] + ) + + def create_message(self, recipient, body): + self.logged_in() + return self.make_request('POST', 'conversations/%s' % recipient, {'body': body}) + + def delete_conversation(self, conversation_id): + self.logged_in() + return self.make_request('DELETE', 'conversations/%d' % conversation_id) + + def report_sender(self, username): + self.logged_in() + return self.make_request('POST', 'conversations/report/%s' % username) + + def block_sender(self, username): + self.logged_in() + return self.make_request('POST', 'conversations/block/%s' % username) + + # Notification-related endpoints + def get_notifications(self, new=True): + self.logged_in() + response = self.make_request('GET', 'notification', {'new': str(new).lower()}) + return build_notifications(response) + + def get_notification(self, notification_id): + self.logged_in() + response = self.make_request('GET', 'notification/%d' % notification_id) + return build_notification(response) + + def mark_notifications_as_read(self, notification_ids): + self.logged_in() + return self.make_request('POST', 'notification', ','.join(notification_ids)) + + # Memegen-related endpoints + def default_memes(self): + response = self.make_request('GET', 'memegen/defaults') + return [Image(meme) for meme in response] diff --git a/imgurpython/helpers/__init__.py b/imgurpython/helpers/__init__.py new file mode 100644 index 0000000..6142833 --- /dev/null +++ b/imgurpython/helpers/__init__.py @@ -0,0 +1,4 @@ +from ..imgur.models.comment import Comment +from ..imgur.models.notification import Notification +from ..imgur.models.gallery_album import GalleryAlbum +from ..imgur.models.gallery_image import GalleryImage \ No newline at end of file diff --git a/imgurpython/helpers/error.py b/imgurpython/helpers/error.py new file mode 100644 index 0000000..f385b28 --- /dev/null +++ b/imgurpython/helpers/error.py @@ -0,0 +1,15 @@ +class ImgurClientError(Exception): + def __init__(self, error_message, status_code=None): + self.status_code = status_code + self.error_message = error_message + + def __str__(self): + if self.status_code: + return "(%s) %s" % (self.status_code, self.error_message) + else: + return self.error_message + + +class ImgurClientRateLimitError(Exception): + def __str__(self): + return 'Rate-limit exceeded!' diff --git a/imgurpython/helpers/format.py b/imgurpython/helpers/format.py new file mode 100644 index 0000000..70cb39f --- /dev/null +++ b/imgurpython/helpers/format.py @@ -0,0 +1,83 @@ +from ..helpers import Comment +from ..helpers import GalleryAlbum +from ..helpers import GalleryImage +from ..helpers import Notification + + +def build_comment_tree(children): + children_objects = [] + for child in children: + to_insert = Comment(child) + to_insert.children = build_comment_tree(to_insert.children) + children_objects.append(to_insert) + + return children_objects + + +def format_comment_tree(response): + if isinstance(response, list): + result = [] + for comment in response: + formatted = Comment(comment) + formatted.children = build_comment_tree(comment['children']) + result.append(formatted) + else: + result = Comment(response) + result.children = build_comment_tree(response['children']) + + return result + + +def build_gallery_images_and_albums(response): + if isinstance(response, list): + result = [] + for item in response: + if item['is_album']: + result.append(GalleryAlbum(item)) + else: + result.append(GalleryImage(item)) + else: + if response['is_album']: + result = GalleryAlbum(response) + else: + result = GalleryImage(response) + + return result + + +def build_notifications(response): + result = { + 'replies': [], + 'messages': [Notification( + item['id'], + item['account_id'], + item['viewed'], + item['content'] + ) for item in response['messages']] + } + + for item in response['replies']: + notification = Notification( + item['id'], + item['account_id'], + item['viewed'], + item['content'] + ) + notification.content = format_comment_tree(item['content']) + result['replies'].append(notification) + + return result + + +def build_notification(item): + notification = Notification( + item['id'], + item['account_id'], + item['viewed'], + item['content'] + ) + + if 'comment' in notification.content: + notification.content = format_comment_tree(item['content']) + + return notification diff --git a/Imgur/Auth/__init__.py b/imgurpython/imgur/__init__.py similarity index 100% rename from Imgur/Auth/__init__.py rename to imgurpython/imgur/__init__.py diff --git a/Imgur/__init__.py b/imgurpython/imgur/models/__init__.py similarity index 100% rename from Imgur/__init__.py rename to imgurpython/imgur/models/__init__.py diff --git a/imgurpython/imgur/models/account.py b/imgurpython/imgur/models/account.py new file mode 100644 index 0000000..01a798a --- /dev/null +++ b/imgurpython/imgur/models/account.py @@ -0,0 +1,9 @@ +class Account(object): + + def __init__(self, account_id, url, bio, reputation, created, pro_expiration): + self.id = account_id + self.url = url + self.bio = bio + self.reputation = reputation + self.created = created + self.pro_expiration = pro_expiration diff --git a/imgurpython/imgur/models/account_settings.py b/imgurpython/imgur/models/account_settings.py new file mode 100644 index 0000000..045aaf2 --- /dev/null +++ b/imgurpython/imgur/models/account_settings.py @@ -0,0 +1,13 @@ +class AccountSettings(object): + + def __init__(self, email, high_quality, public_images, album_privacy, pro_expiration, accepted_gallery_terms, + active_emails, messaging_enabled, blocked_users): + self.email = email + self.high_quality = high_quality + self.public_images = public_images + self.album_privacy = album_privacy + self.pro_expiration = pro_expiration + self.accepted_gallery_terms = accepted_gallery_terms + self.active_emails = active_emails + self.messaging_enabled = messaging_enabled + self.blocked_users = blocked_users diff --git a/imgurpython/imgur/models/album.py b/imgurpython/imgur/models/album.py new file mode 100644 index 0000000..414acc2 --- /dev/null +++ b/imgurpython/imgur/models/album.py @@ -0,0 +1,9 @@ +class Album(object): + + # See documentation at https://api.imgur.com/ for available fields + def __init__(self, *initial_data, **kwargs): + for dictionary in initial_data: + for key in dictionary: + setattr(self, key, dictionary[key]) + for key in kwargs: + setattr(self, key, kwargs[key]) diff --git a/imgurpython/imgur/models/comment.py b/imgurpython/imgur/models/comment.py new file mode 100644 index 0000000..29e4a9f --- /dev/null +++ b/imgurpython/imgur/models/comment.py @@ -0,0 +1,9 @@ +class Comment(object): + + # See documentation at https://api.imgur.com/ for available fields + def __init__(self, *initial_data, **kwargs): + for dictionary in initial_data: + for key in dictionary: + setattr(self, key, dictionary[key]) + for key in kwargs: + setattr(self, key, kwargs[key]) diff --git a/imgurpython/imgur/models/conversation.py b/imgurpython/imgur/models/conversation.py new file mode 100644 index 0000000..335196c --- /dev/null +++ b/imgurpython/imgur/models/conversation.py @@ -0,0 +1,27 @@ +from .message import Message + +class Conversation(object): + + def __init__(self, conversation_id, last_message_preview, datetime, with_account_id, with_account, message_count, messages=None, + done=None, page=None): + self.id = conversation_id + self.last_message_preview = last_message_preview + self.datetime = datetime + self.with_account_id = with_account_id + self.with_account = with_account + self.message_count = message_count + self.page = page + self.done = done + + if messages: + self.messages = [Message( + message['id'], + message['from'], + message['account_id'], + message['sender_id'], + message['body'], + message['conversation_id'], + message['datetime'], + ) for message in messages] + else: + self.messages = None diff --git a/imgurpython/imgur/models/custom_gallery.py b/imgurpython/imgur/models/custom_gallery.py new file mode 100644 index 0000000..912ba6d --- /dev/null +++ b/imgurpython/imgur/models/custom_gallery.py @@ -0,0 +1,16 @@ +from .gallery_album import GalleryAlbum +from .gallery_image import GalleryImage + + +class CustomGallery(object): + + def __init__(self, custom_gallery_id, name, datetime, account_url, link, tags, item_count=None, items=None): + self.id = custom_gallery_id + self.name = name + self.datetime = datetime + self.account_url = account_url + self.link = link + self.tags = tags + self.item_count = item_count + self.items = [GalleryAlbum(item) if item['is_album'] else GalleryImage(item) for item in items] \ + if items else None diff --git a/imgurpython/imgur/models/gallery_album.py b/imgurpython/imgur/models/gallery_album.py new file mode 100644 index 0000000..1622c99 --- /dev/null +++ b/imgurpython/imgur/models/gallery_album.py @@ -0,0 +1,9 @@ +class GalleryAlbum(object): + + # See documentation at https://api.imgur.com/ for available fields + def __init__(self, *initial_data, **kwargs): + for dictionary in initial_data: + for key in dictionary: + setattr(self, key, dictionary[key]) + for key in kwargs: + setattr(self, key, kwargs[key]) diff --git a/imgurpython/imgur/models/gallery_image.py b/imgurpython/imgur/models/gallery_image.py new file mode 100644 index 0000000..88faf19 --- /dev/null +++ b/imgurpython/imgur/models/gallery_image.py @@ -0,0 +1,9 @@ +class GalleryImage(object): + + # See documentation at https://api.imgur.com/ for available fields + def __init__(self, *initial_data, **kwargs): + for dictionary in initial_data: + for key in dictionary: + setattr(self, key, dictionary[key]) + for key in kwargs: + setattr(self, key, kwargs[key]) diff --git a/imgurpython/imgur/models/image.py b/imgurpython/imgur/models/image.py new file mode 100644 index 0000000..18f257e --- /dev/null +++ b/imgurpython/imgur/models/image.py @@ -0,0 +1,9 @@ +class Image(object): + + # See documentation at https://api.imgur.com/ for available fields + def __init__(self, *initial_data, **kwargs): + for dictionary in initial_data: + for key in dictionary: + setattr(self, key, dictionary[key]) + for key in kwargs: + setattr(self, key, kwargs[key]) diff --git a/imgurpython/imgur/models/message.py b/imgurpython/imgur/models/message.py new file mode 100644 index 0000000..0f98f5e --- /dev/null +++ b/imgurpython/imgur/models/message.py @@ -0,0 +1,10 @@ +class Message(object): + + def __init__(self, message_id, from_user, account_id, sender_id, body, conversation_id, datetime): + self.id = message_id + self.from_user = from_user + self.account_id = account_id + self.sender_id = sender_id + self.body = body + self.conversation_id = conversation_id + self.datetime = datetime diff --git a/imgurpython/imgur/models/notification.py b/imgurpython/imgur/models/notification.py new file mode 100644 index 0000000..7966953 --- /dev/null +++ b/imgurpython/imgur/models/notification.py @@ -0,0 +1,7 @@ +class Notification(object): + + def __init__(self, notification_id, account_id, viewed, content): + self.id = notification_id + self.account_id = account_id + self.viewed = viewed + self.content = content diff --git a/imgurpython/imgur/models/tag.py b/imgurpython/imgur/models/tag.py new file mode 100644 index 0000000..d9f8547 --- /dev/null +++ b/imgurpython/imgur/models/tag.py @@ -0,0 +1,13 @@ +from .gallery_album import GalleryAlbum +from .gallery_image import GalleryImage + + +class Tag(object): + + def __init__(self, name, followers, total_items, following, items): + self.name = name + self.followers = followers + self.total_items = total_items + self.following = following + self.items = [GalleryAlbum(item) if item['is_album'] else GalleryImage(item) for item in items] \ + if items else None diff --git a/imgurpython/imgur/models/tag_vote.py b/imgurpython/imgur/models/tag_vote.py new file mode 100644 index 0000000..eb1a995 --- /dev/null +++ b/imgurpython/imgur/models/tag_vote.py @@ -0,0 +1,7 @@ +class TagVote(object): + + def __init__(self, ups, downs, name, author): + self.ups = ups + self.downs = downs + self.name = name + self.author = author diff --git a/main.py b/main.py deleted file mode 100644 index dee1d3c..0000000 --- a/main.py +++ /dev/null @@ -1,261 +0,0 @@ -#!/usr/bin/env python3 -import json, sys, time as dt, pprint, math - -from Imgur.Factory import Factory -from Imgur.Auth.Expired import Expired - -try: - from urllib.request import urlopen as UrlLibOpen - from urllib.request import HTTPError -except ImportError: - from urllib2 import urlopen as UrlLibOpen - from urllib2 import HTTPError - -def center_pad(s, length): - num_dashes = float(length - len(s) - 2) / 2 - num_dashes_left = int(math.floor(num_dashes)) - num_dashes_right = int(math.ceil(num_dashes)) - - return ('=' * num_dashes_left) + ' ' + s + ' ' + ('=' * num_dashes_right) - -def two_column_with_period(left, right, length): - num_periods = int(length - (len(left) + len(right) + 2)) - return left + ' ' + ('.' * num_periods) + ' ' + right - -def usage(argv): - if len(argv) <= 1 or argv[1] == '--help' or argv[1] == '-h': - - print("\nUsage: \tpython main.py (action) [options...]") - - lines = [] - lines.append(('oauth', 'credits', 'View the rate limit information for this client')) - lines.append(('oauth', 'authorize', 'Start the authorization process')) - lines.append(('oauth', 'authorize [pin]', 'Get an access token after starting authorization')) - lines.append(('oauth', 'refresh [refresh-token]', 'Return a new OAuth access token after it\'s expired')) - - lines.append(('anon', 'upload [file]', 'Anonymously upload a file')) - lines.append(('anon', 'list-comments [hash]', 'Get the comments (raw JSON) for a gallery post')) - lines.append(('anon', 'get-album [id]', 'Get information (raw JSON) about an album')) - lines.append(('anon', 'get-comment [id]', 'Get a particular comment (raw JSON) for a gallery comment')) - lines.append(('anon', 'get-gallery [hash]', 'Get information (raw JSON) about a gallery post')) - - lines.append(('auth', 'upload-auth [access-token]', 'Upload a file to your account')) - lines.append(('auth', 'comment [access-token] [hash] [text ...]', 'Comment on a gallery post')) - lines.append(('auth', 'vote-gallery [token] [hash] [direction]', 'Vote on a gallery post. Direction either \'up\', \'down\', or \'veto\'')) - lines.append(('auth', 'vote-comment [token] [id] [direction]', 'Vote on a gallery comment. Direction either \'up\', \'down\', or \'veto\'')) - - headers = { - 'oauth': 'OAuth Actions', - 'anon': 'Unauthorized Actions', - 'auth': 'Authorized Actions' - } - - categoryHeadersOutputSoFar = [] - - col_width = 0 - for category in headers.values(): - col_width = max(col_width, len(category)) - for line in lines: - col_width = max(col_width, len(line[2]) + len(line[1])) - - col_width = math.ceil(col_width * 1.1) - - for line in lines: - (cat, text, desc) = line - - if False == (cat in categoryHeadersOutputSoFar): - print("\n" + center_pad(headers[cat], col_width) + "\n") - categoryHeadersOutputSoFar.append(cat) - print(two_column_with_period(text, desc, col_width)) - - print("") - ''' - - print("\n" + sep + "\nOAuth Actions\n" + sep) - print("credits\n-- View the rate limit information for this client") - print("authorize\n-- Get the authorization URL") - print("authorize [pin]\n-- Get an access token") - print("refresh [refresh-token]\n-- Return a new OAuth access token after it's expired") - - print("\n" + sep + "\nUnauthorized Actions\n" + sep) - print("upload [file]\n-- Anonymously upload a file") - print("list-comments [hash]\n-- Get the comments (raw json) for a gallery item") - print("get-album [id]\n-- View information about an album") - print("get-comment [id]\n-- Get a particular comment (raw json) for a gallery item") - print("get-gallery [hash]\n-- View information about a gallery post") - - print("\n" + sep + "\nAuthorized Actions\n" + sep) - print("upload-auth [access-token] [file]\n-- Upload a file to your account") - print("comment [access-token] [hash] [text]\n-- Comment on a gallery image") - print("vote-gallery [token] [hash] [direction]\n-- Vote on a gallery post. Direction either 'up', 'down', or 'veto'") - print("vote-comment [token] [id] [direction]\n-- Vote on a gallery comment. Direction either 'up', 'down', or 'veto'") - print("\n") - ''' - - sys.exit(1) - -def main(): - usage(sys.argv) - - config = None - try: - fd = open('config.json', 'r') - except: - print("config file [config.json] not found.") - sys.exit(1) - - try: - config = json.loads(fd.read()) - except: - print("invalid json in config file.") - sys.exit(1) - - factory = Factory(config) - - - action = sys.argv[1] - - authorized_commands = [ - 'upload-auth', - 'comment', - 'vote-gallery', - 'vote-comment' - ] - - oauth_commands = [ - 'credits', - 'refresh', - 'authorize' - ] - - if action in authorized_commands: - handle_authorized_commands(factory, action) - else: - if action in oauth_commands: - handle_oauth_commands(factory, config, action) - else: - handle_unauthorized_commands(factory, action) - -def handle_authorized_commands(factory, action): - token = sys.argv[2] - auth = factory.build_oauth(token, None) - imgur = factory.build_api(auth) - - if action == 'upload-auth': - path = sys.argv[3] - req = factory.build_request_upload_from_path(path) - - if action == 'comment': - thash = sys.argv[3] - text = ' '.join(sys.argv[4:]) - - if len(text) > 140: - print("Comment too long (trim by %d characters)." % (len(text) - 140)) - sys.exit(1) - - req = factory.build_request(('gallery', thash, 'comment'), { - 'comment': text - }) - - if action == 'vote-gallery' or action == 'vote-comment': - (tid, vote) = sys.argv[3:] - - target = None - if action == 'vote-gallery': - target = ('gallery', tid, 'vote', vote) - else: - target = ('comment', tid, 'vote', vote) - - req = factory.build_request(target) - - try: - res = imgur.retrieve(req) - if action == 'upload-auth': - print(res['link']) - else: - if action == 'comment': - print("Success! https://www.imgur.com/gallery/%s/comment/%s" % (thash, res['id'])) - else: - print(res) - except Expired: - print("Expired access token") - - - -def handle_oauth_commands(factory, config, action): - imgur = factory.build_api() - - if action == 'credits': - req = factory.build_request(('credits',)) - res = imgur.retrieve(req) - print(res) - - if action == 'refresh': - token = sys.argv[2] - req = factory.build_request_oauth_refresh(token) - - try: - res = imgur.retrieve_raw(req) - except HTTPError as e: - print("Error %d\n%s" % (e.code, e.read().decode('utf8'))) - raise e - - print('Access Token: %s\nRefresh Token: %s\nExpires: %d seconds from now.' % ( - res[1]['access_token'], - res[1]['refresh_token'], - res[1]['expires_in'] - )) - - if action == 'authorize': - if len(sys.argv) == 2: - print("Visit this URL to get a PIN to authorize: " + factory.get_api_url() + "oauth2/authorize?client_id=" + config['client_id'] + "&response_type=pin") - if len(sys.argv) == 3: - pin = sys.argv[2] - imgur = factory.build_api() - req = factory.build_request_oauth_token_swap('pin', pin) - try: - res = imgur.retrieve_raw(req) - except HTTPError as e: - print("Error %d\n%s" % (e.code, e.read().decode('utf8'))) - raise e - - print("Access Token: %s\nRefresh Token: %s\nExpires: %d seconds from now." % ( - res[1]['access_token'], - res[1]['refresh_token'], - res[1]['expires_in'] - )) - - -def handle_unauthorized_commands(factory, action): - imgur = factory.build_api() - req = None - - if action == 'upload': - req = factory.build_request_upload_from_path(sys.argv[2]) - res = imgur.retrieve(req) - - print(res['link']) - - else: - if action == 'list-comments': - thash = sys.argv[2] - req = factory.build_request(('gallery', thash, 'comments')) - - if action == 'get-album': - id = sys.argv[2] - req = factory.build_request(('album', id)) - - if action == 'get-comment': - (thash, cid) = sys.argv[2:4] - req = factory.build_request(('gallery', thash, 'comments', cid)) - - if action == 'get-gallery': - imgur = factory.build_api() - req = factory.build_request(('gallery', id)) - - res = imgur.retrieve(req) - print(res) - - -if __name__ == "__main__": - main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f229360 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +requests diff --git a/res/corgi.jpg b/res/corgi.jpg deleted file mode 100644 index b240c79..0000000 Binary files a/res/corgi.jpg and /dev/null differ diff --git a/res/error.jpg b/res/error.jpg deleted file mode 100644 index 9daeafb..0000000 --- a/res/error.jpg +++ /dev/null @@ -1 +0,0 @@ -test diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..224a779 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +description-file = README.md \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..300e786 --- /dev/null +++ b/setup.py @@ -0,0 +1,80 @@ +from setuptools import setup, find_packages # Always prefer setuptools over distutils +from codecs import open # To use a consistent encoding +from os import path + +here = path.abspath(path.dirname(__file__)) + +# Get the long description from the relevant file +setup( + name='imgurpython', + + # Versions should comply with PEP440. For a discussion on single-sourcing + # the version across setup.py and the project code, see + # http://packaging.python.org/en/latest/tutorial.html#version + version='1.1.7', + + description='Official Imgur python library with OAuth2 and samples', + long_description='', + + # The project's main homepage. + url='https://github.com/Imgur/imgurpython', + + # Author details + author='Imgur Inc.', + author_email='api@imgur.com', + + # Choose your license + license='MIT', + + # See https://pypi.python.org/pypi?%3Aaction=list_classifiers + classifiers=[ + # How mature is this project? Common values are + # 3 - Alpha + # 4 - Beta + # 5 - Production/Stable + 'Development Status :: 4 - Beta', + + # Indicate who your project is intended for + 'Intended Audience :: Developers', + + # Pick your license as you wish (should match "license" above) + 'License :: OSI Approved :: MIT License', + + # Specify the Python versions you support here. In particular, ensure + # that you indicate whether you support Python 2, Python 3 or both. + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + ], + + # What does your project relate to? + keywords=['api', 'imgur', 'client'], + + # You can just specify the packages manually here if your project is + # simple. Or you can use find_packages(). + packages=find_packages(), + + # List run-time dependencies here. These will be installed by pip when your + # project is installed. For an analysis of "install_requires" vs pip's + # requirements files see: + # https://packaging.python.org/en/latest/technical.html#install-requires-vs-requirements-files + install_requires=['requests'], + + # If there are data files included in your packages that need to be + # installed, specify them here. If using Python 2.6 or less, then these + # have to be included in MANIFEST.in as well. + package_data={}, + + # Although 'package_data' is the preferred approach, in some case you may + # need to place data files outside of your packages. + # see http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files + # In this case, 'data_file' will be installed into '/my_data' + data_files=[], + + # To provide executable scripts, use entry points in preference to the + # "scripts" keyword. Entry points provide cross-platform support and allow + # pip to create the appropriate form of executable for the target platform. + entry_points={}, +)