From 2763e45fedf98d6eebd1f484d561c10978d0043c Mon Sep 17 00:00:00 2001 From: PoorLonesomeCoder Date: Fri, 23 May 2014 23:46:59 +0200 Subject: [PATCH 01/89] Update main.py Fix a little bug for python 2.7. 'string' * float(x) seem to work with python 3.x, but it doesn't with 2.7 (not at my system at least). So i added some int() s. --- main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/main.py b/main.py index 5c0f09e..dee1d3c 100644 --- a/main.py +++ b/main.py @@ -13,13 +13,13 @@ def center_pad(s, length): num_dashes = float(length - len(s) - 2) / 2 - num_dashes_left = math.floor(num_dashes) - num_dashes_right = math.ceil(num_dashes) + 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 = (length - (len(left) + len(right) + 2)) + num_periods = int(length - (len(left) + len(right) + 2)) return left + ' ' + ('.' * num_periods) + ' ' + right def usage(argv): From 8f0782289b7d7ba452bc7f15d8f49bfddf7d091c Mon Sep 17 00:00:00 2001 From: PoorLonesomeCoder Date: Thu, 29 May 2014 03:42:31 +0200 Subject: [PATCH 02/89] Fixed an oversight I guess you just forgot to add that second parameter ? *The next pull request will be larger btw. Working on an auto token refresh and config persistence* --- Imgur/RateLimit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Imgur/RateLimit.py b/Imgur/RateLimit.py index ba9ef3f..1e62dde 100644 --- a/Imgur/RateLimit.py +++ b/Imgur/RateLimit.py @@ -20,7 +20,7 @@ def update(self, headers): def is_over(self, time): return self.would_be_over(0, time) - def would_be_over(self, cost): + 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): From 0e33e7e3ef6f99e9739ae338a193f99f3aba1507 Mon Sep 17 00:00:00 2001 From: jasdev Date: Wed, 4 Jun 2014 16:24:25 -0700 Subject: [PATCH 03/89] Fixing get-gallery method --- main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/main.py b/main.py index dee1d3c..e4be5c3 100644 --- a/main.py +++ b/main.py @@ -251,6 +251,7 @@ def handle_unauthorized_commands(factory, action): if action == 'get-gallery': imgur = factory.build_api() + id = sys.argv[2] req = factory.build_request(('gallery', id)) res = imgur.retrieve(req) From 501adead56a79866086a921d069968f2ddac8771 Mon Sep 17 00:00:00 2001 From: Jacob Greenleaf Date: Mon, 16 Jun 2014 17:05:54 -0700 Subject: [PATCH 04/89] Lower case and move stuff to subdir for pypi --- .gitignore | 2 + {Imgur/Auth => imgur-python}/__init__.py | 0 .../data/config.json.sample | 0 {res => imgur-python/data/res}/corgi.jpg | Bin {res => imgur-python/data/res}/error.jpg | 0 LICENSE => imgur-python/docs/LICENSE | 0 {Imgur => imgur-python/imgur}/__init__.py | 0 imgur-python/imgur/auth/__init__.py | 0 .../imgur/auth/accesstoken.py | 5 +- .../imgur/auth/anonymous.py | 2 +- .../imgur/auth/base.py | 2 +- .../imgur/auth/expired.py | 2 +- .../imgur/factory.py | 24 ++--- Imgur/Imgur.py => imgur-python/imgur/imgur.py | 6 +- .../imgur/ratelimit.py | 2 +- main.py => imgur-python/main.py | 16 ++-- setup.py | 82 ++++++++++++++++++ 17 files changed, 114 insertions(+), 29 deletions(-) rename {Imgur/Auth => imgur-python}/__init__.py (100%) rename config.json.sample => imgur-python/data/config.json.sample (100%) rename {res => imgur-python/data/res}/corgi.jpg (100%) rename {res => imgur-python/data/res}/error.jpg (100%) rename LICENSE => imgur-python/docs/LICENSE (100%) rename {Imgur => imgur-python/imgur}/__init__.py (100%) create mode 100644 imgur-python/imgur/auth/__init__.py rename Imgur/Auth/AccessToken.py => imgur-python/imgur/auth/accesstoken.py (89%) rename Imgur/Auth/Anonymous.py => imgur-python/imgur/auth/anonymous.py (95%) rename Imgur/Auth/Base.py => imgur-python/imgur/auth/base.py (96%) rename Imgur/Auth/Expired.py => imgur-python/imgur/auth/expired.py (76%) rename Imgur/Factory.py => imgur-python/imgur/factory.py (85%) rename Imgur/Imgur.py => imgur-python/imgur/imgur.py (94%) rename Imgur/RateLimit.py => imgur-python/imgur/ratelimit.py (98%) rename main.py => imgur-python/main.py (96%) create mode 100644 setup.py diff --git a/.gitignore b/.gitignore index b32b49f..31acaa0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ *.swo *.pyc config.json +dist +imgur_python.egg-_info diff --git a/Imgur/Auth/__init__.py b/imgur-python/__init__.py similarity index 100% rename from Imgur/Auth/__init__.py rename to imgur-python/__init__.py diff --git a/config.json.sample b/imgur-python/data/config.json.sample similarity index 100% rename from config.json.sample rename to imgur-python/data/config.json.sample diff --git a/res/corgi.jpg b/imgur-python/data/res/corgi.jpg similarity index 100% rename from res/corgi.jpg rename to imgur-python/data/res/corgi.jpg diff --git a/res/error.jpg b/imgur-python/data/res/error.jpg similarity index 100% rename from res/error.jpg rename to imgur-python/data/res/error.jpg diff --git a/LICENSE b/imgur-python/docs/LICENSE similarity index 100% rename from LICENSE rename to imgur-python/docs/LICENSE diff --git a/Imgur/__init__.py b/imgur-python/imgur/__init__.py similarity index 100% rename from Imgur/__init__.py rename to imgur-python/imgur/__init__.py diff --git a/imgur-python/imgur/auth/__init__.py b/imgur-python/imgur/auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Imgur/Auth/AccessToken.py b/imgur-python/imgur/auth/accesstoken.py similarity index 89% rename from Imgur/Auth/AccessToken.py rename to imgur-python/imgur/auth/accesstoken.py index ec5973a..60ec35c 100644 --- a/Imgur/Auth/AccessToken.py +++ b/imgur-python/imgur/auth/accesstoken.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 -from .Base import Base as AuthBase +from .base import base as authbase import time as dt -class AccessToken(AuthBase): +class accesstoken(authbase): + def __init__(self, access, refresh, expire_time): self.access = access self.refresh = refresh diff --git a/Imgur/Auth/Anonymous.py b/imgur-python/imgur/auth/anonymous.py similarity index 95% rename from Imgur/Auth/Anonymous.py rename to imgur-python/imgur/auth/anonymous.py index 8f14bd3..80cafd0 100644 --- a/Imgur/Auth/Anonymous.py +++ b/imgur-python/imgur/auth/anonymous.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -class Anonymous: +class anonymous: def __init__(self, client_id): self.client_id = client_id diff --git a/Imgur/Auth/Base.py b/imgur-python/imgur/auth/base.py similarity index 96% rename from Imgur/Auth/Base.py rename to imgur-python/imgur/auth/base.py index 1547561..504bf97 100644 --- a/Imgur/Auth/Base.py +++ b/imgur-python/imgur/auth/base.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -class Base: +class base: def need_to_authorize(self, time): '''Do we need to refresh our authorization token?''' pass diff --git a/Imgur/Auth/Expired.py b/imgur-python/imgur/auth/expired.py similarity index 76% rename from Imgur/Auth/Expired.py rename to imgur-python/imgur/auth/expired.py index 9407393..49eb32a 100644 --- a/Imgur/Auth/Expired.py +++ b/imgur-python/imgur/auth/expired.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -class Expired(BaseException): +class expired(BaseException): def __str__(self): return "Access token invalid or expired." diff --git a/Imgur/Factory.py b/imgur-python/imgur/factory.py similarity index 85% rename from Imgur/Factory.py rename to imgur-python/imgur/factory.py index 58c5bd2..3e69029 100644 --- a/Imgur/Factory.py +++ b/imgur-python/imgur/factory.py @@ -9,12 +9,12 @@ 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 +from .imgur import imgur +from .ratelimit import ratelimit +from .auth.accesstoken import accesstoken +from .auth.anonymous import anonymous -class Factory: +class factory: API_URL = "https://api.imgur.com/" @@ -31,17 +31,17 @@ def build_api(self, auth = None, ratelimit = 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) + return imgur(self.config['client_id'], self.config['secret'], auth, ratelimit) def build_anonymous_auth(self): - return Anonymous(self.config['client_id']) + 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) + return accesstoken(access, refresh, now) else: - return AccessToken(access, refresh, expire_time) + 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'). @@ -60,15 +60,15 @@ def build_request(self, endpoint, data = None): 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']) + return ratelimit(limits['client_limit'], limits['user_limit'], limits['user_reset']) else: - return RateLimit() + 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']) + return ratelimit(res['ClientRemaining'], res['UserRemaining'], res['UserReset']) def build_request_upload_from_path(self, path, params = dict()): diff --git a/Imgur/Imgur.py b/imgur-python/imgur/imgur.py similarity index 94% rename from Imgur/Imgur.py rename to imgur-python/imgur/imgur.py index 774f1e1..1e584b9 100644 --- a/Imgur/Imgur.py +++ b/imgur-python/imgur/imgur.py @@ -9,9 +9,9 @@ from urllib2 import urlopen as UrlLibOpen from urllib2 import HTTPError -from .Auth.Expired import Expired +from .auth.expired import expired -class Imgur: +class imgur: def __init__(self, client_id, secret, auth, ratelimit): self.client_id = client_id @@ -30,7 +30,7 @@ def retrieve(self, request): (req, res) = self.retrieve_raw(request) except HTTPError as e: if e.code == 403: - raise Expired() + raise expired() else: print("Error %d\n%s\n" % (e.code, e.read())) raise e diff --git a/Imgur/RateLimit.py b/imgur-python/imgur/ratelimit.py similarity index 98% rename from Imgur/RateLimit.py rename to imgur-python/imgur/ratelimit.py index 1e62dde..f48009b 100644 --- a/Imgur/RateLimit.py +++ b/imgur-python/imgur/ratelimit.py @@ -2,7 +2,7 @@ import time as dt -class RateLimit: +class ratelimit: def __init__(self, client_remaining = 12500, user_remaining = 500, user_reset = None): self.client_remaining = client_remaining diff --git a/main.py b/imgur-python/main.py similarity index 96% rename from main.py rename to imgur-python/main.py index e4be5c3..810c22f 100644 --- a/main.py +++ b/imgur-python/main.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 import json, sys, time as dt, pprint, math -from Imgur.Factory import Factory -from Imgur.Auth.Expired import Expired +from imgur.factory import factory +from imgur.auth.expired import expired try: from urllib.request import urlopen as UrlLibOpen @@ -99,7 +99,7 @@ def main(): config = None try: - fd = open('config.json', 'r') + fd = open('data/config.json', 'r') except: print("config file [config.json] not found.") sys.exit(1) @@ -110,7 +110,7 @@ def main(): print("invalid json in config file.") sys.exit(1) - factory = Factory(config) + mfactory = factory(config) action = sys.argv[1] @@ -129,12 +129,12 @@ def main(): ] if action in authorized_commands: - handle_authorized_commands(factory, action) + handle_authorized_commands(mfactory, action) else: if action in oauth_commands: - handle_oauth_commands(factory, config, action) + handle_oauth_commands(mfactory, config, action) else: - handle_unauthorized_commands(factory, action) + handle_unauthorized_commands(mfactory, action) def handle_authorized_commands(factory, action): token = sys.argv[2] @@ -177,7 +177,7 @@ def handle_authorized_commands(factory, action): print("Success! https://www.imgur.com/gallery/%s/comment/%s" % (thash, res['id'])) else: print(res) - except Expired: + except expired: print("Expired access token") diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..2ee8d14 --- /dev/null +++ b/setup.py @@ -0,0 +1,82 @@ +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='imgur-python', + + # 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.0.0', + + description='Official reference Imgur python library with OAuth2', + long_description='', + + # The project's main homepage. + url='https://github.com/jacobgreenleaf/imgur-python', + + # Author details + author='Jacob Greenleaf', + author_email='jacob@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', + 'Programming Language :: Python :: 2.6', + '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='sample setuptools development', + + # 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=[], + + # 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={'imgurpython': ['data/res/*', 'data/config.json.sample', 'docs/LICENSE']}, + + # 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={}, +) From 7287c0202df3c8ff2d3b89c7edc54d7ac7fa67e2 Mon Sep 17 00:00:00 2001 From: Jacob Greenleaf Date: Mon, 16 Jun 2014 17:16:04 -0700 Subject: [PATCH 05/89] Update README to reflect PIP --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index a9d5f28..219f070 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,11 @@ You must [register](http://api.imgur.com/oauth2/addclient) your client with the 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. +Installation +------------ + + pip install imgur-python + Usage ----- From 62122e580575265727b8d066d9426b420d4bbae7 Mon Sep 17 00:00:00 2001 From: Jacob Greenleaf Date: Mon, 16 Jun 2014 17:34:35 -0700 Subject: [PATCH 06/89] Rename to imgurpython --- .gitignore | 3 ++- README.md | 2 +- setup.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 31acaa0..298a1fb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ *.pyc config.json dist -imgur_python.egg-_info +build +imgurpython.egg-info diff --git a/README.md b/README.md index 219f070..8598170 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ The **secret** field is required for OAuth. Installation ------------ - pip install imgur-python + pip install imgurpython Usage ----- diff --git a/setup.py b/setup.py index 2ee8d14..a37cd5d 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ # Get the long description from the relevant file setup( - name='imgur-python', + name='imgurpython', # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see From 1e0bc64ad23a938d7981f777a3054b4478c8ffe5 Mon Sep 17 00:00:00 2001 From: Jacob Greenleaf Date: Mon, 16 Jun 2014 17:35:02 -0700 Subject: [PATCH 07/89] Push update to 1.0.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a37cd5d..f5e056d 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ # 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.0.0', + version='1.0.1', description='Official reference Imgur python library with OAuth2', long_description='', From d3ee41e9879f45a5de04b26ef18d952325cb3d58 Mon Sep 17 00:00:00 2001 From: jasdev Date: Tue, 26 Aug 2014 18:19:12 -0700 Subject: [PATCH 08/89] PEP 8, refactoring, and squashing bugs --- README.md | 99 ++++++++------ imgur-python/data/config.json.sample | 2 +- imgur-python/docs/LICENSE | 2 +- imgur-python/helpers/__init__.py | 1 + imgur-python/helpers/format.py | 14 ++ imgur-python/imgur/auth/accesstoken.py | 9 +- imgur-python/imgur/auth/anonymous.py | 13 +- imgur-python/imgur/auth/base.py | 14 +- imgur-python/imgur/auth/expired.py | 6 +- imgur-python/imgur/factory.py | 79 ++++++----- imgur-python/imgur/imgur.py | 42 +++--- imgur-python/imgur/ratelimit.py | 16 ++- imgur-python/main.py | 177 +++++++++++-------------- setup.py | 10 +- 14 files changed, 250 insertions(+), 234 deletions(-) create mode 100644 imgur-python/helpers/__init__.py create mode 100644 imgur-python/helpers/format.py diff --git a/README.md b/README.md index 8598170..c63ea5e 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,63 @@ -Imgur API Example Usage -======================= +imgurpython +=========== -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. +A Python client for the [Imgur API](http://api.imgur.com/). Also includes a friendly demo application. It can be used to +interact with the Imgur API and examine its responses, as a command line utility, and it can be used as a library +within your projects. -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. +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. + +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 would be our +[Google Group](https://groups.google.com/forum/#!forum/imgur), [Twitter](https://twitter.com/imgurapi), or via + api@imgur.com. Installation ------------ pip install imgurpython -Usage ------ +Configuration +------------- + +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, needed fo OAuth2 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. + +Command Line Usage +------------------ > Usage: python main.py (action) [options...] > @@ -51,44 +94,14 @@ Usage > > ### Authorized Actions > -> **upload-auth [access-token]** +> **upload-auth [access-token] [file]** > Upload a file to your account > -> **comment [access-token] [hash] [text ...]** +> **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 on a gallery post. Direction can be 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. +> Vote on a gallery comment. Direction can be either 'up', 'down', or 'veto' \ No newline at end of file diff --git a/imgur-python/data/config.json.sample b/imgur-python/data/config.json.sample index aaaf445..3519804 100644 --- a/imgur-python/data/config.json.sample +++ b/imgur-python/data/config.json.sample @@ -2,4 +2,4 @@ "client_id": "", "secret": "", "token_store": {} -} +} \ No newline at end of file diff --git a/imgur-python/docs/LICENSE b/imgur-python/docs/LICENSE index 57b39f8..b10a2fe 100644 --- a/imgur-python/docs/LICENSE +++ b/imgur-python/docs/LICENSE @@ -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/imgur-python/helpers/__init__.py b/imgur-python/helpers/__init__.py new file mode 100644 index 0000000..f56218a --- /dev/null +++ b/imgur-python/helpers/__init__.py @@ -0,0 +1 @@ +__author__ = 'jasdev' diff --git a/imgur-python/helpers/format.py b/imgur-python/helpers/format.py new file mode 100644 index 0000000..fa9f574 --- /dev/null +++ b/imgur-python/helpers/format.py @@ -0,0 +1,14 @@ +import math + + +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 \ No newline at end of file diff --git a/imgur-python/imgur/auth/accesstoken.py b/imgur-python/imgur/auth/accesstoken.py index 60ec35c..587f946 100644 --- a/imgur-python/imgur/auth/accesstoken.py +++ b/imgur-python/imgur/auth/accesstoken.py @@ -1,20 +1,19 @@ #!/usr/bin/env python3 -from .base import base as authbase -import time as dt +from .base import Base as AuthBase -class accesstoken(authbase): +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) + return self.expire_time <= time def add_authorization_header(self, request): - request.add_header('Authorization', 'Bearer ' + self.access) + request.add_header('Authorization', 'Bearer %s' % self.access) return request def get_access_token(self): diff --git a/imgur-python/imgur/auth/anonymous.py b/imgur-python/imgur/auth/anonymous.py index 80cafd0..47e4954 100644 --- a/imgur-python/imgur/auth/anonymous.py +++ b/imgur-python/imgur/auth/anonymous.py @@ -1,15 +1,18 @@ #!/usr/bin/env python3 -class anonymous: +from .base import Base as AuthBase + + +class Anonymous(AuthBase): def __init__(self, client_id): self.client_id = client_id - def need_to_authorize(self): + def need_to_authorize(self, time): return False - def authorize(self): + def authorize(self, api, request_factory): pass def add_authorization_header(self, request): - request.add_header('Authorization', 'Client-ID ' + self.client_id) - return request + request.add_header('Authorization', 'Client-ID %s' % self.client_id) + return request \ No newline at end of file diff --git a/imgur-python/imgur/auth/base.py b/imgur-python/imgur/auth/base.py index 504bf97..ee9807e 100644 --- a/imgur-python/imgur/auth/base.py +++ b/imgur-python/imgur/auth/base.py @@ -1,12 +1,14 @@ #!/usr/bin/env python3 -class base: + +class Base: def need_to_authorize(self, time): - '''Do we need to refresh our authorization token?''' + """Do we need to refresh our authorization token?""" pass - def authorize(self, api, requestfactory): - '''Refresh our access token''' + + def authorize(self, api, request_factory): + """Refresh our access token""" pass + def add_authorization_header(self, request): - pass - + pass \ No newline at end of file diff --git a/imgur-python/imgur/auth/expired.py b/imgur-python/imgur/auth/expired.py index 49eb32a..6e7185b 100644 --- a/imgur-python/imgur/auth/expired.py +++ b/imgur-python/imgur/auth/expired.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -class expired(BaseException): - def __str__(self): - return "Access token invalid or expired." +class Expired(BaseException): + def __str__(self): + return 'Access token invalid or expired.' \ No newline at end of file diff --git a/imgur-python/imgur/factory.py b/imgur-python/imgur/factory.py index 3e69029..de3784c 100644 --- a/imgur-python/imgur/factory.py +++ b/imgur-python/imgur/factory.py @@ -1,21 +1,22 @@ #!/usr/bin/env python3 -import base64, os.path, time as dt +import base64 +import os.path +import time as dt +from .imgur import Imgur +from .ratelimit import RateLimit +from .auth.anonymous import Anonymous +from .auth.accesstoken import AccessToken try: - from urllib.request import Request as UrlLibRequest - from urllib.parse import urlencode as UrlLibEncode + 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 + from urllib2 import Request as urllibrequest + from urllib import urlencode as urllibencode -class factory: +class Factory: API_URL = "https://api.imgur.com/" def __init__(self, config): @@ -26,61 +27,68 @@ def __init__(self, config): def get_api_url(self): return self.API_URL - def build_api(self, auth = None, ratelimit = None): + def build_api(self, auth=None, rate_limit=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) + + if rate_limit is None: + rate_limit = self.build_rate_limit() + + return Imgur(self.config['client_id'], self.config['secret'], auth, rate_limit) def build_anonymous_auth(self): - return anonymous(self.config['client_id']) + return Anonymous(self.config['client_id']) - def build_oauth(self, access, refresh, expire_time = None): + @staticmethod + def build_oauth(access, refresh, expire_time=None): now = int(dt.time()) if expire_time is None: - return accesstoken(access, refresh, now) + return AccessToken(access, refresh, now) else: - return accesstoken(access, refresh, expire_time) + 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.''' + 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) + req = urllibrequest(url) if data is not None: - req.add_data(UrlLibEncode(data).encode('utf-8')) + 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"''' + + @staticmethod + def build_rate_limit(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']) + return RateLimit(limits['client_limit'], limits['user_limit'], limits['user_reset']) else: - return ratelimit() + 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.''' + """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()): + 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): @@ -92,7 +100,7 @@ def build_request_oauth_token_swap(self, grant_type, token): if grant_type == 'authorization_code': data['code'] = token - if grant_type == 'pin': + elif grant_type == 'pin': data['pin'] = token return self.build_request('oauth2/token', data) @@ -104,4 +112,5 @@ def build_request_oauth_refresh(self, refresh_token): 'client_secret': self.config['secret'], 'grant_type': 'refresh_token' } + return self.build_request('oauth2/token', data) diff --git a/imgur-python/imgur/imgur.py b/imgur-python/imgur/imgur.py index 1e584b9..5c95440 100644 --- a/imgur-python/imgur/imgur.py +++ b/imgur-python/imgur/imgur.py @@ -1,51 +1,51 @@ #!/usr/bin/env python3 import json +from .auth.expired import Expired try: - from urllib.request import urlopen as UrlLibOpen + from urllib.request import urlopen as urllibopen from urllib.request import HTTPError except ImportError: - from urllib2 import urlopen as UrlLibOpen + 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): +class Imgur: + def __init__(self, client_id, secret, auth, rate_limit): self.client_id = client_id self.secret = secret self.auth = auth - self.ratelimit = ratelimit + self.rate_limit = rate_limit + + def get_rate_limit(self): + return self.rate_limit + + def get_auth(self): + return self.auth + + def get_client_id(self): + return self.client_id def retrieve_raw(self, request): request = self.auth.add_authorization_header(request) - req = UrlLibOpen(request) + req = urllibopen(request) res = json.loads(req.read().decode('utf-8')) - return (req, res) + + return req, res def retrieve(self, request): try: (req, res) = self.retrieve_raw(request) except HTTPError as e: if e.code == 403: - raise expired() + raise Expired() else: print("Error %d\n%s\n" % (e.code, e.read())) raise e - self.ratelimit.update(req.info()) + self.rate_limit.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 + return res['data'] \ No newline at end of file diff --git a/imgur-python/imgur/ratelimit.py b/imgur-python/imgur/ratelimit.py index f48009b..b9d8487 100644 --- a/imgur-python/imgur/ratelimit.py +++ b/imgur-python/imgur/ratelimit.py @@ -2,15 +2,16 @@ import time as dt -class ratelimit: - def __init__(self, client_remaining = 12500, user_remaining = 500, user_reset = None): +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''' + """Update the rate limit state with a fresh API response""" if 'X-RateLimit-ClientRemaining' in headers: self.client_remaining = int(headers['X-RateLimit-ClientRemaining']) @@ -21,12 +22,13 @@ 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) + 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). + def __str__(self, time=None): if time is None: time = dt.time() exp = int(self.user_reset) - int(time) - return "" % (self.client_remaining, self.user_remaining, exp) + return '' % \ + (self.client_remaining, self.user_remaining, exp) \ No newline at end of file diff --git a/imgur-python/main.py b/imgur-python/main.py index 810c22f..91b211b 100644 --- a/imgur-python/main.py +++ b/imgur-python/main.py @@ -1,31 +1,44 @@ #!/usr/bin/env python3 -import json, sys, time as dt, pprint, math -from imgur.factory import factory -from imgur.auth.expired import expired +import sys +import json +import math +from imgur.factory import Factory +from imgur.auth.expired import Expired +from helpers import format try: - from urllib.request import urlopen as UrlLibOpen + from urllib.request import urlopen as urlllibopen from urllib.request import HTTPError except ImportError: - from urllib2 import urlopen as UrlLibOpen + from urllib2 import urlopen as urlllibopen 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)) +authorized_commands = [ + 'upload-auth', + 'comment', + 'vote-gallery', + 'vote-comment' +] - return ('=' * num_dashes_left) + ' ' + s + ' ' + ('=' * num_dashes_right) +oauth_commands = [ + 'credits', + 'refresh', + 'authorize' +] + +unauth_commands = [ + 'upload', + 'list-comments', + 'get-album', + 'get-comment', + 'get-gallery' +] -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...]") + print('\nUsage: \tpython main.py (action) [options...]') lines = [] lines.append(('oauth', 'credits', 'View the rate limit information for this client')) @@ -50,9 +63,9 @@ def usage(argv): 'auth': 'Authorized Actions' } - categoryHeadersOutputSoFar = [] - + category_headers_output_so_far = [] col_width = 0 + for category in headers.values(): col_width = max(col_width, len(category)) for line in lines: @@ -63,123 +76,83 @@ def usage(argv): 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") - ''' + if not cat in category_headers_output_so_far: + print('\n' + format.center_pad(headers[cat], col_width) + '\n') + category_headers_output_so_far.append(cat) + print(format.two_column_with_period(text, desc, col_width) + '\n') sys.exit(1) + def main(): usage(sys.argv) - config = None try: fd = open('data/config.json', 'r') - except: - print("config file [config.json] not found.") + except IOError: + print('Config file [config.json] not found.') sys.exit(1) try: config = json.loads(fd.read()) - except: - print("invalid json in config file.") + except ValueError: + print('Invalid JSON in config file.') sys.exit(1) - mfactory = factory(config) - - + mfactory = 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(mfactory, action) + elif action in oauth_commands: + handle_oauth_commands(mfactory, config, action) + elif action in unauth_commands: + handle_unauthorized_commands(mfactory, action) else: - if action in oauth_commands: - handle_oauth_commands(mfactory, config, action) - else: - handle_unauthorized_commands(mfactory, action) + print('Invalid command provided! Use --help to see all available actions.') + def handle_authorized_commands(factory, action): token = sys.argv[2] - auth = factory.build_oauth(token, None) + 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] + elif action == 'comment': + item_hash = sys.argv[3] text = ' '.join(sys.argv[4:]) if len(text) > 140: - print("Comment too long (trim by %d characters)." % (len(text) - 140)) + print('Comment too long (trim by %d characters).' % (len(text) - 140)) sys.exit(1) - req = factory.build_request(('gallery', thash, 'comment'), { + req = factory.build_request(('gallery', item_hash, 'comment'), { 'comment': text }) + elif action in ('vote-gallery', 'vote-comment'): + (target_id, vote) = sys.argv[3:] - if action == 'vote-gallery' or action == 'vote-comment': - (tid, vote) = sys.argv[3:] - - target = None if action == 'vote-gallery': - target = ('gallery', tid, 'vote', vote) + target = ('gallery', target_id, 'vote', vote) else: - target = ('comment', tid, 'vote', vote) + target = ('comment', target_id, 'vote', vote) - req = factory.build_request(target) + 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'])) + print('Success! https://www.imgur.com/gallery/%s/comment/%s' % (item_hash, res['id'])) else: print(res) - except expired: - print("Expired access token") - + except Expired: + print('Expired access token') def handle_oauth_commands(factory, config, action): @@ -189,15 +162,14 @@ def handle_oauth_commands(factory, config, action): req = factory.build_request(('credits',)) res = imgur.retrieve(req) print(res) - - if action == 'refresh': + elif 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'))) + 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.' % ( @@ -205,21 +177,24 @@ def handle_oauth_commands(factory, config, action): res[1]['refresh_token'], res[1]['expires_in'] )) - - if action == 'authorize': + elif 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") + print('Visit this URL to get a PIN to authorize: %soauth2/authorize?client_id=%s&response_type=pin' % ( + factory.get_api_url(), + config['client_id'] + )) 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'))) + 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." % ( + print('Access Token: %s\nRefresh Token: %s\nExpires: %d seconds from now.' % ( res[1]['access_token'], res[1]['refresh_token'], res[1]['expires_in'] @@ -228,26 +203,24 @@ def handle_oauth_commands(factory, config, action): 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')) + item_hash = sys.argv[2] + req = factory.build_request(('gallery', item_hash, '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)) + (item_hash, cid) = sys.argv[2:4] + req = factory.build_request(('gallery', item_hash, 'comments', cid)) if action == 'get-gallery': imgur = factory.build_api() @@ -259,4 +232,4 @@ def handle_unauthorized_commands(factory, action): if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/setup.py b/setup.py index f5e056d..0d2cbcb 100644 --- a/setup.py +++ b/setup.py @@ -11,17 +11,17 @@ # 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.0.1', + version='1.0.2', - description='Official reference Imgur python library with OAuth2', + description='Official Imgur python library with OAuth2 and samples', long_description='', # The project's main homepage. url='https://github.com/jacobgreenleaf/imgur-python', # Author details - author='Jacob Greenleaf', - author_email='jacob@imgur.com', + author='Imgur Inc.', + author_email='api@imgur.com', # Choose your license license='MIT', @@ -52,7 +52,7 @@ ], # What does your project relate to? - keywords='sample setuptools development', + keywords='sample setuptools development imgur', # You can just specify the packages manually here if your project is # simple. Or you can use find_packages(). From 8496b8a84ca3247857d7f9fc07b87feee63317ef Mon Sep 17 00:00:00 2001 From: jasdev Date: Tue, 26 Aug 2014 23:21:41 -0700 Subject: [PATCH 09/89] Fixing get-comment and removing extra variable --- imgur-python/main.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/imgur-python/main.py b/imgur-python/main.py index 91b211b..f749026 100644 --- a/imgur-python/main.py +++ b/imgur-python/main.py @@ -219,11 +219,10 @@ def handle_unauthorized_commands(factory, action): req = factory.build_request(('album', id)) if action == 'get-comment': - (item_hash, cid) = sys.argv[2:4] - req = factory.build_request(('gallery', item_hash, 'comments', cid)) + cid = sys.argv[2] + req = factory.build_request(('comment', cid)) if action == 'get-gallery': - imgur = factory.build_api() id = sys.argv[2] req = factory.build_request(('gallery', id)) From 10e2d71c4509d258e792d834c3eb58c1f9a6725d Mon Sep 17 00:00:00 2001 From: jasdev Date: Tue, 26 Aug 2014 23:22:50 -0700 Subject: [PATCH 10/89] Adding note on config --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c63ea5e..c3120f4 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Installation Configuration ------------- -Configuration is done through the **config.json** file in JSON format. The contents of the file should be a JSON +Configuration is done through the **config.json** (placed in `imgur-python/data`) file in JSON format. The contents of the file should be a JSON object with the following properties: ### client_id From e0cbf7bf195675bda6c52b4ff0236e8997ff47ee Mon Sep 17 00:00:00 2001 From: jasdev Date: Tue, 26 Aug 2014 23:24:33 -0700 Subject: [PATCH 11/89] Removing autogenerated line --- imgur-python/helpers/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/imgur-python/helpers/__init__.py b/imgur-python/helpers/__init__.py index f56218a..e69de29 100644 --- a/imgur-python/helpers/__init__.py +++ b/imgur-python/helpers/__init__.py @@ -1 +0,0 @@ -__author__ = 'jasdev' From 2f22c503bc91749e4777c1684d80026d60f7d3e9 Mon Sep 17 00:00:00 2001 From: Jacob Greenleaf Date: Wed, 27 Aug 2014 11:26:11 -0700 Subject: [PATCH 12/89] Add support for non-POST/GET verbs --- imgur-python/imgur/factory.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/imgur-python/imgur/factory.py b/imgur-python/imgur/factory.py index 3e69029..ae2175e 100644 --- a/imgur-python/imgur/factory.py +++ b/imgur-python/imgur/factory.py @@ -43,7 +43,7 @@ def build_oauth(self, access, refresh, expire_time = None): else: return accesstoken(access, refresh, expire_time) - def build_request(self, endpoint, data = None): + def build_request(self, endpoint, data = None, method = 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.''' @@ -55,6 +55,10 @@ def build_request(self, endpoint, data = None): req = UrlLibRequest(url) if data is not None: req.add_data(UrlLibEncode(data).encode('utf-8')) + + if method is not None: + # python urllib2 is broken... http://stackoverflow.com/a/111988 + req.get_method = lambda: method return req def build_rate_limit(self, limits = None): From f922907f9da797ee0044127229e0ccd378bcbdf1 Mon Sep 17 00:00:00 2001 From: Jacob Greenleaf Date: Wed, 27 Aug 2014 11:26:34 -0700 Subject: [PATCH 13/89] Add action for adding image to album --- imgur-python/main.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/imgur-python/main.py b/imgur-python/main.py index 810c22f..1304074 100644 --- a/imgur-python/main.py +++ b/imgur-python/main.py @@ -43,6 +43,7 @@ def usage(argv): 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\'')) + lines.append(('auth', 'add-album-image [access-token] [album-id] [image-id]', 'Add an image to an album')) headers = { 'oauth': 'OAuth Actions', @@ -119,7 +120,8 @@ def main(): 'upload-auth', 'comment', 'vote-gallery', - 'vote-comment' + 'vote-comment', + 'add-album-image' ] oauth_commands = [ @@ -157,6 +159,14 @@ def handle_authorized_commands(factory, action): 'comment': text }) + if action == 'add-album-image': + album_id = sys.argv[3] + image_ids = ','.join(sys.argv[4:]) + + req = factory.build_request(('album', album_id), { + 'ids[]': image_ids + }, 'PUT') + if action == 'vote-gallery' or action == 'vote-comment': (tid, vote) = sys.argv[3:] From e919f86d60b11d81ae69372fd224581beac915c0 Mon Sep 17 00:00:00 2001 From: jasdev Date: Thu, 28 Aug 2014 10:56:11 -0700 Subject: [PATCH 14/89] Scaffolding for making requests and automated token refresh --- imgur-python/client.py | 87 +++++++++++++++++++++++++++ imgur-python/helpers/error.py | 10 +++ imgur-python/imgur/factory.py | 2 +- imgur-python/imgur/models/__init__.py | 0 imgur-python/imgur/models/account.py | 9 +++ imgur-python/runner.py | 5 ++ 6 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 imgur-python/client.py create mode 100644 imgur-python/helpers/error.py create mode 100644 imgur-python/imgur/models/__init__.py create mode 100644 imgur-python/imgur/models/account.py create mode 100644 imgur-python/runner.py diff --git a/imgur-python/client.py b/imgur-python/client.py new file mode 100644 index 0000000..53a5ddc --- /dev/null +++ b/imgur-python/client.py @@ -0,0 +1,87 @@ +import requests +from helpers.error import ImgurClientError + +API_URL = 'https://api.imgur.com/' + + +class AuthWrapper: + 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: + + def __init__(self, client_id=None, client_secret=None, access_token=None, refresh_token=None): + self.client_id = client_id + self.client_secret = client_secret + self.auth = None + + if refresh_token is not None: + self.auth = AuthWrapper(access_token, refresh_token, client_id, client_secret) + + def get_client_id(self): + return self.client_id + + def prepare_headers(self): + if self.auth is None: + if self.client_id is None: + raise ImgurClientError('Client credentials not found!') + else: + return {'Authorization': 'Client-ID %s' % self.get_client_id()} + else: + return {'Authorization': 'Bearer %s' % self.auth.get_current_access_token()} + + def make_request(self, method, route, data=None): + method = method.lower() + method_to_call = getattr(requests, method) + + header = self.prepare_headers() + url = API_URL + '3/%s' % route + + 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() + response = method_to_call(url, headers=header, data=data) + + try: + response_data = response.json() + + if 'data' in response_data and 'error' in response_data['data']: + raise ImgurClientError(response_data['data'], response.status_code) + except: + raise ImgurClientError('JSON decoding of response failed.') + + return response_data['data'] \ No newline at end of file diff --git a/imgur-python/helpers/error.py b/imgur-python/helpers/error.py new file mode 100644 index 0000000..559d128 --- /dev/null +++ b/imgur-python/helpers/error.py @@ -0,0 +1,10 @@ +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 \ No newline at end of file diff --git a/imgur-python/imgur/factory.py b/imgur-python/imgur/factory.py index de3784c..8c13336 100644 --- a/imgur-python/imgur/factory.py +++ b/imgur-python/imgur/factory.py @@ -17,7 +17,7 @@ class Factory: - API_URL = "https://api.imgur.com/" + API_URL = 'https://api.imgur.com/' def __init__(self, config): self.config = config diff --git a/imgur-python/imgur/models/__init__.py b/imgur-python/imgur/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/imgur-python/imgur/models/account.py b/imgur-python/imgur/models/account.py new file mode 100644 index 0000000..e5006c9 --- /dev/null +++ b/imgur-python/imgur/models/account.py @@ -0,0 +1,9 @@ +class Account: + + def __init__(self, id, url, bio, reputation, created, pro_expiration): + self.id = id + self.url = url + self.bio = bio + self.reputation = reputation + self.created = created, + self.pro_expiration = pro_expiration \ No newline at end of file diff --git a/imgur-python/runner.py b/imgur-python/runner.py new file mode 100644 index 0000000..c04b683 --- /dev/null +++ b/imgur-python/runner.py @@ -0,0 +1,5 @@ +from client import ImgurClient + +if __name__ == '__main__': + img = ImgurClient(client_id='3aa9b7ef8d192ea') + print img.make_request('GET', 'account/jasdev') \ No newline at end of file From 24b37b29e6e28883bc3c40b86cb172ac8adb6b42 Mon Sep 17 00:00:00 2001 From: Jacob Greenleaf Date: Thu, 28 Aug 2014 13:49:27 -0700 Subject: [PATCH 15/89] Add GET queries with params; replies --- imgur-python/imgur/factory.py | 18 ++++++++++++++++++ imgur-python/imgur/imgur.py | 2 +- imgur-python/main.py | 21 ++++++++++++--------- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/imgur-python/imgur/factory.py b/imgur-python/imgur/factory.py index fc88eff..35955d3 100644 --- a/imgur-python/imgur/factory.py +++ b/imgur-python/imgur/factory.py @@ -47,6 +47,24 @@ def build_oauth(access, refresh, expire_time=None): else: return AccessToken(access, refresh, expire_time) + def build_get_request(self, endpoint, urlparams=None): + if isinstance(endpoint, str): + url = self.API_URL + endpoint + else: + url = self.API_URL + '3/' + ('/'.join(endpoint)) + ".json" + + if urlparams is not None: + req = urllibrequest(url + '?' + urllibencode(urlparams)) + else: + req = urllibrequest(url) + + return req + + def build_put_request(self, endpoint, data=None): + self.build_request(self, endpoint, data, 'PUT') + def build_post_request(self, endpoint, data=None): + self.build_request(self, endpoint, data, 'POST') + def build_request(self, endpoint, data=None, method=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.""" diff --git a/imgur-python/imgur/imgur.py b/imgur-python/imgur/imgur.py index 5c95440..63a04cd 100644 --- a/imgur-python/imgur/imgur.py +++ b/imgur-python/imgur/imgur.py @@ -48,4 +48,4 @@ def retrieve(self, request): if res['success'] is not True: raise Exception(res['data']['error']['message']) - return res['data'] \ No newline at end of file + return res['data'] diff --git a/imgur-python/main.py b/imgur-python/main.py index 7aaa241..d41f11e 100644 --- a/imgur-python/main.py +++ b/imgur-python/main.py @@ -19,7 +19,8 @@ 'comment', 'vote-gallery', 'vote-comment', - 'add-album-image' + 'add-album-image', + 'replies' ] oauth_commands = [ @@ -130,14 +131,14 @@ def handle_authorized_commands(factory, action): print('Comment too long (trim by %d characters).' % (len(text) - 140)) sys.exit(1) - req = factory.build_request(('gallery', item_hash, 'comment'), { + req = factory.build_post_request(('gallery', item_hash, 'comment'), { 'comment': text }) elif action == 'add-album-image': album_id = sys.argv[3] image_ids = ','.join(sys.argv[4:]) - req = factory.build_request(('album', album_id), { + req = factory.build_put_request(('album', album_id), { 'ids[]': image_ids }, 'PUT') @@ -149,7 +150,9 @@ def handle_authorized_commands(factory, action): else: target = ('comment', target_id, 'vote', vote) - req = factory.build_request(target, "") + req = factory.build_get_request(target, "") + elif action == 'replies': + req = factory.build_get_request(('account', 'me', 'notifications', 'replies'), {'new': 'false'}) try: res = imgur.retrieve(req) @@ -169,7 +172,7 @@ def handle_oauth_commands(factory, config, action): imgur = factory.build_api() if action == 'credits': - req = factory.build_request(('credits',)) + req = factory.build_get_request(('credits',)) res = imgur.retrieve(req) print(res) elif action == 'refresh': @@ -222,19 +225,19 @@ def handle_unauthorized_commands(factory, action): else: if action == 'list-comments': item_hash = sys.argv[2] - req = factory.build_request(('gallery', item_hash, 'comments')) + req = factory.build_get_request(('gallery', item_hash, 'comments')) if action == 'get-album': id = sys.argv[2] - req = factory.build_request(('album', id)) + req = factory.build_get_request(('album', id)) if action == 'get-comment': cid = sys.argv[2] - req = factory.build_request(('comment', cid)) + req = factory.build_get_request(('comment', cid)) if action == 'get-gallery': id = sys.argv[2] - req = factory.build_request(('gallery', id)) + req = factory.build_get_request(('gallery', id)) res = imgur.retrieve(req) print(res) From 19b5fb7ad8b90092d0ac9ec777f37d95cfc605a4 Mon Sep 17 00:00:00 2001 From: jasdev Date: Thu, 28 Aug 2014 16:27:06 -0700 Subject: [PATCH 16/89] Progress on account endpoints --- imgur-python/client.py | 34 ++++++++++++++++++++-- imgur-python/imgur/models/gallery_album.py | 9 ++++++ imgur-python/imgur/models/gallery_image.py | 9 ++++++ 3 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 imgur-python/imgur/models/gallery_album.py create mode 100644 imgur-python/imgur/models/gallery_image.py diff --git a/imgur-python/client.py b/imgur-python/client.py index 53a5ddc..9219f4a 100644 --- a/imgur-python/client.py +++ b/imgur-python/client.py @@ -1,5 +1,8 @@ import requests from helpers.error import ImgurClientError +from imgur.models.account import Account +from imgur.models.gallery_album import GalleryAlbum +from imgur.models.gallery_image import GalleryImage API_URL = 'https://api.imgur.com/' @@ -79,9 +82,34 @@ def make_request(self, method, route, data=None): try: response_data = response.json() - if 'data' in response_data and 'error' in response_data['data']: - raise ImgurClientError(response_data['data'], response.status_code) + if 'error' in response_data['data']: + raise ImgurClientError(response_data['data']['error'], response.status_code) except: raise ImgurClientError('JSON decoding of response failed.') - return response_data['data'] \ No newline at end of file + return response_data['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.') + + # 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): + self.validate_user_context(username) + favorites = self.make_request('GET', 'account/%s/gallery_favorites' % username) + + result = [GalleryImage(favorite) for favorite in favorites] + return result \ No newline at end of file diff --git a/imgur-python/imgur/models/gallery_album.py b/imgur-python/imgur/models/gallery_album.py new file mode 100644 index 0000000..39f74e1 --- /dev/null +++ b/imgur-python/imgur/models/gallery_album.py @@ -0,0 +1,9 @@ +class GalleryAlbum: + + # 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]) \ No newline at end of file diff --git a/imgur-python/imgur/models/gallery_image.py b/imgur-python/imgur/models/gallery_image.py new file mode 100644 index 0000000..cae69a5 --- /dev/null +++ b/imgur-python/imgur/models/gallery_image.py @@ -0,0 +1,9 @@ +class GalleryImage: + + # 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]) \ No newline at end of file From 9984d30469c17f3e32e43bc990621aeb9144dae5 Mon Sep 17 00:00:00 2001 From: jasdev Date: Thu, 28 Aug 2014 16:31:37 -0700 Subject: [PATCH 17/89] toggle between albums and images --- imgur-python/client.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/imgur-python/client.py b/imgur-python/client.py index 9219f4a..4e4c696 100644 --- a/imgur-python/client.py +++ b/imgur-python/client.py @@ -111,5 +111,11 @@ def get_gallery_favorites(self, username): self.validate_user_context(username) favorites = self.make_request('GET', 'account/%s/gallery_favorites' % username) - result = [GalleryImage(favorite) for favorite in favorites] + result = [] + for favorite in favorites: + if favorite['is_album']: + result.append(GalleryAlbum(favorite)) + else: + result.append(GalleryAlbum(favorite)) + return result \ No newline at end of file From 65a89ff088a63df2deaa319e37b034eac3febcc6 Mon Sep 17 00:00:00 2001 From: jasdev Date: Thu, 28 Aug 2014 16:35:29 -0700 Subject: [PATCH 18/89] Fixing class name --- imgur-python/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imgur-python/client.py b/imgur-python/client.py index 4e4c696..7ea18f0 100644 --- a/imgur-python/client.py +++ b/imgur-python/client.py @@ -116,6 +116,6 @@ def get_gallery_favorites(self, username): if favorite['is_album']: result.append(GalleryAlbum(favorite)) else: - result.append(GalleryAlbum(favorite)) + result.append(GalleryImage(favorite)) return result \ No newline at end of file From 8bef555a43e74bd21a940c4df72dcceeccf25cfb Mon Sep 17 00:00:00 2001 From: jasdev Date: Fri, 29 Aug 2014 17:38:40 -0400 Subject: [PATCH 19/89] Account functionality complete --- imgur-python/client.py | 127 ++++++++++++++++-- imgur-python/imgur/models/account_settings.py | 13 ++ imgur-python/imgur/models/album.py | 9 ++ imgur-python/imgur/models/comment.py | 9 ++ imgur-python/imgur/models/image.py | 9 ++ 5 files changed, 155 insertions(+), 12 deletions(-) create mode 100644 imgur-python/imgur/models/account_settings.py create mode 100644 imgur-python/imgur/models/album.py create mode 100644 imgur-python/imgur/models/comment.py create mode 100644 imgur-python/imgur/models/image.py diff --git a/imgur-python/client.py b/imgur-python/client.py index 7ea18f0..12caf4e 100644 --- a/imgur-python/client.py +++ b/imgur-python/client.py @@ -1,8 +1,12 @@ import requests -from helpers.error import ImgurClientError +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 helpers.error import ImgurClientError from imgur.models.gallery_album import GalleryAlbum from imgur.models.gallery_image import GalleryImage +from imgur.models.account_settings import AccountSettings API_URL = 'https://api.imgur.com/' @@ -79,20 +83,37 @@ def make_request(self, method, route, data=None): header = self.prepare_headers() response = method_to_call(url, headers=header, data=data) + # TODO: Add rate-limit checks + try: response_data = response.json() - - if 'error' in response_data['data']: - raise ImgurClientError(response_data['data']['error'], response.status_code) except: raise ImgurClientError('JSON decoding of response failed.') + if isinstance(response_data['data'], dict) and 'error' in response_data['data']: + raise ImgurClientError(response_data['data']['error'], response.status_code) + return response_data['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.') + + @staticmethod + def build_gallery_images_and_albums(items): + result = [] + for item in items: + if item['is_album']: + result.append(GalleryAlbum(item)) + else: + result.append(GalleryImage(item)) + + return result + # Account-related endpoints def get_account(self, username): self.validate_user_context(username) @@ -109,13 +130,95 @@ def get_account(self, username): def get_gallery_favorites(self, username): self.validate_user_context(username) - favorites = self.make_request('GET', 'account/%s/gallery_favorites' % username) + gallery_favorites = self.make_request('GET', 'account/%s/gallery_favorites' % username) - result = [] - for favorite in favorites: - if favorite['is_album']: - result.append(GalleryAlbum(favorite)) - else: - result.append(GalleryImage(favorite)) + return self.build_gallery_images_and_albums(gallery_favorites) + + def get_account_favorites(self, username): + self.validate_user_context(username) + favorites = self.make_request('GET', 'account/%s/favorites' % username) + + return self.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 self.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): + allowed_fields = { + 'bio', 'public_images', 'messaging_enabled', 'album_privacy', 'accepted_gallery_terms', 'username' + } - return result \ No newline at end of file + post_data = {setting: fields[setting] for setting in set(allowed_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, page=0): + self.validate_user_context(username) + return self.make_request('GET', 'account/%s/images/ids/%d' % (username, page)) \ No newline at end of file diff --git a/imgur-python/imgur/models/account_settings.py b/imgur-python/imgur/models/account_settings.py new file mode 100644 index 0000000..af2ee3b --- /dev/null +++ b/imgur-python/imgur/models/account_settings.py @@ -0,0 +1,13 @@ +class AccountSettings: + + 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 \ No newline at end of file diff --git a/imgur-python/imgur/models/album.py b/imgur-python/imgur/models/album.py new file mode 100644 index 0000000..ddea227 --- /dev/null +++ b/imgur-python/imgur/models/album.py @@ -0,0 +1,9 @@ +class Album: + + # 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]) \ No newline at end of file diff --git a/imgur-python/imgur/models/comment.py b/imgur-python/imgur/models/comment.py new file mode 100644 index 0000000..b00282f --- /dev/null +++ b/imgur-python/imgur/models/comment.py @@ -0,0 +1,9 @@ +class Comment: + + # 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]) \ No newline at end of file diff --git a/imgur-python/imgur/models/image.py b/imgur-python/imgur/models/image.py new file mode 100644 index 0000000..56b743c --- /dev/null +++ b/imgur-python/imgur/models/image.py @@ -0,0 +1,9 @@ +class Image: + + # 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]) \ No newline at end of file From d8d94ef34d2b3ec10b2a63d91f365d20f297ce5e Mon Sep 17 00:00:00 2001 From: jasdev Date: Wed, 3 Sep 2014 12:28:13 -0400 Subject: [PATCH 20/89] Finished album functionality --- imgur-python/client.py | 64 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 3 deletions(-) diff --git a/imgur-python/client.py b/imgur-python/client.py index 12caf4e..dcf4299 100644 --- a/imgur-python/client.py +++ b/imgur-python/client.py @@ -48,6 +48,9 @@ def refresh(self): class ImgurClient: + allowed_album_fields = { + 'ids', 'title', 'description', 'privacy', 'layout', 'cover' + } def __init__(self, client_id=None, client_secret=None, access_token=None, refresh_token=None): self.client_id = client_id @@ -76,12 +79,18 @@ def make_request(self, method, route, data=None): header = self.prepare_headers() url = API_URL + '3/%s' % route - response = method_to_call(url, headers=header, data=data) + if method == 'delete': + response = method_to_call(url, headers=header, params=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() - response = method_to_call(url, headers=header, data=data) + if method == 'delete': + response = method_to_call(url, headers=header, params=data) + else: + response = method_to_call(url, headers=header, data=data) # TODO: Add rate-limit checks @@ -221,4 +230,53 @@ def get_account_image_ids(self, username, page=0): def get_account_images_count(self, username, page=0): self.validate_user_context(username) - return self.make_request('GET', 'account/%s/images/ids/%d' % (username, page)) \ No newline at end of file + return self.make_request('GET', 'account/%s/images/ids/%d' % (username, page)) + + 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}) \ No newline at end of file From 61b4974f0df2591a52bf7fce0f6fdfe79e00c4a8 Mon Sep 17 00:00:00 2001 From: jasdev Date: Sun, 7 Sep 2014 12:20:38 -0700 Subject: [PATCH 21/89] Finishing comment functionality --- imgur-python/client.py | 39 ++++++++++++++++++++++++++++++++-- imgur-python/helpers/format.py | 13 +++++++++++- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/imgur-python/client.py b/imgur-python/client.py index dcf4299..e2bd538 100644 --- a/imgur-python/client.py +++ b/imgur-python/client.py @@ -4,6 +4,7 @@ from imgur.models.account import Account from imgur.models.comment import Comment from helpers.error import ImgurClientError +from helpers.format import build_comment_tree from imgur.models.gallery_album import GalleryAlbum from imgur.models.gallery_image import GalleryImage from imgur.models.account_settings import AccountSettings @@ -92,7 +93,9 @@ def make_request(self, method, route, data=None): else: response = method_to_call(url, headers=header, data=data) - # TODO: Add rate-limit checks + # Rate-limit check + if response.status_code == 429: + raise ImgurClientError('Rate-limit exceeded!') try: response_data = response.json() @@ -232,6 +235,7 @@ def get_account_images_count(self, username, page=0): self.validate_user_context(username) return self.make_request('GET', 'account/%s/images/ids/%d' % (username, page)) + # Album-related endpoints def get_album(self, album_id): album = self.make_request('GET', 'album/%s' % album_id) return Album(album) @@ -279,4 +283,35 @@ 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}) \ No newline at end of file + 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) + replies['children'] = build_comment_tree(replies['children']) + + return replies + + def post_comment_reply(self, comment_id, image_id, comment): + data = { + 'image_id': image_id, + 'comment': comment + } + + return self.make_request('POST', 'comment/%d' % comment_id, data) + + def comment_vote(self, comment_id, vote, toggle=True): + toggle_behavior = 1 if toggle else 0 + + return self.make_request('POST', 'comment/%d/vote/%s?toggle=%d' % (comment_id, vote, toggle_behavior)) + + def comment_report(self, comment_id): + return self.make_request('POST', 'comment/%d/report' % comment_id) \ No newline at end of file diff --git a/imgur-python/helpers/format.py b/imgur-python/helpers/format.py index fa9f574..4a859ab 100644 --- a/imgur-python/helpers/format.py +++ b/imgur-python/helpers/format.py @@ -1,4 +1,5 @@ import math +from imgur.models.comment import Comment def center_pad(s, length): @@ -11,4 +12,14 @@ def center_pad(s, length): def two_column_with_period(left, right, length): num_periods = int(length - (len(left) + len(right) + 2)) - return left + ' ' + ('.' * num_periods) + ' ' + right \ No newline at end of file + return left + ' ' + ('.' * num_periods) + ' ' + right + + +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 \ No newline at end of file From 46d389c3e8d57751ce71b06fbc68d3785dc0f83d Mon Sep 17 00:00:00 2001 From: jasdev Date: Sun, 7 Sep 2014 12:29:34 -0700 Subject: [PATCH 22/89] Removing runner file --- imgur-python/runner.py | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 imgur-python/runner.py diff --git a/imgur-python/runner.py b/imgur-python/runner.py deleted file mode 100644 index c04b683..0000000 --- a/imgur-python/runner.py +++ /dev/null @@ -1,5 +0,0 @@ -from client import ImgurClient - -if __name__ == '__main__': - img = ImgurClient(client_id='3aa9b7ef8d192ea') - print img.make_request('GET', 'account/jasdev') \ No newline at end of file From f8d4474dcb389d011b30711c2ed5be19a7ab3ff5 Mon Sep 17 00:00:00 2001 From: jasdev Date: Sun, 7 Sep 2014 12:47:30 -0700 Subject: [PATCH 23/89] Adding login checks --- imgur-python/client.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/imgur-python/client.py b/imgur-python/client.py index e2bd538..93567eb 100644 --- a/imgur-python/client.py +++ b/imgur-python/client.py @@ -301,6 +301,7 @@ def get_comment_replies(self, comment_id): return replies def post_comment_reply(self, comment_id, image_id, comment): + self.logged_in() data = { 'image_id': image_id, 'comment': comment @@ -309,9 +310,11 @@ def post_comment_reply(self, comment_id, image_id, comment): return self.make_request('POST', 'comment/%d' % comment_id, data) def comment_vote(self, comment_id, vote, toggle=True): + self.logged_in() toggle_behavior = 1 if toggle else 0 return self.make_request('POST', 'comment/%d/vote/%s?toggle=%d' % (comment_id, vote, toggle_behavior)) def comment_report(self, comment_id): + self.logged_in() return self.make_request('POST', 'comment/%d/report' % comment_id) \ No newline at end of file From 2849b76ae2c1c9508967d907ca6c933a57032480 Mon Sep 17 00:00:00 2001 From: jasdev Date: Thu, 2 Oct 2014 12:16:32 -0700 Subject: [PATCH 24/89] Adding Custom Gallery methods --- imgur-python/client.py | 112 +++++++++++++++++++- imgur-python/helpers/format.py | 2 +- imgur-python/imgur/models/custom_gallery.py | 16 +++ 3 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 imgur-python/imgur/models/custom_gallery.py diff --git a/imgur-python/client.py b/imgur-python/client.py index 93567eb..d12caf0 100644 --- a/imgur-python/client.py +++ b/imgur-python/client.py @@ -7,6 +7,7 @@ from helpers.format import build_comment_tree from imgur.models.gallery_album import GalleryAlbum from imgur.models.gallery_image import GalleryImage +from imgur.models.custom_gallery import CustomGallery from imgur.models.account_settings import AccountSettings API_URL = 'https://api.imgur.com/' @@ -81,7 +82,7 @@ def make_request(self, method, route, data=None): url = API_URL + '3/%s' % route if method == 'delete': - response = method_to_call(url, headers=header, params=data) + response = method_to_call(url, headers=header, params=data, data=data) else: response = method_to_call(url, headers=header, data=data) @@ -89,7 +90,7 @@ def make_request(self, method, route, data=None): self.auth.refresh() header = self.prepare_headers() if method == 'delete': - response = method_to_call(url, headers=header, params=data) + response = method_to_call(url, headers=header, params=data, data=data) else: response = method_to_call(url, headers=header, data=data) @@ -317,4 +318,109 @@ def comment_vote(self, comment_id, vote, toggle=True): def comment_report(self, comment_id): self.logged_in() - return self.make_request('POST', 'comment/%d/report' % comment_id) \ No newline at end of file + 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}) \ No newline at end of file diff --git a/imgur-python/helpers/format.py b/imgur-python/helpers/format.py index 4a859ab..bc9812d 100644 --- a/imgur-python/helpers/format.py +++ b/imgur-python/helpers/format.py @@ -22,4 +22,4 @@ def build_comment_tree(children): to_insert.children = build_comment_tree(to_insert.children) children_objects.append(to_insert) - return children_objects \ No newline at end of file + return children_objects diff --git a/imgur-python/imgur/models/custom_gallery.py b/imgur-python/imgur/models/custom_gallery.py new file mode 100644 index 0000000..8c3fc67 --- /dev/null +++ b/imgur-python/imgur/models/custom_gallery.py @@ -0,0 +1,16 @@ +from gallery_album import GalleryAlbum +from gallery_image import GalleryImage + + +class CustomGallery: + + def __init__(self, id, name, datetime, account_url, link, tags, item_count=None, items=None): + self.id = 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 \ No newline at end of file From 60553031fd423482063b786932fd5a31004bc89f Mon Sep 17 00:00:00 2001 From: jasdev Date: Thu, 2 Oct 2014 12:21:21 -0700 Subject: [PATCH 25/89] Style fix --- imgur-python/client.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/imgur-python/client.py b/imgur-python/client.py index d12caf0..7b5cba8 100644 --- a/imgur-python/client.py +++ b/imgur-python/client.py @@ -349,9 +349,7 @@ def get_user_galleries(self): def create_custom_gallery(self, name, tags=None): self.logged_in() - data = { - 'name': name - } + data = {'name': name} if tags: data['tags'] = ','.join(tags) @@ -389,9 +387,7 @@ def custom_gallery_add_tags(self, gallery_id, tags): self.logged_in() if tags: - data = { - 'tags': ','.join(tags) - } + data = {'tags': ','.join(tags)} else: raise ImgurClientError('tags must not be empty!') @@ -401,9 +397,7 @@ def custom_gallery_remove_tags(self, gallery_id, tags): self.logged_in() if tags: - data = { - 'tags': ','.join(tags) - } + data = {'tags': ','.join(tags)} else: raise ImgurClientError('tags must not be empty!') From 83f60263d594e5027a2488b1ddf2d1112c66ffdb Mon Sep 17 00:00:00 2001 From: jasdev Date: Thu, 2 Oct 2014 18:34:37 -0700 Subject: [PATCH 26/89] progress on gallery --- imgur-python/client.py | 51 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/imgur-python/client.py b/imgur-python/client.py index 7b5cba8..7d6120a 100644 --- a/imgur-python/client.py +++ b/imgur-python/client.py @@ -117,13 +117,19 @@ def logged_in(self): raise ImgurClientError('Must be logged in to complete request.') @staticmethod - def build_gallery_images_and_albums(items): - result = [] - for item in items: - if item['is_album']: - result.append(GalleryAlbum(item)) + 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.append(GalleryImage(item)) + result = GalleryImage(response) return result @@ -417,4 +423,35 @@ def block_tag(self, tag): def unblock_tag(self, tag): self.logged_in() - return self.make_request('POST', 'g/unblock_tag', data={'tag': tag}) \ No newline at end of file + 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 self.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 self.build_gallery_images_and_albums(response) + + def memes_subgallery_image(self, item_id): + item = self.make_request('GET', 'g/memes/%s' % item_id) + return self.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 self.build_gallery_images_and_albums(response) From a4fe12fa8cf08a284bbba343951196f4efa6e535 Mon Sep 17 00:00:00 2001 From: jasdev Date: Sat, 4 Oct 2014 17:31:55 -0700 Subject: [PATCH 27/89] Gallery endpoints finished --- imgur-python/client.py | 148 ++++++++++++++++++++------ imgur-python/helpers/error.py | 7 +- imgur-python/helpers/format.py | 33 ++++++ imgur-python/imgur/models/tag.py | 13 +++ imgur-python/imgur/models/tag_vote.py | 7 ++ 5 files changed, 173 insertions(+), 35 deletions(-) create mode 100644 imgur-python/imgur/models/tag.py create mode 100644 imgur-python/imgur/models/tag_vote.py diff --git a/imgur-python/client.py b/imgur-python/client.py index 7d6120a..be9327f 100644 --- a/imgur-python/client.py +++ b/imgur-python/client.py @@ -1,12 +1,14 @@ 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_comment_tree -from imgur.models.gallery_album import GalleryAlbum -from imgur.models.gallery_image import GalleryImage +from helpers.format import format_comment_tree +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 @@ -54,7 +56,11 @@ class ImgurClient: 'ids', 'title', 'description', 'privacy', 'layout', 'cover' } - def __init__(self, client_id=None, client_secret=None, access_token=None, refresh_token=None): + allowed_advanced_search_fields = { + 'q_all', 'q_any', 'q_exactly', 'q_not', 'q_type', 'q_size_px' + } + + def __init__(self, client_id, client_secret, access_token=None, refresh_token=None): self.client_id = client_id self.client_secret = client_secret self.auth = None @@ -62,6 +68,9 @@ def __init__(self, client_id=None, client_secret=None, access_token=None, refres if refresh_token is not None: self.auth = AuthWrapper(access_token, refresh_token, client_id, client_secret) + 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 @@ -81,7 +90,7 @@ def make_request(self, method, route, data=None): header = self.prepare_headers() url = API_URL + '3/%s' % route - if method == 'delete': + 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) @@ -89,14 +98,14 @@ def make_request(self, method, route, data=None): if response.status_code == 403 and self.auth is not None: self.auth.refresh() header = self.prepare_headers() - if method == 'delete': + 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) # Rate-limit check if response.status_code == 429: - raise ImgurClientError('Rate-limit exceeded!') + raise ImgurClientRateLimitError() try: response_data = response.json() @@ -116,23 +125,6 @@ def logged_in(self): if self.auth is None: raise ImgurClientError('Must be logged in to complete request.') - @staticmethod - 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 - # Account-related endpoints def get_account(self, username): self.validate_user_context(username) @@ -151,19 +143,19 @@ def get_gallery_favorites(self, username): self.validate_user_context(username) gallery_favorites = self.make_request('GET', 'account/%s/gallery_favorites' % username) - return self.build_gallery_images_and_albums(gallery_favorites) + return build_gallery_images_and_albums(gallery_favorites) def get_account_favorites(self, username): self.validate_user_context(username) favorites = self.make_request('GET', 'account/%s/favorites' % username) - return self.build_gallery_images_and_albums(favorites) + 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 self.build_gallery_images_and_albums(submissions) + return build_gallery_images_and_albums(submissions) def get_account_settings(self, username): self.logged_in() @@ -303,9 +295,7 @@ def delete_comment(self, comment_id): def get_comment_replies(self, comment_id): replies = self.make_request('GET', 'comment/%d/replies' % comment_id) - replies['children'] = build_comment_tree(replies['children']) - - return replies + return format_comment_tree(replies) def post_comment_reply(self, comment_id, image_id, comment): self.logged_in() @@ -434,7 +424,7 @@ def gallery(self, section='hot', sort='viral', page=0, window='day', show_viral= response = self.make_request('GET', 'gallery/%s/%s/%d?showViral=%s' % (section, sort, page, str(show_viral).lower())) - return self.build_gallery_images_and_albums(response) + return build_gallery_images_and_albums(response) def memes_subgallery(self, sort='viral', page=0, window='week'): if sort == 'top': @@ -442,11 +432,11 @@ def memes_subgallery(self, sort='viral', page=0, window='week'): else: response = self.make_request('GET', 'g/memes/%s/%d' % (sort, page)) - return self.build_gallery_images_and_albums(response) + 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 self.build_gallery_images_and_albums(item) + return build_gallery_images_and_albums(item) def subreddit_gallery(self, subreddit, sort='time', window='week', page=0): if sort == 'top': @@ -454,4 +444,94 @@ def subreddit_gallery(self, subreddit, sort='time', window='week', page=0): else: response = self.make_request('GET', 'gallery/r/%s/%s/%d' % (subreddit, sort, page)) - return self.build_gallery_images_and_albums(response) + 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=1): + 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) \ No newline at end of file diff --git a/imgur-python/helpers/error.py b/imgur-python/helpers/error.py index 559d128..f385b28 100644 --- a/imgur-python/helpers/error.py +++ b/imgur-python/helpers/error.py @@ -7,4 +7,9 @@ def __str__(self): if self.status_code: return "(%s) %s" % (self.status_code, self.error_message) else: - return self.error_message \ No newline at end of file + return self.error_message + + +class ImgurClientRateLimitError(Exception): + def __str__(self): + return 'Rate-limit exceeded!' diff --git a/imgur-python/helpers/format.py b/imgur-python/helpers/format.py index bc9812d..0da3451 100644 --- a/imgur-python/helpers/format.py +++ b/imgur-python/helpers/format.py @@ -1,5 +1,7 @@ import math from imgur.models.comment import Comment +from imgur.models.gallery_album import GalleryAlbum +from imgur.models.gallery_image import GalleryImage def center_pad(s, length): @@ -23,3 +25,34 @@ def build_comment_tree(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 diff --git a/imgur-python/imgur/models/tag.py b/imgur-python/imgur/models/tag.py new file mode 100644 index 0000000..c219f6a --- /dev/null +++ b/imgur-python/imgur/models/tag.py @@ -0,0 +1,13 @@ +from gallery_album import GalleryAlbum +from gallery_image import GalleryImage + + +class Tag: + + 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 \ No newline at end of file diff --git a/imgur-python/imgur/models/tag_vote.py b/imgur-python/imgur/models/tag_vote.py new file mode 100644 index 0000000..75bf3ab --- /dev/null +++ b/imgur-python/imgur/models/tag_vote.py @@ -0,0 +1,7 @@ +class TagVote: + + def __init__(self, ups, downs, name, author): + self.ups = ups + self.downs = downs + self.name = name + self.author = author \ No newline at end of file From 8a9abf7a49a4bd65cc63e2225a7e86dff53b36be Mon Sep 17 00:00:00 2001 From: jasdev Date: Sat, 4 Oct 2014 20:45:21 -0700 Subject: [PATCH 28/89] Finished image functionality --- imgur-python/client.py | 66 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 12 deletions(-) diff --git a/imgur-python/client.py b/imgur-python/client.py index be9327f..d986f11 100644 --- a/imgur-python/client.py +++ b/imgur-python/client.py @@ -1,3 +1,4 @@ +import base64 import requests from imgur.models.tag import Tag from imgur.models.album import Album @@ -60,6 +61,14 @@ class ImgurClient: '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): self.client_id = client_id self.client_secret = client_secret @@ -74,8 +83,8 @@ def set_user_auth(self, access_token, refresh_token): def get_client_id(self): return self.client_id - def prepare_headers(self): - if self.auth is None: + def prepare_headers(self, force_anon=False): + if force_anon or self.auth is None: if self.client_id is None: raise ImgurClientError('Client credentials not found!') else: @@ -83,11 +92,11 @@ def prepare_headers(self): else: return {'Authorization': 'Bearer %s' % self.auth.get_current_access_token()} - def make_request(self, method, route, data=None): + def make_request(self, method, route, data=None, force_anon=False): method = method.lower() method_to_call = getattr(requests, method) - header = self.prepare_headers() + header = self.prepare_headers(force_anon) url = API_URL + '3/%s' % route if method in ('delete', 'get'): @@ -174,12 +183,7 @@ def get_account_settings(self, username): ) def change_account_settings(self, username, fields): - allowed_fields = { - 'bio', 'public_images', 'messaging_enabled', 'album_privacy', 'accepted_gallery_terms', 'username' - } - - post_data = {setting: fields[setting] for setting in set(allowed_fields).intersection(fields.keys())} - + 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): @@ -200,7 +204,7 @@ def get_account_albums(self, username, page=0): 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)) + return self.make_request('GET', 'account/%s/albums/ids/%d' % (username, page)) def get_account_album_count(self, username): self.validate_user_context(username) @@ -534,4 +538,42 @@ 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) \ No newline at end of file + 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): + if not config: config = dict() + + fd = open(path, 'rb') + 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) \ No newline at end of file From a7de3f086b147f9145481ec774708014f6ce6754 Mon Sep 17 00:00:00 2001 From: jasdev Date: Sun, 5 Oct 2014 00:07:53 -0700 Subject: [PATCH 29/89] Finsihed all endpoints, still need docs and pypi updates --- imgur-python/client.py | 70 ++++++++++++++++++++++- imgur-python/helpers/format.py | 39 +++++++++++++ imgur-python/imgur/models/conversation.py | 27 +++++++++ imgur-python/imgur/models/message.py | 10 ++++ imgur-python/imgur/models/notification.py | 7 +++ 5 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 imgur-python/imgur/models/conversation.py create mode 100644 imgur-python/imgur/models/message.py create mode 100644 imgur-python/imgur/models/notification.py diff --git a/imgur-python/client.py b/imgur-python/client.py index d986f11..0274967 100644 --- a/imgur-python/client.py +++ b/imgur-python/client.py @@ -7,7 +7,10 @@ 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 @@ -576,4 +579,69 @@ def delete_image(self, image_id): def favorite_image(self, image_id): self.logged_in() - return self.make_request('POST', 'image/%s/favorite' % image_id) \ No newline at end of file + 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)) + + def default_memes(self): + response = self.make_request('GET', 'memegen/defaults') + return [Image(meme) for meme in response] \ No newline at end of file diff --git a/imgur-python/helpers/format.py b/imgur-python/helpers/format.py index 0da3451..87fe1d6 100644 --- a/imgur-python/helpers/format.py +++ b/imgur-python/helpers/format.py @@ -2,6 +2,7 @@ from imgur.models.comment import Comment from imgur.models.gallery_album import GalleryAlbum from imgur.models.gallery_image import GalleryImage +from imgur.models.notification import Notification def center_pad(s, length): @@ -56,3 +57,41 @@ def build_gallery_images_and_albums(response): 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 \ No newline at end of file diff --git a/imgur-python/imgur/models/conversation.py b/imgur-python/imgur/models/conversation.py new file mode 100644 index 0000000..7d3c9cc --- /dev/null +++ b/imgur-python/imgur/models/conversation.py @@ -0,0 +1,27 @@ +from message import Message + +class Conversation: + + def __init__(self, id, last_message_preview, datetime, with_account_id, with_account, message_count, messages=None, + done=None, page=None): + self.id = 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 \ No newline at end of file diff --git a/imgur-python/imgur/models/message.py b/imgur-python/imgur/models/message.py new file mode 100644 index 0000000..d8e1da0 --- /dev/null +++ b/imgur-python/imgur/models/message.py @@ -0,0 +1,10 @@ +class Message: + + def __init__(self, id, from_user, account_id, sender_id, body, conversation_id, datetime): + self.id = 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 \ No newline at end of file diff --git a/imgur-python/imgur/models/notification.py b/imgur-python/imgur/models/notification.py new file mode 100644 index 0000000..8fd9b58 --- /dev/null +++ b/imgur-python/imgur/models/notification.py @@ -0,0 +1,7 @@ +class Notification: + + def __init__(self, id, account_id, viewed, content): + self.id = id + self.account_id = account_id + self.viewed = viewed + self.content = content \ No newline at end of file From 5a5933f4ac5b3ba29e1b1a2ef2629da9d67d67ab Mon Sep 17 00:00:00 2001 From: jasdev Date: Sun, 5 Oct 2014 00:18:05 -0700 Subject: [PATCH 30/89] PEP8 newlines --- imgur-python/client.py | 2 +- imgur-python/helpers/format.py | 2 +- imgur-python/imgur/imgur.py | 2 +- imgur-python/imgur/models/account.py | 2 +- imgur-python/imgur/models/account_settings.py | 2 +- imgur-python/imgur/models/album.py | 2 +- imgur-python/imgur/models/comment.py | 2 +- imgur-python/imgur/models/conversation.py | 2 +- imgur-python/imgur/models/custom_gallery.py | 2 +- imgur-python/imgur/models/gallery_album.py | 2 +- imgur-python/imgur/models/gallery_image.py | 2 +- imgur-python/imgur/models/image.py | 2 +- imgur-python/imgur/models/message.py | 2 +- imgur-python/imgur/models/notification.py | 2 +- imgur-python/imgur/models/tag.py | 2 +- imgur-python/imgur/models/tag_vote.py | 2 +- imgur-python/imgur/ratelimit.py | 2 +- imgur-python/main.py | 2 +- 18 files changed, 18 insertions(+), 18 deletions(-) diff --git a/imgur-python/client.py b/imgur-python/client.py index 0274967..9542a9f 100644 --- a/imgur-python/client.py +++ b/imgur-python/client.py @@ -644,4 +644,4 @@ def mark_notifications_as_read(self, notification_ids): def default_memes(self): response = self.make_request('GET', 'memegen/defaults') - return [Image(meme) for meme in response] \ No newline at end of file + return [Image(meme) for meme in response] diff --git a/imgur-python/helpers/format.py b/imgur-python/helpers/format.py index 87fe1d6..5ff9a02 100644 --- a/imgur-python/helpers/format.py +++ b/imgur-python/helpers/format.py @@ -94,4 +94,4 @@ def build_notification(item): if 'comment' in notification.content: notification.content = format_comment_tree(item['content']) - return notification \ No newline at end of file + return notification diff --git a/imgur-python/imgur/imgur.py b/imgur-python/imgur/imgur.py index 5c95440..63a04cd 100644 --- a/imgur-python/imgur/imgur.py +++ b/imgur-python/imgur/imgur.py @@ -48,4 +48,4 @@ def retrieve(self, request): if res['success'] is not True: raise Exception(res['data']['error']['message']) - return res['data'] \ No newline at end of file + return res['data'] diff --git a/imgur-python/imgur/models/account.py b/imgur-python/imgur/models/account.py index e5006c9..4f1f9a7 100644 --- a/imgur-python/imgur/models/account.py +++ b/imgur-python/imgur/models/account.py @@ -6,4 +6,4 @@ def __init__(self, id, url, bio, reputation, created, pro_expiration): self.bio = bio self.reputation = reputation self.created = created, - self.pro_expiration = pro_expiration \ No newline at end of file + self.pro_expiration = pro_expiration diff --git a/imgur-python/imgur/models/account_settings.py b/imgur-python/imgur/models/account_settings.py index af2ee3b..93d2da5 100644 --- a/imgur-python/imgur/models/account_settings.py +++ b/imgur-python/imgur/models/account_settings.py @@ -10,4 +10,4 @@ def __init__(self, email, high_quality, public_images, album_privacy, pro_expira self.accepted_gallery_terms = accepted_gallery_terms self.active_emails = active_emails self.messaging_enabled = messaging_enabled - self.blocked_users = blocked_users \ No newline at end of file + self.blocked_users = blocked_users diff --git a/imgur-python/imgur/models/album.py b/imgur-python/imgur/models/album.py index ddea227..6c49507 100644 --- a/imgur-python/imgur/models/album.py +++ b/imgur-python/imgur/models/album.py @@ -6,4 +6,4 @@ def __init__(self, *initial_data, **kwargs): for key in dictionary: setattr(self, key, dictionary[key]) for key in kwargs: - setattr(self, key, kwargs[key]) \ No newline at end of file + setattr(self, key, kwargs[key]) diff --git a/imgur-python/imgur/models/comment.py b/imgur-python/imgur/models/comment.py index b00282f..49e343c 100644 --- a/imgur-python/imgur/models/comment.py +++ b/imgur-python/imgur/models/comment.py @@ -6,4 +6,4 @@ def __init__(self, *initial_data, **kwargs): for key in dictionary: setattr(self, key, dictionary[key]) for key in kwargs: - setattr(self, key, kwargs[key]) \ No newline at end of file + setattr(self, key, kwargs[key]) diff --git a/imgur-python/imgur/models/conversation.py b/imgur-python/imgur/models/conversation.py index 7d3c9cc..06492d1 100644 --- a/imgur-python/imgur/models/conversation.py +++ b/imgur-python/imgur/models/conversation.py @@ -24,4 +24,4 @@ def __init__(self, id, last_message_preview, datetime, with_account_id, with_acc message['datetime'], ) for message in messages] else: - self.messages = None \ No newline at end of file + self.messages = None diff --git a/imgur-python/imgur/models/custom_gallery.py b/imgur-python/imgur/models/custom_gallery.py index 8c3fc67..2ca6e1d 100644 --- a/imgur-python/imgur/models/custom_gallery.py +++ b/imgur-python/imgur/models/custom_gallery.py @@ -13,4 +13,4 @@ def __init__(self, id, name, datetime, account_url, link, tags, item_count=None, 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 \ No newline at end of file + if items else None diff --git a/imgur-python/imgur/models/gallery_album.py b/imgur-python/imgur/models/gallery_album.py index 39f74e1..e947410 100644 --- a/imgur-python/imgur/models/gallery_album.py +++ b/imgur-python/imgur/models/gallery_album.py @@ -6,4 +6,4 @@ def __init__(self, *initial_data, **kwargs): for key in dictionary: setattr(self, key, dictionary[key]) for key in kwargs: - setattr(self, key, kwargs[key]) \ No newline at end of file + setattr(self, key, kwargs[key]) diff --git a/imgur-python/imgur/models/gallery_image.py b/imgur-python/imgur/models/gallery_image.py index cae69a5..73cc7ff 100644 --- a/imgur-python/imgur/models/gallery_image.py +++ b/imgur-python/imgur/models/gallery_image.py @@ -6,4 +6,4 @@ def __init__(self, *initial_data, **kwargs): for key in dictionary: setattr(self, key, dictionary[key]) for key in kwargs: - setattr(self, key, kwargs[key]) \ No newline at end of file + setattr(self, key, kwargs[key]) diff --git a/imgur-python/imgur/models/image.py b/imgur-python/imgur/models/image.py index 56b743c..bd02b0c 100644 --- a/imgur-python/imgur/models/image.py +++ b/imgur-python/imgur/models/image.py @@ -6,4 +6,4 @@ def __init__(self, *initial_data, **kwargs): for key in dictionary: setattr(self, key, dictionary[key]) for key in kwargs: - setattr(self, key, kwargs[key]) \ No newline at end of file + setattr(self, key, kwargs[key]) diff --git a/imgur-python/imgur/models/message.py b/imgur-python/imgur/models/message.py index d8e1da0..185a0f4 100644 --- a/imgur-python/imgur/models/message.py +++ b/imgur-python/imgur/models/message.py @@ -7,4 +7,4 @@ def __init__(self, id, from_user, account_id, sender_id, body, conversation_id, self.sender_id = sender_id self.body = body self.conversation_id = conversation_id - self.datetime = datetime \ No newline at end of file + self.datetime = datetime diff --git a/imgur-python/imgur/models/notification.py b/imgur-python/imgur/models/notification.py index 8fd9b58..3da26b6 100644 --- a/imgur-python/imgur/models/notification.py +++ b/imgur-python/imgur/models/notification.py @@ -4,4 +4,4 @@ def __init__(self, id, account_id, viewed, content): self.id = id self.account_id = account_id self.viewed = viewed - self.content = content \ No newline at end of file + self.content = content diff --git a/imgur-python/imgur/models/tag.py b/imgur-python/imgur/models/tag.py index c219f6a..67370a8 100644 --- a/imgur-python/imgur/models/tag.py +++ b/imgur-python/imgur/models/tag.py @@ -10,4 +10,4 @@ def __init__(self, name, followers, total_items, following, items): 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 \ No newline at end of file + if items else None diff --git a/imgur-python/imgur/models/tag_vote.py b/imgur-python/imgur/models/tag_vote.py index 75bf3ab..6ac444f 100644 --- a/imgur-python/imgur/models/tag_vote.py +++ b/imgur-python/imgur/models/tag_vote.py @@ -4,4 +4,4 @@ def __init__(self, ups, downs, name, author): self.ups = ups self.downs = downs self.name = name - self.author = author \ No newline at end of file + self.author = author diff --git a/imgur-python/imgur/ratelimit.py b/imgur-python/imgur/ratelimit.py index b9d8487..d00debc 100644 --- a/imgur-python/imgur/ratelimit.py +++ b/imgur-python/imgur/ratelimit.py @@ -31,4 +31,4 @@ def __str__(self, time=None): exp = int(self.user_reset) - int(time) return '' % \ - (self.client_remaining, self.user_remaining, exp) \ No newline at end of file + (self.client_remaining, self.user_remaining, exp) diff --git a/imgur-python/main.py b/imgur-python/main.py index f749026..17bded6 100644 --- a/imgur-python/main.py +++ b/imgur-python/main.py @@ -231,4 +231,4 @@ def handle_unauthorized_commands(factory, action): if __name__ == "__main__": - main() \ No newline at end of file + main() From f7f2aeb852e16a01c8d13b26708e72afd51e560a Mon Sep 17 00:00:00 2001 From: jasdev Date: Mon, 6 Oct 2014 22:15:37 -0700 Subject: [PATCH 31/89] Docs, refactoring and cleaning --- imgur-python/docs/LICENSE => LICENSE.txt | 0 README.md | 2 +- imgur-python/imgur/models/__init__.py | 0 imgurpython/__init__.py | 1 + {imgur-python => imgurpython}/client.py | 28 ++++++++++++------ .../data/config.json.sample | 0 .../data/res/corgi.jpg | Bin .../data/res/error.jpg | 0 .../helpers}/__init__.py | 0 .../helpers/error.py | 0 .../helpers/format.py | 0 .../helpers => imgurpython/imgur}/__init__.py | 0 .../imgur/auth}/__init__.py | 0 .../imgur/auth/accesstoken.py | 0 .../imgur/auth/anonymous.py | 0 .../imgur/auth/base.py | 0 .../imgur/auth/expired.py | 0 .../imgur/factory.py | 0 {imgur-python => imgurpython}/imgur/imgur.py | 0 .../imgur/models}/__init__.py | 0 .../imgur/models/account.py | 0 .../imgur/models/account_settings.py | 0 .../imgur/models/album.py | 0 .../imgur/models/comment.py | 0 .../imgur/models/conversation.py | 0 .../imgur/models/custom_gallery.py | 0 .../imgur/models/gallery_album.py | 0 .../imgur/models/gallery_image.py | 0 .../imgur/models/image.py | 0 .../imgur/models/message.py | 0 .../imgur/models/notification.py | 0 .../imgur/models/tag.py | 0 .../imgur/models/tag_vote.py | 0 .../imgur/ratelimit.py | 0 {imgur-python => imgurpython}/main.py | 0 setup.cfg | 2 ++ setup.py | 6 ++-- 37 files changed, 26 insertions(+), 13 deletions(-) rename imgur-python/docs/LICENSE => LICENSE.txt (100%) delete mode 100644 imgur-python/imgur/models/__init__.py create mode 100644 imgurpython/__init__.py rename {imgur-python => imgurpython}/client.py (95%) rename {imgur-python => imgurpython}/data/config.json.sample (100%) rename {imgur-python => imgurpython}/data/res/corgi.jpg (100%) rename {imgur-python => imgurpython}/data/res/error.jpg (100%) rename {imgur-python => imgurpython/helpers}/__init__.py (100%) rename {imgur-python => imgurpython}/helpers/error.py (100%) rename {imgur-python => imgurpython}/helpers/format.py (100%) rename {imgur-python/helpers => imgurpython/imgur}/__init__.py (100%) rename {imgur-python/imgur => imgurpython/imgur/auth}/__init__.py (100%) rename {imgur-python => imgurpython}/imgur/auth/accesstoken.py (100%) rename {imgur-python => imgurpython}/imgur/auth/anonymous.py (100%) rename {imgur-python => imgurpython}/imgur/auth/base.py (100%) rename {imgur-python => imgurpython}/imgur/auth/expired.py (100%) rename {imgur-python => imgurpython}/imgur/factory.py (100%) rename {imgur-python => imgurpython}/imgur/imgur.py (100%) rename {imgur-python/imgur/auth => imgurpython/imgur/models}/__init__.py (100%) rename {imgur-python => imgurpython}/imgur/models/account.py (100%) rename {imgur-python => imgurpython}/imgur/models/account_settings.py (100%) rename {imgur-python => imgurpython}/imgur/models/album.py (100%) rename {imgur-python => imgurpython}/imgur/models/comment.py (100%) rename {imgur-python => imgurpython}/imgur/models/conversation.py (100%) rename {imgur-python => imgurpython}/imgur/models/custom_gallery.py (100%) rename {imgur-python => imgurpython}/imgur/models/gallery_album.py (100%) rename {imgur-python => imgurpython}/imgur/models/gallery_image.py (100%) rename {imgur-python => imgurpython}/imgur/models/image.py (100%) rename {imgur-python => imgurpython}/imgur/models/message.py (100%) rename {imgur-python => imgurpython}/imgur/models/notification.py (100%) rename {imgur-python => imgurpython}/imgur/models/tag.py (100%) rename {imgur-python => imgurpython}/imgur/models/tag_vote.py (100%) rename {imgur-python => imgurpython}/imgur/ratelimit.py (100%) rename {imgur-python => imgurpython}/main.py (100%) create mode 100644 setup.cfg diff --git a/imgur-python/docs/LICENSE b/LICENSE.txt similarity index 100% rename from imgur-python/docs/LICENSE rename to LICENSE.txt diff --git a/README.md b/README.md index c3120f4..66dd8c3 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Installation Configuration ------------- - +` Configuration is done through the **config.json** (placed in `imgur-python/data`) file in JSON format. The contents of the file should be a JSON object with the following properties: diff --git a/imgur-python/imgur/models/__init__.py b/imgur-python/imgur/models/__init__.py deleted file mode 100644 index e69de29..0000000 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/imgur-python/client.py b/imgurpython/client.py similarity index 95% rename from imgur-python/client.py rename to imgurpython/client.py index 9542a9f..6037b47 100644 --- a/imgur-python/client.py +++ b/imgurpython/client.py @@ -86,6 +86,17 @@ def set_user_auth(self, access_token, refresh_token): def get_client_id(self): return self.client_id + 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, + grant_type: response + }, True) + def prepare_headers(self, force_anon=False): if force_anon or self.auth is None: if self.client_id is None: @@ -100,7 +111,7 @@ def make_request(self, method, route, data=None, force_anon=False): method_to_call = getattr(requests, method) header = self.prepare_headers(force_anon) - url = API_URL + '3/%s' % route + url = 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) @@ -124,10 +135,10 @@ def make_request(self, method, route, data=None, force_anon=False): except: raise ImgurClientError('JSON decoding of response failed.') - if isinstance(response_data['data'], dict) and 'error' in response_data['data']: + 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'] + 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: @@ -237,9 +248,9 @@ 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, page=0): + def get_account_images_count(self, username): self.validate_user_context(username) - return self.make_request('GET', 'account/%s/images/ids/%d' % (username, page)) + return self.make_request('GET', 'account/%s/images/count' % username) # Album-related endpoints def get_album(self, album_id): @@ -313,11 +324,9 @@ def post_comment_reply(self, comment_id, image_id, comment): return self.make_request('POST', 'comment/%d' % comment_id, data) - def comment_vote(self, comment_id, vote, toggle=True): + def comment_vote(self, comment_id, vote='up'): self.logged_in() - toggle_behavior = 1 if toggle else 0 - - return self.make_request('POST', 'comment/%d/vote/%s?toggle=%d' % (comment_id, vote, toggle_behavior)) + return self.make_request('POST', 'comment/%d/vote/%s' % (comment_id, vote)) def comment_report(self, comment_id): self.logged_in() @@ -642,6 +651,7 @@ 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/imgur-python/data/config.json.sample b/imgurpython/data/config.json.sample similarity index 100% rename from imgur-python/data/config.json.sample rename to imgurpython/data/config.json.sample diff --git a/imgur-python/data/res/corgi.jpg b/imgurpython/data/res/corgi.jpg similarity index 100% rename from imgur-python/data/res/corgi.jpg rename to imgurpython/data/res/corgi.jpg diff --git a/imgur-python/data/res/error.jpg b/imgurpython/data/res/error.jpg similarity index 100% rename from imgur-python/data/res/error.jpg rename to imgurpython/data/res/error.jpg diff --git a/imgur-python/__init__.py b/imgurpython/helpers/__init__.py similarity index 100% rename from imgur-python/__init__.py rename to imgurpython/helpers/__init__.py diff --git a/imgur-python/helpers/error.py b/imgurpython/helpers/error.py similarity index 100% rename from imgur-python/helpers/error.py rename to imgurpython/helpers/error.py diff --git a/imgur-python/helpers/format.py b/imgurpython/helpers/format.py similarity index 100% rename from imgur-python/helpers/format.py rename to imgurpython/helpers/format.py diff --git a/imgur-python/helpers/__init__.py b/imgurpython/imgur/__init__.py similarity index 100% rename from imgur-python/helpers/__init__.py rename to imgurpython/imgur/__init__.py diff --git a/imgur-python/imgur/__init__.py b/imgurpython/imgur/auth/__init__.py similarity index 100% rename from imgur-python/imgur/__init__.py rename to imgurpython/imgur/auth/__init__.py diff --git a/imgur-python/imgur/auth/accesstoken.py b/imgurpython/imgur/auth/accesstoken.py similarity index 100% rename from imgur-python/imgur/auth/accesstoken.py rename to imgurpython/imgur/auth/accesstoken.py diff --git a/imgur-python/imgur/auth/anonymous.py b/imgurpython/imgur/auth/anonymous.py similarity index 100% rename from imgur-python/imgur/auth/anonymous.py rename to imgurpython/imgur/auth/anonymous.py diff --git a/imgur-python/imgur/auth/base.py b/imgurpython/imgur/auth/base.py similarity index 100% rename from imgur-python/imgur/auth/base.py rename to imgurpython/imgur/auth/base.py diff --git a/imgur-python/imgur/auth/expired.py b/imgurpython/imgur/auth/expired.py similarity index 100% rename from imgur-python/imgur/auth/expired.py rename to imgurpython/imgur/auth/expired.py diff --git a/imgur-python/imgur/factory.py b/imgurpython/imgur/factory.py similarity index 100% rename from imgur-python/imgur/factory.py rename to imgurpython/imgur/factory.py diff --git a/imgur-python/imgur/imgur.py b/imgurpython/imgur/imgur.py similarity index 100% rename from imgur-python/imgur/imgur.py rename to imgurpython/imgur/imgur.py diff --git a/imgur-python/imgur/auth/__init__.py b/imgurpython/imgur/models/__init__.py similarity index 100% rename from imgur-python/imgur/auth/__init__.py rename to imgurpython/imgur/models/__init__.py diff --git a/imgur-python/imgur/models/account.py b/imgurpython/imgur/models/account.py similarity index 100% rename from imgur-python/imgur/models/account.py rename to imgurpython/imgur/models/account.py diff --git a/imgur-python/imgur/models/account_settings.py b/imgurpython/imgur/models/account_settings.py similarity index 100% rename from imgur-python/imgur/models/account_settings.py rename to imgurpython/imgur/models/account_settings.py diff --git a/imgur-python/imgur/models/album.py b/imgurpython/imgur/models/album.py similarity index 100% rename from imgur-python/imgur/models/album.py rename to imgurpython/imgur/models/album.py diff --git a/imgur-python/imgur/models/comment.py b/imgurpython/imgur/models/comment.py similarity index 100% rename from imgur-python/imgur/models/comment.py rename to imgurpython/imgur/models/comment.py diff --git a/imgur-python/imgur/models/conversation.py b/imgurpython/imgur/models/conversation.py similarity index 100% rename from imgur-python/imgur/models/conversation.py rename to imgurpython/imgur/models/conversation.py diff --git a/imgur-python/imgur/models/custom_gallery.py b/imgurpython/imgur/models/custom_gallery.py similarity index 100% rename from imgur-python/imgur/models/custom_gallery.py rename to imgurpython/imgur/models/custom_gallery.py diff --git a/imgur-python/imgur/models/gallery_album.py b/imgurpython/imgur/models/gallery_album.py similarity index 100% rename from imgur-python/imgur/models/gallery_album.py rename to imgurpython/imgur/models/gallery_album.py diff --git a/imgur-python/imgur/models/gallery_image.py b/imgurpython/imgur/models/gallery_image.py similarity index 100% rename from imgur-python/imgur/models/gallery_image.py rename to imgurpython/imgur/models/gallery_image.py diff --git a/imgur-python/imgur/models/image.py b/imgurpython/imgur/models/image.py similarity index 100% rename from imgur-python/imgur/models/image.py rename to imgurpython/imgur/models/image.py diff --git a/imgur-python/imgur/models/message.py b/imgurpython/imgur/models/message.py similarity index 100% rename from imgur-python/imgur/models/message.py rename to imgurpython/imgur/models/message.py diff --git a/imgur-python/imgur/models/notification.py b/imgurpython/imgur/models/notification.py similarity index 100% rename from imgur-python/imgur/models/notification.py rename to imgurpython/imgur/models/notification.py diff --git a/imgur-python/imgur/models/tag.py b/imgurpython/imgur/models/tag.py similarity index 100% rename from imgur-python/imgur/models/tag.py rename to imgurpython/imgur/models/tag.py diff --git a/imgur-python/imgur/models/tag_vote.py b/imgurpython/imgur/models/tag_vote.py similarity index 100% rename from imgur-python/imgur/models/tag_vote.py rename to imgurpython/imgur/models/tag_vote.py diff --git a/imgur-python/imgur/ratelimit.py b/imgurpython/imgur/ratelimit.py similarity index 100% rename from imgur-python/imgur/ratelimit.py rename to imgurpython/imgur/ratelimit.py diff --git a/imgur-python/main.py b/imgurpython/main.py similarity index 100% rename from imgur-python/main.py rename to imgurpython/main.py 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 index 0d2cbcb..53eb7db 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ # 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.0.2', + version='1.1', description='Official Imgur python library with OAuth2 and samples', long_description='', @@ -52,7 +52,7 @@ ], # What does your project relate to? - keywords='sample setuptools development imgur', + keywords=['python', 'imgur', 'client'], # You can just specify the packages manually here if your project is # simple. Or you can use find_packages(). @@ -62,7 +62,7 @@ # 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=[], + 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 From e401319dfb29638c07b6a6fd0bd02985cd0472da Mon Sep 17 00:00:00 2001 From: jasdev Date: Mon, 6 Oct 2014 22:17:26 -0700 Subject: [PATCH 32/89] Readme updates --- README.md | 395 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 363 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 66dd8c3..2ef044b 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,24 @@ imgurpython =========== -A Python client for the [Imgur API](http://api.imgur.com/). Also includes a friendly demo application. It can be used to +A Python client for the [Imgur API](http//api.imgur.com/). Also includes a friendly demo application. It can be used to interact with the Imgur API and examine its responses, as a command line utility, and it can be used as a library within 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 +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. Imgur API Documentation ----------------------- -Our developer documentation can be found [here](https://api.imgur.com/). +Our developer documentation can be found [here](https//api.imgur.com/). Community --------- The best way to reach out to Imgur for API support would be our -[Google Group](https://groups.google.com/forum/#!forum/imgur), [Twitter](https://twitter.com/imgurapi), or via +[Google Group](https//groups.google.com/forum/#!forum/imgur), [Twitter](https//twitter.com/imgurapi), or via api@imgur.com. Installation @@ -26,82 +26,413 @@ 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 + try + ... + except ImgurClientError as e + print e.error_message + print e.status_code + ``` +* ImgurClientRateLimitError - Rate limit error + +## ImgurClient Functions + +### Account + +* ```python + get_account(username) + ``` +* ```python + get_gallery_favorites(username) + ``` +* ```python + get_account_favorites(username) + ``` +* ```python + get_account_submissions(username, page=0) + ``` +* ```python + get_account_settings(username) + ``` +* ```python + change_account_settings(username, fields) + ``` +* ```python + get_email_verification_status(username) + ``` +* ```python + send_verification_email(username) + ``` +* ```python + get_account_albums(username, page=0) + ``` +* ```python + get_account_album_ids(username, page=0) + ``` +* ```python + get_account_album_count(username) + ``` +* ```python + get_account_comments(username, sort='newest', page=0) + ``` +* ```python + get_account_comment_ids(username, sort='newest', page=0) + ``` +* ```python + get_account_comment_count(username) + ``` +* ```python + get_account_images(username, page=0) + ``` +* ```python + get_account_image_ids(username, page=0) + ``` +* ```python + get_account_album_count(username) + ``` + +### Album +* ```python + get_album(album_id) + ``` +* ```python + get_album_images(album_id) + ``` +* ```python + create_album(fields) + ``` +* ```python + update_album(album_id, fields) + ``` +* ```python + album_delete(album_id) + ``` +* ```python + album_favorite(album_id) + ``` +* ```python + album_set_images(album_id, ids) + ``` +* ```python + album_add_images(album_id, ids) + ``` +* ```python + album_remove_images(album_id, ids) + ``` + +### Comment +* ```python + get_comment(comment_id) + ``` +* ```python + delete_comment(comment_id) + ``` +* ```python + create_album(fields) + ``` +* ```python + get_comment_replies(comment_id) + ``` +* ```python + post_comment_reply(comment_id, image_id, comment) + ``` +* ```python + comment_vote(comment_id, vote='up') + ``` +* ```python + comment_report(comment_id) + ``` + +### Custom Gallery + +* ```python + get_custom_gallery(gallery_id, sort='viral', window='week', page=0) + ``` +* ```python + get_user_galleries() + ``` +* ```python + create_custom_gallery(name, tags=None) + ``` +* ```python + custom_gallery_update(gallery_id, name) + ``` +* ```python + custom_gallery_add_tags(gallery_id, tags) + ``` +* ```python + custom_gallery_remove_tags(gallery_id, tags) + ``` +* ```python + custom_gallery_delete(gallery_id) + ``` +* ```python + filtered_out_tags() + ``` +* ```python + block_tag(tag) + ``` +* ```python + unblock_tag(tag) + ``` + +### Gallery + +* ```python + gallery(section='hot', sort='viral', page=0, window='day', show_viral=True) + ``` +* ```python + memes_subgallery(sort='viral', page=0, window='week') + ``` +* ```python + memes_subgallery_image(item_id) + ``` +* ```python + subreddit_gallery(subreddit, sort='time', window='week', page=0) + ``` +* ```python + subreddit_image(subreddit, image_id) + ``` +* ```python + gallery_tag(tag, sort='viral', page=0, window='week') + ``` +* ```python + gallery_tag_image(tag, item_id) + ``` +* ```python + gallery_item_tags(item_id) + ``` +* ```python + gallery_tag_vote(item_id, tag, vote) + ``` +* ```python + gallery_search(q, advanced=None, sort='time', window='all', page=0) + ``` +* ```python + gallery_random(page=0) + ``` +* ```python + share_on_imgur(item_id, title, terms=1) + ``` +* ```python + remove_from_gallery(item_id) + ``` +* ```python + gallery_item(item_id) + ``` +* ```python + report_gallery_item(item_id) + ``` +* ```python + gallery_item_vote(item_id, vote='up') + ``` +* ```python + gallery_item_comments(item_id, sort='best') + ``` +* ```python + gallery_comment(item_id, comment) + ``` +* ```python + gallery_comment_ids(item_id) + ``` +* ```python + gallery_comment_count(item_id) + ``` + +### Image + +* ```python + get_image(image_id) + ``` +* ```python + upload_from_path(path, config=None, anon=True) + ``` +* ```python + upload_from_url(url, config=None, anon=True) + ``` +* ```python + delete_image(image_id) + ``` +* ```python + favorite_image(image_id) + ``` + +### Conversation + +* ```python + conversation_list() + ``` +* ```python + get_conversation(conversation_id, page=1, offset=0) + ``` +* ```python + create_message(recipient, body) + ``` +* ```python + delete_conversation(conversation_id) + ``` +* ```python + report_sender(username) + ``` +* ```python + block_sender(username) + ``` + +### Notification + +* ```python + get_notifications(new=True) + ``` +* ```python + get_notification(notification_id) + ``` +* ```python + mark_notifications_as_read(notification_ids) + ``` + +### Memegen + +* ```python + default_memes() + ``` + +Command Line Usage (deprecated) +------------ + Configuration ------------- ` Configuration is done through the **config.json** (placed in `imgur-python/data`) file in JSON format. The contents of the file should be a JSON -object with the following properties: +object with the following properties ### client_id -**Key**: 'client_id' +**Key** 'client_id' -**Type**: string [16 characters] +**Type** string [16 characters] -**Description**: The Client-ID you got when you registered. Required for any API call. +**Description** The Client-ID you got when you registered. Required for any API call. ### secret -**Key**: 'secret' +**Key** 'secret' -**Type**: string [40 characters] +**Type** string [40 characters] -**Description**: The client secret you got when you registered, needed fo OAuth2 authentication. +**Description** The client secret you got when you registered, needed fo OAuth2 authentication. ### token_store -**Key**: 'token_store' +**Key** 'token_store' -**Type**: object +**Type** object -**Description**: Future configuration to control where the tokens are stored for persistent **insecure** storage of refresh tokens. +**Description** Future configuration to control where the tokens are stored for persistent **insecure** storage of refresh tokens. Command Line Usage ------------------ -> Usage: python main.py (action) [options...] +> Usage python main.py (action) [options...] > > ### OAuth Actions -> -> **credits** +> +> **credits** > View the rate limit information for this client > -> **authorize** +> **authorize** > Start the authorization process > -> **authorize [pin]** +> **authorize [pin]** > Get an access token after starting authorization > -> **refresh [refresh-token]** +> **refresh [refresh-token]** > Return a new OAuth access token after it's expired > > ### Unauthorized Actions -> -> **upload [file]** +> +> **upload [file]** > Anonymously upload a file > -> **list-comments [hash]** +> **list-comments [hash]** > Get the comments (raw JSON) for a gallery post > -> **get-album [id]** +> **get-album [id]** > Get information (raw JSON) about an album > -> **get-comment [id]** +> **get-comment [id]** > Get a particular comment (raw JSON) for a gallery comment > -> **get-gallery [hash]** +> **get-gallery [hash]** > Get information (raw JSON) about a gallery post -> +> > ### Authorized Actions -> +> > **upload-auth [access-token] [file]** > Upload a file to your account > > **comment [access-token] [hash] [text]** > Comment on a gallery post > -> **vote-gallery [token] [hash] [direction]** +> **vote-gallery [token] [hash] [direction]** > Vote on a gallery post. Direction can be either 'up', 'down', or 'veto' > -> **vote-comment [token] [id] [direction]** -> Vote on a gallery comment. Direction can be either 'up', 'down', or 'veto' \ No newline at end of file +> **vote-comment [token] [id] [direction]** +> Vote on a gallery comment. Direction can be either 'up', 'down', or 'veto' From 6d3ec9946ecdd54da6ef6bd208b726bd24d6c606 Mon Sep 17 00:00:00 2001 From: jasdev Date: Mon, 6 Oct 2014 22:48:35 -0700 Subject: [PATCH 33/89] redoing function headers --- README.md | 312 ++++++++++++++---------------------------------------- 1 file changed, 78 insertions(+), 234 deletions(-) diff --git a/README.md b/README.md index 2ef044b..2e9a740 100644 --- a/README.md +++ b/README.md @@ -97,262 +97,106 @@ Error types ### Account -* ```python - get_account(username) - ``` -* ```python - get_gallery_favorites(username) - ``` -* ```python - get_account_favorites(username) - ``` -* ```python - get_account_submissions(username, page=0) - ``` -* ```python - get_account_settings(username) - ``` -* ```python - change_account_settings(username, fields) - ``` -* ```python - get_email_verification_status(username) - ``` -* ```python - send_verification_email(username) - ``` -* ```python - get_account_albums(username, page=0) - ``` -* ```python - get_account_album_ids(username, page=0) - ``` -* ```python - get_account_album_count(username) - ``` -* ```python - get_account_comments(username, sort='newest', page=0) - ``` -* ```python - get_account_comment_ids(username, sort='newest', page=0) - ``` -* ```python - get_account_comment_count(username) - ``` -* ```python - get_account_images(username, page=0) - ``` -* ```python - get_account_image_ids(username, page=0) - ``` -* ```python - get_account_album_count(username) - ``` +* `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 -* ```python - get_album(album_id) - ``` -* ```python - get_album_images(album_id) - ``` -* ```python - create_album(fields) - ``` -* ```python - update_album(album_id, fields) - ``` -* ```python - album_delete(album_id) - ``` -* ```python - album_favorite(album_id) - ``` -* ```python - album_set_images(album_id, ids) - ``` -* ```python - album_add_images(album_id, ids) - ``` -* ```python - album_remove_images(album_id, ids) - ``` +* `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 -* ```python - get_comment(comment_id) - ``` -* ```python - delete_comment(comment_id) - ``` -* ```python - create_album(fields) - ``` -* ```python - get_comment_replies(comment_id) - ``` -* ```python - post_comment_reply(comment_id, image_id, comment) - ``` -* ```python - comment_vote(comment_id, vote='up') - ``` -* ```python - comment_report(comment_id) - ``` +* `get_comment(comment_id)` +* `delete_comment(comment_id)` +* `create_album(fields)` +* `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 -* ```python - get_custom_gallery(gallery_id, sort='viral', window='week', page=0) - ``` -* ```python - get_user_galleries() - ``` -* ```python - create_custom_gallery(name, tags=None) - ``` -* ```python - custom_gallery_update(gallery_id, name) - ``` -* ```python - custom_gallery_add_tags(gallery_id, tags) - ``` -* ```python - custom_gallery_remove_tags(gallery_id, tags) - ``` -* ```python - custom_gallery_delete(gallery_id) - ``` -* ```python - filtered_out_tags() - ``` -* ```python - block_tag(tag) - ``` -* ```python - unblock_tag(tag) - ``` +* `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 -* ```python - gallery(section='hot', sort='viral', page=0, window='day', show_viral=True) - ``` -* ```python - memes_subgallery(sort='viral', page=0, window='week') - ``` -* ```python - memes_subgallery_image(item_id) - ``` -* ```python - subreddit_gallery(subreddit, sort='time', window='week', page=0) - ``` -* ```python - subreddit_image(subreddit, image_id) - ``` -* ```python - gallery_tag(tag, sort='viral', page=0, window='week') - ``` -* ```python - gallery_tag_image(tag, item_id) - ``` -* ```python - gallery_item_tags(item_id) - ``` -* ```python - gallery_tag_vote(item_id, tag, vote) - ``` -* ```python - gallery_search(q, advanced=None, sort='time', window='all', page=0) - ``` -* ```python - gallery_random(page=0) - ``` -* ```python - share_on_imgur(item_id, title, terms=1) - ``` -* ```python - remove_from_gallery(item_id) - ``` -* ```python - gallery_item(item_id) - ``` -* ```python - report_gallery_item(item_id) - ``` -* ```python - gallery_item_vote(item_id, vote='up') - ``` -* ```python - gallery_item_comments(item_id, sort='best') - ``` -* ```python - gallery_comment(item_id, comment) - ``` -* ```python - gallery_comment_ids(item_id) - ``` -* ```python - gallery_comment_count(item_id) - ``` +* `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=1)` +* `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 -* ```python - get_image(image_id) - ``` -* ```python - upload_from_path(path, config=None, anon=True) - ``` -* ```python - upload_from_url(url, config=None, anon=True) - ``` -* ```python - delete_image(image_id) - ``` -* ```python - favorite_image(image_id) - ``` +* `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 -* ```python - conversation_list() - ``` -* ```python - get_conversation(conversation_id, page=1, offset=0) - ``` -* ```python - create_message(recipient, body) - ``` -* ```python - delete_conversation(conversation_id) - ``` -* ```python - report_sender(username) - ``` -* ```python - block_sender(username) - ``` +* `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 -* ```python - get_notifications(new=True) - ``` -* ```python - get_notification(notification_id) - ``` -* ```python - mark_notifications_as_read(notification_ids) - ``` +* `get_notifications(new=True)` +* `get_notification(notification_id)` +* `mark_notifications_as_read(notification_ids)` ### Memegen -* ```python - default_memes() - ``` +* `default_memes()` Command Line Usage (deprecated) ------------ From a97e0f84765a99463c312d83ed6c68a3d8cb8e81 Mon Sep 17 00:00:00 2001 From: Jasdev Singh Date: Mon, 6 Oct 2014 22:52:08 -0700 Subject: [PATCH 34/89] Update README.md --- README.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 2e9a740..7b810a0 100644 --- a/README.md +++ b/README.md @@ -84,13 +84,15 @@ from imgurpython import ImgurClient ### Error Handling Error types * ImgurClientError - General error handler, access message and status code via - * ```python - try - ... - except ImgurClientError as e - print e.error_message - print e.status_code - ``` + +```python + try + ... + except ImgurClientError as e + print e.error_message + print e.status_code +``` + * ImgurClientRateLimitError - Rate limit error ## ImgurClient Functions From b281a756c3b5d300ebdbf38e0ab7a2a33b86c973 Mon Sep 17 00:00:00 2001 From: Jasdev Singh Date: Mon, 6 Oct 2014 22:53:21 -0700 Subject: [PATCH 35/89] Update README.md --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 7b810a0..d82e2a6 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,8 @@ To use the client from a strictly anonymous context (no actions on behalf of a u ```python from imgurpython import ImgurClient -client_id = "YOUR CLIENT ID" -client_secret = "YOUR CLIENT SECRET" +client_id = 'YOUR CLIENT ID' +client_secret = 'YOUR CLIENT SECRET' client = ImgurClient(client_id, client_secret) @@ -52,8 +52,8 @@ 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_id = 'YOUR CLIENT ID' + client_secret = 'YOUR CLIENT SECRET' client = ImgurClient(client_id, client_secret) @@ -72,10 +72,10 @@ or if you already have an access/refresh token pair you can simply do 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" + 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) From 0f19e869044bf0fabe00fc7423772974cd071a03 Mon Sep 17 00:00:00 2001 From: Jasdev Singh Date: Mon, 6 Oct 2014 22:55:08 -0700 Subject: [PATCH 36/89] Update README.md --- README.md | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index d82e2a6..33c957c 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ 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' @@ -44,7 +45,7 @@ client = ImgurClient(client_id, client_secret) # Example request items = client.gallery() for item in items - print item.link + print item.link ``` @@ -52,18 +53,19 @@ 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) +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') +# 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) ... +# ... 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']) +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 @@ -72,13 +74,13 @@ or if you already have an access/refresh token pair you can simply do 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' +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) +# 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 @@ -86,11 +88,11 @@ Error types * ImgurClientError - General error handler, access message and status code via ```python - try - ... - except ImgurClientError as e - print e.error_message - print e.status_code +try + ... +except ImgurClientError as e + print e.error_message + print e.status_code ``` * ImgurClientRateLimitError - Rate limit error From 5fe6fb9668928e4d068c84a28c05e920e6e6116e Mon Sep 17 00:00:00 2001 From: Jasdev Singh Date: Mon, 6 Oct 2014 22:55:39 -0700 Subject: [PATCH 37/89] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 33c957c..578d4e3 100644 --- a/README.md +++ b/README.md @@ -89,10 +89,10 @@ Error types ```python try - ... + ... except ImgurClientError as e - print e.error_message - print e.status_code + print e.error_message + print e.status_code ``` * ImgurClientRateLimitError - Rate limit error From 4cdf7b592c4f526116294b3d978bf5a92f1058db Mon Sep 17 00:00:00 2001 From: jasdev Date: Mon, 6 Oct 2014 23:16:26 -0700 Subject: [PATCH 38/89] Fixing imports --- imgurpython/helpers/__init__.py | 4 ++++ imgurpython/helpers/format.py | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/imgurpython/helpers/__init__.py b/imgurpython/helpers/__init__.py index e69de29..6142833 100644 --- a/imgurpython/helpers/__init__.py +++ 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/format.py b/imgurpython/helpers/format.py index 5ff9a02..8c5a3e9 100644 --- a/imgurpython/helpers/format.py +++ b/imgurpython/helpers/format.py @@ -1,8 +1,8 @@ import math -from imgur.models.comment import Comment -from imgur.models.gallery_album import GalleryAlbum -from imgur.models.gallery_image import GalleryImage -from imgur.models.notification import Notification +from ..helpers import Comment +from ..helpers import GalleryAlbum +from ..helpers import GalleryImage +from ..helpers import Notification def center_pad(s, length): From 842c286bf05ff24d9a5f0744fcd8f4daf72186f4 Mon Sep 17 00:00:00 2001 From: Jasdev Singh Date: Mon, 6 Oct 2014 23:21:23 -0700 Subject: [PATCH 39/89] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 578d4e3..6cc546d 100644 --- a/README.md +++ b/README.md @@ -202,7 +202,7 @@ except ImgurClientError as e * `default_memes()` -Command Line Usage (deprecated) +Command Line Demo (deprecated) ------------ Configuration From 549f23864d9ae8cd14f20b8765e3c02e02e80e58 Mon Sep 17 00:00:00 2001 From: jasdev Date: Tue, 7 Oct 2014 11:44:11 -0700 Subject: [PATCH 40/89] Bumping py versions --- setup.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup.py b/setup.py index 53eb7db..23a4368 100644 --- a/setup.py +++ b/setup.py @@ -42,8 +42,6 @@ # 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', - 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.2', From 00be87ed9bea084d04efc3c0e189d2e6d985c316 Mon Sep 17 00:00:00 2001 From: jasdev Date: Tue, 7 Oct 2014 11:48:48 -0700 Subject: [PATCH 41/89] URL swap --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 23a4368..e16d97f 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ long_description='', # The project's main homepage. - url='https://github.com/jacobgreenleaf/imgur-python', + url='https://github.com/Imgur/imgurpython', # Author details author='Imgur Inc.', From 6f878bf4396577e75700cd2b49cc64bf4471c480 Mon Sep 17 00:00:00 2001 From: jasdev Date: Tue, 7 Oct 2014 14:23:51 -0700 Subject: [PATCH 42/89] py3 support --- imgurpython/client.py | 30 +++++++++++----------- imgurpython/imgur/models/conversation.py | 2 +- imgurpython/imgur/models/custom_gallery.py | 4 +-- imgurpython/imgur/models/tag.py | 4 +-- imgurpython/main.py | 4 +-- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/imgurpython/client.py b/imgurpython/client.py index 6037b47..4c93aed 100644 --- a/imgurpython/client.py +++ b/imgurpython/client.py @@ -1,20 +1,20 @@ 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 +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/' diff --git a/imgurpython/imgur/models/conversation.py b/imgurpython/imgur/models/conversation.py index 06492d1..9d14554 100644 --- a/imgurpython/imgur/models/conversation.py +++ b/imgurpython/imgur/models/conversation.py @@ -1,4 +1,4 @@ -from message import Message +from .message import Message class Conversation: diff --git a/imgurpython/imgur/models/custom_gallery.py b/imgurpython/imgur/models/custom_gallery.py index 2ca6e1d..2752764 100644 --- a/imgurpython/imgur/models/custom_gallery.py +++ b/imgurpython/imgur/models/custom_gallery.py @@ -1,5 +1,5 @@ -from gallery_album import GalleryAlbum -from gallery_image import GalleryImage +from .gallery_album import GalleryAlbum +from .gallery_image import GalleryImage class CustomGallery: diff --git a/imgurpython/imgur/models/tag.py b/imgurpython/imgur/models/tag.py index 67370a8..60a02ce 100644 --- a/imgurpython/imgur/models/tag.py +++ b/imgurpython/imgur/models/tag.py @@ -1,5 +1,5 @@ -from gallery_album import GalleryAlbum -from gallery_image import GalleryImage +from .gallery_album import GalleryAlbum +from .gallery_image import GalleryImage class Tag: diff --git a/imgurpython/main.py b/imgurpython/main.py index 17bded6..e3312f0 100644 --- a/imgurpython/main.py +++ b/imgurpython/main.py @@ -3,8 +3,8 @@ import sys import json import math -from imgur.factory import Factory -from imgur.auth.expired import Expired +from .imgur.factory import Factory +from .imgur.auth.expired import Expired from helpers import format try: From 5bffbb8e73ae0fd233d88156fa290a1edd7c758e Mon Sep 17 00:00:00 2001 From: jasdev Date: Tue, 7 Oct 2014 14:54:43 -0700 Subject: [PATCH 43/89] Missing colons --- README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 6cc546d..54b8920 100644 --- a/README.md +++ b/README.md @@ -6,19 +6,19 @@ interact with the Imgur API and examine its responses, as a command line utility within 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 +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. Imgur API Documentation ----------------------- -Our developer documentation can be found [here](https//api.imgur.com/). +Our developer documentation can be found [here](https://api.imgur.com/). Community --------- The best way to reach out to Imgur for API support would be our -[Google Group](https//groups.google.com/forum/#!forum/imgur), [Twitter](https//twitter.com/imgurapi), or via +[Google Group](https://groups.google.com/forum/#!forum/imgur), [Twitter](https://twitter.com/imgurapi), or via api@imgur.com. Installation @@ -207,38 +207,38 @@ Command Line Demo (deprecated) Configuration ------------- -` + Configuration is done through the **config.json** (placed in `imgur-python/data`) file in JSON format. The contents of the file should be a JSON -object with the following properties +object with the following properties: ### client_id -**Key** 'client_id' +**Key**: 'client_id' -**Type** string [16 characters] +**Type**: string [16 characters] -**Description** The Client-ID you got when you registered. Required for any API call. +**Description**: The Client-ID you got when you registered. Required for any API call. ### secret -**Key** 'secret' +**Key**: 'secret' -**Type** string [40 characters] +**Type**: string [40 characters] -**Description** The client secret you got when you registered, needed fo OAuth2 authentication. +**Description**: The client secret you got when you registered, needed fo OAuth2 authentication. ### token_store -**Key** 'token_store' +**Key**: 'token_store' -**Type** object +**Type**: object -**Description** Future configuration to control where the tokens are stored for persistent **insecure** storage of refresh tokens. +**Description**: Future configuration to control where the tokens are stored for persistent **insecure** storage of refresh tokens. Command Line Usage ------------------ -> Usage python main.py (action) [options...] +> Usage: python main.py (action) [options...] > > ### OAuth Actions > From 1859fff7c738e2c9152ffcb50e0fdd20aadfb1bc Mon Sep 17 00:00:00 2001 From: jasdev Date: Tue, 7 Oct 2014 14:55:29 -0700 Subject: [PATCH 44/89] Missing colons pt2 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 54b8920..94eedd6 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ imgurpython =========== -A Python client for the [Imgur API](http//api.imgur.com/). Also includes a friendly demo application. It can be used to +A Python client for the [Imgur API](http://api.imgur.com/). Also includes a friendly demo application. It can be used to interact with the Imgur API and examine its responses, as a command line utility, and it can be used as a library within your projects. -You must [register](http//api.imgur.com/oauth2/addclient) your client with the Imgur API, and provide the Client-ID to +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. From 546b452598a3342f951e5b068aea080efc6974db Mon Sep 17 00:00:00 2001 From: jasdev Date: Tue, 7 Oct 2014 15:29:48 -0700 Subject: [PATCH 45/89] Review feedback --- README.md | 4 ++-- imgurpython/client.py | 2 +- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 94eedd6..842308f 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ client = ImgurClient(client_id, client_secret) # Example request items = client.gallery() for item in items - print item.link + print(item.link) ``` @@ -75,7 +75,7 @@ from imgurpython import ImgurClient # If you already have an access/refresh pair in hand client_id = 'YOUR CLIENT ID' -client_SECRET = 'YOUR CLIENT SECRET' +client_secret = 'YOUR CLIENT SECRET' access_token = 'USER ACCESS TOKEN' refresh_token = 'USER REFRESH TOKEN' diff --git a/imgurpython/client.py b/imgurpython/client.py index 4c93aed..e08923e 100644 --- a/imgurpython/client.py +++ b/imgurpython/client.py @@ -513,7 +513,7 @@ 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=1): + def share_on_imgur(self, item_id, title, terms=0): self.logged_in() data = { 'title': title, diff --git a/setup.py b/setup.py index e16d97f..9a64a7b 100644 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ ], # What does your project relate to? - keywords=['python', 'imgur', 'client'], + keywords=['api', 'imgur', 'client'], # You can just specify the packages manually here if your project is # simple. Or you can use find_packages(). From 2d76c322779a4f2f92deb610b93c7951ad70ed59 Mon Sep 17 00:00:00 2001 From: jasdev Date: Tue, 7 Oct 2014 15:30:50 -0700 Subject: [PATCH 46/89] Last print fix --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 842308f..04058af 100644 --- a/README.md +++ b/README.md @@ -91,8 +91,8 @@ Error types try ... except ImgurClientError as e - print e.error_message - print e.status_code + print(e.error_message) + print(e.status_code) ``` * ImgurClientRateLimitError - Rate limit error From 719470f6925b370341862939ec4bb80d452ec4d7 Mon Sep 17 00:00:00 2001 From: Jasdev Singh Date: Tue, 7 Oct 2014 16:48:32 -0700 Subject: [PATCH 47/89] Updating share_on_imgur(...) header --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 04058af..34b657b 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,7 @@ except ImgurClientError as e * `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=1)` +* `share_on_imgur(item_id, title, terms=0)` * `remove_from_gallery(item_id)` * `gallery_item(item_id)` * `report_gallery_item(item_id)` From 18a86506b75e0f544cb7bf6aac7aab165323c7b5 Mon Sep 17 00:00:00 2001 From: jasdev Date: Thu, 9 Oct 2014 13:43:23 -0700 Subject: [PATCH 48/89] Deprecating command-line app --- README.md | 88 +-------- imgurpython/data/config.json.sample | 5 - imgurpython/data/res/corgi.jpg | Bin 97868 -> 0 bytes imgurpython/data/res/error.jpg | 1 - imgurpython/helpers/format.py | 14 -- imgurpython/imgur/auth/__init__.py | 0 imgurpython/imgur/auth/accesstoken.py | 23 --- imgurpython/imgur/auth/anonymous.py | 18 -- imgurpython/imgur/auth/base.py | 14 -- imgurpython/imgur/auth/expired.py | 6 - imgurpython/imgur/factory.py | 138 -------------- imgurpython/imgur/imgur.py | 51 ------ imgurpython/imgur/ratelimit.py | 34 ---- imgurpython/main.py | 247 -------------------------- runner.py | 5 + setup.py | 4 +- 16 files changed, 9 insertions(+), 639 deletions(-) delete mode 100644 imgurpython/data/config.json.sample delete mode 100644 imgurpython/data/res/corgi.jpg delete mode 100644 imgurpython/data/res/error.jpg delete mode 100644 imgurpython/imgur/auth/__init__.py delete mode 100644 imgurpython/imgur/auth/accesstoken.py delete mode 100644 imgurpython/imgur/auth/anonymous.py delete mode 100644 imgurpython/imgur/auth/base.py delete mode 100644 imgurpython/imgur/auth/expired.py delete mode 100644 imgurpython/imgur/factory.py delete mode 100644 imgurpython/imgur/imgur.py delete mode 100644 imgurpython/imgur/ratelimit.py delete mode 100644 imgurpython/main.py create mode 100644 runner.py diff --git a/README.md b/README.md index 34b657b..dc016e6 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,7 @@ imgurpython =========== A Python client for the [Imgur API](http://api.imgur.com/). Also includes a friendly demo application. It can be used to -interact with the Imgur API and examine its responses, as a command line utility, and it can be used as a library -within your projects. +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 @@ -200,87 +199,4 @@ except ImgurClientError as e ### Memegen -* `default_memes()` - -Command Line Demo (deprecated) ------------- - -Configuration -------------- - -Configuration is done through the **config.json** (placed in `imgur-python/data`) 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, needed fo OAuth2 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. - -Command Line 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] [file]** -> 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 can be either 'up', 'down', or 'veto' -> -> **vote-comment [token] [id] [direction]** -> Vote on a gallery comment. Direction can be either 'up', 'down', or 'veto' +* `default_memes()` \ No newline at end of file diff --git a/imgurpython/data/config.json.sample b/imgurpython/data/config.json.sample deleted file mode 100644 index 3519804..0000000 --- a/imgurpython/data/config.json.sample +++ /dev/null @@ -1,5 +0,0 @@ -{ - "client_id": "", - "secret": "", - "token_store": {} -} \ No newline at end of file diff --git a/imgurpython/data/res/corgi.jpg b/imgurpython/data/res/corgi.jpg deleted file mode 100644 index b240c794b08d26a58d36042d3562706f0bf362b6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 97868 zcmbTc1yo$m(l5G)!5Q4$-Q5EOcXtRfNN{(D0Ko$U3ogMSNbuk;fe;|LTOc?DC%nmb zzIXoT-nH(%@7-E!cmJk#_3rBG>aN;*`g!4b6R4B*wX*>LMMV|>82|tT00#sHKrr<8 z;s(J1WUyKZcJaV49;}851GsSi$k;{0|Cg*kjQ^LUq-ChI{X9Iaojj;y>}>5kEgY#7 zoh)qaoNWOfE^clSE@2TaJ}Pb=5guLedVOP+gqWXvb{|RA3UEMul7Sn)9 z^IBPZKw|pp7hVh)NEj$1K9`cv(X$^f1 z!(1>7ao5q3hG8)PK!(`<2e$kV>}l-_(-Q!sU0nU#?QCCrQZYf9sknuOg{Ty)eH^Vl zJ=rxZpbi%9R#eh1POcWtegN>VG5>7^;QX~M70k#2+`<9^>^vMW^Z%Rvw>SSS^}h#y z=k{L`mzw`_RDy|rc>lEhhv!lR07AcEwt4pt&oT=DS|R~}Xz3pwT>$`Kz5#&dss9)c z!C!xQ?dj<%!pZ65tSLMId^`cb0eCkOq7J@_`bd0;mNVfEM5j z&70L6h)Kp#MbpbAhus14K$8U;;*7D1b!1JDKN9u5o#fy09%hhu=_gcE|3 zf>Vamfis1(g>!@RhkFB;2$u<01Xm5$4A%=c4mSt40rv~;Hy8k;feFCWV0N$&SQ@Mj zHUis#-NC`&Sa3SH2wV$p2M>d1z#HHr@EtrNJT5#HJUhH7yaK!~JQUsyJ{Ue8{sVkD zd^7w2{0#gi{2BZo1at&41XctQ1Vsb`1X~0jgeZh`gi?eigh7Nkgk6MdL_|aaL`FnG zL;l#CXJ9#7~Glh|`GMh`*7Lkcg33k;IYIkt~ork)n{YkgAcok-j7CBHbdR zAyXprAj=^eAv+_7Bc~%*B6lHwN8U$%K*2(xM-f3$N3lZjLrFp@L1{;sLfJ)mK*dI7 zL={KXLA6H>L(N33MIA(4LA^vnL!&_xM$<&ILkmUAM5{v^LEA*TgWy0|Akq*Mh!-Rg zQU>XPEJ7~P(a{;uCDD!0J<$`mljtV7m6E!TY@`?yNid2$B3thXO9<$SA#c+cZQFH&xfyv?~9*} z--*9L0487{P$Y09NFrz;_(5<_NKPn4XiXSHSWEbw@P>$#NRr5!D3<6G(Hzk|F%_{a zu>)~3aSQP(2|Ni4i6)5;Ngl}{$q^|YsVFIwG?ui1bcqa(jD<{#EP$+-Y=Z1JIR&{q zxhr`Vc|Z9P1p$R5g+0Z4iY|%+N?ghply;QwDZ42Tsqm?!s2r&>sRpRdsL7}msJ*BQ zsVAu)Xc%d9X~Jnf(X7&<(hAYq(7vbbqdlXeq*J8}q^qJ^qDP?@qJK@FK|f4?!@$U( z&k)Vf!tj%kh*6QzpRtN@g$bQWg2|1kh-sD?ky(h@fjO7?8w(r@Kg(;DY?dijI935x zJJwv*X*L8lVK!&BBDQ&U2)h)!H+vQPCI1HqR_CI5aBPvcOn8Jo+1q*r=skl4x*K!KgAfuti?*i zw#8}0EyN4OH(pS^Fn>|-Vnc#j!a|}*VoQ=%(n_*aa$kyB%3i8k>f|NYOZS&eFK?tp zq=Te;WWX}=GO;p~vN*E3vL9sEVUeUdaC-W2BU_XMu#S%riNy= z=AIU}R-o3fHm7nT`%1}Jxo0ly$Zb>eHr}}{S5<7gCK)(LlQ$f z!&W0CBR!*1qu<7|#%ad8Cj2H*CO=FWO?^#AUy;0WeAQ`&X=Y*8XpUg6Z(eEsXrXRV zWbxZl(K5&K3@Q!Hfc~8nwK?NBJ2(%xP`LQIOuMqWM!9adiMXY?ow+N!mwJFa zOg&mX@nFf+gcp-nq}QhR3-4_2TOU22Mqg}SXWt1w7QYz3eSbOsj{)!j(15-`>cFtT z%^<0u!eCIaWpG~zZOEID-B9_^@-Wmehp?~VT;VCKD=CWUA zf65`tiOIRiwaWdTCz)59Po5u_e_vo%uuv#p*j&U=lwOQj>|MN9qE|BfQRrhuDM@Ku z=~J0=*+#im`9OtWMMWi9Wl|Mfl~>h4wMq528kw5bTK3w)PXwRh>VP`0y2E<&`X3F- z4SkJ5jkTZYKj$>zHpMoBn*Ey3T3)wov>LWfw<)#twTrelb#Qc)eWCu6-HG3s)P>sh zrt7)ezx#KOYtK=yZSQuUS>JNMLI3Q4=D_5j^5Dpj+))3p)NuER_{f)0kp|O^6 zf$`=E{)wiq{9l_U1ty!P1gBcR34d#!7Mt$+F7ds0MrLMcR&jQGPHk@bhwhJsd6W5# z1*?Uhi;j!uOWsQl%b_a>E3vCstLbZGYennK>z_6RHaa(DHYc{Ux0bh|+rM@^b{==% z?4j?a?^Etq{N(xh+oAE{?l0G04@XhQ*vC01OeddDB~K^L49<4X-Oryd;xCCW z%dYsY`hIKtUb}X>ez=LfCB7}c6S^C|*T3I?@Oeaf%zR>gYX77BXXV-H`T4mAz($6J zPX;iE8i2zFfw4i)%fK4|4i>UOpuZ&?jKBzBFswv`hX*4dA;KaUA`&7p3K|MBGAc46 z5-K_>DjEcW4nab}z{G%H!Y~8`27}?j@W}A+$T$#G2+sd+dLDo!6aWYw3{&B6mhxW& z57Pt@goF&Bpkl#PgoCN`mk{9*05lLB7#Fu<8ce4eW{gPh=O0= zJI!n9zIjGK$ETfgF0JDcPRI|nDng}iAd=}bu#QOk4g%o*hpu2b69^2CfQW<*{Ivia z2p%@1zuLf*g2M(=<8Z@E(IDVzTJX@io#DMKMC1)i*0QXhK7U>UAYjR*tWgQ?1QLw?1FnWJ2Y)q(QP_4`O}E{AkDiu8t^eSxoETby&+ zp~Kp~s!_MVIC-^k1{>3XC~kKxAf^b^?$1-qZJeC8_8>yzO}i%Sy2boHlV?@Cf{tmH zM0=aLeh8i|Y=Bt?jWs1+8C)lijJ%L>2}PhXzID19Qh@w$HaYzj3L!1Gtkr*Y>}Z{q z^ae+AM-9MtkNccH{Hf%|;zS}55Qml^pknYsU2wq{JxK4-C=u%O(r1Ou!AlX`=b%fX zOJ6CLF~y4>M8^+ebg3?47NMHm^Pm?=K__}cI`CcFf$$U!t8mTFfS50{faI!=BVZN2 z%3gN1sWk6on-1U=tfW>s7i{{Y#HTY1R|p~Tgtoj%bq>Gji$$RJo>ki)2T+;mK7FKH zIC5zw)4+Z^7u1yrOnIG^*o&QTHDOnM(pugCvu+{&SXmC_l+BP=&jSdG6`FZNF`@9_ zDS*NlJqqg~5Bfv~2$s}S4}QH{r^wPp2xSe9s0DCnCG)^{Uj4Q@QN8&=Z5yBpolt|O zn?2!p>Q6-zOX*K)^eEI>#pvvPluu=jjBS5@1^Zeku>Nwd_Wr4c2Wp93D?>&e1C<}p zJh?=|x!9q!WTb=2HPb-rpJ)TE!p>7r?;wEx4T0djpM|}~EgOe_9u%Ok^qymwzjc}- zHkECn7U)5gWaGQI+oqZ9%H;wjyRr=j+T74LIbEy+#OspA+bkmj7Iz!qrCGtaJiLA= zv>D)vZ+Ap~kyC#F?3Obzx?=!@n_74#+Y?Js1?-`aKn$fjmn5IC44LTQH|8KEXTF9Y zw=Es?nGEVs$RS{+I{JGERUoWvD(hD$LK@;`l8G<>Wa`vBKw*wP*|S`VN78c|iiw5; zY8GQdF<$_#>n;rVYhcQz_trE6V(mx98oNp8E#iS^pbd_XoZWf&=Im?SLMT`C38Cid znd`)0sabW284yz92FssdgULuO4#l?pP{@r8H}a45DGJhDaj72gTjG9jXA+g4#mimf zeTz^xjRaLoZ_*Vg6*GMMO6q18K!#e1Thjvlw#{t0I)feskb#xBG%xzT`Qo=31E?C$ z0RGt?Af99nUbqIh5L$*m$E^cIOqeDD<857Xc z3Jp>#rW9;|!qdP?F3{8lPGYUiWv?ekVuR;4iH=!`tJ95-lwNed$p5NT)~~JdYlv>Oh$h#K-e3DM8i{%}_{a zL=T6!;yMN05dV6H3Si~_V?VQdoESlE%+Iq2?zHPT9xx7)FafM;vR~y6i}_ZX$^P*c z0>*v(!{Fnby2PqVR@H_Jc0)0#UV3*v`B_MX*TZjs2himnlhU~a!DF|8_PS}<{tR)} ziF7%#rBKNN184Nk%Y)QAeeVDVkgY^shw&yyd_MyR< zfSV53($6K339Y6V2|(2a2L>2)ehNi^!hBI2wyI$jX@Grw>Om-c2R6Rl3jL=9#ukQG@WlKfg zitNE^;~7*vct>ad)6gg@Zqwf(>AXRD4ksdJAO>ZykEI+zQb_0@TY#AH$C~l>h+ePR ze)BCb^(OrAhB31*{)6n*aj-9NW=1=9JJNImVTbL(D`qy7Y&OdazAZgS3BBj!N$O%u zc(cSIDuDS&uNPwL#c(yL1Ak~2z}*zu6nRH}R(@4%kQxdpgg=}OJdH$bgm3Eo6^dy> zo)d(K$JbPIXADr#nIsr5mt74v89J_XaseMjSC-bYp=*em6)l%Uz(r)eNDNY8Uvgp* zTs9ftYqWCe7@)MSjqyqzKm}V4=W&l1Jp<_gk6aa6(o{O{wL2QxojDj}YW(q@%qqpa&@@L53RrV4lLuNonli{_l1(xXNxZyQzscThjal>Vcb!C2vs-m@L z*l}M6w)(y}OGyx#u;866^+ds3iW-f01qXAjzo@rKpa;!(3cz4lS>dYd4|8lr0*L!W zKXyI?c>umS!J0u2t!5)eU>#5$08k9qfsSZo$gX?fz~}&CQ(XpR`LDtx=>tE}H^t>h zv%e%X#o6un?1x72_5Hzn2MP)m8j5-bvhFwV9=FbJu&oWfT(>v~6CJa8XE8giF8SFi zlZes&Fa+kI*#wEl$-F|B6A{?_kA975d3BK-__s<8cPG2RwGr*K=G|1)LQ^w6*- zR$jV!vLeJ$98pz9tL(m5)&ERZs}w5tB!|cE;~xR;R%=Z$_~^hSOS>PRed;dIGg{0_o7M`J+VzM}U~?9J zJi|IBPWU2Y*Lt_9>?)-V4TY?0vetpeYY9$X;apm(5^$GhzxEFdJ< zTCW{zk#_rWd&b^INO;+UD4ie)IPG!&N>LE?j|W)pqlPp4r17;}+Wzh$I!7XYn*oQj z9ZtV{5IK6A{QR~BEHlLJQbRFmvNI%xQJqdZYeMrT;FjLn5H#a~*Z{(oJfI3MIl{sc z{RFfdou+_ZX>8bTPGpGrNhW$Q7@CfM)`)z=nA>g+{ytOEEYg0}ACL0tXCBO-F$&GN z$Ly7DJ_D~4Ng^1kQ^q{vKgP|NrLBpY%HGFq*OiMZZHTS2M!YE4 zRw=SKdV`KHqgxN}>PNox3}g;P(j`k%kQbeaDORd+jlm^#Ias!w516XEn8s?fC3ZCB zk;l}^Wz;Gs&13D6WsX0{UgH)BA16i0T_de$AZlE1E}WQeiBQb8`dhAF{M!CwWxf-U z%Ghv1Gjb{M$2d=8?vU~sK+z?8gZa*pOz(C2DPASYLOXWI_XTGKQ7(FYOa4#Obxi>) z5h1KOWXa4LH7<4RNz9F*$mdB zd;i-u%a2yTz>^3VHDAN>xe&TYLHy_h>#zq=v8}@(iMyGAuN)x1TLy^cZO-{?B5}~) z!qVnFOAclC%OSO@6|NRoz6epM-}Co_QpXU|!ty`F7U?Mp5-)t$U|0%0SzkcYdIk#x zY}Ul=1ud`uR+{hv$5CKEDd@Y>+Ji@rk{CiJX{w8f1;1cS+v6F|w=VMvSM{?YF>5pv z#}F~NI1D}n05hE{wuRbRL2y4*qV=uinr(O7K`6qu2 zkR@2$bopMZG@J&F3?v$6eeY+eV4QDiZf!bGD%vS;-X%^$mzC(K{uo2@61NMn^6OSX zl}@;gWrs5xB4JBmROzuY-)6fZOZkq?x`KX7?Cm?}6`_hM=5k7bESgxZi7Hz7*&tAV zN1={}>#@(<$~|q(%*o|IX4beQ)w4kHG8;$1N*7Rjy5Pp9rH zWD3^e1@by7DYHMn2XiTOvBh;F;l+^?l4(jp>d)D&A&C-Ag;gmGE#J1Ld%G6MYrXAg zzV`l^&T$}&Q;2B<^L&~R8cft-#4sg1`gGkHpt)mT_Y9o-`>zT-1A98j6POcDartO} zmP1~n3d^6qZB^$5oYRB@vKE?iGK`T` z%ZhR=t}Bt_gqZZbJ8vH~7I(+Vyk;B@hLU6}>EEVx2c-nH;g%VuiG?M+>`T{vDZtZ!W~_$S@ovy56!JMEApAw^ zWK%1>Fl?i=a!P#27qb%OZY68(0(h%5A%2Ng(BA4VC*oW{)laSV&*5&lVFIv(&)4L% z>T);&xS@0QH@}8U3l|)8&b%C{@P;;HEQ%But?aXoZP>@p^MvQ|?Ok-ew%&_PctNhL&`ZcUsdM(}rEUt5q&+})NenIws+ zY~*optCj<*vy(>vMJwXVMU;1;POw!(M#0nqEZ4%x+QFOV7c8SG%kkASthxfnqr34} zJxKLOKmf%*)o~JcDB>OfNIup@Th}`oLjXbRR89TP!PDI+E5?^qPKR7r1C2EKXh#A~ z^@+bjDB#pbFXrWX2-hCc=jJiu``r0nqOONvrVF1haTdg?(N&ID@_$t3gN|nnOnlKK zf87>O*uw7+&kcF=sXC>Kt8hH`Gi{squex84TwgkP{A{W!eQ;U=3Y|tJdjpDIC#wuj zqmZH*ZPrTl=XWa!WFBH<8+hzT`co%K=gzVmmS=?_ZjU2qyYW?ETh23{64VJrIz>aL z&-ohad_Q`MHTQZh9(~9mpgRf<_Vwh~I1Q=c?|8NRP6Q*fH<#9f(XWvdVQqf~Jk8P> zt+5{)sd{C!u^Kb*Lwi`e$8nB_zpb{|n{ph9+C-ZHy8YEiPY;5zNiV2*Iau{3s6c2t ztWv_yH3MI7_j^sIa>m>jS@n-eoGMDGDmB}J&w!O#Q^=D1n}Bx5sidt*8sJrS42@yL zy-)uD(m1))V)SwJ(F1q5P>03QGS}$%Vu-($7Ri!JUh=l)jb{T`tE+5_RGa&mIU>?cq?8w_JBn1 z{itumhVp}TlwwSc*;tQ{XC+y->)V9;Q>dp{=#vY%pl!}Ahak6O^SVak87#Q8ux?gZ z#=t5PC_w87uv@`9vOC=j1@t$K9r-Ndgih9h8gaYC*L;850k{)ay#>FZ#EA~_?`M=v zO|{(Bc8Dp zuU@N6ef}f9J#la-lq6D-?EFg44jZ#hrj zwr~@IBFEHd9cYogb2*HU?=YxQ;H%k2$rD)rB>?q!pEuEZTxg0JhS?5TbJF zetYM;mZhG(wLO*eK0xkI;Y3kYnNg(`TdhESFB;Q{SJ?gbGDD8zCAxN@!bAbO8Rk5i z9_w}d4bnIkxL$OH&0A13(VIxeI^Y+358=XPj}qR*&drCZ^ml%n7iu4ydlFj_@d$OG zl6Mx~vn}kug#b5-YcLuaERFWfHV60rv#{z?hQ|REcT`x~O_HtL=e-gS@S}lLs&Ed# zQ+c>tKkx`vJdhlLA!U10YPsQxA-46%ki#C1snJXtzWH@uba$FL=0 z4PPt|(y;s902q5C6J*Y`!oses17H4P*iOk+|EP|YBtL?2ux|p~(ZyT?bApu(Iod^Eg{?DG zJ3+MKr%M>$JNVeB(>}hvRk*9(S>Z~ljSt!`5^RK4AFZnWK8k(Ilo?>&b$JVWS0I_N zFVSP8gLHfP`6rp6i!ju@#$lWERXo*24LtSOzwE`fNgm?+60NU=v5l3BhV|hU6ZTW8 zu;@uy77@b>j)XuF2vj%fhVdY}!CRD)M20Bs9pHB;Q41LhP?@+GjnLeikpr6;zFk{+ z;`&mars@bY%GO>0D_feDlvtdtq4nEWH_L+#5eY4e`N!1PpN?vyZwL{*E6m_gn~jXZ z8D*aVj5^OJuIX%9oN3i*Jb!WDO zQ*^^K;FPLuW#Xr9+-sQgwFEMA%$$`ZMRP%TNi-ctQ`6AaTlV@0QY`bo`Nx1A`_stri-0s;QZ1XS)O1l~7O2aH8tQ3IZP(#T34#ln%3VxKpG5*i_7FzzmeK|JR{7cRl|CqV|NDns|Oh>dEbS ze({7K*Jg@hY+?|t^6w|tEwmKywvky%lqg=I#%K0`Y#gqiZ@co2r=pJARyL?y$0^>V z2%V_?5b_oV3oJ)8J=S4FN?S1L1lX;tqe-q&`CayaE0{WC>M3kde{{qpv~7R|DSZ2D z>&^Ym!7OatIsb3CG|#rctQ`7!)dPnI7-pUZOCx9KQ@cz7=o3Fs*~iBrWBsNRp?SW@ zmc%&H-o|%O@&U7pvn|a#ZtOzk%5`@(b@8*yFgGd!SA{yE-Ot+f3%@ z^&j4lKd^*i94-S~&QT@P*lM3%=vk(SKCDW$kpeB<5(NXL4YCVZ>Qos0kgY*gcH)?2 zWrf}@@=n*g75mjh3Sw_X-D5zit=PpnF=wCn|t3A~3>|k#Cay*ZGGs?WE*8TR8 z7Hb*>r_I@>b3fsv(y=&iA-kle&d{cV)scPX*W2sRR~{NQFPDg@oxUcrdak%>r;Hbg zXns`aDo}lLiHa+En=(DUHj&??yhOUV9ksVni@tS0Q;-#t$THG-z?`(DL+2P6Hnf&~ zJr*E!&QAC+?94ke(l@-ya}~8;PB`ai(x53l^wn=Dc#XX7yhFjG_6P6SK-ZRki>!of zQ@v_Z%fvi)@YS9cUqZl5%E-WzOBbu5fS4OjmilKi!s&y+?zgPWl<&B7#Gt#)?01AM zgf3!2-%2%?DRE|)#0qL{w4VXfvy}z0R|zri{>(fB_OUD>H^D2Hf@n7^#Zkuhl*mFI zR+P=SOSWvR7}mKvzkf7+uM8iIu7Ggct_C_9TE1RbVSTW>ZBEZMju)jKbJ!r;9k>4g z%fCxLT@KA`U@ns$O%J@rjlEELpnkWg?uYz=`?q$;41qj-73+JWpDTEI3#+kiW)Ojp zo*eauuL{(<)LwLV&fFh`);`4sXiw~H1yj{{g<`Kr4qFi}7}pA|Yfs%6^(U>{8uy;h zc_d?g_%>6;&R|NEE?z|MJ?GN)Q;ksZz2+_x$!qL=wm``gtSbP+3JM?SEP?0fC(IS! z7sXV7cli=3HDQtt-3#J2t1T@l`@mvE%XY~>0`Dt%)jx$!6@MkGv%{M4Ra{%Qvg3B= zJ^ArBvv0tbg%+mjd(-Jrw7OZDLi-P@$WwE_9rDmUO*=&Gt@(QL zWW8nhHNw4&`fW6BuHda=bE5Rbrt6E$7*;#b+T-zCVy-tSG+MGwQXyt}1l2l~$sIU~ zNFbV$#lG6L*01B3D7WqL0F%Ivx!?Vv$89}GwcHyn;{fsBFJl1!5uJPYuPD=8@3{b2 zcpp}@fjI3)t)@TTT@eBHuKbAIGw4dOFMixliQhw%2wu`DO!YG)e_Ka6fq4b*{YIhV zW7Z(3R-;~~U-6-gRH6aXhU1&4VU_+EPn#aD&1ne*so!?C@T{=PPiT0mh<2mPVpBhN zq?qB5n<)AckzZ<@uFKKHk#n^~o13^5oTg3t3ZG=@^f6y|qU!g;FRBqARfN-m<#v&!F&fBp1oqkUb_6mRWrkyWlJI z5~h45Pw}q&{b$LDkt7pdqq?|N)TlzfqGb;Y2aB&Pm5Sve`Xgpo$~qI1y`c*4@oDa- z#N;|MT=^ZN{zM~0e3NBYsVdS_RrWY{X75h3-w9OACZ_UedSKk+Acg3fRC~neE5gdUQ_?eS_hW<5g`;yzA1C0T*L1LlhywRt`z123i zBW!WHI_vk{cA6sLa7leG^C%gNi$lA&Y;W*ZDdl`Q8_AO1T%f9SbBrf5R}`a=P}O@t zy@fb=t>QROSy*wHXyO|cipS0ILTb(<28YMYyydc{!XMmN4ZXzflDrIk!qceo2c+6C$g$?q|IYnw!)~tm{tABXeX}adlaVykE)&L zKe0zrkP~%JBZW{EUInS~#Ft8@FoV;&3CAH!*PlM$_z9dlizL)FWwKQt`Z<2<&iB#(qEK?v8#ZgoIDFx^*zOg*Y!XR+Dq^9!ta=q$=h%WVDQ)i1#th#JTLM z2T}fo{AFK6Xba;75isS;9tiznCIPYAdG~9nM804DWFi|D zSHMPGyXMnJqRN3EHGFE8PKQ`(Wk!l4u(S`(I>jbYWGw#`hB`nhR8K&Tl+L{>|0@b7 z>8pMtX5*04keDKZXIr|_8TCA!Z2vpXaq`vF>D?8^)ZzD5pSh$}()A3|=Y=lv^~OFJ zB(I-sz&=Q-YJD3PGkk+rU2gU8L%cp4 z#_G%<#u}DQyVPJw?gIloY&oZ(K8rEsb8C zoglr*Gq7o9YqpV3QN;9jan6sUQ2UMPS*tM1CWmykaQss%3-2cJSBv%(rtud7MJY>8 zi~7g2N*UAod*9h)hJlM0P5G>0I2b!!bF>U31^hN%&I(0u=S2`FzvRDiBH{;XS=81baVnn z5f2pzb~`M|ax(ipf8)vu>&R;bA72x_ziA`kB41Y|i`kDT=pa$bJIaOhP)J%zv($R9 z%nXrr7#OPEkezeS?5GAN92}i52;yG1We;R|iDp((D$k|S@BycRK^#X&b!$XN9pMF| z4J=p{SPydGLEgKTJmFwDvrc~GY}euAZKsUt$$BOxSp-?h1n9Y_AE$CiZEJy>56-?} z0(Ux#&onFY!9fwh_kxrC{lr>GAeBZ2qZRaAmrrx_VKk#7T%&WsQP;}t1~1XRGsv}P zO0Q})pS>*QL%IMvmT6PHXmjeL>u>8Zc->bP89IB~Vp_yIHs9#p;+7{Ii3hoHE9U7t zNz0RYav7bnvd2-LJOkSE_*QFSLvPuZ{FU(hjlgg}e&DXi6VYe+HuTeswEgWipOq;a4e@1}%Qk|NoASNgbUvwKm&I!>p& zo}j`Bx9gx`4b)>n$3|}PiS2KgomWHOCvK~vnXD8VF=&d*zJKURFT~9#6t&!?yOw9r zZ9KK>>J{>KZ(DaJ<5Z@(RP?}uV2-10cH5oWuF+SRG*TaTME+uce)hb3Z!!8J*VPV* zaWSyCHnB8+or#qu(>RBXh;;MN|L3M`xFma!XsWB5i226=3q^W?bcMW~U=G_^3tROl zRV#mpECCDjO?9klo9AWU`>y1=deVh0lqVwZ>9eq{OQEw=t#wVr>+cw-&C;*NTI`=% zujLnfFvbT>tFPnUyFS4y8BAr(zCA$_1-IQp6CRYGfvY#2F}G^|vCg1^tykP9ikjMK z+Q=0v5#QzaF9+I9(}YLchpMLoL!N;HMqB*I15wWFkfM(^NHUANA-?^4F^={s{nyXH zgTpz$OhSgu7yi^CQtOZ7@UXY^+S^LQ*Vb`-&8xxXd7GTeyT?xb*CtQ5P|>)`IZYz6 zyGNnLHrO8_CF%oBS2g7N7R1WBaDbb^VSa{~{8|x(*xTNxuBXp?{81)x_0IUOlWSJS z^_g5IZ=27Ck56>U4O*XpI)m-_cVxT5VU}WFv(bq!R1$?@6Yn&M_tZo&Y9fUf(k+i= zHR;_c%asM(m2&zq9t&R6P8QhBx2SZb`9t~y-~2J~9mQEz$sT{|5>Mi7V$TSWI~!ki zAF8I7$n2H)^YCKy8F&UX#kgUEMEH{#{Vq@C4)>^~M_3T~tgfacewbhJoGxcJ@VuiW zlA)v%-LkFur%Mw4&YH=!za#7Mrc9oC%KRSLi)m$$M)Wp0w{V*5s`kT2SzhHj)OJ6! zn)6~jHbtKK{zbXhqpMa;ZjA#jE9s4tDuz6V3}3@*Ou@Z+xl%1tq|nHw=`8Egac|vh z73TD`aiyPraVC~f@iF&FnsL$gCEva9kDjAw?7jDv@p*j37c$iqjp^dG64vt|?~a#W zlotodbnwqBoOk_1{h<41F2N(>rPs7sBx-acR`GzRuom9ww{7*feBBpg?pxRPQtBcv z`~3AdJPm@8KWy(gPjH6&bJ5>j* z{aS9!tK&dckFrd`9!+M0e(vtr8`IxGT<>#JL8jlUstoX*j#jyx&4lX>KoqT}C5pVN zVp>uZ%>Yv);Fkl=l6QylH)X)0ogp`}sA z25Tjcq^)Cn?QS}WjLft-SQ_i#o1cwy`c_qK3H}JI3lcY0#TgnJAdiF{v|^%vd1(Jb z9+gso7Mf6XG_?@yw!}%SEyGN?Iv4Hx@O#fCmUrjDH=A=1LrZ>^qql>6^JGd$DVt-Q zNeh9*LSYuIe$QmvTk_kEFhn~!$m%D-?k`7=Ke?1`f&vZ~FPJ6gvxaO9=~Z2@_RJi7 zPG0|{T^S3(rra9f4|W*9(^p;#+Bw zgp(Mp^88@uUTA}`dpZeLzCHsIVuHGZ#Cjn{jeCn_K8jeB=0&4K4T%)YTQ>{>;47#1MHgGcB6b*Nz~!(uV7~_*G5WJCt_z z@9_{)(g>UHj|c_Js=Jm%eJE7jE)PY3e;hW!a2|ZHD+&njta$J{ism&^No_iz9*cK` z`)v3o9?vX2onB#iob(xp4sosUW}q@3fEksel;z{#zrzC-xq>y=i+Q(;*f z7wiGsGf^LOckfhmJ;lRa;~N==)-IV(gY$h35@Vk7x?5<&o~nKGI7UQmHl%vVw3)eQ zCep}{@jd?FS#s1|E?f+bdFyR6iHiOZ>rrP;>JZelKB#h(Q0F2jb-NBViMt$pZ;)UA z;b?|)oOyTRQt9o%M%;i^ZeJy6B6VzGgK|ms!L>v1+AX%FQNq@b={6%SE1+cpN7eH^ z#LplhH~QWE#xF}Hr!J=3-^=q3owWhVZe(A)mRs!FN<&Op9k6x3+WQ+{phj3+5rvHk z#j*QJxFgNOaU>H`l@T)vT)iHSvGmMj4-&`YjReA9kc+!cWL0UEzU_+$n%%^G%n-Oo zxv4ZN!I5ImTaL@I%n+&mXp_QQC~ta} zF}X-DL9weM;0peE@Fw^Qwo|9GX}>&|hkD!PYTAfNNE0Pwbm&qQ>|YLTkJ#}9_C+yQ z*!4{y9+f^k*6$R%&~}gh^t%4e@yMI*qrvBOHO)hjoNa#>Z(#3hpU#9+86H1lN2*9P z7w9b(s1%=)UBGNvmfR$rN#g%`mHzcuyz-9lj{q`?U%As#8a^~d;3;x8w;}bseS1f|Q?!LYvMeij$ z8x#F3{Y{0dX3ok+W0~MrG)qW5FCyzS*jeCeKzCtIM%6G`kOl3x^}=S_X>JWk!eF(? zCQVg?$P(V{p6@5VNX6S)ziqxxtM&<4@JR0JV#;Wkg`W~yl1ISict*Dz-euL__4>x4 zYZ7AY!OFDLB-mJs^L<0T?=OG%zL#1;3<$?t9y}*gT120whQE-A3}hWwBm(oqQlrVz zGIg7YDQ|Vi+|#B^96*d0jDrmrkiKnc(}Z(6xWDjacYnh6Z|Gl~LbNgQ8`N23FjTV2 zT3M9rS~IveJW5-LGFx0npO)fNO4XsbC{iTY`Q36cOnFG$be)(KLwUnn+A6VXtLyi^ zS`+Mm_V@6E^1d$i54EP$$#GTG1A}M=ai`scR~Y;dyO`XSqXM^XY<7{BHv7^UFLr_> zq}!IZJBWQS{V~#;hUJ^tv~&udM($H9+6u;yLc(F$QF^HaV-_6~ulEYyZKc^imhqCC z9v!>OXEA;n$KlJ-?Vly~d5|8!SEa+XKiR5CdfDN*oQ-LsYezz6R!GJ^z=zMzd(PpY zPtPQSV;56hM#& znVsXc{ymvE){~7-R=?71%cA#VTu(b+R#)ZqM?v|AofD)fY~d$c$*O@H16>w z5a(0oGti8gK(j?w1v?jESADfd?B!}?&-N$exWYIk22=Ni?>+Xh z=&|YIdP4}bDfiR)q$PbKRfV!zf=!jX7?mG*cKa35q$rP~P;*jD-g163ZqvG5?ATF` z+qiX>cD+G^Nv&QY{HqTO&ck=x$v+(|CRuhSKiH(Oh!Swt+l5RIYGZ-#yGLs8~%_E2B<1WC*$F}LBSsX>kGpmM|Dm72T@cQazvhrCx;5%f1s zclVpdD)~C>4to3oP0QmLvLD7OHmR5w3dPPj6<+awlhGa0!Rh|K9)Q8F#Ak&hQtR}1 zjusTUP^tUQ(C|IDB+)>7_NlZixmLgulQq%Tr_B-9vgoU|EGsp{Wx~Y5tc&z4xD4I( z5u?62h~V10beyFDRiUiRb%?uU?dC=6`U-E)6*CjRG9SwmQlHyiA?BgPi8nn_4;zwAGIuTsrpQsr1nN_A==V(J@1|XSO56KF z)2J2UZLbMM`RVk;RU1?F(}#A;Xrf6ElFfTczANvv)~ zcw+8z%kRr;2rND#Y8!!P#RaR_ImQ7IGy8E{$N1xpRLYg##t@Dt1jYQdE~sQR+P?^P zv(bc5uUnZa6Qq4AGrhwdf$(~C_?UWIiPeb|kewulHsu)YtSxny;v~OuczbQLAjsQI z#pNvvKLjZ@hU0Lun8EmB#8;OJotna?!7a5x-pP_yXIY7hOUz`!$Dy|IYwv3gA zb4>_tEVL+xV{h%r`Y0xg?-&^^O1T~LVON^I5&&7gob2mai9Gn-J8Q@B>Kj>uAf2P4W#^$8ql|UV#H82KYVy9Kud(c;V#31$tyVOAZ&D zb0(18)qE&RxQ*sX$us!HD&e77^pu9j?uY!S<`*6q#b-wD2Cwy%&d^{QBf<1ou|qIB z^^v<~U?zfKOx78yh$d@jS}_||L|FeWUo>ci^o3=LZv1xKCC7OoSTDD@OfBwGXe!Nl zx>i#WL0;Q*cuv#Gk3pYaz$BjTb#V}!yC9<6Pl4U3JVALIop*B@#aeseb&`?n88c~R zW&^SNHh+jpIc-eF(g<3y8$~h?c&?_&hXZ`*oc30Yzd1Q7>|I?gIw3e0BFlI zciRGf0@@gFs}H-Z$5~3FWTc9A;AEqxD0#$7FTX5{9_#adWywrS*P>+!WC^_(lu$|7 z%Za+A9AYZjAKcbUEN` zx2gx`5p^KtNu381|B}hTutC?(_;Xs5a<6xP8qJ#k`#ZoIhcYz6!coZ|5nW!wO~qdN zMr)vFRc$9xp1{Q+WQly4(aD6mFDUi8Dwpm~P9CXUyCJnw+ri%Bwx(&4bNB_ewXlV| z%3k#;?MOK*v?hYrjbElns1Why(C|wX-NB*wWUN?o0jd;PWEP z>!dSZM>RrC(NUa{GEG9`cpZfaToJ_*Me6$QH_Mm(4N7wq#OXbcDx|L5=yb@g3W;G^ zaLc7NQ?)!c`Ur6*$u7vdZ^^l5-&3PrU}5Z-rQb3nDxfxqUC)j1I=gDrU^W5}IcZ|i zxHyQ#A^|^F!|k=`(z=-VktTy-ky#V+K%V|?e8A!}FkZGFU7lW{dF8P9sxY6b2uCIM zq?u(c^A`rvS7zEsn(|nIy0*5?`u+2eRflWOom!?&t2Qrj-!=%siDyD0e%_8>uu)WH zHdCIK_k*yo|8l7~2J+AS(W9CV?PL3|nJ<=?+7A!w&KdQtAL{|A{sX1``g)ix~|xFhL;AF?M3X3)sErRdr< znt0@buVko3)Th$j4khaQy4acx+AyVb-*Jv-)G#T+(Ql7FGNVCzQhO$yn10bd%6w_q=MXhLRReb{30dJMLAP9B@O*~{_T*^tF($bqP|ugo z3N*z|rT`Gfo)~0ZK}4hhz5KD=+*PBW#SWuH@guhT?8f`y2-1W$P<3y6^Yp^YPRQ+z z*2Y|yjD)z|m|TxTfTJDZHoeqswXcD_5&APUOBaO+3UAM*JPRy=;gpa;52J0pFv;q{ zwml1=kgPWag}`IV-_Hg2Yy&ePB!X;ubi}lY;+fzjNR}x)Bml}u=YnaFKZ-%(EwLRA zCB-yPTL=noc4AZu>K$+%V&*sRoak2Pz42T4=xf>Rqb^L^y=Z%;@r{7S{p<&^D!wA( zte5wmS^ny2HO%~l?a2D$ACo_~j#9ph{7ipxbk4ot45kdTA%Y4P(*#V=#H+bG4Y9QN z3YDrmuYGkf{7nA`bSW>95scx)K;qn;UR8>g%XZtltm;?~0@1%jW z$3+)aP(Jy^3DYp;cFD|--7BetREZ@#5%^R$#!w%P&q9AZ3!G+gXBmikB`>V^C?;LD z<}pr9p%SD*cF&we!M&R1+0|o{PfY<}skeA+f1R=N+l@2I?kM6qoa(aUypfzlr)5%k zVZNCKa=&FhY_}37IzbT=M%U5HeU3ew>YkHBHOHO*08Di|(TO^AL*)+~6=fRgM#p>K z*AkM=B|M7C{D8A4=Y?v4stiofOG;s#fgWv!;*s}}0)pFFPdp-YMSBaRsc5dMg}m%} zVZ6c!Sr&R@&*D>yDRBp|e4egadXDSh(mCG-%~B+=H_%Vs+QZKZsiJE12`IOGBa{Uq z`;c-Bv}6d z3~jy}&qilS9Pv-7WE_WHSZV1M-4m3mDip~nQK;XU2N;G%k&J|H=VDLt#Y^P&O?w() zsz6xyfw8f}mql$UYum+ga4nE#Ig#$Dq_Yc~@;1VSNRHQCBk+#6J(87grAkL8U`M705Q!dSQ^>$7&z!Lq{cnwV3n6LVosM=^agpzpf%mn1YP9O_<*M-x%+k z@#1*Mu}Y7iUdS2zIi5vD5mO|iZ&QxG;OZ=JeTb2x_H<2 zFPUV?G++L&rmTf+{g7*46EMpaG#$onl2W9DYm#;R&OIlNv~uUND>Qm4I&$=x^pV2c z`^yQ_s|L#S64<4dD*bxPEFJ{{Sq7`^&~lpwxk}<$Rbt zIeDYAqSqM6N@4;>ea7lO_BAzTRoK(T0|M$BQ5L#?F@dIq{0}5s z^8_3#jT1+N5;JNLGlku^KT-TYJU>s8*AWvm2SltFQiIg;#5R%$myR6l4~%nIWv*

RD@9Ewb>0AWwT1q;U%HDv+ajoY0ppqT`h1>j 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

xXM-AgrggSR!P*iB8?vU*mjj;+hu(-Yl`n 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 time and - self.user_remaining < cost) - - def __str__(self, time=None): - 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/imgurpython/main.py b/imgurpython/main.py deleted file mode 100644 index 79111a8..0000000 --- a/imgurpython/main.py +++ /dev/null @@ -1,247 +0,0 @@ -#!/usr/bin/env python3 - -import sys -import json -import math -from .imgur.factory import Factory -from .imgur.auth.expired import Expired -from helpers import format - -try: - from urllib.request import urlopen as urlllibopen - from urllib.request import HTTPError -except ImportError: - from urllib2 import urlopen as urlllibopen - from urllib2 import HTTPError - -authorized_commands = [ - 'upload-auth', - 'comment', - 'vote-gallery', - 'vote-comment', - 'add-album-image', - 'replies' -] - -oauth_commands = [ - 'credits', - 'refresh', - 'authorize' -] - -unauth_commands = [ - 'upload', - 'list-comments', - 'get-album', - 'get-comment', - 'get-gallery' -] - - -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\'')) - lines.append(('auth', 'add-album-image [access-token] [album-id] [image-id]', 'Add an image to an album')) - - headers = { - 'oauth': 'OAuth Actions', - 'anon': 'Unauthorized Actions', - 'auth': 'Authorized Actions' - } - - category_headers_output_so_far = [] - 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 not cat in category_headers_output_so_far: - print('\n' + format.center_pad(headers[cat], col_width) + '\n') - category_headers_output_so_far.append(cat) - print(format.two_column_with_period(text, desc, col_width) + '\n') - - sys.exit(1) - - -def main(): - usage(sys.argv) - - try: - fd = open('data/config.json', 'r') - except IOError: - print('Config file [config.json] not found.') - sys.exit(1) - - try: - config = json.loads(fd.read()) - except ValueError: - print('Invalid JSON in config file.') - sys.exit(1) - - mfactory = Factory(config) - action = sys.argv[1] - - if action in authorized_commands: - handle_authorized_commands(mfactory, action) - elif action in oauth_commands: - handle_oauth_commands(mfactory, config, action) - elif action in unauth_commands: - handle_unauthorized_commands(mfactory, action) - else: - print('Invalid command provided! Use --help to see all available actions.') - - -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) - elif action == 'comment': - item_hash = 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_post_request(('gallery', item_hash, 'comment'), { - 'comment': text - }) - elif action == 'add-album-image': - album_id = sys.argv[3] - image_ids = ','.join(sys.argv[4:]) - - req = factory.build_put_request(('album', album_id), { - 'ids[]': image_ids - }, 'PUT') - - elif action in ('vote-gallery', 'vote-comment'): - (target_id, vote) = sys.argv[3:] - - if action == 'vote-gallery': - target = ('gallery', target_id, 'vote', vote) - else: - target = ('comment', target_id, 'vote', vote) - - req = factory.build_get_request(target, "") - elif action == 'replies': - req = factory.build_get_request(('account', 'me', 'notifications', 'replies'), {'new': 'false'}) - - 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' % (item_hash, 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_get_request(('credits',)) - res = imgur.retrieve(req) - print(res) - elif 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'] - )) - elif action == 'authorize': - if len(sys.argv) == 2: - print('Visit this URL to get a PIN to authorize: %soauth2/authorize?client_id=%s&response_type=pin' % ( - factory.get_api_url(), - config['client_id'] - )) - 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() - - 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': - item_hash = sys.argv[2] - req = factory.build_get_request(('gallery', item_hash, 'comments')) - - if action == 'get-album': - id = sys.argv[2] - req = factory.build_get_request(('album', id)) - - if action == 'get-comment': - cid = sys.argv[2] - req = factory.build_get_request(('comment', cid)) - - if action == 'get-gallery': - id = sys.argv[2] - req = factory.build_get_request(('gallery', id)) - - res = imgur.retrieve(req) - print(res) - - -if __name__ == "__main__": - main() diff --git a/runner.py b/runner.py new file mode 100644 index 0000000..6732023 --- /dev/null +++ b/runner.py @@ -0,0 +1,5 @@ +from imgurpython import ImgurClient + +if __name__ == "__main__": + c = ImgurClient('d678b0301ba0dad', 'f2a1b2e3d9310a2f9f1d5a0a078a782c0d94e6b7') + print(c.gallery()) \ No newline at end of file diff --git a/setup.py b/setup.py index 9a64a7b..1c84d09 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ # 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', + version='1.1.1', description='Official Imgur python library with OAuth2 and samples', long_description='', @@ -65,7 +65,7 @@ # 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={'imgurpython': ['data/res/*', 'data/config.json.sample', 'docs/LICENSE']}, + package_data={}, # Although 'package_data' is the preferred approach, in some case you may # need to place data files outside of your packages. From 9d95f03b991b34690f55e95dae643efa63d32d61 Mon Sep 17 00:00:00 2001 From: jasdev Date: Thu, 9 Oct 2014 13:44:00 -0700 Subject: [PATCH 49/89] Removed runner --- runner.py | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 runner.py diff --git a/runner.py b/runner.py deleted file mode 100644 index 6732023..0000000 --- a/runner.py +++ /dev/null @@ -1,5 +0,0 @@ -from imgurpython import ImgurClient - -if __name__ == "__main__": - c = ImgurClient('d678b0301ba0dad', 'f2a1b2e3d9310a2f9f1d5a0a078a782c0d94e6b7') - print(c.gallery()) \ No newline at end of file From d27d3e19ff003f3536de37d666e4709650d0badc Mon Sep 17 00:00:00 2001 From: jasdev Date: Thu, 9 Oct 2014 13:55:00 -0700 Subject: [PATCH 50/89] Readme update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dc016e6..05bf98c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ imgurpython =========== -A Python client for the [Imgur API](http://api.imgur.com/). Also includes a friendly demo application. It can be used to +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 From 9eccadfa3749b9a47ecd74cc04fa5138830d69d2 Mon Sep 17 00:00:00 2001 From: Jacob Greenleaf Date: Fri, 17 Oct 2014 22:49:52 +0000 Subject: [PATCH 51/89] Note requests and python version requirement in README --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 05bf98c..4a2005a 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,14 @@ You must [register](http://api.imgur.com/oauth2/addclient) your client with the 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 + + pip packages: + - [requests](http://docs.python-requests.org/en/latest/user/install/) + Imgur API Documentation ----------------------- @@ -199,4 +207,4 @@ except ImgurClientError as e ### Memegen -* `default_memes()` \ No newline at end of file +* `default_memes()` From 94fd46bb785001dda040bdcabc2c8406ae19c825 Mon Sep 17 00:00:00 2001 From: Jasdev Singh Date: Sun, 19 Oct 2014 00:18:48 -0700 Subject: [PATCH 52/89] Update README.md --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4a2005a..20dab4e 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,8 @@ perform actions on accounts, the user will have to authorize your application th Requirements ------------ - Python >= 2.7 - - pip packages: - - [requests](http://docs.python-requests.org/en/latest/user/install/) +- Python >= 2.7 +- [requests](http://docs.python-requests.org/en/latest/user/install/) Imgur API Documentation ----------------------- From cd7616ad6fa717413a4a544c030da4426f32d027 Mon Sep 17 00:00:00 2001 From: Bill Wiens Date: Wed, 29 Oct 2014 11:01:36 -0700 Subject: [PATCH 53/89] Add missing colon in code example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 20dab4e..99c242e 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ client = ImgurClient(client_id, client_secret) # Example request items = client.gallery() -for item in items +for item in items: print(item.link) ``` From aead5dda30b3d6b8ca90f0c81b2d861e166d90d9 Mon Sep 17 00:00:00 2001 From: jasdev Date: Wed, 29 Oct 2014 21:39:50 -0700 Subject: [PATCH 54/89] Refreshing gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 298a1fb..fe956e6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ config.json dist build imgurpython.egg-info +runner.py \ No newline at end of file From c26964cd95bd2025e42467510057436286ab90e2 Mon Sep 17 00:00:00 2001 From: Muntaser Ahmed Date: Thu, 30 Oct 2014 12:01:55 -0400 Subject: [PATCH 55/89] pep8 says do a barrel roll --- imgurpython/client.py | 6 ++++-- imgurpython/helpers/format.py | 10 +++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/imgurpython/client.py b/imgurpython/client.py index e08923e..541b983 100644 --- a/imgurpython/client.py +++ b/imgurpython/client.py @@ -558,7 +558,8 @@ def get_image(self, image_id): return Image(image) def upload_from_path(self, path, config=None, anon=True): - if not config: config = dict() + if not config: + config = dict() fd = open(path, 'rb') contents = fd.read() @@ -573,7 +574,8 @@ def upload_from_path(self, path, config=None, anon=True): return self.make_request('POST', 'upload', data, anon) def upload_from_url(self, url, config=None, anon=True): - if not config: config = dict() + if not config: + config = dict() data = { 'image': url, diff --git a/imgurpython/helpers/format.py b/imgurpython/helpers/format.py index b36536c..70cb39f 100644 --- a/imgurpython/helpers/format.py +++ b/imgurpython/helpers/format.py @@ -71,11 +71,11 @@ def build_notifications(response): def build_notification(item): notification = Notification( - item['id'], - item['account_id'], - item['viewed'], - item['content'] - ) + item['id'], + item['account_id'], + item['viewed'], + item['content'] + ) if 'comment' in notification.content: notification.content = format_comment_tree(item['content']) From 059f6b8a7106c1f24e80eb88b4e7a4d27ff244dd Mon Sep 17 00:00:00 2001 From: Michael Recachinas Date: Thu, 30 Oct 2014 22:49:12 -0400 Subject: [PATCH 56/89] Removed comma `self.created = created,` builds a tuple in `self.created`. This appears to be a mistake. See below for an example: ```python >>> x = 3, >>> x (3,) ``` --- imgurpython/imgur/models/account.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imgurpython/imgur/models/account.py b/imgurpython/imgur/models/account.py index 4f1f9a7..8bc2827 100644 --- a/imgurpython/imgur/models/account.py +++ b/imgurpython/imgur/models/account.py @@ -5,5 +5,5 @@ def __init__(self, id, url, bio, reputation, created, pro_expiration): self.url = url self.bio = bio self.reputation = reputation - self.created = created, + self.created = created self.pro_expiration = pro_expiration From 2c5d1603f9ff9b0e6ee764d19828a30630c973cf Mon Sep 17 00:00:00 2001 From: jasdev Date: Thu, 30 Oct 2014 20:21:05 -0700 Subject: [PATCH 57/89] Bugs fixes and version bump --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1c84d09..b95b495 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ # 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.1', + version='1.1.2', description='Official Imgur python library with OAuth2 and samples', long_description='', From 6635a698531df9da9f4fbf93181273b3fd1b5062 Mon Sep 17 00:00:00 2001 From: jasdev Date: Wed, 5 Nov 2014 22:23:16 -0800 Subject: [PATCH 58/89] Adding rate limit information to resolve #17 --- README.md | 12 ++++++++++++ imgurpython/client.py | 13 +++++++++++++ 2 files changed, 25 insertions(+) diff --git a/README.md b/README.md index 99c242e..1ab4395 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,18 @@ except ImgurClientError as e * 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)! + ## ImgurClient Functions ### Account diff --git a/imgurpython/client.py b/imgurpython/client.py index 541b983..f54e9f2 100644 --- a/imgurpython/client.py +++ b/imgurpython/client.py @@ -80,12 +80,17 @@ def __init__(self, client_id, client_secret, access_token=None, refresh_token=No 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) @@ -126,6 +131,14 @@ def make_request(self, method, route, data=None, force_anon=False): 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() From e7585c37f36bba4f26d22ed79299636f02e5d16d Mon Sep 17 00:00:00 2001 From: jasdev Date: Wed, 5 Nov 2014 22:24:25 -0800 Subject: [PATCH 59/89] Bumping pypi version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b95b495..1ba4f53 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ # 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.2', + version='1.1.3', description='Official Imgur python library with OAuth2 and samples', long_description='', From 412148a6f8ad5b1473b85e9b4de37b8bae86eab0 Mon Sep 17 00:00:00 2001 From: Yves Dorfsman Date: Fri, 14 Nov 2014 05:09:15 -0700 Subject: [PATCH 60/89] Adding non-API entry points to README --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 1ab4395..7d86451 100644 --- a/README.md +++ b/README.md @@ -218,3 +218,13 @@ For more information about rate-limiting, please see the note in our [docs](http ### 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/{image_id} | gallery | From 9c534adcefbdf140f5d761b3113c5715b4fdf12f Mon Sep 17 00:00:00 2001 From: Yves Dorfsman Date: Fri, 14 Nov 2014 19:15:34 -0700 Subject: [PATCH 61/89] added import for ImgurClientError in README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 1ab4395..c781c96 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,8 @@ Error types * ImgurClientError - General error handler, access message and status code via ```python +from imgurpython.helpers.error import ImgurClientError + try ... except ImgurClientError as e From 81d6772e7e2576a2e8840eceb406a6a6fc3165f0 Mon Sep 17 00:00:00 2001 From: ueg1990 Date: Tue, 18 Nov 2014 21:15:57 -0500 Subject: [PATCH 62/89] Add example to print links of a Gallery --- README.md | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/README.md b/README.md index c781c96..e184383 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,66 @@ To view client and user credit information, use the `credits` attribute of `Imgu For more information about rate-limiting, please see the note in our [docs](http://api.imgur.com/#limits)! +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 meme links 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 meme links 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 + ... + ... + ## ImgurClient Functions ### Account From f92b4f0bb0b87183066330cb7fd628e8c25372c3 Mon Sep 17 00:00:00 2001 From: ueg1990 Date: Wed, 19 Nov 2014 00:22:32 -0500 Subject: [PATCH 63/89] Update README.md --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e184383..8645b9b 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ Examples Output links from gallery could be a GalleyImage or GalleryAlbum #### Default -By default, this will return meme links on the first page (0) with section 'hot' sorted by 'viral', date range is 'day' and show_viral is set to True +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() @@ -148,10 +148,9 @@ for item in items: http://i.imgur.com/EbeztS2.jpg http://i.imgur.com/DuwnhKO.jpg ... - ... #### With Specific Parameters -In this example, return meme links on the fourth page (3) with section 'top' sorted by 'time', date range is 'week' and show_viral is set to False +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) @@ -174,7 +173,6 @@ for item in items: http://i.imgur.com/gWaNC22.jpg http://i.imgur.com/YEQomCd.gif ... - ... ## ImgurClient Functions From eafe1de5c2b2e538eb82538177bfb90b7651ba12 Mon Sep 17 00:00:00 2001 From: Yves Dorfsman Date: Sat, 22 Nov 2014 18:00:03 -0700 Subject: [PATCH 64/89] replaced image_id by gallery_post_id as per jacobgreenleaf suggestion. --- README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 7d86451..6047857 100644 --- a/README.md +++ b/README.md @@ -221,10 +221,11 @@ For more information about rate-limiting, please see the note in our [docs](http 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/{image_id} | gallery | +| 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 | + From 98fe9a86b1fa92dd6bea392cc45bafdc05b0398d Mon Sep 17 00:00:00 2001 From: jasdev Date: Mon, 8 Dec 2014 18:30:49 -0800 Subject: [PATCH 65/89] Fixing code authorize step, resolves #26 --- imgurpython/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imgurpython/client.py b/imgurpython/client.py index f54e9f2..0875f14 100644 --- a/imgurpython/client.py +++ b/imgurpython/client.py @@ -99,7 +99,7 @@ def authorize(self, response, grant_type='pin'): 'client_id': self.client_id, 'client_secret': self.client_secret, 'grant_type': grant_type, - grant_type: response + 'code' if grant_type == 'authorization_code' else grant_type: response }, True) def prepare_headers(self, force_anon=False): From aa30c3f4d106c0660f27990cf22e9be6e48abe30 Mon Sep 17 00:00:00 2001 From: jasdev Date: Mon, 8 Dec 2014 18:32:02 -0800 Subject: [PATCH 66/89] Bumping version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1ba4f53..2cf8866 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ # 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.3', + version='1.1.4', description='Official Imgur python library with OAuth2 and samples', long_description='', From 2421467e1f25f800496b3ac457850048d449af00 Mon Sep 17 00:00:00 2001 From: jasdev Date: Wed, 10 Dec 2014 10:43:31 -0800 Subject: [PATCH 67/89] Removing uses of id variable, fixes #24 --- imgurpython/imgur/models/account.py | 4 ++-- imgurpython/imgur/models/conversation.py | 4 ++-- imgurpython/imgur/models/custom_gallery.py | 4 ++-- imgurpython/imgur/models/message.py | 4 ++-- imgurpython/imgur/models/notification.py | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/imgurpython/imgur/models/account.py b/imgurpython/imgur/models/account.py index 8bc2827..4598bfa 100644 --- a/imgurpython/imgur/models/account.py +++ b/imgurpython/imgur/models/account.py @@ -1,7 +1,7 @@ class Account: - def __init__(self, id, url, bio, reputation, created, pro_expiration): - self.id = id + def __init__(self, account_id, url, bio, reputation, created, pro_expiration): + self.id = account_id self.url = url self.bio = bio self.reputation = reputation diff --git a/imgurpython/imgur/models/conversation.py b/imgurpython/imgur/models/conversation.py index 9d14554..9e8c12e 100644 --- a/imgurpython/imgur/models/conversation.py +++ b/imgurpython/imgur/models/conversation.py @@ -2,9 +2,9 @@ class Conversation: - def __init__(self, id, last_message_preview, datetime, with_account_id, with_account, message_count, messages=None, + def __init__(self, conversation_id, last_message_preview, datetime, with_account_id, with_account, message_count, messages=None, done=None, page=None): - self.id = id + self.id = conversation_id self.last_message_preview = last_message_preview self.datetime = datetime self.with_account_id = with_account_id diff --git a/imgurpython/imgur/models/custom_gallery.py b/imgurpython/imgur/models/custom_gallery.py index 2752764..9f0f78a 100644 --- a/imgurpython/imgur/models/custom_gallery.py +++ b/imgurpython/imgur/models/custom_gallery.py @@ -4,8 +4,8 @@ class CustomGallery: - def __init__(self, id, name, datetime, account_url, link, tags, item_count=None, items=None): - self.id = id + 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 diff --git a/imgurpython/imgur/models/message.py b/imgurpython/imgur/models/message.py index 185a0f4..5da36b2 100644 --- a/imgurpython/imgur/models/message.py +++ b/imgurpython/imgur/models/message.py @@ -1,7 +1,7 @@ class Message: - def __init__(self, id, from_user, account_id, sender_id, body, conversation_id, datetime): - self.id = id + 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 diff --git a/imgurpython/imgur/models/notification.py b/imgurpython/imgur/models/notification.py index 3da26b6..4a333a1 100644 --- a/imgurpython/imgur/models/notification.py +++ b/imgurpython/imgur/models/notification.py @@ -1,7 +1,7 @@ class Notification: - def __init__(self, id, account_id, viewed, content): - self.id = id + def __init__(self, notification_id, account_id, viewed, content): + self.id = notification_id self.account_id = account_id self.viewed = viewed self.content = content From aa5dbee40dcb368bbc5adc7cb6d82c6b6a74f136 Mon Sep 17 00:00:00 2001 From: jasdev Date: Wed, 10 Dec 2014 10:45:15 -0800 Subject: [PATCH 68/89] Bumping version to 1.1.5 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2cf8866..46982b1 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ # 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.4', + version='1.1.5', description='Official Imgur python library with OAuth2 and samples', long_description='', From c25cf7e8704d52f95afe9a0cf7899006b3e16333 Mon Sep 17 00:00:00 2001 From: ueg1990 Date: Wed, 10 Dec 2014 18:41:56 -0500 Subject: [PATCH 69/89] More examples to EXAMPLES.md --- EXAMPLES.md | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 57 +---------------------------------------------------- 2 files changed, 58 insertions(+), 56 deletions(-) create mode 100644 EXAMPLES.md diff --git a/EXAMPLES.md b/EXAMPLES.md new file mode 100644 index 0000000..1e0a585 --- /dev/null +++ b/EXAMPLES.md @@ -0,0 +1,57 @@ +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 + ... \ No newline at end of file diff --git a/README.md b/README.md index 96e2d64..e38054f 100644 --- a/README.md +++ b/README.md @@ -118,61 +118,7 @@ For more information about rate-limiting, please see the note in our [docs](http 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 - ... +Examples can be found [here](EXAMPLES.md) ## ImgurClient Functions @@ -210,7 +156,6 @@ for item in items: ### Comment * `get_comment(comment_id)` * `delete_comment(comment_id)` -* `create_album(fields)` * `get_comment_replies(comment_id)` * `post_comment_reply(comment_id, image_id, comment)` * `comment_vote(comment_id, vote='up')` From 95bce34ab2a67e929b17cce97e96a97f20fc0e7d Mon Sep 17 00:00:00 2001 From: ueg1990 Date: Sun, 14 Dec 2014 17:04:08 -0500 Subject: [PATCH 70/89] Convert old style classes to new style --- imgurpython/client.py | 4 ++-- imgurpython/imgur/models/account.py | 2 +- imgurpython/imgur/models/account_settings.py | 2 +- imgurpython/imgur/models/album.py | 2 +- imgurpython/imgur/models/comment.py | 2 +- imgurpython/imgur/models/conversation.py | 2 +- imgurpython/imgur/models/custom_gallery.py | 2 +- imgurpython/imgur/models/gallery_album.py | 2 +- imgurpython/imgur/models/gallery_image.py | 2 +- imgurpython/imgur/models/image.py | 2 +- imgurpython/imgur/models/message.py | 2 +- imgurpython/imgur/models/notification.py | 2 +- imgurpython/imgur/models/tag.py | 2 +- imgurpython/imgur/models/tag_vote.py | 2 +- 14 files changed, 15 insertions(+), 15 deletions(-) diff --git a/imgurpython/client.py b/imgurpython/client.py index 0875f14..f247d73 100644 --- a/imgurpython/client.py +++ b/imgurpython/client.py @@ -19,7 +19,7 @@ API_URL = 'https://api.imgur.com/' -class AuthWrapper: +class AuthWrapper(object): def __init__(self, access_token, refresh_token, client_id, client_secret): self.current_access_token = access_token @@ -55,7 +55,7 @@ def refresh(self): self.current_access_token = response_data['access_token'] -class ImgurClient: +class ImgurClient(object): allowed_album_fields = { 'ids', 'title', 'description', 'privacy', 'layout', 'cover' } diff --git a/imgurpython/imgur/models/account.py b/imgurpython/imgur/models/account.py index 4598bfa..01a798a 100644 --- a/imgurpython/imgur/models/account.py +++ b/imgurpython/imgur/models/account.py @@ -1,4 +1,4 @@ -class Account: +class Account(object): def __init__(self, account_id, url, bio, reputation, created, pro_expiration): self.id = account_id diff --git a/imgurpython/imgur/models/account_settings.py b/imgurpython/imgur/models/account_settings.py index 93d2da5..045aaf2 100644 --- a/imgurpython/imgur/models/account_settings.py +++ b/imgurpython/imgur/models/account_settings.py @@ -1,4 +1,4 @@ -class AccountSettings: +class AccountSettings(object): def __init__(self, email, high_quality, public_images, album_privacy, pro_expiration, accepted_gallery_terms, active_emails, messaging_enabled, blocked_users): diff --git a/imgurpython/imgur/models/album.py b/imgurpython/imgur/models/album.py index 6c49507..414acc2 100644 --- a/imgurpython/imgur/models/album.py +++ b/imgurpython/imgur/models/album.py @@ -1,4 +1,4 @@ -class Album: +class Album(object): # See documentation at https://api.imgur.com/ for available fields def __init__(self, *initial_data, **kwargs): diff --git a/imgurpython/imgur/models/comment.py b/imgurpython/imgur/models/comment.py index 49e343c..29e4a9f 100644 --- a/imgurpython/imgur/models/comment.py +++ b/imgurpython/imgur/models/comment.py @@ -1,4 +1,4 @@ -class Comment: +class Comment(object): # See documentation at https://api.imgur.com/ for available fields def __init__(self, *initial_data, **kwargs): diff --git a/imgurpython/imgur/models/conversation.py b/imgurpython/imgur/models/conversation.py index 9e8c12e..335196c 100644 --- a/imgurpython/imgur/models/conversation.py +++ b/imgurpython/imgur/models/conversation.py @@ -1,6 +1,6 @@ from .message import Message -class Conversation: +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): diff --git a/imgurpython/imgur/models/custom_gallery.py b/imgurpython/imgur/models/custom_gallery.py index 9f0f78a..912ba6d 100644 --- a/imgurpython/imgur/models/custom_gallery.py +++ b/imgurpython/imgur/models/custom_gallery.py @@ -2,7 +2,7 @@ from .gallery_image import GalleryImage -class CustomGallery: +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 diff --git a/imgurpython/imgur/models/gallery_album.py b/imgurpython/imgur/models/gallery_album.py index e947410..1622c99 100644 --- a/imgurpython/imgur/models/gallery_album.py +++ b/imgurpython/imgur/models/gallery_album.py @@ -1,4 +1,4 @@ -class GalleryAlbum: +class GalleryAlbum(object): # See documentation at https://api.imgur.com/ for available fields def __init__(self, *initial_data, **kwargs): diff --git a/imgurpython/imgur/models/gallery_image.py b/imgurpython/imgur/models/gallery_image.py index 73cc7ff..88faf19 100644 --- a/imgurpython/imgur/models/gallery_image.py +++ b/imgurpython/imgur/models/gallery_image.py @@ -1,4 +1,4 @@ -class GalleryImage: +class GalleryImage(object): # See documentation at https://api.imgur.com/ for available fields def __init__(self, *initial_data, **kwargs): diff --git a/imgurpython/imgur/models/image.py b/imgurpython/imgur/models/image.py index bd02b0c..18f257e 100644 --- a/imgurpython/imgur/models/image.py +++ b/imgurpython/imgur/models/image.py @@ -1,4 +1,4 @@ -class Image: +class Image(object): # See documentation at https://api.imgur.com/ for available fields def __init__(self, *initial_data, **kwargs): diff --git a/imgurpython/imgur/models/message.py b/imgurpython/imgur/models/message.py index 5da36b2..0f98f5e 100644 --- a/imgurpython/imgur/models/message.py +++ b/imgurpython/imgur/models/message.py @@ -1,4 +1,4 @@ -class Message: +class Message(object): def __init__(self, message_id, from_user, account_id, sender_id, body, conversation_id, datetime): self.id = message_id diff --git a/imgurpython/imgur/models/notification.py b/imgurpython/imgur/models/notification.py index 4a333a1..7966953 100644 --- a/imgurpython/imgur/models/notification.py +++ b/imgurpython/imgur/models/notification.py @@ -1,4 +1,4 @@ -class Notification: +class Notification(object): def __init__(self, notification_id, account_id, viewed, content): self.id = notification_id diff --git a/imgurpython/imgur/models/tag.py b/imgurpython/imgur/models/tag.py index 60a02ce..d9f8547 100644 --- a/imgurpython/imgur/models/tag.py +++ b/imgurpython/imgur/models/tag.py @@ -2,7 +2,7 @@ from .gallery_image import GalleryImage -class Tag: +class Tag(object): def __init__(self, name, followers, total_items, following, items): self.name = name diff --git a/imgurpython/imgur/models/tag_vote.py b/imgurpython/imgur/models/tag_vote.py index 6ac444f..eb1a995 100644 --- a/imgurpython/imgur/models/tag_vote.py +++ b/imgurpython/imgur/models/tag_vote.py @@ -1,4 +1,4 @@ -class TagVote: +class TagVote(object): def __init__(self, ups, downs, name, author): self.ups = ups From 1f1f67e3458e0018555a7aa284215caaacec1d92 Mon Sep 17 00:00:00 2001 From: Edwin Amsler Date: Sat, 14 Feb 2015 20:55:34 -0600 Subject: [PATCH 71/89] Add an example with usernames Hopefully this should save some users from trying to figure out what to put in the 'username' field before diving into the data model. --- EXAMPLES.md | 47 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index 1e0a585..ecc8523 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -45,13 +45,40 @@ for item in items: 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 - ... \ No newline at end of file + 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('ID: {0} - {1}'.format(album.id, album_title)) + + 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 + ... + + From c0cda99298d9f9b9df76650b74ff25f77b8b7e1d Mon Sep 17 00:00:00 2001 From: Edwin Amsler Date: Sat, 14 Feb 2015 20:56:30 -0600 Subject: [PATCH 72/89] Fix my formatting --- EXAMPLES.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index ecc8523..fecd718 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -63,7 +63,7 @@ For endpoints that require usernames, once a user is authenticated we can use th ```python for album in client.get_account_albums('me'): album_title = album.title if album.title else 'Untitled' - print('ID: {0} - {1}'.format(album.id, album_title)) + 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' @@ -75,10 +75,11 @@ For endpoints that require usernames, once a user is authenticated we can use th ***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 - ... + + Album: Qittens! (LPNnY) + Untitled: http://i.imgur.com/b9rL7ew.jpg + Untitled: http://i.imgur.com/Ymg3obW.jpg + Untitled: http://i.imgur.com/kMzbu0S.jpg + ... From fc6b1d0596578366f70fe21d43590cc0b20a7e44 Mon Sep 17 00:00:00 2001 From: Edwin Amsler Date: Sat, 14 Feb 2015 20:57:31 -0600 Subject: [PATCH 73/89] Yet more formatting I need to avoid using the Github website to test this stuff. --- EXAMPLES.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index fecd718..2ec2cb7 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -61,16 +61,16 @@ for item in items: 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 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)) +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 +# Save some API credits by not getting all albums +break ``` ***Output*** From c7ab5d8741e1cf129e0222d7c85ead3c8a2afe19 Mon Sep 17 00:00:00 2001 From: Edwin Amsler Date: Sun, 15 Feb 2015 22:59:46 -0600 Subject: [PATCH 74/89] Create standalone auth example This will also save some typing for other examples --- examples/auth.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100755 examples/auth.py diff --git a/examples/auth.py b/examples/auth.py new file mode 100755 index 0000000..14b303d --- /dev/null +++ b/examples/auth.py @@ -0,0 +1,48 @@ +#!/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 here. +''' + +#client_id = 'YOUR CLIENT ID' +#client_secret = 'YOUR CLIENT SECRET' + +client_id = u'6d2d7ee5f212dc1' +client_secret = u'18828c1021069154576672e86b8ab2e1559d329a' + + +from imgurpython import ImgurClient + +def get_input(string): + ''' Get input from console regardless of python 2 or 3 ''' + try: + return raw_input(string) + except: + return input(string) + +def authenticate(): + 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 From faa971adf88b769da5d2878cbbd66e36d9728d42 Mon Sep 17 00:00:00 2001 From: Edwin Amsler Date: Sun, 15 Feb 2015 23:45:54 -0600 Subject: [PATCH 75/89] Add an upload example --- examples/upload.py | 47 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100755 examples/upload.py 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 From ccb3464a87e1f2909fff6c6e69a11952cdd4eadc Mon Sep 17 00:00:00 2001 From: Edwin Amsler Date: Mon, 16 Feb 2015 00:08:43 -0600 Subject: [PATCH 76/89] Move credentials to an INI file So that I can add it to my ignore list and not have people accidentally commit their credentials with code --- examples/auth.ini | 4 ++++ examples/auth.py | 22 +++++++++++++++------- 2 files changed, 19 insertions(+), 7 deletions(-) create mode 100644 examples/auth.ini 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 index 14b303d..88c65fc 100755 --- a/examples/auth.py +++ b/examples/auth.py @@ -6,13 +6,6 @@ set up a test user with API credentials and set them up in here. ''' -#client_id = 'YOUR CLIENT ID' -#client_secret = 'YOUR CLIENT SECRET' - -client_id = u'6d2d7ee5f212dc1' -client_secret = u'18828c1021069154576672e86b8ab2e1559d329a' - - from imgurpython import ImgurClient def get_input(string): @@ -22,7 +15,22 @@ def get_input(string): except: return input(string) +def get_config(): + ''' More version compatibility stuff ''' + try: + import ConfigParser + return ConfigParser.ConfigParser() + except: + import configparser + return configparser.ConfigParser() + def authenticate(): + # Get client ID and secret from auth.ini + config = get_config() + config.read('auth.ini') + client_id = config['credentials']['client_id'] + client_secret = config['credentials']['client_secret'] + client = ImgurClient(client_id, client_secret) # Authorization flow, pin example (see docs for other auth types) From 00eed5f21a4f5c184f969e4ca49c8e7c1704ceeb Mon Sep 17 00:00:00 2001 From: Edwin Amsler Date: Mon, 16 Feb 2015 00:09:51 -0600 Subject: [PATCH 77/89] Ignore the auth.ini file --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index fe956e6..bd3907a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ config.json dist build imgurpython.egg-info -runner.py \ No newline at end of file +runner.py +examples/auth.ini From 052b0266f3a0f32496bad6d5a0bcb5ae29000cdf Mon Sep 17 00:00:00 2001 From: Edwin Amsler Date: Mon, 16 Feb 2015 00:14:43 -0600 Subject: [PATCH 78/89] Move non-helpful stuff into their own file --- examples/auth.py | 17 +---------------- examples/helpers.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 16 deletions(-) create mode 100644 examples/helpers.py diff --git a/examples/auth.py b/examples/auth.py index 88c65fc..656130d 100755 --- a/examples/auth.py +++ b/examples/auth.py @@ -7,22 +7,7 @@ ''' from imgurpython import ImgurClient - -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(): - ''' More version compatibility stuff ''' - try: - import ConfigParser - return ConfigParser.ConfigParser() - except: - import configparser - return configparser.ConfigParser() +from helpers import get_input, get_config def authenticate(): # Get client ID and secret from auth.ini 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 From 24ac383355b5701ff10c6c3698c8fabe97d6fb3c Mon Sep 17 00:00:00 2001 From: Edwin Amsler Date: Mon, 16 Feb 2015 00:20:02 -0600 Subject: [PATCH 79/89] Reference our new INI file --- examples/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/auth.py b/examples/auth.py index 656130d..3ced193 100755 --- a/examples/auth.py +++ b/examples/auth.py @@ -3,7 +3,7 @@ ''' 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 here. + set up a test user with API credentials and set them up in auth.ini. ''' from imgurpython import ImgurClient From 33a5563ab82808589c8c845996991e8c76b21fd9 Mon Sep 17 00:00:00 2001 From: Nate Shoffner Date: Mon, 9 Mar 2015 10:50:18 -0400 Subject: [PATCH 80/89] Use get() with ConfigParser for 2.x compatibility --- examples/auth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/auth.py b/examples/auth.py index 3ced193..2f0b1f4 100755 --- a/examples/auth.py +++ b/examples/auth.py @@ -13,8 +13,8 @@ def authenticate(): # Get client ID and secret from auth.ini config = get_config() config.read('auth.ini') - client_id = config['credentials']['client_id'] - client_secret = config['credentials']['client_secret'] + client_id = config.get('credentials', 'client_id') + client_secret = config.get('credentials', 'client_secret') client = ImgurClient(client_id, client_secret) From 4684e2e2c46bf2aab9e743ce1ea0b7ea925ee51e Mon Sep 17 00:00:00 2001 From: jasdev Date: Thu, 23 Apr 2015 14:09:22 -0700 Subject: [PATCH 81/89] Adding Mashape support --- imgurpython/client.py | 17 +++++++++++++---- setup.py | 2 +- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/imgurpython/client.py b/imgurpython/client.py index f247d73..e19e70e 100644 --- a/imgurpython/client.py +++ b/imgurpython/client.py @@ -17,6 +17,7 @@ from .imgur.models.account_settings import AccountSettings API_URL = 'https://api.imgur.com/' +MASHAPE_URL = 'https://imgur-apiv3.p.mashape.com/' class AuthWrapper(object): @@ -72,10 +73,11 @@ class ImgurClient(object): 'album', 'name', 'title', 'description' } - def __init__(self, client_id, client_secret, access_token=None, refresh_token=None): + 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) @@ -103,20 +105,27 @@ def authorize(self, response, grant_type='pin'): }, 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: - return {'Authorization': 'Client-ID %s' % self.get_client_id()} + headers['Authorization'] = 'Client-ID %s' % self.get_client_id() else: - return {'Authorization': 'Bearer %s' % self.auth.get_current_access_token()} + 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 = API_URL + ('3/%s' % route if 'oauth2' not in route else route) + 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) diff --git a/setup.py b/setup.py index 46982b1..79bcbb8 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ # 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.5', + version='1.1.6', description='Official Imgur python library with OAuth2 and samples', long_description='', From 0aaaa98dc65a31eb234f53e3bd78688f7fb127d4 Mon Sep 17 00:00:00 2001 From: manu Date: Sat, 30 May 2015 12:38:37 +0200 Subject: [PATCH 82/89] adding a "page" parameter to get_gallery_favorites and get_account_favorites --- imgurpython/client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/imgurpython/client.py b/imgurpython/client.py index e19e70e..544330b 100644 --- a/imgurpython/client.py +++ b/imgurpython/client.py @@ -184,15 +184,15 @@ def get_account(self, username): account_data['pro_expiration'], ) - def get_gallery_favorites(self, username): + def get_gallery_favorites(self, username, page=0): self.validate_user_context(username) - gallery_favorites = self.make_request('GET', 'account/%s/gallery_favorites' % 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): + def get_account_favorites(self, username, page=0): self.validate_user_context(username) - favorites = self.make_request('GET', 'account/%s/favorites' % username) + favorites = self.make_request('GET', 'account/%s/favorites/%d' % (username, page)) return build_gallery_images_and_albums(favorites) From 534c2c8d346e7067b78036f7ece9a1d36a6be18e Mon Sep 17 00:00:00 2001 From: Kevin Cramer Date: Thu, 1 Oct 2015 18:05:24 -0700 Subject: [PATCH 83/89] Update support info. --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index e38054f..999a987 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,7 @@ Our developer documentation can be found [here](https://api.imgur.com/). Community --------- -The best way to reach out to Imgur for API support would be our -[Google Group](https://groups.google.com/forum/#!forum/imgur), [Twitter](https://twitter.com/imgurapi), or via - api@imgur.com. +The best way to reach out to Imgur for API support is emailing us at api@imgur.com. Installation ------------ From 33776a80930fe7b6a199b1b86fd8a78786e176e2 Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Sun, 27 Dec 2015 05:28:48 -0500 Subject: [PATCH 84/89] closed file in upload_from_path The file no longer remains open after calling upload_from_path(). This would previously throw warnings. --- imgurpython/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/imgurpython/client.py b/imgurpython/client.py index 544330b..b860a1a 100644 --- a/imgurpython/client.py +++ b/imgurpython/client.py @@ -586,13 +586,13 @@ def upload_from_path(self, path, config=None, anon=True): fd = open(path, 'rb') 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())}) + fd.close() + return self.make_request('POST', 'upload', data, anon) def upload_from_url(self, url, config=None, anon=True): From 3d2d2866770d251bc81c73732fd19fdcaabe012b Mon Sep 17 00:00:00 2001 From: Jacob Greenleaf Date: Fri, 29 Jan 2016 12:54:42 -0800 Subject: [PATCH 85/89] Add TravisYML --- .travis.yml | 9 +++++++++ setup.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..8e11c2f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: python +deploy: + provider: pypi + user: imgurops + password: + secure: DBBXzMOm037T4XUmfo0Gu9mAytw2DCYJT8i0KgihKYxS+uslF+dwHf2clBEWDLUE0xkXhqXetq+sNgfshovGKIqZanASYZ/6Zf5ikg10ApgaBidObv2XMYNyuQxL8Gqv9l2tdlWqdUoOJzRBMV2Nh0B3BJ9hG7V5NFMDcfG/qyo= + on: + tags: true + repo: Imgur/imgurpython diff --git a/setup.py b/setup.py index 79bcbb8..300e786 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ # 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.6', + version='1.1.7', description='Official Imgur python library with OAuth2 and samples', long_description='', From 8694d428046aee4a5ea47dab55ae86e5663b6e7e Mon Sep 17 00:00:00 2001 From: Jacob Greenleaf Date: Fri, 29 Jan 2016 13:04:22 -0800 Subject: [PATCH 86/89] Add requirements.txt to let Travis install requests --- requirements.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f229360 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +requests From c43e2364c26eaeeba58abfa7f4a001528190aa73 Mon Sep 17 00:00:00 2001 From: Jacob Greenleaf Date: Fri, 29 Jan 2016 13:11:33 -0800 Subject: [PATCH 87/89] Do a simple syntax check on Travis script --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 8e11c2f..cd12a9d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,6 @@ language: python +script: + - python -m compileall deploy: provider: pypi user: imgurops From cb717fe7490d304f858f00b459166f8b42900300 Mon Sep 17 00:00:00 2001 From: Khazhismel Date: Wed, 2 Dec 2015 20:45:17 -0500 Subject: [PATCH 88/89] Allow uploading file-like objects, allowing uploading of BytesIO/etc. --- imgurpython/client.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/imgurpython/client.py b/imgurpython/client.py index b860a1a..9c41ea6 100644 --- a/imgurpython/client.py +++ b/imgurpython/client.py @@ -580,10 +580,13 @@ def get_image(self, 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() - fd = open(path, 'rb') contents = fd.read() b64 = base64.b64encode(contents) data = { @@ -591,7 +594,6 @@ def upload_from_path(self, path, config=None, anon=True): 'type': 'base64', } data.update({meta: config[meta] for meta in set(self.allowed_image_fields).intersection(config.keys())}) - fd.close() return self.make_request('POST', 'upload', data, anon) From 1cd23bc471c1682247e9e5afece3305073befd26 Mon Sep 17 00:00:00 2001 From: Kevin Cramer Date: Thu, 19 Oct 2017 13:14:04 -0700 Subject: [PATCH 89/89] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 999a987..c89b846 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +# The imgurpython project is no longer supported. + imgurpython ===========