python-config-client 0.1.2__py3-none-any.whl

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.
@@ -0,0 +1,315 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-config-client
3
+ Version: 0.1.2
4
+ Summary: Python client SDK for Config Service
5
+ Project-URL: Homepage, https://github.com/holdemlab/python-config-client
6
+ Project-URL: Repository, https://github.com/holdemlab/python-config-client
7
+ Project-URL: Issues, https://github.com/holdemlab/python-config-client/issues
8
+ Author-email: Maks <maksymchuk.mm@gmail.com>
9
+ License: MIT
10
+ Keywords: aes-gcm,client,config,sdk,sse
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: cryptography>=42
22
+ Requires-Dist: httpx-sse>=0.4
23
+ Requires-Dist: httpx>=0.27
24
+ Provides-Extra: pydantic
25
+ Requires-Dist: pydantic>=2.0; extra == 'pydantic'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # python-config-client
29
+
30
+ Python SDK for Config Service — fetch, decrypt, and hot-reload service configurations with AES-256-GCM encryption.
31
+
32
+ [![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/)
33
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
34
+ [![Typed](https://img.shields.io/badge/typing-strict-green)](https://mypy.readthedocs.io/)
35
+
36
+ ## Features
37
+
38
+ - **AES-256-GCM decryption** — compatible with Go SDK's wire format
39
+ - **Sync HTTP client** via `httpx` with configurable retry + exponential backoff
40
+ - **SSE watch mode** with auto-reconnect and backoff
41
+ - **Thread-safe `Snapshot[T]`** for zero-lock hot-reload reads
42
+ - **Flexible deserialization** — `dataclass`, Pydantic v2 `BaseModel`, or `dict`
43
+ - **Full `mypy --strict` typing** — no `Any` leaks in the public API
44
+
45
+ ## Requirements
46
+
47
+ - Python ≥ 3.10
48
+ - A running Config Service with a valid service token and encryption key
49
+
50
+ ## Installation
51
+
52
+ ```bash
53
+ pip install python-config-client
54
+
55
+ # With Pydantic v2 support
56
+ pip install "python-config-client[pydantic]"
57
+ ```
58
+
59
+ ## Quick Start
60
+
61
+ ```python
62
+ import os
63
+ from dataclasses import dataclass
64
+ from config_client import ConfigClient, Options
65
+
66
+ @dataclass
67
+ class AppConfig:
68
+ log_level: str
69
+ debug: bool
70
+
71
+ with ConfigClient(Options(
72
+ host=os.environ["CONFIG_SERVICE_HOST"],
73
+ service_token=os.environ["CONFIG_SERVICE_TOKEN"],
74
+ encryption_key=os.environ["CONFIG_SERVICE_KEY"],
75
+ )) as client:
76
+ cfg = client.get("my-service", AppConfig)
77
+ print(cfg.log_level)
78
+ ```
79
+
80
+ ## Configuration
81
+
82
+ ### `Options`
83
+
84
+ | Field | Type | Default | Description |
85
+ |-------|------|---------|-------------|
86
+ | `host` | `str` | required | Base URL of Config Service, e.g. `https://config.example.com` |
87
+ | `service_token` | `str` | required | Plain-text service token (sent as `X-Service-Token`) |
88
+ | `encryption_key` | `str` | required | 64-character hex string representing a 32-byte AES-256 key |
89
+ | `request_timeout` | `float` | `10.0` | Per-request timeout in seconds |
90
+ | `retry_count` | `int` | `3` | Number of retry attempts for 5xx / network errors |
91
+ | `retry_delay` | `float` | `1.0` | Base delay for exponential backoff (seconds) |
92
+ | `on_error` | `Callable[[Exception], None] \| None` | `None` | Invoked on watch errors; does not stop the loop |
93
+ | `on_change` | `Callable[[str], None] \| None` | `None` | Invoked with config name on each received change event |
94
+ | `http_client` | `httpx.Client \| None` | `None` | BYO client (caller owns lifecycle) |
95
+
96
+ ### Environment Variables
97
+
98
+ Load all three required options from the environment with `from_env()`:
99
+
100
+ ```python
101
+ from config_client import ConfigClient
102
+
103
+ client = ConfigClient.from_env()
104
+ ```
105
+
106
+ | Variable | Description |
107
+ |----------|-------------|
108
+ | `CONFIG_SERVICE_HOST` | Base URL of Config Service |
109
+ | `CONFIG_SERVICE_TOKEN` | Plain-text service token |
110
+ | `CONFIG_SERVICE_KEY` | AES-256 encryption key (64 hex chars) |
111
+
112
+ ### `GetOptions`
113
+
114
+ Per-request overrides for `get()`, `get_raw()`, `get_bytes()`:
115
+
116
+ ```python
117
+ from config_client import GetOptions
118
+
119
+ cfg = client.get("my-service", AppConfig, GetOptions(environment="production", version=5))
120
+ ```
121
+
122
+ | Field | Type | Default | Description |
123
+ |-------|------|---------|-------------|
124
+ | `environment` | `str` | `""` | Environment override (e.g. `"production"`) |
125
+ | `version` | `int` | `0` | Specific version to fetch (`0` = latest) |
126
+
127
+ ## API Reference
128
+
129
+ ### `ConfigClient`
130
+
131
+ #### `get(config_name, target_type, opts=None) → T`
132
+
133
+ Fetch, decrypt (AES-256-GCM), and deserialize a configuration. Dispatches to:
134
+ - `pydantic.BaseModel.model_validate(data)` — for Pydantic v2 models
135
+ - Recursive dataclass construction — for `@dataclass` types
136
+ - Identity passthrough — for `dict`
137
+
138
+ ```python
139
+ cfg = client.get("my-service", AppConfig)
140
+ ```
141
+
142
+ #### `get_raw(config_name, opts=None) → dict[str, object]`
143
+
144
+ Decrypt and return the raw JSON as a `dict`.
145
+
146
+ ```python
147
+ data = client.get_raw("my-service")
148
+ ```
149
+
150
+ #### `get_bytes(config_name, opts=None) → bytes`
151
+
152
+ Decrypt and return raw JSON bytes (no parsing).
153
+
154
+ ```python
155
+ raw = client.get_bytes("my-service")
156
+ ```
157
+
158
+ #### `get_formatted(config_name, fmt) → bytes`
159
+
160
+ Fetch a config in plaintext via the `/formatted` endpoint. No decryption.
161
+
162
+ ```python
163
+ from config_client import Format
164
+
165
+ yaml_bytes = client.get_formatted("my-service", Format.YAML)
166
+ env_bytes = client.get_formatted("my-service", Format.ENV)
167
+ json_bytes = client.get_formatted("my-service", Format.JSON)
168
+ ```
169
+
170
+ #### `list() → list[ConfigInfo]`
171
+
172
+ Return metadata for all configurations accessible to this token.
173
+
174
+ ```python
175
+ for info in client.list():
176
+ print(info.name, info.is_valid, info.updated_at)
177
+ ```
178
+
179
+ `ConfigInfo` fields: `name: str`, `is_valid: bool`, `valid_from: datetime`, `updated_at: datetime`.
180
+
181
+ #### `watch(config_name, callback)`
182
+
183
+ Subscribe to SSE change events. Blocks the calling thread. Auto-reconnects.
184
+
185
+ #### `watch_and_decode(config_name, target_type, snapshot, opts=None)`
186
+
187
+ Watch for changes and update a `Snapshot[T]` on every change. Blocks.
188
+
189
+ #### `from_env() → ConfigClient`
190
+
191
+ Class method. Creates a client from `CONFIG_SERVICE_HOST`, `CONFIG_SERVICE_TOKEN`, `CONFIG_SERVICE_KEY`.
192
+
193
+ #### `close()`
194
+
195
+ Stop all watch loops and close the HTTP transport. Also called by `__exit__`.
196
+
197
+ ## Watch & Hot-Reload
198
+
199
+ Use `watch_and_decode` inside a daemon thread to keep a `Snapshot` up to date without blocking the main thread:
200
+
201
+ ```python
202
+ import threading
203
+ from dataclasses import dataclass
204
+ from config_client import ConfigClient, Options, Snapshot
205
+
206
+ @dataclass
207
+ class FeatureFlags:
208
+ dark_mode: bool
209
+ max_connections: int
210
+
211
+ snapshot: Snapshot[FeatureFlags] = Snapshot()
212
+ client = ConfigClient.from_env()
213
+
214
+ def _watcher() -> None:
215
+ client.watch_and_decode("feature-flags", FeatureFlags, snapshot)
216
+
217
+ t = threading.Thread(target=_watcher, daemon=True)
218
+ t.start()
219
+
220
+ # At any point — zero-lock read:
221
+ flags = snapshot.load()
222
+ if flags is not None and flags.dark_mode:
223
+ print("dark mode enabled")
224
+ ```
225
+
226
+ Call `client.close()` to stop the watcher thread gracefully.
227
+
228
+ ## Error Handling
229
+
230
+ | Exception | HTTP status / cause |
231
+ |-----------|---------------------|
232
+ | `UnauthorizedError` | HTTP 401 |
233
+ | `ForbiddenError` | HTTP 403 |
234
+ | `NotFoundError` | HTTP 404 |
235
+ | `InvalidResponseError` | Any other 4xx |
236
+ | `ConnectionError` | Network error or 5xx after all retries exhausted |
237
+ | `DecryptionError` | AES-GCM authentication failure or malformed ciphertext |
238
+ | `UnmarshalError` | JSON → target type deserialization failed |
239
+ | `ConfigClientError` | Base class for all SDK exceptions; also raised for invalid options |
240
+
241
+ ```python
242
+ from config_client import ConfigClient, NotFoundError, DecryptionError
243
+
244
+ try:
245
+ cfg = client.get("my-service", AppConfig)
246
+ except NotFoundError:
247
+ print("config not found")
248
+ except DecryptionError:
249
+ print("wrong encryption key or corrupted payload")
250
+ ```
251
+
252
+ ## Development
253
+
254
+ ### Requirements
255
+
256
+ - [uv](https://github.com/astral-sh/uv) ≥ 0.4
257
+
258
+ ### Setup
259
+
260
+ ```bash
261
+ git clone https://github.com/holdemlab/python-config-client
262
+ cd python-config-client
263
+ make install
264
+ cp .env.example .env # fill in real values for integration tests
265
+ ```
266
+
267
+ ### Makefile Commands
268
+
269
+ | Command | Description |
270
+ |---------|-------------|
271
+ | `make install` | Install all dependencies (including dev) |
272
+ | `make test` | Run unit + integration tests with coverage |
273
+ | `make unit` | Unit tests only |
274
+ | `make lint` | `ruff check` + `ruff format --check` |
275
+ | `make fmt` | Auto-format with `ruff format` |
276
+ | `make typecheck` | `mypy config_client --strict` |
277
+ | `make security` | `bandit -r config_client` + `uv audit` |
278
+ | `make build` | Build sdist + wheel (`uv build`) |
279
+ | `make docker-up` | Start postgres + app via docker-compose |
280
+ | `make docker-down` | Stop containers |
281
+
282
+ ### Running Tests
283
+
284
+ ```bash
285
+ # All unit tests (fast, no external services):
286
+ make unit
287
+
288
+ # Integration tests (requires CONFIG_SERVICE_* env vars):
289
+ make integration
290
+
291
+ # Full suite with coverage report:
292
+ make test
293
+ ```
294
+
295
+ ### Project Structure
296
+
297
+ ```
298
+ config_client/
299
+ __init__.py # public API surface
300
+ client.py # ConfigClient main class
301
+ options.py # Options / GetOptions dataclasses
302
+ types.py # ConfigInfo, ConfigChangeEvent, Format
303
+ errors.py # exception hierarchy
304
+ crypto.py # AES-256-GCM decrypt + parse_encryption_key
305
+ transport.py # httpx transport with retry + backoff
306
+ _sse.py # internal SSE reader (private)
307
+ snapshot.py # thread-safe Snapshot[T]
308
+
309
+ tests/ # pytest suite (97% coverage)
310
+ examples/ # runnable usage examples
311
+ ```
312
+
313
+ ## License
314
+
315
+ MIT — see [LICENSE](LICENSE) for details.
@@ -0,0 +1,12 @@
1
+ config_client/__init__.py,sha256=CHgJWBDHGW7DfmsM6jW90hhjGX0fziEwL7jjgz5hq58,1214
2
+ config_client/_sse.py,sha256=0VRo1TxiIDNviHHY44bB-_K-EXm1y8GzA7F-uhijSLM,3427
3
+ config_client/client.py,sha256=ecpg5eBumqb4p5wxB-_gWhYSNNrofXBd6w3cbmrLqD4,17901
4
+ config_client/crypto.py,sha256=kdOsvJ-ziCcERDhAoOWr4YDHQDZPhy4RQDuXTGNUg8k,2331
5
+ config_client/errors.py,sha256=Q8Y3umQhRRCmD6X4BppHo6dTbW_HBKukY5uXL1hnNkc,2001
6
+ config_client/options.py,sha256=kYDGwdChIauaA22J8ZmLMejGT-WfNwU0VRMDxmTxq_g,3328
7
+ config_client/snapshot.py,sha256=tO7R9JdfrbsSWT6RUX5ovCqTz4EFre9FjRuQqsGZZrU,1453
8
+ config_client/transport.py,sha256=vRsPO8d5mIwpXC6WWBhgtvWO0AmjRptTMTiYRcqGPtI,6960
9
+ config_client/types.py,sha256=5bAL1g1AqGbqpDcW6IFXYx21J3usarZVj4Ic8mwAk-8,1559
10
+ python_config_client-0.1.2.dist-info/METADATA,sha256=HlzMdTB6FMgLDo-UdMpE6Zal5weBFPp4Cwt1rNi_4oA,9681
11
+ python_config_client-0.1.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
12
+ python_config_client-0.1.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any