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/github3/DigiCert_High_Assurance_EV_Root_CA.crt b/github3/DigiCert_High_Assurance_EV_Root_CA.crt new file mode 100644 index 0000000..9e6810a --- /dev/null +++ b/github3/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/github3/__init__.py b/github3/__init__.py new file mode 100644 index 0000000..7cbe8f0 --- /dev/null +++ b/github3/__init__.py @@ -0,0 +1,10 @@ +"Github API v3 library for Python" + +from github3 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/github3/_version.py b/github3/_version.py new file mode 100644 index 0000000..3d3b059 --- /dev/null +++ b/github3/_version.py @@ -0,0 +1,9 @@ +# This is github3 version 0.6.4 (2012-04-27) +# pylint: disable=C0103, C0111, C0121, W0622 + +dotted = "0.6.4" +libtool = "6:44" +hex = 0x000604 +date = "2012-04-27" +tuple = (0, 6, 4) +web = "github/0.6.4" diff --git a/github3/bin/__init__.py b/github3/bin/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/github3/bin/__init__.py @@ -0,0 +1 @@ +# diff --git a/github3/bin/manage_collaborators.py b/github3/bin/manage_collaborators.py new file mode 100755 index 0000000..4e30c72 --- /dev/null +++ b/github3/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 github3.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 = github3.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/github3/bin/search_repos.py b/github3/bin/search_repos.py new file mode 100755 index 0000000..3d0e059 --- /dev/null +++ b/github3/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 github3.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 = github3.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/github3/client.py b/github3/client.py new file mode 100644 index 0000000..5ee4fe3 --- /dev/null +++ b/github3/client.py @@ -0,0 +1,121 @@ +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): + + 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://developer.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/github3/commits.py b/github3/commits.py new file mode 100644 index 0000000..27dc732 --- /dev/null +++ b/github3/commits.py @@ -0,0 +1,53 @@ +from github3.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/github3/core.py b/github3/core.py new file mode 100644 index 0000000..674b133 --- /dev/null +++ b/github3/core.py @@ -0,0 +1,320 @@ +import logging +import sys + +from datetime import datetime +from dateutil import (parser, tz) + + +#: Logger for core module +LOGGER = logging.getLogger('github3.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""" + +class DeprecationException(Exception): + """Deprecated by v3 of the github api""" + +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/github3/issues.py b/github3/issues.py new file mode 100644 index 0000000..95258dd --- /dev/null +++ b/github3/issues.py @@ -0,0 +1,186 @@ +try: + from urllib.parse import quote_plus # For Python 3 +except ImportError: + from urllib import quote_plus + +from github3.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/github3/organizations.py b/github3/organizations.py new file mode 100644 index 0000000..61f4153 --- /dev/null +++ b/github3/organizations.py @@ -0,0 +1,109 @@ +from github3.core import (BaseData, GithubCommand, Attribute, DateAttribute, + requires_auth) +from github3.repositories import Repository +from github3.teams import Team +from github3.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 = "orgs" + + def show(self, organization): + """Get information on organization + + :param str organization: organization to show + """ + 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 + 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 + + 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, 'repos', + filter=None, 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/github3/pull_requests.py b/github3/pull_requests.py new file mode 100644 index 0000000..d2aa7b8 --- /dev/null +++ b/github3/pull_requests.py @@ -0,0 +1,95 @@ +from github3.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/github3/repositories.py b/github3/repositories.py new file mode 100644 index 0000000..73cd56e --- /dev/null +++ b/github3/repositories.py @@ -0,0 +1,323 @@ +from github3.core import (BaseData, GithubCommand, Attribute, DateAttribute, + requires_auth, enhanced_by_auth) + +from github3.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['login'] + "/" + 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) + + @enhanced_by_auth + 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 + """ + 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, "repos", filter=None, + datatype=Repository, page=page) + self.domain = temp_domain + return ret_val + + @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) + + @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') + + @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 diff --git a/github3/request.py b/github3/request.py new file mode 100644 index 0000000..e2dbb9c --- /dev/null +++ b/github3/request.py @@ -0,0 +1,229 @@ +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://api.github.com" + +#: Logger for requests module +LOGGER = logging.getLogger('github3.request') + +#: 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', + '/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" + 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:`github3.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, + } + 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)]) + print('Request url: %s' % url) + 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 = 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)) + 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) + 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"]) + + return json + + @property + def http_headers(self): + return { + "User-Agent": "pygithub3 v1", + "Accept": "application/json", + } diff --git a/github3/teams.py b/github3/teams.py new file mode 100644 index 0000000..36d62b9 --- /dev/null +++ b/github3/teams.py @@ -0,0 +1,78 @@ +from github3.core import BaseData, GithubCommand, Attribute, requires_auth +from github3.repositories import Repository +from github3.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/github3/users.py b/github3/users.py new file mode 100644 index 0000000..75f1f93 --- /dev/null +++ b/github3/users.py @@ -0,0 +1,127 @@ +try: + from urllib.parse import quote_plus # For Python 3 +except ImportError: + from urllib import quote_plus + +from github3.core import (BaseData, GithubCommand, DateAttribute, Attribute, + DeprecationException, 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 = "users" + + def search(self, query): + """Search for users + + .. warning: + Returns at most 100 users + + :param str query: term to search for + """ + raise DeprecationException() + + def search_by_email(self, query): + """Search for users by email address + + :param str query: email to search for + """ + raise DeprecationException() + + @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 + """ + 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 + + :param str username: Github user name + """ + 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(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): + """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") + + 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,