def test_default_value(self): uri = 'https://api.github.com/user{/user=sigmavirus24}' t = URITemplate(uri) self.assertEqual(t.expand(), 'https://api.github.com/user/sigmavirus24') self.assertEqual(t.expand(user='******'), 'https://api.github.com/user/lukasa')
def test_no_variables_in_uri(self): """ This test ensures that if there are no variables present, the template evaluates to itself. """ uri = 'https://api.github.com/users' t = URITemplate(uri) self.assertEqual(t.expand(), uri) self.assertEqual(t.expand(users='foo'), uri)
def test_expand(self): """ This test ensures that expansion works as expected. """ # Single t = URITemplate("https://api.github.com/users{/user}") expanded = "https://api.github.com/users/sigmavirus24" self.assertEqual(t.expand(user="******"), expanded) v = t.variables[0] self.assertEqual(v.expand({"user": None}), {"/user": ""}) # Multiple t = URITemplate("https://api.github.com/users{/user}{/repo}") expanded = "https://api.github.com/users/sigmavirus24/github3.py" self.assertEqual(t.expand({"repo": "github3.py"}, user="******"), expanded)
def test_expand(self): """ This test ensures that expansion works as expected. """ # Single t = URITemplate('https://api.github.com/users{/user}') expanded = 'https://api.github.com/users/sigmavirus24' self.assertEqual(t.expand(user='******'), expanded) v = t.variables[0] self.assertEqual(v.expand({'user': None}), {'/user': ''}) # Multiple t = URITemplate('https://api.github.com/users{/user}{/repo}') expanded = 'https://api.github.com/users/sigmavirus24/github3.py' self.assertEqual( t.expand({'repo': 'github3.py'}, user='******'), expanded )
class PriceURL: """ PriceURl implements an easily readible, funcational, and modifiable URL for retreiving prices :param item_type: :param item_number: :param =color_id: Usage: url_template = PriceURL() uri = url_template.expand(item_type=itemtypeID, item_number=itemID, color_id=itemColorID) 'https://www.bricklink.com/catalogPG.asp?P=3004&colorID=8' """ url = ('https://www.bricklink.com/catalogPG.asp?' '{item_type} = {item_number} &' 'colorID = {color_id}' ) def __init__(self): self.raw_url = PriceURL.url.replace(" ", "") # Spaces improved readability self.url_template = URITemplate(self.raw_url) def expand(self, itemtypeID, itemID, itemColorID): self.url = self.url_template.expand(item_type=itemtypeID, item_number=itemID, color_id=itemColorID) return self.url
def _test_(self): for k, v in d.items(): t = URITemplate(k) self.assertEqual(t.expand(v['expansion']), v['expected'])
def __get_url(self, path, id): rootUrl = self.hal.get_root_url() template = URITemplate(rootUrl + path); return template.expand(id= str(id) if id is not None else "")
def test_default_value(self): uri = "https://api.github.com/user{/user=sigmavirus24}" t = URITemplate(uri) self.assertEqual(t.expand(), "https://api.github.com/user/sigmavirus24") self.assertEqual(t.expand(user="******"), "https://api.github.com/user/lukasa")
class _User(models.GitHubCore): """The :class:`User <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 <github3.events.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 <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 <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 <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 <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 <github3.events.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 <github3.events.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 <github3.orgs.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 <github3.repos.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 <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 Release(models.GitHubCore): """Representation of a GitHub release. It holds the information GitHub returns about a release from a :class:`Repository <github3.repos.repo.Repository>`. Please see GitHub's `Releases Documentation`_ for more information. This object has the following attributes: .. attribute:: original_assets A list of :class:`~github3.repos.release.Asset` objects representing the assets uploaded for this relesae. .. attribute:: assets_url The URL to retrieve the assets from the API. .. attribute:: author A :class:`~github3.users.ShortUser` representing the creator of this release. .. attribute:: body The description of this release as written by the release creator. .. attribute:: created_at A :class:`~datetime.datetime` object representing the date and time when this release was created. .. attribute:: draft A boolean attribute describing whether this release is a draft. .. attribute:: html_url The URL to view this release in a browser. .. attribute:: id The unique identifier of this release. .. attribute:: name The name given to this release by the :attr:`author`. .. attribute:: prerelease A boolean attribute indicating whether the release is a pre-release. .. attribute:: published_at A :class:`~datetime.datetime` object representing the date and time when this release was publisehd. .. attribute:: tag_name The name of the tag associated with this release. .. attribute:: tarball_url The URL to retrieve a GitHub generated tarball for this release from the API. .. attribute:: target_commitish The reference (usually a commit) that is targetted by this release. .. attribute:: upload_urlt A :class:`~uritemplate.URITemplate` object that expands to form the URL to upload assets to. .. attribute:: zipball_url The URL to retrieve a GitHub generated zipball for this release from the API. .. _Releases Documentation: https://developer.github.com/v3/repos/releases/ """ def _update_attributes(self, release): self._api = self.url = release["url"] self.original_assets = [Asset(i, self) for i in release["assets"]] self.assets_url = release["assets_url"] self.author = users.ShortUser(release["author"], self) self.body = release["body"] self.created_at = self._strptime(release["created_at"]) self.draft = release["draft"] self.html_url = release["html_url"] self.id = release["id"] self.name = release["name"] self.prerelease = release["prerelease"] self.published_at = self._strptime(release["published_at"]) self.tag_name = release["tag_name"] self.tarball_url = release["tarball_url"] self.target_commitish = release["target_commitish"] self.upload_urlt = URITemplate(release["upload_url"]) self.zipball_url = release["zipball_url"] def _repr(self): return "<Release [{0}]>".format(self.name) def archive(self, format, path=""): """Get the tarball or zipball archive for this release. :param str format: (required), accepted values: ('tarball', 'zipball') :param path: (optional), path where the file should be saved to, default is the filename provided in the headers and will be written in the current directory. It can take a file-like object as well :type path: str, file :returns: True if successful, False otherwise :rtype: bool """ resp = None if format in ("tarball", "zipball"): repo_url = self._api[: self._api.rfind("/releases")] url = self._build_url(format, self.tag_name, base_url=repo_url) resp = self._get(url, allow_redirects=True, stream=True) if resp and self._boolean(resp, 200, 404): utils.stream_response_to_file(resp, path) return True return False def asset(self, asset_id): """Retrieve the asset from this release with ``asset_id``. :param int asset_id: ID of the Asset to retrieve :returns: the specified asset, if it exists :rtype: :class:`~github3.repos.release.Asset` """ json = None if int(asset_id) > 0: i = self._api.rfind("/") url = self._build_url( "assets", str(asset_id), base_url=self._api[:i] ) json = self._json(self._get(url), 200) return self._instance_or_null(Asset, json) def assets(self, number=-1, etag=None): """Iterate over the assets available for this release. :param int number: (optional), Number of assets to return :param str etag: (optional), last ETag header sent :returns: generator of asset objects :rtype: :class:`~github3.repos.release.Asset` """ url = self._build_url("assets", base_url=self._api) return self._iter(number, url, Asset, etag=etag) @requires_auth def delete(self): """Delete this release. Only users with push access to the repository can delete a release. :returns: True if successful; False if not successful :rtype: bool """ url = self._api return self._boolean(self._delete(url), 204, 404) @requires_auth def edit( self, tag_name=None, target_commitish=None, name=None, body=None, draft=None, prerelease=None, ): """Edit this release. Only users with push access to the repository can edit a release. If the edit is successful, this object will update itself. :param str tag_name: (optional), Name of the tag to use :param str target_commitish: (optional), The "commitish" value that determines where the Git tag is created from. Defaults to the repository's default branch. :param str name: (optional), Name of the release :param str body: (optional), Description of the release :param boolean draft: (optional), True => Release is a draft :param boolean prerelease: (optional), True => Release is a prerelease :returns: True if successful; False if not successful :rtype: bool """ url = self._api data = { "tag_name": tag_name, "target_commitish": target_commitish, "name": name, "body": body, "draft": draft, "prerelease": prerelease, } self._remove_none(data) r = self.session.patch(url, data=json.dumps(data)) successful = self._boolean(r, 200, 404) if successful: # If the edit was successful, let's update the object. self._update_attributes(r.json()) return successful @requires_auth def upload_asset(self, content_type, name, asset, label=None): """Upload an asset to this release. .. note:: All parameters are required. :param str content_type: The content type of the asset. Wikipedia has a list of common media types :param str name: The name of the file :param asset: The file or bytes object to upload. :param label: (optional), An alternate short description of the asset. :returns: the created asset :rtype: :class:`~github3.repos.release.Asset` """ headers = {"Content-Type": content_type} params = {"name": name, "label": label} self._remove_none(params) url = self.upload_urlt.expand(params) r = self._post(url, data=asset, json=False, headers=headers) if r.status_code in (201, 202): return Asset(r.json(), self) raise error_for(r)
class User(BaseAccount): """The :class:`User <User>` object. This handles and structures information in the `User section <http://developer.github.com/v3/users/>`_. Two user instances can be checked like so:: u1 == u2 u1 != u2 And is equivalent to:: u1.id == u2.id u1.id != u2.id """ def _update_attributes(self, user): super(User, self)._update_attributes(user) if not self.type: self.type = 'User' #: ID of the user's image on Gravatar self.gravatar_id = user.get('gravatar_id', '') #: True -- for hire, False -- not for hire self.hireable = user.get('hireable', False) # The number of public_gists #: Number of public gists self.public_gists = user.get('public_gists', 0) # Private information #: How much disk consumed by the user self.disk_usage = user.get('disk_usage', 0) #: Number of private repos owned by this user self.owned_private_repos = user.get('owned_private_repos', 0) #: Number of private gists owned by this user self.total_private_gists = user.get('total_private_gists', 0) #: Total number of private repos self.total_private_repos = user.get('total_private_repos', 0) #: Which plan this user is on self.plan = Plan(user.get('plan', {})) events_url = user.get('events_url', '') #: Events URL Template. Expands with ``privacy`` self.events_urlt = URITemplate(events_url) if events_url else None #: Followers URL (not a template) self.followers_url = user.get('followers_url', '') furl = user.get('following_url', '') #: Following URL Template. Expands with ``other_user`` self.following_urlt = URITemplate(furl) if furl else None gists_url = user.get('gists_url', '') #: Gists URL Template. Expands with ``gist_id`` self.gists_urlt = URITemplate(gists_url) if gists_url else None #: Organizations URL (not a template) self.organizations_url = user.get('organizations_url', '') #: Received Events URL (not a template) self.received_events_url = user.get('received_events_url', '') #: Repostories URL (not a template) self.repos_url = user.get('repos_url', '') starred_url = user.get('starred_url', '') #: Starred URL Template. Expands with ``owner`` and ``repo`` self.starred_urlt = URITemplate(starred_url) if starred_url else None #: Subscriptions URL (not a template) self.subscriptions_url = user.get('subscriptions_url', '') #: Number of repo contributions. Only appears in ``repo.contributors`` contributions = user.get('contributions') # The refresh method uses __init__ to replace the attributes on the # instance with what it receives from the /users/:username endpoint. # What that means is that contributions is no longer returned and as # such is changed because it doesn't exist. This guards against that. if contributions is not None: self.contributions = contributions self._uniq = user.get('id', None) def __str__(self): return self.login @requires_auth def add_email_address(self, address): """Add the single email address to the authenticated user's account. :param str address: (required), email address to add :returns: list of email addresses """ return self.add_email_addresses([address]) @requires_auth def add_email_addresses(self, addresses=[]): """Add the email addresses in ``addresses`` to the authenticated user's account. :param list addresses: (optional), email addresses to be added :returns: list of email addresses """ json = [] if addresses: url = self._build_url('user', 'emails') json = self._json(self._post(url, data=addresses), 201) return json @requires_auth def delete_email_address(self, address): """Delete the email address from the user's account. :param str address: (required), email address to delete :returns: bool """ return self.delete_email_addresses([address]) @requires_auth def delete_email_addresses(self, addresses=[]): """Delete the email addresses in ``addresses`` from the authenticated user's account. :param list addresses: (optional), email addresses to be removed :returns: bool """ url = self._build_url('user', 'emails') return self._boolean(self._delete(url, data=dumps(addresses)), 204, 404) 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): """Checks 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): """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 <github3.events.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): """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 <User>`\ s """ url = self._build_url('followers', base_url=self._api) return self._iter(int(number), url, User, etag=etag) def following(self, number=-1, etag=None): """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 <User>`\ s """ url = self._build_url('following', base_url=self._api) return self._iter(int(number), url, User, etag=etag) def keys(self, number=-1, etag=None): """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 <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): """Iterate over events as they appear on the user's organization dashboard. 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 <github3.events.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): """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 <github3.events.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): """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:`Event <github3.orgs.Organization>`\ s """ # Import here, because a toplevel import causes an import loop from .orgs import Organization url = self._build_url('orgs', base_url=self._api) return self._iter(int(number), url, Organization, 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:`Repository <github3.repos.Repository>` """ from .repos import Repository params = {'sort': sort, 'direction': direction} self._remove_none(params) url = self.starred_urlt.expand(owner=None, repo=None) return self._iter(int(number), url, Repository, params, etag) 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 <github3.repos.Repository>` """ from .repos import Repository url = self._build_url('subscriptions', base_url=self._api) return self._iter(int(number), url, Repository, etag=etag)
class Release(GitHubCore): """The :class:`Release <Release>` object. It holds the information GitHub returns about a release from a :class:`Repository <github3.repos.repo.Repository>`. """ CUSTOM_HEADERS = {'Accept': 'application/vnd.github.manifold-preview'} def __init__(self, release, session=None): super(Release, self).__init__(release, session) self._api = release.get('url') #: List of :class:`Asset <Asset>` objects for this release self.assets = [Asset(i, self) for i in release.get('assets', [])] #: URL for uploaded assets self.assets_url = release.get('assets_url') #: Body of the release (the description) self.body = release.get('body') #: Date the release was created self.created_at = self._strptime(release.get('created_at')) #: Boolean whether value is True or False self.draft = release.get('draft') #: HTML URL of the release self.html_url = release.get('html_url') #: GitHub id self.id = release.get('id') #: Name given to the release self.name = release.get('name') #; Boolean whether release is a prerelease self.prerelease = release.get('prerelease') #: Date the release was published self.published_at = self._strptime(release.get('published_at')) #: Name of the tag self.tag_name = release.get('tag_name') #: "Commit" that this release targets self.target_commitish = release.get('target_commitish') upload_url = release.get('upload_url') #: URITemplate to upload an asset with self.upload_urlt = URITemplate(upload_url) if upload_url else None def _repr(self): return '<Release [{0}]>'.format(self.name) @requires_auth def delete(self): """Users with push access to the repository can delete a release. :returns: True if successful; False if not successful """ url = self._api return self._boolean( self._delete(url, headers=Release.CUSTOM_HEADERS), 204, 404 ) @requires_auth def edit(self, tag_name=None, target_commitish=None, name=None, body=None, draft=None, prerelease=None): """Users with push access to the repository can edit a release. If the edit is successful, this object will update itself. :param str tag_name: (optional), Name of the tag to use :param str target_commitish: (optional), The "commitish" value that determines where the Git tag is created from. Defaults to the repository's default branch. :param str name: (optional), Name of the release :param str body: (optional), Description of the release :param boolean draft: (optional), True => Release is a draft :param boolean prerelease: (optional), True => Release is a prerelease :returns: True if successful; False if not successful """ url = self._api data = { 'tag_name': tag_name, 'target_commitish': target_commitish, 'name': name, 'body': body, 'draft': draft, 'prerelease': prerelease, } self._remove_none(data) r = self._session.patch( url, data=json.dumps(data), headers=Release.CUSTOM_HEADERS ) successful = self._boolean(r, 200, 404) if successful: # If the edit was successful, let's update the object. self.__init__(r.json(), self) return successful def iter_assets(self, number=-1, etag=None): """Iterate over the assets available for this release. :param int number: (optional), Number of assets to return :param str etag: (optional), last ETag header sent :returns: generator of :class:`Asset <Asset>` objects """ url = self._build_url('assets', base_url=self._api) return self._iter(number, url, Asset, etag=etag) @requires_auth def upload_asset(self, content_type, name, asset): """Upload an asset to this release. All parameters are required. :param str content_type: The content type of the asset. Wikipedia has a list of common media types :param str name: The name of the file :param asset: The file or bytes object to upload. :returns: :class:`Asset <Asset>` """ headers = Release.CUSTOM_HEADERS.copy() headers.update({'Content-Type': content_type}) url = self.upload_urlt.expand({'name': name}) r = self._post(url, data=asset, json=False, headers=headers, verify=False) if r.status_code in (201, 202): return Asset(r.json(), self) raise GitHubError(r)
class _User(models.GitHubCore): """The :class:`User <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 <github3.events.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 <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 <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 <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 <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 <github3.events.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 <github3.events.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 <github3.orgs.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 <github3.repos.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 <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 Release(GitHubCore): """The :class:`Release <Release>` object. It holds the information GitHub returns about a release from a :class:`Repository <github3.repos.repo.Repository>`. """ CUSTOM_HEADERS = {'Accept': 'application/vnd.github.manifold-preview'} def _update_attributes(self, release): self._api = release.get('url') #: List of :class:`Asset <Asset>` objects for this release self.original_assets = [ Asset(i, self) for i in release.get('assets', []) ] #: URL for uploaded assets self.assets_url = release.get('assets_url') #: Body of the release (the description) self.body = release.get('body') #: Date the release was created self.created_at = self._strptime(release.get('created_at')) #: Boolean whether value is True or False self.draft = release.get('draft') #: HTML URL of the release self.html_url = release.get('html_url') #: GitHub id self.id = release.get('id') #: Name given to the release self.name = release.get('name') #: Boolean whether release is a prerelease self.prerelease = release.get('prerelease') #: Date the release was published self.published_at = self._strptime(release.get('published_at')) #: Name of the tag self.tag_name = release.get('tag_name') #: URL to download a tarball of the release self.tarball_url = release.get('tarball_url') #: "Commit" that this release targets self.target_commitish = release.get('target_commitish') upload_url = release.get('upload_url') #: URITemplate to upload an asset with self.upload_urlt = URITemplate(upload_url) if upload_url else None #: URL to download a zipball of the release self.zipball_url = release.get('zipball_url') def _repr(self): return '<Release [{0}]>'.format(self.name) def archive(self, format, path=''): """Get the tarball or zipball archive for this release. :param str format: (required), accepted values: ('tarball', 'zipball') :param path: (optional), path where the file should be saved to, default is the filename provided in the headers and will be written in the current directory. it can take a file-like object as well :type path: str, file :returns: bool -- True if successful, False otherwise """ resp = None if format in ('tarball', 'zipball'): repo_url = self._api[:self._api.rfind('/releases')] url = self._build_url(format, self.tag_name, base_url=repo_url) resp = self._get(url, allow_redirects=True, stream=True) if resp and self._boolean(resp, 200, 404): utils.stream_response_to_file(resp, path) return True return False def asset(self, asset_id): """Retrieve the asset from this release with ``asset_id``. :param int asset_id: ID of the Asset to retrieve :returns: :class:`~github3.repos.release.Asset` """ json = None if int(asset_id) > 0: i = self._api.rfind('/') url = self._build_url('assets', str(asset_id), base_url=self._api[:i]) json = self._json(self._get(url), 200) return self._instance_or_null(Asset, json) def assets(self, number=-1, etag=None): """Iterate over the assets available for this release. :param int number: (optional), Number of assets to return :param str etag: (optional), last ETag header sent :returns: generator of :class:`Asset <Asset>` objects """ url = self._build_url('assets', base_url=self._api) return self._iter(number, url, Asset, etag=etag) @requires_auth def delete(self): """Users with push access to the repository can delete a release. :returns: True if successful; False if not successful """ url = self._api return self._boolean( self._delete(url, headers=Release.CUSTOM_HEADERS), 204, 404 ) @requires_auth def edit(self, tag_name=None, target_commitish=None, name=None, body=None, draft=None, prerelease=None): """Users with push access to the repository can edit a release. If the edit is successful, this object will update itself. :param str tag_name: (optional), Name of the tag to use :param str target_commitish: (optional), The "commitish" value that determines where the Git tag is created from. Defaults to the repository's default branch. :param str name: (optional), Name of the release :param str body: (optional), Description of the release :param boolean draft: (optional), True => Release is a draft :param boolean prerelease: (optional), True => Release is a prerelease :returns: True if successful; False if not successful """ url = self._api data = { 'tag_name': tag_name, 'target_commitish': target_commitish, 'name': name, 'body': body, 'draft': draft, 'prerelease': prerelease, } self._remove_none(data) r = self.session.patch( url, data=json.dumps(data), headers=Release.CUSTOM_HEADERS ) successful = self._boolean(r, 200, 404) if successful: # If the edit was successful, let's update the object. self._update_attributes(r.json()) return successful @requires_auth def upload_asset(self, content_type, name, asset, label=None): """Upload an asset to this release. All parameters are required. :param str content_type: The content type of the asset. Wikipedia has a list of common media types :param str name: The name of the file :param asset: The file or bytes object to upload. :param label: (optional), An alternate short description of the asset. :returns: :class:`Asset <Asset>` """ headers = {'Content-Type': content_type} params = {'name': name, 'label': label} self._remove_none(params) url = self.upload_urlt.expand(params) r = self._post(url, data=asset, json=False, headers=headers) if r.status_code in (201, 202): return Asset(r.json(), self) raise error_for(r)
def main(argv): try: parser = argparse.ArgumentParser(description="Arma Automatic Publishing Script") parser.add_argument('manifest', type=argparse.FileType('r'), help='manifest json file') parser.add_argument('-r', '--release_target', type=str, help="the name of the release target in the manifest file.", default="release") parser.add_argument('-v', '--version', type=str, help="the version of the release archive.", default="") args = parser.parse_args() manifest_file = args.manifest release_target = args.release_target version = args.version.split(".") manifest = json.load(manifest_file) if version == "": version = get_project_version("..\\addons\\\main\\script_version.hpp") if(not "CBA_PUBLISH_CREDENTIALS_PATH" in os.environ): raise Exception("CBA_PUBLISH_CREDENTIALS_PATH is not set in the environment") credentials_path = os.environ["CBA_PUBLISH_CREDENTIALS_PATH"] for destination in manifest['publish'][release_target]['destinations']: if(destination["type"] == "steam"): cred_file = json.load(open(os.path.join(credentials_path, destination["cred_file"]))) if("username" in cred_file and "password" in cred_file): steam_username = cred_file["username"] steam_password = cred_file["password"] start_steam_with_user(steam_username, steam_password) else: raise Exception("Credentials file did not specify a username and password for Steam login") if(not "project_id" in destination): raise Exception("Steam Publish","No project ID defined in manifest for Steam publish") project_id = destination["project_id"] if(not "release_dir" in destination): raise Exception("Steam Publish","No release directory defined in manifest for Steam publish") release_dir = destination["release_dir"] if(not "steam_changelog" in destination): raise Exception("Steam Publish","No steam changelog defined in manifest for Steam publish") steam_changelog = destination["steam_changelog"] steam_publish_folder(release_dir, project_id, version, steam_changelog) close_steam() if(destination["type"] == "sftp"): cred_file = json.load(open(os.path.join(credentials_path, destination["cred_file"]))) if("username" in cred_file and "password" in cred_file): sftp_username = cred_file["username"] sftp_password = cred_file["password"] else: raise Exception("Credentials file did not specify a username and password for SFTP login") if(not "hostname" in destination): raise Exception("SFTP Publish","No hostname was defined for the SFTP site.") hostname = destination["hostname"] if(not "local_path" in destination): raise Exception("SFTP Publish","No local path was defined for the SFTP upload.") local_path = destination["local_path"] if(not "remote_path" in destination): raise Exception("SFTP Publish","No remote path was defined for the SFTP upload.") remote_path = destination["remote_path"] cnopts = pysftp.CnOpts() cnopts.hostkeys = None sftp = pysftp.Connection(host=hostname, username=sftp_username, password=sftp_password, cnopts=cnopts) local_path = local_path.format(major=version[0], minor=version[1], patch=version[2], build=version[3]) remote_path = remote_path.format(major=version[0], minor=version[1], patch=version[2], build=version[3]) print("SFTP: Publishing {} to remote {}:{}".format(local_path, hostname, remote_path)) sftp.put(local_path, remotepath=remote_path) print("SFTP: Upload Complete!") if(destination["type"] == "github"): account = destination["account"] tag_name = destination["tag_name"] branch = destination["branch"] name = destination["name"] body_file = destination["body_file"] local_path = destination["local_path"] prerelease = destination["prerelease"] asset_name = destination["asset_name"] tag_name = tag_name.format(major=version[0], minor=version[1], patch=version[2], build=version[3]) name = name.format(major=version[0], minor=version[1], patch=version[2], build=version[3]) asset_name = asset_name.format(major=version[0], minor=version[1], patch=version[2], build=version[3]) local_path = local_path.format(major=version[0], minor=version[1], patch=version[2], build=version[3]) release_text_file = open(body_file, mode='r') release_text = release_text_file.read() release_text_file.close() create_request = { "tag_name": tag_name, "target_commitish": branch, "name": name, "body": release_text, "draft": False, "prerelease": prerelease } github_token = os.environ["IDI_GITHUB_TOKEN"] release_string = json.dumps(create_request, separators=(',',':')) temp_dir = tempfile.mkdtemp() tmpname = os.path.join(temp_dir,"jsonpost") temp_file = open(tmpname, 'w') temp_file.write(release_string) temp_file.close() curl_string = ' '.join(["curl", '-s', '-H "Authorization: token {}"'.format(github_token), '-H "Content-Type: application/json"', "--request POST", "--data", '"@{}"'.format(tmpname).replace('\\','\\\\'), "https://api.github.com/repos/{}/releases".format(account)]) print("Creating Github Release...") response = subprocess.check_output(curl_string) response_json = json.loads(response.decode("ascii")) shutil.rmtree(temp_dir) if("id" in response_json): print("Github Release Created @ {}".format(response_json["url"])) release_id = response_json["id"] upload_url = response_json["upload_url"] t = URITemplate(upload_url) upload_url = t.expand(name=asset_name) curl_string = ' '.join(["curl", '-s', '-H "Authorization: token {}"'.format(github_token), '-H "Content-Type: application/zip"', "--data-binary", '"@{}"'.format(local_path), upload_url]) print("Attaching Asset...") response = subprocess.check_output(curl_string) response_json = json.loads(response.decode("ascii")) if("browser_download_url" in response_json): print("Asset Attached @ {}".format(response_json["browser_download_url"])) else: print(response_json) raise Exception("Github Publish","Failed to Attach Asset") else: print(response_json) raise Exception("Github Publish","Failed to Create Release") except Exception as e: print(e) sys.exit(1)
class User(BaseAccount): """The :class:`User <User>` object. This handles and structures information in the `User section <http://developer.github.com/v3/users/>`_. Two user instances can be checked like so:: u1 == u2 u1 != u2 And is equivalent to:: u1.id == u2.id u1.id != u2.id """ def __init__(self, user, session=None): super(User, self).__init__(user, session) if not self.type: self.type = 'User' #: ID of the user's image on Gravatar self.gravatar_id = user.get('gravatar_id', '') #: True -- for hire, False -- not for hire self.hireable = user.get('hireable', False) ## The number of public_gists #: Number of public gists self.public_gists = user.get('public_gists', 0) # Private information #: How much disk consumed by the user self.disk_usage = user.get('disk_usage', 0) #: Number of private repos owned by this user self.owned_private_repos = user.get('owned_private_repos', 0) #: Number of private gists owned by this user self.total_private_gists = user.get('total_private_gists', 0) #: Total number of private repos self.total_private_repos = user.get('total_private_repos', 0) #: Which plan this user is on self.plan = Plan(user.get('plan', {})) events_url = user.get('events_url', '') #: Events URL Template. Expands with ``privacy`` self.events_urlt = URITemplate(events_url) if events_url else None #: Followers URL (not a template) self.followers_url = user.get('followers_url', '') furl = user.get('following_url', '') #: Following URL Template. Expands with ``other_user`` self.following_urlt = URITemplate(furl) if furl else None gists_url = user.get('gists_url', '') #: Gists URL Template. Expands with ``gist_id`` self.gists_urlt = URITemplate(gists_url) if gists_url else None #: Organizations URL (not a template) self.organizations_url = user.get('organizations_url', '') #: Received Events URL (not a template) self.received_events_url = user.get('received_events_url', '') #: Repostories URL (not a template) self.repos_url = user.get('repos_url', '') starred_url = user.get('starred_url', '') #: Starred URL Template. Expands with ``owner`` and ``repo`` self.starred_urlt = URITemplate(starred_url) if starred_url else None #: Subscriptions URL (not a template) self.subscriptions_url = user.get('subscriptions_url', '') def __str__(self): return self.login def _update_(self, user): self.__init__(user, self._session) @requires_auth def add_email_address(self, address): """Add the single email address to the authenticated user's account. :param str address: (required), email address to add :returns: list of email addresses """ return self.add_email_addresses([address]) @requires_auth def add_email_addresses(self, addresses=[]): """Add the email addresses in ``addresses`` to the authenticated user's account. :param list addresses: (optional), email addresses to be added :returns: list of email addresses """ json = [] if addresses: url = self._build_url('user', 'emails') json = self._json(self._post(url, data=addresses), 201) return json @requires_auth def delete_email_address(self, address): """Delete the email address from the user's account. :param str address: (required), email address to delete :returns: bool """ return self.delete_email_addresses([address]) @requires_auth def delete_email_addresses(self, addresses=[]): """Delete the email addresses in ``addresses`` from the authenticated user's account. :param list addresses: (optional), email addresses to be removed :returns: bool """ url = self._build_url('user', 'emails') return self._boolean(self._delete(url, data=dumps(addresses)), 204, 404) def is_assignee_on(self, login, repository): """Checks if this user can be assigned to issues on login/repository. :returns: :class:`bool` """ url = self._build_url('repos', login, repository, 'assignees', self.login) return self._boolean(self._get(url), 204, 404) def is_following(self, login): """Checks if this user is following ``login``. :param str login: (required) :returns: bool """ url = self.following_urlt.expand(other_user=login) return self._boolean(self._get(url), 204, 404) def iter_events(self, public=False, number=-1, etag=None): """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: list of :class:`Event <github3.events.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 iter_followers(self, number=-1, etag=None): """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 <User>`\ s """ url = self._build_url('followers', base_url=self._api) return self._iter(int(number), url, User, etag=etag) def iter_following(self, number=-1, etag=None): """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 <User>`\ s """ url = self._build_url('following', base_url=self._api) return self._iter(int(number), url, User, etag=etag) def iter_keys(self, number=-1, etag=None): """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 <Key>`\ s """ url = self._build_url('keys', base_url=self._api) return self._iter(int(number), url, Key, etag=etag) def iter_org_events(self, org, number=-1, etag=None): """Iterate over events as they appear on the user's organization dashboard. 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: list of :class:`Event <github3.events.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 iter_received_events(self, public=False, number=-1, etag=None): """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 <github3.events.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 iter_starred(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:`Repository <github3.repos.Repository>` """ from github3.repos import Repository params = {'sort': sort, 'direction': direction} self._remove_none(params) url = self.starred_urlt.expand(owner=None, repo=None) return self._iter(int(number), url, Repository, params, etag) def iter_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 <github3.repos.Repository>` """ from github3.repos import Repository url = self._build_url('subscriptions', base_url=self._api) return self._iter(int(number), url, Repository, etag=etag) @requires_auth def update(self, name=None, email=None, blog=None, company=None, location=None, hireable=False, bio=None): """If authenticated as this user, update the information with the information provided in the parameters. :param str name: e.g., 'John Smith', not login name :param str email: e.g., '*****@*****.**' :param str blog: e.g., 'http://www.example.com/jsmith/blog' :param str company: :param str location: :param bool hireable: defaults to False :param str bio: GitHub flavored markdown :returns: bool """ user = {'name': name, 'email': email, 'blog': blog, 'company': company, 'location': location, 'hireable': hireable, 'bio': bio} self._remove_none(user) url = self._build_url('user') json = self._json(self._patch(url, data=dumps(user)), 200) if json: self._update_(json) return True return False
def test_no_mutate(self): args = {} t = URITemplate('') t.expand(args, key=1) self.assertEqual(args, {})
class ComponentStore: """Component store. Enables external components to be loaded by name and digest/tag. Attributes: local_search_paths: A list of local directories to include in the search. url_seach_prefixes: A list of URL prefixes to include in the search. uri_search_template: A URI template for components, which may include {name}, {digest} and {tag} variables. """ def __init__(self, local_search_paths=None, url_search_prefixes=None, auth=None, uri_search_template=None): """Instantiates a ComponentStore including the specified locations. Args: local_search_paths: A list of local directories to include in the search. url_seach_prefixes: A list of URL prefixes to include in the search. auth: Auth object for the requests library. See https://requests.readthedocs.io/en/master/user/authentication/ uri_search_template: A URI template for components, which may include {name}, {digest} and {tag} variables. """ self.local_search_paths = local_search_paths or ['.'] if uri_search_template: self.uri_search_template = URITemplate(uri_search_template) self.url_search_prefixes = url_search_prefixes or [] self._auth = auth self._component_file_name = 'component.yaml' self._digests_subpath = 'versions/sha256' self._tags_subpath = 'versions/tags' cache_base_dir = Path(tempfile.gettempdir()) / '.kfp_components' self._git_blob_hash_to_data_db = KeyValueStore( cache_dir=cache_base_dir / 'git_blob_hash_to_data') self._url_to_info_db = KeyValueStore(cache_dir=cache_base_dir / 'url_to_info') def load_component_from_url(self, url): """Loads a component from a URL. Args: url: The url of the component specification. Returns: A factory function with a strongly-typed signature. """ return comp.load_component_from_url(url=url, auth=self._auth) def load_component_from_file(self, path): """Loads a component from a path. Args: path: The path of the component specification. Returns: A factory function with a strongly-typed signature. """ return comp.load_component_from_file(path) def load_component(self, name, digest=None, tag=None): """Loads component local file or URL and creates a task factory function. Search locations: * :code:`<local-search-path>/<name>/component.yaml` * :code:`<url-search-prefix>/<name>/component.yaml` If the digest is specified, then the search locations are: * :code:`<local-search-path>/<name>/versions/sha256/<digest>` * :code:`<url-search-prefix>/<name>/versions/sha256/<digest>` If the tag is specified, then the search locations are: * :code:`<local-search-path>/<name>/versions/tags/<digest>` * :code:`<url-search-prefix>/<name>/versions/tags/<digest>` Args: name: Component name used to search and load the component artifact containing the component definition. Component name usually has the following form: group/subgroup/component digest: Strict component version. SHA256 hash digest of the component artifact file. Can be used to load a specific component version so that the pipeline is reproducible. tag: Version tag. Can be used to load component version from a specific branch. The version of the component referenced by a tag can change in future. Returns: A factory function with a strongly-typed signature. Once called with the required arguments, the factory constructs a pipeline task instance (ContainerOp). """ #This function should be called load_task_factory since it returns a factory function. #The real load_component function should produce an object with component properties (e.g. name, description, inputs/outputs). #TODO: Change this function to return component spec object but it should be callable to construct tasks. component_ref = ComponentReference(name=name, digest=digest, tag=tag) component_ref = self._load_component_spec_in_component_ref( component_ref) return comp._create_task_factory_from_component_spec( component_spec=component_ref.spec, component_ref=component_ref, ) def _load_component_spec_in_component_ref( self, component_ref: ComponentReference, ) -> ComponentReference: """Takes component_ref, finds the component spec and returns component_ref with .spec set to the component spec. See ComponentStore.load_component for the details of the search logic. """ if component_ref.spec: return component_ref component_ref = copy.copy(component_ref) if component_ref.url: component_ref.spec = comp._load_component_spec_from_url( url=component_ref.url, auth=self._auth) return component_ref name = component_ref.name if not name: raise TypeError("name is required") if name.startswith('/') or name.endswith('/'): raise ValueError( 'Component name should not start or end with slash: "{}"' .format(name)) digest = component_ref.digest tag = component_ref.tag tried_locations = [] if digest is not None and tag is not None: raise ValueError('Cannot specify both tag and digest') if digest is not None: path_suffix = name + '/' + self._digests_subpath + '/' + digest elif tag is not None: path_suffix = name + '/' + self._tags_subpath + '/' + tag #TODO: Handle symlinks in GIT URLs else: path_suffix = name + '/' + self._component_file_name #Trying local search paths for local_search_path in self.local_search_paths: component_path = Path(local_search_path, path_suffix) tried_locations.append(str(component_path)) if component_path.is_file(): # TODO: Verify that the content matches the digest (if specified). component_ref._local_path = str(component_path) component_ref.spec = comp._load_component_spec_from_file( str(component_path)) return component_ref #Trying URI template if self.uri_search_template: url = self.uri_search_template.expand( name=name, digest=digest, tag=tag) tried_locations.append(url) if self._load_component_spec_from_url(component_ref, url): return component_ref #Trying URL prefixes for url_search_prefix in self.url_search_prefixes: url = url_search_prefix + path_suffix tried_locations.append(url) if self._load_component_spec_from_url(component_ref, url): return component_ref raise RuntimeError( 'Component {} was not found. Tried the following locations:\n{}' .format(name, '\n'.join(tried_locations))) def _load_component_spec_from_url(self, component_ref, url) -> bool: """Loads component spec from a URL. On success, the url and spec attributes of the component_ref arg will be populated. Args: component_ref: the component whose spec to load. url: the location from which to obtain the component spec. Returns: True if the component was retrieved and non-empty; otherwise False. """ try: response = requests.get( url, auth=self._auth ) #Does not throw exceptions on bad status, but throws on dead domains and malformed URLs. Should we log those cases? response.raise_for_status() except: return False if response.content: # TODO: Verify that the content matches the digest (if specified). component_ref.url = url component_ref.spec = comp._load_component_spec_from_yaml_or_zip_bytes( response.content) return True return False def _load_component_from_ref(self, component_ref: ComponentReference) -> Callable: component_ref = self._load_component_spec_in_component_ref( component_ref) return comp._create_task_factory_from_component_spec( component_spec=component_ref.spec, component_ref=component_ref) def search(self, name: str): """Searches for components by name in the configured component store. Prints the component name and URL for components that match the given name. Only components on GitHub are currently supported. Example:: kfp.components.ComponentStore.default_store.search('xgboost') # Returns results: # Xgboost train https://raw.githubusercontent.com/.../components/XGBoost/Train/component.yaml # Xgboost predict https://raw.githubusercontent.com/.../components/XGBoost/Predict/component.yaml """ self._refresh_component_cache() for url in self._url_to_info_db.keys(): component_info = json.loads( self._url_to_info_db.try_get_value_bytes(url)) component_name = component_info['name'] if name.casefold() in component_name.casefold(): print('\t'.join([ component_name, url, ])) def list(self): self.search('') def _refresh_component_cache(self): for url_search_prefix in self.url_search_prefixes: if url_search_prefix.startswith( 'https://raw.githubusercontent.com/'): logging.info('Searching for components in "{}"'.format( url_search_prefix)) for candidate in _list_candidate_component_uris_from_github_repo( url_search_prefix, auth=self._auth): component_url = candidate['url'] if self._url_to_info_db.exists(component_url): continue logging.debug( 'Found new component URL: "{}"'.format(component_url)) blob_hash = candidate['git_blob_hash'] if not self._git_blob_hash_to_data_db.exists(blob_hash): logging.debug( 'Downloading component spec from "{}"'.format( component_url)) response = _get_request_session().get( component_url, auth=self._auth) response.raise_for_status() component_data = response.content # Verifying the hash received_data_hash = _calculate_git_blob_hash( component_data) if received_data_hash.lower() != blob_hash.lower(): raise RuntimeError( 'The downloaded component ({}) has incorrect hash: "{}" != "{}"' .format( component_url, received_data_hash, blob_hash, )) # Verifying that the component is loadable try: component_spec = comp._load_component_spec_from_component_text( component_data) except: continue self._git_blob_hash_to_data_db.store_value_bytes( blob_hash, component_data) else: component_data = self._git_blob_hash_to_data_db.try_get_value_bytes( blob_hash) component_spec = comp._load_component_spec_from_component_text( component_data) component_name = component_spec.name self._url_to_info_db.store_value_text( component_url, json.dumps( dict( name=component_name, url=component_url, git_blob_hash=blob_hash, digest=_calculate_component_digest( component_data), )))