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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Test

on:
push:
pull_request:
branches: [main]

jobs:
test-python:
strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]

runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}

- name: Setup Poetry
uses: pronovic/setup-poetry@v1
with:
version: "1.4.1"
cache-venv: "true"
cache-poetry: "true"

- name: Install dependencies
run: |
poetry install --with dev

- name: Run tests
run: |
source ./.venv/bin/activate
pytest
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ pip install bitbucket-python

## Usage

### Sync client

```python
from bitbucket.client import Client
from bitbucket import AsyncClient
Expand All @@ -24,6 +22,8 @@ async with AsyncClient('EMAIL', 'PASSWORD') as client:

```

### Methods

Get user information
```
response = client.get_user()
Expand Down Expand Up @@ -132,6 +132,21 @@ Delete webhook
response = client.delete_webhook('REPOSITORY_SLUG', 'WEBHOOK_ID')
```

### Helper methods

### all_pages

The `all_pages` method is a helper function that makes it easy to retrieve all items from an API methods that uses pagination (see https://developer.atlassian.com/cloud/bitbucket/rest/intro/#pagination).

```python
client = Client()

items = list(client.all_pages(client.get_repositories))
```

Note that the `all_pages` method uses a generator to return the results.


## Requirements

- requests
Expand Down
59 changes: 52 additions & 7 deletions bitbucket/aclient.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import typing
import httpx

from .base import BaseClient
Expand All @@ -21,11 +22,6 @@ class Client(BaseClient):
```
"""

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=(
Expand All @@ -50,6 +46,52 @@ async def __aexit__(
) -> None:
await self._session.aclose()

async def all_pages(
self,
method: typing.Callable[
...,
typing.Awaitable[typing.Union[typing.Dict[str, typing.Any], None]],
],
*args,
**kwargs
) -> typing.AsyncGenerator[typing.Dict[str, typing.Any], None]:
"""
Retrieves all pages from a BitBucket API list endpoint and yields a generator for the items in the
response.

Example:

```python
async for item in client.all_pages(
client.get_issues,
"{726f1aab-826f-4c08-a127-1224347b3d09}"
):
print(item["id"])
```

Args:
method: A client class method to retrieve all pages from.
*args: Variable length argument list to be passed to the `method` callable.
**kwargs: Arbitrary keyword arguments to be passed to the `method` callable.

Returns:
An asynchronous generator that yields a dictionary of item data for each item in the response.

Raises:
Any exceptions raised by the `method` callable.
"""
resp = await method(*args, **kwargs)
while True:
if resp is None:
break

for v in resp["values"]:
yield v

if "next" not in resp:
break
resp = await self._get(resp["next"])

async def get_user(self, params=None):
"""
Retrieves information about the current user.
Expand Down Expand Up @@ -100,7 +142,7 @@ async def create_repository(self, params=None, data=None, name=None, team=None):
Creates a new repository.

Example data:
```
```json
{
"scm": "git",
"project": {
Expand Down Expand Up @@ -408,7 +450,10 @@ async def _get(self, endpoint, params=None):
Returns:
A dictionary containing the parsed response from the GET request.
"""
response = await self._session.get(self.BASE_URL + endpoint, params=params)
response = await self._session.get(
endpoint if endpoint.startswith("http") else self.BASE_URL + endpoint,
params=params,
)
return self.parse(response)

async def _post(self, endpoint, params=None, data=None):
Expand Down
53 changes: 43 additions & 10 deletions bitbucket/base.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,59 @@
from .exceptions import UnknownError, InvalidIDError, NotFoundIDError, NotAuthenticatedError, PermissionError
import typing

from .exceptions import (
InvalidIDError,
NotAuthenticatedError,
NotFoundIDError,
PermissionError,
UnknownError,
)


class BaseClient(object):
BASE_URL = 'https://api.bitbucket.org/'
BASE_URL = "https://api.bitbucket.org/"

def __init__(self, user: str, password: str, owner: typing.Union[str, None] = None):
self.user = user
self.password = password
self.username = owner

def parse(self, response) -> typing.Union[typing.Dict[str, typing.Any], None]:
"""
Parses the response from the BitBucket API and returns the response data or raises an exception if the response
indicates an error.

Args:
response: The response object returned by the BitBucket API.

Returns:
If the response status code is 200, 201, or 202, returns the JSON data in the response body as a dictionary.
If the response status code is 204, returns None.
Otherwise, raises an exception indicating the error message returned by the API.

def parse(self, response):
Raises:
InvalidIDError: If the response status code is 400.
NotAuthenticatedError: If the response status code is 401.
PermissionError: If the response status code is 403.
NotFoundIDError: If the response status code is 404.
UnknownError: If the response status code is not one of the above.
"""
status_code = response.status_code
if 'application/json' in response.headers['Content-Type']:
if "application/json" in response.headers["Content-Type"]:
r = response.json()
else:
r = response.text
if status_code in (200, 201):
if status_code in (200, 201, 202):
return r
if status_code == 204:
return None
message = None
try:
if type(r) == str:
if type(r) == dict:
message = r["error"]["message"]
else:
message = r
elif 'errorMessages' in r:
message = r['errorMessages']
except Exception:
message = 'No error message.'
message = response.text
if status_code == 400:
raise InvalidIDError(message)
if status_code == 401:
Expand All @@ -29,4 +62,4 @@ def parse(self, response):
raise PermissionError(message)
if status_code == 404:
raise NotFoundIDError(message)
raise UnknownError(message)
raise UnknownError(message)
Loading