# -*- coding: utf-8 -*-
"""This module contains everything relating to Users."""
from __future__ import unicode_literals
from json import dumps
from github3.auths import Authorization
from uritemplate import URITemplate
from . import models
from .decorators import requires_auth
from .events import Event
class GPGKey(models.GitHubCore):
"""The object representing a user's GPG key.
.. versionadded:: 1.2.0
Please see GitHub's `GPG Key Documentation` for more information.
.. _GPG Key Documentation:
https://developer.github.com/v3/users/gpg_keys/
.. attribute:: can_certify
Whether this GPG key can be used to sign a key.
.. attribute:: can_encrypt_comms
Whether this GPG key can be used to encrypt communications.
.. attribute:: can_encrypt_storage
Whether this GPG key can be used to encrypt storage.
.. attribute:: can_sign
Whether this GPG key can be used to sign some data.
.. attribute:: created_at
A :class:`~datetime.datetime` representing the date and time when
this GPG key was created.
.. attribute:: emails
A list of :class:`~github3.users.ShortEmail` attached to this GPG
key.
.. attribute:: expires_at
A :class:`~datetime.datetime` representing the date and time when
this GPG key will expire.
.. attribute:: id
The unique identifier of this GPG key.
.. attribute:: key_id
A hexadecimal string that identifies this GPG key.
.. attribute:: primary_key_id
The unique identifier of the primary key of this GPG key.
.. attribute:: public_key
The public key contained in this GPG key. This is not a GPG formatted
key, and is not suitable to be used directly in programs like GPG.
.. attribute:: subkeys
A list of :class:`~github3.users.GPGKey` of the subkeys of this GPG
key.
"""
def _update_attributes(self, key):
self.can_certify = key["can_certify"]
self.can_encrypt_comms = key["can_encrypt_comms"]
self.can_encrypt_storage = key["can_encrypt_storage"]
self.can_sign = key["can_sign"]
self.created_at = self._strptime(key["created_at"])
self.emails = [ShortEmail(email, self) for email in key["emails"]]
self.expires_at = self._strptime(key["expires_at"])
self.id = key["id"]
self.key_id = key["key_id"]
self.primary_key_id = key["primary_key_id"]
self.public_key = key["public_key"]
self.subkeys = [GPGKey(subkey, self) for subkey in key["subkeys"]]
def _repr(self):
return "".format(self.key_id)
def __str__(self):
return self.key_id
@requires_auth
def delete(self):
"""Delete this GPG key.
:returns:
True if successful, False otherwise
:rtype:
bool
"""
url = self._build_url("user", "gpg_keys", self.id)
return self._boolean(self._delete(url), 204, 404)
class Key(models.GitHubCore):
"""The object representing a user's SSH key.
Please see GitHub's `Key Documentation`_ for more information.
.. _Key Documentation:
http://developer.github.com/v3/users/keys/
.. versionchanged:: 1.0.0
Removed ``title`` attribute
.. attribute:: key
A string containing the actual text of the SSH Key
.. attribute:: id
GitHub's unique ID for this key
"""
def _update_attributes(self, key, session=None):
self._api = key.get("url")
self.key = key["key"]
self.id = key["id"]
def _repr(self):
return "".format(self.title)
def __str__(self):
return self.key
@requires_auth
def delete(self):
"""Delete this key."""
return self._boolean(self._delete(self._api), 204, 404)
@requires_auth
def update(self, title, key):
"""Update this key.
.. warning::
As of 20 June 2014, the API considers keys to be immutable.
This will soon begin to return MethodNotAllowed errors.
:param str title: (required), title of the key
:param str key: (required), text of the key file
:returns: bool
"""
json = None
if title and key:
data = {"title": title, "key": key}
json = self._json(self._patch(self._api, data=dumps(data)), 200)
if json:
self._update_attributes(json)
return True
return False
class Plan(models.GitHubCore):
"""The :class:`Plan ` object.
Please see GitHub's `Authenticated User`_ documentation for more details.
.. _Authenticated User:
http://developer.github.com/v3/users/#get-the-authenticated-user
.. attribute:: collaborators_count
.. versionchanged:: 1.0.0
The number of collaborators allowed on this plan
.. attribute:: name
The name of the plan on GitHub
.. attribute:: private_repos_count
.. versionchanged:: 1.0.0
The number of allowed private repositories
.. attribute:: space
The amount of space allotted by this plan
"""
def _update_attributes(self, plan):
self.collaborators = plan["collaborators"]
self.name = plan["name"]
self.private_repos_count = plan["private_repos"]
self.space = plan["space"]
def _repr(self):
return "".format(self.name) # (No coverage)
def __str__(self):
return self.name
def is_free(self):
"""Check if this is a free plan.
:returns: bool
"""
return self.name == "free" # (No coverage)
class _Email(models.GitHubCore):
"""Base email object."""
class_name = "_Email"
def _update_attributes(self, email):
self.email = email["email"]
self.verified = email["verified"]
def _repr(self):
return "<{0} [{1}]>".format(self.class_name, self.email)
def __str__(self):
return self.email
class ShortEmail(_Email):
"""The object used to represent an email attached to a GPG key.
This object has the following attributes:
.. attribute:: email
The email address as a string
.. attribute:: verified
A boolean value representing whether the address has been verified or
not
"""
class_name = "ShortEmail"
class Email(_Email):
"""The object used to represent an AuthenticatedUser's email.
Please see GitHub's `Emails documentation`_ for more information.
.. _Emails documentation:
https://developer.github.com/v3/users/emails/
This object has all of the attributes of :class:`ShortEmail` as well as
the following attributes:
.. attribute:: primary
A boolean value representing whether the address is the primary
address for the user or not
.. attribute:: visibility
A string value representing whether an authenticated user can view the
email address. Use ``public`` to allow it, ``private`` to disallow it.
"""
def _update_attributes(self, email):
super(Email, self)._update_attributes(email)
self.primary = email["primary"]
self.visibility = email["visibility"]
def _repr(self):
return "".format(self.email)
def __str__(self):
return self.email
class _User(models.GitHubCore):
"""The :class:`User ` object.
This handles and structures information in the `User section`_.
Two user instances can be checked like so::
u1 == u2
u1 != u2
And is equivalent to::
u1.id == u2.id
u1.id != u2.id
.. _User section:
http://developer.github.com/v3/users/
"""
class_name = "_User"
def _update_attributes(self, user):
self.avatar_url = user["avatar_url"]
self.events_urlt = URITemplate(user["events_url"])
self.followers_url = user["followers_url"]
self.following_urlt = URITemplate(user["following_url"])
self.gists_urlt = URITemplate(user["gists_url"])
self.gravatar_id = user["gravatar_id"]
self.html_url = user["html_url"]
self.id = user["id"]
self.login = user["login"]
self.organizations_url = user["organizations_url"]
self.received_events_url = user["received_events_url"]
self.repos_url = user["repos_url"]
self.site_admin = user.get("site_admin")
self.starred_urlt = URITemplate(user["starred_url"])
self.subscriptions_url = user["subscriptions_url"]
self.type = user["type"]
self.url = self._api = user["url"]
self._uniq = self.id
def __str__(self):
return self.login
def _repr(self):
full_name = ""
name = getattr(self, "name", None)
if name is not None:
full_name = ":{}".format(name)
return "<{s.class_name} [{s.login}{full_name}]>".format(
s=self, full_name=full_name
)
def is_assignee_on(self, username, repository):
"""Check if this user can be assigned to issues on username/repository.
:param str username: owner's username of the repository
:param str repository: name of the repository
:returns: True if the use can be assigned, False otherwise
:rtype: :class:`bool`
"""
url = self._build_url(
"repos", username, repository, "assignees", self.login
)
return self._boolean(self._get(url), 204, 404)
def is_following(self, username):
"""Check if this user is following ``username``.
:param str username: (required)
:returns: bool
"""
url = self.following_urlt.expand(other_user=username)
return self._boolean(self._get(url), 204, 404)
def events(self, public=False, number=-1, etag=None):
r"""Iterate over events performed by this user.
:param bool public: (optional), only list public events for the
authenticated user
:param int number: (optional), number of events to return. Default: -1
returns all available events.
:param str etag: (optional), ETag from a previous request to the same
endpoint
:returns: generator of :class:`Event `\ s
"""
path = ["events"]
if public:
path.append("public")
url = self._build_url(*path, base_url=self._api)
return self._iter(int(number), url, Event, etag=etag)
def followers(self, number=-1, etag=None):
r"""Iterate over the followers of this user.
:param int number: (optional), number of followers to return. Default:
-1 returns all available
:param str etag: (optional), ETag from a previous request to the same
endpoint
:returns: generator of :class:`User `\ s
"""
url = self._build_url("followers", base_url=self._api)
return self._iter(int(number), url, ShortUser, etag=etag)
def following(self, number=-1, etag=None):
r"""Iterate over the users being followed by this user.
:param int number: (optional), number of users to return. Default: -1
returns all available users
:param str etag: (optional), ETag from a previous request to the same
endpoint
:returns: generator of :class:`User `\ s
"""
url = self._build_url("following", base_url=self._api)
return self._iter(int(number), url, ShortUser, etag=etag)
def gpg_keys(self, number=-1, etag=None):
r"""Iterate over the GPG keys of this user.
.. versionadded:: 1.2.0
:param int number: (optional), number of GPG keys to return. Default:
-1 returns all available GPG keys
:param str etag: (optional), ETag from a previous request to the same
endpoint
:returns: generator of :class:`GPGKey `\ s
"""
url = self._build_url("gpg_keys", base_url=self._api)
return self._iter(int(number), url, GPGKey, etag=etag)
def keys(self, number=-1, etag=None):
r"""Iterate over the public keys of this user.
.. versionadded:: 0.5
:param int number: (optional), number of keys to return. Default: -1
returns all available keys
:param str etag: (optional), ETag from a previous request to the same
endpoint
:returns: generator of :class:`Key `\ s
"""
url = self._build_url("keys", base_url=self._api)
return self._iter(int(number), url, Key, etag=etag)
@requires_auth
def organization_events(self, org, number=-1, etag=None):
r"""Iterate over events from the user's organization dashboard.
.. note:: You must be authenticated to view this.
:param str org: (required), name of the organization
:param int number: (optional), number of events to return. Default: -1
returns all available events
:param str etag: (optional), ETag from a previous request to the same
endpoint
:returns: generator of :class:`Event `\ s
"""
url = ""
if org:
url = self._build_url("events", "orgs", org, base_url=self._api)
return self._iter(int(number), url, Event, etag=etag)
def received_events(self, public=False, number=-1, etag=None):
r"""Iterate over events that the user has received.
If the user is the authenticated user, you will see private and public
events, otherwise you will only see public events.
:param bool public: (optional), determines if the authenticated user
sees both private and public or just public
:param int number: (optional), number of events to return. Default: -1
returns all events available
:param str etag: (optional), ETag from a previous request to the same
endpoint
:returns: generator of :class:`Event `\ s
"""
path = ["received_events"]
if public:
path.append("public")
url = self._build_url(*path, base_url=self._api)
return self._iter(int(number), url, Event, etag=etag)
def organizations(self, number=-1, etag=None):
r"""Iterate over organizations the user is member of.
:param int number: (optional), number of organizations to return.
Default: -1 returns all available organization
:param str etag: (optional), ETag from a previous request to the same
endpoint
:returns: generator of
:class:`ShortOrganization `\ s
"""
# Import here, because a toplevel import causes an import loop
from .orgs import ShortOrganization
url = self._build_url("orgs", base_url=self._api)
return self._iter(int(number), url, ShortOrganization, etag=etag)
def starred_repositories(
self, sort=None, direction=None, number=-1, etag=None
):
"""Iterate over repositories starred by this user.
.. versionchanged:: 0.5
Added sort and direction parameters (optional) as per the change in
GitHub's API.
:param int number: (optional), number of starred repos to return.
Default: -1, returns all available repos
:param str sort: (optional), either 'created' (when the star was
created) or 'updated' (when the repository was last pushed to)
:param str direction: (optional), either 'asc' or 'desc'. Default:
'desc'
:param str etag: (optional), ETag from a previous request to the same
endpoint
:returns: generator of :class:`~github3.repos.repo.StarredRepository`
"""
from .repos import Repository, StarredRepository
params = {"sort": sort, "direction": direction}
self._remove_none(params)
url = self.starred_urlt.expand(owner=None, repo=None)
return self._iter(
int(number),
url,
StarredRepository,
params,
etag,
headers=Repository.STAR_HEADERS,
)
def subscriptions(self, number=-1, etag=None):
"""Iterate over repositories subscribed to by this user.
:param int number: (optional), number of subscriptions to return.
Default: -1, returns all available
:param str etag: (optional), ETag from a previous request to the same
endpoint
:returns: generator of :class:`Repository `
"""
from .repos import ShortRepository
url = self._build_url("subscriptions", base_url=self._api)
return self._iter(int(number), url, ShortRepository, etag=etag)
@requires_auth
def rename(self, login):
"""Rename the user.
.. note::
This is only available for administrators of a GitHub Enterprise
instance.
:param str login: (required), new name of the user
:returns: bool
"""
url = self._build_url("admin", "users", self.login)
payload = {"login": login}
resp = self._boolean(self._patch(url, data=payload), 202, 403)
return resp
@requires_auth
def impersonate(self, scopes=None):
"""Obtain an impersonation token for the user.
The retrieved token will allow impersonation of the user.
This is only available for admins of a GitHub Enterprise instance.
:param list scopes: (optional), areas you want this token to apply to,
i.e., 'gist', 'user'
:returns: :class:`Authorization `
"""
url = self._build_url("admin", "users", self.login, "authorizations")
data = {}
if scopes:
data["scopes"] = scopes
json = self._json(self._post(url, data=data), 201)
return self._instance_or_null(Authorization, json)
@requires_auth
def revoke_impersonation(self):
"""Revoke all impersonation tokens for the current user.
This is only available for admins of a GitHub Enterprise instance.
:returns: bool -- True if successful, False otherwise
"""
url = self._build_url("admin", "users", self.login, "authorizations")
return self._boolean(self._delete(url), 204, 403)
@requires_auth
def promote(self):
"""Promote a user to site administrator.
This is only available for admins of a GitHub Enterprise instance.
:returns: bool -- True if successful, False otherwise
"""
url = self._build_url("site_admin", base_url=self._api)
return self._boolean(self._put(url), 204, 403)
@requires_auth
def demote(self):
"""Demote a site administrator to simple user.
You can demote any user account except your own.
This is only available for admins of a GitHub Enterprise instance.
:returns: bool -- True if successful, False otherwise
"""
url = self._build_url("site_admin", base_url=self._api)
return self._boolean(self._delete(url), 204, 403)
@requires_auth
def suspend(self):
"""Suspend the user.
This is only available for admins of a GitHub Enterprise instance.
This API is disabled if you use LDAP, check the GitHub API dos for more
information.
:returns: bool -- True if successful, False otherwise
"""
url = self._build_url("suspended", base_url=self._api)
return self._boolean(self._put(url), 204, 403)
@requires_auth
def unsuspend(self):
"""Unsuspend the user.
This is only available for admins of a GitHub Enterprise instance.
This API is disabled if you use LDAP, check the GitHub API dos for more
information.
:returns: bool -- True if successful, False otherwise
"""
url = self._build_url("suspended", base_url=self._api)
return self._boolean(self._delete(url), 204, 403)
@requires_auth
def delete(self):
"""Delete the user.
Per GitHub API documentation, it is often preferable to suspend the
user.
.. note::
This is only available for admins of a GitHub Enterprise instance.
:returns: bool -- True if successful, False otherwise
"""
url = self._build_url("admin", "users", self.login)
return self._boolean(self._delete(url), 204, 403)
class User(_User):
"""Object for the full representation of a User.
GitHub's API returns different amounts of information about users based
upon how that information is retrieved. This object exists to represent
the full amount of information returned for a specific user. For example,
you would receive this class when calling
:meth:`~github3.github.GitHub.user`. To provide a clear distinction
between the types of users, github3.py uses different classes with
different sets of attributes.
This object no longer contains information about the currently
authenticated user (e.g., :meth:`~github3.github.GitHub.me`).
.. versionchanged:: 1.0.0
This object contains all of the attributes available on
:class:`~github3.users.ShortUser` as well as the following:
.. attribute:: bio
The markdown formatted User's biography
.. attribute:: blog
The URL of the user's blog
.. attribute:: company
The name or GitHub handle of the user's company
.. attribute:: created_at
A parsed :class:`~datetime.datetime` object representing the date the
user was created
.. attribute:: email
The email address the user has on their public profile page
.. attribute:: followers_count
The number of followers of this user
.. attribute:: following_count
The number of users this user follows
.. attribute:: hireable
Whether or not the user has opted into GitHub jobs advertising
.. attribute:: location
The location specified by the user on their public profile
.. attribute:: name
The name specified by their user on their public profile
.. attribute:: public_gists_count
The number of public gists owned by this user
.. attribute: public_repos_count
The number of public repositories owned by this user
.. attribute:: updated_at
A parsed :class:`~datetime.datetime` object representing the date
the user was last updated
"""
class_name = "User"
def _update_attributes(self, user):
super(User, self)._update_attributes(user)
self.bio = user["bio"]
self.blog = user["blog"]
self.company = user["company"]
self.created_at = self._strptime(user["created_at"])
self.email = user["email"]
self.followers_count = user["followers"]
self.following_count = user["following"]
self.hireable = user["hireable"]
self.location = user["location"]
self.name = user["name"]
self.public_gists_count = user["public_gists"]
self.public_repos_count = user["public_repos"]
self.updated_at = self._strptime(user["updated_at"])
class ShortUser(_User):
"""Object for the shortened representation of a User.
GitHub's API returns different amounts of information about users based
upon how that information is retrieved. Often times, when iterating over
several users, GitHub will return less information. To provide a clear
distinction between the types of users, github3.py uses different classes
with different sets of attributes.
.. versionadded:: 1.0.0
.. attribute:: avatar_url
The URL of the avatar (possibly from Gravatar)
.. attribute:: events_urlt
A URITemplate object from ``uritemplate`` that can be used to generate
an events URL
.. attribute:: followers_url
A string representing the resource to retrieve a User's followers
.. attribute:: following_urlt
A URITemplate object from ``uritemplate`` that can be used to generate
the URL to check if this user is following ``other_user``
.. attribute:: gists_urlt
A URITemplate object from ``uritemplate`` that can be used to generate
the URL to retrieve a Gist by its id
.. attribute:: gravatar_id
The identifier for the user's gravatar
.. attribute:: html_url
The URL of the user's publicly visible profile. For example,
``https://github.com/sigmavirus24``
.. attribute:: id
The unique ID of the account
.. attribute:: login
The username of the user, e.g., ``sigmavirus24``
.. attribute:: organizations_url
A string representing the resource to retrieve the organizations to
which a user belongs
.. attribute:: received_events_url
A string representing the resource to retrieve the events a user
received
.. attribute:: repos_url
A string representing the resource to list a user's repositories
.. attribute:: site_admin
A boolean attribute indicating whether the user is a member of
GitHub's staff
.. attribute:: starred_urlt
A URITemplate object from ``uritemplate`` that can be used to generate
a URL to retrieve whether the user has starred a repository.
.. attribute:: subscriptions_url
A string representing the resource to list a user's subscriptions
.. attribute:: type
A string representing the type of User account this. In all cases
should be "User"
.. attribute:: url
A string of this exact resource retrievable from GitHub's API
"""
class_name = "ShortUser"
_refresh_to = User
class AuthenticatedUser(User):
"""Object to represent the currently authenticated user.
This is returned by :meth:`~github3.github.GitHub.me`. It contains the
extra informtation that is not returned for other users such as the
currently authenticated user's plan and private email information.
.. versionadded:: 1.0.0
.. versionchanged:: 1.0.0
The ``total_private_gists`` attribute is no longer returned by
GitHub's API and so is removed.
This object has all of the same attribute as the
:class:`~github3.users.ShortUser` and :class:`~github3.users.User` objects
as well as:
.. attribute:: disk_usage
The amount of repository space that has been used by you, the user
.. attribute:: owned_private_repos_count
The number of private repositories owned by you, the user
.. attribute:: plan
.. note::
When used with a Github Enterprise instance <= 2.12.7, this
attribute will not be returned. To handle these situations
sensitively, the attribute will be set to ``None``.
Repositories may still have a license associated with them
in these cases.
The name of the plan that you, the user, have purchased
"""
class_name = "AuthenticatedUser"
def _update_attributes(self, user):
super(AuthenticatedUser, self)._update_attributes(user)
self.disk_usage = user["disk_usage"]
self.owned_private_repos_count = user["owned_private_repos"]
self.total_private_repos_count = user["total_private_repos"]
self.plan = user.get("plan")
if self.plan is not None:
self.plan = Plan(self.plan, self)
class Collaborator(_User):
"""Object for the representation of a collaborator.
.. versionadded:: 1.1.0
When retrieving a repository's contributors, GitHub returns the same
information as a :class:`~github3.users.ShortUser` with an additional
attribute:
.. attribute:: permissions
Admin, push, and pull permissions of a collaborator
"""
class_name = "Collaborator"
_refresh_to = User
def _update_attributes(self, user):
super(Collaborator, self)._update_attributes(user)
self.permissions = user["permissions"]
class Contributor(_User):
"""Object for the specialized representation of a contributor.
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.0
This class now refreshes to a :class:`~github3.users.User`.
The attribute ``contributions`` was renamed to ``contributions_count``,
the documentation already declared it as ``contributions_count``, it
was the implementation now reflects this as well.
When retrieving a repository's contributors, GitHub returns the same
information as a :class:`~github3.users.ShortUser` with an additional
attribute:
.. attribute:: contributions_count
The number of contributions a contributor has made to the repository
"""
class_name = "Contributor"
_refresh_to = User
def _update_attributes(self, contributor):
super(Contributor, self)._update_attributes(contributor)
self.contributions_count = contributor["contributions"]