configdirector-server-sdk 0.2.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.
- configdirector_server_sdk-0.2.0/.gitignore +30 -0
- configdirector_server_sdk-0.2.0/LICENSE +21 -0
- configdirector_server_sdk-0.2.0/PKG-INFO +49 -0
- configdirector_server_sdk-0.2.0/README.md +23 -0
- configdirector_server_sdk-0.2.0/profiling/README.md +126 -0
- configdirector_server_sdk-0.2.0/pyproject.toml +118 -0
- configdirector_server_sdk-0.2.0/samples/README.md +23 -0
- configdirector_server_sdk-0.2.0/samples/flask/README.md +115 -0
- configdirector_server_sdk-0.2.0/src/configdirector/__init__.py +83 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_bundle.py +176 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_evaluation/__init__.py +47 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_evaluation/_json_pointer.py +40 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_evaluation/_json_value.py +28 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_evaluation/_rapidhash.py +115 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_evaluation/array_comparison.py +25 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_evaluation/condition_evaluator.py +94 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_evaluation/config_evaluator.py +123 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_evaluation/date_comparison.py +77 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_evaluation/numeric_comparison.py +61 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_evaluation/percent_hashing.py +12 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_evaluation/semver_comparison.py +63 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_evaluation/text_comparison.py +40 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_evaluation/types.py +134 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_eventsource/__init__.py +27 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_eventsource/client.py +350 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_eventsource/errors.py +31 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_eventsource/parser.py +175 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_eventsource/transport.py +162 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_eventsource/types.py +58 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_http.py +86 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_logger.py +12 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_telemetry/__init__.py +49 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_telemetry/collector.py +188 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_telemetry/compact_json.py +40 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_telemetry/events.py +156 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_telemetry/queue.py +126 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_telemetry/reporter.py +157 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_telemetry/value_id.py +30 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_transport/__init__.py +23 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_transport/base.py +84 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_transport/polling.py +159 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_transport/streaming.py +110 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_value_parser.py +94 -0
- configdirector_server_sdk-0.2.0/src/configdirector/_version.py +3 -0
- configdirector_server_sdk-0.2.0/src/configdirector/client.py +640 -0
- configdirector_server_sdk-0.2.0/src/configdirector/errors.py +45 -0
- configdirector_server_sdk-0.2.0/src/configdirector/py.typed +0 -0
- configdirector_server_sdk-0.2.0/src/configdirector/types.py +552 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*.egg-info/
|
|
4
|
+
.eggs/
|
|
5
|
+
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
|
|
9
|
+
.venv/
|
|
10
|
+
venv/
|
|
11
|
+
|
|
12
|
+
.pytest_cache/
|
|
13
|
+
.mypy_cache/
|
|
14
|
+
.ruff_cache/
|
|
15
|
+
.coverage
|
|
16
|
+
.coverage.*
|
|
17
|
+
coverage.xml
|
|
18
|
+
htmlcov/
|
|
19
|
+
|
|
20
|
+
# Sample apps and the profiling harness resolve the SDK from a local path, so their locks are
|
|
21
|
+
# not portable.
|
|
22
|
+
samples/*/uv.lock
|
|
23
|
+
profiling/uv.lock
|
|
24
|
+
.env
|
|
25
|
+
|
|
26
|
+
.DS_Store
|
|
27
|
+
.idea/
|
|
28
|
+
*.swp
|
|
29
|
+
.claude
|
|
30
|
+
.vscode
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ConfigDirector
|
|
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 furnished
|
|
10
|
+
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,49 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: configdirector-server-sdk
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python server SDK for ConfigDirector. ConfigDirector is a remote configuration and feature flag service.
|
|
5
|
+
Project-URL: Homepage, https://www.configdirector.com
|
|
6
|
+
Project-URL: Documentation, https://docs.configdirector.com/sdks/server/python
|
|
7
|
+
Project-URL: Repository, https://github.com/ConfigDirector/python-server-sdk
|
|
8
|
+
Project-URL: Support, https://www.configdirector.com/support
|
|
9
|
+
Author: ConfigDirector
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: config,configdirector,configuration,feature flag,feature switch,feature toggle,remote config,remote configuration
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: urllib3<3,>=2.7
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# ConfigDirector Python SDK
|
|
28
|
+
|
|
29
|
+
This is the Python server SDK for [ConfigDirector](https://www.configdirector.com).
|
|
30
|
+
|
|
31
|
+
## Documentation
|
|
32
|
+
|
|
33
|
+
Refer to the [official documentation for the Python SDK](https://docs.configdirector.com/sdks/server/python).
|
|
34
|
+
|
|
35
|
+
There is also [a quickstart guide for ConfigDirector and any of our SDKs](https://docs.configdirector.com/getting-started/quickstart).
|
|
36
|
+
|
|
37
|
+
## Sample apps
|
|
38
|
+
|
|
39
|
+
[`samples/`](samples/) holds small, runnable applications built on this SDK, one per web
|
|
40
|
+
framework. Start with [`samples/flask`](samples/flask/):
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
cd samples/flask
|
|
44
|
+
uv run flask --app app run --port 3600
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Getting Help
|
|
48
|
+
|
|
49
|
+
Reach out to us via https://www.configdirector.com/support
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# ConfigDirector Python SDK
|
|
2
|
+
|
|
3
|
+
This is the Python server SDK for [ConfigDirector](https://www.configdirector.com).
|
|
4
|
+
|
|
5
|
+
## Documentation
|
|
6
|
+
|
|
7
|
+
Refer to the [official documentation for the Python SDK](https://docs.configdirector.com/sdks/server/python).
|
|
8
|
+
|
|
9
|
+
There is also [a quickstart guide for ConfigDirector and any of our SDKs](https://docs.configdirector.com/getting-started/quickstart).
|
|
10
|
+
|
|
11
|
+
## Sample apps
|
|
12
|
+
|
|
13
|
+
[`samples/`](samples/) holds small, runnable applications built on this SDK, one per web
|
|
14
|
+
framework. Start with [`samples/flask`](samples/flask/):
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
cd samples/flask
|
|
18
|
+
uv run flask --app app run --port 3600
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Getting Help
|
|
22
|
+
|
|
23
|
+
Reach out to us via https://www.configdirector.com/support
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Profiling
|
|
2
|
+
|
|
3
|
+
Exploratory load profiling for the SDK: drive the [Flask sample](../samples/flask) at a chosen
|
|
4
|
+
request rate for a chosen length of time, record the app process's CPU and memory the whole way
|
|
5
|
+
through, and write the result out in a form you can graph and compare.
|
|
6
|
+
|
|
7
|
+
This is a measurement tool, not a check. It is not part of `make check-all` — it needs a real
|
|
8
|
+
server SDK key, takes minutes, and its numbers depend on the machine it ran on.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
make profile # 25 rps for 60s, the default
|
|
12
|
+
make profile ARGS="--rps 100 --duration 300" # anything the CLI takes
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or directly, which is what the make target does:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
cd profiling
|
|
19
|
+
uv run python run.py --rps 100 --duration 300 --label hundred-rps
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Results land in `profiling/results/<timestamp>-<label>/` (gitignored). Start with `summary.md`,
|
|
23
|
+
graph `metrics.csv`, open `chart.html` if you would rather not.
|
|
24
|
+
|
|
25
|
+
## What one run does
|
|
26
|
+
|
|
27
|
+
| Phase | Why it exists |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| **baseline** (`--baseline`, 5s) | The app is up and idle. Resting RSS and CPU, which every later number is read against. |
|
|
30
|
+
| **warmup** (`--warmup-requests`, 50) | Imports, connection setup, and the SDK's first evaluation of each config all cost something once. Excluded from the report so they do not look like a startup spike. |
|
|
31
|
+
| **load** (`--rps`, `--duration`) | The measurement. |
|
|
32
|
+
| **cooldown** (`--cooldown`, 20s) | Traffic stops, sampling does not. Memory that does not come back down here is the interesting kind. Raise it past 30s to catch a telemetry flush at its default interval. |
|
|
33
|
+
|
|
34
|
+
Then the app gets SIGTERM — the sample's `atexit` hook closes the client and flushes telemetry,
|
|
35
|
+
the same as a real shutdown — and the report is written.
|
|
36
|
+
|
|
37
|
+
## The knobs
|
|
38
|
+
|
|
39
|
+
| Flag | Default | What it changes |
|
|
40
|
+
| --- | --- | --- |
|
|
41
|
+
| `--rps` | 25 | Requests per second. Anything from 1 to ~100 is comfortable on a laptop. |
|
|
42
|
+
| `--duration` | 60 | Seconds to hold that rate. |
|
|
43
|
+
| `--distinct-users` | 500 | How many distinct user ids the traffic cycles through. Raise it to grow the SDK's per-context telemetry; drop it to 1 to take context cardinality out of the picture. |
|
|
44
|
+
| `--mode` | `streaming` | SDK connection mode: `streaming`, `polling`, or `one-time`. |
|
|
45
|
+
| `--offline` | off | Point the SDK at an address nothing answers on. Every config resolves to its default, so this profiles the fallback path — and gives a network-free comparison run. |
|
|
46
|
+
| `--sample-interval` | 0.25 | Seconds between CPU/memory samples. |
|
|
47
|
+
| `--max-in-flight` | `max(50, 4×rps)` | Concurrency cap. Requests over the cap are recorded as skipped rather than queued. |
|
|
48
|
+
| `--cpu-profile` | off | Also run under cProfile. **Serves requests serially and inflates every timing** — a separate investigation, not an addition to a normal run. |
|
|
49
|
+
| `--tracemalloc` | off | Also track allocations, to attribute memory growth to source lines. Significant overhead. POSIX only. |
|
|
50
|
+
| `--label` | — | Suffix for the results directory, so runs are tellable apart. |
|
|
51
|
+
|
|
52
|
+
## What you get
|
|
53
|
+
|
|
54
|
+
| File | What it is |
|
|
55
|
+
| --- | --- |
|
|
56
|
+
| `metrics.csv` | **One row per second**, with CPU, memory and request statistics already on the same clock. This is the file to graph — every column is numeric, `t_seconds` is the x-axis. |
|
|
57
|
+
| `chart.html` | Memory, CPU, throughput and latency plotted over time. Self-contained; open it in a browser. |
|
|
58
|
+
| `summary.md` | The headline numbers, laid out to read. |
|
|
59
|
+
| `summary.json` | The same numbers, for diffing two runs mechanically. |
|
|
60
|
+
| `samples.csv` | Raw CPU/memory samples at the sampler's interval — finer than one second. |
|
|
61
|
+
| `requests.csv` | One row per request: offset, latency, status. |
|
|
62
|
+
| `run.json` | Settings, phase boundaries, Python/SDK versions, commit. |
|
|
63
|
+
| `server.log` | The app's own output. |
|
|
64
|
+
| `cprofile.pstats`, `cprofile.txt` | Function-level CPU, with `--cpu-profile`. Browse the raw stats with `uv run python -m pstats results/<run>/cprofile.pstats`. |
|
|
65
|
+
| `tracemalloc.txt`, `tracemalloc.json` | Allocation growth by source line, with `--tracemalloc`. |
|
|
66
|
+
|
|
67
|
+
The two numbers worth watching:
|
|
68
|
+
|
|
69
|
+
* **`retained_after_cooldown_mb`** — memory still held once the traffic stopped. Growth *during*
|
|
70
|
+
load is ordinary; growth that does not come back is what a leak looks like.
|
|
71
|
+
* **`cpu_ms_per_request`** — CPU seconds burned under load, divided by requests served. Derived
|
|
72
|
+
from the process's monotonic user+system counters, so it is exact rather than an average of
|
|
73
|
+
sampled percentages. It covers the whole request, Flask and Werkzeug included, not just the
|
|
74
|
+
SDK — compare runs against each other rather than reading it as the cost of `get_value`.
|
|
75
|
+
|
|
76
|
+
## Reading the results honestly
|
|
77
|
+
|
|
78
|
+
**Check `client_ready` first.** `summary.md` says whether the SDK client ever became ready.
|
|
79
|
+
Without a working key in `samples/flask/.env` it never does, every config resolves to its default,
|
|
80
|
+
and the run measures the fallback path rather than real evaluation. The report says so in its
|
|
81
|
+
warnings; it does not stop you, because that path is worth profiling too.
|
|
82
|
+
|
|
83
|
+
**The dev server is not your production server.** The app is served by Werkzeug in one process
|
|
84
|
+
with a thread per request. That is the right shape for watching the SDK's own cost and completely
|
|
85
|
+
the wrong shape for a throughput benchmark — Gunicorn with several workers would look different in
|
|
86
|
+
every respect except the per-request SDK work.
|
|
87
|
+
|
|
88
|
+
**The harness changes two things about the sample**, both to keep the measurement honest, and both
|
|
89
|
+
in [`server.py`](server.py): Werkzeug's per-request log line is silenced, and the SDK's logger is
|
|
90
|
+
forced to WARNING. `samples/flask/.env` may ask for DEBUG, which logs every single evaluation and
|
|
91
|
+
would cost more than the evaluation being measured.
|
|
92
|
+
|
|
93
|
+
**Load is generated in a separate process** from the app, so the generator's own CPU and memory
|
|
94
|
+
never land in the numbers. The pacing is open-loop: request *i* goes out at `start + i/rps`
|
|
95
|
+
whether or not earlier ones have come back, so the offered rate stays at the target when the app
|
|
96
|
+
slows down. Back-pressure shows up as rising latency, and past `--max-in-flight` as skipped
|
|
97
|
+
requests, instead of the generator quietly throttling itself and hiding the problem.
|
|
98
|
+
|
|
99
|
+
## Working on it
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
cd profiling
|
|
103
|
+
uv sync
|
|
104
|
+
uv run mypy # this harness only; `make lint` at the root covers its style
|
|
105
|
+
uv run python report.py results/<run> # rebuild a report without re-running the load
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`report.py` is a separate entry point on purpose: regenerating a report is free, so a change to
|
|
109
|
+
the aggregation or the chart does not cost another load test.
|
|
110
|
+
|
|
111
|
+
The pieces, in the order the run uses them: [`run.py`](run.py) orchestrates,
|
|
112
|
+
[`server.py`](server.py) is the app under measurement, [`sampler.py`](sampler.py) reads CPU and
|
|
113
|
+
memory from outside the app process, [`load.py`](load.py) generates traffic,
|
|
114
|
+
[`report.py`](report.py) aggregates, [`chart.py`](chart.py) draws.
|
|
115
|
+
|
|
116
|
+
## Things worth trying
|
|
117
|
+
|
|
118
|
+
* **`--offline` against a live run.** The difference is what evaluation and telemetry actually
|
|
119
|
+
cost, with Flask's own overhead cancelled out on both sides.
|
|
120
|
+
* **`--distinct-users 1` against the default 500.** Isolates what context cardinality costs in
|
|
121
|
+
telemetry.
|
|
122
|
+
* **A long run with a long `--cooldown`.** Telemetry flushes every 30s by default; a 5-minute run
|
|
123
|
+
with a 60s cooldown shows several flushes and whether anything accumulates between them.
|
|
124
|
+
* **`--mode one-time` against `--mode streaming`.** Separates the streaming connection's standing
|
|
125
|
+
cost from the evaluation path.
|
|
126
|
+
* **`--cpu-profile` once you have a suspect**, to find which functions the time is actually in.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "configdirector-server-sdk"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Python server SDK for ConfigDirector. ConfigDirector is a remote configuration and feature flag service."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
authors = [{ name = "ConfigDirector" }]
|
|
14
|
+
keywords = [
|
|
15
|
+
"configdirector",
|
|
16
|
+
"config",
|
|
17
|
+
"configuration",
|
|
18
|
+
"remote config",
|
|
19
|
+
"remote configuration",
|
|
20
|
+
"feature flag",
|
|
21
|
+
"feature toggle",
|
|
22
|
+
"feature switch",
|
|
23
|
+
]
|
|
24
|
+
classifiers = [
|
|
25
|
+
"Development Status :: 3 - Alpha",
|
|
26
|
+
"Intended Audience :: Developers",
|
|
27
|
+
"Operating System :: OS Independent",
|
|
28
|
+
"Programming Language :: Python :: 3",
|
|
29
|
+
"Programming Language :: Python :: 3.10",
|
|
30
|
+
"Programming Language :: Python :: 3.11",
|
|
31
|
+
"Programming Language :: Python :: 3.12",
|
|
32
|
+
"Programming Language :: Python :: 3.13",
|
|
33
|
+
"Programming Language :: Python :: 3.14",
|
|
34
|
+
"Typing :: Typed",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
dependencies = ["urllib3>=2.7,<3"]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://www.configdirector.com"
|
|
41
|
+
Documentation = "https://docs.configdirector.com/sdks/server/python"
|
|
42
|
+
Repository = "https://github.com/ConfigDirector/python-server-sdk"
|
|
43
|
+
Support = "https://www.configdirector.com/support"
|
|
44
|
+
|
|
45
|
+
[dependency-groups]
|
|
46
|
+
dev = [
|
|
47
|
+
"mypy>=1.14",
|
|
48
|
+
"pytest>=8.3",
|
|
49
|
+
"pytest-cov>=6.0",
|
|
50
|
+
"pytest-timeout>=2.3",
|
|
51
|
+
"ruff>=0.9",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
[tool.hatch.version]
|
|
55
|
+
path = "src/configdirector/_version.py"
|
|
56
|
+
|
|
57
|
+
[tool.hatch.build.targets.wheel]
|
|
58
|
+
packages = ["src/configdirector"]
|
|
59
|
+
|
|
60
|
+
[tool.hatch.build.targets.sdist]
|
|
61
|
+
include = ["src/configdirector", "README.md", "LICENSE"]
|
|
62
|
+
|
|
63
|
+
[tool.ruff]
|
|
64
|
+
line-length = 110
|
|
65
|
+
target-version = "py310"
|
|
66
|
+
src = ["src", "."]
|
|
67
|
+
|
|
68
|
+
[tool.ruff.lint]
|
|
69
|
+
select = [
|
|
70
|
+
"E", # pycodestyle errors
|
|
71
|
+
"W", # pycodestyle warnings
|
|
72
|
+
"F", # pyflakes
|
|
73
|
+
"I", # isort
|
|
74
|
+
"N", # pep8-naming
|
|
75
|
+
"UP", # pyupgrade
|
|
76
|
+
"B", # flake8-bugbear
|
|
77
|
+
"A", # flake8-builtins
|
|
78
|
+
"C4", # flake8-comprehensions
|
|
79
|
+
"SIM", # flake8-simplify
|
|
80
|
+
"PT", # flake8-pytest-style
|
|
81
|
+
"RUF", # ruff-specific
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
[tool.ruff.lint.per-file-ignores]
|
|
85
|
+
# Test builders mirror the dataclass fields they construct, including `id` and `type`.
|
|
86
|
+
"tests/*" = ["S101", "A002"]
|
|
87
|
+
|
|
88
|
+
[tool.ruff.format]
|
|
89
|
+
quote-style = "double"
|
|
90
|
+
|
|
91
|
+
[tool.mypy]
|
|
92
|
+
python_version = "3.10"
|
|
93
|
+
files = ["src", "tests"]
|
|
94
|
+
strict = true
|
|
95
|
+
warn_unreachable = true
|
|
96
|
+
enable_error_code = ["redundant-expr", "possibly-undefined", "truthy-bool"]
|
|
97
|
+
|
|
98
|
+
[tool.pytest.ini_options]
|
|
99
|
+
testpaths = ["tests"]
|
|
100
|
+
# The timeout covers setup, call and teardown, and is generous next to the longest deliberate
|
|
101
|
+
# wait in the suite (~10s). Several tests park a reader on a real socket, so a threading bug
|
|
102
|
+
# surfaces as a suite that never finishes rather than as a failure; this turns that back into a
|
|
103
|
+
# failure. The thread method is the one that works here: it dumps every thread's stack, and it
|
|
104
|
+
# can end a run that is wedged on a lock held across a blocking read, which SIGALRM cannot.
|
|
105
|
+
addopts = "-ra --strict-markers --strict-config --timeout=60 --timeout-method=thread"
|
|
106
|
+
xfail_strict = true
|
|
107
|
+
|
|
108
|
+
[tool.coverage.run]
|
|
109
|
+
source = ["src/configdirector"]
|
|
110
|
+
branch = true
|
|
111
|
+
|
|
112
|
+
[tool.coverage.report]
|
|
113
|
+
exclude_lines = [
|
|
114
|
+
"pragma: no cover",
|
|
115
|
+
"raise NotImplementedError",
|
|
116
|
+
"if TYPE_CHECKING:",
|
|
117
|
+
"\\.\\.\\.",
|
|
118
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Sample apps
|
|
2
|
+
|
|
3
|
+
Small, self-contained applications showing how to use the ConfigDirector Python server SDK with
|
|
4
|
+
different web frameworks.
|
|
5
|
+
|
|
6
|
+
| Sample | Framework | Description |
|
|
7
|
+
|---|---|---|
|
|
8
|
+
| [flask/](flask/) | [Flask](https://flask.palletsprojects.com/) | Minimal WSGI app: client lifecycle, per-request evaluation, SSR hydration |
|
|
9
|
+
|
|
10
|
+
Each sample is an independent project with its own `pyproject.toml`. They depend on the SDK
|
|
11
|
+
through a local path (`configdirector-server-sdk = { path = "../.." }`) so that they always run
|
|
12
|
+
against the working copy in this repository rather than a published release.
|
|
13
|
+
|
|
14
|
+
To run one:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
cd samples/flask
|
|
18
|
+
uv run flask --app app run --port 3600
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
> The samples need a real server SDK key to resolve configs. Without one the client stays
|
|
22
|
+
> unready and every config falls back to the default the sample passes in, which is also what
|
|
23
|
+
> a production app sees when it cannot reach ConfigDirector.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Flask sample
|
|
2
|
+
|
|
3
|
+
A minimal [Flask](https://flask.palletsprojects.com/) app using the ConfigDirector Python
|
|
4
|
+
server SDK. It mirrors the `openfeature-server` sample in the JavaScript SDKs: a single
|
|
5
|
+
`/configs` endpoint that evaluates a handful of configs and returns them as JSON.
|
|
6
|
+
|
|
7
|
+
## Running it
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
cd samples/flask
|
|
11
|
+
cp .env.example .env # optional — the sample runs without a real key
|
|
12
|
+
uv run flask --app app run --port 3600
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
curl 'http://localhost:3600/configs?id=user-123&plan=pro'
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"day-of-the-week-config": "Friday",
|
|
24
|
+
"integer-config": 10,
|
|
25
|
+
"json-value-config": {},
|
|
26
|
+
"permanent-kill-switch": false,
|
|
27
|
+
"temporary-feature-flag": true
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Query parameters double as the evaluation context — `id`, `name`, and `anonymous` map to the
|
|
32
|
+
matching `Context` fields, and anything else becomes a trait:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
/configs?id=user-123&name=Ada&plan=pro®ion=eu
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Run the smoke tests with `uv run pytest`.
|
|
39
|
+
|
|
40
|
+
## The client is a singleton
|
|
41
|
+
|
|
42
|
+
This is the single most important thing the sample shows, so it lives in its own module:
|
|
43
|
+
[`configdirector_client.py`](configdirector_client.py). Create one client when the server
|
|
44
|
+
starts, share it for the whole lifetime of the process, and close it on shutdown.
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
# configdirector_client.py — runs exactly once per process
|
|
48
|
+
client = create_client(os.environ["CONFIGDIRECTOR_SERVER_KEY"], ...)
|
|
49
|
+
client.initialize()
|
|
50
|
+
atexit.register(client.close)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
# app.py — every request handler shares that one instance
|
|
55
|
+
from configdirector_client import client
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Importing the module is what creates it: Python caches modules in `sys.modules`, so the code
|
|
59
|
+
runs once no matter how many places import `client`. Never call `create_client()` inside a
|
|
60
|
+
request handler — each client opens its own connection, blocks on `initialize()`, starts
|
|
61
|
+
out not-ready (so it serves defaults), and drops its batched telemetry when it is discarded.
|
|
62
|
+
|
|
63
|
+
Concurrency is not a reason to make more of them: the client is thread-safe, so every worker
|
|
64
|
+
thread shares this one safely. Process-based servers (Gunicorn workers, or `flask run --debug`'s
|
|
65
|
+
reloader) get one client per process, which is correct — a client cannot be shared across
|
|
66
|
+
processes.
|
|
67
|
+
|
|
68
|
+
Evaluation itself is cheap. `get_value()` reads config state the client already holds in memory,
|
|
69
|
+
with no network call on the request path, which is what makes it safe to call several times per
|
|
70
|
+
request.
|
|
71
|
+
|
|
72
|
+
## What else it demonstrates
|
|
73
|
+
|
|
74
|
+
**Initialization is explicit and non-fatal.** `initialize()` blocks until the initial config
|
|
75
|
+
state arrives or the timeout elapses, and never raises on connection failure. The sample checks
|
|
76
|
+
`is_ready`, logs a warning, and carries on serving defaults.
|
|
77
|
+
|
|
78
|
+
**Defaults are the fallback.** Every `get_value()` call passes the value to serve when
|
|
79
|
+
ConfigDirector is unreachable, so it should be the safe choice. Its type also decides how the
|
|
80
|
+
config value is parsed.
|
|
81
|
+
|
|
82
|
+
**Context is per-request; the client is not.** `context_from_request()` maps query parameters
|
|
83
|
+
onto a `Context`; a real app would build this from the authenticated session.
|
|
84
|
+
|
|
85
|
+
**Logging is yours to configure.** The sample passes its own logger to the client, so SDK output
|
|
86
|
+
lands in the application's logging namespace rather than the SDK's:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
sdk_logger = logging.getLogger("flask_sample.configdirector")
|
|
90
|
+
sdk_logger.setLevel(os.environ.get("CONFIGDIRECTOR_LOG_LEVEL", "INFO"))
|
|
91
|
+
|
|
92
|
+
client = create_client(..., logger=sdk_logger)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
flask_sample.configdirector DEBUG No config state found for 'integer-config', returning default value 10
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Any object with `debug`/`info`/`warning`/`error` methods works, and a stdlib `Logger` satisfies
|
|
100
|
+
that. Omit `logger=` entirely and the SDK falls back to the standard library logger named
|
|
101
|
+
`configdirector`, leaving the level to your application — or pass `log_level=` if you would
|
|
102
|
+
rather not configure the `logging` module at all. Set `CONFIGDIRECTOR_LOG_LEVEL=DEBUG` to watch
|
|
103
|
+
every evaluation as it happens.
|
|
104
|
+
|
|
105
|
+
**Shutdown is clean.** `atexit` closes the client, dropping connections and flushing pending
|
|
106
|
+
telemetry. A production deployment would also hook its server's worker-exit signal.
|
|
107
|
+
|
|
108
|
+
The SDK also supports watching configs for changes and subscribing to client events; see the
|
|
109
|
+
[SDK README](../../README.md) for `watch()` and `on()`.
|
|
110
|
+
|
|
111
|
+
## Running without a server SDK key
|
|
112
|
+
|
|
113
|
+
Without a valid key the client stays unready and every config falls back to the default this
|
|
114
|
+
app passes in. That is the same path a production app takes when it cannot reach ConfigDirector,
|
|
115
|
+
so it is worth seeing: the app keeps serving, on the defaults you chose.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""ConfigDirector Python server SDK.
|
|
2
|
+
|
|
3
|
+
ConfigDirector is a remote configuration and feature flag service.
|
|
4
|
+
|
|
5
|
+
Example::
|
|
6
|
+
|
|
7
|
+
from configdirector import Context, Metadata, create_client
|
|
8
|
+
|
|
9
|
+
client = create_client(
|
|
10
|
+
"YOUR-SERVER-SDK-KEY",
|
|
11
|
+
metadata=Metadata(app_name="my-awesome-app", app_version="1.0.0"),
|
|
12
|
+
)
|
|
13
|
+
client.initialize()
|
|
14
|
+
|
|
15
|
+
if client.get_value("new-checkout", False, Context(id="user-123")):
|
|
16
|
+
...
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from ._version import __version__
|
|
20
|
+
from .client import create_client
|
|
21
|
+
from .errors import (
|
|
22
|
+
ConfigDirectorConnectionError,
|
|
23
|
+
ConfigDirectorError,
|
|
24
|
+
ConfigDirectorInitializationError,
|
|
25
|
+
ConfigDirectorTypeError,
|
|
26
|
+
ConfigDirectorValidationError,
|
|
27
|
+
)
|
|
28
|
+
from .types import (
|
|
29
|
+
ClientEvent,
|
|
30
|
+
ClientHooks,
|
|
31
|
+
ClientReadyEvent,
|
|
32
|
+
ClientReadyHandler,
|
|
33
|
+
ConfigDirectorClient,
|
|
34
|
+
ConfigDirectorLogger,
|
|
35
|
+
ConfigEvaluatedEvent,
|
|
36
|
+
ConfigEvaluatedHandler,
|
|
37
|
+
ConfigEvaluation,
|
|
38
|
+
ConfigState,
|
|
39
|
+
ConfigsUpdatedEvent,
|
|
40
|
+
ConfigsUpdatedHandler,
|
|
41
|
+
ConfigType,
|
|
42
|
+
ConfigValue,
|
|
43
|
+
ConnectionMode,
|
|
44
|
+
ConnectionOptions,
|
|
45
|
+
Context,
|
|
46
|
+
EvaluationReason,
|
|
47
|
+
Metadata,
|
|
48
|
+
Subscription,
|
|
49
|
+
TelemetryOptions,
|
|
50
|
+
WatchHandler,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
"ClientEvent",
|
|
55
|
+
"ClientHooks",
|
|
56
|
+
"ClientReadyEvent",
|
|
57
|
+
"ClientReadyHandler",
|
|
58
|
+
"ConfigDirectorClient",
|
|
59
|
+
"ConfigDirectorConnectionError",
|
|
60
|
+
"ConfigDirectorError",
|
|
61
|
+
"ConfigDirectorInitializationError",
|
|
62
|
+
"ConfigDirectorLogger",
|
|
63
|
+
"ConfigDirectorTypeError",
|
|
64
|
+
"ConfigDirectorValidationError",
|
|
65
|
+
"ConfigEvaluatedEvent",
|
|
66
|
+
"ConfigEvaluatedHandler",
|
|
67
|
+
"ConfigEvaluation",
|
|
68
|
+
"ConfigState",
|
|
69
|
+
"ConfigType",
|
|
70
|
+
"ConfigValue",
|
|
71
|
+
"ConfigsUpdatedEvent",
|
|
72
|
+
"ConfigsUpdatedHandler",
|
|
73
|
+
"ConnectionMode",
|
|
74
|
+
"ConnectionOptions",
|
|
75
|
+
"Context",
|
|
76
|
+
"EvaluationReason",
|
|
77
|
+
"Metadata",
|
|
78
|
+
"Subscription",
|
|
79
|
+
"TelemetryOptions",
|
|
80
|
+
"WatchHandler",
|
|
81
|
+
"__version__",
|
|
82
|
+
"create_client",
|
|
83
|
+
]
|