facetkit 0.3.0__tar.gz → 0.4.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.
- {facetkit-0.3.0 → facetkit-0.4.0}/CHANGELOG.md +19 -0
- {facetkit-0.3.0 → facetkit-0.4.0}/PKG-INFO +8 -8
- {facetkit-0.3.0 → facetkit-0.4.0}/README.md +7 -7
- facetkit-0.4.0/docs/component.md +124 -0
- {facetkit-0.3.0 → facetkit-0.4.0}/docs/container.md +74 -74
- facetkit-0.4.0/docs/examples/composed_app.py +57 -0
- {facetkit-0.3.0 → facetkit-0.4.0}/docs/facet.md +73 -73
- {facetkit-0.3.0 → facetkit-0.4.0}/facetkit/__init__.py +3 -3
- facetkit-0.4.0/facetkit/container.py +120 -0
- facetkit-0.4.0/facetkit/exceptions.py +52 -0
- {facetkit-0.3.0 → facetkit-0.4.0}/facetkit/facets/cli.py +6 -6
- {facetkit-0.3.0 → facetkit-0.4.0}/facetkit/facets/gui.py +6 -6
- {facetkit-0.3.0 → facetkit-0.4.0}/facetkit/facets/service.py +11 -11
- {facetkit-0.3.0 → facetkit-0.4.0}/facetkit/facets/tui.py +16 -16
- facetkit-0.4.0/facetkit/facets/web.py +45 -0
- {facetkit-0.3.0 → facetkit-0.4.0}/facetkit/types.py +15 -17
- {facetkit-0.3.0 → facetkit-0.4.0}/tests/conftest.py +2 -2
- {facetkit-0.3.0 → facetkit-0.4.0}/tests/test_container.py +62 -62
- facetkit-0.4.0/tests/test_container_facets.py +207 -0
- {facetkit-0.3.0 → facetkit-0.4.0}/tests/test_facet.py +15 -14
- facetkit-0.3.0/docs/component.md +0 -124
- facetkit-0.3.0/docs/examples/composed_app.py +0 -57
- facetkit-0.3.0/facetkit/container.py +0 -119
- facetkit-0.3.0/facetkit/exceptions.py +0 -52
- facetkit-0.3.0/facetkit/facets/web.py +0 -45
- facetkit-0.3.0/tests/test_container_facets.py +0 -207
- {facetkit-0.3.0 → facetkit-0.4.0}/.gitignore +0 -0
- {facetkit-0.3.0 → facetkit-0.4.0}/LICENSE +0 -0
- {facetkit-0.3.0 → facetkit-0.4.0}/facetkit/facets/__init__.py +0 -0
- {facetkit-0.3.0 → facetkit-0.4.0}/facetkit/py.typed +0 -0
- {facetkit-0.3.0 → facetkit-0.4.0}/pyproject.toml +0 -0
- {facetkit-0.3.0 → facetkit-0.4.0}/requirements.txt +0 -0
|
@@ -5,6 +5,25 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.4.0] - 2026-06-21
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- `mount_facet` was renamed to `bind_facet`
|
|
13
|
+
- `unmount_facet` was renamed to `unbind_facet`
|
|
14
|
+
- `add_component` was renamed to `bind_component`
|
|
15
|
+
- `remove_component` was renamed to `unbind_component`
|
|
16
|
+
- `attach` was renamed to `on_bind`
|
|
17
|
+
- `detach` was renamed to `on_unbind`
|
|
18
|
+
- Function signature for facet functions were normalized to use *_id params
|
|
19
|
+
- Container bind/unbind methods now take `facet_id` and `component_id`
|
|
20
|
+
- `DependentComponentsError` was renamed to `ComponentInUseError`
|
|
21
|
+
- Exception attributes normalized to `facet_id` / `component_id`
|
|
22
|
+
- GUI descriptor and method `parent` renamed to `parent_id`
|
|
23
|
+
- Descriptor identity fields normalized to `id`
|
|
24
|
+
|
|
25
|
+
[0.4.0]: https://github.com/Dev-DanielR/py_facetkit/releases/tag/v0.4.0
|
|
26
|
+
|
|
8
27
|
## [0.3.0] - 2026-06-20
|
|
9
28
|
|
|
10
29
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: facetkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: A composable dependency container with passive UI and service facets.
|
|
5
5
|
Author-email: "Daniel R. Vásquez Montes" <dev.DanielR@gmail.composable>
|
|
6
6
|
License: MIT
|
|
@@ -32,9 +32,9 @@ facetkit separates three pieces:
|
|
|
32
32
|
|
|
33
33
|
| Piece | Role |
|
|
34
34
|
|-------|------|
|
|
35
|
-
| [Container](docs/container.md) | Application root — config, facet
|
|
35
|
+
| [Container](docs/container.md) | Application root — config, facet & component binding |
|
|
36
36
|
| [Facet](docs/facet.md) | Passive registry for a surface area (commands, routes, widgets, …) |
|
|
37
|
-
| [Component](docs/component.md) | Plugin that
|
|
37
|
+
| [Component](docs/component.md) | Plugin that connects to one or several facets in on_bind and cleans up in on_unbind |
|
|
38
38
|
|
|
39
39
|
```
|
|
40
40
|
Container
|
|
@@ -74,8 +74,8 @@ from facetkit import Container, CliFacet, WebFacet
|
|
|
74
74
|
|
|
75
75
|
app = Container({"app": {"name": "demo"}})
|
|
76
76
|
|
|
77
|
-
app.
|
|
78
|
-
app.
|
|
77
|
+
app.bind_facet("cli", CliFacet())
|
|
78
|
+
app.bind_facet("web", WebFacet())
|
|
79
79
|
|
|
80
80
|
def hello():
|
|
81
81
|
"""Say hello."""
|
|
@@ -90,8 +90,8 @@ Register directly on facets (as above) or through [components](docs/component.md
|
|
|
90
90
|
## Documentation
|
|
91
91
|
|
|
92
92
|
- [Container](docs/container.md) — config, lifecycle, introspection
|
|
93
|
-
- [Facets](docs/facet.md) — facet types, registries,
|
|
94
|
-
- [Components](docs/component.md) —
|
|
93
|
+
- [Facets](docs/facet.md) — facet types, registries, binding
|
|
94
|
+
- [Components](docs/component.md) — on_bind/on_unbind, dependencies
|
|
95
95
|
- [Examples](docs/examples/composed_app.py) — composed app with logger and status components
|
|
96
96
|
|
|
97
97
|
## Development
|
|
@@ -101,7 +101,7 @@ pip install -e ".[dev]"
|
|
|
101
101
|
pytest
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
See [CHANGELOG.md](CHANGELOG.md) for release notes. Pre-1.0 (`0.
|
|
104
|
+
See [CHANGELOG.md](CHANGELOG.md) for release notes. Pre-1.0 (`0.4.0`) — public APIs may change between minor releases.
|
|
105
105
|
|
|
106
106
|
## License
|
|
107
107
|
|
|
@@ -6,9 +6,9 @@ facetkit separates three pieces:
|
|
|
6
6
|
|
|
7
7
|
| Piece | Role |
|
|
8
8
|
|-------|------|
|
|
9
|
-
| [Container](docs/container.md) | Application root — config, facet
|
|
9
|
+
| [Container](docs/container.md) | Application root — config, facet & component binding |
|
|
10
10
|
| [Facet](docs/facet.md) | Passive registry for a surface area (commands, routes, widgets, …) |
|
|
11
|
-
| [Component](docs/component.md) | Plugin that
|
|
11
|
+
| [Component](docs/component.md) | Plugin that connects to one or several facets in on_bind and cleans up in on_unbind |
|
|
12
12
|
|
|
13
13
|
```
|
|
14
14
|
Container
|
|
@@ -48,8 +48,8 @@ from facetkit import Container, CliFacet, WebFacet
|
|
|
48
48
|
|
|
49
49
|
app = Container({"app": {"name": "demo"}})
|
|
50
50
|
|
|
51
|
-
app.
|
|
52
|
-
app.
|
|
51
|
+
app.bind_facet("cli", CliFacet())
|
|
52
|
+
app.bind_facet("web", WebFacet())
|
|
53
53
|
|
|
54
54
|
def hello():
|
|
55
55
|
"""Say hello."""
|
|
@@ -64,8 +64,8 @@ Register directly on facets (as above) or through [components](docs/component.md
|
|
|
64
64
|
## Documentation
|
|
65
65
|
|
|
66
66
|
- [Container](docs/container.md) — config, lifecycle, introspection
|
|
67
|
-
- [Facets](docs/facet.md) — facet types, registries,
|
|
68
|
-
- [Components](docs/component.md) —
|
|
67
|
+
- [Facets](docs/facet.md) — facet types, registries, binding
|
|
68
|
+
- [Components](docs/component.md) — on_bind/on_unbind, dependencies
|
|
69
69
|
- [Examples](docs/examples/composed_app.py) — composed app with logger and status components
|
|
70
70
|
|
|
71
71
|
## Development
|
|
@@ -75,7 +75,7 @@ pip install -e ".[dev]"
|
|
|
75
75
|
pytest
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
-
See [CHANGELOG.md](CHANGELOG.md) for release notes. Pre-1.0 (`0.
|
|
78
|
+
See [CHANGELOG.md](CHANGELOG.md) for release notes. Pre-1.0 (`0.4.0`) — public APIs may change between minor releases.
|
|
79
79
|
|
|
80
80
|
## License
|
|
81
81
|
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Components
|
|
2
|
+
|
|
3
|
+
A **Component** is an active plugin that connects to one or several [facets](facet.md) in `on_bind` and cleans up in `on_unbind`. This keeps feature setup and teardown in one place instead of scattered across your application.
|
|
4
|
+
|
|
5
|
+
## Types and exceptions
|
|
6
|
+
|
|
7
|
+
**Protocol**
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
class MyComponent:
|
|
11
|
+
def on_bind(self, container): ...
|
|
12
|
+
def on_unbind(self, container): ...
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Optional class attributes — see [Declared dependencies](#declared-dependencies).
|
|
16
|
+
|
|
17
|
+
**Exceptions**
|
|
18
|
+
|
|
19
|
+
| Exception | When | Attributes |
|
|
20
|
+
|-----------|------|------------|
|
|
21
|
+
| `DuplicateComponentError` | `bind_component(overwrite=False)` | `.component_id` |
|
|
22
|
+
| `MissingComponentDependencyError` | `bind_component` — required component absent | `.component_id`, `.missing` |
|
|
23
|
+
| `MissingFacetDependencyError` | `bind_component` — required facet not bound | `.component_id`, `.missing` |
|
|
24
|
+
| `ComponentInUseError` | `unbind_component` — another component depends on it | `.component_id`, `.dependents` |
|
|
25
|
+
|
|
26
|
+
`FacetInUseError` (raised on `unbind_facet`) is documented in [Facets](facet.md).
|
|
27
|
+
|
|
28
|
+
## Lifecycle
|
|
29
|
+
|
|
30
|
+
Components are bound and unbound through the container:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from facetkit import Container, CliFacet, ServiceFacet
|
|
34
|
+
|
|
35
|
+
class StatusComponent:
|
|
36
|
+
required_facets = ("cli", "service")
|
|
37
|
+
|
|
38
|
+
def on_bind(self, container):
|
|
39
|
+
container.facets["cli"].add_command("status", self.show_status)
|
|
40
|
+
container.facets["service"].add_provider("status", {"healthy": True})
|
|
41
|
+
|
|
42
|
+
def on_unbind(self, container):
|
|
43
|
+
container.facets["cli"].remove_command("status")
|
|
44
|
+
container.facets["service"].remove_provider("status")
|
|
45
|
+
|
|
46
|
+
def show_status(self):
|
|
47
|
+
"""Show application status."""
|
|
48
|
+
return "ok"
|
|
49
|
+
|
|
50
|
+
app = Container({"app": {"name": "demo"}})
|
|
51
|
+
app.bind_facet("cli", CliFacet())
|
|
52
|
+
app.bind_facet("service", ServiceFacet())
|
|
53
|
+
app.bind_component("status", StatusComponent())
|
|
54
|
+
app.unbind_component("status")
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**`bind_component(component_id, comp, *, overwrite=True)`** — validates declared dependencies, calls `on_bind(container)`, then stores the instance. When `overwrite=True` (the default), if a component has already been bound with the same id it is unbound without a dependent check before replacing it with the new component. Pass `overwrite=False` to raise `DuplicateComponentError` instead:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
app.bind_component("logger", Logger(), overwrite=False)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
If validation fails, `on_bind` is not called.
|
|
64
|
+
|
|
65
|
+
**`unbind_component(component_id)`** — calls `on_unbind(container)`, then removes the instance. Raises `ComponentInUseError` if any other bound component lists `component_id` in `required_components`.
|
|
66
|
+
|
|
67
|
+
Inside `on_bind` / `on_unbind`, components typically:
|
|
68
|
+
|
|
69
|
+
- Register or unregister entries on `container.facets[...]`
|
|
70
|
+
- Read shared state from `container.config`
|
|
71
|
+
- Reach peer plugins via `container.components["component_id"]`
|
|
72
|
+
|
|
73
|
+
See [examples/composed_app.py](examples/composed_app.py) for a full walkthrough with peer dependencies and CLI dispatch.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
python docs/examples/composed_app.py
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Declared dependencies
|
|
80
|
+
|
|
81
|
+
Components can declare what must already be present before they bind. Both attributes are optional class-level tuples:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
class ApiComponent:
|
|
85
|
+
required_components = ("logger", "database") # peer components
|
|
86
|
+
required_facets = ("cli", "web") # bound facet ids
|
|
87
|
+
|
|
88
|
+
def on_bind(self, container):
|
|
89
|
+
logger = container.components["logger"]
|
|
90
|
+
container.facets["cli"].add_command("api-status", self.status)
|
|
91
|
+
container.facets["web"].add_route("api-status", "/status", self.status)
|
|
92
|
+
|
|
93
|
+
def on_unbind(self, container):
|
|
94
|
+
container.facets["cli"].remove_command("api-status")
|
|
95
|
+
container.facets["web"].remove_route("api-status")
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The container enforces these at lifecycle boundaries:
|
|
99
|
+
|
|
100
|
+
| Action | Validation |
|
|
101
|
+
|--------|------------|
|
|
102
|
+
| `bind_component` | Every component_id in `required_components` must already be bound; every facet_id in `required_facets` must already be bound. `on_bind` is not called if anything is missing. |
|
|
103
|
+
| `unbind_component` | Blocked if any other bound component lists this component_id in `required_components`. |
|
|
104
|
+
| `unbind_facet` | Blocked if any bound component lists this facet_id in `required_facets`. |
|
|
105
|
+
| Replace component | Allowed — dependent checks are skipped so the slot stays filled. |
|
|
106
|
+
|
|
107
|
+
Register dependencies before dependents:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
app.bind_component("logger", LoggerComponent())
|
|
111
|
+
app.bind_component("database", DatabaseComponent())
|
|
112
|
+
app.bind_component("api", ApiComponent()) # OK
|
|
113
|
+
|
|
114
|
+
app.bind_component("api", ApiComponent()) # raises MissingComponentDependencyError
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Tear down in reverse:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
app.unbind_component("api") # OK
|
|
121
|
+
app.unbind_component("logger") # OK once api is gone
|
|
122
|
+
|
|
123
|
+
app.unbind_facet("cli") # raises FacetInUseError while a component still requires it
|
|
124
|
+
```
|
|
@@ -1,75 +1,75 @@
|
|
|
1
|
-
# Container
|
|
2
|
-
|
|
3
|
-
The **Container** is the application root. It holds shared configuration, a map of [
|
|
4
|
-
|
|
5
|
-
```
|
|
6
|
-
Container
|
|
7
|
-
├── config → shared application settings
|
|
8
|
-
├── components → named plugins (
|
|
9
|
-
└── facets → passive registries, keyed by
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
Create one at startup and pass it through your app:
|
|
13
|
-
|
|
14
|
-
```python
|
|
15
|
-
from facetkit import Container
|
|
16
|
-
|
|
17
|
-
app = Container({"app": {"name": "demo"}, "port": 8080})
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
## Config
|
|
21
|
-
|
|
22
|
-
The constructor takes a plain dict. Components and application code read it through `
|
|
23
|
-
|
|
24
|
-
```python
|
|
25
|
-
app.config["port"] # 8080
|
|
26
|
-
app.get("config.app.name") # "demo"
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## Lifecycle
|
|
30
|
-
|
|
31
|
-
The container exposes lifecycle methods for [facets](facet.md) (`
|
|
32
|
-
|
|
33
|
-
## Boot order
|
|
34
|
-
|
|
35
|
-
Typical startup sequence:
|
|
36
|
-
|
|
37
|
-
1. Create a `Container` with config
|
|
38
|
-
2. `
|
|
39
|
-
3. `
|
|
40
|
-
|
|
41
|
-
Your framework layer (argparse, FastAPI, Textual, Qt, etc.) then reads the facet registries and dispatches.
|
|
42
|
-
|
|
43
|
-
## Introspection with `get()`
|
|
44
|
-
|
|
45
|
-
`Container.get(path)` uses [glom](https://glom.readthedocs.io/) to read nested state:
|
|
46
|
-
|
|
47
|
-
```python
|
|
48
|
-
app.get("") # the container itself
|
|
49
|
-
app.get("config.app.name") # config values
|
|
50
|
-
app.get("facets") # all
|
|
51
|
-
app.get("facets.cli") # CliFacet instance
|
|
52
|
-
app.get("facets.cli.commands") # command registry
|
|
53
|
-
app.get("facets.web.routes.users") # a single route entry
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
Without `default`, resolution errors propagate (missing paths, glom failures, etc.):
|
|
57
|
-
|
|
58
|
-
```python
|
|
59
|
-
app.get("facets.cli.commands") # works when the cli facet is
|
|
60
|
-
app.get("facets.missing") # raises glom.PathAccessError
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
Pass `default` for optional lookups — any resolution error returns that value:
|
|
64
|
-
|
|
65
|
-
```python
|
|
66
|
-
app.get("facets.missing", default=None) # None
|
|
67
|
-
app.get("facets.missing", default={}) # {}
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
Direct dict access also works when you know a facet is
|
|
71
|
-
|
|
72
|
-
```python
|
|
73
|
-
app.facets["cli"].commands["hello"]
|
|
74
|
-
app.components["logger"]
|
|
1
|
+
# Container
|
|
2
|
+
|
|
3
|
+
The **Container** is the application root. It holds shared configuration, a map of [bound facets](facet.md), and a registry of [bound components](component.md).
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
Container
|
|
7
|
+
├── config → shared application settings
|
|
8
|
+
├── components → named plugins (on_bind / on_unbind lifecycle)
|
|
9
|
+
└── facets → passive registries, keyed by facet id
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Create one at startup and pass it through your app:
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from facetkit import Container
|
|
16
|
+
|
|
17
|
+
app = Container({"app": {"name": "demo"}, "port": 8080})
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Config
|
|
21
|
+
|
|
22
|
+
The constructor takes a plain dict. Components and application code read it through `container.config` or `container.get("config....")`.
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
app.config["port"] # 8080
|
|
26
|
+
app.get("config.app.name") # "demo"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Lifecycle
|
|
30
|
+
|
|
31
|
+
The container exposes lifecycle methods for [facets](facet.md) (`bind_facet`, `unbind_facet`) and [components](component.md) (`bind_component`, `unbind_component`). Both bind methods accept an `overwrite` flag (default `True`) to control duplicate-id behavior.
|
|
32
|
+
|
|
33
|
+
## Boot order
|
|
34
|
+
|
|
35
|
+
Typical startup sequence:
|
|
36
|
+
|
|
37
|
+
1. Create a `Container` with config
|
|
38
|
+
2. `bind_facet` for each surface your app uses
|
|
39
|
+
3. `bind_component` for each feature plugin — bind dependencies before dependents
|
|
40
|
+
|
|
41
|
+
Your framework layer (argparse, FastAPI, Textual, Qt, etc.) then reads the facet registries and dispatches.
|
|
42
|
+
|
|
43
|
+
## Introspection with `get()`
|
|
44
|
+
|
|
45
|
+
`Container.get(path)` uses [glom](https://glom.readthedocs.io/) to read nested state:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
app.get("") # the container itself
|
|
49
|
+
app.get("config.app.name") # config values
|
|
50
|
+
app.get("facets") # all bound facets
|
|
51
|
+
app.get("facets.cli") # CliFacet instance
|
|
52
|
+
app.get("facets.cli.commands") # command registry
|
|
53
|
+
app.get("facets.web.routes.users") # a single route entry
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Without `default`, resolution errors propagate (missing paths, glom failures, etc.):
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
app.get("facets.cli.commands") # works when the cli facet is bound
|
|
60
|
+
app.get("facets.missing") # raises glom.PathAccessError
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Pass `default` for optional lookups — any resolution error returns that value:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
app.get("facets.missing", default=None) # None
|
|
67
|
+
app.get("facets.missing", default={}) # {}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Direct dict access also works when you know a facet is bound:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
app.facets["cli"].commands["hello"]
|
|
74
|
+
app.components["logger"]
|
|
75
75
|
```
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Compose a container with logger and status components.
|
|
2
|
+
|
|
3
|
+
Demonstrates component dependencies, facet registration, and dispatch
|
|
4
|
+
over the CLI command registry.
|
|
5
|
+
|
|
6
|
+
Run from the project root after installing facetkit:
|
|
7
|
+
|
|
8
|
+
python docs/examples/composed_app.py
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from facetkit import Container, CliFacet, ServiceFacet
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LoggerComponent:
|
|
15
|
+
def on_bind(self, container):
|
|
16
|
+
container.facets["service"].add_provider("logger", self)
|
|
17
|
+
|
|
18
|
+
def on_unbind(self, container):
|
|
19
|
+
container.facets["service"].remove_provider("logger")
|
|
20
|
+
|
|
21
|
+
def info(self, msg):
|
|
22
|
+
print(msg)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class StatusComponent:
|
|
26
|
+
required_components = ("logger",)
|
|
27
|
+
required_facets = ("cli", "service")
|
|
28
|
+
|
|
29
|
+
def on_bind(self, container):
|
|
30
|
+
self._logger = container.components["logger"]
|
|
31
|
+
container.facets["cli"].add_command("status", self.show_status)
|
|
32
|
+
container.facets["service"].add_provider("status", {"healthy": True})
|
|
33
|
+
|
|
34
|
+
def on_unbind(self, container):
|
|
35
|
+
container.facets["cli"].remove_command("status")
|
|
36
|
+
container.facets["service"].remove_provider("status")
|
|
37
|
+
|
|
38
|
+
def show_status(self):
|
|
39
|
+
"""Show application status."""
|
|
40
|
+
self._logger.info("status check")
|
|
41
|
+
return "ok"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def main():
|
|
45
|
+
app = Container({"app": {"name": "demo"}})
|
|
46
|
+
app.bind_facet("cli", CliFacet())
|
|
47
|
+
app.bind_facet("service", ServiceFacet())
|
|
48
|
+
app.bind_component("logger", LoggerComponent())
|
|
49
|
+
app.bind_component("status", StatusComponent())
|
|
50
|
+
|
|
51
|
+
print("Registered CLI commands:")
|
|
52
|
+
for command_id, cmd in app.facets["cli"].commands.items():
|
|
53
|
+
print(f" {command_id}: {cmd.description}")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
main()
|
|
@@ -1,74 +1,74 @@
|
|
|
1
|
-
# Facets
|
|
2
|
-
|
|
3
|
-
A **Facet** is a passive registry for an application surface — CLI commands, web routes, GUI widgets, background tasks, and so on. Facets collect descriptors; your dispatch layer reads them and runs the app.
|
|
4
|
-
|
|
5
|
-
## Types and exceptions
|
|
6
|
-
|
|
7
|
-
**Protocol**
|
|
8
|
-
|
|
9
|
-
- `Facet` — `name: str`, `clear()`
|
|
10
|
-
|
|
11
|
-
**Implementations**
|
|
12
|
-
|
|
13
|
-
| Facet | Registries | Purpose |
|
|
14
|
-
|-------|------------|---------|
|
|
15
|
-
| `CliFacet` | `commands` | Named CLI commands. Description is taken from the handler's docstring |
|
|
16
|
-
| `TuiFacet` | `screens`, `keybindings`, `current_screen` | Terminal UI descriptors |
|
|
17
|
-
| `GuiFacet` | `widgets`, `menus`, `toolbars`, `layouts` | Desktop UI descriptors |
|
|
18
|
-
| `WebFacet` | `routes`, `middleware`, `error_handlers` | HTTP/API descriptors |
|
|
19
|
-
| `ServiceFacet` | `tasks`, `providers` | Background work and shared providers |
|
|
20
|
-
|
|
21
|
-
**Descriptor types** — `Command`, `ScreenDescriptor`, `KeybindingDescriptor`, `WidgetDescriptor`, `MenuDescriptor`, `ToolbarDescriptor`, `LayoutDescriptor`, `RouteDescriptor`, `MiddlewareDescriptor`, `ErrorHandlerDescriptor`, `TaskDescriptor`
|
|
22
|
-
|
|
23
|
-
**Exceptions**
|
|
24
|
-
|
|
25
|
-
| Exception | When | Attributes |
|
|
26
|
-
|-----------|------|------------|
|
|
27
|
-
| `DuplicateFacetError` | `
|
|
28
|
-
| `FacetInUseError` | `
|
|
29
|
-
|
|
30
|
-
## Lifecycle
|
|
31
|
-
|
|
32
|
-
Facets are
|
|
33
|
-
|
|
34
|
-
```python
|
|
35
|
-
from facetkit import Container, CliFacet, WebFacet
|
|
36
|
-
|
|
37
|
-
app = Container({})
|
|
38
|
-
app.
|
|
39
|
-
app.
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
**`
|
|
43
|
-
|
|
44
|
-
```python
|
|
45
|
-
app.
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
**`
|
|
49
|
-
|
|
50
|
-
Replacing a facet reuses the
|
|
51
|
-
|
|
52
|
-
## Registering entries
|
|
53
|
-
|
|
54
|
-
Populate registries directly or through [components](component.md) during `
|
|
55
|
-
|
|
56
|
-
```python
|
|
57
|
-
def hello():
|
|
58
|
-
"""Say hello."""
|
|
59
|
-
return "Hello!"
|
|
60
|
-
|
|
61
|
-
app.facets["cli"].add_command("hello", hello)
|
|
62
|
-
app.facets["web"].add_route("hello", "/hello", lambda: {"message": "Hello!"}, methods=["GET"])
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
Each facet exposes typed `add_*` / `remove_*` helpers over plain dict registries. Your application dispatches however you like — argparse, FastAPI, Textual, Qt, etc.
|
|
66
|
-
|
|
67
|
-
##
|
|
68
|
-
|
|
69
|
-
The
|
|
70
|
-
|
|
71
|
-
```python
|
|
72
|
-
app.
|
|
73
|
-
app.
|
|
1
|
+
# Facets
|
|
2
|
+
|
|
3
|
+
A **Facet** is a passive registry for an application surface — CLI commands, web routes, GUI widgets, background tasks, and so on. Facets collect descriptors; your dispatch layer reads them and runs the app. Bind only what you need on the [Container](container.md).
|
|
4
|
+
|
|
5
|
+
## Types and exceptions
|
|
6
|
+
|
|
7
|
+
**Protocol**
|
|
8
|
+
|
|
9
|
+
- `Facet` — `name: str`, `clear()`
|
|
10
|
+
|
|
11
|
+
**Implementations**
|
|
12
|
+
|
|
13
|
+
| Facet | Registries | Purpose |
|
|
14
|
+
|-------|------------|---------|
|
|
15
|
+
| `CliFacet` | `commands` | Named CLI commands. Description is taken from the handler's docstring |
|
|
16
|
+
| `TuiFacet` | `screens`, `keybindings`, `current_screen` | Terminal UI descriptors |
|
|
17
|
+
| `GuiFacet` | `widgets`, `menus`, `toolbars`, `layouts` | Desktop UI descriptors |
|
|
18
|
+
| `WebFacet` | `routes`, `middleware`, `error_handlers` | HTTP/API descriptors |
|
|
19
|
+
| `ServiceFacet` | `tasks`, `providers` | Background work and shared providers |
|
|
20
|
+
|
|
21
|
+
**Descriptor types** — `Command`, `ScreenDescriptor`, `KeybindingDescriptor`, `WidgetDescriptor`, `MenuDescriptor`, `ToolbarDescriptor`, `LayoutDescriptor`, `RouteDescriptor`, `MiddlewareDescriptor`, `ErrorHandlerDescriptor`, `TaskDescriptor`
|
|
22
|
+
|
|
23
|
+
**Exceptions**
|
|
24
|
+
|
|
25
|
+
| Exception | When | Attributes |
|
|
26
|
+
|-----------|------|------------|
|
|
27
|
+
| `DuplicateFacetError` | `bind_facet(overwrite=False)` | `.facet_id` |
|
|
28
|
+
| `FacetInUseError` | `unbind_facet` while a component requires the facet id | `.facet_id`, `.dependents` |
|
|
29
|
+
|
|
30
|
+
## Lifecycle
|
|
31
|
+
|
|
32
|
+
Facets are bound and unbound through the container:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from facetkit import Container, CliFacet, WebFacet
|
|
36
|
+
|
|
37
|
+
app = Container({})
|
|
38
|
+
app.bind_facet("cli", CliFacet())
|
|
39
|
+
app.unbind_facet("cli")
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**`bind_facet(facet_id, facet, *, overwrite=True)`** — stores the facet under `facet_id`. When `overwrite=True` (the default), an existing binding of the same id is cleared and replaced without a dependent check. Pass `overwrite=False` to raise `DuplicateFacetError` instead:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
app.bind_facet("cli", CliFacet(), overwrite=False)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**`unbind_facet(facet_id)`** — pops the facet and calls `clear()` on it. Raises `FacetInUseError` if any bound [component](component.md) lists `facet_id` in `required_facets`. Unbind those components first.
|
|
49
|
+
|
|
50
|
+
Replacing a facet reuses the facet id — dependent checks are skipped on replacement so the slot stays available.
|
|
51
|
+
|
|
52
|
+
## Registering entries
|
|
53
|
+
|
|
54
|
+
Populate registries directly or through [components](component.md) during `on_bind`:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
def hello():
|
|
58
|
+
"""Say hello."""
|
|
59
|
+
return "Hello!"
|
|
60
|
+
|
|
61
|
+
app.facets["cli"].add_command("hello", hello)
|
|
62
|
+
app.facets["web"].add_route("hello", "/hello", lambda: {"message": "Hello!"}, methods=["GET"])
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Each facet exposes typed `add_*` / `remove_*` helpers over plain dict registries. Your application dispatches however you like — argparse, FastAPI, Textual, Qt, etc.
|
|
66
|
+
|
|
67
|
+
## Facet ids
|
|
68
|
+
|
|
69
|
+
The facet id is arbitrary. The library does not require `"cli"` for a `CliFacet`, though consistent naming helps components declare `required_facets`:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
app.bind_facet("cli", CliFacet()) # conventional
|
|
73
|
+
app.bind_facet("commands", CliFacet()) # also valid
|
|
74
74
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from facetkit.container import Container
|
|
2
2
|
from facetkit.exceptions import (
|
|
3
|
-
|
|
3
|
+
ComponentInUseError,
|
|
4
4
|
DuplicateComponentError,
|
|
5
5
|
DuplicateFacetError,
|
|
6
6
|
FacetInUseError,
|
|
@@ -27,7 +27,7 @@ from facetkit.facets import CliFacet, ServiceFacet, TuiFacet, GuiFacet, WebFacet
|
|
|
27
27
|
__all__ = [
|
|
28
28
|
"Container",
|
|
29
29
|
"Component",
|
|
30
|
-
"
|
|
30
|
+
"ComponentInUseError",
|
|
31
31
|
"DuplicateComponentError",
|
|
32
32
|
"DuplicateFacetError",
|
|
33
33
|
"FacetInUseError",
|
|
@@ -52,4 +52,4 @@ __all__ = [
|
|
|
52
52
|
"WebFacet",
|
|
53
53
|
]
|
|
54
54
|
|
|
55
|
-
__version__ = "0.
|
|
55
|
+
__version__ = "0.4.0"
|