facetkit 0.2.0__tar.gz → 0.3.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/CHANGELOG.md +44 -0
- facetkit-0.3.0/PKG-INFO +108 -0
- facetkit-0.3.0/README.md +82 -0
- facetkit-0.3.0/docs/component.md +124 -0
- facetkit-0.3.0/docs/container.md +75 -0
- facetkit-0.3.0/docs/examples/composed_app.py +57 -0
- facetkit-0.3.0/docs/facet.md +74 -0
- {facetkit-0.2.0 → facetkit-0.3.0}/facetkit/__init__.py +55 -41
- facetkit-0.3.0/facetkit/container.py +119 -0
- facetkit-0.3.0/facetkit/exceptions.py +52 -0
- {facetkit-0.2.0 → facetkit-0.3.0}/facetkit/types.py +12 -1
- {facetkit-0.2.0 → facetkit-0.3.0}/tests/test_container.py +239 -136
- facetkit-0.3.0/tests/test_container_facets.py +207 -0
- facetkit-0.2.0/CHANGELOG.md +0 -19
- facetkit-0.2.0/PKG-INFO +0 -219
- facetkit-0.2.0/README.md +0 -193
- facetkit-0.2.0/facetkit/container.py +0 -59
- facetkit-0.2.0/tests/test_container_facets.py +0 -89
- {facetkit-0.2.0 → facetkit-0.3.0}/.gitignore +0 -0
- {facetkit-0.2.0 → facetkit-0.3.0}/LICENSE +0 -0
- {facetkit-0.2.0 → facetkit-0.3.0}/facetkit/facets/__init__.py +0 -0
- {facetkit-0.2.0 → facetkit-0.3.0}/facetkit/facets/cli.py +0 -0
- {facetkit-0.2.0 → facetkit-0.3.0}/facetkit/facets/gui.py +0 -0
- {facetkit-0.2.0 → facetkit-0.3.0}/facetkit/facets/service.py +0 -0
- {facetkit-0.2.0 → facetkit-0.3.0}/facetkit/facets/tui.py +0 -0
- {facetkit-0.2.0 → facetkit-0.3.0}/facetkit/facets/web.py +0 -0
- {facetkit-0.2.0 → facetkit-0.3.0}/facetkit/py.typed +0 -0
- {facetkit-0.2.0 → facetkit-0.3.0}/pyproject.toml +0 -0
- {facetkit-0.2.0 → facetkit-0.3.0}/requirements.txt +0 -0
- {facetkit-0.2.0 → facetkit-0.3.0}/tests/conftest.py +0 -0
- {facetkit-0.2.0 → facetkit-0.3.0}/tests/test_facet.py +0 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.3.0] - 2026-06-20
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `overwrite` parameter on `mount_facet` and `add_component` (default `True`; pass `False` to block duplicate keys)
|
|
13
|
+
- `DuplicateFacetError` and `DuplicateComponentError` for blocked duplicate registration
|
|
14
|
+
- `Component.required_components` class attribute for declarative peer dependencies
|
|
15
|
+
- `Component.required_facets` class attribute for declarative facet mount dependencies
|
|
16
|
+
- `MissingComponentDependencyError` raised on attach when required components are absent
|
|
17
|
+
- `MissingFacetDependencyError` raised on attach when required facets are not mounted
|
|
18
|
+
- `DependentComponentsError` raised on remove when other components still depend on it
|
|
19
|
+
- `FacetInUseError` raised on unmount when attached components still depend on the facet
|
|
20
|
+
- Added '/docs' folder, with explanations for container, facet and component.
|
|
21
|
+
- Added '/docs/examples' folder, with an example of a composable app.
|
|
22
|
+
|
|
23
|
+
[0.3.0]: https://github.com/Dev-DanielR/py_facetkit/releases/tag/v0.3.0
|
|
24
|
+
|
|
25
|
+
## [0.2.0] - 2026-06-19
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- `Container.get()` now does a strict check if a default value is omitted
|
|
30
|
+
|
|
31
|
+
[0.2.0]: https://github.com/Dev-DanielR/py_facetkit/releases/tag/v0.2.0
|
|
32
|
+
|
|
33
|
+
## [0.1.0] - 2026-06-18
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- `Container` with config, component lifecycle, and facet mounting
|
|
38
|
+
- Passive facets: `CliFacet`, `TuiFacet`, `GuiFacet`, `WebFacet`, `ServiceFacet`
|
|
39
|
+
- `Component` attach/detach protocol for composable plugins
|
|
40
|
+
- `Container.get()` introspection via glom paths
|
|
41
|
+
- Descriptor types for registry entries (`Command`, `RouteDescriptor`, etc.)
|
|
42
|
+
- CLI command descriptions derived from handler docstrings
|
|
43
|
+
|
|
44
|
+
[0.1.0]: https://github.com/Dev-DanielR/py_facetkit/releases/tag/v0.1.0
|
facetkit-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: facetkit
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: A composable dependency container with passive UI and service facets.
|
|
5
|
+
Author-email: "Daniel R. Vásquez Montes" <dev.DanielR@gmail.composable>
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: cli,container,dependency-injection,gui,plugin,registry,tui,web
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: glom>=23.0
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: build>=1.0; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: twine>=5.0; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# facetkit
|
|
28
|
+
|
|
29
|
+
A composable Python container for application state and passive registries — CLI commands, TUI screens, GUI widgets, web routes, and background services.
|
|
30
|
+
|
|
31
|
+
facetkit separates three pieces:
|
|
32
|
+
|
|
33
|
+
| Piece | Role |
|
|
34
|
+
|-------|------|
|
|
35
|
+
| [Container](docs/container.md) | Application root — config, facet mounting, component lifecycle |
|
|
36
|
+
| [Facet](docs/facet.md) | Passive registry for a surface area (commands, routes, widgets, …) |
|
|
37
|
+
| [Component](docs/component.md) | Plugin that registers into facets on attach and cleans up on detach |
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
Container
|
|
41
|
+
├── config
|
|
42
|
+
├── components
|
|
43
|
+
└── facets
|
|
44
|
+
├── cli → commands
|
|
45
|
+
├── tui → screens, keybindings
|
|
46
|
+
├── gui → widgets, menus, toolbars, layouts
|
|
47
|
+
├── web → routes, middleware, error handlers
|
|
48
|
+
└── service → tasks, providers
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Requirements
|
|
52
|
+
|
|
53
|
+
- Python 3.10+
|
|
54
|
+
- [glom](https://github.com/mahmoud/glom)
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install facetkit
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
For local development:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
git clone https://github.com/Dev-DanielR/py_facetkit.git
|
|
66
|
+
cd facetkit
|
|
67
|
+
pip install -e ".[dev]"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Quick start
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from facetkit import Container, CliFacet, WebFacet
|
|
74
|
+
|
|
75
|
+
app = Container({"app": {"name": "demo"}})
|
|
76
|
+
|
|
77
|
+
app.mount_facet("cli", CliFacet())
|
|
78
|
+
app.mount_facet("web", WebFacet())
|
|
79
|
+
|
|
80
|
+
def hello():
|
|
81
|
+
"""Say hello."""
|
|
82
|
+
return "Hello!"
|
|
83
|
+
|
|
84
|
+
app.facets["cli"].add_command("hello", hello)
|
|
85
|
+
app.facets["web"].add_route("hello", "/hello", lambda: {"message": "Hello!"}, methods=["GET"])
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Register directly on facets (as above) or through [components](docs/component.md). Your dispatch layer reads the registries and wires them to argparse, FastAPI, Textual, Qt, or whatever you use.
|
|
89
|
+
|
|
90
|
+
## Documentation
|
|
91
|
+
|
|
92
|
+
- [Container](docs/container.md) — config, lifecycle, introspection
|
|
93
|
+
- [Facets](docs/facet.md) — facet types, registries, mounting
|
|
94
|
+
- [Components](docs/component.md) — attach/detach, dependencies
|
|
95
|
+
- [Examples](docs/examples/composed_app.py) — composed app with logger and status components
|
|
96
|
+
|
|
97
|
+
## Development
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
pip install -e ".[dev]"
|
|
101
|
+
pytest
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
See [CHANGELOG.md](CHANGELOG.md) for release notes. Pre-1.0 (`0.3.0`) — public APIs may change between minor releases.
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT — see [LICENSE](LICENSE).
|
facetkit-0.3.0/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# facetkit
|
|
2
|
+
|
|
3
|
+
A composable Python container for application state and passive registries — CLI commands, TUI screens, GUI widgets, web routes, and background services.
|
|
4
|
+
|
|
5
|
+
facetkit separates three pieces:
|
|
6
|
+
|
|
7
|
+
| Piece | Role |
|
|
8
|
+
|-------|------|
|
|
9
|
+
| [Container](docs/container.md) | Application root — config, facet mounting, component lifecycle |
|
|
10
|
+
| [Facet](docs/facet.md) | Passive registry for a surface area (commands, routes, widgets, …) |
|
|
11
|
+
| [Component](docs/component.md) | Plugin that registers into facets on attach and cleans up on detach |
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
Container
|
|
15
|
+
├── config
|
|
16
|
+
├── components
|
|
17
|
+
└── facets
|
|
18
|
+
├── cli → commands
|
|
19
|
+
├── tui → screens, keybindings
|
|
20
|
+
├── gui → widgets, menus, toolbars, layouts
|
|
21
|
+
├── web → routes, middleware, error handlers
|
|
22
|
+
└── service → tasks, providers
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Requirements
|
|
26
|
+
|
|
27
|
+
- Python 3.10+
|
|
28
|
+
- [glom](https://github.com/mahmoud/glom)
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install facetkit
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
For local development:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
git clone https://github.com/Dev-DanielR/py_facetkit.git
|
|
40
|
+
cd facetkit
|
|
41
|
+
pip install -e ".[dev]"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quick start
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from facetkit import Container, CliFacet, WebFacet
|
|
48
|
+
|
|
49
|
+
app = Container({"app": {"name": "demo"}})
|
|
50
|
+
|
|
51
|
+
app.mount_facet("cli", CliFacet())
|
|
52
|
+
app.mount_facet("web", WebFacet())
|
|
53
|
+
|
|
54
|
+
def hello():
|
|
55
|
+
"""Say hello."""
|
|
56
|
+
return "Hello!"
|
|
57
|
+
|
|
58
|
+
app.facets["cli"].add_command("hello", hello)
|
|
59
|
+
app.facets["web"].add_route("hello", "/hello", lambda: {"message": "Hello!"}, methods=["GET"])
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Register directly on facets (as above) or through [components](docs/component.md). Your dispatch layer reads the registries and wires them to argparse, FastAPI, Textual, Qt, or whatever you use.
|
|
63
|
+
|
|
64
|
+
## Documentation
|
|
65
|
+
|
|
66
|
+
- [Container](docs/container.md) — config, lifecycle, introspection
|
|
67
|
+
- [Facets](docs/facet.md) — facet types, registries, mounting
|
|
68
|
+
- [Components](docs/component.md) — attach/detach, dependencies
|
|
69
|
+
- [Examples](docs/examples/composed_app.py) — composed app with logger and status components
|
|
70
|
+
|
|
71
|
+
## Development
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install -e ".[dev]"
|
|
75
|
+
pytest
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
See [CHANGELOG.md](CHANGELOG.md) for release notes. Pre-1.0 (`0.3.0`) — public APIs may change between minor releases.
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Components
|
|
2
|
+
|
|
3
|
+
A **Component** is an active plugin that registers into [facets](facet.md) on attach and cleans up on detach. 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 attach(self, ctx): ...
|
|
12
|
+
def detach(self, ctx): ...
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Optional class attributes — see [Declared dependencies](#declared-dependencies).
|
|
16
|
+
|
|
17
|
+
**Exceptions**
|
|
18
|
+
|
|
19
|
+
| Exception | When | Attributes |
|
|
20
|
+
|-----------|------|------------|
|
|
21
|
+
| `DuplicateComponentError` | `add_component(overwrite=False)` | `.name` |
|
|
22
|
+
| `MissingComponentDependencyError` | `add_component` — required component absent | `.component`, `.missing` |
|
|
23
|
+
| `MissingFacetDependencyError` | `add_component` — required facet not mounted | `.component`, `.missing` |
|
|
24
|
+
| `DependentComponentsError` | `remove_component` — another component depends on it | `.component`, `.dependents` |
|
|
25
|
+
|
|
26
|
+
`FacetInUseError` (raised on `unmount_facet`) is documented in [Facets](facet.md).
|
|
27
|
+
|
|
28
|
+
## Lifecycle
|
|
29
|
+
|
|
30
|
+
Components are registered and removed 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 attach(self, ctx):
|
|
39
|
+
ctx.facets["cli"].add_command("status", self.show_status)
|
|
40
|
+
ctx.facets["service"].add_provider("status", {"healthy": True})
|
|
41
|
+
|
|
42
|
+
def detach(self, ctx):
|
|
43
|
+
ctx.facets["cli"].remove_command("status")
|
|
44
|
+
ctx.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.mount_facet("cli", CliFacet())
|
|
52
|
+
app.mount_facet("service", ServiceFacet())
|
|
53
|
+
app.add_component("status", StatusComponent())
|
|
54
|
+
app.remove_component("status")
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**`add_component(name, comp, *, overwrite=True)`** — validates declared dependencies, calls `attach(ctx)`, then stores the instance. The [container](container.md) is passed as `ctx`. When `overwrite=True` (the default), an existing registration of the same name is detached and replaced without a dependent check. Pass `overwrite=False` to raise `DuplicateComponentError` instead:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
app.add_component("logger", Logger(), overwrite=False)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
If validation fails, `attach` is not called.
|
|
64
|
+
|
|
65
|
+
**`remove_component(name)`** — calls `detach(ctx)`, then removes the instance. Raises `DependentComponentsError` if any other attached component lists `name` in `required_components`.
|
|
66
|
+
|
|
67
|
+
Inside `attach` / `detach`, components typically:
|
|
68
|
+
|
|
69
|
+
- Register or unregister entries on `ctx.facets[...]`
|
|
70
|
+
- Read shared state from `ctx.config`
|
|
71
|
+
- Reach peer plugins via `ctx.components["name"]`
|
|
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 attach. 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") # mounted facet names
|
|
87
|
+
|
|
88
|
+
def attach(self, ctx):
|
|
89
|
+
logger = ctx.components["logger"]
|
|
90
|
+
ctx.facets["cli"].add_command("api-status", self.status)
|
|
91
|
+
ctx.facets["web"].add_route("api-status", "/status", self.status)
|
|
92
|
+
|
|
93
|
+
def detach(self, ctx):
|
|
94
|
+
ctx.facets["cli"].remove_command("api-status")
|
|
95
|
+
ctx.facets["web"].remove_route("api-status")
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The container enforces these at lifecycle boundaries:
|
|
99
|
+
|
|
100
|
+
| Action | Validation |
|
|
101
|
+
|--------|------------|
|
|
102
|
+
| `add_component` | Every name in `required_components` must already be registered; every name in `required_facets` must already be mounted. `attach` is not called if anything is missing. |
|
|
103
|
+
| `remove_component` | Blocked if any other attached component lists this name in `required_components`. |
|
|
104
|
+
| `unmount_facet` | Blocked if any attached component lists this mount name in `required_facets`. |
|
|
105
|
+
| Replace same name | Allowed — dependent checks are skipped so the slot stays filled. |
|
|
106
|
+
|
|
107
|
+
Register dependencies before dependents:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
app.add_component("logger", LoggerComponent())
|
|
111
|
+
app.add_component("database", DatabaseComponent())
|
|
112
|
+
app.add_component("api", ApiComponent()) # OK
|
|
113
|
+
|
|
114
|
+
app.add_component("api", ApiComponent()) # raises MissingComponentDependencyError
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Tear down in reverse:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
app.remove_component("api") # OK
|
|
121
|
+
app.remove_component("logger") # OK once api is gone
|
|
122
|
+
|
|
123
|
+
app.unmount_facet("cli") # raises FacetInUseError while a component still requires it
|
|
124
|
+
```
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Container
|
|
2
|
+
|
|
3
|
+
The **Container** is the application root. It holds shared configuration, a map of [mounted facets](facet.md), and a registry of [attached components](component.md).
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
Container
|
|
7
|
+
├── config → shared application settings
|
|
8
|
+
├── components → named plugins (attach / detach lifecycle)
|
|
9
|
+
└── facets → passive registries, keyed by mount name
|
|
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 `ctx.config` or `ctx.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) (`mount_facet`, `unmount_facet`) and [components](component.md) (`add_component`, `remove_component`). Both mount and add accept an `overwrite` flag (default `True`) to control duplicate-name behavior.
|
|
32
|
+
|
|
33
|
+
## Boot order
|
|
34
|
+
|
|
35
|
+
Typical startup sequence:
|
|
36
|
+
|
|
37
|
+
1. Create a `Container` with config
|
|
38
|
+
2. `mount_facet` for each surface your app uses
|
|
39
|
+
3. `add_component` for each feature plugin — register 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 mounted 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 mounted
|
|
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 mounted:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
app.facets["cli"].commands["hello"]
|
|
74
|
+
app.components["logger"]
|
|
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 attach(self, ctx):
|
|
16
|
+
ctx.facets["service"].add_provider("logger", self)
|
|
17
|
+
|
|
18
|
+
def detach(self, ctx):
|
|
19
|
+
ctx.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 attach(self, ctx):
|
|
30
|
+
self._logger = ctx.components["logger"]
|
|
31
|
+
ctx.facets["cli"].add_command("status", self.show_status)
|
|
32
|
+
ctx.facets["service"].add_provider("status", {"healthy": True})
|
|
33
|
+
|
|
34
|
+
def detach(self, ctx):
|
|
35
|
+
ctx.facets["cli"].remove_command("status")
|
|
36
|
+
ctx.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.mount_facet("cli", CliFacet())
|
|
47
|
+
app.mount_facet("service", ServiceFacet())
|
|
48
|
+
app.add_component("logger", LoggerComponent())
|
|
49
|
+
app.add_component("status", StatusComponent())
|
|
50
|
+
|
|
51
|
+
print("Registered CLI commands:")
|
|
52
|
+
for name, cmd in app.facets["cli"].commands.items():
|
|
53
|
+
print(f" {name}: {cmd.description}")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
main()
|
|
@@ -0,0 +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. Mount 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` | `mount_facet(overwrite=False)` | `.name` |
|
|
28
|
+
| `FacetInUseError` | `unmount_facet` while a component requires the mount name | `.facet`, `.dependents` |
|
|
29
|
+
|
|
30
|
+
## Lifecycle
|
|
31
|
+
|
|
32
|
+
Facets are mounted and unmounted through the container:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from facetkit import Container, CliFacet, WebFacet
|
|
36
|
+
|
|
37
|
+
app = Container({})
|
|
38
|
+
app.mount_facet("cli", CliFacet())
|
|
39
|
+
app.unmount_facet("cli")
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**`mount_facet(name, facet, *, overwrite=True)`** — stores the facet under `name`. When `overwrite=True` (the default), an existing mount of the same name is cleared and replaced without a dependent check. Pass `overwrite=False` to raise `DuplicateFacetError` instead:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
app.mount_facet("cli", CliFacet(), overwrite=False)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**`unmount_facet(name)`** — pops the facet and calls `clear()` on it. Raises `FacetInUseError` if any attached [component](component.md) lists `name` in `required_facets`. Remove those components first.
|
|
49
|
+
|
|
50
|
+
Replacing a facet reuses the mount name — 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 `attach`:
|
|
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
|
+
## Mount names
|
|
68
|
+
|
|
69
|
+
The mount name is arbitrary. The library does not require `"cli"` for a `CliFacet`, though consistent naming helps components declare `required_facets`:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
app.mount_facet("cli", CliFacet()) # conventional
|
|
73
|
+
app.mount_facet("commands", CliFacet()) # also valid
|
|
74
|
+
```
|
|
@@ -1,41 +1,55 @@
|
|
|
1
|
-
from facetkit.container import Container
|
|
2
|
-
from facetkit.
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
1
|
+
from facetkit.container import Container
|
|
2
|
+
from facetkit.exceptions import (
|
|
3
|
+
DependentComponentsError,
|
|
4
|
+
DuplicateComponentError,
|
|
5
|
+
DuplicateFacetError,
|
|
6
|
+
FacetInUseError,
|
|
7
|
+
MissingComponentDependencyError,
|
|
8
|
+
MissingFacetDependencyError,
|
|
9
|
+
)
|
|
10
|
+
from facetkit.types import (
|
|
11
|
+
Component,
|
|
12
|
+
Facet,
|
|
13
|
+
Command,
|
|
14
|
+
TaskDescriptor,
|
|
15
|
+
ScreenDescriptor,
|
|
16
|
+
KeybindingDescriptor,
|
|
17
|
+
WidgetDescriptor,
|
|
18
|
+
MenuDescriptor,
|
|
19
|
+
ToolbarDescriptor,
|
|
20
|
+
LayoutDescriptor,
|
|
21
|
+
RouteDescriptor,
|
|
22
|
+
MiddlewareDescriptor,
|
|
23
|
+
ErrorHandlerDescriptor,
|
|
24
|
+
)
|
|
25
|
+
from facetkit.facets import CliFacet, ServiceFacet, TuiFacet, GuiFacet, WebFacet
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"Container",
|
|
29
|
+
"Component",
|
|
30
|
+
"DependentComponentsError",
|
|
31
|
+
"DuplicateComponentError",
|
|
32
|
+
"DuplicateFacetError",
|
|
33
|
+
"FacetInUseError",
|
|
34
|
+
"MissingComponentDependencyError",
|
|
35
|
+
"MissingFacetDependencyError",
|
|
36
|
+
"Facet",
|
|
37
|
+
"Command",
|
|
38
|
+
"TaskDescriptor",
|
|
39
|
+
"ScreenDescriptor",
|
|
40
|
+
"KeybindingDescriptor",
|
|
41
|
+
"WidgetDescriptor",
|
|
42
|
+
"MenuDescriptor",
|
|
43
|
+
"ToolbarDescriptor",
|
|
44
|
+
"LayoutDescriptor",
|
|
45
|
+
"RouteDescriptor",
|
|
46
|
+
"MiddlewareDescriptor",
|
|
47
|
+
"ErrorHandlerDescriptor",
|
|
48
|
+
"CliFacet",
|
|
49
|
+
"ServiceFacet",
|
|
50
|
+
"TuiFacet",
|
|
51
|
+
"GuiFacet",
|
|
52
|
+
"WebFacet",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
__version__ = "0.3.0"
|