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.
- composat-1.0.0/LICENSE +21 -0
- composat-1.0.0/PKG-INFO +191 -0
- composat-1.0.0/README.md +167 -0
- composat-1.0.0/pyproject.toml +126 -0
- composat-1.0.0/src/composat/__init__.py +70 -0
- composat-1.0.0/src/composat/assembly.py +1048 -0
- composat-1.0.0/src/composat/component.py +514 -0
- composat-1.0.0/src/composat/context.py +147 -0
- composat-1.0.0/src/composat/dependency.py +169 -0
- composat-1.0.0/src/composat/errors.py +130 -0
- composat-1.0.0/src/composat/identity.py +75 -0
- composat-1.0.0/src/composat/loader.py +364 -0
- composat-1.0.0/src/composat/manager.py +135 -0
- composat-1.0.0/src/composat/py.typed +0 -0
- composat-1.0.0/src/composat/registry.py +294 -0
- composat-1.0.0/src/composat/scope.py +221 -0
- composat-1.0.0/src/composat/spec.py +28 -0
- composat-1.0.0/src/composat/state.py +318 -0
- composat-1.0.0/src/composat/std/__init__.py +5 -0
- composat-1.0.0/src/composat/std/drain.py +76 -0
- composat-1.0.0/src/composat/std/pluggable.py +621 -0
- composat-1.0.0/src/composat/std/root.py +136 -0
- composat-1.0.0/src/composat/std/wait.py +61 -0
- composat-1.0.0/tests/__init__.py +1 -0
- composat-1.0.0/tests/test_assembly.py +1475 -0
- composat-1.0.0/tests/test_assembly_properties.py +274 -0
- composat-1.0.0/tests/test_component.py +291 -0
- composat-1.0.0/tests/test_context.py +335 -0
- composat-1.0.0/tests/test_dependency.py +175 -0
- composat-1.0.0/tests/test_drain.py +137 -0
- composat-1.0.0/tests/test_final.py +70 -0
- composat-1.0.0/tests/test_identity.py +70 -0
- composat-1.0.0/tests/test_loader.py +326 -0
- composat-1.0.0/tests/test_manager.py +253 -0
- composat-1.0.0/tests/test_pluggable.py +507 -0
- composat-1.0.0/tests/test_readme.py +35 -0
- composat-1.0.0/tests/test_registry.py +376 -0
- composat-1.0.0/tests/test_root.py +397 -0
- composat-1.0.0/tests/test_scope.py +276 -0
- composat-1.0.0/tests/test_state.py +271 -0
- composat-1.0.0/tests/test_state_properties.py +102 -0
- 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.
|
composat-1.0.0/PKG-INFO
ADDED
|
@@ -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.
|
composat-1.0.0/README.md
ADDED
|
@@ -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
|