Skip to content

Commit 687bca7

Browse files
zeevdrclaude
andcommitted
Python SDK v0.1.0: sync/async clients, ConfigWatcher, 171 tests, 97% coverage
Co-Authored-By: Claude <noreply@anthropic.com>
1 parent ab13738 commit 687bca7

36 files changed

Lines changed: 4380 additions & 0 deletions
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
name: Bug Report
3+
about: Report a bug in the OpenDecree Python SDK
4+
title: ''
5+
labels: bug
6+
assignees: ''
7+
---
8+
9+
## Describe the bug
10+
11+
A clear description of what the bug is.
12+
13+
## To reproduce
14+
15+
Steps to reproduce the behavior:
16+
17+
1. ...
18+
2. ...
19+
20+
## Expected behavior
21+
22+
What you expected to happen.
23+
24+
## Environment
25+
26+
- `opendecree` version: (e.g., 0.1.0)
27+
- Python version: (e.g., 3.12)
28+
- OS: (e.g., Linux, macOS)
29+
- OpenDecree server version: (e.g., 0.3.1)
30+
31+
## Additional context
32+
33+
Any other context, logs, or tracebacks.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
name: Feature Request
3+
about: Suggest a new feature or improvement
4+
title: ''
5+
labels: enhancement
6+
assignees: ''
7+
---
8+
9+
## Problem
10+
11+
What problem does this feature solve? What's the use case?
12+
13+
## Proposed solution
14+
15+
Describe the solution you'd like.
16+
17+
## Alternatives considered
18+
19+
Any alternative solutions or workarounds you've considered.
20+
21+
## Additional context
22+
23+
Any other context, mockups, or examples.

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
## Summary
2+
3+
What does this PR do?
4+
5+
## Test plan
6+
7+
- [ ] `make lint` passes
8+
- [ ] `make typecheck` passes
9+
- [ ] `make test` passes (107+ tests, 80%+ coverage)

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6+
7+
## [0.1.0] - 2026-04-12
8+
9+
### Added
10+
11+
- ConfigClient (sync) with `@overload` typed `get()` returning str/int/float/bool/timedelta
12+
- AsyncConfigClient mirroring the sync API with async/await
13+
- ConfigWatcher with `WatchedField[T]` for live config subscriptions (background thread)
14+
- AsyncConfigWatcher for asyncio-native subscriptions (background task)
15+
- Error hierarchy mapping gRPC status codes to typed Python exceptions
16+
- Exponential backoff retry with jitter for transient gRPC errors
17+
- Auth metadata interceptors (x-subject, x-role, x-tenant-id, Bearer token)
18+
- Context managers for all client and watcher lifecycles
19+
- `on_change` callbacks and `changes()` iterators on watched fields
20+
- Auto-reconnect with backoff on subscription stream failures
21+
22+
[0.1.0]: https://github.com/opendecree/decree-python/releases/tag/v0.1.0

CODE_OF_CONDUCT.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Code of Conduct
2+
3+
This project follows the [Contributor Covenant v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/).
4+
5+
## Reporting
6+
7+
If you experience or witness unacceptable behavior, please email **via GitHub Issues**.
8+
9+
All reports will be reviewed and investigated promptly and fairly.

CONTRIBUTING.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Contributing to OpenDecree Python SDK
2+
3+
Thank you for your interest in contributing! This guide covers how to set up your development environment, build, test, and submit changes.
4+
5+
## Prerequisites
6+
7+
- **Python** (3.11+)
8+
- **Docker**
9+
- **Make**
10+
11+
That's it. All dev tools (ruff, mypy, pytest, protoc) run inside Docker — no local installation needed.
12+
13+
## Getting Started
14+
15+
```bash
16+
# Clone the repository
17+
git clone https://github.com/opendecree/decree-python.git
18+
cd decree-python
19+
20+
# Build the tools image (one-time)
21+
make tools
22+
23+
# Run the full check suite
24+
make lint && make typecheck && make test
25+
```
26+
27+
## Development Cycle
28+
29+
```
30+
edit code -> lint -> typecheck -> test -> commit -> PR
31+
```
32+
33+
### Makefile Targets
34+
35+
| Target | Description |
36+
|--------|-------------|
37+
| `make generate` | Regenerate proto stubs from BSR |
38+
| `make lint` | Lint with ruff (check + format) |
39+
| `make format` | Auto-format with ruff |
40+
| `make typecheck` | Type check with mypy (strict) |
41+
| `make test` | Run tests with coverage (pytest) |
42+
| `make build` | Build sdist + wheel |
43+
| `make clean` | Remove build artifacts |
44+
45+
### Proto Stubs
46+
47+
Generated proto stubs live in `sdk/src/opendecree/_generated/` and are committed to git. If the upstream `.proto` files change, regenerate with:
48+
49+
```bash
50+
make generate
51+
```
52+
53+
This requires the `decree` repo checked out alongside `decree-python` (for proto source files).
54+
55+
## Project Structure
56+
57+
```
58+
sdk/
59+
├── src/opendecree/ # SDK source
60+
│ ├── client.py # ConfigClient (sync)
61+
│ ├── async_client.py # AsyncConfigClient
62+
│ ├── watcher.py # ConfigWatcher (sync)
63+
│ ├── async_watcher.py # AsyncConfigWatcher
64+
│ ├── errors.py # Exception hierarchy
65+
│ ├── types.py # Dataclass return types
66+
│ ├── _channel.py # gRPC channel factory
67+
│ ├── _interceptors.py # Auth metadata interceptors
68+
│ ├── _retry.py # Exponential backoff retry
69+
│ ├── _convert.py # TypedValue conversion
70+
│ ├── _stubs.py # Lazy proto stub loading
71+
│ └── _generated/ # Proto stubs (committed)
72+
├── tests/ # pytest test suite
73+
├── docs/ # Usage documentation
74+
└── pyproject.toml # Package metadata + tool config
75+
```
76+
77+
## Testing
78+
79+
```bash
80+
make test
81+
```
82+
83+
Tests use pytest with pytest-asyncio. Coverage must stay above 80% (enforced in pyproject.toml). Tests mock gRPC stubs — no running server needed.
84+
85+
## Code Style
86+
87+
- **Linting and formatting**: ruff (replaces black + isort + flake8)
88+
- **Type checking**: mypy in strict mode
89+
- Run `make lint && make typecheck` before submitting
90+
91+
## Submitting Changes
92+
93+
1. Fork the repository
94+
2. Create a feature branch from `main`
95+
3. Make your changes
96+
4. Ensure `make lint && make typecheck && make test` passes
97+
5. Open a pull request against `main`
98+
99+
## License
100+
101+
By contributing, you agree that your contributions will be licensed under the Apache License 2.0.

SECURITY.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Security Policy
2+
3+
## Reporting a Vulnerability
4+
5+
If you discover a security vulnerability in the OpenDecree Python SDK, please report it responsibly.
6+
7+
**Do not open a public GitHub issue for security vulnerabilities.**
8+
9+
Instead, please email **via GitHub Security Advisories** with:
10+
11+
1. A description of the vulnerability
12+
2. Steps to reproduce
13+
3. The potential impact
14+
4. Any suggested fix (optional)
15+
16+
You should receive a response within 48 hours. We will work with you to understand and address the issue before any public disclosure.
17+
18+
## Supported Versions
19+
20+
| Version | Supported |
21+
|---------|-----------|
22+
| latest | Yes |
23+
24+
## Scope
25+
26+
This policy covers the `opendecree` Python package published on PyPI.

build/Dockerfile.tools

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
FROM python:3.12-slim-bookworm
2+
3+
RUN pip install --no-cache-dir \
4+
grpcio-tools>=1.68.0 \
5+
mypy-protobuf>=3.6 \
6+
mypy>=1.14 \
7+
ruff>=0.8 \
8+
pytest>=8.3 \
9+
pytest-cov>=6.0 \
10+
pytest-asyncio>=0.24 \
11+
grpcio>=1.68.0 \
12+
protobuf>=5.29.0 \
13+
googleapis-common-protos>=1.66.0 \
14+
build \
15+
pdoc>=15.0
16+
17+
WORKDIR /workspace

sdk/docs/async.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Async Usage
2+
3+
The SDK provides async equivalents for all sync APIs, built on `grpc.aio`.
4+
5+
## AsyncConfigClient
6+
7+
```python
8+
from opendecree import AsyncConfigClient
9+
10+
async with AsyncConfigClient("localhost:9090", subject="myapp") as client:
11+
# Typed gets (same overload pattern as sync)
12+
fee = await client.get("tenant-id", "payments.fee") # → str
13+
retries = await client.get("tenant-id", "payments.retries", int) # → int
14+
enabled = await client.get("tenant-id", "payments.enabled", bool)# → bool
15+
16+
# Get all config
17+
all_config = await client.get_all("tenant-id") # → dict[str, str]
18+
19+
# Writes
20+
await client.set("tenant-id", "payments.fee", "0.5%")
21+
await client.set_many("tenant-id", {"a": "1", "b": "2"})
22+
await client.set_null("tenant-id", "payments.fee")
23+
```
24+
25+
Same constructor options as `ConfigClient` — see [Configuration](configuration.md).
26+
27+
## AsyncConfigWatcher
28+
29+
```python
30+
from opendecree import AsyncConfigClient
31+
32+
async with AsyncConfigClient("localhost:9090", subject="myapp") as client:
33+
async with client.watch("tenant-id") as watcher:
34+
fee = watcher.field("payments.fee", float, default=0.01)
35+
enabled = watcher.field("payments.enabled", bool, default=False)
36+
37+
# .value works the same
38+
print(fee.value)
39+
40+
# __bool__ works the same
41+
if enabled:
42+
print("enabled")
43+
```
44+
45+
### Async change iteration
46+
47+
Use `async for` instead of `for`:
48+
49+
```python
50+
async with client.watch("tenant-id") as watcher:
51+
fee = watcher.field("payments.fee", float, default=0.01)
52+
53+
async for change in fee.changes():
54+
print(f"{change.old_value} -> {change.new_value}")
55+
```
56+
57+
### Callbacks
58+
59+
Callbacks work the same as the sync watcher — they are plain functions (not coroutines):
60+
61+
```python
62+
@fee.on_change
63+
def handle_change(old: float, new: float):
64+
print(f"Fee changed: {old} -> {new}")
65+
```
66+
67+
## Differences from sync
68+
69+
| Aspect | Sync | Async |
70+
|--------|------|-------|
71+
| Client | `ConfigClient` | `AsyncConfigClient` |
72+
| Context manager | `with` | `async with` |
73+
| Methods | `client.get(...)` | `await client.get(...)` |
74+
| Watcher | `ConfigWatcher` | `AsyncConfigWatcher` |
75+
| Change iterator | `for change in field.changes()` | `async for change in field.changes()` |
76+
| Background work | Thread | asyncio Task |
77+
| Callbacks | Same (plain functions) | Same (plain functions) |
78+
79+
The public API is otherwise identical — same constructor options, same `get()` overloads, same `WatchedField[T]` interface.
80+
81+
## When to use async
82+
83+
Use the async API when:
84+
- Your application already uses asyncio (FastAPI, aiohttp, etc.)
85+
- You need to manage many concurrent connections efficiently
86+
87+
Use the sync API when:
88+
- Your application is synchronous (Flask, Django, scripts)
89+
- Simplicity matters more than concurrency
90+
91+
Both APIs are equally capable and tested.

0 commit comments

Comments
 (0)