From 7ef5a699368df270ed1c1631d07ab90f81d25944 Mon Sep 17 00:00:00 2001 From: David Lathrop Date: Tue, 24 Jan 2012 00:35:40 -0500 Subject: [PATCH 01/14] Renamed github2 to github3 in preparation for the migration to the Github API v3. I also modified the setup.py, setup.cfg and doc/conf.py for this refactoring. Additionally, I made code changes to support some of the API v3 changes in core, client, users, repositories and init.py. --- .../DigiCert_High_Assurance_EV_Root_CA.crt | 0 {github2 => github3}/__init__.py | 4 +- {github2 => github3}/_version.py | 2 +- {github2 => github3}/bin/__init__.py | 0 .../bin/manage_collaborators.py | 4 +- {github2 => github3}/bin/search_repos.py | 4 +- {github2 => github3}/client.py | 18 ++--- {github2 => github3}/commits.py | 2 +- {github2 => github3}/core.py | 4 +- {github2 => github3}/issues.py | 2 +- {github2 => github3}/organizations.py | 8 +- {github2 => github3}/pull_requests.py | 2 +- {github2 => github3}/repositories.py | 23 +++++- {github2 => github3}/request.py | 19 ++--- {github2 => github3}/teams.py | 6 +- {github2 => github3}/users.py | 74 ++++++++++++++++--- 16 files changed, 121 insertions(+), 51 deletions(-) rename {github2 => github3}/DigiCert_High_Assurance_EV_Root_CA.crt (100%) rename {github2 => github3}/__init__.py (72%) rename {github2 => github3}/_version.py (77%) rename {github2 => github3}/bin/__init__.py (100%) rename {github2 => github3}/bin/manage_collaborators.py (98%) rename {github2 => github3}/bin/search_repos.py (96%) rename {github2 => github3}/client.py (92%) rename {github2 => github3}/commits.py (97%) rename {github2 => github3}/core.py (98%) rename {github2 => github3}/issues.py (99%) rename {github2 => github3}/organizations.py (95%) rename {github2 => github3}/pull_requests.py (98%) rename {github2 => github3}/repositories.py (92%) rename {github2 => github3}/request.py (94%) rename {github2 => github3}/teams.py (94%) rename {github2 => github3}/users.py (58%) diff --git a/github2/DigiCert_High_Assurance_EV_Root_CA.crt b/github3/DigiCert_High_Assurance_EV_Root_CA.crt similarity index 100% rename from github2/DigiCert_High_Assurance_EV_Root_CA.crt rename to github3/DigiCert_High_Assurance_EV_Root_CA.crt diff --git a/github2/__init__.py b/github3/__init__.py similarity index 72% rename from github2/__init__.py rename to github3/__init__.py index 30d323e..7cbe8f0 100644 --- a/github2/__init__.py +++ b/github3/__init__.py @@ -1,6 +1,6 @@ -"Github API v2 library for Python" +"Github API v3 library for Python" -from github2 import _version +from github3 import _version VERSION = _version.tuple diff --git a/github2/_version.py b/github3/_version.py similarity index 77% rename from github2/_version.py rename to github3/_version.py index bf9f4df..7514dc4 100644 --- a/github2/_version.py +++ b/github3/_version.py @@ -1,4 +1,4 @@ -# This is github2 version 0.6.0 (2011-12-21) +# This is github3 version 0.6.0 (2011-12-21) # pylint: disable=C0103, C0111, C0121, W0622 dotted = "0.6.0" diff --git a/github2/bin/__init__.py b/github3/bin/__init__.py similarity index 100% rename from github2/bin/__init__.py rename to github3/bin/__init__.py diff --git a/github2/bin/manage_collaborators.py b/github3/bin/manage_collaborators.py similarity index 98% rename from github2/bin/manage_collaborators.py rename to github3/bin/manage_collaborators.py index 3efafee..4e30c72 100755 --- a/github2/bin/manage_collaborators.py +++ b/github3/bin/manage_collaborators.py @@ -16,7 +16,7 @@ from optparse import OptionParser -import github2.client +import github3.client #: Running under Python 3 @@ -80,7 +80,7 @@ def main(): if not options.account: options.account = options.login - github = github2.client.Github(username=options.login, + github = github3.client.Github(username=options.login, api_token=options.apitoken, cache=options.cache) diff --git a/github2/bin/search_repos.py b/github3/bin/search_repos.py similarity index 96% rename from github2/bin/search_repos.py rename to github3/bin/search_repos.py index c52be46..3d0e059 100755 --- a/github2/bin/search_repos.py +++ b/github3/bin/search_repos.py @@ -9,7 +9,7 @@ from optparse import OptionParser from textwrap import wrap -import github2.client +import github3.client #: Running under Python 3 @@ -51,7 +51,7 @@ def main(): options, term = parse_commandline() - github = github2.client.Github(cache=options.cache) + github = github3.client.Github(cache=options.cache) # PEP-308 conditional expressions are much better, but we're keeping Py2.4 # compatibility elsewhere. diff --git a/github2/client.py b/github3/client.py similarity index 92% rename from github2/client.py rename to github3/client.py index f761b62..5ee4fe3 100644 --- a/github2/client.py +++ b/github3/client.py @@ -1,11 +1,11 @@ -from github2.request import GithubRequest -from github2.issues import Issues -from github2.repositories import Repositories -from github2.users import Users -from github2.commits import Commits -from github2.organizations import Organizations -from github2.teams import Teams -from github2.pull_requests import PullRequests +from github3.request import GithubRequest +from github3.issues import Issues +from github3.repositories import Repositories +from github3.users import Users +from github3.commits import Commits +from github3.organizations import Organizations +from github3.teams import Teams +from github3.pull_requests import PullRequests class Github(object): @@ -15,7 +15,7 @@ def __init__(self, username=None, api_token=None, requests_per_second=None, proxy_port=8080, github_url=None): """ An interface to GitHub's API: - http://develop.github.com/ + http://developer.github.com/ .. versionadded:: 0.2.0 The ``requests_per_second`` parameter diff --git a/github2/commits.py b/github3/commits.py similarity index 97% rename from github2/commits.py rename to github3/commits.py index 8910937..27dc732 100644 --- a/github2/commits.py +++ b/github3/commits.py @@ -1,4 +1,4 @@ -from github2.core import (BaseData, GithubCommand, Attribute, DateAttribute, +from github3.core import (BaseData, GithubCommand, Attribute, DateAttribute, repr_string) diff --git a/github2/core.py b/github3/core.py similarity index 98% rename from github2/core.py rename to github3/core.py index 60da833..674b133 100644 --- a/github2/core.py +++ b/github3/core.py @@ -6,7 +6,7 @@ #: Logger for core module -LOGGER = logging.getLogger('github2.core') +LOGGER = logging.getLogger('github3.core') #: Running under Python 3 PY3K = sys.version_info[0] == 3 @@ -96,6 +96,8 @@ def datetime_to_isodate(datetime_): class AuthError(Exception): """Requires authentication""" +class DeprecationException(Exception): + """Deprecated by v3 of the github api""" def requires_auth(f): """Decorate to check a function call for authentication diff --git a/github2/issues.py b/github3/issues.py similarity index 99% rename from github2/issues.py rename to github3/issues.py index ae9723b..95258dd 100644 --- a/github2/issues.py +++ b/github3/issues.py @@ -3,7 +3,7 @@ except ImportError: from urllib import quote_plus -from github2.core import (GithubCommand, BaseData, Attribute, DateAttribute, +from github3.core import (GithubCommand, BaseData, Attribute, DateAttribute, repr_string, requires_auth) diff --git a/github2/organizations.py b/github3/organizations.py similarity index 95% rename from github2/organizations.py rename to github3/organizations.py index 0c8aa00..3390bcc 100644 --- a/github2/organizations.py +++ b/github3/organizations.py @@ -1,8 +1,8 @@ -from github2.core import (BaseData, GithubCommand, Attribute, DateAttribute, +from github3.core import (BaseData, GithubCommand, Attribute, DateAttribute, requires_auth) -from github2.repositories import Repository -from github2.teams import Team -from github2.users import User +from github3.repositories import Repository +from github3.teams import Team +from github3.users import User class Organization(BaseData): diff --git a/github2/pull_requests.py b/github3/pull_requests.py similarity index 98% rename from github2/pull_requests.py rename to github3/pull_requests.py index 3573f75..d2aa7b8 100644 --- a/github2/pull_requests.py +++ b/github3/pull_requests.py @@ -1,4 +1,4 @@ -from github2.core import (BaseData, GithubCommand, Attribute, DateAttribute, +from github3.core import (BaseData, GithubCommand, Attribute, DateAttribute, repr_string) diff --git a/github2/repositories.py b/github3/repositories.py similarity index 92% rename from github2/repositories.py rename to github3/repositories.py index cbd9ad4..2e27b5c 100644 --- a/github2/repositories.py +++ b/github3/repositories.py @@ -1,7 +1,7 @@ -from github2.core import (BaseData, GithubCommand, Attribute, DateAttribute, +from github3.core import (BaseData, GithubCommand, Attribute, DateAttribute, requires_auth) -from github2.users import User +from github3.users import User class Repository(BaseData): @@ -26,7 +26,7 @@ class Repository(BaseData): parent = Attribute("The parent project of this fork.") def _project(self): - return self.owner + "/" + self.name + return self.owner['login'] + "/" + self.name project = property(_project) def __repr__(self): @@ -76,8 +76,23 @@ def list(self, user=None, page=1): :param int page: optional page number """ user = user or self.request.username - return self.get_values("show", user, filter="repositories", + temp_domain = self.domain + self.domain = 'users' + ret_val = self.get_values(user, "repos", filter=None, datatype=Repository, page=page) + self.domain = temp_domain + return ret_val + + @requires_auth + def list_auth(self): + """Returns a list of all repositories for the authenticated user. + """ + temp_domain = self.domain + self.domain = 'user' + ret_val = self.get_values("repos", filter=None, + datatype=Repository) + self.domain = temp_domain + return ret_val @requires_auth def watch(self, project): diff --git a/github2/request.py b/github3/request.py similarity index 94% rename from github2/request.py rename to github3/request.py index d6190e2..cb45ff3 100644 --- a/github2/request.py +++ b/github3/request.py @@ -34,12 +34,12 @@ #: Hostname for API access -DEFAULT_GITHUB_URL = "https://github.com" +DEFAULT_GITHUB_URL = "https://api.github.com" #: Logger for requests module -LOGGER = logging.getLogger('github2.request') +LOGGER = logging.getLogger('github3.request') -#: Whether github2 is using the system's certificates for SSL connections +#: Whether github3 is using the system's certificates for SSL connections SYSTEM_CERTS = not httplib2.CA_CERTS.startswith(path.dirname(httplib2.__file__)) if not SYSTEM_CERTS and sys.platform.startswith('linux'): for cert_file in ['/etc/ssl/certs/ca-certificates.crt', @@ -101,9 +101,7 @@ def __init__(self, message, content, code): class GithubRequest(object): - url_format = "%(github_url)s/api/%(api_version)s/%(api_format)s" - api_version = "v2" - api_format = "json" + url_format = "%(github_url)s" GithubError = GithubError def __init__(self, username=None, api_token=None, url_prefix=None, @@ -112,7 +110,7 @@ def __init__(self, username=None, api_token=None, url_prefix=None, github_url=None): """Make an API request. - :see: :class:`github2.client.Github` + :see: :class:`github3.client.Github` """ self.username = username self.api_token = api_token @@ -130,8 +128,6 @@ def __init__(self, username=None, api_token=None, url_prefix=None, if not self.url_prefix: self.url_prefix = self.url_format % { "github_url": self.github_url, - "api_version": self.api_version, - "api_format": self.api_format, } if proxy_host is None: self._http = httplib2.Http(cache=cache) @@ -189,6 +185,7 @@ def make_request(self, path, extra_post_data=None, method="GET"): extra_post_data = extra_post_data or {} url = "/".join([self.url_prefix, quote(path)]) + print url result = self.raw_request(url, extra_post_data, method=method) if self.delay: @@ -215,7 +212,7 @@ def raw_request(self, url, extra_post_data, method="GET"): % (response.status, content), content, response.status) json = simplejson.loads(content.decode(charset_from_headers(response))) - if json.get("error"): + if 'error' in json: raise self.GithubError(json["error"][0]["error"]) return json @@ -223,6 +220,6 @@ def raw_request(self, url, extra_post_data, method="GET"): @property def http_headers(self): return { - "User-Agent": "pygithub2 v1", + "User-Agent": "pygithub3 v1", "Accept": "application/json", } diff --git a/github2/teams.py b/github3/teams.py similarity index 94% rename from github2/teams.py rename to github3/teams.py index b46116f..36d62b9 100644 --- a/github2/teams.py +++ b/github3/teams.py @@ -1,6 +1,6 @@ -from github2.core import BaseData, GithubCommand, Attribute, requires_auth -from github2.repositories import Repository -from github2.users import User +from github3.core import BaseData, GithubCommand, Attribute, requires_auth +from github3.repositories import Repository +from github3.users import User class Team(BaseData): diff --git a/github2/users.py b/github3/users.py similarity index 58% rename from github2/users.py rename to github3/users.py index 7dba3e6..767f886 100644 --- a/github2/users.py +++ b/github3/users.py @@ -3,8 +3,8 @@ except ImportError: from urllib import quote_plus -from github2.core import (BaseData, GithubCommand, DateAttribute, Attribute, - enhanced_by_auth, requires_auth) +from github3.core import (BaseData, GithubCommand, DateAttribute, Attribute, + DeprecationException, enhanced_by_auth, requires_auth) class User(BaseData): @@ -42,7 +42,7 @@ def __repr__(self): class Users(GithubCommand): - domain = "user" + domain = "users" def search(self, query): """Search for users @@ -52,15 +52,14 @@ def search(self, query): :param str query: term to search for """ - return self.get_values("search", quote_plus(query), filter="users", - datatype=User) + raise DeprecationException() def search_by_email(self, query): """Search for users by email address :param str query: email to search for """ - return self.get_value("email", query, filter="user", datatype=User) + raise DeprecationException() @enhanced_by_auth def show(self, username): @@ -71,21 +70,33 @@ def show(self, username): :param str username: Github user name """ - return self.get_value("show", username, filter="user", datatype=User) + return self.get_value(None, username, filter=None, datatype=User) def followers(self, username): """Get list of Github user's followers :param str username: Github user name """ - return self.get_values("show", username, "followers", filter="users") + return self.get_values(None, username, "followers", filter=None) def following(self, username): """Get list of users a Github user is following :param str username: Github user name """ - return self.get_values("show", username, "following", filter="users") + return self.get_values(None, username, "following", filter=None) + + @requires_auth + def is_following(self, other_user): + """Check if the authenticated user is following another. + + :param str other_user: Github username + """ + temp_domain = self.domain + self.domain = 'user' + val = self.get_value('following', other_user, filter=None) + self.domain = temp_domain + return val @requires_auth def follow(self, other_user): @@ -102,3 +113,48 @@ def unfollow(self, other_user): :param str other_user: Github user name """ return self.get_values("unfollow", other_user, method="POST") + + # @requires_auth + # def keys(self): + # temp_domain = self.domain + # self.domain = 'user' + # v = self.get_values("keys") + # self.domain = temp_domain + + # @requires_auth + # def key(self, id): + # temp_domain = self.domain + # self.domain = 'user' + # v = self.get_value("key", id) + # self.domain = temp_domain + # return v + + # @requires_auth + # def create_key(self, title, key_data): + # key = {'title': title, + # 'key': key_data} + # temp_domain = self.domain + # self.domain = 'user' + # v = self.get_value('keys', post_data=key, method='POST') + # self.domain = temp_domain + # return v + + # @requires_auth + # def update_key(self, id, title, key_data): + # key = {'title': title, + # 'key': key_data} + # temp_domain = self.domain + # self.domain = 'user' + # v = self.get_value('keys', id, post_data=key, method='POST') + # self.domain = temp_domain + # return v + + # @requires_auth + # def delete_key(self, id): + # temp_domain = self.domain + # self.domain = 'user' + # v = self.get_value('keys', id, method='DELETE') + # self.domain = temp_domain + # return v + + From 40e8ee9702cd24c1994affb3706f73fb7baec711 Mon Sep 17 00:00:00 2001 From: David Lathrop Date: Tue, 24 Jan 2012 00:35:57 -0500 Subject: [PATCH 02/14] Renamed github2 to github3 in preparation for the migration to the Github API v3. I also modified the setup.py, setup.cfg and doc/conf.py for this refactoring. Additionally, I made code changes to support some of the API v3 changes in core, client, users, repositories and init.py. --- doc/conf.py | 16 ++++++++-------- setup.cfg | 2 +- setup.py | 18 +++++++++--------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 1fb7895..8b00125 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# python-github2 documentation build configuration file, created by +# python-github3 documentation build configuration file, created by # sphinx-quickstart on Mon Apr 11 16:16:25 2011. # # This file is execfile()d with the current directory set to its containing dir. @@ -45,18 +45,18 @@ master_doc = 'index' # General information about the project. -project = u'github2' +project = u'github3' copyright = u'2009-2012, Ask Solem' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # -import github2 +import github3 # The short X.Y version. -version = ".".join(map(str, github2.VERSION[:2])) +version = ".".join(map(str, github3.VERSION[:2])) # The full version, including alpha/beta/rc tags. -release = github2.__version__ +release = github3.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -172,7 +172,7 @@ #html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'github2doc' +htmlhelp_basename = 'github3doc' # -- Options for LaTeX output -------------------------------------------------- @@ -186,7 +186,7 @@ # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'github2.tex', u'github2 Documentation', + ('index', 'github3.tex', u'github3 Documentation', u'Ask Solem', 'manual'), ] @@ -219,7 +219,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'github2', u'github2 Documentation', + ('index', 'github3', u'github3 Documentation', [u'Ask Solem'], 1) ] diff --git a/setup.cfg b/setup.cfg index 141e45e..fd944b2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,7 +3,7 @@ build_dist = sdist --formats=gztar,bztar,zip [upload_docs] upload-dir = doc/.build/html [nosetests] -cover-package = github2 +cover-package = github3 detailed-errors = 1 with-coverage = 1 [build_sphinx] diff --git a/setup.py b/setup.py index 8613f1f..34eb879 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup, find_packages -import github2 +import github3 install_requires = ['httplib2 >= 0.7.0', ] @@ -22,13 +22,13 @@ + "\n" + codecs.open('NEWS.rst', "r", "utf-8").read()) setup( - name='github2', - version=github2.__version__, - description=github2.__doc__, + name='github3', + version=github3.__version__, + description=github3.__doc__, long_description=long_description, - author=github2.__author__, - author_email=github2.__contact__, - url=github2.__homepage__, + author=github3.__author__, + author_email=github3.__contact__, + url=github3.__homepage__, license='BSD', keywords="git github api", platforms=["any"], @@ -37,8 +37,8 @@ package_data={'': ['*.crt', ], }, entry_points={ 'console_scripts': [ - 'github_manage_collaborators = github2.bin.manage_collaborators:main', - 'github_search_repos = github2.bin.search_repos:main', + 'github_manage_collaborators = github3.bin.manage_collaborators:main', + 'github_search_repos = github3.bin.search_repos:main', ], }, install_requires=install_requires, From d556732077c5e462af14d5ba401bf773b2117d6d Mon Sep 17 00:00:00 2001 From: David Lathrop Date: Tue, 24 Jan 2012 13:27:48 -0500 Subject: [PATCH 03/14] I thought of a better way to list a user's repos, when they are authenticated. --- github3/repositories.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/github3/repositories.py b/github3/repositories.py index 2e27b5c..5f6ca79 100644 --- a/github3/repositories.py +++ b/github3/repositories.py @@ -1,5 +1,5 @@ from github3.core import (BaseData, GithubCommand, Attribute, DateAttribute, - requires_auth) + requires_auth, enhanced_by_auth) from github3.users import User @@ -64,6 +64,7 @@ def pushable(self): return self.get_values("pushable", filter="repositories", datatype=Repository) + @enhanced_by_auth def list(self, user=None, page=1): """Return a list of all repositories for a user. @@ -78,22 +79,15 @@ def list(self, user=None, page=1): user = user or self.request.username temp_domain = self.domain self.domain = 'users' + if self.request.access_token or self.request.api_token: + user=None + self.domain = 'user' + ret_val = self.get_values(user, "repos", filter=None, datatype=Repository, page=page) self.domain = temp_domain return ret_val - @requires_auth - def list_auth(self): - """Returns a list of all repositories for the authenticated user. - """ - temp_domain = self.domain - self.domain = 'user' - ret_val = self.get_values("repos", filter=None, - datatype=Repository) - self.domain = temp_domain - return ret_val - @requires_auth def watch(self, project): """Watch a project From 23d6b5fdcdf525f8ae706c6abb41c4f22e06b2fa Mon Sep 17 00:00:00 2001 From: David Lathrop Date: Tue, 24 Jan 2012 13:44:46 -0500 Subject: [PATCH 04/14] The way I was doing user repo requests was no adequite. It only ever worked for the authenticated user, instead of allowing for a search against the non-authenticated user. --- github3/repositories.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/github3/repositories.py b/github3/repositories.py index 5f6ca79..6a472cf 100644 --- a/github3/repositories.py +++ b/github3/repositories.py @@ -76,12 +76,13 @@ def list(self, user=None, page=1): :param str user: Github user name to list repositories for :param int page: optional page number """ - user = user or self.request.username temp_domain = self.domain - self.domain = 'users' - if self.request.access_token or self.request.api_token: - user=None + if (self.request.access_token or self.request.api_token) and (user is None or user == self.request.username): + user = None self.domain = 'user' + else: + user = user or self.request.username + self.domain = 'users' ret_val = self.get_values(user, "repos", filter=None, datatype=Repository, page=page) From 088aaad4305af50e78474d1a82b2aade99365fa6 Mon Sep 17 00:00:00 2001 From: David Lathrop Date: Wed, 25 Jan 2012 11:09:22 -0500 Subject: [PATCH 05/14] Restoring github2 directory --- .../DigiCert_High_Assurance_EV_Root_CA.crt | 23 ++ github2/__init__.py | 10 + github2/_version.py | 9 + github2/bin/__init__.py | 1 + github2/bin/manage_collaborators.py | 116 +++++++ github2/bin/search_repos.py | 78 +++++ github2/client.py | 121 +++++++ github2/commits.py | 53 +++ github2/core.py | 318 ++++++++++++++++++ github2/issues.py | 186 ++++++++++ github2/organizations.py | 99 ++++++ github2/pull_requests.py | 95 ++++++ github2/repositories.py | 231 +++++++++++++ github2/request.py | 228 +++++++++++++ github2/teams.py | 78 +++++ github2/users.py | 104 ++++++ 16 files changed, 1750 insertions(+) create mode 100644 github2/DigiCert_High_Assurance_EV_Root_CA.crt create mode 100644 github2/__init__.py create mode 100644 github2/_version.py create mode 100644 github2/bin/__init__.py create mode 100755 github2/bin/manage_collaborators.py create mode 100755 github2/bin/search_repos.py create mode 100644 github2/client.py create mode 100644 github2/commits.py create mode 100644 github2/core.py create mode 100644 github2/issues.py create mode 100644 github2/organizations.py create mode 100644 github2/pull_requests.py create mode 100644 github2/repositories.py create mode 100644 github2/request.py create mode 100644 github2/teams.py create mode 100644 github2/users.py diff --git a/github2/DigiCert_High_Assurance_EV_Root_CA.crt b/github2/DigiCert_High_Assurance_EV_Root_CA.crt new file mode 100644 index 0000000..9e6810a --- /dev/null +++ b/github2/DigiCert_High_Assurance_EV_Root_CA.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug +RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm ++9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW +PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM +xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB +Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 +hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg +EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA +FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec +nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z +eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF +hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 +Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep ++OkuE6N36B9K +-----END CERTIFICATE----- diff --git a/github2/__init__.py b/github2/__init__.py new file mode 100644 index 0000000..30d323e --- /dev/null +++ b/github2/__init__.py @@ -0,0 +1,10 @@ +"Github API v2 library for Python" + +from github2 import _version + +VERSION = _version.tuple + +__author__ = "Ask Solem" +__contact__ = "askh@opera.com" +__homepage__ = "http://github.com/ask/python-github2" +__version__ = _version.dotted diff --git a/github2/_version.py b/github2/_version.py new file mode 100644 index 0000000..bf9f4df --- /dev/null +++ b/github2/_version.py @@ -0,0 +1,9 @@ +# This is github2 version 0.6.0 (2011-12-21) +# pylint: disable=C0103, C0111, C0121, W0622 + +dotted = "0.6.0" +libtool = "6:20" +hex = 0x000600 +date = "2011-12-21" +tuple = (0, 6, 0) +web = "github/0.6.0" diff --git a/github2/bin/__init__.py b/github2/bin/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/github2/bin/__init__.py @@ -0,0 +1 @@ +# diff --git a/github2/bin/manage_collaborators.py b/github2/bin/manage_collaborators.py new file mode 100755 index 0000000..3efafee --- /dev/null +++ b/github2/bin/manage_collaborators.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python +# encoding: utf-8 +"""github_manage_collaborators - add/remove collaborators to all github +projects of an account. + +Typically used with company accounts where all coders should have +R/W access to all private repositories of the company account. +""" + +# Created by Maximillian Dornseif on 2009-12-31 for HUDORA. +# Copyright (c) 2009 HUDORA. All rights reserved. +# BSD licensed + +import logging +import sys + +from optparse import OptionParser + +import github2.client + + +#: Running under Python 3 +PY3K = sys.version_info[0] == 3 and True or False + + +def print_(text): + """Python 2 & 3 compatible print function + + We support <2.6, so can't use __future__.print_function""" + if PY3K: + print(text) + else: + sys.stdout.write(text + '\n') + + +def parse_commandline(): + """Parse the comandline and return parsed options.""" + + parser = OptionParser() + parser.description = __doc__ + + parser.set_usage('usage: %prog [options] (list|add|remove) [collaborator].' + '\nTry %prog --help for details.') + parser.add_option('-d', '--debug', action='store_true', + help='Enables debugging mode') + parser.add_option('-c', '--cache', default=None, + help='Location for network cache [default: None]') + parser.add_option('-l', '--login', + help='Username to login with') + parser.add_option('-a', '--account', + help='User owning the repositories to be changed ' \ + '[default: same as --login]') + parser.add_option('-t', '--apitoken', + help='API Token - can be found on the lower right of ' \ + 'https://github.com/account') + + options, args = parser.parse_args() + if len(args) not in [1, 2]: + parser.error('wrong number of arguments') + if (len(args) == 1 and args[0] in ['add', 'remove']): + parser.error('%r needs a collaborator name as second parameter\n' + % args[0]) + elif (len(args) == 1 and args[0] != 'list'): + parser.error('unknown command %r. Try "list", "add" or "remove"\n' + % args[0]) + if (len(args) == 2 and args[0] not in ['add', 'remove']): + parser.error('unknown command %r. Try "list", "add" or "remove"\n' + % args[0]) + if not options.login: + parser.error('you must provide --login information\n') + + return options, args + + +def main(): + """This implements the actual program functionality""" + + options, args = parse_commandline() + + if not options.account: + options.account = options.login + + github = github2.client.Github(username=options.login, + api_token=options.apitoken, + cache=options.cache) + + # PEP-308 conditional expressions are much better, but we're keeping Py2.4 + # compatibility elsewhere. + logging.basicConfig(level=options.debug and logging.DEBUG or logging.WARN, + format="%(asctime)s - %(message)s", + datefmt="%Y-%m-%dT%H:%M:%S") + if len(args) == 1: + for repos in github.repos.list(options.account): + fullreposname = github.project_for_user_repo(options.account, + repos.name) + collabs = github.repos.list_collaborators(fullreposname) + print_("%s: %s" % (repos.name, ' '.join(collabs))) + elif len(args) == 2: + command, collaborator = args + for repos in github.repos.list(options.account): + fullreposname = github.project_for_user_repo(options.account, + repos.name) + if collaborator in github.repos.list_collaborators(fullreposname): + if command == 'remove': + github.repos.remove_collaborator(repos.name, collaborator) + print_("removed %r from %r" % (collaborator, repos.name)) + else: + if command == 'add': + github.repos.add_collaborator(repos.name, collaborator) + print_("added %r to %r" % (collaborator, repos.name)) + + logging.shutdown() + + +if __name__ == '__main__': + main() diff --git a/github2/bin/search_repos.py b/github2/bin/search_repos.py new file mode 100755 index 0000000..c52be46 --- /dev/null +++ b/github2/bin/search_repos.py @@ -0,0 +1,78 @@ +#! /usr/bin/env python +# coding: utf-8 +"""github_search_repos - search for repositories on GitHub""" + + +import logging +import sys + +from optparse import OptionParser +from textwrap import wrap + +import github2.client + + +#: Running under Python 3 +PY3K = sys.version_info[0] == 3 and True or False + + +def print_(text): + """Python 2 & 3 compatible print function + + We support <2.6, so can't use __future__.print_function""" + if PY3K: + print(text) + else: + sys.stdout.write(text + '\n') + + +def parse_commandline(): + """Parse the comandline and return parsed options.""" + + parser = OptionParser() + parser.description = __doc__ + + parser.set_usage('usage: %prog [options] ') + parser.add_option('-d', '--debug', action='store_true', + help='Enables debugging mode') + parser.add_option('-c', '--cache', default=None, + help='Location for network cache [default: None]') + + options, args = parser.parse_args() + if len(args) != 1: + parser.error('wrong number of arguments') + + return options, args[0] + + +def main(): + """This implements the actual program functionality""" + return_value = 0 + + options, term = parse_commandline() + + github = github2.client.Github(cache=options.cache) + + # PEP-308 conditional expressions are much better, but we're keeping Py2.4 + # compatibility elsewhere. + logging.basicConfig(level=options.debug and logging.DEBUG or logging.WARN, + format="%(asctime)s - %(message)s", + datefmt="%Y-%m-%dT%H:%M:%S") + + repos = github.repos.search(term) + if not repos: + print_('No repos found!') + return_value = 255 + else: + for repo in repos: + print(repo.project) + if repo.description: + print_('\n'.join(wrap(repo.description, initial_indent=' ', + subsequent_indent=' '))) + + logging.shutdown() + return return_value + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/github2/client.py b/github2/client.py new file mode 100644 index 0000000..f761b62 --- /dev/null +++ b/github2/client.py @@ -0,0 +1,121 @@ +from github2.request import GithubRequest +from github2.issues import Issues +from github2.repositories import Repositories +from github2.users import Users +from github2.commits import Commits +from github2.organizations import Organizations +from github2.teams import Teams +from github2.pull_requests import PullRequests + + +class Github(object): + + def __init__(self, username=None, api_token=None, requests_per_second=None, + access_token=None, cache=None, proxy_host=None, + proxy_port=8080, github_url=None): + """ + An interface to GitHub's API: + http://develop.github.com/ + + .. versionadded:: 0.2.0 + The ``requests_per_second`` parameter + .. versionadded:: 0.3.0 + The ``cache`` and ``access_token`` parameters + .. versionadded:: 0.4.0 + The ``proxy_host`` and ``proxy_port`` parameters + + :param str username: your own GitHub username. + :param str api_token: can be found at https://github.com/account + (while logged in as that user): + :param str access_token: can be used when no ``username`` and/or + ``api_token`` is used. The ``access_token`` is the OAuth access + token that is received after successful OAuth authentication. + :param float requests_per_second: indicate the API rate limit you're + operating under (1 per second per GitHub at the moment), + or None to disable delays. The default is to disable delays (for + backwards compatibility). + :param str cache: a directory for caching GitHub responses. + :param str proxy_host: the hostname for the HTTP proxy, if needed. + :param str proxy_port: the hostname for the HTTP proxy, if needed (will + default to 8080 if a proxy_host is set and no port is set). + """ + + self.request = GithubRequest(username=username, api_token=api_token, + requests_per_second=requests_per_second, + access_token=access_token, cache=cache, + proxy_host=proxy_host, + proxy_port=proxy_port, + github_url=github_url) + self.issues = Issues(self.request) + self.users = Users(self.request) + self.repos = Repositories(self.request) + self.commits = Commits(self.request) + self.organizations = Organizations(self.request) + self.teams = Teams(self.request) + self.pull_requests = PullRequests(self.request) + + def project_for_user_repo(self, user, repo): + """Return Github identifier for a user's repository + + :param str user: repository owner + :param str repo: repository name + """ + return "/".join([user, repo]) + + def get_all_blobs(self, project, tree_sha): + """Get a list of all blobs for a specific tree + + .. versionadded:: 0.3.0 + + :param str project: GitHub project + :param str tree_sha: object ID of tree + """ + blobs = self.request.get("blob/all", project, tree_sha) + return blobs.get("blobs") + + def get_blob_info(self, project, tree_sha, path): + """Get the blob for a file within a specific tree + + :param str project: GitHub project + :param str tree_sha: object ID of tree + :param str path: path within tree to fetch blob for + """ + blob = self.request.get("blob/show", project, tree_sha, path) + return blob.get("blob") + + def get_tree(self, project, tree_sha): + """Get tree information for a specifc tree + + :param str project: GitHub project + :param str tree_sha: object ID of tree + """ + tree = self.request.get("tree/show", project, tree_sha) + return tree.get("tree", []) + + def get_network_meta(self, project): + """Get Github metadata associated with a project + + :param str project: GitHub project + """ + return self.request.raw_request("/".join([self.request.github_url, + project, + "network_meta"]), {}) + + def get_network_data(self, project, nethash, start=None, end=None): + """Get chunk of Github network data + + :param str project: GitHub project + :param str nethash: identifier provided by ``get_network_meta`` + :param int start: optional start point for data + :param int stop: optional end point for data + """ + data = {"nethash": nethash} + if start: + data["start"] = start + if end: + data["end"] = end + + return self.request.raw_request("/".join([self.request.github_url, + project, + "network_data_chunk"]), + data) diff --git a/github2/commits.py b/github2/commits.py new file mode 100644 index 0000000..8910937 --- /dev/null +++ b/github2/commits.py @@ -0,0 +1,53 @@ +from github2.core import (BaseData, GithubCommand, Attribute, DateAttribute, + repr_string) + + +class Commit(BaseData): + message = Attribute("Commit message.") + parents = Attribute("List of parents for this commit.") + url = Attribute("Canonical URL for this commit.") + author = Attribute("Author metadata (dict with name/email.)") + id = Attribute("Commit ID.") + committed_date = DateAttribute("Date committed.", format="commit") + authored_date = DateAttribute("Date authored.", format="commit") + tree = Attribute("Tree SHA for this commit.") + committer = Attribute("Comitter metadata (dict with name/email.)") + + added = Attribute("(If present) Datastructure representing what's been " + "added since last commit.") + removed = Attribute("(if present) Datastructure representing what's been " + "removed since last commit.") + modified = Attribute("(If present) Datastructure representing what's " + "been modified since last commit.") + + def __repr__(self): + return "" % (self.id[:8], repr_string(self.message)) + + +class Commits(GithubCommand): + domain = "commits" + + def list(self, project, branch="master", file=None, page=1): + """List commits on a project + + .. warning:: + Not all projects use ``master`` as their default branch, you can + check the value of the ``Repo(project).master_branch`` attribute to + determine the default branch of a given repository. + + :param str project: project name + :param str branch: branch name, or ``master`` if not given + :param str file: optional file filter + :param int page: optional page number + """ + return self.get_values("list", project, branch, file, filter="commits", + datatype=Commit, page=page) + + def show(self, project, sha): + """Get a specific commit + + :param str project: project name + :param str sha: commit id + """ + return self.get_value("show", project, sha, + filter="commit", datatype=Commit) diff --git a/github2/core.py b/github2/core.py new file mode 100644 index 0000000..60da833 --- /dev/null +++ b/github2/core.py @@ -0,0 +1,318 @@ +import logging +import sys + +from datetime import datetime +from dateutil import (parser, tz) + + +#: Logger for core module +LOGGER = logging.getLogger('github2.core') + +#: Running under Python 3 +PY3K = sys.version_info[0] == 3 + +#: Running under Python 2.7, or newer +PY27 = sys.version_info[:2] >= (2, 7) + +GITHUB_DATE_FORMAT = "%Y/%m/%d %H:%M:%S %z" +# We need to manually mangle the timezone for commit date formatting because it +# uses -xx:xx format +COMMIT_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S" +#: GitHub timezone used in API output +GITHUB_TZ = tz.gettz("America/Los_Angeles") + +#: Operate on naive :class:`datetime.datetime` objects, this is the default +#: for backwards compatibility +NAIVE = True + + +def string_to_datetime(string): + """Convert a string to Python datetime + + :param str github_date: date string to parse + """ + parsed = parser.parse(string) + if NAIVE: + parsed = parsed.replace(tzinfo=None) + return parsed + + +def _handle_naive_datetimes(f): + """Decorator to make datetime arguments use GitHub timezone + + :param func f: Function to wrap + """ + def wrapper(datetime_): + if not datetime_.tzinfo: + datetime_ = datetime_.replace(tzinfo=GITHUB_TZ) + else: + datetime_ = datetime_.astimezone(GITHUB_TZ) + return f(datetime_) + wrapped = wrapper + wrapped.__name__ = f.__name__ + wrapped.__doc__ = ( + f.__doc__ + + """\n .. note:: Supports naive and timezone-aware datetimes""" + ) + return wrapped + + +@_handle_naive_datetimes +def datetime_to_ghdate(datetime_): + """Convert Python datetime to Github date string + + :param datetime datetime_: datetime object to convert + """ + return datetime_.strftime(GITHUB_DATE_FORMAT) + + +@_handle_naive_datetimes +def datetime_to_commitdate(datetime_): + """Convert Python datetime to Github date string + + :param datetime datetime_: datetime object to convert + """ + date_without_tz = datetime_.strftime(COMMIT_DATE_FORMAT) + utcoffset = GITHUB_TZ.utcoffset(datetime_) + hours, minutes = divmod(utcoffset.days * 86400 + utcoffset.seconds, 3600) + + return "".join([date_without_tz, "%+03d:%02d" % (hours, minutes)]) + + +def datetime_to_isodate(datetime_): + """Convert Python datetime to Github date string + + :param str datetime_: datetime object to convert + + .. note:: Supports naive and timezone-aware datetimes + """ + if not datetime_.tzinfo: + datetime_ = datetime_.replace(tzinfo=tz.tzutc()) + else: + datetime_ = datetime_.astimezone(tz.tzutc()) + return "%sZ" % datetime_.isoformat()[:-6] + + +class AuthError(Exception): + """Requires authentication""" + + +def requires_auth(f): + """Decorate to check a function call for authentication + + Sets a ``requires_auth`` attribute on functions, for use in introspection. + + :param func f: Function to wrap + :raises AuthError: If function called without an authenticated session + """ + # When Python 2.4 support is dropped move straight to functools.wraps, + # don't pass go and don't collect $200. + def wrapper(self, *args, **kwargs): + if not self.request.access_token and not self.request.api_token: + raise AuthError("%r requires an authenticated session" + % f.__name__) + return f(self, *args, **kwargs) + wrapped = wrapper + wrapped.__name__ = f.__name__ + wrapped.__doc__ = f.__doc__ + """\n.. warning:: Requires authentication""" + wrapped.requires_auth = True + return wrapped + + +def enhanced_by_auth(f): + """Decorator to mark a function as enhanced by authentication + + Sets a ``enhanced_by_auth`` attribute on functions, for use in + introspection. + + :param func f: Function to wrap + """ + f.enhanced_by_auth = True + f.__doc__ += """\n.. note:: This call is enhanced with authentication""" + return f + + +class GithubCommand(object): + + def __init__(self, request): + self.request = request + + def make_request(self, command, *args, **kwargs): + filter = kwargs.get("filter") + post_data = kwargs.get("post_data") or {} + page = kwargs.pop("page", 1) + if page and not page == 1: + post_data["page"] = page + method = kwargs.get("method", "GET").upper() + if method == "POST" or method == "GET" and post_data: + response = self.request.post(self.domain, command, *args, + **post_data) + elif method == "PUT": + response = self.request.put(self.domain, command, *args, + **post_data) + elif method == "DELETE": + response = self.request.delete(self.domain, command, *args, + **post_data) + else: + response = self.request.get(self.domain, command, *args) + if filter: + return response[filter] + return response + + def get_value(self, *args, **kwargs): + datatype = kwargs.pop("datatype", None) + value = self.make_request(*args, **kwargs) + if datatype: + if not PY27: + # unicode keys are not accepted as kwargs by python, until 2.7: + # http://bugs.python.org/issue2646 + # So we make a local dict with the same keys but as strings: + return datatype(**dict((str(k), v) + for (k, v) in value.items())) + else: + return datatype(**value) + return value + + def get_values(self, *args, **kwargs): + datatype = kwargs.pop("datatype", None) + values = self.make_request(*args, **kwargs) + if datatype: + if not PY27: + # Same as above, unicode keys will blow up in **args, so we + # need to create a new 'values' dict with string keys + return [datatype(**dict((str(k), v) + for (k, v) in value.items())) + for value in values] + else: + return [datatype(**value) for value in values] + else: + return values + + +def doc_generator(docstring, attributes): + """Utility function to augment BaseDataType docstring + + :param str docstring: docstring to augment + :param dict attributes: attributes to add to docstring + """ + docstring = docstring or "" + + def bullet(title, text): + return """.. attribute:: %s\n\n %s\n""" % (title, text) + + b = "\n".join([bullet(attr_name, attr.help) + for attr_name, attr in attributes.items()]) + return "\n\n".join([docstring, b]) + + +class Attribute(object): + + def __init__(self, help): + self.help = help + + def to_python(self, value): + return value + + from_python = to_python + + +class DateAttribute(Attribute): + format = "github" + converter_for_format = { + "github": datetime_to_ghdate, + "commit": datetime_to_commitdate, + "user": datetime_to_ghdate, + "iso": datetime_to_isodate, + } + + def __init__(self, *args, **kwargs): + self.format = kwargs.pop("format", self.format) + super(DateAttribute, self).__init__(*args, **kwargs) + + def to_python(self, value): + if value and not isinstance(value, datetime): + return string_to_datetime(value) + return value + + def from_python(self, value): + if value and isinstance(value, datetime): + return self.converter_for_format[self.format](value) + return value + + +class BaseDataType(type): + + def __new__(cls, name, bases, attrs): + super_new = super(BaseDataType, cls).__new__ + + _meta = dict([(attr_name, attr_value) + for attr_name, attr_value in attrs.items() + if isinstance(attr_value, Attribute)]) + attrs["_meta"] = _meta + attributes = _meta.keys() + attrs.update(dict([(attr_name, None) + for attr_name in attributes])) + + def _contribute_method(name, func): + func.__name__ = name + attrs[name] = func + + def constructor(self, **kwargs): + for attr_name, attr_value in kwargs.items(): + attr = self._meta.get(attr_name) + if attr: + setattr(self, attr_name, attr.to_python(attr_value)) + else: + setattr(self, attr_name, attr_value) + _contribute_method("__init__", constructor) + + def iterate(self): + not_empty = lambda e: e[1] is not None + return iter(filter(not_empty, vars(self).items())) + _contribute_method("__iter__", iterate) + + result_cls = super_new(cls, name, bases, attrs) + result_cls.__doc__ = doc_generator(result_cls.__doc__, _meta) + return result_cls + + +# Ugly base class definition for Python 2 and 3 compatibility, where metaclass +# syntax is incompatible +class BaseData(BaseDataType('BaseData', (object, ), {})): + def __getitem__(self, key): + """Access objects's attribute using subscript notation + + This is here purely to maintain compatibility when switching ``dict`` + responses to ``BaseData`` derived objects. + """ + LOGGER.warning("Subscript access on %r is deprecated, use object " + "attributes" % self.__class__.__name__, + DeprecationWarning) + if not key in self._meta.keys(): + raise KeyError(key) + return getattr(self, key) + + def __setitem__(self, key, value): + """Update object's attribute using subscript notation + + :see: ``BaseData.__getitem__`` + """ + LOGGER.warning("Subscript access on %r is deprecated, use object " + "attributes" % self.__class__.__name__, + DeprecationWarning) + if not key in self._meta.keys(): + raise KeyError(key) + setattr(self, key, value) + + +def repr_string(string): + """Shorten string for use in repr() output + + :param str string: string to operate on + :return: string, with maximum length of 20 characters + """ + if len(string) > 20: + string = string[:17] + '...' + if not PY3K: + string.decode('utf-8') + return string diff --git a/github2/issues.py b/github2/issues.py new file mode 100644 index 0000000..ae9723b --- /dev/null +++ b/github2/issues.py @@ -0,0 +1,186 @@ +try: + from urllib.parse import quote_plus # For Python 3 +except ImportError: + from urllib import quote_plus + +from github2.core import (GithubCommand, BaseData, Attribute, DateAttribute, + repr_string, requires_auth) + + +class Issue(BaseData): + position = Attribute("The position of this issue in a list.") + number = Attribute("The issue number (unique for project).") + votes = Attribute("Number of votes for this issue.") + body = Attribute("The full description for this issue.") + title = Attribute("Issue title.") + user = Attribute("The username of the user that created this issue.") + state = Attribute("State of this issue. Can be ``open`` or ``closed``.") + labels = Attribute("Labels associated with this issue.") + created_at = DateAttribute("The date this issue was created.") + closed_at = DateAttribute("The date this issue was closed.") + updated_at = DateAttribute("The date when this issue was last updated.") + diff_url = Attribute("URL for diff output associated with this issue.") + patch_url = Attribute("URL for format-patch associated with this issue.") + pull_request_url = Attribute("URL for the issue's related pull request.") + + def __repr__(self): + return "" % repr_string(self.title) + + +class Comment(BaseData): + created_at = DateAttribute("The date this comment was created.") + updated_at = DateAttribute("The date when this comment was last updated.") + body = Attribute("The full text of this comment.") + id = Attribute("The comment id.") + user = Attribute("The username of the user that created this comment.") + + def __repr__(self): + return "" % repr_string(self.body) + + +class Issues(GithubCommand): + domain = "issues" + + def search(self, project, term, state="open"): + """Get all issues for project that match term with given state. + + .. versionadded:: 0.3.0 + + :param str project: GitHub project + :param str term: term to search issues for + :param str state: can be either ``open`` or ``closed``. + """ + return self.get_values("search", project, state, quote_plus(term), + filter="issues", datatype=Issue) + + def list(self, project, state="open"): + """Get all issues for project with given state. + + :param str project: GitHub project + :param str state: can be either ``open`` or ``closed``. + """ + return self.get_values("list", project, state, filter="issues", + datatype=Issue) + + def list_by_label(self, project, label): + """Get all issues for project with label. + + .. versionadded:: 0.3.0 + + :param str project: GitHub project + :param str label: a string representing a label (e.g., ``bug``). + """ + return self.get_values("list", project, "label", label, + filter="issues", datatype=Issue) + + def list_labels(self, project): + """Get all labels for project. + + .. versionadded:: 0.3.0 + + :param str project: GitHub project + """ + return self.get_values("labels", project, filter="labels") + + def show(self, project, number): + """Get all the data for issue by issue-number. + + :param str project: GitHub project + :param int number: issue number in the Github database + """ + return self.get_value("show", project, str(number), + filter="issue", datatype=Issue) + + @requires_auth + def open(self, project, title, body): + """Open up a new issue. + + :param str project: GitHub project + :param str title: title for issue + :param str body: body for issue + """ + issue_data = {"title": title, "body": body} + return self.get_value("open", project, post_data=issue_data, + filter="issue", datatype=Issue) + + @requires_auth + def close(self, project, number): + """Close an issue + + :param str project: GitHub project + :param int number: issue number in the Github database + """ + return self.get_value("close", project, str(number), filter="issue", + datatype=Issue, method="POST") + + @requires_auth + def reopen(self, project, number): + """Reopen a closed issue + + .. versionadded:: 0.3.0 + + :param str project: GitHub project + :param int number: issue number in the Github database + """ + return self.get_value("reopen", project, str(number), filter="issue", + datatype=Issue, method="POST") + + @requires_auth + def edit(self, project, number, title, body): + """Edit an existing issue + + .. versionadded:: 0.3.0 + + :param str project: GitHub project + :param int number: issue number in the Github database + :param str title: title for issue + :param str body: body for issue + """ + issue_data = {"title": title, "body": body} + return self.get_value("edit", project, str(number), + post_data=issue_data, filter="issue", + datatype=Issue) + + @requires_auth + def add_label(self, project, number, label): + """Add a label to an issue + + :param str project: GitHub project + :param int number: issue number in the Github database + :param str label: label to attach to issue + """ + return self.get_values("label/add", project, label, str(number), + filter="labels", method="POST") + + @requires_auth + def remove_label(self, project, number, label): + """Remove an existing label from an issue + + :param str project: GitHub project + :param int number: issue number in the Github database + :param str label: label to remove from issue + """ + return self.get_values("label/remove", project, label, str(number), + filter="labels", method="POST") + + @requires_auth + def comment(self, project, number, comment): + """Comment on an issue. + + :param str project: GitHub project + :param int number: issue number in the Github database + :param str comment: comment to attach to issue + """ + comment_data = {'comment': comment} + return self.get_value("comment", project, str(number), + post_data=comment_data, filter='comment', + datatype=Comment) + + def comments(self, project, number): + """View comments on an issue. + + :param str project: GitHub project + :param int number: issue number in the Github database + """ + return self.get_values("comments", project, str(number), + filter="comments", datatype=Comment) diff --git a/github2/organizations.py b/github2/organizations.py new file mode 100644 index 0000000..0c8aa00 --- /dev/null +++ b/github2/organizations.py @@ -0,0 +1,99 @@ +from github2.core import (BaseData, GithubCommand, Attribute, DateAttribute, + requires_auth) +from github2.repositories import Repository +from github2.teams import Team +from github2.users import User + + +class Organization(BaseData): + """.. versionadded:: 0.4.0""" + id = Attribute("The organization id.") + name = Attribute("The full name of the organization.") + blog = Attribute("The organization's blog.") + location = Attribute("Location of the organization.") + gravatar_id = Attribute("Gravatar ID.") + login = Attribute("The login username.") + email = Attribute("The organization's e-mail address.") + company = Attribute("The organization's company name.") + created_at = DateAttribute("The date the organization was created.", + format="commit") + following_count = Attribute("Number of users the organization is following.") + followers_count = Attribute("Number of users following this organization.") + public_gist_count = Attribute("Organization's number of active public gists.") + public_repo_count = Attribute("Organization's number of active repositories.") + permission = Attribute("Permissions within this organization.") + plan = Attribute("GitHub plan for this organization.") + + def is_authenticated(self): + return self.plan is not None + + def __repr__(self): + return "" % self.login + + +class Organizations(GithubCommand): + """.. versionadded:: 0.4.0""" + domain = "organizations" + + def show(self, organization): + """Get information on organization + + :param str organization: organization to show + """ + return self.get_value(organization, filter="organization", + datatype=Organization) + + def list(self): + """Get list of all of your organizations""" + return self.get_values('', filter="organizations", + datatype=Organization) + + def repositories(self, organization=''): + """Get list of all repositories in an organization + + If organization is not given, or is empty, then this will list + repositories for all organizations the authenticated user belongs to. + + :param: str organization: organization to list repositories for + """ + return self.get_values(organization, 'repositories', + filter="repositories", datatype=Repository) + + def public_repositories(self, organization): + """Get list of public repositories in an organization + + :param str organization: organization to list public repositories for + """ + return self.get_values(organization, 'public_repositories', + filter="repositories", datatype=Repository) + + def public_members(self, organization): + """Get list of public members in an organization + + :param str organization: organization to list members for + """ + return self.get_values(organization, 'public_members', filter="users", + datatype=User) + + def teams(self, organization): + """Get list of teams in an organization + + :param str organization: organization to list teams for + """ + return self.get_values(organization, 'teams', filter="teams", + datatype=Team) + + @requires_auth + def add_team(self, organization, name, permission='pull', projects=None): + """Add a team to an organization + + :param str organization: organization to add team to + :param str team: name of team to add + :param str permission: permissions for team(push, pull or admin) + :param list projects: optional GitHub projects for this team + """ + team_data = {'team[name]': name, 'team[permission]': permission} + if projects: + team_data['team[repo_names][]'] = projects + return self.get_value(organization, 'teams', post_data=team_data, + method='POST', filter='team', datatype=Team) diff --git a/github2/pull_requests.py b/github2/pull_requests.py new file mode 100644 index 0000000..3573f75 --- /dev/null +++ b/github2/pull_requests.py @@ -0,0 +1,95 @@ +from github2.core import (BaseData, GithubCommand, Attribute, DateAttribute, + repr_string) + + +class PullRequest(BaseData): + """Pull request encapsulation + + .. versionadded:: 0.5.0 + """ + state = Attribute("The pull request state") + base = Attribute("The base repo") + head = Attribute("The head of the pull request") + issue_user = Attribute("The user who created the pull request.") + user = Attribute("The owner of the repo.") + title = Attribute("The text of the pull request title.") + body = Attribute("The text of the body.") + position = Attribute("Floating point position of the pull request.") + number = Attribute("Number of this request.") + votes = Attribute("Number of votes for this request.") + comments = Attribute("Number of comments made on this request.") + diff_url = Attribute("The URL to the unified diff.") + patch_url = Attribute("The URL to the downloadable patch.") + labels = Attribute("A list of labels attached to the pull request.") + html_url = Attribute("The URL to the pull request.") + issue_created_at = DateAttribute("The date the issue for this pull request was opened.", + format='iso') + issue_updated_at = DateAttribute("The date the issue for this pull request was last updated.", + format='iso') + created_at = DateAttribute("The date when this pull request was created.", + format='iso') + updated_at = DateAttribute("The date when this pull request was last updated.", + format='iso') + closed_at = DateAttribute("The date when this pull request was closed", + format='iso') + discussion = Attribute("Discussion thread for the pull request.") + mergeable = Attribute("Whether the pull request can be merge cleanly") + + def __repr__(self): + return "" % repr_string(self.title) + + +class PullRequests(GithubCommand): + """Operations on pull requests + + .. versionadded:: 0.5.0 + """ + domain = "pulls" + + def create(self, project, base, head, title=None, body=None, issue=None): + """Create a new pull request + + Pull requests can be created from scratch, or attached to an existing + issue. If an ``issue`` parameter is supplied the pull request is + attached to that issue, else a new pull request is created. + + :param str project: the Github project to send the pull request to + :param str base: branch changes should be pulled into + :param str head: branch of the changes to be pulled + :param str title: title for pull request + :param str body: optional body for pull request + :param str issue: existing issue to attach pull request to + """ + post_data = {"base": base, "head": head} + if issue: + post_data["issue"] = issue + elif title: + post_data["title"] = title + if body: + post_data["body"] = body + else: + raise TypeError("You must either specify a title for the " + "pull request or an issue number to which the " + "pull request should be attached.") + pull_request_data = [("pull[%s]" % k, v) for k, v in post_data.items()] + return self.get_value(project, post_data=dict(pull_request_data), + filter="pull", datatype=PullRequest) + + def show(self, project, number): + """Show a single pull request + + :param str project: Github project + :param int number: pull request number in the Github database + """ + return self.get_value(project, str(number), filter="pull", + datatype=PullRequest) + + def list(self, project, state="open", page=1): + """List all pull requests for a project + + :param str project: Github project + :param str state: can be either ``open`` or ``closed`` + :param int page: optional page number + """ + return self.get_values(project, state, filter="pulls", + datatype=PullRequest, page=page) diff --git a/github2/repositories.py b/github2/repositories.py new file mode 100644 index 0000000..cbd9ad4 --- /dev/null +++ b/github2/repositories.py @@ -0,0 +1,231 @@ +from github2.core import (BaseData, GithubCommand, Attribute, DateAttribute, + requires_auth) + +from github2.users import User + + +class Repository(BaseData): + name = Attribute("Name of repository.") + description = Attribute("Repository description.") + forks = Attribute("Number of forks of this repository.") + watchers = Attribute("Number of people watching this repository.") + private = Attribute("If True, the repository is private.") + url = Attribute("Canonical URL to this repository") + fork = Attribute("If True, this is a fork of another repository.") + owner = Attribute("Username of the user owning this repository.") + homepage = Attribute("Homepage for this project.") + master_branch = Attribute("Default branch, if set.") + integration_branch = Attribute("Integration branch, if set.") + open_issues = Attribute("List of open issues for this repository.") + created_at = DateAttribute("Datetime the repository was created.") + pushed_at = DateAttribute("Datetime of the last push to this repository") + has_downloads = Attribute("If True, this repository has downloads.") + has_wiki = Attribute("If True, this repository has a wiki.") + has_issues = Attribute("If True, this repository has an issue tracker.") + language = Attribute("Primary language for the repository.") + parent = Attribute("The parent project of this fork.") + + def _project(self): + return self.owner + "/" + self.name + project = property(_project) + + def __repr__(self): + return "" % self.project + + +class Repositories(GithubCommand): + domain = "repos" + + def search(self, query): + """Get all repositories that match term. + + .. warning: + Returns at most 100 repositories + + :param str query: term to search issues for + """ + return self.get_values("search", query, filter="repositories", + datatype=Repository) + + def show(self, project): + """Get repository object for project. + + :param str project: GitHub project + """ + return self.get_value("show", project, filter="repository", + datatype=Repository) + + @requires_auth + def pushable(self): + """Return a list of repos you can push to that are not your own. + + .. versionadded:: 0.3.0 + """ + return self.get_values("pushable", filter="repositories", + datatype=Repository) + + def list(self, user=None, page=1): + """Return a list of all repositories for a user. + + .. deprecated: 0.4.0 + Previous releases would attempt to display repositories for the + logged-in user when ``user`` wasn't supplied. This functionality is + brittle and will be removed in a future release! + + :param str user: Github user name to list repositories for + :param int page: optional page number + """ + user = user or self.request.username + return self.get_values("show", user, filter="repositories", + datatype=Repository, page=page) + + @requires_auth + def watch(self, project): + """Watch a project + + :param str project: GitHub project + """ + return self.get_value("watch", project, filter='repository', + datatype=Repository) + + @requires_auth + def unwatch(self, project): + """Unwatch a project + + :param str project: GitHub project + """ + return self.get_value("unwatch", project, filter='repository', + datatype=Repository) + + @requires_auth + def fork(self, project): + """Fork a project + + :param str project: GitHub project + """ + return self.get_value("fork", project, filter="repository", + datatype=Repository) + + @requires_auth + def create(self, project, description=None, homepage=None, public=True): + """Create a repository + + :param str project: new project name + :param str description: optional project description + :param str homepage: optional project homepage + :param bool public: whether to make a public project + """ + repo_data = {"name": project, "description": description, + "homepage": homepage, "public": str(int(public))} + return self.get_value("create", post_data=repo_data, + filter="repository", datatype=Repository) + + @requires_auth + def delete(self, project): + """Delete a repository + + :param str project: project name to delete + """ + # Two-step delete mechanism. We must echo the delete_token value back + # to GitHub to actually delete a repository + result = self.make_request("delete", project, method="POST") + self.make_request("delete", project, post_data=result) + + @requires_auth + def set_private(self, project): + """Mark repository as private + + :param str project: project name to set as private + """ + return self.make_request("set/private", project) + + @requires_auth + def set_public(self, project): + """Mark repository as public + + :param str project: project name to set as public + """ + return self.make_request("set/public", project) + + def list_collaborators(self, project): + """Lists all the collaborators in a project + + :param str project: GitHub project + """ + return self.get_values("show", project, "collaborators", + filter="collaborators") + + @requires_auth + def add_collaborator(self, project, username): + """Adds an add_collaborator to a repo + + :param str project: Github project + :param str username: Github user to add as collaborator + """ + return self.make_request("collaborators", project, "add", username, + method="POST") + + @requires_auth + def remove_collaborator(self, project, username): + """Removes an add_collaborator from a repo + + :param str project: Github project + :param str username: Github user to add as collaborator + """ + return self.make_request("collaborators", project, "remove", + username, method="POST") + + def network(self, project): + """Get network data for project + + :param str project: Github project + """ + return self.get_values("show", project, "network", filter="network", + datatype=Repository) + + def languages(self, project): + """Get programming language data for project + + :param str project: Github project + """ + return self.get_values("show", project, "languages", + filter="languages") + + def tags(self, project): + """Get tags for project + + :param str project: Github project + """ + return self.get_values("show", project, "tags", filter="tags") + + def branches(self, project): + """Get branch names for project + + :param str project: Github project + """ + return self.get_values("show", project, "branches", filter="branches") + + def watchers(self, project): + """Get list of watchers for project + + :param str project: Github project + """ + return self.get_values("show", project, "watchers", filter="watchers") + + def watching(self, for_user=None, page=None): + """Lists all the repos a user is watching + + :param str for_user: optional Github user name to list repositories for + :param int page: optional page number + """ + for_user = for_user or self.request.username + return self.get_values("watched", for_user, filter="repositories", + datatype=Repository, page=page) + + def list_contributors(self, project): + """Lists all the contributors in a project + + :param str project: Github project + """ + return self.get_values("show", project, "contributors", + filter="contributors", datatype=User) diff --git a/github2/request.py b/github2/request.py new file mode 100644 index 0000000..d6190e2 --- /dev/null +++ b/github2/request.py @@ -0,0 +1,228 @@ +import datetime +import logging +import re +import sys +import time + +try: + # For Python 3 + from http.client import responses +except ImportError: # For Python 2.5-2.7 + try: + from httplib import responses + except ImportError: # For Python 2.4 + from BaseHTTPServer import BaseHTTPRequestHandler + responses = dict([(k, v[0]) + for k, v in BaseHTTPRequestHandler.responses.items()]) +try: + import json as simplejson # For Python 2.6+ +except ImportError: + import simplejson +from os import path +try: + # For Python 3 + from urllib.parse import (parse_qs, quote, urlencode, urlsplit, urlunsplit) +except ImportError: + from urlparse import (urlsplit, urlunsplit) + try: + from urlparse import parse_qs + except ImportError: + from cgi import parse_qs + from urllib import urlencode, quote + +import httplib2 + + +#: Hostname for API access +DEFAULT_GITHUB_URL = "https://github.com" + +#: Logger for requests module +LOGGER = logging.getLogger('github2.request') + +#: Whether github2 is using the system's certificates for SSL connections +SYSTEM_CERTS = not httplib2.CA_CERTS.startswith(path.dirname(httplib2.__file__)) +if not SYSTEM_CERTS and sys.platform.startswith('linux'): + for cert_file in ['/etc/ssl/certs/ca-certificates.crt', + '/etc/pki/tls/certs/ca-bundle.crt']: + if path.exists(cert_file): + CA_CERTS = cert_file + SYSTEM_CERTS = True + break +elif not SYSTEM_CERTS and sys.platform.startswith('freebsd'): + if path.exists('/usr/local/share/certs/ca-root-nss.crt'): + CA_CERTS = '/usr/local/share/certs/ca-root-nss.crt' + SYSTEM_CERTS = True +else: + CA_CERTS = path.join(path.dirname(path.abspath(__file__)), + "DigiCert_High_Assurance_EV_Root_CA.crt") + + +# Common missing entries from the HTTP status code dict, basically anything +# GitHub reports that isn't basic HTTP/1.1. +responses[422] = 'Unprocessable Entity' + + +def charset_from_headers(headers): + """Parse charset from headers + + :param httplib2.Response headers: Request headers + :return: Defined encoding, or default to ASCII + """ + match = re.search("charset=([^ ;]+)", headers.get('content-type', "")) + if match: + charset = match.groups()[0] + else: + charset = "ascii" + return charset + + +class GithubError(Exception): + """An error occured when making a request to the Github API.""" + + +class HttpError(RuntimeError): + """A HTTP error occured when making a request to the Github API.""" + def __init__(self, message, content, code): + """Create a HttpError exception + + :param str message: Exception string + :param str content: Full content of HTTP request + :param int code: HTTP status code + """ + self.args = (message, content, code) + self.message = message + self.content = content + self.code = code + if code in responses: + self.code_reason = responses[code] + else: + self.code_reason = "" + LOGGER.warning('Unknown HTTP status %r, please file an issue', code) + + +class GithubRequest(object): + url_format = "%(github_url)s/api/%(api_version)s/%(api_format)s" + api_version = "v2" + api_format = "json" + GithubError = GithubError + + def __init__(self, username=None, api_token=None, url_prefix=None, + requests_per_second=None, access_token=None, + cache=None, proxy_host=None, proxy_port=None, + github_url=None): + """Make an API request. + + :see: :class:`github2.client.Github` + """ + self.username = username + self.api_token = api_token + self.access_token = access_token + self.url_prefix = url_prefix + if github_url is None: + self.github_url = DEFAULT_GITHUB_URL + else: + self.github_url = github_url + if requests_per_second is None: + self.delay = 0 + else: + self.delay = 1.0 / requests_per_second + self.last_request = datetime.datetime(1900, 1, 1) + if not self.url_prefix: + self.url_prefix = self.url_format % { + "github_url": self.github_url, + "api_version": self.api_version, + "api_format": self.api_format, + } + if proxy_host is None: + self._http = httplib2.Http(cache=cache) + else: + proxy_info = httplib2.ProxyInfo(httplib2.socks.PROXY_TYPE_HTTP, + proxy_host, proxy_port) + self._http = httplib2.Http(proxy_info=proxy_info, cache=cache) + self._http.ca_certs = CA_CERTS + if SYSTEM_CERTS: + LOGGER.info('Using system certificates in %r', CA_CERTS) + else: + LOGGER.warning('Using bundled certificate for HTTPS connections') + + def encode_authentication_data(self, extra_post_data): + post_data = [] + if self.access_token: + post_data.append(("access_token", self.access_token)) + elif self.username and self.api_token: + post_data.append(("login", self.username)) + post_data.append(("token", self.api_token)) + for key, value in extra_post_data.items(): + if isinstance(value, list): + for elem in value: + post_data.append((key, elem)) + else: + post_data.append((key, value)) + return urlencode(post_data) + + def get(self, *path_components): + path_components = filter(None, path_components) + return self.make_request("/".join(path_components)) + + def post(self, *path_components, **extra_post_data): + path_components = filter(None, path_components) + return self.make_request("/".join(path_components), extra_post_data, + method="POST") + + def put(self, *path_components, **extra_post_data): + path_components = filter(None, path_components) + return self.make_request("/".join(path_components), extra_post_data, + method="PUT") + + def delete(self, *path_components, **extra_post_data): + path_components = filter(None, path_components) + return self.make_request("/".join(path_components), extra_post_data, + method="DELETE") + + def make_request(self, path, extra_post_data=None, method="GET"): + if self.delay: + since_last = (datetime.datetime.utcnow() - self.last_request) + if since_last.days == 0 and since_last.seconds < self.delay: + duration = self.delay - since_last.seconds + LOGGER.warning("delaying API call %g second(s)", duration) + time.sleep(duration) + + extra_post_data = extra_post_data or {} + url = "/".join([self.url_prefix, quote(path)]) + result = self.raw_request(url, extra_post_data, method=method) + + if self.delay: + self.last_request = datetime.datetime.utcnow() + return result + + def raw_request(self, url, extra_post_data, method="GET"): + scheme, netloc, path, query, fragment = urlsplit(url) + post_data = None + headers = self.http_headers + method = method.upper() + if extra_post_data or method == "POST": + post_data = self.encode_authentication_data(extra_post_data) + headers["Content-Length"] = str(len(post_data)) + else: + query = self.encode_authentication_data(parse_qs(query)) + url = urlunsplit((scheme, netloc, path, query, fragment)) + response, content = self._http.request(url, method, post_data, headers) + if LOGGER.isEnabledFor(logging.DEBUG): + logging.debug("URL: %r POST_DATA: %r RESPONSE_TEXT: %r", url, + post_data, content) + if response.status >= 400: + raise HttpError("Unexpected response from github.com %d: %r" + % (response.status, content), content, + response.status) + json = simplejson.loads(content.decode(charset_from_headers(response))) + if json.get("error"): + raise self.GithubError(json["error"][0]["error"]) + + return json + + @property + def http_headers(self): + return { + "User-Agent": "pygithub2 v1", + "Accept": "application/json", + } diff --git a/github2/teams.py b/github2/teams.py new file mode 100644 index 0000000..b46116f --- /dev/null +++ b/github2/teams.py @@ -0,0 +1,78 @@ +from github2.core import BaseData, GithubCommand, Attribute, requires_auth +from github2.repositories import Repository +from github2.users import User + + +class Team(BaseData): + """.. versionadded:: 0.4.0""" + id = Attribute("The team id") + name = Attribute("Name of the team") + permission = Attribute("Permissions of the team") + + def __repr__(self): + return "" % self.name + + +class Teams(GithubCommand): + """.. versionadded:: 0.4.0""" + domain = "teams" + + def show(self, team_id): + """Get information on team_id + + :param int team_id: team to get information for + """ + return self.get_value(str(team_id), filter="team", datatype=Team) + + def members(self, team_id): + """Get list of all team members + + :param int team_id: team to get information for + """ + return self.get_values(str(team_id), "members", filter="users", + datatype=User) + + @requires_auth + def add_member(self, team_id, username): + """Add a new member to a team + + :param int team_id: team to add new member to + :param str username: GitHub username to add to team + """ + return self.get_values(str(team_id), 'members', method='POST', + post_data={'name': username}, filter='users', + datatype=User) + + def repositories(self, team_id): + """Get list of all team repositories + + :param int team_id: team to get information for + """ + return self.get_values(str(team_id), "repositories", + filter="repositories", datatype=Repository) + + @requires_auth + def add_project(self, team_id, project): + """Add a project to a team + + :param int team_id: team to add repository to + :param str project: GitHub project + """ + if isinstance(project, Repository): + project = project.project + return self.get_values(str(team_id), "repositories", method="POST", + post_data={'name': project}, + filter="repositories", datatype=Repository) + + @requires_auth + def remove_project(self, team_id, project): + """Remove a project to a team + + :param int team_id: team to remove project from + :param str project: GitHub project + """ + if isinstance(project, Repository): + project = project.project + return self.get_values(str(team_id), "repositories", method="DELETE", + post_data={'name': project}, + filter="repositories", datatype=Repository) diff --git a/github2/users.py b/github2/users.py new file mode 100644 index 0000000..7dba3e6 --- /dev/null +++ b/github2/users.py @@ -0,0 +1,104 @@ +try: + from urllib.parse import quote_plus # For Python 3 +except ImportError: + from urllib import quote_plus + +from github2.core import (BaseData, GithubCommand, DateAttribute, Attribute, + enhanced_by_auth, requires_auth) + + +class User(BaseData): + id = Attribute("The user id") + login = Attribute("The login username") + name = Attribute("The users full name") + company = Attribute("Name of the company the user is associated with") + location = Attribute("Location of the user") + email = Attribute("The users e-mail address") + blog = Attribute("The users blog") + following_count = Attribute("Number of other users the user is following") + followers_count = Attribute("Number of users following this user") + public_gist_count = Attribute( + "Number of active public gists owned by the user") + public_repo_count = Attribute( + "Number of active repositories owned by the user") + total_private_repo_count = Attribute("Number of private repositories") + collaborators = Attribute("Number of collaborators") + disk_usage = Attribute("Currently used disk space") + owned_private_repo_count = Attribute("Number of privately owned repos") + private_gist_count = Attribute( + "Number of private gists owned by the user") + plan = Attribute("Current active github plan") + created_at = DateAttribute("The date this user was registered", + format="user") + + def is_authenticated(self): + """Test for user auththenication + + :return bool: ``True`` if user is authenticated""" + return self.plan is not None + + def __repr__(self): + return "" % self.login + + +class Users(GithubCommand): + domain = "user" + + def search(self, query): + """Search for users + + .. warning: + Returns at most 100 users + + :param str query: term to search for + """ + return self.get_values("search", quote_plus(query), filter="users", + datatype=User) + + def search_by_email(self, query): + """Search for users by email address + + :param str query: email to search for + """ + return self.get_value("email", query, filter="user", datatype=User) + + @enhanced_by_auth + def show(self, username): + """Get information on Github user + + if ``username`` is ``None`` or an empty string information for the + currently authenticated user is returned. + + :param str username: Github user name + """ + return self.get_value("show", username, filter="user", datatype=User) + + def followers(self, username): + """Get list of Github user's followers + + :param str username: Github user name + """ + return self.get_values("show", username, "followers", filter="users") + + def following(self, username): + """Get list of users a Github user is following + + :param str username: Github user name + """ + return self.get_values("show", username, "following", filter="users") + + @requires_auth + def follow(self, other_user): + """Follow a Github user + + :param str other_user: Github user name + """ + return self.get_values("follow", other_user, method="POST") + + @requires_auth + def unfollow(self, other_user): + """Unfollow a Github user + + :param str other_user: Github user name + """ + return self.get_values("unfollow", other_user, method="POST") From 85ec7d65b1321dfd0373545cdc08c7e065529798 Mon Sep 17 00:00:00 2001 From: David Lathrop Date: Mon, 30 Jan 2012 14:32:27 -0500 Subject: [PATCH 06/14] Updates for the github api v3 --- github3/repositories.py | 50 +++++++++++++++++++++++++++++++++++++++++ github3/request.py | 10 ++++++--- github3/users.py | 43 ----------------------------------- 3 files changed, 57 insertions(+), 46 deletions(-) diff --git a/github3/repositories.py b/github3/repositories.py index 6a472cf..f65b21e 100644 --- a/github3/repositories.py +++ b/github3/repositories.py @@ -239,3 +239,53 @@ def list_contributors(self, project): """ return self.get_values("show", project, "contributors", filter="contributors", datatype=User) + + @requires_auth + def list_keys(self, project, user): + """List the keys for a repo""" + return self.get_values(user, project, "keys") + + @requires_auth + def get_key(self, project, user, id): + """Get a specific key using the key id + + :param str project: The github project name + :param str user: The github user + :param str key: Key id + """ + return self.get_value(user, project, "keys", str(id)) + + @requires_auth + def create_key(self, project, user, key_title, key_data): + """Create a key for a repo + + :param str title: The name of the key + :param str key_data: The public key + """ + key = {'title': key_title, + 'key': key_data} + return self.get_value(user, project, 'keys', post_data=key, method='POST') + + @requires_auth + def update_key(self, project, user, id, key_title, key_data): + """Update a specific key + + :param str project: The github project to be updated + :param str user: The github username + :param str id: The id of the key + :param str key_title: The key title + :param str key_data: The public key + """ + key = {'title': key_title, + 'key': key_data} + return self.get_value(user, project, 'keys', id, post_data=key, method='POST') + + @requires_auth + def delete_key(self, project, user, id): + """Delete a github key + + :param str project: The github project + :param str user: The github user + :param str id: The id of the key to be deleted + """ + return self.get_value(user, project, 'keys', id, method='DELETE') diff --git a/github3/request.py b/github3/request.py index cb45ff3..e2dbb9c 100644 --- a/github3/request.py +++ b/github3/request.py @@ -185,7 +185,7 @@ def make_request(self, path, extra_post_data=None, method="GET"): extra_post_data = extra_post_data or {} url = "/".join([self.url_prefix, quote(path)]) - print url + print('Request url: %s' % url) result = self.raw_request(url, extra_post_data, method=method) if self.delay: @@ -198,8 +198,9 @@ def raw_request(self, url, extra_post_data, method="GET"): headers = self.http_headers method = method.upper() if extra_post_data or method == "POST": - post_data = self.encode_authentication_data(extra_post_data) + post_data = simplejson.dumps(extra_post_data) headers["Content-Length"] = str(len(post_data)) + headers["Authorization"] = "token %s" % self.access_token else: query = self.encode_authentication_data(parse_qs(query)) url = urlunsplit((scheme, netloc, path, query, fragment)) @@ -211,7 +212,10 @@ def raw_request(self, url, extra_post_data, method="GET"): raise HttpError("Unexpected response from github.com %d: %r" % (response.status, content), content, response.status) - json = simplejson.loads(content.decode(charset_from_headers(response))) + if response.status != 204: + json = simplejson.loads(content.decode(charset_from_headers(response))) + else: + json = {'success': True} if 'error' in json: raise self.GithubError(json["error"][0]["error"]) diff --git a/github3/users.py b/github3/users.py index 767f886..d217f1c 100644 --- a/github3/users.py +++ b/github3/users.py @@ -114,47 +114,4 @@ def unfollow(self, other_user): """ return self.get_values("unfollow", other_user, method="POST") - # @requires_auth - # def keys(self): - # temp_domain = self.domain - # self.domain = 'user' - # v = self.get_values("keys") - # self.domain = temp_domain - - # @requires_auth - # def key(self, id): - # temp_domain = self.domain - # self.domain = 'user' - # v = self.get_value("key", id) - # self.domain = temp_domain - # return v - - # @requires_auth - # def create_key(self, title, key_data): - # key = {'title': title, - # 'key': key_data} - # temp_domain = self.domain - # self.domain = 'user' - # v = self.get_value('keys', post_data=key, method='POST') - # self.domain = temp_domain - # return v - - # @requires_auth - # def update_key(self, id, title, key_data): - # key = {'title': title, - # 'key': key_data} - # temp_domain = self.domain - # self.domain = 'user' - # v = self.get_value('keys', id, post_data=key, method='POST') - # self.domain = temp_domain - # return v - - # @requires_auth - # def delete_key(self, id): - # temp_domain = self.domain - # self.domain = 'user' - # v = self.get_value('keys', id, method='DELETE') - # self.domain = temp_domain - # return v - From 7d62890608748821a818fe62c30ea4f617c3cb7b Mon Sep 17 00:00:00 2001 From: David Lathrop Date: Thu, 15 Mar 2012 17:38:21 -0400 Subject: [PATCH 07/14] Adding code to list a user's organizations --- github3/organizations.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/github3/organizations.py b/github3/organizations.py index 3390bcc..6ecaf2e 100644 --- a/github3/organizations.py +++ b/github3/organizations.py @@ -33,7 +33,7 @@ def __repr__(self): class Organizations(GithubCommand): """.. versionadded:: 0.4.0""" - domain = "organizations" + domain = "orgs" def show(self, organization): """Get information on organization @@ -43,10 +43,21 @@ def show(self, organization): return self.get_value(organization, filter="organization", datatype=Organization) - def list(self): + def list(self, user=None): """Get list of all of your organizations""" - return self.get_values('', filter="organizations", + + temp_domain = self.domain + if (self.request.access_token or self.request.api_token) and (user is None or user == self.request.username): + user = None + self.domain = 'user' + else: + user = user or self.request.username + self.domain = 'users' + + ret_val = self.get_values(user, 'orgs', filter=None, datatype=Organization) + self.domain = temp_domain + return ret_val def repositories(self, organization=''): """Get list of all repositories in an organization From 149e3abe48c9e06e252f2720ffbae7b0be8fefc1 Mon Sep 17 00:00:00 2001 From: David Lathrop Date: Mon, 19 Mar 2012 23:52:19 -0400 Subject: [PATCH 08/14] Making changes to access organizations in github3 api --- github3/organizations.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/github3/organizations.py b/github3/organizations.py index 6ecaf2e..61f4153 100644 --- a/github3/organizations.py +++ b/github3/organizations.py @@ -40,12 +40,11 @@ def show(self, organization): :param str organization: organization to show """ - return self.get_value(organization, filter="organization", + return self.get_value(organization, filter=None, datatype=Organization) def list(self, user=None): """Get list of all of your organizations""" - temp_domain = self.domain if (self.request.access_token or self.request.api_token) and (user is None or user == self.request.username): user = None @@ -67,8 +66,8 @@ def repositories(self, organization=''): :param: str organization: organization to list repositories for """ - return self.get_values(organization, 'repositories', - filter="repositories", datatype=Repository) + return self.get_values(organization, 'repos', + filter=None, datatype=Repository) def public_repositories(self, organization): """Get list of public repositories in an organization From a690b17ae925254ac3beb0b8c36ae4a29acb73e6 Mon Sep 17 00:00:00 2001 From: David Lathrop Date: Mon, 2 Apr 2012 18:10:42 -0400 Subject: [PATCH 09/14] Bumping version number to 0.6.1 for the github api v3 changes. --- github3/_version.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/github3/_version.py b/github3/_version.py index 7514dc4..e82cb03 100644 --- a/github3/_version.py +++ b/github3/_version.py @@ -1,9 +1,9 @@ -# This is github3 version 0.6.0 (2011-12-21) +# This is github3 version 0.6.1 (2012-04-02) # pylint: disable=C0103, C0111, C0121, W0622 -dotted = "0.6.0" -libtool = "6:20" -hex = 0x000600 -date = "2011-12-21" -tuple = (0, 6, 0) -web = "github/0.6.0" +dotted = "0.6.1" +libtool = "6:21" +hex = 0x000601 +date = "2012-04-02" +tuple = (0, 6, 1) +web = "github/0.6.1" From bb0ae315c8c88096faf6de87397b38a18bb917b0 Mon Sep 17 00:00:00 2001 From: David Lathrop Date: Wed, 4 Apr 2012 18:16:18 -0400 Subject: [PATCH 10/14] Adding list, create and delete for github hooks. --- github3/repositories.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/github3/repositories.py b/github3/repositories.py index f65b21e..73cd56e 100644 --- a/github3/repositories.py +++ b/github3/repositories.py @@ -289,3 +289,35 @@ def delete_key(self, project, user, id): :param str id: The id of the key to be deleted """ return self.get_value(user, project, 'keys', id, method='DELETE') + + @requires_auth + def create_hook(self, project, user, url): + """Create a github post receive hook + + :param str project: The github project + :param str user: The github user + :param str url: The url that will be called + """ + data = {'name': 'web', + 'active': True, + 'config':{'content-type': 'form', + 'insecure_ssl': '1', + 'url': url}} + return self.get_value(user, project, 'hooks', post_data=data, method='POST') + + def list_hooks(self, project, user): + """List all github post receive hooks + + :param str project: The github project + :param str user: The github user + """ + return self.get_values(user, project, "hooks", method='GET') + + def delete_hook(self, project, user, id): + """Delete a github post receive hooks + + :param str project: The github project + :param str user: The github user + :param str id: The github hook id + """ + return self.get_value(user, project, "hooks", id, method='DELETE') \ No newline at end of file From 95b77b1e8e41b0b6312832840a74fffe1aa524e5 Mon Sep 17 00:00:00 2001 From: David Lathrop Date: Wed, 4 Apr 2012 20:10:47 -0400 Subject: [PATCH 11/14] Bumping version number for testing. --- github3/_version.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/github3/_version.py b/github3/_version.py index e82cb03..06bc9a1 100644 --- a/github3/_version.py +++ b/github3/_version.py @@ -1,9 +1,9 @@ -# This is github3 version 0.6.1 (2012-04-02) +# This is github3 version 0.6.2 (2012-04-04) # pylint: disable=C0103, C0111, C0121, W0622 -dotted = "0.6.1" -libtool = "6:21" -hex = 0x000601 -date = "2012-04-02" -tuple = (0, 6, 1) -web = "github/0.6.1" +dotted = "0.6.2" +libtool = "6:22" +hex = 0x000602 +date = "2012-04-04" +tuple = (0, 6, 2) +web = "github/0.6.2" From ab1507313877052a26768c472b1bace387d9e37a Mon Sep 17 00:00:00 2001 From: David Lathrop Date: Wed, 11 Apr 2012 16:21:55 -0400 Subject: [PATCH 12/14] Updated how users were queried. An authenticated user can query for itself by doing https://api.github.com/user --- github3/users.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/github3/users.py b/github3/users.py index d217f1c..75f1f93 100644 --- a/github3/users.py +++ b/github3/users.py @@ -70,7 +70,17 @@ def show(self, username): :param str username: Github user name """ - return self.get_value(None, username, filter=None, datatype=User) + if username is None and self.request.username is None: + temp_domain = self.domain + self.domain = "user" + ret_val = self.get_value(None, None, filter=None, datatype=User) + self.domain = temp_domain + else: + if username is None: + username = self.request.username + ret_val = self.get_value(None, username, filter=None, datatype=User) + + return ret_val def followers(self, username): """Get list of Github user's followers From 9ad1d918ed59f7988b228d87dff4f563fcd56839 Mon Sep 17 00:00:00 2001 From: David Lathrop Date: Thu, 19 Apr 2012 16:33:50 -0400 Subject: [PATCH 13/14] Updating version number. --- github3/_version.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/github3/_version.py b/github3/_version.py index 06bc9a1..a08684a 100644 --- a/github3/_version.py +++ b/github3/_version.py @@ -1,9 +1,9 @@ -# This is github3 version 0.6.2 (2012-04-04) +# This is github3 version 0.6.3 (2012-04-19) # pylint: disable=C0103, C0111, C0121, W0622 -dotted = "0.6.2" -libtool = "6:22" -hex = 0x000602 -date = "2012-04-04" -tuple = (0, 6, 2) -web = "github/0.6.2" +dotted = "0.6.3" +libtool = "6:33" +hex = 0x000603 +date = "2012-04-19" +tuple = (0, 6, 3) +web = "github/0.6.3" From f2c24cbfedd17484f1efffbd822b247bb85ad55c Mon Sep 17 00:00:00 2001 From: David Lathrop Date: Fri, 27 Apr 2012 14:58:59 -0400 Subject: [PATCH 14/14] Bumping the version number before the 1.3.0 (github_login) release of testerfy. --- github3/_version.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/github3/_version.py b/github3/_version.py index a08684a..3d3b059 100644 --- a/github3/_version.py +++ b/github3/_version.py @@ -1,9 +1,9 @@ -# This is github3 version 0.6.3 (2012-04-19) +# This is github3 version 0.6.4 (2012-04-27) # pylint: disable=C0103, C0111, C0121, W0622 -dotted = "0.6.3" -libtool = "6:33" -hex = 0x000603 -date = "2012-04-19" -tuple = (0, 6, 3) -web = "github/0.6.3" +dotted = "0.6.4" +libtool = "6:44" +hex = 0x000604 +date = "2012-04-27" +tuple = (0, 6, 4) +web = "github/0.6.4"