composat 1.0.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 (42) hide show
  1. composat-1.0.0/LICENSE +21 -0
  2. composat-1.0.0/PKG-INFO +191 -0
  3. composat-1.0.0/README.md +167 -0
  4. composat-1.0.0/pyproject.toml +126 -0
  5. composat-1.0.0/src/composat/__init__.py +70 -0
  6. composat-1.0.0/src/composat/assembly.py +1048 -0
  7. composat-1.0.0/src/composat/component.py +514 -0
  8. composat-1.0.0/src/composat/context.py +147 -0
  9. composat-1.0.0/src/composat/dependency.py +169 -0
  10. composat-1.0.0/src/composat/errors.py +130 -0
  11. composat-1.0.0/src/composat/identity.py +75 -0
  12. composat-1.0.0/src/composat/loader.py +364 -0
  13. composat-1.0.0/src/composat/manager.py +135 -0
  14. composat-1.0.0/src/composat/py.typed +0 -0
  15. composat-1.0.0/src/composat/registry.py +294 -0
  16. composat-1.0.0/src/composat/scope.py +221 -0
  17. composat-1.0.0/src/composat/spec.py +28 -0
  18. composat-1.0.0/src/composat/state.py +318 -0
  19. composat-1.0.0/src/composat/std/__init__.py +5 -0
  20. composat-1.0.0/src/composat/std/drain.py +76 -0
  21. composat-1.0.0/src/composat/std/pluggable.py +621 -0
  22. composat-1.0.0/src/composat/std/root.py +136 -0
  23. composat-1.0.0/src/composat/std/wait.py +61 -0
  24. composat-1.0.0/tests/__init__.py +1 -0
  25. composat-1.0.0/tests/test_assembly.py +1475 -0
  26. composat-1.0.0/tests/test_assembly_properties.py +274 -0
  27. composat-1.0.0/tests/test_component.py +291 -0
  28. composat-1.0.0/tests/test_context.py +335 -0
  29. composat-1.0.0/tests/test_dependency.py +175 -0
  30. composat-1.0.0/tests/test_drain.py +137 -0
  31. composat-1.0.0/tests/test_final.py +70 -0
  32. composat-1.0.0/tests/test_identity.py +70 -0
  33. composat-1.0.0/tests/test_loader.py +326 -0
  34. composat-1.0.0/tests/test_manager.py +253 -0
  35. composat-1.0.0/tests/test_pluggable.py +507 -0
  36. composat-1.0.0/tests/test_readme.py +35 -0
  37. composat-1.0.0/tests/test_registry.py +376 -0
  38. composat-1.0.0/tests/test_root.py +397 -0
  39. composat-1.0.0/tests/test_scope.py +276 -0
  40. composat-1.0.0/tests/test_state.py +271 -0
  41. composat-1.0.0/tests/test_state_properties.py +102 -0
  42. composat-1.0.0/tests/test_wait.py +264 -0
composat-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 HivertMoZara
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
10
+ furnished 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,191 @@
1
+ Metadata-Version: 2.4
2
+ Name: composat
3
+ Version: 1.0.0
4
+ Summary: Component lifespan interface
5
+ Keywords: asyncio,component,dependency-injection,lifecycle,plugin,reload
6
+ Author-Email: HivertMoZara <worldmozara@163.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Framework :: AsyncIO
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Typing :: Typed
18
+ Project-URL: Homepage, https://github.com/NCBM/composat
19
+ Project-URL: Repository, https://github.com/NCBM/composat
20
+ Project-URL: Issues, https://github.com/NCBM/composat/issues
21
+ Requires-Python: >=3.12
22
+ Requires-Dist: typing-extensions>=4.16.0
23
+ Description-Content-Type: text/markdown
24
+
25
+ # composat
26
+
27
+ A component lifespan interface. Components declare what they need, an application declares what
28
+ exists, and an assembly brings instances up and takes them down: it fills a table in dependency order,
29
+ reports what happened, and keeps a key answering through a swap.
30
+
31
+ Requires Python 3.12+.
32
+
33
+ ## A component
34
+
35
+ ```python
36
+ from composat import Component, require
37
+
38
+
39
+ class Store(Component): ...
40
+
41
+
42
+ class Engine(Component):
43
+ store: Store = require()
44
+ ```
45
+
46
+ A field marked with `require()` is a requirement, and its annotation says what the field reads as.
47
+ Nothing is inferred from the annotation's shape, so an ordinary field stays ordinary. Reading the
48
+ field answers the instance that provides it, at the reader's own key — it is a live read, not a copy
49
+ taken at construction. The annotation may name a class defined further down the module: the
50
+ requirement is settled the first time anything asks the class what it requires.
51
+
52
+ The framework drives six hooks — `load`, `unload`, `reload`, `enable`, `disable`, `drain` — and a type
53
+ that writes none of them is one nothing drives. A hook is written with `@override`, which is also what
54
+ turns a misspelled hook into a checker error instead of a hook that silently never runs.
55
+
56
+ ## Declaring and running
57
+
58
+ ```python
59
+ from composat import Assembly, ComponentIdentity, ComponentKey, ComponentRegistry, Declaration
60
+
61
+ registry = ComponentRegistry()
62
+ _ = registry.declare_all((Declaration(Store), Declaration(Engine)))
63
+ assembly = Assembly(registry, up={})
64
+
65
+ key = ComponentKey(ComponentIdentity.of(Engine))
66
+ result = await assembly.start(key) # brings Store up too: what a requirement needs is the closure
67
+ assert result.ok
68
+ await assembly.stop(*result.completed) # what this batch built, not what it named
69
+ ```
70
+
71
+ Neither batch verb raises for what a component did: both report it, so one tenant's broken
72
+ configuration stays that tenant's problem. `strict=True` is the other reading of the same batch — what
73
+ that call brought up is taken back down, and the original failure goes on to the caller.
74
+ `assembly.replace(key, Declaration(NewEngine))` puts a new generation behind a key without a gap in
75
+ what the key answers. A reader that arrives in such a window meets a generation that is built and not
76
+ yet serving: `composat.std.wait.serving(assembly, key)` waits it out, and refuses a key that rests
77
+ below serving with no move taking it there instead of spending its deadline on it.
78
+
79
+ The verbs are one vocabulary:
80
+
81
+ | verb | what it is for |
82
+ |---|---|
83
+ | `start(*keys, enable=True, strict=False)` | the keys named and what they require, built — serving unless told otherwise |
84
+ | `stop(*keys)` | the keys named, taken down; a key that is not up is passed over, so `stop(*assembly.keys())` is the teardown a host runs unconditionally |
85
+ | `migrate(*keys, to=level)` | the keys named, moved to a level an assembly holds: `LOADED`, `DRAINING` or `ENABLED` |
86
+ | `enable(*keys)` | that move, to serving |
87
+ | `quiesce(*keys)` | that move, out of service and still built: what `enable` puts back |
88
+ | `admit(*declarations, enable=True)` | a declaration and the batch that brings it up, as one call |
89
+ | `evict(*keys)` | a key taken down and its declaration withdrawn: the other end of `admit` |
90
+ | `replace(key, declaration)` | a new generation behind a key that is serving; the instance, or a `Replacement` when part of it did not go through |
91
+
92
+ What a key is *between* levels is readable as well: `Assembly.in_transit(key)` answers what a move is
93
+ doing to it and where that move ends, which is how a reader tells a swap worth waiting out from a
94
+ teardown worth failing on. `docs/best-practices/` is what to do with that when a request arrives in the
95
+ middle of one.
96
+
97
+ A key is an identity plus a qualifier: `ComponentIdentity` is the class's own path
98
+ (`module.qualname`), and the qualifier is whatever tells instances of it apart — a tenant, a row id,
99
+ or nothing at all. `Declaration(Store, instance="tenant-a")` is how a deployment puts an instance at
100
+ one, and `args`/`kwargs` are passed to the component's constructor as they are.
101
+
102
+ ## Loading a directory
103
+
104
+ `load_units(root)` answers one `Unit` per module: the module, the component classes its entry namespace
105
+ holds, and a hash of its source. Declaring what those hold is the application's, and so is deciding
106
+ which of them are roots:
107
+
108
+ ```python
109
+ from composat import Declaration, load_units
110
+
111
+ for unit in load_units(root):
112
+ _ = registry.declare_all(Declaration(cls) for cls in unit.components)
113
+
114
+ result = await assembly.start(*roots) # the rest come up as requirements of these
115
+ ```
116
+
117
+ Which keys are roots is a deployment decision: a manifest can be grown in stages, and "nothing needs
118
+ it" then does not mean "it should be up". Tolerating one broken module is also the caller's —
119
+ `load_unit` raises, so the policy is written in a loop with a `try` in it.
120
+
121
+ ## Editing a running module
122
+
123
+ `composat.std.pluggable.UnitLoader` holds the modules one root loaded and answers which of them no
124
+ longer match their source. It is a component, declared like any other:
125
+
126
+ ```python
127
+ from composat import Declaration
128
+ from composat.std.pluggable import UnitLoader
129
+
130
+ _ = registry.declare_all((Declaration(UnitLoader, args=(root,)),))
131
+ ```
132
+
133
+ Two verbs, because moving code and moving instances are two decisions:
134
+
135
+ ```python
136
+ for name in loader.stale():
137
+ _ = loader.reload_module(name) # a new generation of the module, under the same name
138
+ _ = await loader.swap_instances(assembly, name) # every key built from the old one, on the new
139
+ ```
140
+
141
+ A reload replaces the classes, not the module's globals: what has to survive a generation belongs
142
+ outside the module and is restored in `load`. A unit whose class the edit renamed has no counterpart
143
+ to swap to, and is reported.
144
+
145
+ ## What this is not
146
+
147
+ These are the boundaries a reader needs before choosing. The refusals that cost a design argument are
148
+ recorded in `docs/adr/`, each with the alternatives it turned down.
149
+
150
+ - **A discovery mechanism.** The framework loads the paths it is handed; it does not go looking for
151
+ components in entry points, in a directory or in configuration.
152
+ - **A version axis.** An identity is `module.qualname`; which version a deployment runs is decided
153
+ where the classes are declared.
154
+ - **A file watcher.** `composat.std.pluggable` answers what a module's source says now and how to move
155
+ a generation; the loop, the debounce and the ignore rules are the application's.
156
+ - **An event dispatcher.** Priorities, rules, permissions and session continuation are a loop the
157
+ application writes — including a plugin registering its matchers into a shared dispatcher.
158
+ - **A configuration system.** The framework does not read the application's self-description (author,
159
+ description, version, a schema), and has no settings of its own.
160
+ - **A runtime.** It does not run a loop, a scheduler or a server; it is driven from the one the
161
+ application already has, and timed work beyond a one-shot `EffectScope.after` is the application's.
162
+ - **Reactive or conditional requirements.** A requirement is declared and unconditional; there is no
163
+ activation on demand, no requirement that applies to some callers only, and no lexical isolation
164
+ between components.
165
+ - **Surgery on a host application.** It does not reach into another framework's cache or registry to
166
+ take a component over: a component is declared to this assembly, not captured from somewhere else.
167
+ - **An introspection tool.** A batch answers with a report, a move with an event, and the table with
168
+ what is up now — "why is this key not here" is a record the application keeps itself.
169
+ - **Effect bookkeeping beyond a scope.** A scope holds what was registered on it and answers the names
170
+ of those effects; there are no effect labels, no hierarchy and no global ledger.
171
+ - **A swap across identities.** `replace` moves one key to a new class at the same identity; replacing
172
+ a tree of keys in one step is not offered.
173
+
174
+ ## The gate
175
+
176
+ The checks a change has to pass are the ones in
177
+ [`.github/workflows/checks.yml`](https://github.com/NCBM/composat/blob/main/.github/workflows/checks.yml);
178
+ `pdm install -G dev` at the root installs what they need.
179
+
180
+ ## Documentation
181
+
182
+ | file | what it holds |
183
+ |---|---|
184
+ | `docs/architecture.md` | how the layers fit, the direction of imports, and what holds across modules |
185
+ | `docs/adr/` | the decisions behind those shapes, numbered in the order they were made, with the alternatives each refused |
186
+ | `docs/best-practices/` | what to do with the shapes: the verbs a host calls and their order, how a request reads through a swap, what a replaced component registers, and how a directory of code is reloaded |
187
+ | `examples/README.md` | one row per shape an application takes, each a working directory |
188
+ | `CHANGELOG.md` | what changed in each release |
189
+
190
+ There is no separate API reference: a public symbol's docstring is the reference, and the pages above
191
+ point into the code.
@@ -0,0 +1,167 @@
1
+ # composat
2
+
3
+ A component lifespan interface. Components declare what they need, an application declares what
4
+ exists, and an assembly brings instances up and takes them down: it fills a table in dependency order,
5
+ reports what happened, and keeps a key answering through a swap.
6
+
7
+ Requires Python 3.12+.
8
+
9
+ ## A component
10
+
11
+ ```python
12
+ from composat import Component, require
13
+
14
+
15
+ class Store(Component): ...
16
+
17
+
18
+ class Engine(Component):
19
+ store: Store = require()
20
+ ```
21
+
22
+ A field marked with `require()` is a requirement, and its annotation says what the field reads as.
23
+ Nothing is inferred from the annotation's shape, so an ordinary field stays ordinary. Reading the
24
+ field answers the instance that provides it, at the reader's own key — it is a live read, not a copy
25
+ taken at construction. The annotation may name a class defined further down the module: the
26
+ requirement is settled the first time anything asks the class what it requires.
27
+
28
+ The framework drives six hooks — `load`, `unload`, `reload`, `enable`, `disable`, `drain` — and a type
29
+ that writes none of them is one nothing drives. A hook is written with `@override`, which is also what
30
+ turns a misspelled hook into a checker error instead of a hook that silently never runs.
31
+
32
+ ## Declaring and running
33
+
34
+ ```python
35
+ from composat import Assembly, ComponentIdentity, ComponentKey, ComponentRegistry, Declaration
36
+
37
+ registry = ComponentRegistry()
38
+ _ = registry.declare_all((Declaration(Store), Declaration(Engine)))
39
+ assembly = Assembly(registry, up={})
40
+
41
+ key = ComponentKey(ComponentIdentity.of(Engine))
42
+ result = await assembly.start(key) # brings Store up too: what a requirement needs is the closure
43
+ assert result.ok
44
+ await assembly.stop(*result.completed) # what this batch built, not what it named
45
+ ```
46
+
47
+ Neither batch verb raises for what a component did: both report it, so one tenant's broken
48
+ configuration stays that tenant's problem. `strict=True` is the other reading of the same batch — what
49
+ that call brought up is taken back down, and the original failure goes on to the caller.
50
+ `assembly.replace(key, Declaration(NewEngine))` puts a new generation behind a key without a gap in
51
+ what the key answers. A reader that arrives in such a window meets a generation that is built and not
52
+ yet serving: `composat.std.wait.serving(assembly, key)` waits it out, and refuses a key that rests
53
+ below serving with no move taking it there instead of spending its deadline on it.
54
+
55
+ The verbs are one vocabulary:
56
+
57
+ | verb | what it is for |
58
+ |---|---|
59
+ | `start(*keys, enable=True, strict=False)` | the keys named and what they require, built — serving unless told otherwise |
60
+ | `stop(*keys)` | the keys named, taken down; a key that is not up is passed over, so `stop(*assembly.keys())` is the teardown a host runs unconditionally |
61
+ | `migrate(*keys, to=level)` | the keys named, moved to a level an assembly holds: `LOADED`, `DRAINING` or `ENABLED` |
62
+ | `enable(*keys)` | that move, to serving |
63
+ | `quiesce(*keys)` | that move, out of service and still built: what `enable` puts back |
64
+ | `admit(*declarations, enable=True)` | a declaration and the batch that brings it up, as one call |
65
+ | `evict(*keys)` | a key taken down and its declaration withdrawn: the other end of `admit` |
66
+ | `replace(key, declaration)` | a new generation behind a key that is serving; the instance, or a `Replacement` when part of it did not go through |
67
+
68
+ What a key is *between* levels is readable as well: `Assembly.in_transit(key)` answers what a move is
69
+ doing to it and where that move ends, which is how a reader tells a swap worth waiting out from a
70
+ teardown worth failing on. `docs/best-practices/` is what to do with that when a request arrives in the
71
+ middle of one.
72
+
73
+ A key is an identity plus a qualifier: `ComponentIdentity` is the class's own path
74
+ (`module.qualname`), and the qualifier is whatever tells instances of it apart — a tenant, a row id,
75
+ or nothing at all. `Declaration(Store, instance="tenant-a")` is how a deployment puts an instance at
76
+ one, and `args`/`kwargs` are passed to the component's constructor as they are.
77
+
78
+ ## Loading a directory
79
+
80
+ `load_units(root)` answers one `Unit` per module: the module, the component classes its entry namespace
81
+ holds, and a hash of its source. Declaring what those hold is the application's, and so is deciding
82
+ which of them are roots:
83
+
84
+ ```python
85
+ from composat import Declaration, load_units
86
+
87
+ for unit in load_units(root):
88
+ _ = registry.declare_all(Declaration(cls) for cls in unit.components)
89
+
90
+ result = await assembly.start(*roots) # the rest come up as requirements of these
91
+ ```
92
+
93
+ Which keys are roots is a deployment decision: a manifest can be grown in stages, and "nothing needs
94
+ it" then does not mean "it should be up". Tolerating one broken module is also the caller's —
95
+ `load_unit` raises, so the policy is written in a loop with a `try` in it.
96
+
97
+ ## Editing a running module
98
+
99
+ `composat.std.pluggable.UnitLoader` holds the modules one root loaded and answers which of them no
100
+ longer match their source. It is a component, declared like any other:
101
+
102
+ ```python
103
+ from composat import Declaration
104
+ from composat.std.pluggable import UnitLoader
105
+
106
+ _ = registry.declare_all((Declaration(UnitLoader, args=(root,)),))
107
+ ```
108
+
109
+ Two verbs, because moving code and moving instances are two decisions:
110
+
111
+ ```python
112
+ for name in loader.stale():
113
+ _ = loader.reload_module(name) # a new generation of the module, under the same name
114
+ _ = await loader.swap_instances(assembly, name) # every key built from the old one, on the new
115
+ ```
116
+
117
+ A reload replaces the classes, not the module's globals: what has to survive a generation belongs
118
+ outside the module and is restored in `load`. A unit whose class the edit renamed has no counterpart
119
+ to swap to, and is reported.
120
+
121
+ ## What this is not
122
+
123
+ These are the boundaries a reader needs before choosing. The refusals that cost a design argument are
124
+ recorded in `docs/adr/`, each with the alternatives it turned down.
125
+
126
+ - **A discovery mechanism.** The framework loads the paths it is handed; it does not go looking for
127
+ components in entry points, in a directory or in configuration.
128
+ - **A version axis.** An identity is `module.qualname`; which version a deployment runs is decided
129
+ where the classes are declared.
130
+ - **A file watcher.** `composat.std.pluggable` answers what a module's source says now and how to move
131
+ a generation; the loop, the debounce and the ignore rules are the application's.
132
+ - **An event dispatcher.** Priorities, rules, permissions and session continuation are a loop the
133
+ application writes — including a plugin registering its matchers into a shared dispatcher.
134
+ - **A configuration system.** The framework does not read the application's self-description (author,
135
+ description, version, a schema), and has no settings of its own.
136
+ - **A runtime.** It does not run a loop, a scheduler or a server; it is driven from the one the
137
+ application already has, and timed work beyond a one-shot `EffectScope.after` is the application's.
138
+ - **Reactive or conditional requirements.** A requirement is declared and unconditional; there is no
139
+ activation on demand, no requirement that applies to some callers only, and no lexical isolation
140
+ between components.
141
+ - **Surgery on a host application.** It does not reach into another framework's cache or registry to
142
+ take a component over: a component is declared to this assembly, not captured from somewhere else.
143
+ - **An introspection tool.** A batch answers with a report, a move with an event, and the table with
144
+ what is up now — "why is this key not here" is a record the application keeps itself.
145
+ - **Effect bookkeeping beyond a scope.** A scope holds what was registered on it and answers the names
146
+ of those effects; there are no effect labels, no hierarchy and no global ledger.
147
+ - **A swap across identities.** `replace` moves one key to a new class at the same identity; replacing
148
+ a tree of keys in one step is not offered.
149
+
150
+ ## The gate
151
+
152
+ The checks a change has to pass are the ones in
153
+ [`.github/workflows/checks.yml`](https://github.com/NCBM/composat/blob/main/.github/workflows/checks.yml);
154
+ `pdm install -G dev` at the root installs what they need.
155
+
156
+ ## Documentation
157
+
158
+ | file | what it holds |
159
+ |---|---|
160
+ | `docs/architecture.md` | how the layers fit, the direction of imports, and what holds across modules |
161
+ | `docs/adr/` | the decisions behind those shapes, numbered in the order they were made, with the alternatives each refused |
162
+ | `docs/best-practices/` | what to do with the shapes: the verbs a host calls and their order, how a request reads through a swap, what a replaced component registers, and how a directory of code is reloaded |
163
+ | `examples/README.md` | one row per shape an application takes, each a working directory |
164
+ | `CHANGELOG.md` | what changed in each release |
165
+
166
+ There is no separate API reference: a public symbol's docstring is the reference, and the pages above
167
+ point into the code.
@@ -0,0 +1,126 @@
1
+ [project]
2
+ name = "composat"
3
+ version = "1.0.0"
4
+ description = "Component lifespan interface"
5
+ keywords = [
6
+ "asyncio",
7
+ "component",
8
+ "dependency-injection",
9
+ "lifecycle",
10
+ "plugin",
11
+ "reload",
12
+ ]
13
+ authors = [
14
+ { name = "HivertMoZara", email = "worldmozara@163.com" },
15
+ ]
16
+ dependencies = [
17
+ "typing-extensions>=4.16.0",
18
+ ]
19
+ requires-python = ">=3.12"
20
+ readme = "README.md"
21
+ license = "MIT"
22
+ license-files = [
23
+ "LICENSE",
24
+ ]
25
+ classifiers = [
26
+ "Development Status :: 5 - Production/Stable",
27
+ "Framework :: AsyncIO",
28
+ "Intended Audience :: Developers",
29
+ "Operating System :: OS Independent",
30
+ "Programming Language :: Python :: 3 :: Only",
31
+ "Programming Language :: Python :: 3.12",
32
+ "Programming Language :: Python :: 3.13",
33
+ "Programming Language :: Python :: 3.14",
34
+ "Typing :: Typed",
35
+ ]
36
+
37
+ [project.urls]
38
+ Homepage = "https://github.com/NCBM/composat"
39
+ Repository = "https://github.com/NCBM/composat"
40
+ Issues = "https://github.com/NCBM/composat/issues"
41
+
42
+ [dependency-groups]
43
+ dev = [
44
+ "basedpyright>=1.40.2",
45
+ "hypothesis>=6.170.14",
46
+ "pytest>=9.1.1",
47
+ "ruff>=0.17.0",
48
+ ]
49
+
50
+ [build-system]
51
+ requires = [
52
+ "pdm-backend",
53
+ ]
54
+ build-backend = "pdm.backend"
55
+
56
+ [tool.basedpyright]
57
+ typeCheckingMode = "recommended"
58
+ failOnWarnings = true
59
+ pythonVersion = "3.12"
60
+
61
+ [tool.pdm]
62
+ distribution = true
63
+
64
+ [tool.pytest.ini_options]
65
+ testpaths = [
66
+ "tests",
67
+ "examples",
68
+ ]
69
+ pythonpath = [
70
+ "src",
71
+ ]
72
+ addopts = "--strict-markers"
73
+ filterwarnings = [
74
+ "error::RuntimeWarning",
75
+ ]
76
+
77
+ [tool.ruff]
78
+ src = [
79
+ "src",
80
+ "tests",
81
+ "examples",
82
+ ]
83
+ target-version = "py312"
84
+ line-length = 120
85
+
86
+ [tool.ruff.lint]
87
+ select = [
88
+ "E",
89
+ "W",
90
+ "F",
91
+ "I",
92
+ "UP",
93
+ "B",
94
+ "C4",
95
+ "SIM",
96
+ "RUF",
97
+ "PERF",
98
+ "PIE",
99
+ "RET",
100
+ "N",
101
+ "PTH",
102
+ "TID",
103
+ "FURB",
104
+ "FLY",
105
+ "ISC",
106
+ "T20",
107
+ "DTZ",
108
+ "LOG",
109
+ "G",
110
+ "BLE",
111
+ "PT",
112
+ "S",
113
+ "ANN",
114
+ ]
115
+
116
+ [tool.ruff.lint.per-file-ignores]
117
+ "tests/**" = [
118
+ "S101",
119
+ "PT011",
120
+ "PT018",
121
+ ]
122
+ "examples/**" = [
123
+ "S101",
124
+ "PT011",
125
+ "PT018",
126
+ ]
@@ -0,0 +1,70 @@
1
+ """composat: a component lifespan interface.
2
+
3
+ An assembly brings instances up and takes them down in dependency order, and the modules here are the
4
+ layers it stands on — identities and keys, effect scopes, the call level that says whose work a read
5
+ belongs to and where its effects end, the component base and what it declares, the state machine and
6
+ the manager that drives it, the dependency graph, the registry of declarations, and the assembly: the
7
+ table of what is up, the rule a requirement is answered by, the batch verbs that fill the table and
8
+ empty it, the report they hand back, and the stream of moves they report while they run.
9
+
10
+ ``composat.std`` adds what a deployment needs on top of that: the loader that turns a path into the
11
+ module there and the components it exports, the component that owns moving a loaded module onto a new
12
+ generation, the wait for a swap to land, and the drain of an instance's work.
13
+ """
14
+
15
+ from . import context as context
16
+ from .assembly import Assembly as Assembly
17
+ from .assembly import AssemblyResult as AssemblyResult
18
+ from .assembly import Candidates as Candidates
19
+ from .assembly import Claim as Claim
20
+ from .assembly import Event as Event
21
+ from .assembly import Policy as Policy
22
+ from .assembly import Replacement as Replacement
23
+ from .assembly import answering_from as answering_from
24
+ from .assembly import default_rule as default_rule
25
+ from .component import LIFECYCLE_HOOKS as LIFECYCLE_HOOKS
26
+ from .component import Component as Component
27
+ from .component import RequirementResolver as RequirementResolver
28
+ from .component import attach as attach
29
+ from .component import capabilities_of as capabilities_of
30
+ from .component import reopen_epoch as reopen_epoch
31
+ from .component import require as require
32
+ from .component import requirements_of as requirements_of
33
+ from .context import CallContext as CallContext
34
+ from .context import current as current
35
+ from .context import current_or_none as current_or_none
36
+ from .context import open_call as open_call
37
+ from .context import put as put
38
+ from .context import resolve as resolve
39
+ from .dependency import DependencyCycleError as DependencyCycleError
40
+ from .dependency import DependencyError as DependencyError
41
+ from .dependency import DependencyGraph as DependencyGraph
42
+ from .dependency import DependencyInUseError as DependencyInUseError
43
+ from .errors import KeyInTransitError as KeyInTransitError
44
+ from .errors import LifecycleReentryError as LifecycleReentryError
45
+ from .errors import NoCallContextError as NoCallContextError
46
+ from .errors import ServiceNotUpError as ServiceNotUpError
47
+ from .errors import UnboundRequirementError as UnboundRequirementError
48
+ from .errors import UnboundServiceError as UnboundServiceError
49
+ from .identity import ComponentIdentity as ComponentIdentity
50
+ from .identity import ComponentKey as ComponentKey
51
+ from .loader import LoadedUnit as LoadedUnit
52
+ from .loader import Unit as Unit
53
+ from .loader import components_of as components_of
54
+ from .loader import fingerprint_of as fingerprint_of
55
+ from .loader import load_unit as load_unit
56
+ from .loader import load_units as load_units
57
+ from .loader import loaded_unit as loaded_unit
58
+ from .manager import ComponentManager as ComponentManager
59
+ from .registry import ComponentRegistry as ComponentRegistry
60
+ from .registry import Declaration as Declaration
61
+ from .scope import EffectScope as EffectScope
62
+ from .scope import Reclaimable as Reclaimable
63
+ from .scope import name_of as name_of
64
+ from .spec import RequirementSpec as RequirementSpec
65
+ from .state import Committed as Committed
66
+ from .state import ComponentState as ComponentState
67
+ from .state import ComponentStateControl as ComponentStateControl
68
+ from .state import ComponentStateError as ComponentStateError
69
+ from .state import Migration as Migration
70
+ from .state import Operation as Operation