without-configmap 0.0.1__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.
@@ -0,0 +1,61 @@
1
+ Metadata-Version: 2.4
2
+ Name: without-configmap
3
+ Version: 0.0.1
4
+ Summary: A without behavior source backed by a Kubernetes ConfigMap mount, reloaded with watchfiles.
5
+ Author: Josh Karpel
6
+ Author-email: Josh Karpel <josh.karpel@gmail.com>
7
+ License-Expression: MIT
8
+ Classifier: Development Status :: 2 - Pre-Alpha
9
+ Classifier: Framework :: AsyncIO
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Topic :: Software Development :: Libraries
16
+ Classifier: Typing :: Typed
17
+ Requires-Dist: without-core==0.0.1
18
+ Requires-Dist: watchfiles>=0.21
19
+ Requires-Dist: pydantic>=2
20
+ Requires-Dist: pyyaml>=6
21
+ Requires-Python: >=3.14
22
+ Description-Content-Type: text/markdown
23
+
24
+ # without-configmap
25
+
26
+ A `without` behavior source backed by a Kubernetes ConfigMap mount: the first
27
+ context that actually *changes*, proving the context-updated-by-a-stream half of
28
+ the model end to end. Where [`without-env`](../without-env) loads a static value
29
+ once, this one re-parses on every mount change.
30
+
31
+ `watch_config(mount, parse)` is a `Stream` that yields the parsed config now and a
32
+ freshly parsed value every time the mount changes. It watches the mount
33
+ *directory*, not the file, so it catches the atomic `..data` symlink swap that
34
+ [projected ConfigMaps](https://kubernetes.io/docs/concepts/configuration/configmap/#mounted-configmaps-are-updated-automatically)
35
+ use. Feed the stream through `without.sample` to read the latest value as a
36
+ `Context`:
37
+
38
+ ```python
39
+ from pathlib import Path
40
+
41
+ from pydantic import BaseModel
42
+ from without import sample
43
+ from without_configmap import read_yaml_file, watch_config
44
+
45
+
46
+ class MyConfig(BaseModel):
47
+ model_config = {"frozen": True}
48
+ default_mode: str
49
+ max_bytes: int
50
+
51
+
52
+ source = watch_config(Path("/etc/config"), read_yaml_file(MyConfig, "config.yaml"))
53
+ async with sample(source) as config:
54
+ config.current() # always the latest reloaded value, never blocks
55
+ await config.updated() # block until the next reload lands, then return it
56
+ ```
57
+
58
+ See the
59
+ [`without-configmap` guide](https://without.help/guides/without-configmap/)
60
+ (with the [API reference](https://without.help/reference/without_configmap/))
61
+ for the full surface.
@@ -0,0 +1,38 @@
1
+ # without-configmap
2
+
3
+ A `without` behavior source backed by a Kubernetes ConfigMap mount: the first
4
+ context that actually *changes*, proving the context-updated-by-a-stream half of
5
+ the model end to end. Where [`without-env`](../without-env) loads a static value
6
+ once, this one re-parses on every mount change.
7
+
8
+ `watch_config(mount, parse)` is a `Stream` that yields the parsed config now and a
9
+ freshly parsed value every time the mount changes. It watches the mount
10
+ *directory*, not the file, so it catches the atomic `..data` symlink swap that
11
+ [projected ConfigMaps](https://kubernetes.io/docs/concepts/configuration/configmap/#mounted-configmaps-are-updated-automatically)
12
+ use. Feed the stream through `without.sample` to read the latest value as a
13
+ `Context`:
14
+
15
+ ```python
16
+ from pathlib import Path
17
+
18
+ from pydantic import BaseModel
19
+ from without import sample
20
+ from without_configmap import read_yaml_file, watch_config
21
+
22
+
23
+ class MyConfig(BaseModel):
24
+ model_config = {"frozen": True}
25
+ default_mode: str
26
+ max_bytes: int
27
+
28
+
29
+ source = watch_config(Path("/etc/config"), read_yaml_file(MyConfig, "config.yaml"))
30
+ async with sample(source) as config:
31
+ config.current() # always the latest reloaded value, never blocks
32
+ await config.updated() # block until the next reload lands, then return it
33
+ ```
34
+
35
+ See the
36
+ [`without-configmap` guide](https://without.help/guides/without-configmap/)
37
+ (with the [API reference](https://without.help/reference/without_configmap/))
38
+ for the full surface.
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["uv_build>=0.11.25,<0.12"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "without-configmap"
7
+ version = "0.0.1"
8
+ description = "A without behavior source backed by a Kubernetes ConfigMap mount, reloaded with watchfiles."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ authors = [
12
+ { name = "Josh Karpel", email = "josh.karpel@gmail.com" },
13
+ ]
14
+ requires-python = ">=3.14"
15
+ classifiers = [
16
+ "Development Status :: 2 - Pre-Alpha",
17
+ "Framework :: AsyncIO",
18
+ "Intended Audience :: Developers",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3 :: Only",
22
+ "Programming Language :: Python :: 3.14",
23
+ "Topic :: Software Development :: Libraries",
24
+ "Typing :: Typed",
25
+ ]
26
+ dependencies = [
27
+ "without-core==0.0.1",
28
+ "watchfiles>=0.21",
29
+ "pydantic>=2",
30
+ "pyyaml>=6",
31
+ ]
32
+
33
+ [tool.uv.sources]
34
+ without-core = { workspace = true }
@@ -0,0 +1,7 @@
1
+ from without_configmap.parse import read_yaml_file
2
+ from without_configmap.source import watch_config
3
+
4
+ __all__ = [
5
+ "read_yaml_file",
6
+ "watch_config",
7
+ ]
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from pathlib import Path
5
+
6
+ import yaml
7
+ from pydantic import BaseModel
8
+
9
+
10
+ def read_yaml_file[ModelT: BaseModel](model_type: type[ModelT], file_name: str) -> Callable[[Path], ModelT]:
11
+ """
12
+ Parse a single YAML file from the mount into a validated model.
13
+
14
+ The ConfigMap is mounted with one file (e.g. `config.yaml`) holding a YAML
15
+ mapping. This reads `mount / file_name`, loads it, and validates it into
16
+ `model_type` at the boundary, so missing keys fall back to declared model
17
+ defaults and the rest of the system sees an already-valid value.
18
+ """
19
+
20
+ def parse(mount: Path) -> ModelT:
21
+ contents = yaml.safe_load((mount / file_name).read_text())
22
+ return model_type.model_validate(contents)
23
+
24
+ return parse
File without changes
@@ -0,0 +1,44 @@
1
+ # A behavior source backed by a Kubernetes ConfigMap mount. Watches the mount
2
+ # directory (not a file) because a projected ConfigMap swaps an atomic ..data
3
+ # symlink rather than rewriting files in place, so a per-file watch can miss
4
+ # updates. On each change the whole mount is reparsed to its desired state
5
+ # (declarative) instead of applying deltas. Feed the result to without.sample.
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import AsyncIterator
10
+ from collections.abc import Callable
11
+ from pathlib import Path
12
+
13
+ from watchfiles import awatch
14
+ from without.contracts import Stream
15
+
16
+ type Changes = Callable[[Path], AsyncIterator[object]]
17
+
18
+
19
+ async def _awatch_changes(mount: Path) -> AsyncIterator[object]:
20
+ async for batch in awatch(mount): # pragma: no cover - default I/O adapter; tests drive the `changes` seam
21
+ yield batch
22
+
23
+
24
+ def watch_config[T](
25
+ mount: Path,
26
+ parse: Callable[[Path], T],
27
+ *,
28
+ changes: Changes = _awatch_changes,
29
+ ) -> Stream[T]:
30
+ """
31
+ The parsed config now, and a freshly parsed value on every mount change.
32
+
33
+ `parse` is the boundary: it turns the mount directory into a validated
34
+ value. `changes` is the source of reload signals, injectable so tests can
35
+ drive reloads deterministically without real filesystem events; it defaults
36
+ to watching the mount directory with `watchfiles`.
37
+ """
38
+
39
+ async def source() -> AsyncIterator[T]:
40
+ yield parse(mount)
41
+ async for _change in changes(mount):
42
+ yield parse(mount)
43
+
44
+ return source()