diff --git a/README.md b/README.md index 2701813..717af69 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,22 @@ pip install bitbucket-python ``` ## Usage -``` + +### Sync client + +```python from bitbucket.client import Client +from bitbucket import AsyncClient client = Client('EMAIL', 'PASSWORD') # Or to specify owner URL to find repo own by other user client = Client('EMAIL', 'PASSWORD', 'Owner') +# Async client +async with AsyncClient('EMAIL', 'PASSWORD') as client: + ... + ``` Get user information @@ -125,4 +133,6 @@ response = client.delete_webhook('REPOSITORY_SLUG', 'WEBHOOK_ID') ``` ## Requirements + - requests +- [httpx](https://github.com/encode/httpx/) diff --git a/bitbucket/__init__.py b/bitbucket/__init__.py index e69de29..92b7472 100644 --- a/bitbucket/__init__.py +++ b/bitbucket/__init__.py @@ -0,0 +1,7 @@ +from .client import Client +from .aclient import Client as AsyncClient + +__all__ = [ + 'Client', + 'AsyncClient', +] \ No newline at end of file diff --git a/bitbucket/aclient.py b/bitbucket/aclient.py new file mode 100644 index 0000000..b57541c --- /dev/null +++ b/bitbucket/aclient.py @@ -0,0 +1,465 @@ +import httpx + +from .base import BaseClient + + +class Client(BaseClient): + """ + A client for the BitBucket API that has to be used as an async context manager. + + Args: + user (str): The username to use for authentication. + password (str): The password to use for authentication. + owner (str, optional): The name of the BitBucket account to use. If not + specified, the authenticated user will be used. + + Example usage: + ``` + async with Client('myusername', 'mypassword', 'myaccount') as client: + response = await client.get_user() + print(response) + ``` + """ + + def __init__(self, user, password, owner=None): + self.user = user + self.password = password + self.username = owner + + async def __aenter__(self): + self._session = httpx.AsyncClient( + auth=( + self.user, + self.password, + ) + ) + + user_data = await self.get_user() + + # for shared repo, set baseURL to owner + if self.username is None and user_data is not None: + self.username = user_data.get("username") + + return self + + async def __aexit__( + self, + exc_type, + exc_value, + traceback, + ) -> None: + await self._session.aclose() + + async def get_user(self, params=None): + """ + Retrieves information about the current user. + + Args: + params (dict, optional): A dictionary of query parameters to include + in the request. + + Returns: + A dictionary that contains information about the user, as returned + by the BitBucket API. + """ + return await self._get("2.0/user", params=params) + + async def get_repositories(self, params=None): + """ + Retrieves a paginated list of all repositories owned by the workspace. + + Args: + params (dict, optional): A dictionary of query parameters to include + in the request. + + Returns: + A dictionary that contains a paginated list of all repositories owned by the workspace. + """ + return await self._get( + "2.0/repositories/{}".format(self.username), params=params + ) + + async def get_repository(self, repository_slug, params=None): + """ + Retrieves information about a specific repository. + + Args: + repository_slug (str): The slug of the repository to retrieve information for. + params (dict, optional): A dictionary of query parameters to include in the request. + + Returns: + A dictionary containing information about the specified repository, as returned by the BitBucket API. + """ + return await self._get( + "2.0/repositories/{}/{}".format(self.username, repository_slug), + params=params, + ) + + async def create_repository(self, params=None, data=None, name=None, team=None): + """ + Creates a new repository. + + Example data: + ``` + { + "scm": "git", + "project": { + "key": "MARS" + } + } + ``` + + Args: + params (dict, optional): A dictionary of query parameters to include in the request. + data (dict, optional): A dictionary of data to include in the request body + name (str): The name of the new repository. + team (str): The team that the new repository should be created under. + + Returns: + A dictionary containing information about the newly created repository, as returned by the BitBucket API. + """ + return await self._post( + "2.0/repositories/{}/{}".format(team, name), params, data + ) + + async def get_repository_branches(self, repository_slug, params=None): + """ + Retrieves a paginated list of all open branches within the specified repository. + + Args: + repository_slug (str): The slug of the repository to retrieve information for. + params (dict, optional): A dictionary of query parameters to include in the request. + + Returns: + A dictionary containing a paginated list of all open branches within the specified repository, as returned by the BitBucket API. + """ + return await self._get( + "2.0/repositories/{}/{}/refs/branches".format( + self.username, repository_slug + ), + params=params, + ) + + async def get_repository_tags(self, repository_slug, params=None): + """ + Retrieves the tags in the repository. + + Args: + repository_slug (str): The slug of the repository to retrieve branches for. + params (dict, optional): A dictionary of query parameters to include in the request. + + Returns: + A dictionary containing a paginated list of tags in the repository., as returned by the BitBucket API. + """ + return await self._get( + "2.0/repositories/{}/{}/refs/tags".format(self.username, repository_slug), + params=params, + ) + + async def get_repository_commits(self, repository_slug, params=None): + """ + Retrieves a paginated list of commits for a specific repository. + + Args: + repository_slug (str): The slug of the repository to retrieve commits for. + params (dict, optional): A dictionary of query parameters to include in the request. + + Returns: + A dictionary containing information about the commits for the specified repository, as returned by the BitBucket API. + """ + return await self._get( + "2.0/repositories/{}/{}/commits".format(self.username, repository_slug), + params=params, + ) + + async def get_repository_components(self, repository_slug, params=None): + """ + Returns the components that have been defined in the issue tracker. + + Args: + repository_slug (str): The slug of the repository to retrieve components for. + params (dict, optional): A dictionary of query parameters to include in the request. + + Returns: + A dictionary containing information about the components that have been defined in the issue tracker, as returned by the BitBucket API. + """ + return await self._get( + "2.0/repositories/{}/{}/components".format(self.username, repository_slug), + params=params, + ) + + async def get_repository_milestones(self, repository_slug, params=None): + """ + Retrieves the milestones that have been defined in the issue tracker. + + Args: + repository_slug (str): The slug of the repository to retrieve milestones for. + params (dict, optional): A dictionary of query parameters to include in the request. + + Returns: + A dictionary containing information about the milestones that have been defined in the issue tracker, as returned by the BitBucket API. + """ + return await self._get( + "2.0/repositories/{}/{}/milestones".format(self.username, repository_slug), + params=params, + ) + + async def get_repository_versions(self, repository_slug, params=None): + """ + Retrieves the versions that have been defined in the issue tracker. + + Args: + repository_slug (str): The slug of the repository to retrieve versions for. + params (dict, optional): A dictionary of query parameters to include in the request. + + Returns: + A dictionary containing information about the versions that have been defined in the issue tracker, as returned by the BitBucket API. + """ + return await self._get( + "2.0/repositories/{}/{}/versions".format(self.username, repository_slug), + params=params, + ) + + async def get_repository_source_code(self, repository_slug, params=None): + """ + Retrieves the directory listing of the root directory on the main branch. + + Args: + repository_slug (str): The slug of the repository. + params (dict, optional): A dictionary of query parameters to include in the request. + + Returns: + A dictionary containing the directory listing of the root directory on the main branch, as returned by the BitBucket API. + """ + return await self._get( + "2.0/repositories/{}/{}/src".format(self.username, repository_slug), + params=params, + ) + + async def get_repository_commit_path_source_code( + self, repository_slug, commit_hash, path, params=None + ): + """ + Retrieves the contents of a single file, or the contents of a directory at a specified revision. + + Args: + repository_slug (str): The slug of the repository to retrieve source code for. + commit_hash (str): The hash of the commit to retrieve source code for. + path (str): The path of the file or directory to retrieve source code for. + params (dict, optional): A dictionary of query parameters to include in the request. + + Returns: + When `path` points to a file, this endpoint returns the raw contents. + When `path` points to a directory instead of a file, the response is a paginated list of directory and file objects. + """ + return await self._get( + "2.0/repositories/{}/{}/src/{}/{}".format( + self.username, repository_slug, commit_hash, path + ), + params=params, + ) + + async def create_issue(self, repository_slug, data, params=None): + """ + Creates a new issue in the specified repository. + + Args: + repository_slug (str): The slug of the repository to create the issue in. + data (dict): A dictionary of data to include in the request body, which must include the "title" field at a minimum. + params (dict, optional): A dictionary of query parameters to include in the request. + + Returns: + A dictionary containing information about the newly created issue, as returned by the BitBucket API. + """ + return await self._post( + "2.0/repositories/{}/{}/issues".format(self.username, repository_slug), + data=data, + params=params, + ) + + async def get_issue(self, repository_slug, issue_id, params=None): + """ + Retrieves information about a specific issue in the specified repository. + + Args: + repository_slug (str): The slug of the repository to retrieve the issue from. + issue_id (int): The ID of the issue to retrieve. + params (dict, optional): A dictionary of query parameters to include in the request. + + Returns: + A dictionary containing information about the specified issue, as returned by the BitBucket API. + """ + return await self._get( + "2.0/repositories/{}/{}/issues/{}".format( + self.username, repository_slug, issue_id + ), + params=params, + ) + + async def get_issues(self, repository_slug, params=None): + """ + Retrieves a list of issues in the specified repository. + + Args: + repository_slug (str): The slug of the repository to retrieve issues from. + params (dict, optional): A dictionary of query parameters to include in the request. + + Returns: + A dictionary containing information about the issues in the specified repository, as returned by the BitBucket API. + """ + return await self._get( + "2.0/repositories/{}/{}/issues".format(self.username, repository_slug), + params=params, + ) + + async def delete_issue(self, repository_slug, issue_id, params=None): + """ + Deletes a specific issue in the specified repository. + + Args: + repository_slug (str): The slug of the repository to delete the issue from. + issue_id (int): The ID of the issue to delete. + params (dict, optional): A dictionary of query parameters to include in the request. + """ + return await self._delete( + "2.0/repositories/{}/{}/issues/{}".format( + self.username, repository_slug, issue_id + ), + params=params, + ) + + async def create_webhook(self, repository_slug, data, params=None): + """ + Creates a new webhook for the specified repository. + + Args: + repository_slug (str): The slug of the repository to create the webhook for. + data (dict): A dictionary of data to include in the request body, which must include the "url" field at a minimum. + params (dict, optional): A dictionary of query parameters to include in the request. + + Returns: + A dictionary containing information about the newly created webhook, as returned by the BitBucket API. + """ + return await self._post( + "2.0/repositories/{}/{}/hooks".format(self.username, repository_slug), + data=data, + params=params, + ) + + async def get_webhook(self, repository_slug, webhook_uid, params=None): + """ + Retrieves information about a specific webhook in the specified repository. + + Args: + repository_slug (str): The slug of the repository to retrieve the webhook from. + webhook_uid (str): The UID of the webhook to retrieve. + params (dict, optional): A dictionary of query parameters to include in the request. + + Returns: + A dictionary containing information about the specified webhook, as returned by the BitBucket API. + """ + return await self._get( + "2.0/repositories/{}/{}/hooks/{}".format( + self.username, repository_slug, webhook_uid + ), + params=params, + ) + + async def get_webhooks(self, repository_slug, params=None): + """ + Retrieves a list of webhooks in the specified repository. + + Args: + repository_slug (str): The slug of the repository to retrieve webhooks from. + params (dict, optional): A dictionary of query parameters to include in the request. + + Returns: + A dictionary containing information about the webhooks in the specified repository, as returned by the BitBucket API. + """ + return await self._get( + "2.0/repositories/{}/{}/hooks".format(self.username, repository_slug), + params=params, + ) + + async def delete_webhook(self, repository_slug, webhook_uid, params=None): + """ + Deletes a specific webhook in the specified repository. + + Args: + repository_slug (str): The slug of the repository to delete the webhook from. + webhook_uid (str): The UID of the webhook to delete. + params (dict, optional): A dictionary of query parameters to include in the request. + """ + return await self._delete( + "2.0/repositories/{}/{}/hooks/{}".format( + self.username, repository_slug, webhook_uid + ), + params=params, + ) + + async def _get(self, endpoint, params=None): + """ + Sends a GET request to the specified endpoint and returns the parsed response. + + Args: + endpoint (str): The endpoint to send the GET request to. + params (dict, optional): A dictionary of query parameters to include in the request. + + Returns: + A dictionary containing the parsed response from the GET request. + """ + response = await self._session.get(self.BASE_URL + endpoint, params=params) + return self.parse(response) + + async def _post(self, endpoint, params=None, data=None): + """ + Sends a POST request to the specified endpoint with the specified data and returns the parsed response. + + Args: + endpoint (str): The endpoint to send the POST request to. + params (dict, optional): A dictionary of query parameters to include in the request. + data (dict, optional): A dictionary of data to include in the request body. + + Returns: + A dictionary containing the parsed response from the POST request. + """ + response = await self._session.post( + self.BASE_URL + endpoint, + params=params, + json=data, + ) + return self.parse(response) + + async def _put(self, endpoint, params=None, data=None): + """ + Sends a PUT request to the specified endpoint with the specified data and returns the parsed response. + + Args: + endpoint (str): The endpoint to send the PUT request to. + params (dict, optional): A dictionary of query parameters to include in the request. + data (dict, optional): A dictionary of data to include in the request body. + + Returns: + A dictionary containing the parsed response from the PUT request. + """ + response = await self._session.put( + self.BASE_URL + endpoint, + params=params, + json=data, + ) + return self.parse(response) + + async def _delete(self, endpoint, params=None): + """ + Sends a DELETE request to the specified endpoint with the specified data and returns the parsed response. + + Args: + endpoint (str): The endpoint to send the DELETE request to. + params (dict, optional): A dictionary of query parameters to include in the request. + data (dict, optional): A dictionary of data to include in the request body. + + Returns: + A dictionary containing the parsed response from the DELETE request. + """ + response = await self._session.delete(self.BASE_URL + endpoint, params=params) + return self.parse(response) diff --git a/bitbucket/base.py b/bitbucket/base.py new file mode 100644 index 0000000..5df61ec --- /dev/null +++ b/bitbucket/base.py @@ -0,0 +1,32 @@ +from .exceptions import UnknownError, InvalidIDError, NotFoundIDError, NotAuthenticatedError, PermissionError + +class BaseClient(object): + BASE_URL = 'https://api.bitbucket.org/' + + def parse(self, response): + status_code = response.status_code + if 'application/json' in response.headers['Content-Type']: + r = response.json() + else: + r = response.text + if status_code in (200, 201): + return r + if status_code == 204: + return None + message = None + try: + if type(r) == str: + message = r + elif 'errorMessages' in r: + message = r['errorMessages'] + except Exception: + message = 'No error message.' + if status_code == 400: + raise InvalidIDError(message) + if status_code == 401: + raise NotAuthenticatedError(message) + if status_code == 403: + raise PermissionError(message) + if status_code == 404: + raise NotFoundIDError(message) + raise UnknownError(message) \ No newline at end of file diff --git a/bitbucket/client.py b/bitbucket/client.py index 4e55422..197a11b 100644 --- a/bitbucket/client.py +++ b/bitbucket/client.py @@ -1,10 +1,10 @@ import requests -from bitbucket.exceptions import UnknownError, InvalidIDError, NotFoundIDError, NotAuthenticatedError, PermissionError +from .base import BaseClient -class Client(object): - BASE_URL = 'https://api.bitbucket.org/' +class Client(BaseClient): + def __init__(self, user, password, owner=None): """Initial session with user/password, and setup repository owner @@ -22,7 +22,7 @@ def __init__(self, user, password, owner=None): user_data = self.get_user() # for shared repo, set baseURL to owner - if owner is None : + if owner is None and user_data is not None: owner = user_data.get('username') self.username = owner @@ -331,42 +331,17 @@ def delete_webhook(self, repository_slug, webhook_uid, params=None): def _get(self, endpoint, params=None): response = requests.get(self.BASE_URL + endpoint, params=params, auth=(self.user, self.password)) - return self._parse(response) + return self.parse(response) def _post(self, endpoint, params=None, data=None): response = requests.post(self.BASE_URL + endpoint, params=params, json=data, auth=(self.user, self.password)) - return self._parse(response) + return self.parse(response) def _put(self, endpoint, params=None, data=None): response = requests.put(self.BASE_URL + endpoint, params=params, json=data, auth=(self.user, self.password)) - return self._parse(response) + return self.parse(response) def _delete(self, endpoint, params=None): response = requests.delete(self.BASE_URL + endpoint, params=params, auth=(self.user, self.password)) - return self._parse(response) - - def _parse(self, response): - status_code = response.status_code - if 'application/json' in response.headers['Content-Type']: - r = response.json() - else: - r = response.text - if status_code in (200, 201): - return r - if status_code == 204: - return None - message = None - try: - if 'errorMessages' in r: - message = r['errorMessages'] - except Exception: - message = 'No error message.' - if status_code == 400: - raise InvalidIDError(message) - if status_code == 401: - raise NotAuthenticatedError(message) - if status_code == 403: - raise PermissionError(message) - if status_code == 404: - raise NotFoundIDError(message) - raise UnknownError(message) + return self.parse(response) + diff --git a/pyproject.toml b/pyproject.toml index 61e141b..65c2c69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "bitbucket-python" -version = "0.2.4" +version = "0.3.0" description = "API wrapper for Bitbucket written in Python" authors = ["Miguel Ferrer "] license = "MIT" @@ -10,6 +10,7 @@ packages = [{include = "bitbucket"}] [tool.poetry.dependencies] python = "^3.7" requests = "^2.26.0" +httpx = "^0.23.0" [build-system]