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
22 changes: 22 additions & 0 deletions .github/workflows/test.yml → .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,28 @@ jobs:
with:
black_args: ". --check"

check-sync-code:
name: "Check auto-generated sync code"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.13

- name: Install uv
uses: astral-sh/setup-uv@v3
with:
version: "0.5.1"
enable-cache: true

- name: Install dependencies
run: uv pip install -e '.[dev]'

- name: Run unasync to regenerate code
run: python utils/run-unasync.py --check

integration-test:
runs-on: ${{ matrix.os }}
strategy:
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/tag-new-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ jobs:
echo "Error: 'release_version' should be in SemVer format."
exit 1
fi
if [[ ! "${{ inputs.next_version }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: 'next_version' should be in SemVer format."
if [[ ! "${{ inputs.next_version }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ && "${{ inputs.next_version }}" != "skip" ]]; then
echo "Error: 'next_version' should be in SemVer format or 'skip'."
exit 1
fi

Expand Down Expand Up @@ -62,4 +62,4 @@ jobs:
RELEASE_VERSION: ${{ inputs.release_version }}
NEXT_VERSION: ${{ inputs.next_version }}
run: |
python .github/scripts/release.py
python utils/run-release.py
70 changes: 70 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Contributing to the Central Dogma Client

First of all, thank you so much for taking your time to visit centraldogma-python.
Here, everything you do or even your mere presence is a contribution in a broad sense.
We strongly believe your contribution will enrich our community and create
a virtuous cycle that propels the project into something great.

## Install

```
$ uv pip install -e ".[dev]"
```

## Code generation

This project uses `unasync` to automatically generate all synchronous (sync) client code from its asynchronous (async) counterpart.

As a contributor, you must not edit the generated synchronous code (e.g., files directly under `centraldogma/_sync/`) by hand.
All changes must be made to the asynchronous source files, which are located in the centraldogma/_async/ directory.
After you modify any code in `centraldogma/_async/`, you must run the code generation script to update the synchronous code:

```
$ python utils/run-unasync.py
```

This command regenerates the synchronous code based on your changes.
You must include both your original changes (in `centraldogma/_async/`) and the newly generated synchronous files in your Pull Request.

## Running tests locally

### Unit test

```
$ pytest
```

### Integration test

1. Run local Central Dogma server with docker-compose
```
$ docker-compose up -d
```

2. Run integration tests
```
$ INTEGRATION_TEST=true pytest
```

3. Stop the server
```
$ docker-compose down
```

## Lint

- [PEP 8](https://www.python.org/dev/peps/pep-0008)
```
$ black .
```

## Documentation

- [PEP 257](https://www.python.org/dev/peps/pep-0257)

### To build sphinx at local

```
$ pip install sphinx sphinx_rtd_theme
$ cd docs && make html
```
42 changes: 4 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
Python client library for [Central Dogma](https://line.github.io/centraldogma/).

## Install

```
$ pip install centraldogma-python
```

## Getting started

Only URL indicating CentralDogma server and access token are required.
```pycon
>>> from centraldogma.dogma import Dogma
Expand All @@ -29,42 +31,6 @@ It supports client configurations.

Please see [`examples` folder](https://github.com/line/centraldogma-python/tree/main/examples) for more detail.

---

## Development
### Tests
#### Unit test
```
$ pytest
```

#### Integration test
1. Run local Central Dogma server with docker-compose
```
$ docker-compose up -d
```
## Contributing

2. Run integration tests
```
$ INTEGRATION_TEST=true pytest
```

3. Stop the server
```
$ docker-compose down
```

### Lint
- [PEP 8](https://www.python.org/dev/peps/pep-0008)
```
$ black .
```

### Documentation
- [PEP 257](https://www.python.org/dev/peps/pep-0257)

#### To build sphinx at local
```
$ pip install sphinx sphinx_rtd_theme
$ cd docs && make html
```
See [CONTRIBUTING.md](./CONTRIBUTING.md)
113 changes: 113 additions & 0 deletions centraldogma/_async/base_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Copyright 2025 LINE Corporation
#
# LINE Corporation licenses this file to you under the Apache License,
# version 2.0 (the "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at:
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

from typing import Any, Dict, Union, Callable, TypeVar, Optional

from httpx import AsyncClient, Limits, Response
from tenacity import stop_after_attempt, wait_exponential, AsyncRetrying

from centraldogma.exceptions import to_exception

T = TypeVar("T")


class BaseClient:
def __init__(
self,
base_url: str,
token: str,
http2: bool = True,
retries: int = 1,
max_connections: int = 10,
max_keepalive_connections: int = 2,
**configs,
):
assert retries >= 0, "retries must be greater than or equal to zero"
assert max_connections > 0, "max_connections must be greater than zero"
assert (
max_keepalive_connections > 0
), "max_keepalive_connections must be greater than zero"

base_url = base_url[:-1] if base_url[-1] == "/" else base_url

for key in ["transport", "limits"]:
if key in configs:
del configs[key]

self.retries = retries
self.client = AsyncClient(
base_url=f"{base_url}/api/v1",
http2=http2,
limits=Limits(
max_connections=max_connections,
max_keepalive_connections=max_keepalive_connections,
),
**configs,
)
self.token = token
self.headers = self._get_headers(token)
self.patch_headers = self._get_patch_headers(token)

async def __aexit__(self, *_: Any) -> None:
await self.client.aclose()

async def request(
self,
method: str,
path: str,
handler: Optional[Dict[int, Callable[[Response], T]]] = None,
**kwargs,
) -> Union[Response, T]:
kwargs = self._set_request_headers(method, **kwargs)
retryer = AsyncRetrying(
stop=stop_after_attempt(self.retries + 1),
wait=wait_exponential(max=60),
reraise=True,
)
return retryer(self._request, method, path, handler, **kwargs)

def _set_request_headers(self, method: str, **kwargs) -> Dict:
default_headers = self.patch_headers if method == "patch" else self.headers
kwargs["headers"] = {**default_headers, **(kwargs.get("headers") or {})}
return kwargs

async def _request(
self,
method: str,
path: str,
handler: Optional[Dict[int, Callable[[Response], T]]] = None,
**kwargs,
):
resp = await self.client.request(method, path, **kwargs)
if handler:
converter = handler.get(resp.status_code)
if converter:
return converter(resp)
else: # Unexpected response status
raise to_exception(resp)
return resp

@staticmethod
def _get_headers(token: str) -> Dict:
return {
"Authorization": f"bearer {token}",
"Content-Type": "application/json",
}

@staticmethod
def _get_patch_headers(token: str) -> Dict:
return {
"Authorization": f"bearer {token}",
"Content-Type": "application/json-patch+json",
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2021 LINE Corporation
# Copyright 2025 LINE Corporation
#
# LINE Corporation licenses this file to you under the Apache License,
# version 2.0 (the "License"); you may not use this file except in compliance
Expand All @@ -11,9 +11,10 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from typing import Dict, Union, Callable, TypeVar, Optional

from httpx import Client, HTTPTransport, Limits, Response
from typing import Any, Dict, Union, Callable, TypeVar, Optional

from httpx import Client, Limits, Response
from tenacity import stop_after_attempt, wait_exponential, Retrying

from centraldogma.exceptions import to_exception
Expand Down Expand Up @@ -48,7 +49,6 @@ def __init__(
self.client = Client(
base_url=f"{base_url}/api/v1",
http2=http2,
transport=HTTPTransport(retries=retries),
limits=Limits(
max_connections=max_connections,
max_keepalive_connections=max_keepalive_connections,
Expand All @@ -59,6 +59,9 @@ def __init__(
self.headers = self._get_headers(token)
self.patch_headers = self._get_patch_headers(token)

def __exit__(self, *_: Any) -> None:
self.client.close()

def request(
self,
method: str,
Expand Down
2 changes: 1 addition & 1 deletion centraldogma/content_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

from httpx import Response

from centraldogma.base_client import BaseClient
from centraldogma._sync.base_client import BaseClient
from centraldogma.data import Content
from centraldogma.data.change import Change
from centraldogma.data.commit import Commit
Expand Down
2 changes: 1 addition & 1 deletion centraldogma/dogma.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import os
from typing import List, Optional, TypeVar, Callable

from centraldogma.base_client import BaseClient
from centraldogma._sync.base_client import BaseClient
from centraldogma.content_service import ContentService

# noinspection PyUnresolvedReferences
Expand Down
2 changes: 1 addition & 1 deletion centraldogma/project_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from http import HTTPStatus
from typing import List

from centraldogma.base_client import BaseClient
from centraldogma._sync.base_client import BaseClient
from centraldogma.data import Project


Expand Down
2 changes: 1 addition & 1 deletion centraldogma/repository_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from http import HTTPStatus
from typing import List

from centraldogma.base_client import BaseClient
from centraldogma._sync.base_client import BaseClient
from centraldogma.data import Repository


Expand Down
6 changes: 4 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

[build-system]
requires = ["setuptools >= 61"]
build-backend = "setuptools.build_meta"
Expand Down Expand Up @@ -59,10 +60,11 @@ dev = [
"pytest",
"pytest-cov",
"pytest-mock",
"respx"
"respx",
"unasync>=0.6.0"
]
docs = [
"sphinx_rtd_theme"
"sphinx_rtd_theme>=3.0"
]

[tool.setuptools]
Expand Down
3 changes: 2 additions & 1 deletion tests/test_base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

from http import HTTPStatus
import json

from centraldogma._sync.base_client import BaseClient
from centraldogma.exceptions import UnauthorizedException, NotFoundException
from centraldogma.base_client import BaseClient
from httpx import ConnectError, NetworkError, Response
import pytest

Expand Down
File renamed without changes.
Loading
Loading