diff options
-rw-r--r-- | AUTHORS | 4 | ||||
-rw-r--r-- | README.md (renamed from README.rst) | 59 | ||||
-rw-r--r-- | github3/api.py | 60 | ||||
-rw-r--r-- | github3/converters.py | 35 | ||||
-rw-r--r-- | github3/core.py | 10 | ||||
-rw-r--r-- | github3/errors.py | 8 | ||||
-rw-r--r-- | github3/exceptions.py | 7 | ||||
-rw-r--r-- | github3/handlers/base.py | 76 | ||||
-rw-r--r-- | github3/handlers/gists.py | 145 | ||||
-rw-r--r-- | github3/handlers/users.py | 299 | ||||
-rw-r--r-- | github3/helpers.py | 188 | ||||
-rw-r--r-- | github3/models/__init__.py | 2 | ||||
-rw-r--r-- | github3/models/base.py | 11 | ||||
-rw-r--r-- | github3/models/gists.py | 29 | ||||
-rw-r--r-- | github3/models/orgs.py | 2 | ||||
-rw-r--r-- | github3/models/repos.py | 2 | ||||
-rw-r--r-- | github3/models/user.py | 24 | ||||
-rw-r--r-- | github3/packages/link_header.py | 3 | ||||
-rw-r--r-- | github3/tests/api_test.py | 124 | ||||
-rw-r--r-- | github3/tests/converters_test.py | 96 | ||||
-rw-r--r-- | github3/tests/fixtures.py | 164 | ||||
-rw-r--r-- | github3/tests/get_handlers_test.py | 31 | ||||
-rw-r--r-- | github3/tests/gists_handler_test.py | 110 | ||||
-rw-r--r-- | github3/tests/handler_test.py | 133 | ||||
-rw-r--r-- | github3/tests/user_handler_test.py | 326 | ||||
-rw-r--r-- | reqs.txt | 2 | ||||
-rw-r--r-- | run_tests.sh | 1 |
27 files changed, 1637 insertions, 314 deletions
@@ -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> @@ -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) @@ -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 |