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.
Files changed (31) hide show
  1. facetkit-0.3.0/CHANGELOG.md +44 -0
  2. facetkit-0.3.0/PKG-INFO +108 -0
  3. facetkit-0.3.0/README.md +82 -0
  4. facetkit-0.3.0/docs/component.md +124 -0
  5. facetkit-0.3.0/docs/container.md +75 -0
  6. facetkit-0.3.0/docs/examples/composed_app.py +57 -0
  7. facetkit-0.3.0/docs/facet.md +74 -0
  8. {facetkit-0.2.0 → facetkit-0.3.0}/facetkit/__init__.py +55 -41
  9. facetkit-0.3.0/facetkit/container.py +119 -0
  10. facetkit-0.3.0/facetkit/exceptions.py +52 -0
  11. {facetkit-0.2.0 → facetkit-0.3.0}/facetkit/types.py +12 -1
  12. {facetkit-0.2.0 → facetkit-0.3.0}/tests/test_container.py +239 -136
  13. facetkit-0.3.0/tests/test_container_facets.py +207 -0
  14. facetkit-0.2.0/CHANGELOG.md +0 -19
  15. facetkit-0.2.0/PKG-INFO +0 -219
  16. facetkit-0.2.0/README.md +0 -193
  17. facetkit-0.2.0/facetkit/container.py +0 -59
  18. facetkit-0.2.0/tests/test_container_facets.py +0 -89
  19. {facetkit-0.2.0 → facetkit-0.3.0}/.gitignore +0 -0
  20. {facetkit-0.2.0 → facetkit-0.3.0}/LICENSE +0 -0
  21. {facetkit-0.2.0 → facetkit-0.3.0}/facetkit/facets/__init__.py +0 -0
  22. {facetkit-0.2.0 → facetkit-0.3.0}/facetkit/facets/cli.py +0 -0
  23. {facetkit-0.2.0 → facetkit-0.3.0}/facetkit/facets/gui.py +0 -0
  24. {facetkit-0.2.0 → facetkit-0.3.0}/facetkit/facets/service.py +0 -0
  25. {facetkit-0.2.0 → facetkit-0.3.0}/facetkit/facets/tui.py +0 -0
  26. {facetkit-0.2.0 → facetkit-0.3.0}/facetkit/facets/web.py +0 -0
  27. {facetkit-0.2.0 → facetkit-0.3.0}/facetkit/py.typed +0 -0
  28. {facetkit-0.2.0 → facetkit-0.3.0}/pyproject.toml +0 -0
  29. {facetkit-0.2.0 → facetkit-0.3.0}/requirements.txt +0 -0
  30. {facetkit-0.2.0 → facetkit-0.3.0}/tests/conftest.py +0 -0
  31. {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
@@ -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).
@@ -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.types import (
3
- Component,
4
- Facet,
5
- Command,
6
- TaskDescriptor,
7
- ScreenDescriptor,
8
- KeybindingDescriptor,
9
- WidgetDescriptor,
10
- MenuDescriptor,
11
- ToolbarDescriptor,
12
- LayoutDescriptor,
13
- RouteDescriptor,
14
- MiddlewareDescriptor,
15
- ErrorHandlerDescriptor,
16
- )
17
- from facetkit.facets import CliFacet, ServiceFacet, TuiFacet, GuiFacet, WebFacet
18
-
19
- __all__ = [
20
- "Container",
21
- "Component",
22
- "Facet",
23
- "Command",
24
- "TaskDescriptor",
25
- "ScreenDescriptor",
26
- "KeybindingDescriptor",
27
- "WidgetDescriptor",
28
- "MenuDescriptor",
29
- "ToolbarDescriptor",
30
- "LayoutDescriptor",
31
- "RouteDescriptor",
32
- "MiddlewareDescriptor",
33
- "ErrorHandlerDescriptor",
34
- "CliFacet",
35
- "ServiceFacet",
36
- "TuiFacet",
37
- "GuiFacet",
38
- "WebFacet",
39
- ]
40
-
41
- __version__ = "0.2.0"
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"