detent-sdk 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- detent_sdk-0.1.0/.github/workflows/ci.yml +49 -0
- detent_sdk-0.1.0/.github/workflows/release.yml +75 -0
- detent_sdk-0.1.0/.gitignore +9 -0
- detent_sdk-0.1.0/LICENSE +21 -0
- detent_sdk-0.1.0/PKG-INFO +71 -0
- detent_sdk-0.1.0/README.md +52 -0
- detent_sdk-0.1.0/openapi/detent.json +1760 -0
- detent_sdk-0.1.0/pyproject.toml +46 -0
- detent_sdk-0.1.0/src/detent/__init__.py +35 -0
- detent_sdk-0.1.0/src/detent/_config.py +8 -0
- detent_sdk-0.1.0/src/detent/_core.py +127 -0
- detent_sdk-0.1.0/src/detent/async_client.py +128 -0
- detent_sdk-0.1.0/src/detent/client.py +128 -0
- detent_sdk-0.1.0/src/detent/errors.py +31 -0
- detent_sdk-0.1.0/src/detent/models.py +54 -0
- detent_sdk-0.1.0/src/detent/py.typed +0 -0
- detent_sdk-0.1.0/tests/test_async_client.py +82 -0
- detent_sdk-0.1.0/tests/test_client.py +146 -0
- detent_sdk-0.1.0/tests/test_contract.py +27 -0
- detent_sdk-0.1.0/tests/test_core.py +47 -0
- detent_sdk-0.1.0/tests/test_integration.py +21 -0
- detent_sdk-0.1.0/tests/test_models.py +28 -0
- detent_sdk-0.1.0/tests/test_smoke.py +5 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
# Cancel superseded runs on the same ref (e.g. rapid PR pushes).
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ci-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
test:
|
|
18
|
+
name: test (py ${{ matrix.python }})
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
strategy:
|
|
21
|
+
fail-fast: false
|
|
22
|
+
matrix:
|
|
23
|
+
# The SDK targets Python 3.11+ (pyproject requires-python). Cover the
|
|
24
|
+
# supported minors. The OpenAPI drift-guard test (tests/test_contract.py)
|
|
25
|
+
# runs as part of pytest — no separate job needed.
|
|
26
|
+
python: ['3.11', '3.12', '3.13', '3.14']
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v5
|
|
29
|
+
|
|
30
|
+
- uses: actions/setup-python@v6
|
|
31
|
+
with:
|
|
32
|
+
python-version: ${{ matrix.python }}
|
|
33
|
+
cache: pip
|
|
34
|
+
cache-dependency-path: pyproject.toml
|
|
35
|
+
|
|
36
|
+
- name: Install (with dev extras)
|
|
37
|
+
run: pip install -e ".[dev]"
|
|
38
|
+
|
|
39
|
+
- name: Ruff lint
|
|
40
|
+
run: ruff check src tests
|
|
41
|
+
|
|
42
|
+
- name: Ruff format check
|
|
43
|
+
run: ruff format --check src tests
|
|
44
|
+
|
|
45
|
+
- name: Mypy (strict)
|
|
46
|
+
run: mypy src/detent
|
|
47
|
+
|
|
48
|
+
- name: Test
|
|
49
|
+
run: pytest -q
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Publishes detent-sdk to PyPI when a version tag is pushed (e.g. `v0.1.0`).
|
|
4
|
+
# Auth is tokenless via PyPI Trusted Publishing (OIDC) — no API token secret.
|
|
5
|
+
# One-time setup on PyPI: add a Trusted Publisher for project `detent-sdk`
|
|
6
|
+
# (a "pending" publisher works before the first release) with:
|
|
7
|
+
# owner cguillerminet, repo detent-sdk-python, workflow release.yml.
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
tags: ['v*']
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: release-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: false
|
|
15
|
+
|
|
16
|
+
permissions:
|
|
17
|
+
contents: write # create the GitHub Release
|
|
18
|
+
id-token: write # OIDC token for PyPI Trusted Publishing
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
release:
|
|
22
|
+
name: publish to PyPI
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
# Don't let forks attempt to publish under this name.
|
|
25
|
+
if: github.repository == 'cguillerminet/detent-sdk-python'
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v5
|
|
28
|
+
|
|
29
|
+
- uses: actions/setup-python@v6
|
|
30
|
+
with:
|
|
31
|
+
python-version: '3.12'
|
|
32
|
+
|
|
33
|
+
- name: Install (dev extras + build)
|
|
34
|
+
run: pip install -e ".[dev]" build
|
|
35
|
+
|
|
36
|
+
- name: Verify tag matches package version
|
|
37
|
+
env:
|
|
38
|
+
TAG: ${{ github.ref_name }}
|
|
39
|
+
run: |
|
|
40
|
+
PKG="v$(python -c 'import tomllib,pathlib; print(tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]["version"])')"
|
|
41
|
+
echo "tag=$TAG package=$PKG"
|
|
42
|
+
if [ "$TAG" != "$PKG" ]; then
|
|
43
|
+
echo "::error::Tag $TAG does not match package version $PKG — bump pyproject or fix the tag."
|
|
44
|
+
exit 1
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
- name: Lint + type-check + test
|
|
48
|
+
run: |
|
|
49
|
+
ruff check src tests
|
|
50
|
+
ruff format --check src tests
|
|
51
|
+
mypy src/detent
|
|
52
|
+
pytest -q
|
|
53
|
+
|
|
54
|
+
- name: Build sdist + wheel
|
|
55
|
+
run: python -m build
|
|
56
|
+
|
|
57
|
+
# Tokenless: PyPI Trusted Publishing exchanges the OIDC token. Uploads dist/*.
|
|
58
|
+
- name: Publish to PyPI
|
|
59
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
60
|
+
|
|
61
|
+
# Cut the matching GitHub Release (auto-generated notes). Idempotent.
|
|
62
|
+
- name: Create GitHub Release
|
|
63
|
+
env:
|
|
64
|
+
GH_TOKEN: ${{ github.token }}
|
|
65
|
+
TAG: ${{ github.ref_name }}
|
|
66
|
+
run: |
|
|
67
|
+
if gh release view "$TAG" >/dev/null 2>&1; then
|
|
68
|
+
echo "Release $TAG already exists — skipping."
|
|
69
|
+
else
|
|
70
|
+
gh release create "$TAG" \
|
|
71
|
+
--title "$TAG" \
|
|
72
|
+
--generate-notes \
|
|
73
|
+
--latest \
|
|
74
|
+
--verify-tag
|
|
75
|
+
fi
|
detent_sdk-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cyril Guillerminet
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: detent-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Typed Python client for the Detent rate-limiting API.
|
|
5
|
+
Project-URL: Homepage, https://github.com/cguillerminet/detent-sdk-python
|
|
6
|
+
Project-URL: Issues, https://github.com/cguillerminet/detent-sdk-python/issues
|
|
7
|
+
Author: Cyril GUILLERMINET
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: api-client,detent,rate-limit,rate-limiter,sdk
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Requires-Dist: httpx>=0.27
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
16
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
17
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# detent-sdk
|
|
21
|
+
|
|
22
|
+
Typed Python client for the [Detent](https://detent.dev) rate-limiting API. Sync and async, one dependency (httpx).
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install detent-sdk
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from detent import Detent
|
|
32
|
+
|
|
33
|
+
rg = Detent(api_key="rg_live_...")
|
|
34
|
+
|
|
35
|
+
# Rate-limit check (fails open by default on transport error or 5xx)
|
|
36
|
+
r = rg.limit(namespace="api", key=user_id)
|
|
37
|
+
if not r.allowed:
|
|
38
|
+
... # return HTTP 429
|
|
39
|
+
|
|
40
|
+
# Concurrent-limit lease (auto-released)
|
|
41
|
+
with rg.lease(namespace="jobs", key=user_id):
|
|
42
|
+
run_expensive_job()
|
|
43
|
+
|
|
44
|
+
# Read-only usage stats
|
|
45
|
+
stats = rg.get_stats(namespace="api")
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Async
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from detent import AsyncDetent
|
|
52
|
+
|
|
53
|
+
async with AsyncDetent(api_key="rg_live_...") as rg:
|
|
54
|
+
r = await rg.limit(namespace="api", key=user_id)
|
|
55
|
+
async with rg.lease(namespace="jobs", key=user_id):
|
|
56
|
+
await run_expensive_job()
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Configuration
|
|
60
|
+
|
|
61
|
+
| Option | Default | Notes |
|
|
62
|
+
|-------------|----------------------------|----------------------------------------------------------|
|
|
63
|
+
| `api_key` | — (required) | `rg_live_…` / `rg_test_…` |
|
|
64
|
+
| `base_url` | `https://api.detent.dev` | Override for self-host / tests |
|
|
65
|
+
| `timeout` | `1.0` | Seconds; client-side transport timeout |
|
|
66
|
+
| `fail_mode` | `"open"` | `"open"` allows, `"closed"` denies on a degraded backend |
|
|
67
|
+
| `on_error` | `None` | Called on a degraded (transport/5xx) `limit()` call |
|
|
68
|
+
|
|
69
|
+
`limit()` never raises on a degraded backend (transport error or 5xx) — it
|
|
70
|
+
returns a result with `degraded=True`. A `4xx` (bad key, plan gate, unknown
|
|
71
|
+
rule) raises `DetentAPIError`.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# detent-sdk
|
|
2
|
+
|
|
3
|
+
Typed Python client for the [Detent](https://detent.dev) rate-limiting API. Sync and async, one dependency (httpx).
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install detent-sdk
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from detent import Detent
|
|
13
|
+
|
|
14
|
+
rg = Detent(api_key="rg_live_...")
|
|
15
|
+
|
|
16
|
+
# Rate-limit check (fails open by default on transport error or 5xx)
|
|
17
|
+
r = rg.limit(namespace="api", key=user_id)
|
|
18
|
+
if not r.allowed:
|
|
19
|
+
... # return HTTP 429
|
|
20
|
+
|
|
21
|
+
# Concurrent-limit lease (auto-released)
|
|
22
|
+
with rg.lease(namespace="jobs", key=user_id):
|
|
23
|
+
run_expensive_job()
|
|
24
|
+
|
|
25
|
+
# Read-only usage stats
|
|
26
|
+
stats = rg.get_stats(namespace="api")
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Async
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from detent import AsyncDetent
|
|
33
|
+
|
|
34
|
+
async with AsyncDetent(api_key="rg_live_...") as rg:
|
|
35
|
+
r = await rg.limit(namespace="api", key=user_id)
|
|
36
|
+
async with rg.lease(namespace="jobs", key=user_id):
|
|
37
|
+
await run_expensive_job()
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Configuration
|
|
41
|
+
|
|
42
|
+
| Option | Default | Notes |
|
|
43
|
+
|-------------|----------------------------|----------------------------------------------------------|
|
|
44
|
+
| `api_key` | — (required) | `rg_live_…` / `rg_test_…` |
|
|
45
|
+
| `base_url` | `https://api.detent.dev` | Override for self-host / tests |
|
|
46
|
+
| `timeout` | `1.0` | Seconds; client-side transport timeout |
|
|
47
|
+
| `fail_mode` | `"open"` | `"open"` allows, `"closed"` denies on a degraded backend |
|
|
48
|
+
| `on_error` | `None` | Called on a degraded (transport/5xx) `limit()` call |
|
|
49
|
+
|
|
50
|
+
`limit()` never raises on a degraded backend (transport error or 5xx) — it
|
|
51
|
+
returns a result with `degraded=True`. A `4xx` (bad key, plan gate, unknown
|
|
52
|
+
rule) raises `DetentAPIError`.
|