8000 Add support for repository invitations by jacquerie · Pull Request #877 · sigmavirus24/github3.py · GitHub
[go: up one dir, main page]

Skip to content

Add support for repository invitations #877

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/github3/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from . import auths
from . import events
from . import gists
from .repos import invitation
from . import issues
from . import licenses
from . import models
Expand Down Expand Up @@ -1524,6 +1525,23 @@ def repository(self, owner, repository):
json = self._json(self._get(url), 200)
return self._instance_or_null(repo.Repository, json)

@requires_auth
def repository_invitations(self, number=-1, etag=None):
"""Iterate over the repository invitations for the current user.

:param int number:
(optional), number of invitations to return. Default: -1 returns
all available invitations
:param str etag:
(optional), ETag from a previous request to the same endpoint
:returns:
generator of repository invitation objects
:rtype:
:class:`~github3.repos.invitation.Invitation`
"""
url = self._build_url('user', 'repository_invitations')
return self._iter(int(number), url, invitation.Invitation, etag=etag)

def repository_with_id(self, number):
"""Retrieve the repository with the globally unique id.

Expand Down
128 changes: 128 additions & 0 deletions src/github3/repos/invitation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# -*- coding: utf-8 -*-
"""Invitation related logic."""
from __future__ import unicode_literals

from json import dumps

from .. import models
from .. import users

from ..decorators import requires_auth


class Invitation(models.GitHubCore):
"""Representation of an invitation to collaborate on a repository.

.. attribute:: created_at

A :class:`~datetime.datetime` instance representing the time and date
when this invitation was created.

.. attribute:: html_url

The URL to view this invitation in a browser.

.. attribute:: id

The unique identifier for this invitation.

.. attribute:: invitee

A :class:`~github3.users.ShortUser` representing the user who was
invited to collaborate.

.. attribute:: inviter

A :class:`~github3.users.ShortUser` representing the user who invited
the ``invitee``.

.. attribute:: permissions

The permissions that the ``invitee`` will have on the repository. Valid
values are ``read``, ``write``, and ``admin``.

.. attribute:: repository

A :class:`~github3.repos.ShortRepository` representing the repository
on which the ``invitee` was invited to collaborate.

.. attribute:: url

The API URL that the ``invitee`` can use to respond to the invitation.
Note that the ``inviter`` must use a different URL, not returned by
the API, to update or cancel the invitation.
"""

class_name = 'Invitation'
allowed_permissions = frozenset(['admin', 'read', 'write'])

def _update_attributes(self, invitation):
from . import repo
self.created_at = self._strptime(invitation['created_at'])
self.html_url = invitation['html_url']
self.id = invitation['id']
self.invitee = users.ShortUser(invitation['invitee'], self)
self.inviter = users.ShortUser(invitation['inviter'], self)
self.permissions = invitation['permissions']
self.repository = repo.ShortRepository(invitation['repository'], self)
self.url = invitation['url']

def _repr(self):
return '<Invitation [{0}]>'.format(self.repository.full_name)

@requires_auth
def accept(self):
"""Accept this invitation.

:returns:
True if successful, False otherwise
:rtype:
bool
"""
return self._boolean(self._patch(self.url), 204, 404)

@requires_auth
def decline(self):
"""Decline this invitation.

:returns:
True if successful, False otherwise
:rtype:
bool
"""
return self._boolean(self._delete(self.url), 204, 404)

@requires_auth
def delete(self):
"""Delete this invitation.

:returns:
True if successful, False otherwise
:rtype:
bool
"""
url = self._build_url(
'invitations', self.id, base_url=self.repository.url)
return self._boolean(self._delete(url), 204, 404)

@requires_auth
def update(self, permissions):
"""Update this invitation.

:param str permissions:
(required), the permissions that will be granted by this invitation
once it has been updated. Options: 'admin', 'read', 'write'
:returns:
The updated invitation
:rtype:
:class:`~github3.repos.invitation.Invitation`
"""
if permissions not in self.allowed_permissions:
raise ValueError("'permissions' must be one of {0}".format(
', '.join(sorted(self.allowed_permissions))
))
url = self._build_url(
'invitations', self.id, base_url=self.repository.url)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This URL-building logic is repeated in delete and update. Should I do it once in _update_attributes and assign to self._api ? (The url returned by the API is sadly not the right one.)

Copy link
Collaborator Author
@jacquerie jacquerie Aug 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The crux of the issue is that there's two API URLs, one used by the inviter to cancel/update an invitation, the other used by the invitee to accept/decline. WDYT of computing two private properties called _invitee_api and _inviter_api in _update_attributes ?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We build/rebuild URLs in a few places. I don't think it's the worst thing to have these work this way.

data = {'permissions': permissions}
json = self._json(self._patch(url, data=dumps(data)), 200)
return self._instance_or_null(Invitation, json)
18 changes: 18 additions & 0 deletions src/github3/repos/repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from . import contents
from . import deployment
from . import hook
from . import invitation
from . import issue_import
from . import pages
from . import release
Expand Down Expand Up @@ -1536,6 +1537,23 @@ def import_issue(self, title, body, created_at, assignee=None,
json = self._json(data, 202)
return self._instance_or_null(issue_import.ImportedIssue, json)

@requires_auth
def invitations(self, number=-1, etag=None):
"""Iterate over the invitations to this repository.

:param int number:
(optional), number of invitations to return. Default: -1 returns
all available invitations
:param str etag:
(optional), ETag from a previous request to the same endpoint
:returns:
generator of repository invitation objects
:rtype:
:class:`~github3.repos.invitation.Invitation`
"""
url = self._build_url('invitations', base_url=self._api)
return self._iter(int(number), url, invitation.Invitation, etag=etag)

def is_assignee(self, username):
"""Check if the user can be assigned an issue on this repository.

Expand Down
1 change: 1 addition & 0 deletions tests/cassettes/GitHub_repository_invitations.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"http_interactions": [{"request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Accept-Encoding": ["gzip, deflate"], "Accept": ["application/vnd.github.v3.full+json"], "User-Agent": ["github3.py/1.1.0"], "Accept-Charset": ["utf-8"], "Connection": ["keep-alive"], "Content-Type": ["application/json"], "Authorization": ["token <AUTH_TOKEN>"]}, "method": "GET", "uri": "https://api.github.com/user"}, "response": {"body": {"string": "", "base64_string": "H4sIAAAAAAAAA51U24rbMBT8lUXPyfqSixNDKIVSaGkWCmlZ+mJkWXG0VSRVkhPSkH/vyHaWbFgKyVPi4zOj0ZwzPhKpa6FITl4o+9NwKzgZEFGRfDRL0lk8IEpXvAgFsvz0ffrz+Umyl+V4ufoyXn5cLNBMd9RTWzRWomfjvXF5FHVFlz7Wwm+asnHcMq08V/6R6W3URB39h91iDIra9iTtOShckRnR83RgkLnoUu/Gb+WVgO7ctv+yc62l1Hvgr/X+94joFQZt3X+h6nsoADtG2m84DMM1TuHywvkb5bSQYxR+MJpA4jACy6vbJPUgCNoraDlGlhvdsjWlY1YYL7S6UdobKKi0rakSf+kdVIA6MARRN4poIYDyHRbuRmyHOUbGih1lh2CH5YyLHdy9h+8KDDp/MBxr/gPzD14LzwtabUMI11Q6jsjRbWj4Spk2+uFJY7bO8zVVGv3YaEPVgeSqkXJASuS3zx1i97rq57yIAJGatf6j79tBq8HDZ0sVC0HnWyoQ245qIyynpcTR3jaQUQLcvzJNKQUrOl/z+WxA+kq7iSRPs3MuEC2SJ6PxRU7wnEwhHOweJlIPHWmcxMN4NhzFqzTN4zRP01/Q05jqTc9sGGfDJFvFE3yP8jQJPe1g4Fl/9GQER2GRLM4vepX4doW9rt6pV8L9Rv5ojbtOskmWBFulpKW21OtwA4D9XhdryvBc0AaJVV6cbeznZCSFp8fzvNaWB0+dofA2n2fTyTQdz+fvcV9LPZ3+AVw/d5eJBQAA", "encoding": "utf-8"}, "headers": {"X-XSS-Protection": ["1; mode=block"], "Content-Security-Policy": ["default-src 'none'"], "Access-Control-Expose-Headers": ["ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval"], "Transfer-Encoding": ["chunked"], "Last-Modified": ["Tue, 17 Jul 2018 05:38:21 GMT"], "Access-Control-Allow-Origin": ["*"], "X-Frame-Options": ["deny"], "Status": ["200 OK"], "X-GitHub-Request-Id": ["88EE:3FAB:10C7C9D:2446B09:5B62EA4B"], "ETag": ["W/\"2c29c3ba637f55254842a060df00153d\""], "Date": ["Thu, 02 Aug 2018 11:26:03 GMT"], "X-RateLimit-Remaining": ["4996"], "Strict-Transport-Security": ["max-age=31536000; includeSubdomains; preload"], "Server": ["GitHub.com"], "X-OAuth-Scopes": ["read:user, repo:invite"], "X-GitHub-Media-Type": ["github.v3; param=full; format=json"], "X-Content-Type-Options": ["nosniff"], "Content-Encoding": ["gzip"], "X-Runtime-rack": ["0.057858"], "Vary": ["Accept, Authorization, Cookie, X-GitHub-OTP"], "X-RateLimit-Limit": ["5000"], "Cache-Control": ["private, max-age=60, s-maxage=60"], "Referrer-Policy": ["origin-when-cross-origin, strict-origin-when-cross-origin"], "Content-Type": ["application/json; charset=utf-8"], "X-Accepted-OAuth-Scopes": [""], "X-RateLimit-Reset": ["1533211541"]}, "status": {"message": "OK", "code": 200}, "url": "https://api.github.com/user"}, "recorded_at": "2018-08-02T11:26:03"}, {"request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Accept-Encoding": ["gzip, deflate"], "Accept": ["application/vnd.github.v3.full+json"], "User-Agent": ["github3.py/1.1.0"], "Accept-Charset": ["utf-8"], "Connection": ["keep-alive"], "Content-Type": ["application/json"], "Authorization": ["token <AUTH_TOKEN>"]}, "method": "GET", "uri": "https://api.github.com/user/repository_invitations?per_page=100"}, "response": {"body": {"string": "", "base64_string": "H4sIAAAAAAAAA+2Za2/iOBSG/wrK16WYW2cpUjWqtLMjqoXOhe5Ws1ohJzHEbWJnbYeKRv3ve2yHJFBNgLrSfqlU9ZL6PHlz7OMcv/ydezT0xr3u4Hz44bzb9hgPyUJf8qa/TR5v4us4+HzxhO++rQMWX/vsRxp+/r2L/7rIpvOrp9n8a382v7r02p4gKZdUcbHxxhZ6cf7rqHc+HO5CP+1BH85n82A4nd92Z1eXGsRwQuD2lMmUCnIW8nvJGVxfZnG8KP6Z0CDCCZfoxSj+yIjQCmK+ogw426FA0I/VH4Gsi8Gupq8f/rybxcH9ZHgzvx3czKdaB15jhcUiEzFQIqVSOUbIXpSDzoqqKPMzSUTAmSJMdQKeoAwV/I/ryyEwVqKgmIzChT1aSguQjQaaRDXBkUriPQH2vmZ4beCSxzF/hOh9uU03QGWUzq4hULZ6BQGicsRVRCBb8AjP+sGpVKeJMRE50j9g/WmGhPQLEp4kqIgBOXodPOdIL0sDy3wZCJoqytlpwnYigcTFCjP6hE8nQaQEgKmUk57KREAkWcNCOy3UhuQoFXSNg41OhSABoWtI7Ctwe7FAU5tUl+stTLxOM1VkgcNEl94Sx5I8tz1zawWDzIU21NIxq/plaYeknEG44WT2/cvk26czmZKALmnQEllMZEvxlhKYySUXSWspeNK6m/6hr15/v5m1MAtbPg4eOiAVRjx4YyUykNRYlib7ZbW91KVBByalEQF1CABQ9EA2ThwdnyP4XhRQADWNfS4w7MpO4B1Qjup/6gWlCE6c+AYAoIhzt0waAIColBk5an03z4zhSLQtIpYlvt3fjimdZrQlgFYsJV0xQpwyWEJytN2CfSiDIHLDbhk5sr+Z2cYrJ6k6HjB+zH0nDrwJkYHkSEbYvnTUwlWdpmrGDlSQpbNUzSihSjjOt5GpISUS3nwKpt5J55aB8iKjMWarDK/cqCUEZl2/n1f46WCn0lw7FQWQuv8S1M/cN7mKo5XahgHq3S2lFaaCmi6kuQM4kIBaN2NSkCT0UGPQTCwQO8v+DbB6ne6j9d+H+5jDcjUjR9WebDf9gu6S3WLX3+qs36No9p2WxJaB8l9SrCK9c8GtUiyIi+gCgXIfQ9fV6XTyiGDTRydEOFawJQAKiyCC1tFFZ75l6A4NK9OiL7XMEFr2mOPQKbclBIB2Gl20WkJ9/lM4hDoJNIA6MaHQtirO3PbYilJnM650a3zMUaW53HZA+UdJWUDaOI7bsGoVDSisYzgE6lmEhpO4ZcgS4DHAB7DHlZjAknbKuiCWkSN7tAxJGvON8y5Uw3hw0KFsDccfOOhU7sM9Dv7NiKBEr0fwAAajXn+0Z7Ns3YcpuCCT4dS6IE3uQ/+n7oPFO5gPdb3N57T6yJPshzLw9f7DPsLFgKhYTg5EhXk7C6LGrLsXsJRO9iAq1KkmRBVptgi4+TFnH30IkaiKfRsboq5lx8MAVYd9CFue7+Zg6WW+m4Pv5qDpof9Pc5CIBNos7cqCqfco4AUKxRwIAo5huMAKLva7vdFZF776815v3B+Mu4MfMKbRs9P7j/F+7UcSC1P7tg9C2w87gNH8ivvJpwzgAJYw7/mf/wB9sVVYQxkAAA==", "encoding": "utf-8"}, "headers": {"X-XSS-Protection": ["1; mode=block"], "Content-Security-Policy": ["default-src 'none'"], "Access-Control-Expose-Headers": ["ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval"], "Transfer-Encoding": ["chunked"], "Access-Control-Allow-Origin": ["*"], "X-Frame-Options": ["deny"], "Status": ["200 OK"], "X-GitHub-Request-Id": ["88EE:3FAB:10C7CC5:2446B27:5B62EA4B"], "ETag": ["W/\"c36a06096decdef54b48591988b79b89\""], "Date": ["Thu, 02 Aug 2018 11:26:03 GMT"], "X-RateLimit-Remaining": ["4995"], "Strict-Transport-Security": ["max-age=31536000; includeSubdomains; preload"], "Server": ["GitHub.com"], "X-OAuth-Scopes": ["read:user, repo:invite"], "X-GitHub-Media-Type": ["github.v3; param=full; format=json"], "X-Content-Type-Options": ["nosniff"], "Content-Encoding": ["gzip"], "X-Runtime-rack": ["0.044125"], "Vary": ["Accept, Authorization, Cookie, X-GitHub-OTP"], "X-RateLimit-Limit": ["5000"], "Cache-Control": ["private, max-age=60, s-maxage=60"], "Referrer-Policy": ["origin-when-cross-origin, strict-origin-when-cross-origin"], "Content-Type": ["application/json; charset=utf-8"], "X-Accepted-OAuth-Scopes": ["public_repo, repo, repo:invite"], "X-RateLimit-Reset": ["1533211541"]}, "status": {"message": "OK", "code": 200}, "url": "https://api.github.com/user/repository_invitations?per_page=100"}, "recorded_at": "2018-08-02T11:26:03"}], "recorded_with": "betamax/0.8.1"}
Loading
0