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.
Files changed (48) hide show
  1. configdirector_server_sdk-0.2.0/.gitignore +30 -0
  2. configdirector_server_sdk-0.2.0/LICENSE +21 -0
  3. configdirector_server_sdk-0.2.0/PKG-INFO +49 -0
  4. configdirector_server_sdk-0.2.0/README.md +23 -0
  5. configdirector_server_sdk-0.2.0/profiling/README.md +126 -0
  6. configdirector_server_sdk-0.2.0/pyproject.toml +118 -0
  7. configdirector_server_sdk-0.2.0/samples/README.md +23 -0
  8. configdirector_server_sdk-0.2.0/samples/flask/README.md +115 -0
  9. configdirector_server_sdk-0.2.0/src/configdirector/__init__.py +83 -0
  10. configdirector_server_sdk-0.2.0/src/configdirector/_bundle.py +176 -0
  11. configdirector_server_sdk-0.2.0/src/configdirector/_evaluation/__init__.py +47 -0
  12. configdirector_server_sdk-0.2.0/src/configdirector/_evaluation/_json_pointer.py +40 -0
  13. configdirector_server_sdk-0.2.0/src/configdirector/_evaluation/_json_value.py +28 -0
  14. configdirector_server_sdk-0.2.0/src/configdirector/_evaluation/_rapidhash.py +115 -0
  15. configdirector_server_sdk-0.2.0/src/configdirector/_evaluation/array_comparison.py +25 -0
  16. configdirector_server_sdk-0.2.0/src/configdirector/_evaluation/condition_evaluator.py +94 -0
  17. configdirector_server_sdk-0.2.0/src/configdirector/_evaluation/config_evaluator.py +123 -0
  18. configdirector_server_sdk-0.2.0/src/configdirector/_evaluation/date_comparison.py +77 -0
  19. configdirector_server_sdk-0.2.0/src/configdirector/_evaluation/numeric_comparison.py +61 -0
  20. configdirector_server_sdk-0.2.0/src/configdirector/_evaluation/percent_hashing.py +12 -0
  21. configdirector_server_sdk-0.2.0/src/configdirector/_evaluation/semver_comparison.py +63 -0
  22. configdirector_server_sdk-0.2.0/src/configdirector/_evaluation/text_comparison.py +40 -0
  23. configdirector_server_sdk-0.2.0/src/configdirector/_evaluation/types.py +134 -0
  24. configdirector_server_sdk-0.2.0/src/configdirector/_eventsource/__init__.py +27 -0
  25. configdirector_server_sdk-0.2.0/src/configdirector/_eventsource/client.py +350 -0
  26. configdirector_server_sdk-0.2.0/src/configdirector/_eventsource/errors.py +31 -0
  27. configdirector_server_sdk-0.2.0/src/configdirector/_eventsource/parser.py +175 -0
  28. configdirector_server_sdk-0.2.0/src/configdirector/_eventsource/transport.py +162 -0
  29. configdirector_server_sdk-0.2.0/src/configdirector/_eventsource/types.py +58 -0
  30. configdirector_server_sdk-0.2.0/src/configdirector/_http.py +86 -0
  31. configdirector_server_sdk-0.2.0/src/configdirector/_logger.py +12 -0
  32. configdirector_server_sdk-0.2.0/src/configdirector/_telemetry/__init__.py +49 -0
  33. configdirector_server_sdk-0.2.0/src/configdirector/_telemetry/collector.py +188 -0
  34. configdirector_server_sdk-0.2.0/src/configdirector/_telemetry/compact_json.py +40 -0
  35. configdirector_server_sdk-0.2.0/src/configdirector/_telemetry/events.py +156 -0
  36. configdirector_server_sdk-0.2.0/src/configdirector/_telemetry/queue.py +126 -0
  37. configdirector_server_sdk-0.2.0/src/configdirector/_telemetry/reporter.py +157 -0
  38. configdirector_server_sdk-0.2.0/src/configdirector/_telemetry/value_id.py +30 -0
  39. configdirector_server_sdk-0.2.0/src/configdirector/_transport/__init__.py +23 -0
  40. configdirector_server_sdk-0.2.0/src/configdirector/_transport/base.py +84 -0
  41. configdirector_server_sdk-0.2.0/src/configdirector/_transport/polling.py +159 -0
  42. configdirector_server_sdk-0.2.0/src/configdirector/_transport/streaming.py +110 -0
  43. configdirector_server_sdk-0.2.0/src/configdirector/_value_parser.py +94 -0
  44. configdirector_server_sdk-0.2.0/src/configdirector/_version.py +3 -0
  45. configdirector_server_sdk-0.2.0/src/configdirector/client.py +640 -0
  46. configdirector_server_sdk-0.2.0/src/configdirector/errors.py +45 -0
  47. configdirector_server_sdk-0.2.0/src/configdirector/py.typed +0 -0
  48. 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&region=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
+ ]