From 26cedc1426c24d48d5776ba6156d963ee438e45f Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 13 Apr 2011 17:51:47 -0400 Subject: [PATCH 001/106] skeleton --- github3/__init__.py | 0 setup.py | 50 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 github3/__init__.py create mode 100644 setup.py diff --git a/github3/__init__.py b/github3/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..7c3cf06 --- /dev/null +++ b/setup.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import os +import sys + +from distutils.core import setup + +import clint + + + +def publish(): + """Publish to PyPi""" + os.system("python setup.py sdist upload") + +if sys.argv[-1] == "publish": + publish() + sys.exit() + +required = [] + +setup( + name='github3', + version='0.0.0', + description='[placeholder] Python wrapper for the github v3 api!', + long_description='\n\n', + author='Kenneth Reitz', + author_email='me@kennethreitz.com', + url='https://github.com/kennethreitz/python-github3', + packages= [ + 'clint', + 'clint.textui', + 'clint.packages', 'clint.packages.colorama' + ], + install_requires=required, + license='ISC', + classifiers=( +# 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'Natural Language :: English', + 'License :: OSI Approved :: ISC License (ISCL)', + 'Programming Language :: Python', + # 'Programming Language :: Python :: 2.5', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + # 'Programming Language :: Python :: 3.0', + # 'Programming Language :: Python :: 3.1', + ), +) From 3a455f9aa20369f37cb45ef0ec30d40b5eb2cf7c Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 13 Apr 2011 17:51:52 -0400 Subject: [PATCH 002/106] existing docs --- ext/general.md | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 ext/general.md diff --git a/ext/general.md b/ext/general.md new file mode 100644 index 0000000..fc57e05 --- /dev/null +++ b/ext/general.md @@ -0,0 +1,88 @@ + + +## GitHub API v3 + +**Note:** This API is in a beta state. Breaking changes may occur. + +### Schema + +All API access is over HTTPS, and accessed from the `api.github.com` +domain. All data is sent and received as JSON. + +Blank fields are included as `null` instead of being omitted. + +All timestamps are returned in ISO 8601 format: + + YYYY-MM-DDTHH:MM:SSZ + +### Authentication + +There are two ways to authenticate through GitHub API v3: + +Basic Authentication: + + $ curl -u "username:PASSWORD" https://api.github.com + +OAuth2 Token (sent in a header): + + $ curl -H "Authorization: token OAUTH-TOKEN" https://api.github.com + +OAuth2 Token (sent as a parameter): + + $ curl https://api.github.com?access_token=OAUTH-TOKEN + +Read [more about OAuth2](http://develop.github.com). + +### Pagination + +Requests that return multiple items will be paginated to 30 items by +default. You can specify further pages with the `?page` parameter. You +can also set a custom page size up to 100 with the `?per_page` parameter. + + $ curl https://api.github.com/repos.json?page=2&per_page=100 + +### Rate Limiting + +We limit requests to API v3 to 5000 per day. This is keyed off either your login, or your request IP. You can check the returned HTTP headers to see your current status: + + $ curl -i https://api.github.com + HTTP/1.1 200 OK + Status: 200 OK + X-RateLimit-Limit: 5000 + X-RateLimit-Remaining: 4966 + +You can file a [support issue](http://support.github.com/dashboard/queues/2386-api) to request white listed access for your application. We prefer sites that setup OAuth applications for their users. + +### JSON-P Callbacks + +You can send a `?callback` parameter to any GET call to have the results +wrapped in a JSON function. This is typically used when browsers want +to embed GitHub content in web pages by getting around cross domain +issues. The responses always return 200, and are wrapped in meta +objects containing the actual meta information: + + $ curl https://api.github.com?callback=foo + + foo({ + "data": {}, + "meta": { + "status": 200, + "pagination": {"page": 1, "per_page": 30}, + "rate": {"key": "IP ADDRESS", "remaining": 4999, "limit": 5000} + } + }) + +### Versions + +Api V3 uses content negotiation to specify the expected output format. +V3 is specified with this mime type: + + application/vnd.github.v3+json + +This allows us to upgrade the API if needed, and support multiple versions of the API in transition period. If you want to ensure your API usage works uninterrupted, you will want to specify this mime type in an Accept header: + + curl -H "Accept: application/vnd.github.v3+json" https://api.github.com + +Specifying `application/json` will assume the latest version of the API. Specifying an unknown version will result in a `406 Not Acceptable` error. + +We don't plan to bump the API version unless absolutely necessary. Things like removing end points, renaming or removing object attributes, or removing resources completely will result in a new version. This should be extremely rare, however. From a43d8aa76027e8f30b24daf955496aa8e1f00fea Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 13 Apr 2011 17:51:58 -0400 Subject: [PATCH 003/106] empty readme --- README.rst | 0 ext/general.md | 4 +--- 2 files changed, 1 insertion(+), 3 deletions(-) create mode 100644 README.rst diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..e69de29 diff --git a/ext/general.md b/ext/general.md index fc57e05..eb57fc2 100644 --- a/ext/general.md +++ b/ext/general.md @@ -1,5 +1,3 @@ - - ## GitHub API v3 **Note:** This API is in a beta state. Breaking changes may occur. @@ -85,4 +83,4 @@ This allows us to upgrade the API if needed, and support multiple versions of th Specifying `application/json` will assume the latest version of the API. Specifying an unknown version will result in a `406 Not Acceptable` error. -We don't plan to bump the API version unless absolutely necessary. Things like removing end points, renaming or removing object attributes, or removing resources completely will result in a new version. This should be extremely rare, however. +We don't plan to bump the API version unless absolutely necessary. Things like removing end points, renaming or removing object attributes, or removing resources completely will result in a new version. This should be extremely rare, however. From 0a8ebea5cff8a520e81b8ffe1c9aae29f56c869a Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 13 Apr 2011 17:54:17 -0400 Subject: [PATCH 004/106] basic fabfile --- fabfile.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 fabfile.py diff --git a/fabfile.py b/fabfile.py new file mode 100644 index 0000000..4f69c5a --- /dev/null +++ b/fabfile.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- + +import os +from fabric.api import * + + +DOCS_URL = 'https://gist.github.com/raw/71c2878f53e886dc921a/general.md' + + +def get_docs(): + """Removed Trashcan. + """ + os.chdir('ext') + os.system('curl -O {0}'.format(DOCS_URL)) + From 8f895bc922a3ce5b7afc7820799acd6e609dfcdb Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 13 Apr 2011 18:02:59 -0400 Subject: [PATCH 005/106] basic readme --- README.rst | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/README.rst b/README.rst index e69de29..9ba0393 100644 --- a/README.rst +++ b/README.rst @@ -0,0 +1,34 @@ +Github3: Python wrapper for the (new) GitHub API +================================================ + +Github's working on a new API. This is the new Python wrapper for it. + +This is going to be awesome. + + + +Dependencies +------------ + +- Requests +- AnyJSON +- OrderedDict + + +License +------- + +ISC License. + + + +Roadmap +------- + +- Get it Started +- HTTP BASIC +- Get it working +- Sphinx Documetnation +- Examples +- Unittests +- OAuth Last (how?) \ No newline at end of file From bb60e50be17a57842d525498ad5125d25aa5b93f Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 13 Apr 2011 18:03:06 -0400 Subject: [PATCH 006/106] vendorization! --- NOTICE | 64 ++++++++++++++++ github3/packages/__init__.py | 0 github3/packages/anyjson.py | 117 +++++++++++++++++++++++++++++ github3/packages/ordereddict.py | 127 ++++++++++++++++++++++++++++++++ 4 files changed, 308 insertions(+) create mode 100644 NOTICE create mode 100644 github3/packages/__init__.py create mode 100644 github3/packages/anyjson.py create mode 100644 github3/packages/ordereddict.py diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..9d89278 --- /dev/null +++ b/NOTICE @@ -0,0 +1,64 @@ +python-github3 includes some vendorized python libraries: ordereddict and anyjson. + + +OrderedDict License +=================== + +Copyright (c) 2009 Raymond Hettinger + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation files +(the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, 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. + + + +AnyJSON License +================== + +This software is licensed under the ``New BSD License``: + +Copyright (c) 2009, by the authors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +Neither the name of the authors nor the names of its contributors may be used +to endorse or promote products derived from this software without specific +prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + + diff --git a/github3/packages/__init__.py b/github3/packages/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/github3/packages/anyjson.py b/github3/packages/anyjson.py new file mode 100644 index 0000000..a7d1a5f --- /dev/null +++ b/github3/packages/anyjson.py @@ -0,0 +1,117 @@ +""" +Wraps the best available JSON implementation available in a common interface +""" + +__version__ = "0.2.0" +__author__ = "Rune Halvorsen " +__homepage__ = "http://bitbucket.org/runeh/anyjson/" +__docformat__ = "restructuredtext" + +""" + +.. function:: serialize(obj) + + Serialize the object to JSON. + +.. function:: deserialize(str) + + Deserialize JSON-encoded object to a Python object. + +.. function:: force_implementation(name) + + Load a specific json module. This is useful for testing and not much else + +.. attribute:: implementation + + The json implementation object. This is probably not useful to you, + except to get the name of the implementation in use. The name is + available through `implementation.name`. +""" + +import sys + +implementation = None + +""" +.. data:: _modules + + List of known json modules, and the names of their serialize/unserialize + methods, as well as the exception they throw. Exception can be either + an exception class or a string. +""" +_modules = [("cjson", "encode", "EncodeError", "decode", "DecodeError"), + ("jsonlib2", "write", "WriteError", "read", "ReadError"), + ("jsonlib", "write", "WriteError", "read", "ReadError"), + ("simplejson", "dumps", TypeError, "loads", ValueError), + ("json", "dumps", TypeError, "loads", ValueError), + ("django.utils.simplejson", "dumps", TypeError, "loads", + ValueError)] +_fields = ("modname", "encoder", "encerror", "decoder", "decerror") + + +class _JsonImplementation(object): + """Incapsulates a JSON implementation""" + + def __init__(self, modspec): + modinfo = dict(list(zip(_fields, modspec))) + + # No try block. We want importerror to end up at caller + module = self._attempt_load(modinfo["modname"]) + + self.implementation = modinfo["modname"] + self._encode = getattr(module, modinfo["encoder"]) + self._decode = getattr(module, modinfo["decoder"]) + self._encode_error = modinfo["encerror"] + self._decode_error = modinfo["decerror"] + + if isinstance(modinfo["encerror"], str): + self._encode_error = getattr(module, modinfo["encerror"]) + if isinstance(modinfo["decerror"], str): + self._decode_error = getattr(module, modinfo["decerror"]) + + self.name = modinfo["modname"] + + def _attempt_load(self, modname): + """Attempt to load module name modname, returning it on success, + throwing ImportError if module couldn't be imported""" + __import__(modname) + return sys.modules[modname] + + def serialize(self, data): + """Serialize the datastructure to json. Returns a string. Raises + TypeError if the object could not be serialized.""" + try: + return self._encode(data) + except self._encode_error as exc: + raise TypeError(*exc.args) + + def deserialize(self, s): + """deserialize the string to python data types. Raises + ValueError if the string vould not be parsed.""" + try: + return self._decode(s) + except self._decode_error as exc: + raise ValueError(*exc.args) + + +def force_implementation(modname): + """Forces anyjson to use a specific json module if it's available""" + global implementation + for name, spec in [(e[0], e) for e in _modules]: + if name == modname: + implementation = _JsonImplementation(spec) + return + raise ImportError("No module named: %s" % modname) + + +for modspec in _modules: + try: + implementation = _JsonImplementation(modspec) + break + except ImportError: + pass +else: + raise ImportError("No supported JSON module found") + +serialize = lambda value: implementation.serialize(value) +deserialize = lambda value: implementation.deserialize(value) diff --git a/github3/packages/ordereddict.py b/github3/packages/ordereddict.py new file mode 100644 index 0000000..a5b896d --- /dev/null +++ b/github3/packages/ordereddict.py @@ -0,0 +1,127 @@ +# Copyright (c) 2009 Raymond Hettinger +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, 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. + +from UserDict import DictMixin + +class OrderedDict(dict, DictMixin): + + def __init__(self, *args, **kwds): + if len(args) > 1: + raise TypeError('expected at most 1 arguments, got %d' % len(args)) + try: + self.__end + except AttributeError: + self.clear() + self.update(*args, **kwds) + + def clear(self): + self.__end = end = [] + end += [None, end, end] # sentinel node for doubly linked list + self.__map = {} # key --> [key, prev, next] + dict.clear(self) + + def __setitem__(self, key, value): + if key not in self: + end = self.__end + curr = end[1] + curr[2] = end[1] = self.__map[key] = [key, curr, end] + dict.__setitem__(self, key, value) + + def __delitem__(self, key): + dict.__delitem__(self, key) + key, prev, next = self.__map.pop(key) + prev[2] = next + next[1] = prev + + def __iter__(self): + end = self.__end + curr = end[2] + while curr is not end: + yield curr[0] + curr = curr[2] + + def __reversed__(self): + end = self.__end + curr = end[1] + while curr is not end: + yield curr[0] + curr = curr[1] + + def popitem(self, last=True): + if not self: + raise KeyError('dictionary is empty') + if last: + key = next(reversed(self)) + else: + key = next(iter(self)) + value = self.pop(key) + return key, value + + def __reduce__(self): + items = [[k, self[k]] for k in self] + tmp = self.__map, self.__end + del self.__map, self.__end + inst_dict = vars(self).copy() + self.__map, self.__end = tmp + if inst_dict: + return (self.__class__, (items,), inst_dict) + return self.__class__, (items,) + + def keys(self): + return list(self) + + setdefault = DictMixin.setdefault + update = DictMixin.update + pop = DictMixin.pop + values = DictMixin.values + items = DictMixin.items + iterkeys = DictMixin.iterkeys + itervalues = DictMixin.itervalues + iteritems = DictMixin.iteritems + + def __repr__(self): + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, list(self.items())) + + def copy(self): + return self.__class__(self) + + @classmethod + def fromkeys(cls, iterable, value=None): + d = cls() + for key in iterable: + d[key] = value + return d + + def __eq__(self, other): + if isinstance(other, OrderedDict): + if len(self) != len(other): + return False + for p, q in zip(list(self.items()), list(other.items())): + if p != q: + return False + return True + return dict.__eq__(self, other) + + def __ne__(self, other): + return not self == other From 4bcfafdcf8ba350d3b6819f39428d0b8f9cbc69c Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 13 Apr 2011 18:04:34 -0400 Subject: [PATCH 007/106] now v3 --- README.rst | 47 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 9ba0393..dfd59ec 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,5 @@ -Github3: Python wrapper for the (new) GitHub API -================================================ +Github3: Python wrapper for the (new) GitHub API v3 +=================================================== Github's working on a new API. This is the new Python wrapper for it. @@ -15,10 +15,47 @@ Dependencies - OrderedDict -License -------- +Installation +------------ + +To install clint, simply: :: + + $ pip install github3 + +Or, if you absolutely must: :: + + $ easy_install github3 + +But, you really shouldn't do that. + + + +License: +-------- + +ISC License. :: + + Copyright (c) 2011, Kenneth Reitz + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +Contribute +---------- -ISC License. +If you'd like to contribute, simply fork `the repository`_, commit your changes +to the **develop** branch (or branch off of it), and send a pull request. Make +sure you add yourself to AUTHORS_. From d17d5af0e8a01361463eb2032c5b9d53641496e8 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 13 Apr 2011 18:14:26 -0400 Subject: [PATCH 008/106] basic setup --- github3/__init__.py | 3 +++ setup.py | 17 +++++++---------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/github3/__init__.py b/github3/__init__.py index e69de29..c2fdbad 100644 --- a/github3/__init__.py +++ b/github3/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from core import * \ No newline at end of file diff --git a/setup.py b/setup.py index 7c3cf06..c5d2230 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,8 @@ import os import sys -from distutils.core import setup +# from distutils.core import setup +from setuptools import setup, find_packages import clint @@ -18,21 +19,17 @@ def publish(): publish() sys.exit() -required = [] +required = ['requests==0.3.1'] setup( name='github3', - version='0.0.0', - description='[placeholder] Python wrapper for the github v3 api!', - long_description='\n\n', + version='0.0.1', + description='Python wrapper for the github v3 api!', + long_description=open('README.rst').read(), author='Kenneth Reitz', author_email='me@kennethreitz.com', url='https://github.com/kennethreitz/python-github3', - packages= [ - 'clint', - 'clint.textui', - 'clint.packages', 'clint.packages.colorama' - ], + packages= find_packages('github3'), install_requires=required, license='ISC', classifiers=( From 1a5a69a8dd1d7473209ccdb2d9d4494b7e7a8999 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 13 Apr 2011 18:14:33 -0400 Subject: [PATCH 009/106] ISC --- LICENSE | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8a9ee98 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2011 Kenneth Reitz. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. \ No newline at end of file From 5d648d14f5a2dd3ebee354aa029049cdf44a011a Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 13 Apr 2011 18:14:40 -0400 Subject: [PATCH 010/106] hacking guidelines --- HACKING | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 HACKING diff --git a/HACKING b/HACKING new file mode 100644 index 0000000..018f9b7 --- /dev/null +++ b/HACKING @@ -0,0 +1,14 @@ +Where possible, please follow PEP8 with regard to coding style. Sometimes the line +length restriction is too hard to follow, so don't bend over backwards there. + +Triple-quotes should always be """, single quotes are ' unless using " +would result in less escaping within the string. + +All modules, functions, and methods should be well documented reStructuredText for +Sphinx AutoDoc. + +All functionality should be available in pure Python. Optional C (via Cython) +implementations may be written for performance reasons, but should never +replace the Python implementation. + +Lastly, don't take yourself too seriously :) \ No newline at end of file From 56a41a7f625cc9d04a94931375bdffd29abdd6b2 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 13 Apr 2011 18:14:46 -0400 Subject: [PATCH 011/106] authors --- AUTHORS | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 AUTHORS diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..e4e5e13 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,13 @@ +python-github3 is written and maintained by Kenneth Reitz and +various contributors: + +Development Lead +```````````````` + +- Kenneth Reitz + + +Patches and Suggestions +``````````````````````` + +- You ? \ No newline at end of file From dd0e2ee8ccdaec04ffdbe6d59e6bcd2d647c3b04 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 13 Apr 2011 18:15:06 -0400 Subject: [PATCH 012/106] package manifest --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) create mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..4c7a2bc --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include README.rst LICENSE AUTHORS \ No newline at end of file From 87c7b24ac4a9f77c8a01a759df204a3386570d38 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 13 Apr 2011 18:15:13 -0400 Subject: [PATCH 013/106] simple requirements --- reqs.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 reqs.txt diff --git a/reqs.txt b/reqs.txt new file mode 100644 index 0000000..3ac1a76 --- /dev/null +++ b/reqs.txt @@ -0,0 +1,2 @@ +requests=0.3.1 +unittest2 \ No newline at end of file From 55a7511b79ac7b5e00058721ea9105bc14fc5cf9 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 13 Apr 2011 18:16:15 -0400 Subject: [PATCH 014/106] gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb1f680 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.workon \ No newline at end of file From 53c42a9fae94c26b52360398955a617976a176c3 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 13 Apr 2011 18:16:21 -0400 Subject: [PATCH 015/106] simple core --- github3/core.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 github3/core.py diff --git a/github3/core.py b/github3/core.py new file mode 100644 index 0000000..65b6ca0 --- /dev/null +++ b/github3/core.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +""" +github3.core +~~~~~~~~~~~~ + +This module contains the core GitHub 3 interface. + +""" + + +class GitHub(object): + pass + + def __init__(self): + pass \ No newline at end of file From 2ca5804e0a718d83285b8b4bb7905b34a162a49a Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 13 Apr 2011 18:16:25 -0400 Subject: [PATCH 016/106] models --- github3/models.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 github3/models.py diff --git a/github3/models.py b/github3/models.py new file mode 100644 index 0000000..e69de29 From 197802f7770debf037a29f702ba68ea020c3aa16 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 13 Apr 2011 18:16:32 -0400 Subject: [PATCH 017/106] dummy tests --- test_github3.py | 74 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 test_github3.py diff --git a/test_github3.py b/test_github3.py new file mode 100644 index 0000000..a0464c6 --- /dev/null +++ b/test_github3.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" + convore tests + ~~~~~~~~~~~~~ + + Convore test suite. + + Looks for authentication in 'CONVORE_NAME' and + 'CONVORE_PASS' environment variables for configuration. + + :copyright: (c) 2011 by Kenneth Reitz. + :license: ISC, see LICENSE for more details. +""" + + +import os + +import unittest2 as unittest + +import convore + + +CONVORE_NAME = os.environ.get('CONVORE_NAME') +CONVORE_PASS = os.environ.get('CONVORE_PASS') + + + +class ConvoreAPI(unittest.TestCase): + """Requests test cases.""" + + def setUp(self): + self.convore = convore.Convore(CONVORE_NAME, CONVORE_PASS) + + def tearDown(self): + pass + + def test_convore_login(self): + self.assertEqual(self.convore.account_verify(), True) + + + + + +class ConvoreGroups(unittest.TestCase): + def setUp(self): + self.convore = convore.Convore(CONVORE_NAME, CONVORE_PASS) + + def test_works(self): + self.convore.groups + + def test_cache_works(self): + int_before = int(len(self.convore.groups)) + + self.convore.groups[81] + + int_after = int(len(self.convore.groups)) + self.assertEqual((int_after - int_before), 1) + + def test_discover_explore(self): + self.convore.groups.discover.explore.popular() + self.convore.groups.discover.explore.recent() + self.convore.groups.discover.explore.alphabetical() + + + def test_discover_category(self): + self.convore.groups.discover.category + self.convore.groups.discover.category['gossip'] + + def test_discover_search(self): + self.convore.groups.discover.search('github') + +if __name__ == '__main__': + unittest.main() From 5efd35a00e11774cecd956ad191f45899bfa7540 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 13 Apr 2011 18:20:25 -0400 Subject: [PATCH 018/106] hrmm --- api.py | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 api.py diff --git a/api.py b/api.py new file mode 100644 index 0000000..3d2bd78 --- /dev/null +++ b/api.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +""" + github3.api + ~~~~~~~~~~~ + + This module implements the GitHub3 API wrapper objects. + + :copyright: (c) 2011 by Kenneth Reitz. + :license: ISC, see LICENSE for more details. +""" + +from convore.packages.anyjson import deserialize + +import requests + +from . import models + + + +API_URL = 'https://api.github.com' +AUTH = None + +# ======= +# Helpers +# ======= + +def _safe_response(r, error=None): + try: + r.raise_for_status() + return r + except requests.HTTPError: + if r.status_code == 401: + raise LoginFailed + else: + raise APIError(error) if error else APIError + + +def get(*path, **kwargs): + """ + Accepts optional error parameter, which will be passed in the event of a + non-401 HTTP error. + + api.get('groups') + api.get('groups', 'id') + api.get('accounts', 'verify') + """ + url = '%s%s%s' % (API_URL, '/'.join(map(str, path)), '.json') + + params = kwargs.get('params', None) + + r = requests.get(url, params=params, auth=auth) + + error = kwargs.get('error', None) + return _safe_response(r, error) + + +def post(params, *path): + + url = '%s%s%s' % (API_URL, '/'.join(map(str, path)), '.json') + r = requests.post(url, params=params, auth=auth) + return _safe_response(r) + + +# ========== +# Exceptions +# ========== + +class LoginFailed(RuntimeError): + """Login falied!""" + +class APIError(RuntimeError): + """There was a problem properly accessing the Convore API.""" + + + +def login(username, password): + """Configured API Credentials""" + global auth + + auth = (username, password) + # print requests.auth_manager.__dict__ + +# ========== +# End Points +# ========== + From 0189aff9ba05e8a2954ca864c03a4de022f1ab5c Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 13 Apr 2011 18:38:32 -0400 Subject: [PATCH 019/106] basic --- README.rst | 17 ++++++++++++----- github3/core.py | 20 +++++++++++++++++--- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index dfd59ec..7958179 100644 --- a/README.rst +++ b/README.rst @@ -7,12 +7,19 @@ This is going to be awesome. -Dependencies ------------- +Usage +----- + +:: + from github3 import github + + github.login('username', 'password') + # optional + + github. + + -- Requests -- AnyJSON -- OrderedDict Installation diff --git a/github3/core.py b/github3/core.py index 65b6ca0..383ab88 100644 --- a/github3/core.py +++ b/github3/core.py @@ -9,8 +9,22 @@ """ +from .api import API_URL + + + class GitHub(object): - pass + """Central GitHub object.""" + + ratelimit = None + = None + + def __init__(self, apiurl=API_URL): + pass + + def login(self): + pass + - def __init__(self): - pass \ No newline at end of file +# Default instance +github = GitHub() \ No newline at end of file From c1e387d83bd5a00d1b2379a5c2a10ebc33a31eb4 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 13 Apr 2011 19:13:16 -0400 Subject: [PATCH 020/106] grmmm --- github3/core.py | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/github3/core.py b/github3/core.py index 383ab88..6d1ee21 100644 --- a/github3/core.py +++ b/github3/core.py @@ -9,22 +9,44 @@ """ -from .api import API_URL +from .api import API_URL, get class GitHub(object): """Central GitHub object.""" - ratelimit = None - = None + rate_limit = None + rate_left = None + per_page = 30 def __init__(self, apiurl=API_URL): - pass + self.__basic_auth = None + return self.logged_in() + + def _get(self, *path): + r = get(*path, auth=self.__basic_auth) + + rate_limit = r.headers.get('x-ratelimit-remaining', None) + rate_left = r.headers.get('x-ratelimit-limit', None) + + if (rate_limit is not None) or (rate_left is not None): + self.rate_limit = rate_limit + self.rate_left = rate_left - def login(self): + return r + + def auth(self, username, password): + self.__basic_auth = (username, password) + + def oauth(self): + # TODO: oAuth pass + def logged_in(self): + print self._get('').headers + + # Default instance github = GitHub() \ No newline at end of file From 82c1cf1acfd86d49c025ca30c16463e19f883fbe Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 13 Apr 2011 19:13:19 -0400 Subject: [PATCH 021/106] basic models --- github3/models.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/github3/models.py b/github3/models.py index e69de29..c049aba 100644 --- a/github3/models.py +++ b/github3/models.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- + +""" +github3.models +~~~~~~~~~~~~~~ + +This module provides the GitHub3 object models. + +""" + + + +class Repo(object): + """GitHub Repository.""" + pass + + + +class Gist(object): + """GitHub Gist. + + gist.files['filename.py'] + """ + + def __init__(self): + pass + + + +class GistComment(object): + """GitHub GistComment.""" + + def __init__(self): + pass \ No newline at end of file From 4a173283eb55d7405bc56c75e47e94b2eb294795 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 13 Apr 2011 19:13:24 -0400 Subject: [PATCH 022/106] move api --- api.py => github3/api.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) rename api.py => github3/api.py (81%) diff --git a/api.py b/github3/api.py similarity index 81% rename from api.py rename to github3/api.py index 3d2bd78..c1f7687 100644 --- a/api.py +++ b/github3/api.py @@ -9,16 +9,19 @@ :license: ISC, see LICENSE for more details. """ -from convore.packages.anyjson import deserialize +from . import models +from .packages.anyjson import deserialize import requests -from . import models API_URL = 'https://api.github.com' -AUTH = None +API_MIME = 'application/vnd.github.v3+json' + + + # ======= # Helpers @@ -35,7 +38,7 @@ def _safe_response(r, error=None): raise APIError(error) if error else APIError -def get(*path, **kwargs): +def get(*path, **params): """ Accepts optional error parameter, which will be passed in the event of a non-401 HTTP error. @@ -44,14 +47,13 @@ def get(*path, **kwargs): api.get('groups', 'id') api.get('accounts', 'verify') """ - url = '%s%s%s' % (API_URL, '/'.join(map(str, path)), '.json') + url = '{0}{1}'.format(API_URL, '/'.join(map(str, path))) - params = kwargs.get('params', None) + # params = kwargs.get('params', None) - r = requests.get(url, params=params, auth=auth) + r = requests.get(url, params=params, auth=None) - error = kwargs.get('error', None) - return _safe_response(r, error) + return _safe_response(r) def post(params, *path): From 9cf503801a4c812f6948e9d5c43f2ff6061f3ead Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 13 Apr 2011 19:34:50 -0400 Subject: [PATCH 023/106] hrm --- github3/api.py | 4 +++- github3/core.py | 11 +++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/github3/api.py b/github3/api.py index c1f7687..8b30d52 100644 --- a/github3/api.py +++ b/github3/api.py @@ -28,11 +28,12 @@ # ======= def _safe_response(r, error=None): + try: r.raise_for_status() return r except requests.HTTPError: - if r.status_code == 401: + if (r.status_code == 404) or (r.status_code == 401): raise LoginFailed else: raise APIError(error) if error else APIError @@ -47,6 +48,7 @@ def get(*path, **params): api.get('groups', 'id') api.get('accounts', 'verify') """ + url = '{0}{1}'.format(API_URL, '/'.join(map(str, path))) # params = kwargs.get('params', None) diff --git a/github3/core.py b/github3/core.py index 6d1ee21..e0703a5 100644 --- a/github3/core.py +++ b/github3/core.py @@ -22,7 +22,6 @@ class GitHub(object): def __init__(self, apiurl=API_URL): self.__basic_auth = None - return self.logged_in() def _get(self, *path): r = get(*path, auth=self.__basic_auth) @@ -38,13 +37,21 @@ def _get(self, *path): def auth(self, username, password): self.__basic_auth = (username, password) + return self.logged_in def oauth(self): # TODO: oAuth pass + @property def logged_in(self): - print self._get('').headers + r = self._get('') + + # print r + if r.status_code == 200: + return True + else: + return False From 3f0f06845a23a0726254fc8f0b2c6db44c38ae34 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 13 Apr 2011 19:41:58 -0400 Subject: [PATCH 024/106] fab update --- fabfile.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/fabfile.py b/fabfile.py index 4f69c5a..444e532 100644 --- a/fabfile.py +++ b/fabfile.py @@ -4,12 +4,16 @@ from fabric.api import * -DOCS_URL = 'https://gist.github.com/raw/71c2878f53e886dc921a/general.md' +DOCS_URL = 'https://gist.github.com/raw/71c2878f53e886dc921a/' def get_docs(): """Removed Trashcan. """ os.chdir('ext') - os.system('curl -O {0}'.format(DOCS_URL)) + os.system('curl -O {0}{1}'.format(DOCS_URL, 'general.md')) + os.system('curl -O {0}{1}'.format(DOCS_URL, 'issue_comments.md')) + os.system('curl -O {0}{1}'.format(DOCS_URL, 'issues.md')) + os.system('curl -O {0}{1}'.format(DOCS_URL, 'labels.md')) + os.system('curl -O {0}{1}'.format(DOCS_URL, 'milestones.md')) From a71fc4816b821dd7695bf609b6bb424091a2bf40 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 13 Apr 2011 19:43:13 -0400 Subject: [PATCH 025/106] docs --- ext/issue_comments.md | 15 +++++++++ ext/issues.md | 48 +++++++++++++++++++++++++++++ ext/labels.md | 71 +++++++++++++++++++++++++++++++++++++++++++ ext/milestones.md | 44 +++++++++++++++++++++++++++ fabfile.py | 10 +++--- 5 files changed, 183 insertions(+), 5 deletions(-) create mode 100644 ext/issue_comments.md create mode 100644 ext/issues.md create mode 100644 ext/labels.md create mode 100644 ext/milestones.md diff --git a/ext/issue_comments.md b/ext/issue_comments.md new file mode 100644 index 0000000..ee5ca27 --- /dev/null +++ b/ext/issue_comments.md @@ -0,0 +1,15 @@ +# Issue Comments API + +## Get Comments for an Issue + +### GET `/repos/:user/:repo/issues/:id/comments.json` + +## Create a Comment for an Issue + +### POST `/repos/:user/:repo/issues/:id/comments.json` + +### Input + + { + body: String, + } diff --git a/ext/issues.md b/ext/issues.md new file mode 100644 index 0000000..83920ff --- /dev/null +++ b/ext/issues.md @@ -0,0 +1,48 @@ +# Issues API + +## List issues for this Repository + +### GET `/repos/:user/:repo/issues.json` + +* `?milestone` = (Fixnum) +* `?sort` = (String) +* `?direction` = (String) +* `?state` = open, closed, default: open +* `?assignee` = (String) +* `?mentioned` = (String) +* `?labels` = (String) + + +## Create an Issue + +### POST `/repos/:user/:repo/issues.json` + +### Input + + { + title: String, + body: String, + assignee: String, + milestone: Fixnum, + } + +## Get a single Issue + +### GET `/repos/:user/:repo/issues/:id.json` + +## Edit an Issue + +### PUT `/repos/:user/:repo/issues/:id.json` + +### Input + + { + title: String, + body: String, + assignee: String, + milestone: Fixnum, + } + +## Delete an Issue + +### DELETE `/repos/:user/:repo/issues/:id.json` diff --git a/ext/labels.md b/ext/labels.md new file mode 100644 index 0000000..2dcca58 --- /dev/null +++ b/ext/labels.md @@ -0,0 +1,71 @@ +# Labels API + +## List all Labels for this Repository + +### GET `/repos/:user/:repo/labels.json` + +## Create a Label + +### POST `/repos/:user/:repo/labels.json` + +### Input + + { + name: String, + color: String, + } + +## Get a single Label + +### GET `/repos/:user/:repo/labels/:id.json` + +## Update a Label + +### PUT `/repos/:user/:repo/labels/:id.json` + +### Input + + { + name: String, + color: String, + } + +## Delete a label + +### DELETE `/repos/:user/:repo/labels/:id.json` + +## List labels on the Issue + +### GET `/repos/:user/:repo/issues/:id/labels.json` + +## Add a Label to an Issue + +### POST `/repos/:user/:repo/issues/:id/labels.json` + +### Input + + [{ + name: String, + }, ...] + +## Remove a Label from an Issue + +### DELETE `/repos/:user/:repo/issues/:id/labels/:id.json` + +## Replace all Labels for an Issue + +### PUT `/repos/:user/:repo/issues/:id/labels.json` + +### Input + + [{ + name: String, + }, ...] + +## Remove all Labels from an Issue + +### DELETE `/repos/:user/:repo/issues/:id/labels.json` + +## Get Labels for every Issue in a Milestone + +### GET `/repos/:user/:repo/milestones/:id/labels.json` diff --git a/ext/milestones.md b/ext/milestones.md new file mode 100644 index 0000000..381228b --- /dev/null +++ b/ext/milestones.md @@ -0,0 +1,44 @@ +# Milestones API + +## List Milestones for an Issue + +### GET `/repos/:user/:repo/milestones.json` + +* `?sort` = (String) +* `?direction` = (String) +* `?state` = open, closed, default: open + + +## Create a Milestone + +### POST `/repos/:user/:repo/milestones.json` + +### Input + + { + title: String, + state: String, + description: String, + due_on: Time, + } + +## Get a single Milestone + +### GET `/repos/:user/:repo/milestones/:id.json` + +## Update a Milestone + +### PUT `/repos/:user/:repo/milestones/:id.json` + +### Input + + { + title: String, + state: String, + description: String, + due_on: Time, + } + +## Delete a Milestone + +### DELETE `/repos/:user/:repo/milestones/:id.json` diff --git a/fabfile.py b/fabfile.py index 444e532..7642557 100644 --- a/fabfile.py +++ b/fabfile.py @@ -11,9 +11,9 @@ def get_docs(): """Removed Trashcan. """ os.chdir('ext') - os.system('curl -O {0}{1}'.format(DOCS_URL, 'general.md')) - os.system('curl -O {0}{1}'.format(DOCS_URL, 'issue_comments.md')) - os.system('curl -O {0}{1}'.format(DOCS_URL, 'issues.md')) - os.system('curl -O {0}{1}'.format(DOCS_URL, 'labels.md')) - os.system('curl -O {0}{1}'.format(DOCS_URL, 'milestones.md')) + os.system('curl -s -O {0}{1}'.format(DOCS_URL, 'general.md')) + os.system('curl -s -O {0}{1}'.format(DOCS_URL, 'issue_comments.md')) + os.system('curl -s -O {0}{1}'.format(DOCS_URL, 'issues.md')) + os.system('curl -s -O {0}{1}'.format(DOCS_URL, 'labels.md')) + os.system('curl -s -O {0}{1}'.format(DOCS_URL, 'milestones.md')) From b895d6cc3b7cd171718e557fb4db07fb8bcb2b98 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 13 Apr 2011 21:51:46 -0400 Subject: [PATCH 026/106] models update --- github3/models.py | 46 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/github3/models.py b/github3/models.py index c049aba..e99c73b 100644 --- a/github3/models.py +++ b/github3/models.py @@ -10,6 +10,21 @@ +class User(object): + pass + + def __init__(self): + self.type = None + self.email = None + self.location = None + self.name = None + self.company = None + self.login = None + self.blog = None + self.gravatar_url = None + + + class Repo(object): """GitHub Repository.""" pass @@ -31,4 +46,33 @@ class GistComment(object): """GitHub GistComment.""" def __init__(self): - pass \ No newline at end of file + pass + + +class Issue(object): + + + def __init__(self): + self.number = None + self.updated_at = None + self.closed_at = None + self.labels = [] + self.title= None + self.comments = [] + self.user = None + self.body = None + self.url = None + self.state = None + self.api_url = None + + # api + self.milestone + self.assignee + + + +class Milestone(object): + + + def __init__(self):pass + From b6ab012bb0415a490a3bbb8dcc7783b1d415bb1a Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 13 Apr 2011 22:08:41 -0400 Subject: [PATCH 027/106] GitHubModel --- github3/core.py | 2 +- github3/models.py | 28 +++++++++++++++++----------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/github3/core.py b/github3/core.py index e0703a5..2f85040 100644 --- a/github3/core.py +++ b/github3/core.py @@ -46,8 +46,8 @@ def oauth(self): @property def logged_in(self): r = self._get('') + print - # print r if r.status_code == 200: return True else: diff --git a/github3/models.py b/github3/models.py index e99c73b..55d2be9 100644 --- a/github3/models.py +++ b/github3/models.py @@ -9,8 +9,14 @@ """ +class GitHubModel(object): -class User(object): + def __init__(self): + pass + + + +class User(GitHubModel): pass def __init__(self): @@ -25,31 +31,31 @@ def __init__(self): -class Repo(object): +class Repo(GitHubModel): """GitHub Repository.""" pass -class Gist(object): +class Gist(GitHubModel): """GitHub Gist. gist.files['filename.py'] """ def __init__(self): - pass + self.api_url = None -class GistComment(object): +class GistComment(GitHubModel): """GitHub GistComment.""" def __init__(self): pass -class Issue(object): +class Issue(GitHubModel): def __init__(self): @@ -66,13 +72,13 @@ def __init__(self): self.api_url = None # api - self.milestone - self.assignee - + self.milestone = None + self.assignee = None -class Milestone(object): +class Milestone(GitHubModel): - def __init__(self):pass + def __init__(self): + self.api_url = None From 37dbc386ad021ea2ed89ac66a6d23a4c7b207ba3 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sun, 24 Apr 2011 20:10:56 -0400 Subject: [PATCH 028/106] logged in workaround. --- github3/core.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/github3/core.py b/github3/core.py index 2f85040..95716a9 100644 --- a/github3/core.py +++ b/github3/core.py @@ -19,15 +19,18 @@ class GitHub(object): rate_limit = None rate_left = None per_page = 30 + accept = 'application/vnd.github.v3+json' def __init__(self, apiurl=API_URL): self.__basic_auth = None def _get(self, *path): - r = get(*path, auth=self.__basic_auth) + headers = {'Accept': self.accept} - rate_limit = r.headers.get('x-ratelimit-remaining', None) - rate_left = r.headers.get('x-ratelimit-limit', None) + r = get(*path, auth=self.__basic_auth, headers=headers) + + rate_left = r.headers.get('x-ratelimit-remaining', None) + rate_limit = r.headers.get('x-ratelimit-limit', None) if (rate_limit is not None) or (rate_left is not None): self.rate_limit = rate_limit @@ -48,7 +51,7 @@ def logged_in(self): r = self._get('') print - if r.status_code == 200: + if r.status_code == 200 and self.__basic_auth: return True else: return False From c4c4b6ef1b409e79393c08ed3c2a1a4f7ec46e35 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sun, 24 Apr 2011 20:11:21 -0400 Subject: [PATCH 029/106] whitespace --- github3/core.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/github3/core.py b/github3/core.py index 95716a9..db8b213 100644 --- a/github3/core.py +++ b/github3/core.py @@ -24,6 +24,7 @@ class GitHub(object): def __init__(self, apiurl=API_URL): self.__basic_auth = None + def _get(self, *path): headers = {'Accept': self.accept} @@ -38,14 +39,18 @@ def _get(self, *path): return r + + def auth(self, username, password): self.__basic_auth = (username, password) return self.logged_in + def oauth(self): # TODO: oAuth pass + @property def logged_in(self): r = self._get('') From 2b52351b212ff7dae782cee32c79ff22e358516a Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sun, 24 Apr 2011 20:56:28 -0400 Subject: [PATCH 030/106] model improvements --- github3/models.py | 76 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 68 insertions(+), 8 deletions(-) diff --git a/github3/models.py b/github3/models.py index 55d2be9..5546080 100644 --- a/github3/models.py +++ b/github3/models.py @@ -20,20 +20,80 @@ class User(GitHubModel): pass def __init__(self): - self.type = None - self.email = None - self.location = None - self.name = None - self.company = None - self.login = None - self.blog = None + self.email = None, + self.type = None, + self.url = None, + self.login = None, + self.created_at = None, self.gravatar_url = None + self.blog = None, + self.name = None, + self.company = None, + self.location = None + + def from_dict(self, d): + self.email = d.get('email', None), + self.type = d.get('type', None), + self.url = d.get('url', None), + self.login = d.get('login', None), + self.created_at = d.get('created_at', None), + self.gravatar_url = d.get('gravatar_url', None), + self.blog = d.get('blog', None), + self.name = d.get('name', None), + self.company = d.get('company', None), + self.location = d.get('location', None) + + + class Repo(GitHubModel): """GitHub Repository.""" - pass + + def __init__(self): + self.has_downloads = None, + self.forks = None, + self.url = None, + self.created_at = None, + self.watchers = None, + self.description = None, + self.master_branch = None, + self.has_wiki = None, + self.open_issues = None, + self.fork = None, + self.html_url = None, + self.homepage = None, + self.has_issues = None, + self.pushed_at = None, + self.language = None, + self.private = None, + self.size = None, + self.integrate_branch = None, + self.owner = None, + self.name = None + + def from_dict(self, d): + self.has_downloads = d.get('has_downloads', None), + self.forks = d.get('forks', None), + self.url = d.get('url', None), + self.created_at = d.get('created_at', None), + self.watchers = d.get('watchers', None), + self.description = d.get('description', None), + self.master_branch = d.get('master_branch', None), + self.has_wiki = d.get('has_wiki', None), + self.open_issues = d.get('open_issues', None), + self.fork = d.get('fork', None), + self.html_url = d.get('html_url', None), + self.homepage = d.get('homepage', None), + self.has_issues = d.get('has_issues', None), + self.pushed_at = d.get('pushed_at', None), + self.language = d.get('language', None), + self.private = d.get('private', None), + self.size = d.get('size', None), + self.integrate_branch = d.get('integrate_branch', None), + self.owner = d.get('owner', None), + self.name = d.get('name', None), From 2b8fa24053877e37ccf362476d5fd58d47ed9753 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sun, 24 Apr 2011 21:32:05 -0400 Subject: [PATCH 031/106] repo api works :) --- github3/api.py | 8 +++++-- github3/core.py | 61 ++++++++++++++++++++++++++++++++++++++++++++--- github3/models.py | 9 ++++++- 3 files changed, 72 insertions(+), 6 deletions(-) diff --git a/github3/api.py b/github3/api.py index 8b30d52..3225e43 100644 --- a/github3/api.py +++ b/github3/api.py @@ -49,8 +49,11 @@ def get(*path, **params): api.get('accounts', 'verify') """ - url = '{0}{1}'.format(API_URL, '/'.join(map(str, path))) + path = list(path) + path.insert(0, '') + url = '{0}{1}'.format(API_URL, '/'.join(map(str, path))) + print url # params = kwargs.get('params', None) r = requests.get(url, params=params, auth=None) @@ -60,7 +63,8 @@ def get(*path, **params): def post(params, *path): - url = '%s%s%s' % (API_URL, '/'.join(map(str, path)), '.json') + path += API_URL + url = '%s%s' % ('/'.join(map(str, path)), '.json') r = requests.post(url, params=params, auth=auth) return _safe_response(r) diff --git a/github3/core.py b/github3/core.py index db8b213..73bbea2 100644 --- a/github3/core.py +++ b/github3/core.py @@ -10,7 +10,9 @@ from .api import API_URL, get - +import json +import models +# TODO: switch to anyjson class GitHub(object): @@ -25,9 +27,14 @@ def __init__(self, apiurl=API_URL): self.__basic_auth = None - def _get(self, *path): + def _get(self, *path, **kwargs): + """optional json=False, paged=False""" + headers = {'Accept': self.accept} + is_json = kwargs.get('json', False) + is_paged = kwargs.get('paged', False) + r = get(*path, auth=self.__basic_auth, headers=headers) rate_left = r.headers.get('x-ratelimit-remaining', None) @@ -37,6 +44,12 @@ def _get(self, *path): self.rate_limit = rate_limit self.rate_left = rate_left + if is_json: + r = json.loads(r.content) + + if is_paged: + pass + # TODO: paged support (__iter__) return r @@ -61,7 +74,49 @@ def logged_in(self): else: return False - + def repo(self, username, reponame): + d = self._get('repos', username, '{0}.json'.format(reponame), json=True) + + + repo = models.Repo() + repo.from_dict(d) + + return repo + + +# { +# "has_downloads": true, +# "forks": 10, +# "url": "https://api.github.com/repos/kennethreitz/requests.json", +# "created_at": "2011-02-13T18:38:17Z", +# "watchers": 166, +# "description": "Python HTTP modules suck. This one doesn't.", +# "master_branch": "develop", +# "has_wiki": true, +# "open_issues": 5, +# "fork": false, +# "html_url": "https://github.com/kennethreitz/requests", +# "homepage": "http://pypi.python.org/pypi/requests/", +# "has_issues": true, +# "pushed_at": "2011-04-21T21:39:45Z", +# "language": "Python", +# "private": false, +# "size": 2748, +# "integrate_branch": null, +# "owner": { +# "email": "_@kennethreitz.com", +# "type": "User", +# "url": "https://api.github.com/users/kennethreitz.json", +# "login": "kennethreitz", +# "created_at": "2009-08-26T21:17:47Z", +# "gravatar_url": "https://secure.gravatar.com/avatar/2eccc4005572c1e2b12a9c00580bc86f?s=30&d=https://d3nwyuy0nl342s.cloudfront.net%2Fimages%2Fgravatars%2Fgravatar-140.png", +# "blog": "http://kennethreitz.com", +# "name": "Kenneth Reitz", +# "company": "NetApp, Inc", +# "location": "Washington, DC" +# }, +# "name": "requests" +# } # Default instance github = GitHub() \ No newline at end of file diff --git a/github3/models.py b/github3/models.py index 5546080..eed8efc 100644 --- a/github3/models.py +++ b/github3/models.py @@ -31,6 +31,9 @@ def __init__(self): self.company = None, self.location = None + def __repr__(self): + return ' Date: Mon, 25 Apr 2011 10:48:18 -0400 Subject: [PATCH 032/106] no toys --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index eb1f680..a6cb653 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.workon \ No newline at end of file +.workon +toy.py From 77487df604db49dcc2ee7b3817c8b42178dcee00 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Mon, 25 Apr 2011 14:24:23 -0400 Subject: [PATCH 033/106] wtf was i doing? --- github3/models.py | 114 +++++++++++++++++++++++----------------------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/github3/models.py b/github3/models.py index eed8efc..5768b9e 100644 --- a/github3/models.py +++ b/github3/models.py @@ -20,30 +20,30 @@ class User(GitHubModel): pass def __init__(self): - self.email = None, - self.type = None, - self.url = None, - self.login = None, - self.created_at = None, + self.email = None + self.type = None + self.url = None + self.login = None + self.created_at = None self.gravatar_url = None - self.blog = None, - self.name = None, - self.company = None, + self.blog = None + self.name = None + self.company = None self.location = None def __repr__(self): - return ''.format(self.login) def from_dict(self, d): - self.email = d.get('email', None), - self.type = d.get('type', None), - self.url = d.get('url', None), - self.login = d.get('login', None), - self.created_at = d.get('created_at', None), - self.gravatar_url = d.get('gravatar_url', None), - self.blog = d.get('blog', None), - self.name = d.get('name', None), - self.company = d.get('company', None), + self.email = d.get('email', None) + self.type = d.get('type', None) + self.url = d.get('url', None) + self.login = d.get('login', None) + self.created_at = d.get('created_at', None) + self.gravatar_url = d.get('gravatar_url', None) + self.blog = d.get('blog', None) + self.name = d.get('name', None) + self.company = d.get('company', None) self.location = d.get('location', None) @@ -55,52 +55,52 @@ class Repo(GitHubModel): """GitHub Repository.""" def __init__(self): - self.has_downloads = None, - self.forks = None, - self.url = None, - self.created_at = None, - self.watchers = None, - self.description = None, - self.master_branch = None, - self.has_wiki = None, - self.open_issues = None, - self.fork = None, - self.html_url = None, - self.homepage = None, - self.has_issues = None, - self.pushed_at = None, - self.language = None, - self.private = None, - self.size = None, - self.integrate_branch = None, - self.owner = None, + self.has_downloads = None + self.forks = None + self.url = None + self.created_at = None + self.watchers = None + self.description = None + self.master_branch = None + self.has_wiki = None + self.open_issues = None + self.fork = None + self.html_url = None + self.homepage = None + self.has_issues = None + self.pushed_at = None + self.language = None + self.private = None + self.size = None + self.integrate_branch = None + self.owner = None self.name = None def __repr__(self): - return ''.format(self.owner.login, self.name) def from_dict(self, d): - self.has_downloads = d.get('has_downloads', None), - self.forks = d.get('forks', None), - self.url = d.get('url', None), - self.created_at = d.get('created_at', None), - self.watchers = d.get('watchers', None), - self.description = d.get('description', None), - self.master_branch = d.get('master_branch', None), - self.has_wiki = d.get('has_wiki', None), - self.open_issues = d.get('open_issues', None), - self.fork = d.get('fork', None), - self.html_url = d.get('html_url', None), - self.homepage = d.get('homepage', None), - self.has_issues = d.get('has_issues', None), - self.pushed_at = d.get('pushed_at', None), - self.language = d.get('language', None), - self.private = d.get('private', None), - self.size = d.get('size', None), - self.integrate_branch = d.get('integrate_branch', None), + self.has_downloads = d.get('has_downloads', None) + self.forks = d.get('forks', None) + self.url = d.get('url', None) + self.created_at = d.get('created_at', None) + self.watchers = d.get('watchers', None) + self.description = d.get('description', None) + self.master_branch = d.get('master_branch', None) + self.has_wiki = d.get('has_wiki', None) + self.open_issues = d.get('open_issues', None) + self.fork = d.get('fork', None) + self.html_url = d.get('html_url', None) + self.homepage = d.get('homepage', None) + self.has_issues = d.get('has_issues', None) + self.pushed_at = d.get('pushed_at', None) + self.language = d.get('language', None) + self.private = d.get('private', None) + self.size = d.get('size', None) + self.integrate_branch = d.get('integrate_branch', None) self.owner = User() self.owner.from_dict(d.get('owner', dict())) - self.name = d.get('name', None), + self.name = d.get('name', None) From 28950b5f59baa1beb572ff78f011b632320a7b6a Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Thu, 28 Apr 2011 14:19:02 -0400 Subject: [PATCH 034/106] work in progress note --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 7958179..3baf0a3 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,7 @@ Github3: Python wrapper for the (new) GitHub API v3 Github's working on a new API. This is the new Python wrapper for it. -This is going to be awesome. +**This a work in progress.** Should be relased before the first week of May. From 065cee2a18213e2ef67c47c6ed951cd840417779 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sat, 14 May 2011 13:14:50 -0400 Subject: [PATCH 035/106] site's up, no need to clone docs --- ext/general.md | 86 ------------------------------------------- ext/issue_comments.md | 15 -------- ext/issues.md | 48 ------------------------ ext/labels.md | 71 ----------------------------------- ext/milestones.md | 44 ---------------------- 5 files changed, 264 deletions(-) delete mode 100644 ext/general.md delete mode 100644 ext/issue_comments.md delete mode 100644 ext/issues.md delete mode 100644 ext/labels.md delete mode 100644 ext/milestones.md diff --git a/ext/general.md b/ext/general.md deleted file mode 100644 index eb57fc2..0000000 --- a/ext/general.md +++ /dev/null @@ -1,86 +0,0 @@ -## GitHub API v3 - -**Note:** This API is in a beta state. Breaking changes may occur. - -### Schema - -All API access is over HTTPS, and accessed from the `api.github.com` -domain. All data is sent and received as JSON. - -Blank fields are included as `null` instead of being omitted. - -All timestamps are returned in ISO 8601 format: - - YYYY-MM-DDTHH:MM:SSZ - -### Authentication - -There are two ways to authenticate through GitHub API v3: - -Basic Authentication: - - $ curl -u "username:PASSWORD" https://api.github.com - -OAuth2 Token (sent in a header): - - $ curl -H "Authorization: token OAUTH-TOKEN" https://api.github.com - -OAuth2 Token (sent as a parameter): - - $ curl https://api.github.com?access_token=OAUTH-TOKEN - -Read [more about OAuth2](http://develop.github.com). - -### Pagination - -Requests that return multiple items will be paginated to 30 items by -default. You can specify further pages with the `?page` parameter. You -can also set a custom page size up to 100 with the `?per_page` parameter. - - $ curl https://api.github.com/repos.json?page=2&per_page=100 - -### Rate Limiting - -We limit requests to API v3 to 5000 per day. This is keyed off either your login, or your request IP. You can check the returned HTTP headers to see your current status: - - $ curl -i https://api.github.com - HTTP/1.1 200 OK - Status: 200 OK - X-RateLimit-Limit: 5000 - X-RateLimit-Remaining: 4966 - -You can file a [support issue](http://support.github.com/dashboard/queues/2386-api) to request white listed access for your application. We prefer sites that setup OAuth applications for their users. - -### JSON-P Callbacks - -You can send a `?callback` parameter to any GET call to have the results -wrapped in a JSON function. This is typically used when browsers want -to embed GitHub content in web pages by getting around cross domain -issues. The responses always return 200, and are wrapped in meta -objects containing the actual meta information: - - $ curl https://api.github.com?callback=foo - - foo({ - "data": {}, - "meta": { - "status": 200, - "pagination": {"page": 1, "per_page": 30}, - "rate": {"key": "IP ADDRESS", "remaining": 4999, "limit": 5000} - } - }) - -### Versions - -Api V3 uses content negotiation to specify the expected output format. -V3 is specified with this mime type: - - application/vnd.github.v3+json - -This allows us to upgrade the API if needed, and support multiple versions of the API in transition period. If you want to ensure your API usage works uninterrupted, you will want to specify this mime type in an Accept header: - - curl -H "Accept: application/vnd.github.v3+json" https://api.github.com - -Specifying `application/json` will assume the latest version of the API. Specifying an unknown version will result in a `406 Not Acceptable` error. - -We don't plan to bump the API version unless absolutely necessary. Things like removing end points, renaming or removing object attributes, or removing resources completely will result in a new version. This should be extremely rare, however. diff --git a/ext/issue_comments.md b/ext/issue_comments.md deleted file mode 100644 index ee5ca27..0000000 --- a/ext/issue_comments.md +++ /dev/null @@ -1,15 +0,0 @@ -# Issue Comments API - -## Get Comments for an Issue - -### GET `/repos/:user/:repo/issues/:id/comments.json` - -## Create a Comment for an Issue - -### POST `/repos/:user/:repo/issues/:id/comments.json` - -### Input - - { - body: String, - } diff --git a/ext/issues.md b/ext/issues.md deleted file mode 100644 index 83920ff..0000000 --- a/ext/issues.md +++ /dev/null @@ -1,48 +0,0 @@ -# Issues API - -## List issues for this Repository - -### GET `/repos/:user/:repo/issues.json` - -* `?milestone` = (Fixnum) -* `?sort` = (String) -* `?direction` = (String) -* `?state` = open, closed, default: open -* `?assignee` = (String) -* `?mentioned` = (String) -* `?labels` = (String) - - -## Create an Issue - -### POST `/repos/:user/:repo/issues.json` - -### Input - - { - title: String, - body: String, - assignee: String, - milestone: Fixnum, - } - -## Get a single Issue - -### GET `/repos/:user/:repo/issues/:id.json` - -## Edit an Issue - -### PUT `/repos/:user/:repo/issues/:id.json` - -### Input - - { - title: String, - body: String, - assignee: String, - milestone: Fixnum, - } - -## Delete an Issue - -### DELETE `/repos/:user/:repo/issues/:id.json` diff --git a/ext/labels.md b/ext/labels.md deleted file mode 100644 index 2dcca58..0000000 --- a/ext/labels.md +++ /dev/null @@ -1,71 +0,0 @@ -# Labels API - -## List all Labels for this Repository - -### GET `/repos/:user/:repo/labels.json` - -## Create a Label - -### POST `/repos/:user/:repo/labels.json` - -### Input - - { - name: String, - color: String, - } - -## Get a single Label - -### GET `/repos/:user/:repo/labels/:id.json` - -## Update a Label - -### PUT `/repos/:user/:repo/labels/:id.json` - -### Input - - { - name: String, - color: String, - } - -## Delete a label - -### DELETE `/repos/:user/:repo/labels/:id.json` - -## List labels on the Issue - -### GET `/repos/:user/:repo/issues/:id/labels.json` - -## Add a Label to an Issue - -### POST `/repos/:user/:repo/issues/:id/labels.json` - -### Input - - [{ - name: String, - }, ...] - -## Remove a Label from an Issue - -### DELETE `/repos/:user/:repo/issues/:id/labels/:id.json` - -## Replace all Labels for an Issue - -### PUT `/repos/:user/:repo/issues/:id/labels.json` - -### Input - - [{ - name: String, - }, ...] - -## Remove all Labels from an Issue - -### DELETE `/repos/:user/:repo/issues/:id/labels.json` - -## Get Labels for every Issue in a Milestone - -### GET `/repos/:user/:repo/milestones/:id/labels.json` diff --git a/ext/milestones.md b/ext/milestones.md deleted file mode 100644 index 381228b..0000000 --- a/ext/milestones.md +++ /dev/null @@ -1,44 +0,0 @@ -# Milestones API - -## List Milestones for an Issue - -### GET `/repos/:user/:repo/milestones.json` - -* `?sort` = (String) -* `?direction` = (String) -* `?state` = open, closed, default: open - - -## Create a Milestone - -### POST `/repos/:user/:repo/milestones.json` - -### Input - - { - title: String, - state: String, - description: String, - due_on: Time, - } - -## Get a single Milestone - -### GET `/repos/:user/:repo/milestones/:id.json` - -## Update a Milestone - -### PUT `/repos/:user/:repo/milestones/:id.json` - -### Input - - { - title: String, - state: String, - description: String, - due_on: Time, - } - -## Delete a Milestone - -### DELETE `/repos/:user/:repo/milestones/:id.json` From 90aa87a6c207425f745e4d76fcd8e03dd9fc721d Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sat, 14 May 2011 13:15:21 -0400 Subject: [PATCH 036/106] requests 0.3.3 --- reqs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reqs.txt b/reqs.txt index 3ac1a76..963161c 100644 --- a/reqs.txt +++ b/reqs.txt @@ -1,2 +1,2 @@ -requests=0.3.1 +requests=0.3.3 unittest2 \ No newline at end of file From 126c3d0f692d9a4a0fd6e67daaa02cb6fb5ff7d7 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sat, 14 May 2011 13:15:46 -0400 Subject: [PATCH 037/106] syntax --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index 3baf0a3..19635e4 100644 --- a/README.rst +++ b/README.rst @@ -11,6 +11,7 @@ Usage ----- :: + from github3 import github github.login('username', 'password') From 12af2764111dc188a98a02fd6716a60242d74109 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sat, 14 May 2011 13:16:05 -0400 Subject: [PATCH 038/106] deadline fix --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 19635e4..1af8424 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,7 @@ Github3: Python wrapper for the (new) GitHub API v3 Github's working on a new API. This is the new Python wrapper for it. -**This a work in progress.** Should be relased before the first week of May. +**This a work in progress.** Should be relased before the last week of May. From 596e0d621625ac31d1b2b9e7819ef2debbd665ca Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Thu, 16 Jun 2011 01:15:47 -0400 Subject: [PATCH 039/106] fuck deadlines --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 1af8424..1ef348d 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,7 @@ Github3: Python wrapper for the (new) GitHub API v3 Github's working on a new API. This is the new Python wrapper for it. -**This a work in progress.** Should be relased before the last week of May. +**This a work in progress.** Should be relased eventually. From 86037bc4e133dd371109d305cbef4df970aa4161 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Tue, 21 Jun 2011 23:10:48 -0400 Subject: [PATCH 040/106] AUTHORS --- AUTHORS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index e4e5e13..1f3ab8c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -10,4 +10,4 @@ Development Lead Patches and Suggestions ``````````````````````` -- You ? \ No newline at end of file +- Mahdi Yusuf \ No newline at end of file From be3ac031dda3812fa6d1d9d88da1a57ce8072743 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Tue, 21 Jun 2011 23:11:03 -0400 Subject: [PATCH 041/106] bunk --- github3/api.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/github3/api.py b/github3/api.py index 3225e43..7ec59e2 100644 --- a/github3/api.py +++ b/github3/api.py @@ -1,14 +1,16 @@ # -*- coding: utf-8 -*- """ - github3.api - ~~~~~~~~~~~ +github3.api +~~~~~~~~~~~ - This module implements the GitHub3 API wrapper objects. +This module implements the GitHub3 API wrapper objects. + +:copyright: (c) 2011 by Kenneth Reitz. +:license: ISC, see LICENSE for more details. - :copyright: (c) 2011 by Kenneth Reitz. - :license: ISC, see LICENSE for more details. """ + from . import models from .packages.anyjson import deserialize From 3047e8bbcfbc0eb817b60407b0fe214d1d6985cf Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Tue, 21 Jun 2011 23:11:13 -0400 Subject: [PATCH 042/106] reqs! --- reqs.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reqs.txt b/reqs.txt index 963161c..a3447bd 100644 --- a/reqs.txt +++ b/reqs.txt @@ -1,2 +1,2 @@ -requests=0.3.3 -unittest2 \ No newline at end of file +requests=0.5.0 +omnijson=0.1.2 \ No newline at end of file From 894dfb81097fa690ec2e39a198b53fb388a7178d Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Tue, 21 Jun 2011 23:17:10 -0400 Subject: [PATCH 043/106] setup.py, ready for action --- setup.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) mode change 100644 => 100755 setup.py diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 index c5d2230..6cb32ea --- a/setup.py +++ b/setup.py @@ -4,22 +4,23 @@ import os import sys -# from distutils.core import setup -from setuptools import setup, find_packages - -import clint - +try: + from setuptools import setup, find_packages +except ImportError: + from distutils.core import setup, find_packages def publish(): """Publish to PyPi""" - os.system("python setup.py sdist upload") + os.system('python setup.py sdist upload') + -if sys.argv[-1] == "publish": +if sys.argv[-1] == 'publish': publish() sys.exit() -required = ['requests==0.3.1'] +with open('reqs.txt') as f: + required = f.readlines() setup( name='github3', @@ -29,7 +30,7 @@ def publish(): author='Kenneth Reitz', author_email='me@kennethreitz.com', url='https://github.com/kennethreitz/python-github3', - packages= find_packages('github3'), + packages=find_packages(exclude='docs'), install_requires=required, license='ISC', classifiers=( @@ -38,7 +39,7 @@ def publish(): 'Natural Language :: English', 'License :: OSI Approved :: ISC License (ISCL)', 'Programming Language :: Python', - # 'Programming Language :: Python :: 2.5', + 'Programming Language :: Python :: 2.5', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', # 'Programming Language :: Python :: 3.0', From af80458f80d1478b14e019d0f443d6179a906dde Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Tue, 21 Jun 2011 23:17:20 -0400 Subject: [PATCH 044/106] +x --- test_github3.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 test_github3.py diff --git a/test_github3.py b/test_github3.py old mode 100644 new mode 100755 From fbcbac3c3409f6b59d6620b8b44690d63a03732a Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Tue, 21 Jun 2011 23:18:39 -0400 Subject: [PATCH 045/106] no tests --- test_github3.py | 74 ------------------------------------------------- 1 file changed, 74 deletions(-) delete mode 100755 test_github3.py diff --git a/test_github3.py b/test_github3.py deleted file mode 100755 index a0464c6..0000000 --- a/test_github3.py +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" - convore tests - ~~~~~~~~~~~~~ - - Convore test suite. - - Looks for authentication in 'CONVORE_NAME' and - 'CONVORE_PASS' environment variables for configuration. - - :copyright: (c) 2011 by Kenneth Reitz. - :license: ISC, see LICENSE for more details. -""" - - -import os - -import unittest2 as unittest - -import convore - - -CONVORE_NAME = os.environ.get('CONVORE_NAME') -CONVORE_PASS = os.environ.get('CONVORE_PASS') - - - -class ConvoreAPI(unittest.TestCase): - """Requests test cases.""" - - def setUp(self): - self.convore = convore.Convore(CONVORE_NAME, CONVORE_PASS) - - def tearDown(self): - pass - - def test_convore_login(self): - self.assertEqual(self.convore.account_verify(), True) - - - - - -class ConvoreGroups(unittest.TestCase): - def setUp(self): - self.convore = convore.Convore(CONVORE_NAME, CONVORE_PASS) - - def test_works(self): - self.convore.groups - - def test_cache_works(self): - int_before = int(len(self.convore.groups)) - - self.convore.groups[81] - - int_after = int(len(self.convore.groups)) - self.assertEqual((int_after - int_before), 1) - - def test_discover_explore(self): - self.convore.groups.discover.explore.popular() - self.convore.groups.discover.explore.recent() - self.convore.groups.discover.explore.alphabetical() - - - def test_discover_category(self): - self.convore.groups.discover.category - self.convore.groups.discover.category['gossip'] - - def test_discover_search(self): - self.convore.groups.discover.search('github') - -if __name__ == '__main__': - unittest.main() From 61667e3ebd4ef6d89941fd9b84b045d64f3775eb Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Tue, 21 Jun 2011 23:26:21 -0400 Subject: [PATCH 046/106] omnijson --- NOTICE | 53 +- github3/packages/anyjson.py | 117 ---- github3/packages/omnijson/__init__.py | 13 + github3/packages/omnijson/core.py | 93 ++++ .../packages/omnijson/packages/__init__.py | 0 .../omnijson/packages/simplejson/__init__.py | 438 +++++++++++++++ .../omnijson/packages/simplejson/decoder.py | 421 +++++++++++++++ .../omnijson/packages/simplejson/encoder.py | 503 ++++++++++++++++++ .../packages/simplejson/ordered_dict.py | 119 +++++ .../omnijson/packages/simplejson/scanner.py | 70 +++ 10 files changed, 1684 insertions(+), 143 deletions(-) delete mode 100644 github3/packages/anyjson.py create mode 100644 github3/packages/omnijson/__init__.py create mode 100644 github3/packages/omnijson/core.py create mode 100644 github3/packages/omnijson/packages/__init__.py create mode 100644 github3/packages/omnijson/packages/simplejson/__init__.py create mode 100644 github3/packages/omnijson/packages/simplejson/decoder.py create mode 100644 github3/packages/omnijson/packages/simplejson/encoder.py create mode 100644 github3/packages/omnijson/packages/simplejson/ordered_dict.py create mode 100644 github3/packages/omnijson/packages/simplejson/scanner.py diff --git a/NOTICE b/NOTICE index 9d89278..0a0d3fe 100644 --- a/NOTICE +++ b/NOTICE @@ -1,4 +1,4 @@ -python-github3 includes some vendorized python libraries: ordereddict and anyjson. +python-github3 includes some vendorized python libraries: ordereddict, omijson, and simplejson. OrderedDict License @@ -28,37 +28,38 @@ subject to the following conditions: -AnyJSON License +OmniJSON License ================== -This software is licensed under the ``New BSD License``: +Copyright (c) 2011 Kenneth Reitz -Copyright (c) 2009, by the authors -All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - * Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 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. -Neither the name of the authors nor the names of its contributors may be used -to endorse or promote products derived from this software without specific -prior written permission. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS -BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. +SimpleJSON License +================== + + +Copyright (c) 2006 Bob Ippolito + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +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. diff --git a/github3/packages/anyjson.py b/github3/packages/anyjson.py deleted file mode 100644 index a7d1a5f..0000000 --- a/github3/packages/anyjson.py +++ /dev/null @@ -1,117 +0,0 @@ -""" -Wraps the best available JSON implementation available in a common interface -""" - -__version__ = "0.2.0" -__author__ = "Rune Halvorsen " -__homepage__ = "http://bitbucket.org/runeh/anyjson/" -__docformat__ = "restructuredtext" - -""" - -.. function:: serialize(obj) - - Serialize the object to JSON. - -.. function:: deserialize(str) - - Deserialize JSON-encoded object to a Python object. - -.. function:: force_implementation(name) - - Load a specific json module. This is useful for testing and not much else - -.. attribute:: implementation - - The json implementation object. This is probably not useful to you, - except to get the name of the implementation in use. The name is - available through `implementation.name`. -""" - -import sys - -implementation = None - -""" -.. data:: _modules - - List of known json modules, and the names of their serialize/unserialize - methods, as well as the exception they throw. Exception can be either - an exception class or a string. -""" -_modules = [("cjson", "encode", "EncodeError", "decode", "DecodeError"), - ("jsonlib2", "write", "WriteError", "read", "ReadError"), - ("jsonlib", "write", "WriteError", "read", "ReadError"), - ("simplejson", "dumps", TypeError, "loads", ValueError), - ("json", "dumps", TypeError, "loads", ValueError), - ("django.utils.simplejson", "dumps", TypeError, "loads", - ValueError)] -_fields = ("modname", "encoder", "encerror", "decoder", "decerror") - - -class _JsonImplementation(object): - """Incapsulates a JSON implementation""" - - def __init__(self, modspec): - modinfo = dict(list(zip(_fields, modspec))) - - # No try block. We want importerror to end up at caller - module = self._attempt_load(modinfo["modname"]) - - self.implementation = modinfo["modname"] - self._encode = getattr(module, modinfo["encoder"]) - self._decode = getattr(module, modinfo["decoder"]) - self._encode_error = modinfo["encerror"] - self._decode_error = modinfo["decerror"] - - if isinstance(modinfo["encerror"], str): - self._encode_error = getattr(module, modinfo["encerror"]) - if isinstance(modinfo["decerror"], str): - self._decode_error = getattr(module, modinfo["decerror"]) - - self.name = modinfo["modname"] - - def _attempt_load(self, modname): - """Attempt to load module name modname, returning it on success, - throwing ImportError if module couldn't be imported""" - __import__(modname) - return sys.modules[modname] - - def serialize(self, data): - """Serialize the datastructure to json. Returns a string. Raises - TypeError if the object could not be serialized.""" - try: - return self._encode(data) - except self._encode_error as exc: - raise TypeError(*exc.args) - - def deserialize(self, s): - """deserialize the string to python data types. Raises - ValueError if the string vould not be parsed.""" - try: - return self._decode(s) - except self._decode_error as exc: - raise ValueError(*exc.args) - - -def force_implementation(modname): - """Forces anyjson to use a specific json module if it's available""" - global implementation - for name, spec in [(e[0], e) for e in _modules]: - if name == modname: - implementation = _JsonImplementation(spec) - return - raise ImportError("No module named: %s" % modname) - - -for modspec in _modules: - try: - implementation = _JsonImplementation(modspec) - break - except ImportError: - pass -else: - raise ImportError("No supported JSON module found") - -serialize = lambda value: implementation.serialize(value) -deserialize = lambda value: implementation.deserialize(value) diff --git a/github3/packages/omnijson/__init__.py b/github3/packages/omnijson/__init__.py new file mode 100644 index 0000000..c10c328 --- /dev/null +++ b/github3/packages/omnijson/__init__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +from .core import loads, dumps, JSONError + + +__all__ = ('loads', 'dumps', 'JSONError') + + +__version__ = '0.1.2' +__author__ = 'Kenneth Reitz' +__license__ = 'MIT' diff --git a/github3/packages/omnijson/core.py b/github3/packages/omnijson/core.py new file mode 100644 index 0000000..8b49537 --- /dev/null +++ b/github3/packages/omnijson/core.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- + +""" +omijson.core +~~~~~~~~~~~~ + +This module provides the core omnijson functionality. + +""" + +import sys + +engine = None +_engine = None + + +options = [ + ['ujson', 'loads', 'dumps', (ValueError,)], + ['yajl', 'loads', 'dumps', (TypeError, ValueError)], + ['jsonlib2', 'read', 'write', (ValueError,)], + ['jsonlib', 'read', 'write', (ValueError,)], + ['simplejson', 'loads', 'dumps', (TypeError, ValueError)], + ['json', 'loads', 'dumps', (TypeError, ValueError)], + ['simplejson_from_packages', 'loads', 'dumps', (ValueError,)], +] + + +def _import(engine): + try: + if '_from_' in engine: + engine, package = engine.split('_from_') + m = __import__(package, globals(), locals(), [engine], -1) + return getattr(m, engine) + + return __import__(engine) + + except ImportError: + return False + + +def loads(s, **kwargs): + """Loads JSON object.""" + + try: + return _engine[0](s) + + except: + # crazy 2/3 exception hack + # http://www.voidspace.org.uk/python/weblog/arch_d7_2010_03_20.shtml + + ExceptionClass, why = sys.exc_info()[:2] + + if any([(issubclass(ExceptionClass, e)) for e in _engine[2]]): + raise JSONError(why) + else: + raise why + + +def dumps(o, **kwargs): + """Dumps JSON object.""" + + try: + return _engine[1](o) + + except: + ExceptionClass, why = sys.exc_info()[:2] + + if any([(issubclass(ExceptionClass, e)) for e in _engine[2]]): + raise JSONError(why) + else: + raise why + + +class JSONError(ValueError): + """JSON Failed.""" + + +# ------ +# Magic! +# ------ + + +for e in options: + + __engine = _import(e[0]) + + if __engine: + engine, _engine = e[0], e[1:4] + + for i in (0, 1): + _engine[i] = getattr(__engine, _engine[i]) + + break diff --git a/github3/packages/omnijson/packages/__init__.py b/github3/packages/omnijson/packages/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/github3/packages/omnijson/packages/simplejson/__init__.py b/github3/packages/omnijson/packages/simplejson/__init__.py new file mode 100644 index 0000000..210b957 --- /dev/null +++ b/github3/packages/omnijson/packages/simplejson/__init__.py @@ -0,0 +1,438 @@ +r"""JSON (JavaScript Object Notation) is a subset of +JavaScript syntax (ECMA-262 3rd edition) used as a lightweight data +interchange format. + +:mod:`simplejson` exposes an API familiar to users of the standard library +:mod:`marshal` and :mod:`pickle` modules. It is the externally maintained +version of the :mod:`json` library contained in Python 2.6, but maintains +compatibility with Python 2.4 and Python 2.5 and (currently) has +significant performance advantages, even without using the optional C +extension for speedups. + +Encoding basic Python object hierarchies:: + + >>> import simplejson as json + >>> json.dumps(['foo', {'bar': ('baz', None, 1.0, 2)}]) + '["foo", {"bar": ["baz", null, 1.0, 2]}]' + >>> print json.dumps("\"foo\bar") + "\"foo\bar" + >>> print json.dumps(u'\u1234') + "\u1234" + >>> print json.dumps('\\') + "\\" + >>> print json.dumps({"c": 0, "b": 0, "a": 0}, sort_keys=True) + {"a": 0, "b": 0, "c": 0} + >>> from StringIO import StringIO + >>> io = StringIO() + >>> json.dump(['streaming API'], io) + >>> io.getvalue() + '["streaming API"]' + +Compact encoding:: + + >>> import simplejson as json + >>> json.dumps([1,2,3,{'4': 5, '6': 7}], separators=(',',':')) + '[1,2,3,{"4":5,"6":7}]' + +Pretty printing:: + + >>> import simplejson as json + >>> s = json.dumps({'4': 5, '6': 7}, sort_keys=True, indent=' ') + >>> print '\n'.join([l.rstrip() for l in s.splitlines()]) + { + "4": 5, + "6": 7 + } + +Decoding JSON:: + + >>> import simplejson as json + >>> obj = [u'foo', {u'bar': [u'baz', None, 1.0, 2]}] + >>> json.loads('["foo", {"bar":["baz", null, 1.0, 2]}]') == obj + True + >>> json.loads('"\\"foo\\bar"') == u'"foo\x08ar' + True + >>> from StringIO import StringIO + >>> io = StringIO('["streaming API"]') + >>> json.load(io)[0] == 'streaming API' + True + +Specializing JSON object decoding:: + + >>> import simplejson as json + >>> def as_complex(dct): + ... if '__complex__' in dct: + ... return complex(dct['real'], dct['imag']) + ... return dct + ... + >>> json.loads('{"__complex__": true, "real": 1, "imag": 2}', + ... object_hook=as_complex) + (1+2j) + >>> from decimal import Decimal + >>> json.loads('1.1', parse_float=Decimal) == Decimal('1.1') + True + +Specializing JSON object encoding:: + + >>> import simplejson as json + >>> def encode_complex(obj): + ... if isinstance(obj, complex): + ... return [obj.real, obj.imag] + ... raise TypeError(repr(o) + " is not JSON serializable") + ... + >>> json.dumps(2 + 1j, default=encode_complex) + '[2.0, 1.0]' + >>> json.JSONEncoder(default=encode_complex).encode(2 + 1j) + '[2.0, 1.0]' + >>> ''.join(json.JSONEncoder(default=encode_complex).iterencode(2 + 1j)) + '[2.0, 1.0]' + + +Using simplejson.tool from the shell to validate and pretty-print:: + + $ echo '{"json":"obj"}' | python -m simplejson.tool + { + "json": "obj" + } + $ echo '{ 1.2:3.4}' | python -m simplejson.tool + Expecting property name: line 1 column 2 (char 2) +""" +__version__ = '2.1.6' +__all__ = [ + 'dump', 'dumps', 'load', 'loads', + 'JSONDecoder', 'JSONDecodeError', 'JSONEncoder', + 'OrderedDict', +] + +__author__ = 'Bob Ippolito ' + +from decimal import Decimal + +from decoder import JSONDecoder, JSONDecodeError +from encoder import JSONEncoder +def _import_OrderedDict(): + import collections + try: + return collections.OrderedDict + except AttributeError: + import ordered_dict + return ordered_dict.OrderedDict +OrderedDict = _import_OrderedDict() + +def _import_c_make_encoder(): + try: + from simplejson._speedups import make_encoder + return make_encoder + except ImportError: + return None + +_default_encoder = JSONEncoder( + skipkeys=False, + ensure_ascii=True, + check_circular=True, + allow_nan=True, + indent=None, + separators=None, + encoding='utf-8', + default=None, + use_decimal=False, +) + +def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True, + allow_nan=True, cls=None, indent=None, separators=None, + encoding='utf-8', default=None, use_decimal=False, **kw): + """Serialize ``obj`` as a JSON formatted stream to ``fp`` (a + ``.write()``-supporting file-like object). + + If ``skipkeys`` is true then ``dict`` keys that are not basic types + (``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``) + will be skipped instead of raising a ``TypeError``. + + If ``ensure_ascii`` is false, then the some chunks written to ``fp`` + may be ``unicode`` instances, subject to normal Python ``str`` to + ``unicode`` coercion rules. Unless ``fp.write()`` explicitly + understands ``unicode`` (as in ``codecs.getwriter()``) this is likely + to cause an error. + + If ``check_circular`` is false, then the circular reference check + for container types will be skipped and a circular reference will + result in an ``OverflowError`` (or worse). + + If ``allow_nan`` is false, then it will be a ``ValueError`` to + serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) + in strict compliance of the JSON specification, instead of using the + JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``). + + If *indent* is a string, then JSON array elements and object members + will be pretty-printed with a newline followed by that string repeated + for each level of nesting. ``None`` (the default) selects the most compact + representation without any newlines. For backwards compatibility with + versions of simplejson earlier than 2.1.0, an integer is also accepted + and is converted to a string with that many spaces. + + If ``separators`` is an ``(item_separator, dict_separator)`` tuple + then it will be used instead of the default ``(', ', ': ')`` separators. + ``(',', ':')`` is the most compact JSON representation. + + ``encoding`` is the character encoding for str instances, default is UTF-8. + + ``default(obj)`` is a function that should return a serializable version + of obj or raise TypeError. The default simply raises TypeError. + + If *use_decimal* is true (default: ``False``) then decimal.Decimal + will be natively serialized to JSON with full precision. + + To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the + ``.default()`` method to serialize additional types), specify it with + the ``cls`` kwarg. + + """ + # cached encoder + if (not skipkeys and ensure_ascii and + check_circular and allow_nan and + cls is None and indent is None and separators is None and + encoding == 'utf-8' and default is None and not use_decimal + and not kw): + iterable = _default_encoder.iterencode(obj) + else: + if cls is None: + cls = JSONEncoder + iterable = cls(skipkeys=skipkeys, ensure_ascii=ensure_ascii, + check_circular=check_circular, allow_nan=allow_nan, indent=indent, + separators=separators, encoding=encoding, + default=default, use_decimal=use_decimal, **kw).iterencode(obj) + # could accelerate with writelines in some versions of Python, at + # a debuggability cost + for chunk in iterable: + fp.write(chunk) + + +def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True, + allow_nan=True, cls=None, indent=None, separators=None, + encoding='utf-8', default=None, use_decimal=False, **kw): + """Serialize ``obj`` to a JSON formatted ``str``. + + If ``skipkeys`` is false then ``dict`` keys that are not basic types + (``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``) + will be skipped instead of raising a ``TypeError``. + + If ``ensure_ascii`` is false, then the return value will be a + ``unicode`` instance subject to normal Python ``str`` to ``unicode`` + coercion rules instead of being escaped to an ASCII ``str``. + + If ``check_circular`` is false, then the circular reference check + for container types will be skipped and a circular reference will + result in an ``OverflowError`` (or worse). + + If ``allow_nan`` is false, then it will be a ``ValueError`` to + serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) in + strict compliance of the JSON specification, instead of using the + JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``). + + If ``indent`` is a string, then JSON array elements and object members + will be pretty-printed with a newline followed by that string repeated + for each level of nesting. ``None`` (the default) selects the most compact + representation without any newlines. For backwards compatibility with + versions of simplejson earlier than 2.1.0, an integer is also accepted + and is converted to a string with that many spaces. + + If ``separators`` is an ``(item_separator, dict_separator)`` tuple + then it will be used instead of the default ``(', ', ': ')`` separators. + ``(',', ':')`` is the most compact JSON representation. + + ``encoding`` is the character encoding for str instances, default is UTF-8. + + ``default(obj)`` is a function that should return a serializable version + of obj or raise TypeError. The default simply raises TypeError. + + If *use_decimal* is true (default: ``False``) then decimal.Decimal + will be natively serialized to JSON with full precision. + + To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the + ``.default()`` method to serialize additional types), specify it with + the ``cls`` kwarg. + + """ + # cached encoder + if (not skipkeys and ensure_ascii and + check_circular and allow_nan and + cls is None and indent is None and separators is None and + encoding == 'utf-8' and default is None and not use_decimal + and not kw): + return _default_encoder.encode(obj) + if cls is None: + cls = JSONEncoder + return cls( + skipkeys=skipkeys, ensure_ascii=ensure_ascii, + check_circular=check_circular, allow_nan=allow_nan, indent=indent, + separators=separators, encoding=encoding, default=default, + use_decimal=use_decimal, **kw).encode(obj) + + +_default_decoder = JSONDecoder(encoding=None, object_hook=None, + object_pairs_hook=None) + + +def load(fp, encoding=None, cls=None, object_hook=None, parse_float=None, + parse_int=None, parse_constant=None, object_pairs_hook=None, + use_decimal=False, **kw): + """Deserialize ``fp`` (a ``.read()``-supporting file-like object containing + a JSON document) to a Python object. + + *encoding* determines the encoding used to interpret any + :class:`str` objects decoded by this instance (``'utf-8'`` by + default). It has no effect when decoding :class:`unicode` objects. + + Note that currently only encodings that are a superset of ASCII work, + strings of other encodings should be passed in as :class:`unicode`. + + *object_hook*, if specified, will be called with the result of every + JSON object decoded and its return value will be used in place of the + given :class:`dict`. This can be used to provide custom + deserializations (e.g. to support JSON-RPC class hinting). + + *object_pairs_hook* is an optional function that will be called with + the result of any object literal decode with an ordered list of pairs. + The return value of *object_pairs_hook* will be used instead of the + :class:`dict`. This feature can be used to implement custom decoders + that rely on the order that the key and value pairs are decoded (for + example, :func:`collections.OrderedDict` will remember the order of + insertion). If *object_hook* is also defined, the *object_pairs_hook* + takes priority. + + *parse_float*, if specified, will be called with the string of every + JSON float to be decoded. By default, this is equivalent to + ``float(num_str)``. This can be used to use another datatype or parser + for JSON floats (e.g. :class:`decimal.Decimal`). + + *parse_int*, if specified, will be called with the string of every + JSON int to be decoded. By default, this is equivalent to + ``int(num_str)``. This can be used to use another datatype or parser + for JSON integers (e.g. :class:`float`). + + *parse_constant*, if specified, will be called with one of the + following strings: ``'-Infinity'``, ``'Infinity'``, ``'NaN'``. This + can be used to raise an exception if invalid JSON numbers are + encountered. + + If *use_decimal* is true (default: ``False``) then it implies + parse_float=decimal.Decimal for parity with ``dump``. + + To use a custom ``JSONDecoder`` subclass, specify it with the ``cls`` + kwarg. + + """ + return loads(fp.read(), + encoding=encoding, cls=cls, object_hook=object_hook, + parse_float=parse_float, parse_int=parse_int, + parse_constant=parse_constant, object_pairs_hook=object_pairs_hook, + use_decimal=use_decimal, **kw) + + +def loads(s, encoding=None, cls=None, object_hook=None, parse_float=None, + parse_int=None, parse_constant=None, object_pairs_hook=None, + use_decimal=False, **kw): + """Deserialize ``s`` (a ``str`` or ``unicode`` instance containing a JSON + document) to a Python object. + + *encoding* determines the encoding used to interpret any + :class:`str` objects decoded by this instance (``'utf-8'`` by + default). It has no effect when decoding :class:`unicode` objects. + + Note that currently only encodings that are a superset of ASCII work, + strings of other encodings should be passed in as :class:`unicode`. + + *object_hook*, if specified, will be called with the result of every + JSON object decoded and its return value will be used in place of the + given :class:`dict`. This can be used to provide custom + deserializations (e.g. to support JSON-RPC class hinting). + + *object_pairs_hook* is an optional function that will be called with + the result of any object literal decode with an ordered list of pairs. + The return value of *object_pairs_hook* will be used instead of the + :class:`dict`. This feature can be used to implement custom decoders + that rely on the order that the key and value pairs are decoded (for + example, :func:`collections.OrderedDict` will remember the order of + insertion). If *object_hook* is also defined, the *object_pairs_hook* + takes priority. + + *parse_float*, if specified, will be called with the string of every + JSON float to be decoded. By default, this is equivalent to + ``float(num_str)``. This can be used to use another datatype or parser + for JSON floats (e.g. :class:`decimal.Decimal`). + + *parse_int*, if specified, will be called with the string of every + JSON int to be decoded. By default, this is equivalent to + ``int(num_str)``. This can be used to use another datatype or parser + for JSON integers (e.g. :class:`float`). + + *parse_constant*, if specified, will be called with one of the + following strings: ``'-Infinity'``, ``'Infinity'``, ``'NaN'``. This + can be used to raise an exception if invalid JSON numbers are + encountered. + + If *use_decimal* is true (default: ``False``) then it implies + parse_float=decimal.Decimal for parity with ``dump``. + + To use a custom ``JSONDecoder`` subclass, specify it with the ``cls`` + kwarg. + + """ + if (cls is None and encoding is None and object_hook is None and + parse_int is None and parse_float is None and + parse_constant is None and object_pairs_hook is None + and not use_decimal and not kw): + return _default_decoder.decode(s) + if cls is None: + cls = JSONDecoder + if object_hook is not None: + kw['object_hook'] = object_hook + if object_pairs_hook is not None: + kw['object_pairs_hook'] = object_pairs_hook + if parse_float is not None: + kw['parse_float'] = parse_float + if parse_int is not None: + kw['parse_int'] = parse_int + if parse_constant is not None: + kw['parse_constant'] = parse_constant + if use_decimal: + if parse_float is not None: + raise TypeError("use_decimal=True implies parse_float=Decimal") + kw['parse_float'] = Decimal + return cls(encoding=encoding, **kw).decode(s) + + +def _toggle_speedups(enabled): + import simplejson.decoder as dec + import simplejson.encoder as enc + import simplejson.scanner as scan + c_make_encoder = _import_c_make_encoder() + if enabled: + dec.scanstring = dec.c_scanstring or dec.py_scanstring + enc.c_make_encoder = c_make_encoder + enc.encode_basestring_ascii = (enc.c_encode_basestring_ascii or + enc.py_encode_basestring_ascii) + scan.make_scanner = scan.c_make_scanner or scan.py_make_scanner + else: + dec.scanstring = dec.py_scanstring + enc.c_make_encoder = None + enc.encode_basestring_ascii = enc.py_encode_basestring_ascii + scan.make_scanner = scan.py_make_scanner + dec.make_scanner = scan.make_scanner + global _default_decoder + _default_decoder = JSONDecoder( + encoding=None, + object_hook=None, + object_pairs_hook=None, + ) + global _default_encoder + _default_encoder = JSONEncoder( + skipkeys=False, + ensure_ascii=True, + check_circular=True, + allow_nan=True, + indent=None, + separators=None, + encoding='utf-8', + default=None, + ) diff --git a/github3/packages/omnijson/packages/simplejson/decoder.py b/github3/packages/omnijson/packages/simplejson/decoder.py new file mode 100644 index 0000000..3e36e56 --- /dev/null +++ b/github3/packages/omnijson/packages/simplejson/decoder.py @@ -0,0 +1,421 @@ +"""Implementation of JSONDecoder +""" +import re +import sys +import struct + +from .scanner import make_scanner +def _import_c_scanstring(): + try: + from simplejson._speedups import scanstring + return scanstring + except ImportError: + return None +c_scanstring = _import_c_scanstring() + +__all__ = ['JSONDecoder'] + +FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL + +def _floatconstants(): + _BYTES = '7FF80000000000007FF0000000000000'.decode('hex') + # The struct module in Python 2.4 would get frexp() out of range here + # when an endian is specified in the format string. Fixed in Python 2.5+ + if sys.byteorder != 'big': + _BYTES = _BYTES[:8][::-1] + _BYTES[8:][::-1] + nan, inf = struct.unpack('dd', _BYTES) + return nan, inf, -inf + +NaN, PosInf, NegInf = _floatconstants() + + +class JSONDecodeError(ValueError): + """Subclass of ValueError with the following additional properties: + + msg: The unformatted error message + doc: The JSON document being parsed + pos: The start index of doc where parsing failed + end: The end index of doc where parsing failed (may be None) + lineno: The line corresponding to pos + colno: The column corresponding to pos + endlineno: The line corresponding to end (may be None) + endcolno: The column corresponding to end (may be None) + + """ + def __init__(self, msg, doc, pos, end=None): + ValueError.__init__(self, errmsg(msg, doc, pos, end=end)) + self.msg = msg + self.doc = doc + self.pos = pos + self.end = end + self.lineno, self.colno = linecol(doc, pos) + if end is not None: + self.endlineno, self.endcolno = linecol(doc, end) + else: + self.endlineno, self.endcolno = None, None + + +def linecol(doc, pos): + lineno = doc.count('\n', 0, pos) + 1 + if lineno == 1: + colno = pos + else: + colno = pos - doc.rindex('\n', 0, pos) + return lineno, colno + + +def errmsg(msg, doc, pos, end=None): + # Note that this function is called from _speedups + lineno, colno = linecol(doc, pos) + if end is None: + #fmt = '{0}: line {1} column {2} (char {3})' + #return fmt.format(msg, lineno, colno, pos) + fmt = '%s: line %d column %d (char %d)' + return fmt % (msg, lineno, colno, pos) + endlineno, endcolno = linecol(doc, end) + #fmt = '{0}: line {1} column {2} - line {3} column {4} (char {5} - {6})' + #return fmt.format(msg, lineno, colno, endlineno, endcolno, pos, end) + fmt = '%s: line %d column %d - line %d column %d (char %d - %d)' + return fmt % (msg, lineno, colno, endlineno, endcolno, pos, end) + + +_CONSTANTS = { + '-Infinity': NegInf, + 'Infinity': PosInf, + 'NaN': NaN, +} + +STRINGCHUNK = re.compile(r'(.*?)(["\\\x00-\x1f])', FLAGS) +BACKSLASH = { + '"': u'"', '\\': u'\\', '/': u'/', + 'b': u'\b', 'f': u'\f', 'n': u'\n', 'r': u'\r', 't': u'\t', +} + +DEFAULT_ENCODING = "utf-8" + +def py_scanstring(s, end, encoding=None, strict=True, + _b=BACKSLASH, _m=STRINGCHUNK.match): + """Scan the string s for a JSON string. End is the index of the + character in s after the quote that started the JSON string. + Unescapes all valid JSON string escape sequences and raises ValueError + on attempt to decode an invalid string. If strict is False then literal + control characters are allowed in the string. + + Returns a tuple of the decoded string and the index of the character in s + after the end quote.""" + if encoding is None: + encoding = DEFAULT_ENCODING + chunks = [] + _append = chunks.append + begin = end - 1 + while 1: + chunk = _m(s, end) + if chunk is None: + raise JSONDecodeError( + "Unterminated string starting at", s, begin) + end = chunk.end() + content, terminator = chunk.groups() + # Content is contains zero or more unescaped string characters + if content: + if not isinstance(content, unicode): + content = unicode(content, encoding) + _append(content) + # Terminator is the end of string, a literal control character, + # or a backslash denoting that an escape sequence follows + if terminator == '"': + break + elif terminator != '\\': + if strict: + msg = "Invalid control character %r at" % (terminator,) + #msg = "Invalid control character {0!r} at".format(terminator) + raise JSONDecodeError(msg, s, end) + else: + _append(terminator) + continue + try: + esc = s[end] + except IndexError: + raise JSONDecodeError( + "Unterminated string starting at", s, begin) + # If not a unicode escape sequence, must be in the lookup table + if esc != 'u': + try: + char = _b[esc] + except KeyError: + msg = "Invalid \\escape: " + repr(esc) + raise JSONDecodeError(msg, s, end) + end += 1 + else: + # Unicode escape sequence + esc = s[end + 1:end + 5] + next_end = end + 5 + if len(esc) != 4: + msg = "Invalid \\uXXXX escape" + raise JSONDecodeError(msg, s, end) + uni = int(esc, 16) + # Check for surrogate pair on UCS-4 systems + if 0xd800 <= uni <= 0xdbff and sys.maxunicode > 65535: + msg = "Invalid \\uXXXX\\uXXXX surrogate pair" + if not s[end + 5:end + 7] == '\\u': + raise JSONDecodeError(msg, s, end) + esc2 = s[end + 7:end + 11] + if len(esc2) != 4: + raise JSONDecodeError(msg, s, end) + uni2 = int(esc2, 16) + uni = 0x10000 + (((uni - 0xd800) << 10) | (uni2 - 0xdc00)) + next_end += 6 + char = unichr(uni) + end = next_end + # Append the unescaped character + _append(char) + return u''.join(chunks), end + + +# Use speedup if available +scanstring = c_scanstring or py_scanstring + +WHITESPACE = re.compile(r'[ \t\n\r]*', FLAGS) +WHITESPACE_STR = ' \t\n\r' + +def JSONObject((s, end), encoding, strict, scan_once, object_hook, + object_pairs_hook, memo=None, + _w=WHITESPACE.match, _ws=WHITESPACE_STR): + # Backwards compatibility + if memo is None: + memo = {} + memo_get = memo.setdefault + pairs = [] + # Use a slice to prevent IndexError from being raised, the following + # check will raise a more specific ValueError if the string is empty + nextchar = s[end:end + 1] + # Normally we expect nextchar == '"' + if nextchar != '"': + if nextchar in _ws: + end = _w(s, end).end() + nextchar = s[end:end + 1] + # Trivial empty object + if nextchar == '}': + if object_pairs_hook is not None: + result = object_pairs_hook(pairs) + return result, end + 1 + pairs = {} + if object_hook is not None: + pairs = object_hook(pairs) + return pairs, end + 1 + elif nextchar != '"': + raise JSONDecodeError("Expecting property name", s, end) + end += 1 + while True: + key, end = scanstring(s, end, encoding, strict) + key = memo_get(key, key) + + # To skip some function call overhead we optimize the fast paths where + # the JSON key separator is ": " or just ":". + if s[end:end + 1] != ':': + end = _w(s, end).end() + if s[end:end + 1] != ':': + raise JSONDecodeError("Expecting : delimiter", s, end) + + end += 1 + + try: + if s[end] in _ws: + end += 1 + if s[end] in _ws: + end = _w(s, end + 1).end() + except IndexError: + pass + + try: + value, end = scan_once(s, end) + except StopIteration: + raise JSONDecodeError("Expecting object", s, end) + pairs.append((key, value)) + + try: + nextchar = s[end] + if nextchar in _ws: + end = _w(s, end + 1).end() + nextchar = s[end] + except IndexError: + nextchar = '' + end += 1 + + if nextchar == '}': + break + elif nextchar != ',': + raise JSONDecodeError("Expecting , delimiter", s, end - 1) + + try: + nextchar = s[end] + if nextchar in _ws: + end += 1 + nextchar = s[end] + if nextchar in _ws: + end = _w(s, end + 1).end() + nextchar = s[end] + except IndexError: + nextchar = '' + + end += 1 + if nextchar != '"': + raise JSONDecodeError("Expecting property name", s, end - 1) + + if object_pairs_hook is not None: + result = object_pairs_hook(pairs) + return result, end + pairs = dict(pairs) + if object_hook is not None: + pairs = object_hook(pairs) + return pairs, end + +def JSONArray((s, end), scan_once, _w=WHITESPACE.match, _ws=WHITESPACE_STR): + values = [] + nextchar = s[end:end + 1] + if nextchar in _ws: + end = _w(s, end + 1).end() + nextchar = s[end:end + 1] + # Look-ahead for trivial empty array + if nextchar == ']': + return values, end + 1 + _append = values.append + while True: + try: + value, end = scan_once(s, end) + except StopIteration: + raise JSONDecodeError("Expecting object", s, end) + _append(value) + nextchar = s[end:end + 1] + if nextchar in _ws: + end = _w(s, end + 1).end() + nextchar = s[end:end + 1] + end += 1 + if nextchar == ']': + break + elif nextchar != ',': + raise JSONDecodeError("Expecting , delimiter", s, end) + + try: + if s[end] in _ws: + end += 1 + if s[end] in _ws: + end = _w(s, end + 1).end() + except IndexError: + pass + + return values, end + +class JSONDecoder(object): + """Simple JSON decoder + + Performs the following translations in decoding by default: + + +---------------+-------------------+ + | JSON | Python | + +===============+===================+ + | object | dict | + +---------------+-------------------+ + | array | list | + +---------------+-------------------+ + | string | unicode | + +---------------+-------------------+ + | number (int) | int, long | + +---------------+-------------------+ + | number (real) | float | + +---------------+-------------------+ + | true | True | + +---------------+-------------------+ + | false | False | + +---------------+-------------------+ + | null | None | + +---------------+-------------------+ + + It also understands ``NaN``, ``Infinity``, and ``-Infinity`` as + their corresponding ``float`` values, which is outside the JSON spec. + + """ + + def __init__(self, encoding=None, object_hook=None, parse_float=None, + parse_int=None, parse_constant=None, strict=True, + object_pairs_hook=None): + """ + *encoding* determines the encoding used to interpret any + :class:`str` objects decoded by this instance (``'utf-8'`` by + default). It has no effect when decoding :class:`unicode` objects. + + Note that currently only encodings that are a superset of ASCII work, + strings of other encodings should be passed in as :class:`unicode`. + + *object_hook*, if specified, will be called with the result of every + JSON object decoded and its return value will be used in place of the + given :class:`dict`. This can be used to provide custom + deserializations (e.g. to support JSON-RPC class hinting). + + *object_pairs_hook* is an optional function that will be called with + the result of any object literal decode with an ordered list of pairs. + The return value of *object_pairs_hook* will be used instead of the + :class:`dict`. This feature can be used to implement custom decoders + that rely on the order that the key and value pairs are decoded (for + example, :func:`collections.OrderedDict` will remember the order of + insertion). If *object_hook* is also defined, the *object_pairs_hook* + takes priority. + + *parse_float*, if specified, will be called with the string of every + JSON float to be decoded. By default, this is equivalent to + ``float(num_str)``. This can be used to use another datatype or parser + for JSON floats (e.g. :class:`decimal.Decimal`). + + *parse_int*, if specified, will be called with the string of every + JSON int to be decoded. By default, this is equivalent to + ``int(num_str)``. This can be used to use another datatype or parser + for JSON integers (e.g. :class:`float`). + + *parse_constant*, if specified, will be called with one of the + following strings: ``'-Infinity'``, ``'Infinity'``, ``'NaN'``. This + can be used to raise an exception if invalid JSON numbers are + encountered. + + *strict* controls the parser's behavior when it encounters an + invalid control character in a string. The default setting of + ``True`` means that unescaped control characters are parse errors, if + ``False`` then control characters will be allowed in strings. + + """ + self.encoding = encoding + self.object_hook = object_hook + self.object_pairs_hook = object_pairs_hook + self.parse_float = parse_float or float + self.parse_int = parse_int or int + self.parse_constant = parse_constant or _CONSTANTS.__getitem__ + self.strict = strict + self.parse_object = JSONObject + self.parse_array = JSONArray + self.parse_string = scanstring + self.memo = {} + self.scan_once = make_scanner(self) + + def decode(self, s, _w=WHITESPACE.match): + """Return the Python representation of ``s`` (a ``str`` or ``unicode`` + instance containing a JSON document) + + """ + obj, end = self.raw_decode(s, idx=_w(s, 0).end()) + end = _w(s, end).end() + if end != len(s): + raise JSONDecodeError("Extra data", s, end, len(s)) + return obj + + def raw_decode(self, s, idx=0): + """Decode a JSON document from ``s`` (a ``str`` or ``unicode`` + beginning with a JSON document) and return a 2-tuple of the Python + representation and the index in ``s`` where the document ended. + + This can be used to decode a JSON document from a string that may + have extraneous data at the end. + + """ + try: + obj, end = self.scan_once(s, idx) + except StopIteration: + raise JSONDecodeError("No JSON object could be decoded", s, idx) + return obj, end diff --git a/github3/packages/omnijson/packages/simplejson/encoder.py b/github3/packages/omnijson/packages/simplejson/encoder.py new file mode 100644 index 0000000..f1269f3 --- /dev/null +++ b/github3/packages/omnijson/packages/simplejson/encoder.py @@ -0,0 +1,503 @@ +"""Implementation of JSONEncoder +""" +import re +from decimal import Decimal + +def _import_speedups(): + try: + from simplejson import _speedups + return _speedups.encode_basestring_ascii, _speedups.make_encoder + except ImportError: + return None, None +c_encode_basestring_ascii, c_make_encoder = _import_speedups() + +from .decoder import PosInf + +ESCAPE = re.compile(r'[\x00-\x1f\\"\b\f\n\r\t]') +ESCAPE_ASCII = re.compile(r'([\\"]|[^\ -~])') +HAS_UTF8 = re.compile(r'[\x80-\xff]') +ESCAPE_DCT = { + '\\': '\\\\', + '"': '\\"', + '\b': '\\b', + '\f': '\\f', + '\n': '\\n', + '\r': '\\r', + '\t': '\\t', +} +for i in range(0x20): + #ESCAPE_DCT.setdefault(chr(i), '\\u{0:04x}'.format(i)) + ESCAPE_DCT.setdefault(chr(i), '\\u%04x' % (i,)) + +FLOAT_REPR = repr + +def encode_basestring(s): + """Return a JSON representation of a Python string + + """ + if isinstance(s, str) and HAS_UTF8.search(s) is not None: + s = s.decode('utf-8') + def replace(match): + return ESCAPE_DCT[match.group(0)] + return u'"' + ESCAPE.sub(replace, s) + u'"' + + +def py_encode_basestring_ascii(s): + """Return an ASCII-only JSON representation of a Python string + + """ + if isinstance(s, str) and HAS_UTF8.search(s) is not None: + s = s.decode('utf-8') + def replace(match): + s = match.group(0) + try: + return ESCAPE_DCT[s] + except KeyError: + n = ord(s) + if n < 0x10000: + #return '\\u{0:04x}'.format(n) + return '\\u%04x' % (n,) + else: + # surrogate pair + n -= 0x10000 + s1 = 0xd800 | ((n >> 10) & 0x3ff) + s2 = 0xdc00 | (n & 0x3ff) + #return '\\u{0:04x}\\u{1:04x}'.format(s1, s2) + return '\\u%04x\\u%04x' % (s1, s2) + return '"' + str(ESCAPE_ASCII.sub(replace, s)) + '"' + + +encode_basestring_ascii = ( + c_encode_basestring_ascii or py_encode_basestring_ascii) + +class JSONEncoder(object): + """Extensible JSON encoder for Python data structures. + + Supports the following objects and types by default: + + +-------------------+---------------+ + | Python | JSON | + +===================+===============+ + | dict | object | + +-------------------+---------------+ + | list, tuple | array | + +-------------------+---------------+ + | str, unicode | string | + +-------------------+---------------+ + | int, long, float | number | + +-------------------+---------------+ + | True | true | + +-------------------+---------------+ + | False | false | + +-------------------+---------------+ + | None | null | + +-------------------+---------------+ + + To extend this to recognize other objects, subclass and implement a + ``.default()`` method with another method that returns a serializable + object for ``o`` if possible, otherwise it should call the superclass + implementation (to raise ``TypeError``). + + """ + item_separator = ', ' + key_separator = ': ' + def __init__(self, skipkeys=False, ensure_ascii=True, + check_circular=True, allow_nan=True, sort_keys=False, + indent=None, separators=None, encoding='utf-8', default=None, + use_decimal=False): + """Constructor for JSONEncoder, with sensible defaults. + + If skipkeys is false, then it is a TypeError to attempt + encoding of keys that are not str, int, long, float or None. If + skipkeys is True, such items are simply skipped. + + If ensure_ascii is true, the output is guaranteed to be str + objects with all incoming unicode characters escaped. If + ensure_ascii is false, the output will be unicode object. + + If check_circular is true, then lists, dicts, and custom encoded + objects will be checked for circular references during encoding to + prevent an infinite recursion (which would cause an OverflowError). + Otherwise, no such check takes place. + + If allow_nan is true, then NaN, Infinity, and -Infinity will be + encoded as such. This behavior is not JSON specification compliant, + but is consistent with most JavaScript based encoders and decoders. + Otherwise, it will be a ValueError to encode such floats. + + If sort_keys is true, then the output of dictionaries will be + sorted by key; this is useful for regression tests to ensure + that JSON serializations can be compared on a day-to-day basis. + + If indent is a string, then JSON array elements and object members + will be pretty-printed with a newline followed by that string repeated + for each level of nesting. ``None`` (the default) selects the most compact + representation without any newlines. For backwards compatibility with + versions of simplejson earlier than 2.1.0, an integer is also accepted + and is converted to a string with that many spaces. + + If specified, separators should be a (item_separator, key_separator) + tuple. The default is (', ', ': '). To get the most compact JSON + representation you should specify (',', ':') to eliminate whitespace. + + If specified, default is a function that gets called for objects + that can't otherwise be serialized. It should return a JSON encodable + version of the object or raise a ``TypeError``. + + If encoding is not None, then all input strings will be + transformed into unicode using that encoding prior to JSON-encoding. + The default is UTF-8. + + If use_decimal is true (not the default), ``decimal.Decimal`` will + be supported directly by the encoder. For the inverse, decode JSON + with ``parse_float=decimal.Decimal``. + + """ + + self.skipkeys = skipkeys + self.ensure_ascii = ensure_ascii + self.check_circular = check_circular + self.allow_nan = allow_nan + self.sort_keys = sort_keys + self.use_decimal = use_decimal + if isinstance(indent, (int, long)): + indent = ' ' * indent + self.indent = indent + if separators is not None: + self.item_separator, self.key_separator = separators + elif indent is not None: + self.item_separator = ',' + if default is not None: + self.default = default + self.encoding = encoding + + def default(self, o): + """Implement this method in a subclass such that it returns + a serializable object for ``o``, or calls the base implementation + (to raise a ``TypeError``). + + For example, to support arbitrary iterators, you could + implement default like this:: + + def default(self, o): + try: + iterable = iter(o) + except TypeError: + pass + else: + return list(iterable) + return JSONEncoder.default(self, o) + + """ + raise TypeError(repr(o) + " is not JSON serializable") + + def encode(self, o): + """Return a JSON string representation of a Python data structure. + + >>> from simplejson import JSONEncoder + >>> JSONEncoder().encode({"foo": ["bar", "baz"]}) + '{"foo": ["bar", "baz"]}' + + """ + # This is for extremely simple cases and benchmarks. + if isinstance(o, basestring): + if isinstance(o, str): + _encoding = self.encoding + if (_encoding is not None + and not (_encoding == 'utf-8')): + o = o.decode(_encoding) + if self.ensure_ascii: + return encode_basestring_ascii(o) + else: + return encode_basestring(o) + # This doesn't pass the iterator directly to ''.join() because the + # exceptions aren't as detailed. The list call should be roughly + # equivalent to the PySequence_Fast that ''.join() would do. + chunks = self.iterencode(o, _one_shot=True) + if not isinstance(chunks, (list, tuple)): + chunks = list(chunks) + if self.ensure_ascii: + return ''.join(chunks) + else: + return u''.join(chunks) + + def iterencode(self, o, _one_shot=False): + """Encode the given object and yield each string + representation as available. + + For example:: + + for chunk in JSONEncoder().iterencode(bigobject): + mysocket.write(chunk) + + """ + if self.check_circular: + markers = {} + else: + markers = None + if self.ensure_ascii: + _encoder = encode_basestring_ascii + else: + _encoder = encode_basestring + if self.encoding != 'utf-8': + def _encoder(o, _orig_encoder=_encoder, _encoding=self.encoding): + if isinstance(o, str): + o = o.decode(_encoding) + return _orig_encoder(o) + + def floatstr(o, allow_nan=self.allow_nan, + _repr=FLOAT_REPR, _inf=PosInf, _neginf=-PosInf): + # Check for specials. Note that this type of test is processor + # and/or platform-specific, so do tests which don't depend on + # the internals. + + if o != o: + text = 'NaN' + elif o == _inf: + text = 'Infinity' + elif o == _neginf: + text = '-Infinity' + else: + return _repr(o) + + if not allow_nan: + raise ValueError( + "Out of range float values are not JSON compliant: " + + repr(o)) + + return text + + + key_memo = {} + if (_one_shot and c_make_encoder is not None + and self.indent is None): + _iterencode = c_make_encoder( + markers, self.default, _encoder, self.indent, + self.key_separator, self.item_separator, self.sort_keys, + self.skipkeys, self.allow_nan, key_memo, self.use_decimal) + else: + _iterencode = _make_iterencode( + markers, self.default, _encoder, self.indent, floatstr, + self.key_separator, self.item_separator, self.sort_keys, + self.skipkeys, _one_shot, self.use_decimal) + try: + return _iterencode(o, 0) + finally: + key_memo.clear() + + +class JSONEncoderForHTML(JSONEncoder): + """An encoder that produces JSON safe to embed in HTML. + + To embed JSON content in, say, a script tag on a web page, the + characters &, < and > should be escaped. They cannot be escaped + with the usual entities (e.g. &) because they are not expanded + within