Skip to content

Commit 16bd29a

Browse files
authored
Uses unasync to automatically generate all synchronous client code (#72)
1 parent 06bda7a commit 16bd29a

15 files changed

Lines changed: 354 additions & 53 deletions
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,28 @@ jobs:
3838
with:
3939
black_args: ". --check"
4040

41+
check-sync-code:
42+
name: "Check auto-generated sync code"
43+
runs-on: ubuntu-latest
44+
steps:
45+
- uses: actions/checkout@v3
46+
- name: Set up Python
47+
uses: actions/setup-python@v5
48+
with:
49+
python-version: 3.13
50+
51+
- name: Install uv
52+
uses: astral-sh/setup-uv@v3
53+
with:
54+
version: "0.5.1"
55+
enable-cache: true
56+
57+
- name: Install dependencies
58+
run: uv pip install -e '.[dev]'
59+
60+
- name: Run unasync to regenerate code
61+
run: python utils/run-unasync.py --check
62+
4163
integration-test:
4264
runs-on: ${{ matrix.os }}
4365
strategy:

.github/workflows/tag-new-release.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ jobs:
2626
echo "Error: 'release_version' should be in SemVer format."
2727
exit 1
2828
fi
29-
if [[ ! "${{ inputs.next_version }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
30-
echo "Error: 'next_version' should be in SemVer format."
29+
if [[ ! "${{ inputs.next_version }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ && "${{ inputs.next_version }}" != "skip" ]]; then
30+
echo "Error: 'next_version' should be in SemVer format or 'skip'."
3131
exit 1
3232
fi
3333
@@ -62,4 +62,4 @@ jobs:
6262
RELEASE_VERSION: ${{ inputs.release_version }}
6363
NEXT_VERSION: ${{ inputs.next_version }}
6464
run: |
65-
python .github/scripts/release.py
65+
python utils/run-release.py

CONTRIBUTING.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Contributing to the Central Dogma Client
2+
3+
First of all, thank you so much for taking your time to visit centraldogma-python.
4+
Here, everything you do or even your mere presence is a contribution in a broad sense.
5+
We strongly believe your contribution will enrich our community and create
6+
a virtuous cycle that propels the project into something great.
7+
8+
## Install
9+
10+
```
11+
$ uv pip install -e ".[dev]"
12+
```
13+
14+
## Code generation
15+
16+
This project uses `unasync` to automatically generate all synchronous (sync) client code from its asynchronous (async) counterpart.
17+
18+
As a contributor, you must not edit the generated synchronous code (e.g., files directly under `centraldogma/_sync/`) by hand.
19+
All changes must be made to the asynchronous source files, which are located in the centraldogma/_async/ directory.
20+
After you modify any code in `centraldogma/_async/`, you must run the code generation script to update the synchronous code:
21+
22+
```
23+
$ python utils/run-unasync.py
24+
```
25+
26+
This command regenerates the synchronous code based on your changes.
27+
You must include both your original changes (in `centraldogma/_async/`) and the newly generated synchronous files in your Pull Request.
28+
29+
## Running tests locally
30+
31+
### Unit test
32+
33+
```
34+
$ pytest
35+
```
36+
37+
### Integration test
38+
39+
1. Run local Central Dogma server with docker-compose
40+
```
41+
$ docker-compose up -d
42+
```
43+
44+
2. Run integration tests
45+
```
46+
$ INTEGRATION_TEST=true pytest
47+
```
48+
49+
3. Stop the server
50+
```
51+
$ docker-compose down
52+
```
53+
54+
## Lint
55+
56+
- [PEP 8](https://www.python.org/dev/peps/pep-0008)
57+
```
58+
$ black .
59+
```
60+
61+
## Documentation
62+
63+
- [PEP 257](https://www.python.org/dev/peps/pep-0257)
64+
65+
### To build sphinx at local
66+
67+
```
68+
$ pip install sphinx sphinx_rtd_theme
69+
$ cd docs && make html
70+
```

README.md

Lines changed: 4 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88
Python client library for [Central Dogma](https://line.github.io/centraldogma/).
99

1010
## Install
11+
1112
```
1213
$ pip install centraldogma-python
1314
```
1415

1516
## Getting started
17+
1618
Only URL indicating CentralDogma server and access token are required.
1719
```pycon
1820
>>> from centraldogma.dogma import Dogma
@@ -29,42 +31,6 @@ It supports client configurations.
2931

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

32-
---
33-
34-
## Development
35-
### Tests
36-
#### Unit test
37-
```
38-
$ pytest
39-
```
40-
41-
#### Integration test
42-
1. Run local Central Dogma server with docker-compose
43-
```
44-
$ docker-compose up -d
45-
```
34+
## Contributing
4635

47-
2. Run integration tests
48-
```
49-
$ INTEGRATION_TEST=true pytest
50-
```
51-
52-
3. Stop the server
53-
```
54-
$ docker-compose down
55-
```
56-
57-
### Lint
58-
- [PEP 8](https://www.python.org/dev/peps/pep-0008)
59-
```
60-
$ black .
61-
```
62-
63-
### Documentation
64-
- [PEP 257](https://www.python.org/dev/peps/pep-0257)
65-
66-
#### To build sphinx at local
67-
```
68-
$ pip install sphinx sphinx_rtd_theme
69-
$ cd docs && make html
70-
```
36+
See [CONTRIBUTING.md](./CONTRIBUTING.md)

centraldogma/_async/base_client.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# Copyright 2025 LINE Corporation
2+
#
3+
# LINE Corporation licenses this file to you under the Apache License,
4+
# version 2.0 (the "License"); you may not use this file except in compliance
5+
# with the License. You may obtain a copy of the License at:
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations
13+
# under the License.
14+
15+
from typing import Any, Dict, Union, Callable, TypeVar, Optional
16+
17+
from httpx import AsyncClient, Limits, Response
18+
from tenacity import stop_after_attempt, wait_exponential, AsyncRetrying
19+
20+
from centraldogma.exceptions import to_exception
21+
22+
T = TypeVar("T")
23+
24+
25+
class BaseClient:
26+
def __init__(
27+
self,
28+
base_url: str,
29+
token: str,
30+
http2: bool = True,
31+
retries: int = 1,
32+
max_connections: int = 10,
33+
max_keepalive_connections: int = 2,
34+
**configs,
35+
):
36+
assert retries >= 0, "retries must be greater than or equal to zero"
37+
assert max_connections > 0, "max_connections must be greater than zero"
38+
assert (
39+
max_keepalive_connections > 0
40+
), "max_keepalive_connections must be greater than zero"
41+
42+
base_url = base_url[:-1] if base_url[-1] == "/" else base_url
43+
44+
for key in ["transport", "limits"]:
45+
if key in configs:
46+
del configs[key]
47+
48+
self.retries = retries
49+
self.client = AsyncClient(
50+
base_url=f"{base_url}/api/v1",
51+
http2=http2,
52+
limits=Limits(
53+
max_connections=max_connections,
54+
max_keepalive_connections=max_keepalive_connections,
55+
),
56+
**configs,
57+
)
58+
self.token = token
59+
self.headers = self._get_headers(token)
60+
self.patch_headers = self._get_patch_headers(token)
61+
62+
async def __aexit__(self, *_: Any) -> None:
63+
await self.client.aclose()
64+
65+
async def request(
66+
self,
67+
method: str,
68+
path: str,
69+
handler: Optional[Dict[int, Callable[[Response], T]]] = None,
70+
**kwargs,
71+
) -> Union[Response, T]:
72+
kwargs = self._set_request_headers(method, **kwargs)
73+
retryer = AsyncRetrying(
74+
stop=stop_after_attempt(self.retries + 1),
75+
wait=wait_exponential(max=60),
76+
reraise=True,
77+
)
78+
return retryer(self._request, method, path, handler, **kwargs)
79+
80+
def _set_request_headers(self, method: str, **kwargs) -> Dict:
81+
default_headers = self.patch_headers if method == "patch" else self.headers
82+
kwargs["headers"] = {**default_headers, **(kwargs.get("headers") or {})}
83+
return kwargs
84+
85+
async def _request(
86+
self,
87+
method: str,
88+
path: str,
89+
handler: Optional[Dict[int, Callable[[Response], T]]] = None,
90+
**kwargs,
91+
):
92+
resp = await self.client.request(method, path, **kwargs)
93+
if handler:
94+
converter = handler.get(resp.status_code)
95+
if converter:
96+
return converter(resp)
97+
else: # Unexpected response status
98+
raise to_exception(resp)
99+
return resp
100+
101+
@staticmethod
102+
def _get_headers(token: str) -> Dict:
103+
return {
104+
"Authorization": f"bearer {token}",
105+
"Content-Type": "application/json",
106+
}
107+
108+
@staticmethod
109+
def _get_patch_headers(token: str) -> Dict:
110+
return {
111+
"Authorization": f"bearer {token}",
112+
"Content-Type": "application/json-patch+json",
113+
}
Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2021 LINE Corporation
1+
# Copyright 2025 LINE Corporation
22
#
33
# LINE Corporation licenses this file to you under the Apache License,
44
# version 2.0 (the "License"); you may not use this file except in compliance
@@ -11,9 +11,10 @@
1111
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1212
# License for the specific language governing permissions and limitations
1313
# under the License.
14-
from typing import Dict, Union, Callable, TypeVar, Optional
1514

16-
from httpx import Client, HTTPTransport, Limits, Response
15+
from typing import Any, Dict, Union, Callable, TypeVar, Optional
16+
17+
from httpx import Client, Limits, Response
1718
from tenacity import stop_after_attempt, wait_exponential, Retrying
1819

1920
from centraldogma.exceptions import to_exception
@@ -48,7 +49,6 @@ def __init__(
4849
self.client = Client(
4950
base_url=f"{base_url}/api/v1",
5051
http2=http2,
51-
transport=HTTPTransport(retries=retries),
5252
limits=Limits(
5353
max_connections=max_connections,
5454
max_keepalive_connections=max_keepalive_connections,
@@ -59,6 +59,9 @@ def __init__(
5959
self.headers = self._get_headers(token)
6060
self.patch_headers = self._get_patch_headers(token)
6161

62+
def __exit__(self, *_: Any) -> None:
63+
self.client.close()
64+
6265
def request(
6366
self,
6467
method: str,

centraldogma/content_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
from httpx import Response
2121

22-
from centraldogma.base_client import BaseClient
22+
from centraldogma._sync.base_client import BaseClient
2323
from centraldogma.data import Content
2424
from centraldogma.data.change import Change
2525
from centraldogma.data.commit import Commit

centraldogma/dogma.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import os
1515
from typing import List, Optional, TypeVar, Callable
1616

17-
from centraldogma.base_client import BaseClient
17+
from centraldogma._sync.base_client import BaseClient
1818
from centraldogma.content_service import ContentService
1919

2020
# noinspection PyUnresolvedReferences

centraldogma/project_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from http import HTTPStatus
1515
from typing import List
1616

17-
from centraldogma.base_client import BaseClient
17+
from centraldogma._sync.base_client import BaseClient
1818
from centraldogma.data import Project
1919

2020

centraldogma/repository_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from http import HTTPStatus
1515
from typing import List
1616

17-
from centraldogma.base_client import BaseClient
17+
from centraldogma._sync.base_client import BaseClient
1818
from centraldogma.data import Repository
1919

2020

0 commit comments

Comments
 (0)