diff options
author | David Medina <davidmedina9@gmail.com> | 2012-06-16 13:56:29 +0200 |
---|---|---|
committer | David Medina <davidmedina9@gmail.com> | 2012-06-16 13:58:12 +0200 |
commit | 107b12b3b48040488ff6ddd54cb1300200dc8b37 (patch) | |
tree | f35c57bb8b5c5cd85511aaad1c0df0e4f97c0c25 | |
parent | Merge #5 'services/orgs' (diff) | |
parent | Tests on services.issues working (diff) | |
download | python-github3-107b12b3b48040488ff6ddd54cb1300200dc8b37.tar.xz python-github3-107b12b3b48040488ff6ddd54cb1300200dc8b37.zip |
Merge #12 'services/issues'
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | docs/issues.rst | 74 | ||||
-rw-r--r-- | docs/services.rst | 1 | ||||
-rw-r--r-- | pygithub3/github.py | 10 | ||||
-rw-r--r-- | pygithub3/requests/base.py | 4 | ||||
-rw-r--r-- | pygithub3/requests/issues/__init__.py | 44 | ||||
-rw-r--r-- | pygithub3/requests/issues/comments.py | 42 | ||||
-rw-r--r-- | pygithub3/requests/issues/events.py | 22 | ||||
-rw-r--r-- | pygithub3/requests/issues/labels.py | 77 | ||||
-rw-r--r-- | pygithub3/requests/issues/milestones.py | 39 | ||||
-rw-r--r-- | pygithub3/requests/repos/__init__.py | 1 | ||||
-rw-r--r-- | pygithub3/resources/base.py | 7 | ||||
-rw-r--r-- | pygithub3/resources/issues.py | 65 | ||||
-rw-r--r-- | pygithub3/services/base.py | 14 | ||||
-rw-r--r-- | pygithub3/services/issues/__init__.py | 121 | ||||
-rw-r--r-- | pygithub3/services/issues/comments.py | 90 | ||||
-rw-r--r-- | pygithub3/services/issues/events.py | 42 | ||||
-rw-r--r-- | pygithub3/services/issues/labels.py | 197 | ||||
-rw-r--r-- | pygithub3/services/issues/milestones.py | 96 | ||||
-rw-r--r-- | pygithub3/tests/resources/test_issues.py | 17 | ||||
-rw-r--r-- | pygithub3/tests/services/test_core.py | 13 | ||||
-rw-r--r-- | pygithub3/tests/services/test_issues.py | 233 |
22 files changed, 1204 insertions, 6 deletions
@@ -5,4 +5,5 @@ MANIFEST docs/_build dist/ +build/ *egg-info diff --git a/docs/issues.rst b/docs/issues.rst new file mode 100644 index 0000000..708d363 --- /dev/null +++ b/docs/issues.rst @@ -0,0 +1,74 @@ +.. _Issues service: + +Issues services +=============== + +**Fast sample**:: + + from pygithub3 import Github + + auth = dict(login='octocat', password='pass') + gh = Github(**auth) + + octocat_issues = gh.issues.list() + octocat_repo_issues = gh.issues.list_by_repo('octocat', 'Hello-World') + +Issues +------ + +.. autoclass:: pygithub3.services.issues.Issue + :members: + + .. attribute:: comments + + :ref:`Comments service` + + .. attribute:: events + + :ref:`Events service` + + .. attribute:: labels + + :ref:`Labels service` + + .. attribute:: milestones + + :ref:`Milestones service` + +.. _Comments service: + +Comments +-------- + +.. autoclass:: pygithub3.services.issues.Comments + :members: + +.. _Events service: + +Events +------ + +.. autoclass:: pygithub3.services.issues.Events + :members: + +.. _Labels service: + +Labels +------ + +.. autoclass:: pygithub3.services.issues.Labels + :members: + +.. _Milestones service: + +Milestones +---------- + +.. autoclass:: pygithub3.services.issues.Milestones + :members: + +.. _github issues doc: http://developer.github.com/v3/issues +.. _github comments doc: http://developer.github.com/v3/issues/comments +.. _github events doc: http://developer.github.com/v3/issues/events +.. _github labels doc: http://developer.github.com/v3/issues/labels +.. _github milestones doc: http://developer.github.com/v3/issues/milestones diff --git a/docs/services.rst b/docs/services.rst index f7de042..7152aca 100644 --- a/docs/services.rst +++ b/docs/services.rst @@ -75,5 +75,6 @@ List of services git_data pull_requests orgs + issues .. _mimetypes: http://developer.github.com/v3/mime diff --git a/pygithub3/github.py b/pygithub3/github.py index d135865..50b5a1a 100644 --- a/pygithub3/github.py +++ b/pygithub3/github.py @@ -20,12 +20,14 @@ class Github(object): from pygithub3.services.git_data import GitData from pygithub3.services.pull_requests import PullRequests from pygithub3.services.orgs import Org + from pygithub3.services.issues import Issue self._users = User(**config) self._repos = Repo(**config) self._gists = Gist(**config) self._git_data = GitData(**config) self._pull_requests = PullRequests(**config) self._orgs = Org(**config) + self._issues = Issue(**config) @property def remaining_requests(self): @@ -68,8 +70,16 @@ class Github(object): """ return self._pull_requests + @property def orgs(self): """ :ref:`Orgs service <Orgs service>` """ return self._orgs + + @property + def issues(self): + """ + :ref:`Issues service <Issues service>` + """ + return self._issues diff --git a/pygithub3/requests/base.py b/pygithub3/requests/base.py index c4fe5cc..6088bae 100644 --- a/pygithub3/requests/base.py +++ b/pygithub3/requests/base.py @@ -27,8 +27,8 @@ class Body(object): def parse(self): """ Parse body with schema-required rules """ if not hasattr(self.content, 'items'): - raise ValidationError("'%s' needs a content dictionary" - % self.__class__.__name__) + raise ValidationError("It needs a content dictionary (%s)" % ( + self.content, )) parsed = dict([(key, self.content[key]) for key in self.schema if key in self.content]) for attr_required in self.required: diff --git a/pygithub3/requests/issues/__init__.py b/pygithub3/requests/issues/__init__.py new file mode 100644 index 0000000..d6c7b06 --- /dev/null +++ b/pygithub3/requests/issues/__init__.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- + +from pygithub3.requests.base import Request +from pygithub3.resources.issues import Issue + + +class List(Request): + + uri = 'issues' + resource = Issue + + +class List_by_repo(Request): + + uri = 'repos/{user}/{repo}/issues' + resource = Issue + + +class Get(Request): + + uri = 'repos/{user}/{repo}/issues/{number}' + resource = Issue + + +class Create(Request): + + uri = 'repos/{user}/{repo}/issues' + resource = Issue + body_schema = { + 'schema': ('title', 'body', 'assignee', 'milestone', 'labels'), + 'required': ('title', ) + } + + +class Update(Request): + + uri = 'repos/{user}/{repo}/issues/{number}' + resource = Issue + body_schema = { + 'schema': ('title', 'body', 'assignee', 'state', 'milestone', + 'lables'), + 'required': () + } diff --git a/pygithub3/requests/issues/comments.py b/pygithub3/requests/issues/comments.py new file mode 100644 index 0000000..638c9cf --- /dev/null +++ b/pygithub3/requests/issues/comments.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- + +from pygithub3.requests.base import Request, ValidationError +from pygithub3.resources.issues import Comment + +class List(Request): + + uri = 'repos/{user}/{repo}/issues/{number}/comments' + resource = Comment + + +class Get(Request): + + uri = 'repos/{user}/{repo}/issues/comments/{id}' + resource = Comment + + +class Create(Request): + + uri = 'repos/{user}/{repo}/issues/{number}/comments' + resource = Comment + body_schema = { + 'schema': ('body', ), + 'required': ('body', ) + } + + +class Edit(Request): + + uri = 'repos/{user}/{repo}/issues/comments/{id}' + resource = Comment + body_schema = { + 'schema': ('body', ), + 'required': ('body', ) + } + + +class Delete(Request): + + uri = 'repos/{user}/{repo}/issues/comments/{id}' + resource = Comment diff --git a/pygithub3/requests/issues/events.py b/pygithub3/requests/issues/events.py new file mode 100644 index 0000000..dfefe7e --- /dev/null +++ b/pygithub3/requests/issues/events.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- + +from pygithub3.requests.base import Request, ValidationError +from pygithub3.resources.issues import Event + +class List_by_issue(Request): + + uri = 'repos/{user}/{repo}/issues/{number}/events' + resource = Event + + +class List_by_repo(Request): + + uri = 'repos/{user}/{repo}/issues/events' + resource = Event + + +class Get(Request): + + uri = 'repos/{user}/{repo}/issues/events/{id}' + resource = Event diff --git a/pygithub3/requests/issues/labels.py b/pygithub3/requests/issues/labels.py new file mode 100644 index 0000000..9ae6025 --- /dev/null +++ b/pygithub3/requests/issues/labels.py @@ -0,0 +1,77 @@ +# -*- encoding: utf-8 -*- + +from pygithub3.requests.base import Request, ValidationError +from pygithub3.resources.issues import Label + + +class List(Request): + + uri = 'repos/{user}/{repo}/labels' + resource = Label + + +class Get(Request): + uri = 'repos/{user}/{repo}/labels/{name}' + resource = Label + + +class Create(Request): + uri = 'repos/{user}/{repo}/labels' + resource = Label + body_schema = { + 'schema': ('name', 'color'), + 'required': ('name', 'color' ) + } + + def clean_body(self): + color = self.body.get('color', '') + if not Label.is_valid_color(color): + raise ValidationError('colors must have 6 hexadecimal characters, ' + 'without # in the beggining') + else: + return self.body + + +class Update(Create): + + uri = 'repos/{user}/{repo}/labels/{name}' + resource = Label + body_schema = { + 'schema': ('name', 'color'), + 'required': ('name', 'color' ) + } + + +class Delete(Request): + uri = 'repos/{user}/{repo}/labels/{name}' + resource = Label + + +class List_by_issue(Request): + uri = 'repos/{user}/{repo}/issues/{number}/labels' + resource = Label + + +class Add_to_issue(Request): + uri = 'repos/{user}/{repo}/issues/{number}/labels' + resource = Label + + +class Remove_from_issue(Request): + uri = 'repos/{user}/{repo}/issues/{number}/labels/{name}' + resource = Label + + +class Replace_all(Request): + uri = 'repos/{user}/{repo}/issues/{number}/labels' + resource = Label + + +class Remove_all(Request): + uri = 'repos/{user}/{repo}/issues/{number}/labels' + resource = Label + + +class List_by_milestone(Request): + uri = 'repos/{user}/{repo}/milestones/{number}/labels' + resource = Label diff --git a/pygithub3/requests/issues/milestones.py b/pygithub3/requests/issues/milestones.py new file mode 100644 index 0000000..4093c7e --- /dev/null +++ b/pygithub3/requests/issues/milestones.py @@ -0,0 +1,39 @@ +# -*- encoding: utf-8 -*- + +from pygithub3.requests.base import Request, ValidationError +from pygithub3.resources.issues import Milestone + + +class List(Request): + uri = 'repos/{user}/{repo}/milestones' + resource = Milestone + + +class Get(Request): + uri = 'repos/{user}/{repo}/milestones/{number}' + resource = Milestone + + +class Create(Request): + uri = 'repos/{user}/{repo}/milestones' + resource = Milestone + body_schema = { + 'schema': ('title', 'state', 'description', 'due_on'), + 'required': ('title',) + } + + def clean_body(self): # Test if API normalize it + state = self.body.get('state', '') + if state and state.lower() not in ('open', 'closed'): + raise ValidationError("'state' must be 'open' or 'closed'") + return self.body + + +class Update(Create): + + uri = 'repos/{user}/{repo}/milestones/{number}' + + +class Delete(Request): + uri = 'repos/{user}/{repo}/milestones/{number}' + resource = Milestone diff --git a/pygithub3/requests/repos/__init__.py b/pygithub3/requests/repos/__init__.py index 7bbcf3e..a1b3607 100644 --- a/pygithub3/requests/repos/__init__.py +++ b/pygithub3/requests/repos/__init__.py @@ -4,6 +4,7 @@ from pygithub3.requests.base import Request, ValidationError from pygithub3.resources.orgs import Team from pygithub3.resources.repos import Repo, Tag, Branch from pygithub3.resources.users import User +from pygithub3.resources.issues import Label, Milestone class List(Request): diff --git a/pygithub3/resources/base.py b/pygithub3/resources/base.py index 7045529..4c65b8b 100644 --- a/pygithub3/resources/base.py +++ b/pygithub3/resources/base.py @@ -1,8 +1,12 @@ #!/usr/bin/env python # -*- encoding: utf-8 -*- +from datetime import datetime + from pygithub3.core.utils import json +GITHUB_DATE_FORMAT = '%Y-%m-%dT%H:%M:%SZ' + class Resource(object): @@ -44,9 +48,8 @@ class Resource(object): return wrapper def parse_date(string_date): - from datetime import datetime try: - date = datetime.strptime(string_date, '%Y-%m-%dT%H:%M:%SZ') + date = datetime.strptime(string_date, GITHUB_DATE_FORMAT) except TypeError: date = None return date diff --git a/pygithub3/resources/issues.py b/pygithub3/resources/issues.py new file mode 100644 index 0000000..b2301a6 --- /dev/null +++ b/pygithub3/resources/issues.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- + +import re + +from .base import Resource +from .users import User +from .pull_requests import PullRequest + + +class Label(Resource): + + @staticmethod + def is_valid_color(color): + valid_color = re.compile(r'[0-9abcdefABCDEF]{6}') + match = valid_color.match(color) + if match is None: + return False + return match.start() == 0 and match.end() == len(color) + + def __str__(self): + return '<Label (%s)>' % getattr(self, 'name', '') + + +class Milestone(Resource): + + _dates = ('created_at', 'due_on') + _maps = {'creator': User} + + def __str__(self): + return '<Milestone (%s)>' % getattr(self, 'title', '') + + +class Issue(Resource): + + _dates = ('created_at', 'updated_at', 'closed_at') + _maps = { + 'assignee': User, + 'user': User, + 'milestone': Milestone, + 'pull_request': PullRequest + } + + _collection_maps = {'labels': Label} + + def __str__(self): + return '<Issue (%s)>' % getattr(self, 'number', '') + + +class Comment(Resource): + + _dates = ('created_at', 'updated_at') + _maps = {'user': User} + + def __str__(self): + return '<Comment (%s)>' % (getattr(self, 'user', '')) + + +class Event(Resource): + + _dates = ('created_at', ) + _maps = {'actor': User, 'issue': Issue} + + def __str__(self): + return '<Event (%s)>' % (getattr(self, 'commit_id', '')) diff --git a/pygithub3/services/base.py b/pygithub3/services/base.py index 649b2b3..5fed1db 100644 --- a/pygithub3/services/base.py +++ b/pygithub3/services/base.py @@ -1,10 +1,13 @@ #!/usr/bin/env python # -*- encoding: utf-8 -*- +from datetime import datetime + from pygithub3.core.client import Client +from pygithub3.core.errors import NotFound from pygithub3.core.result import smart, normal from pygithub3.requests.base import Factory -from pygithub3.core.errors import NotFound +from pygithub3.resources.base import GITHUB_DATE_FORMAT class Service(object): @@ -41,6 +44,15 @@ class Service(object): self._client = Client(**config) self.request_builder = Factory() + def _normalize_date(self, key, _dict): + """ If ``key`` comes as ``datetime``, it'll normalize it """ + try: + key = str(key) + date = datetime.strftime(_dict.get(key), GITHUB_DATE_FORMAT) + _dict.update({key: date}) + except: + pass + @property def remaining_requests(self): return Client.remaining_requests diff --git a/pygithub3/services/issues/__init__.py b/pygithub3/services/issues/__init__.py new file mode 100644 index 0000000..7851e6f --- /dev/null +++ b/pygithub3/services/issues/__init__.py @@ -0,0 +1,121 @@ +# -*- encoding: utf-8 -*- + +from pygithub3.services.base import Service, MimeTypeMixin +from .comments import Comments +from .events import Events +from .labels import Labels +from .milestones import Milestones + + +class Issue(Service, MimeTypeMixin): + """ Consume `Issues API <http://developer.github.com/v3/issues>`_ """ + + def __init__(self, **config): + self.comments = Comments(**config) + self.events = Events(**config) + self.labels = Labels(**config) + self.milestones = Milestones(**config) + super(Issue, self).__init__(**config) + + def list(self, filter='assigned', state='open', labels='', sort='created', + direction='desc', since=None): + """ List your issues + + :param str filter: 'assigned', 'created', 'mentioned' or 'subscribed' + :param str state: 'open' or 'closed' + :param str labels: List of comma separated Label names. e.g: bug,ui, + @high + :param str sort: 'created', 'updated' or 'comments' + :param str direction: 'asc' or 'desc' + :param datetime since: Date filter (datetime or str in ISO 8601) + :returns: A :doc:`result` + + .. warning:: + You must be authenticated + """ + params = dict(filter=filter, state=state, labels=labels, sort=sort, + direction=direction) + self._normalize_date('since', params) + request = self.request_builder('issues.list') + return self._get_result(request, **params) + + def list_by_repo(self, user=None, repo=None, milestone='*', state='open', + assignee='*', mentioned='', labels='', sort='created', + direction='desc', since=None): + """ List issues for a repo + + :param str milestone: Milestone ID, 'none' or '*' + :param str state: 'open' or 'closed' + :param str assignee: Username, 'none' or '*' + :param str mentioned: Username + :param str labels: List of comma separated Label names. e.g: bug,ui, + @high + :param str sort: 'created', 'updated' or 'comments' + :param str direction: 'asc' or 'desc' + :param datetime since: Date filter (datetime or str in ISO 8601) + :returns: A :doc:`result` + + .. note:: + Remember :ref:`config precedence` + """ + params = dict(milestone=milestone, state=state, assignee=assignee, + mentioned=mentioned, labels=labels, sort=sort, direction=direction) + self._normalize_date('since', params) + request = self.make_request('issues.list_by_repo', user=user, + repo=repo) + return self._get_result(request, **params) + + def get(self, number, user=None, repo=None): + """ Get a single issue + + :param int number: Issue number + :param str user: Username + :param str repo: Repo name + + .. note:: + Remember :ref:`config precedence` + """ + request = self.make_request('issues.get', user=user, repo=repo, + number=number) + return self._get(request) + + def create(self, data, user=None, repo=None): + """ Create an issue + + :param dict data: Input. See `github issues doc`_ + :param str user: Username + :param str repo: Repo name + + .. warning:: + You must be authenticated + + .. note:: + Remember :ref:`config precedence` + + :: + + issues_service.create(dict(title='My test issue', + body='This needs to be fixed ASAP.', + assignee='copitux')) + """ + request = self.make_request('issues.create', user=user, repo=repo, + body=data) + return self._post(request) + + def update(self, number, data, user=None, repo=None): + """ Update an issue + + :param int number: Issue number + :param dict data: Input. See `github issues doc`_ + :param str user: Username + :param str repo: Repo name + + .. warning:: + You must be authenticated + + .. note:: + Remember :ref:`config precedence` + """ + request = self.make_request('issues.update', user=user, repo=repo, + number=number, body=data) + return self._patch(request) diff --git a/pygithub3/services/issues/comments.py b/pygithub3/services/issues/comments.py new file mode 100644 index 0000000..54d4287 --- /dev/null +++ b/pygithub3/services/issues/comments.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- + +from pygithub3.services.base import Service, MimeTypeMixin + +class Comments(Service, MimeTypeMixin): + """ Consume `Comments API + <http://developer.github.com/v3/issues/comments>`_ """ + + def list(self, number, user=None, repo=None): + """ List comments for an issue + + :param int number: Issue number + :param str user: Username + :param str repo: Repo name + :returns: A :doc:`result` + + .. note:: + Remember :ref:`config precedence` + """ + request = self.make_request('issues.comments.list', user=user, + repo=repo, number=number) + return self._get_result(request) + + def get(self, id, user=None, repo=None): + """ Get a single comment + + :param int id: Comment id + :param str user: Username + :param str repo: Repo name + + .. note:: + Remember :ref:`config precedence` + """ + request = self.make_request('issues.comments.get', user=user, + repo=repo, id=id) + return self._get(request) + + def create(self, number, message, user=None, repo=None): + """ Create a comment on an issue + + :param int number: Issue number + :param str message: Comment message + :param str user: Username + :param str repo: Repo name + + .. warning:: + You must be authenticated + + .. note:: + Remember :ref:`config precedence` + """ + request = self.make_request('issues.comments.create', user=user, + repo=repo, number=number, body={'body': message}) + return self._post(request) + + def update(self, id, message, user=None, repo=None): + """ Update a comment on an issue + + :param int id: Issue id + :param str message: Comment message + :param str user: Username + :param str repo: Repo name + + .. warning:: + You must be authenticated + + .. note:: + Remember :ref:`config precedence` + """ + request = self.make_request('issues.comments.edit', user=user, + repo=repo, id=id, body={'body': message}) + return self._patch(request) + + def delete(self, id, user=None, repo=None): + """ Delete a single comment + + :param int id: Comment id + :param str user: Username + :param str repo: Repo name + + .. warning:: + You must be authenticated + + .. note:: + Remember :ref:`config precedence` + """ + request = self.make_request('issues.comments.delete', user=user, + repo=repo, id=id) + self._delete(request) diff --git a/pygithub3/services/issues/events.py b/pygithub3/services/issues/events.py new file mode 100644 index 0000000..92bb332 --- /dev/null +++ b/pygithub3/services/issues/events.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- + +from pygithub3.services.base import Service + +class Events(Service): + """ Consume `Events API + <http://developer.github.com/v3/issues/events>`_ """ + + def list_by_issue(self, number, user=None, repo=None): + """ List events for an issue + + :param int number: Issue number + :param str user: Username + :param str repo: Repo name + :returns: A :doc:`result` + """ + request = self.make_request('issues.events.list_by_issue', + user=user, repo=repo, number=number) + return self._get_result(request) + + def list_by_repo(self, user=None, repo=None): + """ List events for a repository + + :param str user: Username + :param str repo: Repo name + :returns: A :doc:`result` + """ + request = self.make_request('issues.events.list_by_repo', + user=user, repo=repo) + return self._get_result(request) + + def get(self, id, user=None, repo=None): + """ Get a single event + + :param int id: Comment id + :param str user: Username + :param str repo: Repo name + """ + request = self.make_request('issues.events.get', user=user, + repo=repo, id=id) + return self._get(request) diff --git a/pygithub3/services/issues/labels.py b/pygithub3/services/issues/labels.py new file mode 100644 index 0000000..d1119d3 --- /dev/null +++ b/pygithub3/services/issues/labels.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- + +from pygithub3.services.base import Service + + +class Labels(Service): + """ Consume `Labels API + <http://developer.github.com/v3/issues/labels>`_ """ + + def list(self, user=None, repo=None): + """ Get repository's labels + + :param str user: Username + :param str repo: Repository + :returns: A :doc:`result` + + .. note:: + Remember :ref:`config precedence` + """ + request = self.make_request('issues.labels.list', user=user, repo=repo) + return self._get_result(request) + + def get(self, name, user=None, repo=None): + """ Get a single label + + :param str name: Label name + :param str user: Username + :param str repo: Repo name + + .. note:: + Remember :ref:`config precedence` + """ + request = self.make_request('issues.labels.get', user=user, + repo=repo, name=name) + return self._get(request) + + def create(self, data, user=None, repo=None): + """ Create a label on an repo + + :param dict data: Input. See `github labels doc`_ + :param str user: Username + :param str repo: Repo name + + .. warning:: + You must be authenticated + + .. note:: + Remember :ref:`config precedence` + """ + request = self.make_request('issues.labels.create', user=user, + repo=repo, body=data) + return self._post(request) + + def update(self, name, data, user=None, repo=None): + """ Update a label on an repo + + :param str name: Label name + :param dict data: Input. See `github labels doc`_ + :param str user: Username + :param str repo: Repo name + + .. warning:: + You must be authenticated + + .. note:: + Remember :ref:`config precedence` + """ + request = self.make_request('issues.labels.update', user=user, + repo=repo, name=name, body=data) + return self._patch(request) + + def delete(self, name, user=None, repo=None): + """ Delete a label on an repo + + :param str name: Label name + :param str user: Username + :param str repo: Repo name + + .. warning:: + You must be authenticated + + .. note:: + Remember :ref:`config precedence` + """ + request = self.make_request('issues.labels.delete', user=user, + repo=repo, name=name) + return self._delete(request) + + def list_by_issue(self, number, user=None, repo=None): + """ List labels for an issue + + :param int number: Issue number + :param str user: Username + :param str repo: Repo name + :returns: A :doc:`result` + + .. note:: + Remember :ref:`config precedence` + """ + request = self.make_request('issues.labels.list_by_issue', user=user, + repo=repo, number=number) + return self._get(request) + + def add_to_issue(self, number, labels, user=None, repo=None): + """ Add labels to issue + + :param int number: Issue number + :param str user: Username + :param str repo: Repo name + :param list labels: Label names + :returns: A :doc:`result` + + .. note:: + Remember :ref:`config precedence` + + :: + + labels_service.add_to_issue(2, user='github', repo='github', + 'label1', 'label2', 'label3') + """ + request = self.make_request('issues.labels.add_to_issue', user=user, + repo=repo, number=number, body=map(str, labels)) + return self._post(request) + + def remove_from_issue(self, number, label, user=None, repo=None): + """ Remove a label from an issue + + :param int number: Issue number + :param str label: Label name + :param str user: Username + :param str repo: Repo name + :returns: A :doc:`result` + + .. note:: + Remember :ref:`config precedence` + """ + request = self.make_request('issues.labels.remove_from_issue', + user=user, + repo=repo, + number=number, + name=label) + return self._delete(request) + + def replace_all(self, number, labels, user=None, repo=None): + """ Replace all labels for a issue + + :param int number: Issue number + :param list labels: New labels + :param str user: Username + :param str repo: Repo name + :returns: A :doc:`result` + + .. note:: + If labels weren't especified, it'd remove all labels from the issue + + .. note:: + Remember :ref:`config precedence` + """ + request = self.make_request('issues.labels.replace_all', + user=user, + repo=repo, + number=number, + body=map(str, labels)) + return self._put(request) + + def remove_all(self, number, user=None, repo=None): + """ Remove all labels from a issue + + :param int number: Issue number + :param str user: Username + :param str repo: Repo name + :returns: A :doc:`result` + + .. note:: + Remember :ref:`config precedence` + """ + request = self.make_request('issues.labels.remove_all', + user=user, + repo=repo, + number=number,) + return self._delete(request) + + def list_by_milestone(self, number, user=None, repo=None): + """ Get labels for every issue in a milestone + + :param int number: Milestone ID + :param str user: Username + :param str repo: Repo name + :returns: A :doc:`result` + + .. note:: + Remember :ref:`config precedence` + """ + request = self.make_request('issues.labels.list_by_milestone', + user=user, repo=repo, number=number) + return self._get_result(request) diff --git a/pygithub3/services/issues/milestones.py b/pygithub3/services/issues/milestones.py new file mode 100644 index 0000000..ac43a0c --- /dev/null +++ b/pygithub3/services/issues/milestones.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- + +from pygithub3.services.base import Service + + +class Milestones(Service): + """ Consume `Milestones API + <http://developer.github.com/v3/issues/milestones>`_ """ + + def list(self, user=None, repo=None, state='open', sort='due_date', + direction='desc'): + """ List milestones for a repo + + :param str user: Username + :param str repo: Repo name + :param str state: 'open' or 'closed' + :param str sort: 'due_date' or 'completeness' + :param str direction: 'asc' or 'desc' + :returns: A :doc:`result` + + .. note:: + Remember :ref:`config precedence` + """ + request = self.make_request('issues.milestones.list', user=user, + repo=repo) + return self._get_result(request, state=state, sort=sort, + direction=direction) + + def get(self, number, user=None, repo=None): + """ Get a single milestone + + :param int number: Milestone number + :param str user: Username + :param str repo: Repo name + + .. note:: + Remember :ref:`config precedence` + """ + request = self.make_request('issues.milestones.get', user=user, + repo=repo, number=number) + return self._get(request) + + def create(self, data, user=None, repo=None): + """ Create a milestone + + :param dict data: Input. See `github milestones doc`_ + :param str user: Username + :param str repo: Repo name + + .. warning:: + You must be authenticated + + .. note:: + Remember :ref:`config precedence` + """ + self._normalize_date('due_on', data) + request = self.make_request('issues.milestones.create', user=user, + repo=repo, body=data) + return self._post(request) + + def update(self, number, data, user=None, repo=None): + """ Update a milestone + + :param int number: Milestone number + :param dict data: Input. See `github milestones doc`_ + :param str user: Username + :param str repo: Repo name + + .. warning:: + You must be authenticated + + .. note:: + Remember :ref:`config precedence` + """ + self._normalize_date('due_on', data) + request = self.make_request('issues.milestones.update', user=user, + repo=repo, number=number, body=data) + return self._patch(request) + + def delete(self, number, user=None, repo=None): + """ Delete a milestone + + :param int number: Milestone number + :param str user: Username + :param str repo: Repo name + + .. warning:: + You must be authenticated + + .. note:: + Remember :ref:`config precedence` + """ + request = self.make_request('issues.milestones.delete', user=user, + repo=repo, number=number) + self._delete(request) diff --git a/pygithub3/tests/resources/test_issues.py b/pygithub3/tests/resources/test_issues.py new file mode 100644 index 0000000..ae572af --- /dev/null +++ b/pygithub3/tests/resources/test_issues.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- + +from unittest import TestCase + +from pygithub3.resources.issues import Label + + +class TestLabel(TestCase): + def test_is_valid_color(self): + valid_colors = ['BADa55', 'FF42FF', '45DFCA'] + for color in valid_colors: + self.assertTrue(Label.is_valid_color(color)) + + invalid_colors = ['BDA55', '#FFAABB', 'FFf'] + for color in invalid_colors: + self.assertFalse(Label.is_valid_color(color)) diff --git a/pygithub3/tests/services/test_core.py b/pygithub3/tests/services/test_core.py index 8a2bbbe..556f3d9 100644 --- a/pygithub3/tests/services/test_core.py +++ b/pygithub3/tests/services/test_core.py @@ -1,8 +1,9 @@ #!/usr/bin/env python # -*- encoding: utf-8 -*- -import requests +from datetime import datetime +import requests from mock import patch from pygithub3.tests.utils.core import TestCase @@ -54,6 +55,16 @@ class TestServiceCalls(TestCase): self.assertFalse(request_method.called) self.assertIsInstance(result, base.Result) + def test_normalize_ok(self, request_method): + data = {'test': datetime(2012, 12, 12, 3, 3, 3)} + self.s._normalize_date('test', data) + self.assertEqual(data['test'], '2012-12-12T03:03:03Z') + + def test_normalize_fail(self, request_method): + data = {'test': 'fail'} + self.s._normalize_date('test', data) + self.assertEqual(data['test'], 'fail') + @patch.object(requests.sessions.Session, 'request') class TestMimeType(TestCase): diff --git a/pygithub3/tests/services/test_issues.py b/pygithub3/tests/services/test_issues.py new file mode 100644 index 0000000..2d42405 --- /dev/null +++ b/pygithub3/tests/services/test_issues.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- + +import requests +from mock import patch, Mock + +from pygithub3.exceptions import ValidationError +from pygithub3.tests.utils.core import TestCase +from pygithub3.resources.base import json +from pygithub3.services.issues import (Issue, Comments, Events, Labels, + Milestones) +from pygithub3.tests.utils.base import (mock_response, mock_response_result, + mock_json) +from pygithub3.tests.utils.services import _ + +json.dumps = Mock(side_effect=mock_json) +json.loads = Mock(side_effect=mock_json) + + +@patch.object(requests.sessions.Session, 'request') +class TestIssuesService(TestCase): + + def setUp(self): + self.isu = Issue(user='octocat', repo='Hello-World') + + def test_LIST_without_user(self, request_method): + request_method.return_value = mock_response_result() + self.isu.list().all() + self.assertEqual(request_method.call_args[0], ('get', _('issues'))) + + def test_LIST_by_repo(self, request_method): + request_method.return_value = mock_response_result() + self.isu.list_by_repo().all() + self.assertEqual(request_method.call_args[0], + ('get', _('repos/octocat/Hello-World/issues'))) + + def test_GET(self, request_method): + request_method.return_value = mock_response() + self.isu.get(1) + self.assertEqual(request_method.call_args[0], + ('get', _('repos/octocat/Hello-World/issues/1'))) + + def test_CREATE(self, request_method): + request_method.return_value = mock_response('post') + self.isu.create(dict(title='My issue', body='Fix this issue')) + self.assertEqual(request_method.call_args[0], + ('post', _('repos/octocat/Hello-World/issues'))) + + def test_UPDATE(self, request_method): + request_method.return_value = mock_response('patch') + self.isu.update(1, {'body': 'edited'}) + self.assertEqual(request_method.call_args[0], + ('patch', _('repos/octocat/Hello-World/issues/1'))) + + +@patch.object(requests.sessions.Session, 'request') +class TestCommentService(TestCase): + + def setUp(self): + self.cs = Comments(user='octocat', repo='Hello-World') + + def test_LIST(self, request_method): + request_method.return_value = mock_response_result() + self.cs.list(1).all() + self.assertEqual(request_method.call_args[0], + ('get', _('repos/octocat/Hello-World/issues/1/comments'))) + + def test_GET(self, request_method): + request_method.return_value = mock_response() + self.cs.get(1) + self.assertEqual(request_method.call_args[0], + ('get', _('repos/octocat/Hello-World/issues/comments/1'))) + + def test_CREATE(self, request_method): + request_method.return_value = mock_response('post') + self.cs.create(1, 'comment') + self.assertEqual(request_method.call_args[0], + ('post', _('repos/octocat/Hello-World/issues/1/comments'))) + + def test_UPDATE(self, request_method): + request_method.return_value = mock_response('patch') + self.cs.update(1, 'new comment') + self.assertEqual(request_method.call_args[0], + ('patch', _('repos/octocat/Hello-World/issues/comments/1'))) + + def test_DELETE(self, request_method): + request_method.return_value = mock_response('delete') + self.cs.delete(1) + self.assertEqual(request_method.call_args[0], + ('delete', _('repos/octocat/Hello-World/issues/comments/1'))) + + +@patch.object(requests.sessions.Session, 'request') +class TestEventsService(TestCase): + + def setUp(self): + self.ev = Events(user='octocat', repo='Hello-World') + + def test_LIST_by_issue(self, request_method): + request_method.return_value = mock_response_result() + self.ev.list_by_issue(1).all() + self.assertEqual(request_method.call_args[0], + ('get', _('repos/octocat/Hello-World/issues/1/events'))) + + def test_LIST_by_repo(self, request_method): + request_method.return_value = mock_response_result() + self.ev.list_by_repo().all() + self.assertEqual(request_method.call_args[0], + ('get', _('repos/octocat/Hello-World/issues/events'))) + + def test_GET(self, request_method): + request_method.return_value = mock_response() + self.ev.get(1) + self.assertEqual(request_method.call_args[0], + ('get', _('repos/octocat/Hello-World/issues/events/1'))) + + +@patch.object(requests.sessions.Session, 'request') +class TestLabelsService(TestCase): + + def setUp(self): + self.lb = Labels(user='octocat', repo='Hello-World') + + def test_GET(self, request_method): + request_method.return_value = mock_response() + self.lb.get('bug') + self.assertEqual(request_method.call_args[0], + ('get', _('repos/octocat/Hello-World/labels/bug'))) + + def test_CREATE(self, request_method): + request_method.return_value = mock_response('post') + self.lb.create(dict(name='bug', color='FF0000')) + self.assertEqual(request_method.call_args[0], + ('post', _('repos/octocat/Hello-World/labels'))) + + def test_CREATE_with_invalid_color(self, request_method): + request_method.return_value = mock_response('post') + # invalid color + with self.assertRaises(ValidationError): + args={'name': 'bug', 'color': 'FF00'} + self.lb.create(args) + + def test_UPDATE(self, request_method): + request_method.return_value = mock_response('patch') + self.lb.update('bug', dict(name='critical', color='FF0000')) + self.assertEqual(request_method.call_args[0], + ('patch', _('repos/octocat/Hello-World/labels/bug'))) + + def test_UPDATE_with_invalid_color(self, request_method): + request_method.return_value = mock_response('post') + # invalid color + with self.assertRaises(ValidationError): + args={'name': 'critical', + 'color': 'FF00',} + self.lb.update('bug', args) + + def test_DELETE(self, request_method): + request_method.return_value = mock_response('delete') + self.lb.delete('bug') + self.assertEqual(request_method.call_args[0], + ('delete', _('repos/octocat/Hello-World/labels/bug'))) + + def test_LIST_by_issue(self, request_method): + request_method.return_value = mock_response() + self.lb.list_by_issue(1) + self.assertEqual(request_method.call_args[0], + ('get', _('repos/octocat/Hello-World/issues/1/labels'))) + + def test_ADD_to_issue(self, request_method): + request_method.return_value = mock_response('post') + self.lb.add_to_issue(1, ('bug', 'critical')) + self.assertEqual(request_method.call_args[0], + ('post', _('repos/octocat/Hello-World/issues/1/labels'))) + + def test_REMOVE_from_issue(self, request_method): + request_method.return_value = mock_response('delete') + self.lb.remove_from_issue(1, 'bug') + self.assertEqual(request_method.call_args[0], + ('delete', _('repos/octocat/Hello-World/issues/1/labels/bug'))) + + def test_REPLACE_all(self, request_method): + self.lb.replace_all(1, ['bug', 'critical']) + self.assertEqual(request_method.call_args[0], + ('put', _('repos/octocat/Hello-World/issues/1/labels'))) + + def test_REMOVE_all(self, request_method): + request_method.return_value = mock_response('delete') + self.lb.remove_all(1) + self.assertEqual(request_method.call_args[0], + ('delete', _('repos/octocat/Hello-World/issues/1/labels'))) + + def test_LIST_by_milestone(self, request_method): + request_method.return_value = mock_response_result() + self.lb.list_by_milestone(1).all() + self.assertEqual(request_method.call_args[0], + ('get', _('repos/octocat/Hello-World/milestones/1/labels'))) + + +@patch.object(requests.sessions.Session, 'request') +class TestMilestonesService(TestCase): + + def setUp(self): + self.mi = Milestones(user='octocat', repo='Hello-World') + + def test_LIST_by_repo(self, request_method): + request_method.return_value = mock_response_result() + self.mi.list().all() + self.assertEqual(request_method.call_args[0], + ('get', _('repos/octocat/Hello-World/milestones'))) + + def test_GET(self, request_method): + request_method.return_value = mock_response() + self.mi.get(1) + self.assertEqual(request_method.call_args[0], + ('get', _('repos/octocat/Hello-World/milestones/1'))) + + def test_CREATE(self, request_method): + request_method.return_value = mock_response('post') + self.mi.create(dict(title='test')) + self.assertEqual(request_method.call_args[0], + ('post', _('repos/octocat/Hello-World/milestones'))) + + def test_UPDATE(self, request_method): + request_method.return_value = mock_response('patch') + self.mi.update(1, dict(title='test')) + self.assertEqual(request_method.call_args[0], + ('patch', _('repos/octocat/Hello-World/milestones/1'))) + + def test_DELETE(self, request_method): + request_method.return_value = mock_response('delete') + self.mi.delete(1) + self.assertEqual(request_method.call_args[0], + ('delete', _('repos/octocat/Hello-World/milestones/1'))) |