aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AUTHORS4
-rw-r--r--README.md (renamed from README.rst)59
-rw-r--r--github3/api.py60
-rw-r--r--github3/converters.py35
-rw-r--r--github3/core.py10
-rw-r--r--github3/errors.py8
-rw-r--r--github3/exceptions.py7
-rw-r--r--github3/handlers/base.py76
-rw-r--r--github3/handlers/gists.py145
-rw-r--r--github3/handlers/users.py299
-rw-r--r--github3/helpers.py188
-rw-r--r--github3/models/__init__.py2
-rw-r--r--github3/models/base.py11
-rw-r--r--github3/models/gists.py29
-rw-r--r--github3/models/orgs.py2
-rw-r--r--github3/models/repos.py2
-rw-r--r--github3/models/user.py24
-rw-r--r--github3/packages/link_header.py3
-rw-r--r--github3/tests/api_test.py124
-rw-r--r--github3/tests/converters_test.py96
-rw-r--r--github3/tests/fixtures.py164
-rw-r--r--github3/tests/get_handlers_test.py31
-rw-r--r--github3/tests/gists_handler_test.py110
-rw-r--r--github3/tests/handler_test.py133
-rw-r--r--github3/tests/user_handler_test.py326
-rw-r--r--reqs.txt2
-rw-r--r--run_tests.sh1
27 files changed, 1637 insertions, 314 deletions
diff --git a/AUTHORS b/AUTHORS
index bcf74ac..29bea31 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -5,10 +5,12 @@ Development Lead
````````````````
- Kenneth Reitz <me@kennethreitz.com>
+- David Medina <davidmedina9@gmail.com>
Patches and Suggestions
```````````````````````
- Mahdi Yusuf
-- Rok Garbas \ No newline at end of file
+- Rok Garbas
+- Antti Kaihola <akaihol+github@ambitone.com>
diff --git a/README.rst b/README.md
index 214d30a..334b975 100644
--- a/README.rst
+++ b/README.md
@@ -1,50 +1,34 @@
Fork
-======================================
+====
Refactor and complete api wrapper. Intensive work in progress
-Github3: Python wrapper for the (new) GitHub API v3
-===================================================
-
-Github has a new API. This is the best Python wrapper for it.
-
-**This a work in progress.** Should be relased soon.
-
-
-
-Usage
------
-
-::
-
- import github3
-
- gh = github3.basic_auth('username', 'password')
-
- gh.get_repo('kennethreitz', 'python-github3')
+Use with auth user
+------------------
+ from github3.api import Github
+ gh = Github('user', 'password')
+ users_handler = gh.users
+ for repo in users_handler.get_repos():
+ print repo
+ gists_handler = gh.gists
+ gists_handler.create_gist(
+ u'Description',
+ files={'file1.txt': {'content': u'Content of first file'}})
Installation
------------
-To install Github3, simply: ::
-
- $ pip install github3
-
-Or, if you absolutely must: ::
-
- $ easy_install github3
-
-But, you really shouldn't do that.
-
+To install Github3, simply:
+ $ pip -e git+https://copitux@github.com/copitux/python-github3#egg=python-github3
License
-------
-ISC License. ::
+ISC License.
Copyright (c) 2011, Kenneth Reitz <me@kennethreitz.com>
@@ -64,19 +48,16 @@ ISC License. ::
Contribute
----------
-If you'd like to contribute, simply fork `the repository`_, commit your changes
+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_.
-
+sure you add yourself to `AUTHORS`.
Roadmap
-------
-- Get it Started
-- HTTP BASIC
-- Get it working
-- Sphinx Documetnation
-- Examples
- Unittests
+- Handlers
+- Sphinx Documentation
+- Examples
- OAuth Last (how?)
diff --git a/github3/api.py b/github3/api.py
index b7435ff..e1d1fbe 100644
--- a/github3/api.py
+++ b/github3/api.py
@@ -1,14 +1,15 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
-#
-# author: David Medina
import requests
import json
from errors import GithubError
+from handlers import users, gists
RESOURCES_PER_PAGE = 100
+
+#TODO: refactor: loads json in request editing Response object
class GithubCore(object):
"""
Wrapper to github api requests
@@ -69,7 +70,9 @@ class GithubCore(object):
def put(self, request, **kwargs):
""" PUT request """
- response = self._request('PUT', request, **kwargs)
+
+ response = self._request('PUT', request,
+ headers={'Content-length': '0'}, **kwargs)
assert response.status_code == 204
return response
@@ -87,22 +90,21 @@ class GithubCore(object):
"""
Arg's parser to `_request` method
- It check keyword args to parse extra request args to params
- Sample:
- _parse_args(arg1=1, arg2=2) => params = {'arg1': 1, 'arg2': 2}
+ Put extra request_args in params
"""
request_core = (
- 'params','data','headers','cookies','files','auth','tiemout',
- 'allow_redirects','proxies','return_response','config')
+ 'params', 'data', 'headers', 'cookies', 'files', 'auth', 'tiemout',
+ 'allow_redirects', 'proxies', 'return_response', 'config')
request_params = request_args.get('params')
extra_params = {}
for k, v in request_args.items():
- if k in request_core: continue
+ if k in request_core:
+ continue
extra_params.update({k: v})
del request_args[k]
- if request_params:
+ if request_params and getattr(request_params, 'update'):
request_args['params'].update(extra_params)
- else:
+ elif extra_params:
request_args['params'] = extra_params
return request_args
@@ -116,14 +118,42 @@ class GithubCore(object):
:param kwargs: Keyword args to request
"""
request = self.base_url + request
- parsed_args = self._parse_args(kwargs)
- response = self.session.request(verb, request, **parsed_args)
+ self._parse_args(kwargs)
+ response = self.session.request(verb, request, **kwargs)
self.requests_remaining = response.headers.get(
- 'x-ratelimit-remaining',-1)
+ 'x-ratelimit-remaining', -1)
error = GithubError(response)
error.process()
return response
+ return response
+
class Github(GithubCore):
- pass
+ """ Library enter """
+
+ def __init__(self, *args):
+ super(Github, self).__init__()
+ self.authenticated = False
+ auth = len(args)
+ if auth == 2: # Basic auth
+ self.session.auth = tuple(map(str, args))
+ self.authenticated = True
+ elif auth == 1: # Token oauth
+ raise NotImplementedError
+ elif auth > 2:
+ raise TypeError("user, password or token")
+
+ @property
+ def users(self):
+ if self.authenticated:
+ return users.AuthUser(self)
+ else:
+ return users.User(self)
+
+ @property
+ def gists(self):
+ if self.authenticated:
+ return gists.AuthGist(self)
+ else:
+ return gists.Gist(self)
diff --git a/github3/converters.py b/github3/converters.py
index 1df61a6..9f975be 100644
--- a/github3/converters.py
+++ b/github3/converters.py
@@ -1,9 +1,9 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
-#
-# author: David Medina
+
from .core import Converter
+
class Rawlizer(Converter):
""" Raw converter """
@@ -16,6 +16,7 @@ class Rawlizer(Converter):
def dumps(self):
pass
+
class Json(Converter):
""" Json converter """
@@ -32,6 +33,7 @@ class Json(Converter):
def dumps(self):
pass
+
class Modelizer(Converter):
""" Own model converter """
@@ -52,17 +54,18 @@ class Modelizer(Converter):
self.model = model
def _parse_map(self, model, raw_resource):
- return Modelizer(model).loads(raw_resource)
+ if hasattr(raw_resource, 'items'):
+ return Modelizer(model).loads(raw_resource)
def _parse_collection_map(self, model, raw_resources):
# Dict of resources (Ex: Gist file)
- if getattr(raw_resources, 'items', False):
+ if hasattr(raw_resources, 'items'):
dict_map = {}
for key, raw_resource in raw_resources.items():
dict_map[key] = Modelizer(model).loads(raw_resource)
return dict_map
# list of resources
- else:
+ elif hasattr(raw_resources, '__iter__'):
return [Modelizer(model).loads(raw_resource)
for raw_resource in raw_resources]
@@ -73,25 +76,25 @@ class Modelizer(Converter):
self.__class__.__name__)
idl = self.model.idl()
attrs.update(
- {attr: raw_resource[attr] for attr in idl.get('strs',())
- if raw_resource.get(attr)})
+ {attr: raw_resource[attr] for attr in idl.get('strs', ())
+ if attr in raw_resource})
attrs.update(
- {attr: raw_resource[attr] for attr in idl.get('ints',())
- if raw_resource.get(attr)})
+ {attr: raw_resource[attr] for attr in idl.get('ints', ())
+ if attr in raw_resource})
attrs.update(
{attr: self._parse_date(raw_resource[attr])
- for attr in idl.get('dates',()) if raw_resource.get(attr)})
+ for attr in idl.get('dates', ()) if attr in raw_resource})
attrs.update(
- {attr: raw_resource[attr] for attr in idl.get('bools',())
- if raw_resource.get(attr)})
+ {attr: raw_resource[attr] for attr in idl.get('bools', ())
+ if attr in raw_resource})
attrs.update(
{attr: self._parse_map(model, raw_resource[attr])
- for attr, model in idl.get('maps',{}).items()
- if raw_resource.get(attr)})
+ for attr, model in idl.get('maps', {}).items()
+ if attr in raw_resource})
attrs.update(
{attr: self._parse_collection_map(model, raw_resource[attr])
- for attr, model in idl.get('collection_maps',{}).items()
- if raw_resource.get(attr)})
+ for attr, model in idl.get('collection_maps', {}).items()
+ if attr in raw_resource})
return self.model(attrs)
diff --git a/github3/core.py b/github3/core.py
index d7237af..ab71943 100644
--- a/github3/core.py
+++ b/github3/core.py
@@ -1,7 +1,6 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
-#
-# author: David Medina
+
class Paginate:
""" Paginate resource iterator
@@ -29,12 +28,14 @@ class Paginate:
return self.last
- # TODO: reset iterators... multiple?
def __iter__(self):
return self
def initial(self):
- """ First request. Force requester to paginate returning link header """
+ """
+ First request
+ Force requester to paginate returning link header
+ """
link, content = self.requester(self.resource, paginate=True,
page=1, **self.kwargs)
self.last = self._last_page(link) if link else 1
@@ -54,6 +55,7 @@ class Paginate:
self.page += 1
return content
+
class Converter(object):
""" Abstract converter class """
diff --git a/github3/errors.py b/github3/errors.py
index 09e616b..e96e2da 100644
--- a/github3/errors.py
+++ b/github3/errors.py
@@ -1,11 +1,10 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
-#
-# author: David Medina
import json
import github3.exceptions as exceptions
+
class GithubError(object):
""" Handler for API errors """
@@ -14,12 +13,15 @@ class GithubError(object):
self.status_code = response.status_code
try:
self.debug = self._parser.loads(response.content)
- except ValueError:
+ except (ValueError, TypeError):
self.debug = {'message': response.content}
def error_400(self):
return exceptions.BadRequest("400 - %s" % self.debug.get('message'))
+ def error_401(self):
+ return exceptions.Unauthorized("401 - %s" % self.debug.get('message'))
+
def error_404(self):
return exceptions.NotFound("404 - %s" % self.debug.get('message'))
diff --git a/github3/exceptions.py b/github3/exceptions.py
index b0894a9..b9070d7 100644
--- a/github3/exceptions.py
+++ b/github3/exceptions.py
@@ -1,7 +1,5 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
-#
-# author: David Medina
class BadRequest(Exception):
pass
@@ -9,6 +7,7 @@ class UnprocessableEntity(Exception):
pass
class NotFound(Exception):
pass
-class AnomUser(Exception):
- """ Exception for AnomUser handler """
+class Unauthorized(Exception):
+ pass
+class UserIsAnonymous(Exception):
pass
diff --git a/github3/handlers/base.py b/github3/handlers/base.py
index 50e2df8..565978f 100644
--- a/github3/handlers/base.py
+++ b/github3/handlers/base.py
@@ -1,11 +1,43 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
-#
-# author: David Medina
from github3.core import Paginate
from github3.converters import Modelizer
+
+class MimeTypeMixin(object):
+
+ VERSION = 'beta'
+
+ def __init__(self):
+ self.mimetypes = set()
+
+ def _parse_mime_type(self, type):
+ return 'application/vnd.github.%s.%s+json' % (
+ self.VERSION, type)
+
+ def add_raw(self):
+ self.mimetypes.add(self._parse_mime_type('raw'))
+ return self
+
+ def add_text(self):
+ self.mimetypes.add(self._parse_mime_type('text'))
+ return self
+
+ def add_html(self):
+ self.mimetypes.add(self._parse_mime_type('html'))
+ return self
+
+ def add_full(self):
+ self.mimetypes.add(self._parse_mime_type('full'))
+ return self
+
+ def mime_header(self):
+ if self.mimetypes:
+ return {'Accept': ', '.join(self.mimetypes)}
+ return None
+
+
class Handler(object):
""" Handler base. Requests to API and modelize responses """
@@ -13,17 +45,23 @@ class Handler(object):
self._gh = gh
super(Handler, self).__init__()
+ def _inject_handler(self, handler, prefix=''):
+ import inspect
+ for method, callback in inspect.getmembers(handler):
+ if method.startswith(prefix) and inspect.ismethod(callback):
+ setattr(self, method, callback)
+
def _prefix_resource(self, resource):
prefix = getattr(self, 'prefix', '')
- return '/'.join((prefix, resource)).rstrip('/')
+ return '/'.join((prefix, str(resource))).strip('/')
- def _get_converter(self, **kwargs):
- converter = kwargs.get(
- 'converter', # 1. in kwargs
- getattr(self, 'converter', # 2. in handler
- Modelizer())) # 3. Default
+ def _get_converter(self, kwargs={}):
+ converter = kwargs.pop(
+ 'converter', # 1. in kwargs
+ getattr(self, 'converter', # 2. in handler
+ Modelizer)) # 3. Default
- return converter
+ return converter()
def _put(self, resource, **kwargs):
""" Put proxy request"""
@@ -41,27 +79,29 @@ class Handler(object):
from github3.exceptions import NotFound
resource = self._prefix_resource(resource)
try:
- callback = getattr(self._gh, kwargs.get('method',''), self._gh.head)
+ callback = getattr(self._gh, kwargs.get('method', ''),
+ self._gh.head)
response = callback(resource, **kwargs)
except NotFound:
return False
assert response.status_code == 204
return True
- #TODO: if limit is multiple of per_page... it do another request for nothing
def _get_resources(self, resource, model=None, limit=None, **kwargs):
""" Hander request to multiple resources """
+ if limit:
+ limit = abs(limit)
resource = self._prefix_resource(resource)
- page_resources = Paginate(resource, self._gh.get, **kwargs)
+ converter = self._get_converter(kwargs)
counter = 1
- for page in page_resources:
+ for page in Paginate(resource, self._gh.get, **kwargs):
for raw_resource in page:
- if limit and counter > limit: break
counter += 1
- converter = self._get_converter(**kwargs)
converter.inject(model)
yield converter.loads(raw_resource)
+ if limit and counter > limit:
+ break
else:
continue
break
@@ -70,8 +110,8 @@ class Handler(object):
""" Handler request to single resource """
resource = self._prefix_resource(resource)
- raw_resource = self._gh.get(resource)
- converter = self._get_converter(**kwargs)
+ converter = self._get_converter(kwargs)
+ raw_resource = self._gh.get(resource, **kwargs)
converter.inject(model)
return converter.loads(raw_resource)
@@ -80,6 +120,6 @@ class Handler(object):
resource = self._prefix_resource(resource)
raw_resource = self._gh.post(resource, data=data)
- converter = self._get_converter(**kwargs)
+ converter = self._get_converter(kwargs)
converter.inject(model)
return converter.loads(raw_resource)
diff --git a/github3/handlers/gists.py b/github3/handlers/gists.py
new file mode 100644
index 0000000..ed03c31
--- /dev/null
+++ b/github3/handlers/gists.py
@@ -0,0 +1,145 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+
+from .base import Handler, MimeTypeMixin
+from github3 import models
+
+
+class Gist(Handler, MimeTypeMixin):
+ """ Gist handler with public access """
+
+ prefix = 'gists'
+
+ def __repr__(self):
+ return '<Gist handler>'
+
+ def all_gists(self, limit=None):
+ """ Return all public gists
+
+ NOTE: It returns all gists in github environment. Maybe you
+ want to use `limit` parameter
+ """
+
+ return self._get_resources('', model=models.Gist, limit=limit)
+
+ def get(self, gist_id):
+ """ Return gist
+
+ param `gist_id`: Gist id
+ """
+
+ return self._get_resource(gist_id, model=models.Gist)
+
+ def get_comments(self, gist_id, limit=None):
+ """ Return gist's comments
+
+ param `gist_id`: Gist id
+ param `limit`: Number of comments
+ """
+
+ return self._get_resources('%s/comments' % gist_id,
+ model=models.GistComment, limit=limit,
+ headers=self.mime_header())
+
+ def get_comment(self, comment_id):
+ """ Return gist's comment
+
+ param `comment_id`: Comment id
+ """
+
+ return self._get_resource('comments/%s' % comment_id,
+ model=models.GistComment, headers=self.mime_header())
+
+
+class AuthGist(Gist):
+
+ def all_gists(self, limit=None):
+ """ Return all public gists
+
+ NOTE: It returns all gists in github environment. Maybe you
+ want to use `limit` parameter
+ """
+
+ return self._get_resources('public', model=models.Gist, limit=limit)
+
+ def my_gists(self, limit=None):
+ """ Return authenticated user's gists
+
+ param `limit`: Number of gists
+ """
+
+ return self._get_resources('', model=models.Gist, limit=limit)
+
+ def my_starred_gists(self, limit=None):
+ """ Return authenticated user's starred gists
+
+ param `limit`: Number of gists
+ """
+
+ return self._get_resources('starred', model=models.Gist, limit=limit)
+
+ def create_gist(self, is_public, files, desc=None):
+ """ Create and return a gist """
+
+ data = {
+ 'public': bool(is_public),
+ 'files': files, # TODO: Issue #1
+ 'desc': desc or '',
+ }
+ return self._post_resource('', data=data, model=models.Gist)
+
+ def star_gist(self, gist_id):
+ """ Star a gist
+
+ param `gist_id`: Gist id to star
+ """
+
+ return self._put('%s/star' % gist_id)
+
+ def unstar_gist(self, gist_id):
+ """ Unstar a gist
+
+ param `gist_id`: Gist id to unstar
+ """
+
+ return self._delete('%s/star' % gist_id)
+
+ def is_starred(self, gist_id):
+ """ True if gist is starred
+
+ param `gist_id`: Gist id
+ """
+
+ return self._bool('%s/star' % gist_id)
+
+ def fork_gist(self, gist_id):
+ """ Return forked gist from id
+
+ param `gist_id`: Gist id to be forked...
+ """
+
+ return self._post_resource('%s/fork' % gist_id, data=None,
+ model=models.Gist)
+
+ def delete_gist(self, gist_id):
+ """ Delete the gist
+
+ param `gist_id`: Gist id
+ """
+
+ return self._delete(str(gist_id))
+
+ def create_comment(self, gist_id, comment):
+ """ Create comment into gist """
+
+ data = {'body': comment}
+ return self._post_resource('%s/comments' % gist_id, data=data,
+ model=models.GistComment)
+
+ def delete_comment(self, comment_id):
+ """ Delete comment
+
+ param `comment_id`: Comment id
+ """
+
+ return self._delete('comments/%s' % comment_id)
diff --git a/github3/handlers/users.py b/github3/handlers/users.py
new file mode 100644
index 0000000..2f12184
--- /dev/null
+++ b/github3/handlers/users.py
@@ -0,0 +1,299 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+
+from .base import Handler
+from github3 import models
+from github3.converters import Rawlizer
+from github3.exceptions import UserIsAnonymous
+
+
+class User(Handler):
+ """ User handler with public access """
+
+ prefix = 'users'
+
+ def __repr__(self):
+ return '<User handler> %s>' % getattr(self, 'username', 'without user')
+
+ def _parse_user(self, user):
+ """ Parse user, and if it fails then try with username in handler
+
+ :param user: It can be a `models.User` or alphanumeric user string
+
+ """
+ username = getattr(user, 'login', user)
+ if not username or not str(username).isalpha():
+ username = getattr(self, 'username', False)
+ if not username:
+ raise UserIsAnonymous('%s user is not valid' % username)
+ return str(username)
+
+ def set_username(self, user):
+ """ Set username to query public handler
+ Helper to less writing
+
+ :param user: It can be a `models.User` or alphanumeric user string
+
+ """
+ self.username = self._parse_user(user)
+ return self
+
+ def get(self, user=None):
+ """ Return user
+
+ :param `user`: User model or username string
+
+ """
+ user = self._parse_user(user)
+ return self._get_resource(user, model=models.User)
+
+ def get_followers(self, user=None, limit=None):
+ """ Return user's followers
+
+ :param `user`: User model or username string
+
+ """
+ user = self._parse_user(user)
+ return self._get_resources('%s/followers' % user, model=models.User,
+ limit=limit)
+
+ def get_following(self, user=None, limit=None):
+ """ Return users that follow
+
+ :param `user`: User model or username string
+
+ """
+ user = self._parse_user(user)
+ return self._get_resources('%s/following' % user, model=models.User,
+ limit=limit)
+
+ def get_repos(self, user=None, limit=None):
+ """ Return user's public repositories
+
+ :param `user`: User model or username string
+
+ """
+ user = self._parse_user(user)
+ return self._get_resources('%s/repos' % user, model=models.Repo,
+ limit=limit)
+
+ def get_watched(self, user=None, limit=None):
+ """ Return repositories that user watch
+
+ :param `user`: User model or username string
+
+ """
+ user = self._parse_user(user)
+ return self._get_resources('%s/watched' % user, model=models.Repo,
+ limit=limit)
+
+ def get_orgs(self, user=None, limit=None):
+ """ Return user's public organizations
+
+ :param `user`: User model or username string
+
+ """
+ user = self._parse_user(user)
+ return self._get_resources('%s/orgs' % user, model=models.Org,
+ limit=limit)
+
+ def get_gists(self, user=None, limit=None):
+ """ Return user's gists
+
+ :param `user`: User model or username string
+
+ """
+ user = self._parse_user(user)
+ return self._get_resources('%s/gists' % user, model=models.Gist,
+ limit=limit)
+
+
+class AuthUser(Handler):
+ """ User handler with public and private access """
+
+ prefix = 'user'
+
+ def __init__(self, gh):
+ super(AuthUser, self).__init__(gh)
+ self._inject_handler(User(gh), prefix='get')
+
+ def __repr__(self):
+ return '<AuthUser handler> %s>' % self._gh.session.auth[0]
+
+ def me(self):
+ """ Return authenticated user """
+
+ return self._get_resource('', model=models.AuthUser)
+
+ def my_followers(self, limit=None):
+ """ Return authenticated user followers """
+
+ return self._get_resources('followers', model=models.User,
+ limit=limit)
+
+ def my_following(self, limit=None):
+ """ Return authenticated user following """
+
+ return self._get_resources('following', model=models.User,
+ limit=limit)
+
+ def get_emails(self):
+ """ Return list of emails """
+
+ # Ignore converter, it must be Rawlizer
+ emails = self._get_resource('emails', converter=Rawlizer)
+ return emails
+
+ def create_emails(self, *args):
+ """
+ Add emails
+
+ :param args: Collection of emails
+ create_emails(*('test1@example.com', 'test2@example.cm'))
+ """
+ parsed_emails = map(str, args)
+ all_mails = self._post_resource(
+ 'emails', data=parsed_emails, converter=Rawlizer)
+ return all_mails
+
+ def delete_emails(self, *args):
+ """
+ Delete emails
+
+ :param args: Collection of emails
+ create_emails(*('test1@example.com', 'test2@example.cm'))
+ """
+ parsed_emails = map(str, args)
+ return self._delete('emails', data=parsed_emails)
+
+ def is_following(self, user):
+ """
+ Return true if you are following the user
+
+ :param `user`: User model or username string
+ """
+
+ parse_user = getattr(user, 'login', user)
+ return self._bool('following/%s' % parse_user)
+
+ def follow(self, user):
+ """
+ Follow user
+
+ :param `user`: User model or username string
+
+ """
+
+ parse_user = getattr(user, 'login', user)
+ return self._put('following/%s' % parse_user)
+
+ def unfollow(self, user):
+ """
+ Unfollow user
+
+ :param `user`: User model or username string
+ """
+
+ parse_user = getattr(user, 'login', user)
+ return self._delete('following/%s' % parse_user)
+
+ def get_keys(self, limit=None):
+ """ Get public keys """
+
+ return self._get_resources('keys', model=models.Key,
+ limit=limit)
+
+ def get_key(self, key):
+ """ Get public key
+
+ :param `key`: Key model or key id
+
+ """
+
+ parse_key_id = getattr(key, 'id', key)
+ return self._get_resource('keys/%s' % parse_key_id, model=models.Key)
+
+ def create_key(self, **kwargs):
+ """
+ Create public key
+
+ :param title
+ :param key: Key string (It must starts with 'ssh-rsa')
+ """
+
+ #TODO: render key.pub file
+ key = {
+ 'title': kwargs.get('title', ''),
+ 'key': kwargs.get('key', '')
+ }
+ return self._post_resource('keys', data=key, model=models.Key)
+
+ def delete_key(self, key):
+ """ Delete public key
+
+ :param `key`: Key model or key id
+
+ """
+
+ parse_key_id = getattr(key, 'id', key)
+ return self._delete('keys/%s' % parse_key_id)
+
+ def my_repos(self, filter='all', limit=None):
+ """
+ Return user's public repositories
+
+ param: filter: 'all', 'owner', 'public', 'private' or 'member'
+ """
+
+ return self._get_resources('repos', model=models.Repo,
+ limit=limit, type=str(filter))
+
+ def my_watched(self, limit=None):
+ """ Return authenticated user repos that he watch """
+
+ return self._get_resources('watched', model=models.Repo,
+ limit=limit)
+
+ def is_watching_repo(self, owner, repo):
+ """
+ Return true if you are watching the user repository
+
+ :param owner: Model user or username string
+ :param repo: Model repo or repo name string
+ is_watching_repo('copitux', 'python-github3')
+ """
+
+ owner = getattr(owner, 'login', owner)
+ repo = getattr(repo, 'name', repo)
+ return self._bool('watched/%s/%s' % (owner, repo))
+
+ def watch_repo(self, owner, repo):
+ """
+ Watch the repository
+
+ :param owner: Model user or username string
+ :param repo: Model repo or repo name string
+ """
+
+ owner = getattr(owner, 'login', owner)
+ repo = getattr(repo, 'name', repo)
+ return self._put('watched/%s/%s' % (owner, repo))
+
+ def unwatch_repo(self, owner, repo):
+ """
+ Unwatch the repository
+
+ :param owner: Model user or username string
+ :param repo: Model repo or repo name string
+ """
+
+ owner = getattr(owner, 'login', owner)
+ repo = getattr(repo, 'name', repo)
+ return self._delete('watched/%s/%s' % (owner, repo))
+
+ def my_orgs(self, limit=None):
+ """ List public and private organizations
+ for the authenticated user
+ """
+
+ return self._get_resources('orgs', model=models.Org, limit=limit)
diff --git a/github3/helpers.py b/github3/helpers.py
deleted file mode 100644
index 205e097..0000000
--- a/github3/helpers.py
+++ /dev/null
@@ -1,188 +0,0 @@
-# -*- coding: utf-8 -*-
-
-"""
-github3.helpers
-~~~~~~~~~~~~~~~~
-
-This module provides various helper functions to the rest of the package.
-"""
-
-import inspect
-from datetime import datetime
-
-from dateutil.parser import parse as parse_datetime
-
-
-def is_collection(obj):
- """Tests if an object is a collection."""
-
- col = getattr(obj, '__getitem__', False)
- val = False if (not col) else True
-
- if isinstance(obj, basestring):
- val = False
-
- return val
-
-
-def key_diff(source, update, pack=False):
- """Given two dictionaries, returns a list of the changed keys."""
-
- source = dict(source)
- update = dict(update)
-
- changed = []
-
- for (k, v) in source.items():
- u_v = update.get(k)
-
- if (v != u_v) and (u_v is not None):
- changed.append(k)
-
- if pack is False:
- return changed
-
- d = dict()
-
- for k in changed:
- d[k] = update[k]
-
- return d
-
-
-# from arc90/python-readability-api
-def to_python(obj,
- in_dict,
- str_keys=None,
- date_keys=None,
- int_keys=None,
- object_map=None,
- list_map=None,
- bool_keys=None, **kwargs):
- """Extends a given object for API Consumption.
-
- :param obj: Object to extend.
- :param in_dict: Dict to extract data from.
- :param string_keys: List of in_dict keys that will be extracted as strings.
- :param date_keys: List of in_dict keys that will be extrad as datetimes.
- :param object_map: Dict of {key, obj} map, for nested object results.
- """
-
- d = dict()
-
- if str_keys:
- for in_key in str_keys:
- d[in_key] = in_dict.get(in_key)
-
- if date_keys:
- for in_key in date_keys:
- in_date = in_dict.get(in_key)
- try:
- out_date = datetime.strptime(in_date, '%Y-%m-%dT%H:%M:%SZ')
- except TypeError:
- out_date = None
-
- d[in_key] = out_date
-
- if int_keys:
- for in_key in int_keys:
- if (in_dict is not None) and (in_dict.get(in_key) is not None):
- d[in_key] = int(in_dict.get(in_key))
-
- if bool_keys:
- for in_key in bool_keys:
- if in_dict.get(in_key) is not None:
- d[in_key] = bool(in_dict.get(in_key))
-
- if object_map:
- for (k, v) in object_map.items():
- if in_dict.get(k):
- if v == 'self':
- v = obj.__class__
- d[k] = v.new_from_dict(in_dict.get(k))
-
- if list_map:
- for k, model in list_map.items():
- nested_map = in_dict.get(k)
- if nested_map:
- if getattr(nested_map, 'items', False):
- map_dict = {}
- for nested_item, nested_dict in nested_map.items():
- map_dict[nested_item] = model.new_from_dict(nested_dict)
- d[k] = map_dict
- else:
- map_list = []
- for item_map in nested_map:
- map_list.append(model.new_from_dict(item_map))
- d[k] = map_list
-
- obj.__dict__.update(d)
- obj.__dict__.update(kwargs)
-
- # Save the dictionary, for write comparisons.
- obj._cache = d
- obj.__cache = in_dict
- obj.post_map()
-
- return obj
-
-
-# from arc90/python-readability-api
-def to_api(in_dict, int_keys=None, date_keys=None, bool_keys=None):
- """Extends a given object for API Production."""
-
- # Cast all int_keys to int()
- if int_keys:
- for in_key in int_keys:
- if (in_key in in_dict) and (in_dict.get(in_key, None) is not None):
- in_dict[in_key] = int(in_dict[in_key])
-
- # Cast all date_keys to datetime.isoformat
- if date_keys:
- for in_key in date_keys:
- if (in_key in in_dict) and (in_dict.get(in_key, None) is not None):
-
- _from = in_dict[in_key]
-
- if isinstance(_from, basestring):
- dtime = parse_datetime(_from)
-
- elif isinstance(_from, datetime):
- dtime = _from
-
- in_dict[in_key] = dtime.isoformat()
-
- elif (in_key in in_dict) and in_dict.get(in_key, None) is None:
- del in_dict[in_key]
-
- # Remove all Nones
- for k, v in in_dict.items():
- if v is None:
- del in_dict[k]
-
- return in_dict
-
-
-
-# from kennethreitz/showme
-def get_scope(f, args=None):
- """Get scope of given function for Exception scopes."""
-
- if args is None:
- args=list()
-
- scope = inspect.getmodule(f).__name__
- # guess that function is a method of it's class
- try:
- if f.func_name in dir(args[0].__class__):
- scope += '.' + args[0].__class__.__name__
- scope += '.' + f.__name__
- else:
- scope += '.' + f.__name__
- except IndexError:
- scope += '.' + f.__name__
-
- # scrub readability.models namespace
- scope = scope.replace('readability.api.', '')
-
- return scope
diff --git a/github3/models/__init__.py b/github3/models/__init__.py
index ff0c28a..0471393 100644
--- a/github3/models/__init__.py
+++ b/github3/models/__init__.py
@@ -1,4 +1,4 @@
from .user import AuthUser, User, Key
from .repos import Repo
from .orgs import Org
-from .gists import Gist
+from .gists import Gist, GistComment
diff --git a/github3/models/base.py b/github3/models/base.py
index df0c82b..5295d07 100644
--- a/github3/models/base.py
+++ b/github3/models/base.py
@@ -1,9 +1,5 @@
-"""
-github3.models
-~~~~~~~~~~~~~~
-
-This package provides the Github3 object model.
-"""
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
class BaseResource(object):
"""A BaseResource object."""
@@ -14,6 +10,9 @@ class BaseResource(object):
setattr(self, attr, value)
super(BaseResource, self).__init__()
+ def __len__(self):
+ return len(self.__dict__)
+
@classmethod
def idl(self):
raise NotImplementedError('Each model need subcass that method')
diff --git a/github3/models/gists.py b/github3/models/gists.py
index d1b416d..8979dbb 100644
--- a/github3/models/gists.py
+++ b/github3/models/gists.py
@@ -1,11 +1,26 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
-#
-# author: David Medina
from .base import BaseResource
from .user import User
+
+class GistComment(BaseResource):
+ """ Gist comment """
+
+ @classmethod
+ def idl(self):
+ return {
+ 'strs': ['url', 'body', 'body_text', 'body_html'],
+ 'ints': ['id'],
+ 'maps': {'user': User},
+ 'dates': ['created_at'],
+ }
+
+ def __repr__(self):
+ return '<GistComment %s>' % self.user.login
+
+
class File(BaseResource):
""" File model """
@@ -19,6 +34,7 @@ class File(BaseResource):
def __repr__(self):
return '<File gist> %s' % self.filename
+
class GistFork(BaseResource):
""" GistFork model """
@@ -33,6 +49,7 @@ class GistFork(BaseResource):
def __repr__(self):
return '<Gist fork> %s>' % self.user.login
+
class ChangeStatus(BaseResource):
""" ChangeStatus model """
@@ -45,6 +62,7 @@ class ChangeStatus(BaseResource):
def __repr__(self):
return '<Gist history> change_status>'
+
class GistHistory(BaseResource):
""" """
@@ -59,18 +77,21 @@ class GistHistory(BaseResource):
def __repr__(self):
return '<GistHistory %s/%s>' % (self.user, self.committed_at)
+
class Gist(BaseResource):
""" """
@classmethod
def idl(self):
return {
- 'strs': ['url', 'description', 'html_url', 'git_pull_url', 'git_push_url'],
+ 'strs': ['url', 'description', 'html_url', 'git_pull_url',
+ 'git_push_url'],
'ints': ['id', 'comments'],
'bools': ['public'],
'dates': ['created_at'],
'maps': {'user': User},
- 'collection_maps': {'files': File, 'forks': GistFork, 'history': GistHistory},
+ 'collection_maps': {'files': File, 'forks': GistFork,
+ 'history': GistHistory},
}
def __repr__(self):
diff --git a/github3/models/orgs.py b/github3/models/orgs.py
index 5e66c35..b2dacbd 100644
--- a/github3/models/orgs.py
+++ b/github3/models/orgs.py
@@ -1,7 +1,5 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
-#
-# author: David Medina
from .base import BaseResource
from .user import Plan
diff --git a/github3/models/repos.py b/github3/models/repos.py
index d1b7b75..882fb37 100644
--- a/github3/models/repos.py
+++ b/github3/models/repos.py
@@ -1,7 +1,5 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
-#
-# author: David Medina
from .base import BaseResource
from .user import User
diff --git a/github3/models/user.py b/github3/models/user.py
index 7ec7999..aed6f09 100644
--- a/github3/models/user.py
+++ b/github3/models/user.py
@@ -1,10 +1,9 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
-#
-# author: David Medina
from .base import BaseResource
+
class Plan(BaseResource):
"""Github Plan object model."""
@@ -18,6 +17,7 @@ class Plan(BaseResource):
def __repr__(self):
return '<Plan %s>' % self.name
+
class Key(BaseResource):
"""Github Key object model."""
@@ -31,35 +31,39 @@ class Key(BaseResource):
def __repr__(self):
return '<Key %s>' % self.title
+
class User(BaseResource):
"""Github User object model."""
@classmethod
def idl(self):
return {
- 'strs': ['login','avatar_url', 'url', 'name', 'company', 'blog',
- 'location', 'email', 'bio', 'html_url', 'type'],
+ 'strs': [
+ 'login', 'avatar_url', 'gravatar_id', 'url', 'name',
+ 'company', 'blog', 'location', 'email', 'bio', 'html_url',
+ 'type'],
'ints': [
'id', 'public_repos', 'public_gists', 'followers', 'following',
'total_private_repos', 'owned_private_repos', 'private_gists',
'disk_usage', 'collaborators'],
'maps': {'plan': Plan},
- 'dates': ['created_at',],
+ 'dates': ['created_at', ],
'bools': ['hireable', ],
}
def __repr__(self):
- return '<User %s>' % self.login
+ return '<User %s>' % getattr(self, 'login', 'without user')
#def handler(self):
- # return self._gh.user_handler(self.login, force=True)
+ # return self._gh.users
+
class AuthUser(User):
"""Github Authenticated User object model."""
- #def handler(self):
- # return self._gh.user_handler(self.login, force=True, private=True)
-
def __repr__(self):
return '<AuthUser %s>' % self.login
+ #def handler(self):
+ # return self._gh.users
+
diff --git a/github3/packages/link_header.py b/github3/packages/link_header.py
index 3959604..5ad20f1 100644
--- a/github3/packages/link_header.py
+++ b/github3/packages/link_header.py
@@ -1,4 +1,5 @@
#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
"""
HTTP Link Header Parsing
@@ -86,4 +87,4 @@ def parse_link_value(instr):
if __name__ == '__main__':
import sys
if len(sys.argv) > 1:
- print parse_link_value(sys.argv[1]) \ No newline at end of file
+ print parse_link_value(sys.argv[1])
diff --git a/github3/tests/api_test.py b/github3/tests/api_test.py
new file mode 100644
index 0000000..3ae75ee
--- /dev/null
+++ b/github3/tests/api_test.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+
+from mock import Mock, patch
+from unittest import TestCase
+from github3 import api
+from github3.exceptions import *
+import json
+import requests
+
+
+@patch.object(requests.sessions.Session, 'request')
+class TestGithubCore(TestCase):
+
+ def setUp(self):
+ self.gh = api.GithubCore()
+ self.assertEquals(self.gh.base_url, 'https://api.github.com/')
+ self.assertEquals(self.gh._parser, json)
+ self.base_url = self.gh.base_url
+ self.parser = self.gh._parser
+
+ def test_parse_args(self, request_method):
+ args = {
+ 'data': {'some': 'data'},
+ 'params': {'arg0': 'some'},
+ 'headers': 'out',
+ 'auth': 'out',
+ 'arg1': 'some',
+ 'arg2': 'some',
+ 'arg3': {'some': 'data', 'are': {'nested': 'true'}},
+ }
+ self.gh._parse_args(args)
+ self.assertEquals(args, {
+ 'data': {'some': 'data'},
+ 'params': {'arg0': 'some', 'arg1': 'some', 'arg2': 'some',
+ 'arg3': {'some': 'data', 'are': {'nested': 'true'}}},
+ 'headers': 'out',
+ 'auth': 'out',
+ })
+
+ def test_raise_errors(self, request_method):
+ real_request = (self.gh._request, 'GET', 'test')
+ request_method.return_value.status_code = 404
+ self.assertRaises(NotFound, *real_request)
+
+ request_method.return_value.status_code = 400
+ self.assertRaises(BadRequest, *real_request)
+
+ request_method.return_value.status_code = 422
+ self.assertRaises(UnprocessableEntity, *real_request)
+
+ request_method.return_value.status_code = 401
+ self.assertRaises(Unauthorized, *real_request)
+
+ def test_get(self, request_method):
+ response = request_method.return_value
+ response.content = self.parser.dumps({'test': 'test'})
+ content = self.gh.get('core')
+ request_method.assert_called_with('GET', self.base_url + 'core')
+ self.assertEquals(content, {'test': 'test'})
+
+ response = request_method.return_value
+ response.headers = {'link': 'url_with_links'}
+ response.content = self.parser.dumps({'test': 'test'})
+ header, content = self.gh.get('core', paginate=True)
+ request_method.assert_called_with('GET', self.base_url + 'core')
+ self.assertEquals(header, 'url_with_links')
+ self.assertEquals(content, {'test': 'test'})
+
+ def test_head(self, request_method):
+ pass # It has no sense using mocks
+
+ def test_post_and_patch(self, request_method):
+ data = {'login': 'test', 'bio': 'test'}
+ response = request_method.return_value
+ response.status_code = 201
+ response.content = self.parser.dumps({'post': 'done'})
+
+ content = self.gh.post('core', data=data)
+ request_method.assert_called_with(
+ 'POST', self.base_url + 'core',
+ data=self.parser.dumps(data))
+ self.assertEquals(content, {'post': 'done'})
+
+ content = self.gh.post('core')
+ request_method.assert_called_with(
+ 'POST', self.base_url + 'core',
+ data=self.parser.dumps(None))
+ self.assertEquals(content, {'post': 'done'})
+
+ response.status_code = 200
+ content = self.gh.patch('core', data=data)
+ request_method.assert_called_with(
+ 'PATCH', self.base_url + 'core',
+ data=self.parser.dumps(data))
+ self.assertEquals(content, {'post': 'done'})
+
+ content = self.gh.patch('core')
+ request_method.assert_called_with(
+ 'PATCH', self.base_url + 'core',
+ data=self.parser.dumps(None))
+ self.assertEquals(content, {'post': 'done'})
+
+ def test_delete(self, request_method):
+ data = {'test': 'test'}
+ response = request_method.return_value
+ response.status_code = 204
+ response.content = self.parser.dumps({'delete': 'done'})
+ delete = self.gh.delete('core', data=data)
+ request_method.assert_called_with(
+ 'DELETE', self.base_url + 'core',
+ data=self.parser.dumps(data))
+ delete = self.gh.delete('core')
+ request_method.assert_called_with(
+ 'DELETE', self.base_url + 'core')
+
+ def test_put(self, request_method):
+ response = request_method.return_value
+ response.status_code = 204
+ response.content = ''
+ put = self.gh.put('core')
+ request_method.assert_called_with(
+ 'PUT', self.base_url + 'core',
+ headers={'Content-length': '0'})
diff --git a/github3/tests/converters_test.py b/github3/tests/converters_test.py
new file mode 100644
index 0000000..66eedc3
--- /dev/null
+++ b/github3/tests/converters_test.py
@@ -0,0 +1,96 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+
+from github3.converters import *
+from github3.models.base import BaseResource
+from unittest import TestCase
+from datetime import datetime
+
+API_STUB = {
+ 'test_str': 'string',
+ 'test_int': 1,
+ 'test_date': '2008-01-14T04:33:35Z',
+ 'test_bool': True,
+ 'map': {'test_str': 'string'},
+ 'dict_map': {
+ 'map1': {
+ 'test_str': 'string',
+ 'test_int': 1
+ },
+ 'map2': {
+ 'test_str': 'string',
+ 'test_int': 2
+ },
+ },
+ 'list_map': [
+ {'test_str': 'string', 'test_int': 1},
+ {'test_str': 'string', 'test_int': 2},
+ ],
+ 'fake_map': 9,
+}
+
+
+class Model(BaseResource):
+
+ @classmethod
+ def idl(self):
+ return {
+ 'strs': ['test_str'],
+ 'ints': ['test_int'],
+ 'dates': ['test_date'],
+ 'bools': ['test_bool'],
+ 'maps': {'map': Model, 'fake_map': Model},
+ 'collection_maps': {
+ 'dict_map': Model,
+ 'list_map': Model,
+ 'fake_map': Model,
+ },
+ }
+
+
+class TestModelizer(TestCase):
+
+ def setUp(self):
+ model = Model
+ self.modelizer = Modelizer()
+ self.modelizer.inject(model)
+
+ def test_loads(self):
+ parsed_model = self.modelizer.loads(API_STUB)
+ self.assertEquals(len(parsed_model), len(API_STUB))
+ self.assertEquals(parsed_model.test_str, 'string')
+ self.assertEquals(parsed_model.test_int, 1)
+ self.assertEquals(
+ parsed_model.test_date,
+ datetime(2008, 1, 14, 4, 33, 35))
+ self.assertTrue(parsed_model.test_bool)
+ self.assertTrue(isinstance(parsed_model.map, Model))
+ self.assertEquals(parsed_model.map.test_str, 'string')
+ self.assertTrue(isinstance(parsed_model.dict_map, dict))
+ map1 = parsed_model.dict_map['map1']
+ map2 = parsed_model.dict_map['map2']
+ self.assertTrue(isinstance(map1, Model))
+ self.assertTrue(isinstance(map2, Model))
+ self.assertEquals(map1.test_str, 'string')
+ self.assertEquals(map1.test_int, 1)
+ self.assertEquals(map2.test_str, 'string')
+ self.assertEquals(map2.test_int, 2)
+
+ list_map = parsed_model.list_map
+ self.assertTrue(isinstance(list_map, list))
+ self.assertEquals(list_map[0].test_str, 'string')
+ self.assertEquals(list_map[0].test_int, 1)
+ self.assertEquals(list_map[1].test_str, 'string')
+ self.assertEquals(list_map[1].test_int, 2)
+
+
+class TestRawlizer(TestCase):
+
+ def setUp(self):
+ model = Model
+ self.rawlizer = Rawlizer()
+
+ # Trivial, I know it
+ def test_loads(self):
+ raw = self.rawlizer.loads(API_STUB)
+ self.assertEquals(raw, API_STUB)
diff --git a/github3/tests/fixtures.py b/github3/tests/fixtures.py
new file mode 100644
index 0000000..ff5179c
--- /dev/null
+++ b/github3/tests/fixtures.py
@@ -0,0 +1,164 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+
+GET_USER = {
+ "login": "octocat",
+ "id": 1,
+ "avatar_url": "https://github.com/images/error/octocat_happy.gif",
+ "gravatar_id": "somehexcode",
+ "url": "https://api.github.com/users/octocat",
+ "name": "monalisa octocat",
+ "company": "GitHub",
+ "blog": "https://github.com/blog",
+ "location": "San Francisco",
+ "email": "octocat@github.com",
+ "hireable": False,
+ "bio": "There once was...",
+ "public_repos": 2,
+ "public_gists": 1,
+ "followers": 20,
+ "following": 0,
+ "html_url": "https://github.com/octocat",
+ "created_at": "2008-01-14T04:33:35Z",
+ "type": "User"
+}
+
+GET_LINK = '<https://api.github.com/it_doesnt_matter?page=2>; rel="next", \
+<https://api.github.com/it_doesnt_matter?page=5>; rel="last"'
+
+GET_RESOURCES = [
+ {'login': 'octocat'},
+ {'login': 'octocat'}
+]
+
+GET_SHORT_USERS = [
+ {
+ "login": "octocat",
+ "id": 1,
+ "avatar_url": "https://github.com/images/error/octocat_happy.gif",
+ "gravatar_id": "somehexcode",
+ "url": "https://api.github.com/users/octocat"
+ },
+ {
+ "login": "octocat",
+ "id": 1,
+ "avatar_url": "https://github.com/images/error/octocat_happy.gif",
+ "gravatar_id": "somehexcode",
+ "url": "https://api.github.com/users/octocat"
+ },
+]
+
+GET_SHORT_ORGS = [
+ {
+ "login": "github",
+ "id": 1,
+ "url": "https://api.github.com/orgs/1",
+ "avatar_url": "https://github.com/images/error/octocat_happy.gif"
+ }
+]
+
+GET_SHORT_REPOS = [
+ {
+ "url": "https://api.github.com/repos/octocat/Hello-World",
+ "html_url": "https://github.com/octocat/Hello-World",
+ "clone_url": "https://github.com/octocat/Hello-World.git",
+ "git_url": "git://github.com/octocat/Hello-World.git",
+ "ssh_url": "git@github.com:octocat/Hello-World.git",
+ "svn_url": "https://svn.github.com/octocat/Hello-World",
+ "owner": {
+ "login": "octocat",
+ "id": 1,
+ "avatar_url": "https://github.com/images/error/octocat_happy.gif",
+ "gravatar_id": "somehexcode",
+ "url": "https://api.github.com/users/octocat"
+ },
+ "name": "Hello-World",
+ "description": "This your first repo!",
+ "homepage": "https://github.com",
+ "language": None,
+ "private": False,
+ "fork": False,
+ "forks": 9,
+ "watchers": 80,
+ "size": 108,
+ "master_branch": "master",
+ "open_issues": 0,
+ "pushed_at": "2011-01-26T19:06:43Z",
+ "created_at": "2011-01-26T19:01:12Z"
+ }
+]
+
+GET_SHORT_GISTS = [
+ {
+ "url": "https://api.github.com/gists/1",
+ "id": "1",
+ "description": "description of gist",
+ "public": True,
+ "user": {
+ "login": "octocat",
+ "id": 1,
+ "avatar_url": "https://github.com/images/error/octocat_happy.gif",
+ "gravatar_id": "somehexcode",
+ "url": "https://api.github.com/users/octocat"
+ },
+ "files": {
+ "ring.erl": {
+ "size": 932,
+ "filename": "ring.erl",
+ "raw_url": "https://gist.github.com/raw/365370/8c4d2d43d178df\
+ 44f4c03a7f2ac0ff512853564e/ring.erl",
+ "content": "contents of gist"
+ }
+ },
+ "comments": 0,
+ "html_url": "https://gist.github.com/1",
+ "git_pull_url": "git://gist.github.com/1.git",
+ "git_push_url": "git@gist.github.com:1.git",
+ "created_at": "2010-04-14T02:15:15Z"
+ }
+]
+GET_FULL_USER = {
+ "login": "octocat",
+ "id": 1,
+ "avatar_url": "https://github.com/images/error/octocat_happy.gif",
+ "gravatar_id": "somehexcode",
+ "url": "https://api.github.com/users/octocat",
+ "name": "monalisa octocat",
+ "company": "GitHub",
+ "blog": "https://github.com/blog",
+ "location": "San Francisco",
+ "email": "octocat@github.com",
+ "hireable": False,
+ "bio": "There once was...",
+ "public_repos": 2,
+ "public_gists": 1,
+ "followers": 20,
+ "following": 0,
+ "html_url": "https://github.com/octocat",
+ "created_at": "2008-01-14T04:33:35Z",
+ "type": "User",
+ "total_private_repos": 100,
+ "owned_private_repos": 100,
+ "private_gists": 81,
+ "disk_usage": 10000,
+ "collaborators": 8,
+ "plan": {
+ "name": "Medium",
+ "space": 400,
+ "collaborators": 10,
+ "private_repos": 20
+ }
+}
+GET_USER_EMAILS = [
+ "octocat@github.com",
+ "support@github.com"
+]
+
+GET_USER_KEYS = [
+ {
+ "url": "https://api.github.com/user/keys/1",
+ "id": 1,
+ "title": "octocat@octomac",
+ "key": "ssh-rsa AAA..."
+ }
+]
diff --git a/github3/tests/get_handlers_test.py b/github3/tests/get_handlers_test.py
new file mode 100644
index 0000000..0d88670
--- /dev/null
+++ b/github3/tests/get_handlers_test.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+
+from unittest import TestCase
+from github3 import api
+from github3 import handlers
+
+
+class TestGetHandlers(TestCase):
+
+ def setUp(self):
+ self.anom_gh = api.Github()
+ self.auth_gh = api.Github('test', 'password')
+
+ def test_get_user(self):
+ anom_user = self.anom_gh.users
+ auth_user = self.auth_gh.users
+
+ self.assertIsInstance(anom_user, handlers.users.User)
+ self.assertEquals(anom_user.prefix, 'users')
+ self.assertIsInstance(auth_user, handlers.users.AuthUser)
+ self.assertEquals(auth_user.prefix, 'user')
+
+ def test_get_gists(self):
+ anom_gists = self.anom_gh.gists
+ auth_gists = self.auth_gh.gists
+
+ self.assertIsInstance(anom_gists, handlers.gists.Gist)
+ self.assertEquals(anom_gists.prefix, 'gists')
+ self.assertIsInstance(auth_gists, handlers.gists.AuthGist)
+ self.assertEquals(anom_gists.prefix, 'gists')
diff --git a/github3/tests/gists_handler_test.py b/github3/tests/gists_handler_test.py
new file mode 100644
index 0000000..ecadcfb
--- /dev/null
+++ b/github3/tests/gists_handler_test.py
@@ -0,0 +1,110 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+
+from unittest import TestCase
+from mock import Mock, patch
+from github3 import api
+from github3.models import Gist, GistComment
+from github3.handlers.base import Handler
+
+
+class TestGistHandler(TestCase):
+
+ def setUp(self):
+ self.gh = api.Github()
+ self.handler = self.gh.gists
+
+ @patch.object(Handler, '_get_resources')
+ def test_get_gists(self, get):
+ gists = self.handler.all_gists()
+ get.assert_called_with('', model=Gist, limit=None)
+
+ @patch.object(Handler, '_get_resource')
+ def test_get(self, get):
+ gist = self.handler.get(1)
+ get.assert_called_with(1, model=Gist)
+
+ @patch.object(Handler, '_get_resources')
+ def test_get_comments(self, get):
+ comments = self.handler.get_comments(1)
+ get.assert_called_with('1/comments', model=GistComment, limit=None,
+ headers=None)
+
+ @patch.object(Handler, '_get_resource')
+ def test_get_comment(self, get):
+ comment = self.handler.get_comment(1)
+ get.assert_called_with('comments/1', model=GistComment, headers=None)
+
+
+class TestAuthGistHandler(TestCase):
+
+ def setUp(self):
+ self.gh = api.Github('test', 'pass')
+ self.handler = self.gh.gists
+
+ def test_inherit(self):
+ self.assertTrue(hasattr(self.handler, 'get'))
+ self.assertTrue(hasattr(self.handler, 'get_comments'))
+ self.assertTrue(hasattr(self.handler, 'get_comment'))
+
+ @patch.object(Handler, '_get_resources')
+ def test_all_gists(self, get):
+ gists = self.handler.all_gists()
+ get.assert_called_with('public', model=Gist, limit=None)
+
+ @patch.object(Handler, '_get_resources')
+ def test_my_gists(self, get):
+ gists = self.handler.my_gists()
+ get.assert_called_with('', model=Gist, limit=None)
+
+ @patch.object(Handler, '_get_resources')
+ def test_my_starred_gists(self, get):
+ gists = self.handler.my_starred_gists()
+ get.assert_called_with('starred', model=Gist, limit=None)
+
+ @patch.object(Handler, '_post_resource')
+ def test_create_gist(self, post):
+ data = {
+ 'public': False,
+ 'files': {'file': {'contents': 'file_data'}},
+ 'desc': 'some'
+ }
+ gist = self.handler.create_gist(data['public'], data['files'],
+ data['desc'])
+ post.assert_called_with('', data=data, model=Gist)
+
+ @patch.object(Handler, '_put')
+ def test_star_gist(self, put):
+ boolean = self.handler.star_gist(1)
+ put.assert_called_with('1/star')
+
+ @patch.object(Handler, '_delete')
+ def test_unstar_gist(self, delete):
+ boolean = self.handler.unstar_gist(1)
+ delete.assert_callted_with('1/star')
+
+ @patch.object(Handler, '_bool')
+ def test_is_starred(self, bool):
+ boolean = self.handler.is_starred(1)
+ bool.assert_called_with('1/star')
+
+ @patch.object(Handler, '_post_resource')
+ def test_fork_gist(self, post):
+ gist = self.handler.fork_gist(1)
+ post.assert_called_with('1/fork', data=None, model=Gist)
+
+ @patch.object(Handler, '_delete')
+ def test_delete_gist(self, delete):
+ boolean = self.handler.delete_gist(1)
+ delete.assert_called_with('1')
+
+ @patch.object(Handler, '_post_resource')
+ def test_create_comment(self, post):
+ gist_comment = self.handler.create_comment(1, 'comment')
+ post.assert_called_with('1/comments', data={'body': 'comment'},
+ model=GistComment)
+
+ @patch.object(Handler, '_delete')
+ def test_delete_comment(self, delete):
+ boolean = self.handler.delete_comment(1)
+ delete.assert_called_with('comments/1')
diff --git a/github3/tests/handler_test.py b/github3/tests/handler_test.py
new file mode 100644
index 0000000..83c89ef
--- /dev/null
+++ b/github3/tests/handler_test.py
@@ -0,0 +1,133 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+
+from mock import Mock, patch
+from unittest import TestCase
+from github3 import api
+from github3.handlers.base import Handler, MimeTypeMixin
+from github3.exceptions import *
+from github3.converters import *
+from github3.models.user import User
+from fixtures import *
+import json
+import requests
+
+
+class TestMimeTypeMixin(TestCase):
+
+ def setUp(self):
+ self.mixin = MimeTypeMixin()
+
+ def _parse_mime_type(self, type):
+ return 'application/vnd.github.%s.%s+json' % (
+ MimeTypeMixin.VERSION, type)
+
+ def test_header(self):
+ self.assertEquals(self.mixin.mime_header(), None)
+
+ def test_add_mimetypes(self):
+ self.mixin.add_raw()
+ self.mixin.add_text()
+ self.mixin.add_html()
+ self.mixin.add_full()
+ self.assertEquals(sorted(self.mixin.mime_header()), sorted({
+ 'Accept': '%s, %s, %s, %s' % (
+ self._parse_mime_type('raw'),
+ self._parse_mime_type('text'),
+ self._parse_mime_type('html'),
+ self._parse_mime_type('full'))}))
+
+
+class TestHandler(TestCase):
+
+ def setUp(self):
+ self.gh = api.Github()
+ self.handler = Handler(self.gh)
+
+ def test_get_converter(self):
+ self.assertIsInstance(self.handler._get_converter(), Modelizer)
+ kwargs = {'converter': Rawlizer}
+ self.assertIsInstance(self.handler._get_converter(kwargs),
+ Rawlizer)
+ self.assertEquals(kwargs, {})
+ self.handler.converter = Modelizer
+ self.assertIsInstance(self.handler._get_converter(), Modelizer)
+
+ def test_bool(self):
+ with patch.object(api.Github, 'head') as head:
+ response = head.return_value
+ response.status_code = 204
+ bool1 = self.handler._bool('test')
+ head.side_effect = NotFound()
+ bool2 = self.handler._bool('test')
+ head.assert_called_with('test')
+ self.assertTrue(bool1)
+ self.assertFalse(bool2)
+
+ with patch.object(api.Github, 'put') as put:
+ response = put.return_value
+ response.status_code = 204
+ booll = self.handler._put('test')
+ put.assert_called_with('test', method='put')
+ self.assertTrue(booll)
+
+ with patch.object(api.Github, 'delete') as delete:
+ response = delete.return_value
+ response.content = self.gh._parser.dumps({'data': 'test'})
+ response.status_code = 204
+ bool1 = self.handler._bool('test', method='delete')
+ bool2 = self.handler._bool('test', method='delete',
+ data={'some': 'data'})
+ self.assertTrue(bool1)
+ self.assertTrue(bool2)
+
+ @patch.object(api.Github, '_request')
+ def test_get_resources(self, request):
+ # Simulating per_page=2 with STUB (it returns two resources)
+ response = request.return_value
+ response.status_code = 200
+ response.headers = {'link': GET_LINK}
+ response.content = self.gh._parser.dumps(GET_RESOURCES)
+ resources = self.handler._get_resources('users', model=User)
+ self.assertFalse(request.called)
+ resources = list(resources)
+ self.assertTrue(request.call_count, 5)
+ request_args = ('GET', 'users')
+ self.assertEquals(request.call_args_list, [
+ (request_args, {'page': 1}),
+ (request_args, {'page': 2}),
+ (request_args, {'page': 3}),
+ (request_args, {'page': 4}),
+ (request_args, {'page': 5})])
+ self.assertEquals(len(resources), 10)
+ self.assertEquals(resources[0].login, 'octocat')
+
+ request.reset_mock()
+ resources = self.handler._get_resources('users', model=User, limit=5)
+ resources = list(resources)
+ self.assertEquals(request.call_count, 3)
+ self.assertEquals(len(resources), 5)
+ request.reset_mock()
+ resources = self.handler._get_resources('users', model=User, limit=4)
+ resources = list(resources)
+ self.assertEquals(request.call_count, 2)
+ self.assertEquals(len(resources), 4)
+ request.reset_mock()
+ resources = self.handler._get_resources('users', model=User, limit=-5)
+ resources = list(resources)
+ self.assertEquals(request.call_count, 3)
+ self.assertEquals(len(resources), 5)
+
+ @patch.object(api.Github, 'get')
+ def test_get_resource(self, get):
+ # Converter test + api(get) test. Half trivial
+ get.return_value = {'login': 'octocat'}
+ model = self.handler._get_resource('test', model=User)
+ self.assertEquals(model.login, 'octocat')
+
+ @patch.object(api.Github, 'post')
+ def test_post_resource(self, post):
+ post.return_value = {'data': 'posted'}
+ data = {'data': 'to_post'}
+ user_new = self.handler._post_resource('test', data=data, model=User)
+ post.assert_called_with('test', data=data)
diff --git a/github3/tests/user_handler_test.py b/github3/tests/user_handler_test.py
new file mode 100644
index 0000000..33658d2
--- /dev/null
+++ b/github3/tests/user_handler_test.py
@@ -0,0 +1,326 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+
+from unittest import TestCase
+from mock import Mock, patch
+from github3 import api
+from fixtures import *
+from github3.models import User, AuthUser, Repo, Gist, Org, Key
+from github3.exceptions import *
+from github3 import handlers
+
+
+class TestAuthUserHandler(TestCase):
+ """ Test private api about user logged """
+
+ def setUp(self):
+ self.gh = api.Github('test', 'pass')
+ self.handler = self.gh.users
+ self.user_mock = Mock()
+ self.user_mock.login = 'user_model'
+
+ def test_inject_user_handler(self):
+ self.assertEquals(self.handler.get.im_class, handlers.users.User)
+ self.assertEquals(self.handler.get_followers.im_class,
+ handlers.users.User)
+ self.assertEquals(self.handler.get_following.im_class,
+ handlers.users.User)
+ self.assertEquals(self.handler.get_repos.im_class,
+ handlers.users.User)
+ self.assertEquals(self.handler.get_watched.im_class,
+ handlers.users.User)
+ self.assertEquals(self.handler.get_orgs.im_class,
+ handlers.users.User)
+ self.assertEquals(self.handler.get_gists.im_class,
+ handlers.users.User)
+
+ @patch.object(api.Github, 'get')
+ def test_me(self, get):
+ get.return_value = GET_FULL_USER
+ user = self.handler.me()
+ self.assertIsInstance(user, AuthUser)
+ get.assert_called_with('user')
+ self.assertEquals(len(user), len(GET_FULL_USER))
+
+ @patch.object(handlers.base.Handler, '_get_resource')
+ def test_get(self, get):
+ user = self.handler.get('test')
+ get.assert_called_with('test', model=User)
+
+ @patch.object(handlers.base.Handler, '_get_resources')
+ def test_get_my_followers(self, get):
+ followers = self.handler.my_followers()
+ get.assert_called_with('followers', model=User, limit=None)
+
+ @patch.object(handlers.base.Handler, '_get_resources')
+ def test_get_my_following(self, get):
+ following = self.handler.my_following()
+ get.assert_called_with('following', model=User, limit=None)
+
+ @patch.object(handlers.base.Handler, '_get_resources')
+ def test_get_my_watched(self, get):
+ following = self.handler.my_watched()
+ get.assert_called_with('watched', model=Repo, limit=None)
+
+ @patch.object(handlers.base.Handler, '_get_resources')
+ def test_get_my_orgs(self, get):
+ following = self.handler.my_orgs()
+ get.assert_called_with('orgs', model=Org, limit=None)
+
+ @patch.object(api.Github, 'get')
+ def test_get_emails(self, get):
+ get.return_value = GET_USER_EMAILS
+ emails = self.handler.get_emails()
+ get.assert_called_with('user/emails')
+ self.assertEquals(emails, GET_USER_EMAILS)
+
+ @patch.object(api.Github, 'post')
+ def test_create_emails(self, post):
+ post.return_value = GET_USER_EMAILS
+ emails = self.handler.create_emails(*GET_USER_EMAILS)
+ post.assert_called_with('user/emails', data=GET_USER_EMAILS)
+ self.assertEquals(emails, GET_USER_EMAILS)
+
+ @patch.object(api.Github, 'delete')
+ def test_delete_emails(self, delete):
+ response = delete.return_value
+ response.return_value = ''
+ response.status_code = 204
+ emails = self.handler.delete_emails(*GET_USER_EMAILS)
+ delete.assert_called_with('user/emails', data=GET_USER_EMAILS,
+ method='delete')
+ self.assertTrue(emails)
+
+ @patch.object(api.Github, 'head')
+ def test_is_following(self, head):
+ response = head.return_value
+ response.status_code = 204
+ self.assertTrue(self.handler.is_following('test'))
+ head.assert_called_with('user/following/test')
+ self.handler.is_following(self.user_mock)
+ head.assert_called_with('user/following/user_model')
+
+ @patch.object(api.Github, 'put')
+ def test_follow(self, put):
+ response = put.return_value
+ response.status_code = 204
+ self.assertTrue(self.handler.follow('test'))
+ put.assert_called_with('user/following/test', method='put')
+
+ @patch.object(api.Github, 'delete')
+ def test_unfollow(self, delete):
+ response = delete.return_value
+ response.status_code = 204
+ self.assertTrue(self.handler.unfollow('test'))
+ delete.assert_called_with('user/following/test', method='delete')
+
+ @patch.object(api.Github, '_request')
+ def test_get_keys(self, request):
+ response = request.return_value
+ response.status_code = 200
+ response.content = self.gh._parser.dumps(GET_USER_KEYS)
+ response.headers = {'link': GET_LINK} # 1 per page
+ keys = list(self.handler.get_keys())
+ self.assertEquals(len(keys), 5)
+ self.assertIsInstance(keys[0], Key)
+ request.assert_called_with('GET', 'user/keys', page=5)
+ keys = list(self.handler.get_keys(limit=2))
+ self.assertEquals(len(keys), 2)
+
+ @patch.object(api.Github, 'get')
+ def test_get_key(self, get):
+ get.return_value = GET_USER_KEYS[0]
+ key = self.handler.get_key(1)
+ self.assertIsInstance(key, Key)
+ get.assert_called_with('user/keys/1')
+ model_key = Mock()
+ model_key.id = 1
+ key = self.handler.get_key(model_key)
+ get.assert_called_with('user/keys/1')
+
+ @patch.object(api.Github, 'post')
+ def test_create_key(self, post):
+ post.return_value = GET_USER_KEYS[0]
+ key_data = {'title': 'some', 'key': 'ssh-rsa AAA'}
+ created_key = self.handler.create_key(**key_data)
+ self.assertIsInstance(created_key, Key)
+ post.assert_called_with('user/keys', data=key_data)
+
+ @patch.object(api.Github, 'delete')
+ def test_delete_key(self, delete):
+ response = delete.return_value
+ response.status_code = 204
+ self.assertTrue(self.handler.delete_key(1))
+ delete.assert_called_with('user/keys/1', method='delete')
+ model_key = Mock()
+ model_key.id = 1
+ key = self.handler.delete_key(model_key)
+ delete.assert_called_with('user/keys/1', method='delete')
+
+ @patch.object(api.Github, '_request')
+ def test_my_repos(self, request):
+ response = request.return_value
+ response.status_code = 200
+ response.content = self.gh._parser.dumps(GET_SHORT_REPOS)
+ response.headers = {'link': GET_LINK} # 1 per page
+ repos = list(self.handler.my_repos(filter='public'))
+ self.assertEquals(len(repos), 5)
+ self.assertIsInstance(repos[0], Repo)
+ request.assert_called_with('GET', 'user/repos',
+ page=5, type='public')
+ repos = list(self.handler.my_repos(limit=2))
+ self.assertEquals(len(repos), 2)
+
+ @patch.object(api.Github, 'head')
+ def test_is_watching_repo(self, head):
+ response = head.return_value
+ response.status_code = 204
+ self.assertTrue(self.handler.is_watching_repo('user', 'repo'))
+ head.assert_called_with('user/watched/user/repo')
+ model_user, model_repo = Mock(), Mock()
+ model_user.login = 'user'
+ model_repo.name = 'repo'
+ self.assertTrue(self.handler.is_watching_repo('user', 'repo'))
+ head.assert_called_with('user/watched/user/repo')
+
+ @patch.object(api.Github, 'put')
+ def test_watch_repo(self, put):
+ response = put.return_value
+ response.status_code = 204
+ self.assertTrue(self.handler.watch_repo('user', 'repo'))
+ put.assert_called_with('user/watched/user/repo', method='put')
+ model_user, model_repo = Mock(), Mock()
+ model_user.login = 'user'
+ model_repo.name = 'repo'
+ self.assertTrue(self.handler.watch_repo('user', 'repo'))
+ put.assert_called_with('user/watched/user/repo', method='put')
+
+ @patch.object(api.Github, 'delete')
+ def test_unwatch_repo(self, delete):
+ response = delete.return_value
+ response.status_code = 204
+ self.assertTrue(self.handler.unwatch_repo('user', 'repo'))
+ delete.assert_called_with('user/watched/user/repo', method='delete')
+ model_user, model_repo = Mock(), Mock()
+ model_user.login = 'user'
+ model_repo.name = 'repo'
+ self.assertTrue(self.handler.unwatch_repo('user', 'repo'))
+ delete.assert_called_with('user/watched/user/repo', method='delete')
+
+
+class TestUserHandler(TestCase):
+ """ Test public api about users """
+
+ def setUp(self):
+ self.gh = api.Github()
+ self.handler = self.gh.users
+
+ def test_set_username(self):
+ handler = self.handler.set_username('test')
+ self.assertEquals(id(handler), id(self.handler))
+ self.assertEquals(handler.username, 'test')
+ model_user = Mock()
+ model_user.login = 'test'
+ handler = self.handler.set_username(model_user)
+ self.assertEquals(handler.username, 'test')
+
+ def test_parse_user(self):
+ model_user = Mock()
+ model_user.login = 'test'
+ self.assertRaises(UserIsAnonymous, self.handler._parse_user, None)
+ user = self.handler._parse_user(model_user)
+ self.assertEquals(user, 'test')
+ user = self.handler._parse_user('test')
+ self.assertEquals(user, 'test')
+ self.assertRaises(UserIsAnonymous, self.handler._parse_user, Mock())
+ self.handler.set_username('octocat')
+ self.assertEquals('octocat', self.handler._parse_user(None))
+ self.assertEquals('octocat', self.handler._parse_user(Mock()))
+ self.assertEquals('test', self.handler._parse_user('test'))
+ self.assertEquals('test', self.handler._parse_user(model_user))
+
+ @patch.object(api.Github, 'get')
+ def test_get(self, get):
+ get.return_value = GET_USER
+ self.assertRaises(UserIsAnonymous, self.handler.get)
+ user = self.handler.get('octocat')
+ self.assertIsInstance(user, User)
+ get.assert_called_with('users/octocat')
+
+ @patch.object(api.Github, '_request')
+ def test_get_followers(self, request):
+ response = request.return_value
+ response.headers = {'link': GET_LINK}
+ response.content = self.gh._parser.dumps(GET_SHORT_USERS) # 2 users
+ followers = list(self.handler.get_followers('test'))
+ request.assert_called_with('GET', 'users/test/followers', page=5)
+ self.assertIsInstance(followers[0], User)
+ self.assertEquals(len(followers), 10)
+ followers = list(self.handler.get_followers('test', limit=2))
+ self.assertEquals(len(followers), 2)
+ self.assertEquals(followers[0].login, 'octocat')
+
+ @patch.object(api.Github, '_request')
+ def test_get_following(self, request):
+ response = request.return_value
+ response.headers = {'link': GET_LINK}
+ response.content = self.gh._parser.dumps(GET_SHORT_USERS) # 2 users
+ following = list(self.handler.get_following('test'))
+ request.assert_called_with('GET', 'users/test/following', page=5)
+ self.assertIsInstance(following[0], User)
+ self.assertEquals(len(following), 10)
+ following = list(self.handler.get_following('test', limit=2))
+ self.assertEquals(len(following), 2)
+
+ @patch.object(api.Github, '_request')
+ def test_get_repos(self, request):
+ response = request.return_value
+ response.headers = {'link': GET_LINK}
+ response.content = self.gh._parser.dumps(GET_SHORT_REPOS) # 1 repo
+ repos = list(self.handler.get_repos('test'))
+ request.assert_called_with('GET', 'users/test/repos', page=5)
+ self.assertIsInstance(repos[0], Repo)
+ self.assertEquals(len(repos), 5)
+ repos = list(self.handler.get_repos('test', limit=2))
+ self.assertEquals(len(repos), 2)
+ self.assertIsInstance(repos[0].owner, User)
+
+ @patch.object(api.Github, '_request')
+ def test_get_watched(self, request):
+ response = request.return_value
+ response.headers = {'link': GET_LINK}
+ response.content = self.gh._parser.dumps(GET_SHORT_REPOS) # 1 repo
+ watched = list(self.handler.get_watched('test'))
+ request.assert_called_with('GET', 'users/test/watched', page=5)
+ self.assertIsInstance(watched[0], Repo)
+ self.assertEquals(len(watched), 5)
+ watched = list(self.handler.get_watched('test', limit=2))
+ self.assertEquals(len(watched), 2)
+
+ @patch.object(api.Github, '_request')
+ def test_get_orgs(self, request):
+ response = request.return_value
+ response.headers = {'link': GET_LINK}
+ response.content = self.gh._parser.dumps(GET_SHORT_ORGS) # 1 repo
+ orgs = list(self.handler.get_orgs('test'))
+ request.assert_called_with('GET', 'users/test/orgs', page=5)
+ self.assertIsInstance(orgs[0], Org)
+ self.assertEquals(len(orgs), 5)
+ orgs = list(self.handler.get_orgs('test', limit=2))
+ self.assertEquals(len(orgs), 2)
+ self.assertEquals(orgs[0].login, 'github')
+
+ @patch.object(api.Github, '_request')
+ def test_get_gists(self, request):
+ response = request.return_value
+ response.headers = {'link': GET_LINK}
+ response.content = self.gh._parser.dumps(GET_SHORT_GISTS) # 1 repo
+ gists = list(self.handler.get_gists('test'))
+ request.assert_called_with('GET', 'users/test/gists', page=5)
+ self.assertIsInstance(gists[0], Gist)
+ self.assertEquals(len(gists), 5)
+ gists = list(self.handler.get_gists('test', limit=2))
+ self.assertEquals(len(gists), 2)
+ self.assertIsInstance(gists[0].files, dict)
+ from github3.models.gists import File
+ self.assertIsInstance(gists[0].files['ring.erl'], File)
diff --git a/reqs.txt b/reqs.txt
index e97df0b..516c4b0 100644
--- a/reqs.txt
+++ b/reqs.txt
@@ -1,2 +1,4 @@
requests==0.7.4
python-dateutil==2.0
+mock==0.7.2
+nose
diff --git a/run_tests.sh b/run_tests.sh
new file mode 100644
index 0000000..5b110e5
--- /dev/null
+++ b/run_tests.sh
@@ -0,0 +1 @@
+nosetests