From 656dc608bea00c9cdaaef9354f8ddac956b2afc9 Mon Sep 17 00:00:00 2001 From: Jared Hobbs Date: Mon, 11 Jan 2016 09:48:06 -0800 Subject: [PATCH 1/6] add setup.py --- setup.py | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100755 setup.py diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..72170c7 --- /dev/null +++ b/setup.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python +from distutils.core import setup + +setup(name='agithub', + version='1.3', + author='Jonathan Paugh', + py_modules=['agithub']) From 8373a472e789e15b10c75d38dacd79b094bf3e4e Mon Sep 17 00:00:00 2001 From: Jared Hobbs Date: Tue, 12 Jan 2016 09:18:18 -0800 Subject: [PATCH 2/6] cleanup/flake8 --- .gitignore | 1 + Facebook.py | 14 ++-- SalesForce.py | 15 ++-- agithub.py | 202 ++++++++++++++++++++++++++---------------------- agithub_test.py | 41 +++++----- mock.py | 13 ++-- test.py | 61 +++++++++------ 7 files changed, 192 insertions(+), 155 deletions(-) diff --git a/.gitignore b/.gitignore index 727943c..592cd7e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ tags *.py[co] +.* diff --git a/Facebook.py b/Facebook.py index af2161b..fe10da7 100644 --- a/Facebook.py +++ b/Facebook.py @@ -1,7 +1,8 @@ from agithub import API, ConnectionProperties, Client + class Facebook(API): - ''' + """ Facebook Graph API The following example taken from @@ -11,12 +12,13 @@ class Facebook(API): >>> fb.facebook.picture.get(redirect='false') {u'data': {u'is_silhouette': False, u'url': - u'https://fbcdn-profile-a.akamaihd.net/hprofile-ak-frc3/t1.0-1/p50x50/1377580_10152203108461729_809245696_n.png'}}) - ''' + u'https://fbcdn-profile-a.akamaihd.net/hprofile-ak-frc3/ + t1.0-1/p50x50/1377580_10152203108461729_809245696_n.png'}}) + """ def __init__(self, *args, **kwargs): props = ConnectionProperties( - api_url = 'graph.facebook.com' - , secure_http = True - ) + api_url='graph.facebook.com', + secure_http=True, + ) self.setClient(Client(*args, **kwargs)) self.setConnectionProperties(props) diff --git a/SalesForce.py b/SalesForce.py index 51fe0d9..5800c97 100644 --- a/SalesForce.py +++ b/SalesForce.py @@ -1,11 +1,14 @@ from agithub import API, ConnectionProperties, Client + class SalesForce(API): - ''' + """ SalesForce.com REST API Example taken from - http://www.salesforce.com/us/developer/docs/api_rest/index_Left.htm#CSHID=quickstart_code.htm|StartTopic=Content%2Fquickstart_code.htm|SkinName=webhelp + http://www.salesforce.com/us/developer/docs/api_rest/ + index_Left.htm#CSHID=quickstart_code.htm| + StartTopic=Content%2Fquickstart_code.htm|SkinName=webhelp >>> from SalesForce import SalesForce >>> sf = SalesForce() @@ -27,11 +30,11 @@ class SalesForce(API): NB: XML is not automically decoded or de-serialized. Patch the Content class to fix this. - ''' + """ def __init__(self, *args, **kwargs): props = ConnectionProperties( - api_url = 'na1.salesforce.com' - , secure_http = True - ) + api_url='na1.salesforce.com', + secure_http=True, + ) self.setClient(Client(*args, **kwargs)) self.setConnectionProperties(props) diff --git a/agithub.py b/agithub.py index 6553365..3c75b95 100644 --- a/agithub.py +++ b/agithub.py @@ -2,43 +2,44 @@ # See COPYING for license details import json import base64 -import re from functools import partial, update_wrapper import sys -if sys.version_info[0:2] > (3,0): - import http.client - import urllib.parse +if sys.version_info[0:2] > (3, 0): + from http.client import HTTPConnection, HTTPSConnection + from urllib.parse import urlencode else: - import httplib as http - http.client = http - import urllib as urllib - urllib.parse = urllib + from httplib import HTTPConnection, HTTPSConnection + from urllib import urlencode -VERSION = [1,3] + class ConnectionError(OSError): + pass + +VERSION = [1, 3] STR_VERSION = 'v' + '.'.join(str(v) for v in VERSION) # These headers are implicitly included in each request; however, each # can be explicitly overridden by the client code. (Used in Client # objects.) _default_headers = { - #XXX: Header field names MUST be lowercase; this is not checked - 'user-agent': 'agithub/' + STR_VERSION - } + # XXX: Header field names MUST be lowercase; this is not checked + 'user-agent': 'agithub/' + STR_VERSION +} + class API(object): - ''' + """ The toplevel object, and the "entry-point" into the client API. Subclass this to develop an application for a particular REST API. Model your __init__ after the Github example. - ''' + """ def __init__(self, *args, **kwargs): - raise Exception ( - 'Please subclass API and override __init__() to' - 'provide a ConnectionProperties object. See the Github' - ' class for an example' - ) + raise Exception( + 'Please subclass API and override __init__() to' + 'provide a ConnectionProperties object. See the Github' + ' class for an example' + ) def setClient(self, client): self.client = client @@ -56,8 +57,9 @@ def __repr__(self): def getheaders(self): return self.client.headers + class Github(API): - '''The agnostic Github API. It doesn't know, and you don't care. + """The agnostic Github API. It doesn't know, and you don't care. >>> from agithub import Github >>> g = Github('user', 'pass') >>> status, data = g.issues.get(filter='subscribed') @@ -81,21 +83,21 @@ class Github(API): NOTE: It is up to you to spell things correctly. A Github object doesn't even try to validate the url you feed it. On the other hand, it automatically supports the full API--so why should you care? - ''' + """ def __init__(self, *args, **kwargs): props = ConnectionProperties( - api_url = 'api.github.com', - secure_http = True, - extra_headers = { - 'accept' : 'application/vnd.github.v3+json' - } - ) - + api_url='api.github.com', + secure_http=True, + extra_headers={ + 'accept': 'application/vnd.github.v3+json' + } + ) self.setClient(Client(*args, **kwargs)) self.setConnectionProperties(props) + class IncompleteRequest(object): - '''IncompleteRequests are partially-built HTTP requests. + """IncompleteRequests are partially-built HTTP requests. They can be built via an HTTP-idiomatic notation, or via "normal" method calls. @@ -113,7 +115,7 @@ class IncompleteRequest(object): >>> Github('user','pass').whatever[1][x][y].post() To understand the method(...) calls, check out github.client.Client. - ''' + """ def __init__(self, client): self.client = client self.url = '' @@ -123,9 +125,8 @@ def __getattr__(self, key): mfun = getattr(self.client, key) fun = partial(mfun, url=self.url) return update_wrapper(fun, mfun) - else: - self.url += '/' + str(key) - return self + self.url += '/' + str(key) + return self __getitem__ = __getattr__ @@ -135,23 +136,23 @@ def __str__(self): def __repr__(self): return '%s: %s' % (self.__class__, self.url) + class Client(object): http_methods = ( - 'head', - 'get', - 'post', - 'put', - 'delete', - 'patch', - ) + 'head', + 'get', + 'post', + 'put', + 'delete', + 'patch', + ) default_headers = {} headers = None - def __init__(self, username=None, - password=None, token=None, - connection_properties=None - ): + def __init__(self, username=None, password=None, token=None, + connection_properties=None): + self.prop = None # Set up connection properties if connection_properties is not None: @@ -161,22 +162,30 @@ def __init__(self, username=None, self.auth_header = None if token is not None: if password is not None: - raise TypeError("You cannot use both password and oauth token authenication") + raise TypeError( + "You cannot use both password and oauth token " + "authenication" + ) self.auth_header = 'Token %s' % token elif username is not None: if password is None: - raise TypeError("You need a password to authenticate as " + username) + raise TypeError( + "You need a password to authenticate as " + username + ) self.username = username self.auth_header = self.hash_pass(password) def setConnectionProperties(self, props): - ''' + """ Initialize the connection properties. This must be called (either by passing connection_properties=... to __init__ or directly) before any request can be sent. - ''' + """ if type(props) is not ConnectionProperties: - raise TypeError("Client.setConnectionProperties: Expected ConnectionProperties object") + raise TypeError( + "Client.setConnectionProperties: " + "Expected ConnectionProperties object" + ) self.prop = props if self.prop.extra_headers is not None: @@ -185,56 +194,66 @@ def setConnectionProperties(self, props): # Enforce case restrictions on self.default_headers tmp_dict = {} - for k,v in self.default_headers.items(): + for k, v in self.default_headers.items(): tmp_dict[k.lower()] = v self.default_headers = tmp_dict - def head(self, url, headers={}, **params): + def head(self, url, headers=None, **params): + headers = headers or {} url += self.urlencode(params) return self.request('HEAD', url, None, headers) - def get(self, url, headers={}, **params): + def get(self, url, headers=None, **params): + headers = headers or {} url += self.urlencode(params) return self.request('GET', url, None, headers) - def post(self, url, body=None, headers={}, **params): + def post(self, url, body=None, headers=None, **params): + headers = headers or {} url += self.urlencode(params) - if not 'content-type' in headers: - # We're doing a json.dumps of body, so let's set the content-type to json + if 'content-type' not in headers: + # We're doing a json.dumps of body, so let's set the + # content-type to json headers['content-type'] = 'application/json' return self.request('POST', url, json.dumps(body), headers) - def put(self, url, body=None, headers={}, **params): + def put(self, url, body=None, headers=None, **params): + headers = headers or {} url += self.urlencode(params) - if not 'content-type' in headers: - # We're doing a json.dumps of body, so let's set the content-type to json + if 'content-type' not in headers: + # We're doing a json.dumps of body, so let's set the + # content-type to json headers['content-type'] = 'application/json' return self.request('PUT', url, json.dumps(body), headers) - def delete(self, url, headers={}, **params): + def delete(self, url, headers=None, **params): + headers = headers or {} url += self.urlencode(params) return self.request('DELETE', url, None, headers) - def patch(self, url, body=None, headers={}, **params): + def patch(self, url, body=None, headers=None, **params): """ - Do a http patch request on the given url with given body, headers and parameters + Do a http patch request on the given url with given body, + headers and parameters. Parameters is a dictionary that will will be urlencoded """ + headers = headers or {} url += self.urlencode(params) - if not 'content-type' in headers: - # We're doing a json.dumps of body, so let's set the content-type to json + if 'content-type' not in headers: + # We're doing a json.dumps of body, so let's set the + # content-type to json headers['content-type'] = 'application/json' return self.request('PATCH', url, json.dumps(body), headers) def request(self, method, url, body, headers): - '''Low-level networking. All HTTP-method methods call this''' + """Low-level networking. All HTTP-method methods call this""" headers = self._fix_headers(headers) if self.auth_header: headers['authorization'] = self.auth_header - #TODO: Context manager + # TODO: Context manager conn = self.get_connection() conn.request(method, url, body, headers) response = conn.getresponse() @@ -248,12 +267,12 @@ def request(self, method, url, body, headers): def _fix_headers(self, headers): # Convert header names to a uniform case tmp_dict = {} - for k,v in headers.items(): + for k, v in headers.items(): tmp_dict[k.lower()] = v headers = tmp_dict # Add default headers (if unspecified) - for k,v in self.default_headers.items(): + for k, v in self.default_headers.items(): if k not in headers: headers[k] = v return headers @@ -261,7 +280,7 @@ def _fix_headers(self, headers): def urlencode(self, params): if not params: return '' - return '?' + urllib.parse.urlencode(params) + return '?%s' % urlencode(params) def hash_pass(self, password): auth_str = ('%s:%s' % (self.username, password)).encode('utf-8') @@ -269,29 +288,30 @@ def hash_pass(self, password): def get_connection(self): if self.prop.secure_http: - conn = http.client.HTTPSConnection(self.prop.api_url) + conn = HTTPSConnection(self.prop.api_url) elif self.auth_header is None: - conn = http.client.HTTPConnection(self.prop.api_url) + conn = HTTPConnection(self.prop.api_url) else: raise ConnectionError( - 'Refusing to authenticate over non-secure (HTTP) connection.') + 'Refusing to authenticate over non-secure (HTTP) connection.' + ) return conn + class Content(object): - ''' + """ Decode a response from the server, respecting the Content-Type field - ''' + """ def __init__(self, response): self.response = response self.body = response.read() - (self.mediatype, self.encoding) = self.get_ctype() + self.mediatype, self.encoding = self.get_ctype() def get_ctype(self): - '''Split the content-type field into mediatype and charset''' + """Split the content-type field into mediatype and charset""" ctype = self.response.getheader('Content-Type') - start = 0 end = 0 try: end = ctype.index(';') @@ -304,43 +324,39 @@ def get_ctype(self): end = ctype.index(';', start) charset = ctype[start:end].rstrip() except: - charset = 'ISO-8859-1' #TODO + charset = 'ISO-8859-1' # TODO - return (mediatype, charset) + return mediatype, charset def decode_body(self): - ''' + """ Decode (and replace) self.body via the charset encoding specified in the content-type header - ''' + """ self.body = self.body.decode(self.encoding) - def processBody(self): - ''' + """ Retrieve the body of the response, encoding it into a usuable form based on the media-type (mime-type) - ''' + """ handlerName = self.mangled_mtype() handler = getattr(self, handlerName, self.x_application_unknown) return handler() - def mangled_mtype(self): - ''' + """ Mangle the media type into a suitable function name - ''' - return self.mediatype.replace('-','_').replace('/','_') - - - ## media-type handlers + """ + return self.mediatype.replace('-', '_').replace('/', '_') + # media-type handlers def x_application_unknown(self): - '''Handler for unknown media-types''' + """Handler for unknown media-types""" return self.body def application_json(self): - '''Handler for application/json media-type''' + """Handler for application/json media-type""" self.decode_body() try: @@ -353,9 +369,9 @@ def application_json(self): text_javascript = application_json # XXX: This isn't technically correct, but we'll hope for the best. # Patches welcome! - # Insert new media-type handlers here + class ConnectionProperties(object): __slots__ = ['api_url', 'secure_http', 'extra_headers'] diff --git a/agithub_test.py b/agithub_test.py index 0cc8790..668f72d 100755 --- a/agithub_test.py +++ b/agithub_test.py @@ -3,6 +3,7 @@ import mock import unittest + class TestGithubObjectCreation(unittest.TestCase): def test_user_pw(self): gh = agithub.Github('korfuri', '1234') @@ -20,8 +21,9 @@ def test_token(self): def test_token_password(self): with self.assertRaises(TypeError): - gh = agithub.Github( - username='korfuri', password='1234', token='deadbeef') + agithub.Github( + username='korfuri', password='1234', token='deadbeef' + ) class TestIncompleteRequest(unittest.TestCase): @@ -36,31 +38,30 @@ def test_pathByGetAttr(self): def test_callMethodDemo(self): rb = self.newIncompleteRequest() - self.assertEqual(rb.path.demo(), - { "methodName" : "demo" - , "args" : () - , "params" : { "url" : "/path" } - }) + self.assertEqual( + rb.path.demo(), + { + "methodName": "demo", + "args": (), + "params": {"url": "/path"} + } + ) + def test_pathByGetItem(self): rb = self.newIncompleteRequest() rb["hug"][1]["octocat"] self.assertEqual(rb.url, "/hug/1/octocat") - def test_callMethodDemo(self): - rb = self.newIncompleteRequest() - self.assertEqual(rb.path.demo(), - { "methodName" : "demo" - , "args" : () - , "params" : { "url" : "/path" } - }) - def test_callMethodTest(self): rb = self.newIncompleteRequest() - self.assertEqual(rb.path.test(), - { "methodName" : "test" - , "args" : () - , "params" : { "url" : "/path" } - }) + self.assertEqual( + rb.path.demo(), + { + "methodName": "test", + "args": (), + "params": {"url": "/path"} + } + ) if __name__ == '__main__': unittest.main() diff --git a/mock.py b/mock.py index 0cb4a60..2b8cc46 100644 --- a/mock.py +++ b/mock.py @@ -3,18 +3,21 @@ class Client(object): http_methods = ('demo', 'test') def __init__(self, username=None, password=None, token=None, - connection_properties=None): + connection_properties=None): pass def setConnectionProperties(self, props): pass - def demo(self, *args,**params): + def demo(self, *args, **params): return self.methodCalled('demo', *args, **params) - def test(self, *args,**params): + def test(self, *args, **params): return self.methodCalled('test', *args, **params) def methodCalled(self, methodName, *args, **params): - return { 'methodName' : methodName, 'args' : args - , 'params' : params } + return { + 'methodName': methodName, + 'args': args, + 'params': params + } diff --git a/test.py b/test.py index 8ef14b6..506c746 100644 --- a/test.py +++ b/test.py @@ -10,6 +10,7 @@ Fail = 'Fail' Skip = 'Skip' + class Test(object): _the_label = 'test' _the_testno = 0 @@ -22,7 +23,7 @@ def gatherTests(self, testObj): print(self.tests) def doTestsFor(self, api): - '''Run all tests over the given API session''' + """Run all tests over the given API session""" results = [] for name, test in self.tests.items(): self._the_label = name @@ -31,26 +32,33 @@ def doTestsFor(self, api): fails = skips = passes = 0 for res in results: if res == Pass: - passes +=1 + passes += 1 elif res == Fail: - fails +=1 + fails += 1 elif res == Skip: - skips +=1 + skips += 1 else: raise ValueError('Bad test result ' + (res)) print( - '\n' - ' Results\n' - '--------------------------------------\n' - 'Tests Run: ', len(results), '\n' - ' Passed: ', passes, '\n' - ' Failed: ', fails, '\n' - ' Skipped: ', skips - ) + '\n' + ' Results\n' + '--------------------------------------\n' + 'Tests Run: ', + len(results), + '\n' + ' Passed: ', + passes, + '\n' + ' Failed: ', + fails, + '\n' + ' Skipped: ', + skips + ) def runTest(self, test, api): - '''Run a single test with the given API session''' + """Run a single test with the given API session""" self._the_testno += 1 (stat, _) = test(api) @@ -59,7 +67,7 @@ def runTest(self, test, api): if stat in [Pass, Fail, Skip]: return stat elif stat < 400: - result = Pass + result = Pass elif stat >= 500: result = Skip else: @@ -69,16 +77,16 @@ def runTest(self, test, api): return result def setlabel(self, lbl): - '''Set the global field _the_label, which is used by runTest''' + """Set the global field _the_label, which is used by runTest""" self._the_label += ' ' + lbl def label(self, result): - '''Print out a test label showing the result''' + """Print out a test label showing the result""" print (result + ':', self._the_testno, self._the_label) def haveAuth(self, api): username = getattr(api.client, 'username', NotImplemented) - if username == NotImplemented or username == None: + if username == NotImplemented or username is None: return False else: return True @@ -109,10 +117,12 @@ def test_userRepos(self, api): # Utility ### + # Session initializers def initAnonymousSession(klass): return klass() + def initAuthenticatedSession(klass, **kwargs): for k in kwargs: if k not in ['username', 'password', 'token']: @@ -123,7 +133,7 @@ def initAuthenticatedSession(klass, **kwargs): # UI def yesno(ans): - '''Convert user input (Yes or No) to a boolean''' + """Convert user input (Yes or No) to a boolean""" ans = ans.lower() if ans == 'y' or ans == 'yes': return True @@ -140,17 +150,18 @@ def yesno(ans): authSession = None ans = input( - 'Some of the tests require an authenticated session.' - ' Do you want to provide a username and password [y/N]? ' - ) + 'Some of the tests require an authenticated session. ' + 'Do you want to provide a username and password [y/N]? ' + ) if yesno(ans): username = input('Username: ') - password = input ('Password (plain text): ') + password = input('Password (plain text): ') authSession = initAuthenticatedSession( - Github - , username=username, password=password - ) + Github, + username=username, + password=password, + ) tests = filter(lambda var: var.startswith('test_'), globals().copy()) tester = Basic() From de8084777e74c205013471da4a40793c6f7fc395 Mon Sep 17 00:00:00 2001 From: Jared Hobbs Date: Tue, 12 Jan 2016 09:34:15 -0800 Subject: [PATCH 3/6] add more info for the cheese shop --- setup.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 72170c7..fcb137b 100755 --- a/setup.py +++ b/setup.py @@ -4,4 +4,17 @@ setup(name='agithub', version='1.3', author='Jonathan Paugh', - py_modules=['agithub']) + url='https://github.com/jaredhobbs/agithub', + description="The agnostic Github API. It doesn't know, " + "and you don't care.", + py_modules=['agithub'], + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Environment :: Console', + 'Intended Audience :: Developers', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 3', + 'Topic :: Utilities', + ], + keywords=['github', 'api']) From 4d48ca26f5a235fddd8fcf233ac73c843e093271 Mon Sep 17 00:00:00 2001 From: Jared Hobbs Date: Tue, 12 Jan 2016 09:51:50 -0800 Subject: [PATCH 4/6] add manifest and dist to gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 592cd7e..862b0a0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ tags *.py[co] .* +MANIFEST +dist/* From f970a726afd4a6bccd39e70a1eaf2b9edb05307f Mon Sep 17 00:00:00 2001 From: Jared Hobbs Date: Mon, 27 Jun 2016 12:18:13 -0300 Subject: [PATCH 5/6] comment out setup.cfg egg_info variables --- setup.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 01bb954..406686a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,3 @@ [egg_info] -tag_build = dev -tag_svn_revision = true +#tag_build = dev +#tag_svn_revision = true From d78b731394b09fd5b6c8edb8bd5c5855a94ac6db Mon Sep 17 00:00:00 2001 From: Jared Hobbs Date: Tue, 6 Mar 2018 23:10:54 -0800 Subject: [PATCH 6/6] remove merge conflict --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index 8a1db8d..f8032b5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,9 @@ tags *.py[co] -<<<<<<< HEAD .* MANIFEST dist/* -======= agithub.egg-info/ agithub.egg-info/ build/ dist/ ->>>>>>> 285837c353eefb12723de23117b952f126a6ce92