facetkit 0.1.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.1.0/.gitignore +31 -0
- facetkit-0.1.0/CHANGELOG.md +19 -0
- facetkit-0.1.0/LICENSE +21 -0
- facetkit-0.1.0/PKG-INFO +214 -0
- facetkit-0.1.0/README.md +188 -0
- facetkit-0.1.0/facetkit/__init__.py +41 -0
- facetkit-0.1.0/facetkit/container.py +51 -0
- facetkit-0.1.0/facetkit/facets/__init__.py +7 -0
- facetkit-0.1.0/facetkit/facets/cli.py +26 -0
- facetkit-0.1.0/facetkit/facets/gui.py +53 -0
- facetkit-0.1.0/facetkit/facets/service.py +31 -0
- facetkit-0.1.0/facetkit/facets/tui.py +41 -0
- facetkit-0.1.0/facetkit/facets/web.py +45 -0
- facetkit-0.1.0/facetkit/py.typed +0 -0
- facetkit-0.1.0/facetkit/types.py +47 -0
- facetkit-0.1.0/pyproject.toml +47 -0
- facetkit-0.1.0/requirements.txt +1 -0
- facetkit-0.1.0/tests/conftest.py +22 -0
- facetkit-0.1.0/tests/test_container.py +116 -0
- facetkit-0.1.0/tests/test_container_facets.py +89 -0
- facetkit-0.1.0/tests/test_facet.py +256 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Python artifacts
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
|
|
8
|
+
# Packaging
|
|
9
|
+
build/
|
|
10
|
+
dist/
|
|
11
|
+
*.egg-info/
|
|
12
|
+
*.egg
|
|
13
|
+
|
|
14
|
+
# Virtual environments
|
|
15
|
+
.venv/
|
|
16
|
+
venv/
|
|
17
|
+
.env/
|
|
18
|
+
env/
|
|
19
|
+
|
|
20
|
+
# Test / type / lint caches
|
|
21
|
+
.pytest_cache/
|
|
22
|
+
.mypy_cache/
|
|
23
|
+
.ruff_cache/
|
|
24
|
+
.coverage
|
|
25
|
+
htmlcov/
|
|
26
|
+
|
|
27
|
+
# IDE
|
|
28
|
+
.vscode/
|
|
29
|
+
.idea/
|
|
30
|
+
*.swp
|
|
31
|
+
*.swo
|
|
@@ -0,0 +1,19 @@
|
|
|
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.1.0] - 2026-06-18
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `Container` with config, component lifecycle, and facet mounting
|
|
13
|
+
- Passive facets: `CliFacet`, `TuiFacet`, `GuiFacet`, `WebFacet`, `ServiceFacet`
|
|
14
|
+
- `Component` attach/detach protocol for composable plugins
|
|
15
|
+
- `Container.get()` introspection via glom paths
|
|
16
|
+
- Descriptor types for registry entries (`Command`, `RouteDescriptor`, etc.)
|
|
17
|
+
- CLI command descriptions derived from handler docstrings
|
|
18
|
+
|
|
19
|
+
[0.1.0]: https://github.com/Dev-DanielR/py_facetkit/releases/tag/v0.1.0
|
facetkit-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Daniel R. Vásquez Montes
|
|
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.
|
facetkit-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: facetkit
|
|
3
|
+
Version: 0.1.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.
|
|
30
|
+
Provides basic functionality for CLI commands, TUI screens, GUI widgets, web routes and background services.
|
|
31
|
+
|
|
32
|
+
## Design
|
|
33
|
+
|
|
34
|
+
- **Container** — shared config, component lifecycle, and a map of mounted facets
|
|
35
|
+
- **Facets** — framework-agnostic registries (commands, routes, widgets, tasks, etc)
|
|
36
|
+
- **Components** — plugins that register into facets on `attach` and clean up on `detach`
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
Container
|
|
40
|
+
├── config
|
|
41
|
+
├── components
|
|
42
|
+
└── facets
|
|
43
|
+
├── cli → commands
|
|
44
|
+
├── tui → screens, keybindings
|
|
45
|
+
├── gui → widgets, menus, toolbars, layouts
|
|
46
|
+
├── web → routes, middleware, error handlers
|
|
47
|
+
└── service → tasks, providers
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Requirements
|
|
51
|
+
|
|
52
|
+
- Python 3.10+
|
|
53
|
+
- [glom](https://github.com/mahmoud/glom) (declared as a dependency)
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install facetkit
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
For local development:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
git clone https://github.com/Dev-DanielR/py_facetkit.git
|
|
65
|
+
cd facetkit
|
|
66
|
+
pip install -e ".[dev]"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Quick start
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from facetkit import Container, CliFacet, WebFacet
|
|
73
|
+
|
|
74
|
+
app = Container({"app": {"name": "demo"}})
|
|
75
|
+
|
|
76
|
+
app.mount_facet("cli", CliFacet())
|
|
77
|
+
app.mount_facet("web", WebFacet())
|
|
78
|
+
|
|
79
|
+
def hello():
|
|
80
|
+
"""Say hello."""
|
|
81
|
+
return "Hello!"
|
|
82
|
+
|
|
83
|
+
cli = app.facets["cli"]
|
|
84
|
+
cli.add_command("hello", hello)
|
|
85
|
+
|
|
86
|
+
web = app.facets["web"]
|
|
87
|
+
web.add_route("hello", "/hello", lambda: {"message": "Hello!"}, methods=["GET"])
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Your application reads the registries and dispatches however you like — argparse, FastAPI, Textual, Qt, etc.
|
|
91
|
+
|
|
92
|
+
## Components
|
|
93
|
+
|
|
94
|
+
Components implement `attach(ctx)` and `detach(ctx)`. The container is passed as context:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
class StatusComponent:
|
|
98
|
+
def attach(self, ctx):
|
|
99
|
+
ctx.facets["cli"].add_command("status", self.show_status)
|
|
100
|
+
ctx.facets["service"].add_provider("status", {"healthy": True})
|
|
101
|
+
|
|
102
|
+
def detach(self, ctx):
|
|
103
|
+
ctx.facets["cli"].remove_command("status")
|
|
104
|
+
ctx.facets["service"].remove_provider("status")
|
|
105
|
+
|
|
106
|
+
def show_status(self):
|
|
107
|
+
"""Show application status."""
|
|
108
|
+
return "ok"
|
|
109
|
+
|
|
110
|
+
app.mount_facet("cli", CliFacet())
|
|
111
|
+
app.mount_facet("service", ServiceFacet())
|
|
112
|
+
app.add_component("status", StatusComponent())
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Replacing or removing a component calls `detach` on the old instance before the new one attaches.
|
|
116
|
+
|
|
117
|
+
## Facets
|
|
118
|
+
|
|
119
|
+
| Facet | Registries | Purpose |
|
|
120
|
+
|-------|------------|---------|
|
|
121
|
+
| `CliFacet` | `commands` | Named CLI commands. Description is taken from the handler's docstring |
|
|
122
|
+
| `TuiFacet` | `screens`, `keybindings`, `current_screen` | Terminal UI descriptors |
|
|
123
|
+
| `GuiFacet` | `widgets`, `menus`, `toolbars`, `layouts` | Desktop UI descriptors |
|
|
124
|
+
| `WebFacet` | `routes`, `middleware`, `error_handlers` | HTTP/API descriptors |
|
|
125
|
+
| `ServiceFacet` | `tasks`, `providers` | Background work and shared providers |
|
|
126
|
+
|
|
127
|
+
Mount only what you need:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
app.mount_facet("cli", CliFacet())
|
|
131
|
+
app.mount_facet("web", WebFacet())
|
|
132
|
+
# No TUI/GUI facets required
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Unmounting clears the facet's registries:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
app.unmount_facet("cli")
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Registry behavior
|
|
142
|
+
|
|
143
|
+
- **Duplicate keys** — registering the same name again overwrites the previous entry (last wins).
|
|
144
|
+
- **Unmount** — `unmount_facet` clears that facet's registries. Attached components are not automatically detached; call `remove_component` first if you need a full teardown.
|
|
145
|
+
- **Facet names** — the mount name is arbitrary (`app.mount_facet("cli", CliFacet())`). The library does not enforce that the name matches the facet type.
|
|
146
|
+
- **Missing facets** — `app.facets["cli"]` raises `KeyError`; `app.get("facets.cli")` returns `None`.
|
|
147
|
+
|
|
148
|
+
## Introspection with `get()`
|
|
149
|
+
|
|
150
|
+
`Container.get(path)` uses [glom](https://glom.readthedocs.io/) to read nested state:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
app.get("") # the container itself
|
|
154
|
+
app.get("config.app.name") # config values
|
|
155
|
+
app.get("facets") # all mounted facets
|
|
156
|
+
app.get("facets.cli") # CliFacet instance
|
|
157
|
+
app.get("facets.cli.commands") # command registry
|
|
158
|
+
app.get("facets.web.routes.users") # a single route entry
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Missing paths return `default` (or `None`):
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
app.get("facets.missing") # None
|
|
165
|
+
app.get("facets.missing", default={}) # {}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`get()` also returns `default` on any internal error while resolving a path, not only missing keys. That keeps introspection safe but can hide bugs — prefer direct attribute access when you want exceptions to surface.
|
|
169
|
+
|
|
170
|
+
Direct dict access also works when you know a facet is mounted:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
app.facets["cli"].commands["hello"]
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Public API
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from facetkit import (
|
|
180
|
+
Container,
|
|
181
|
+
Component, # protocol
|
|
182
|
+
Facet, # protocol
|
|
183
|
+
CliFacet,
|
|
184
|
+
TuiFacet,
|
|
185
|
+
GuiFacet,
|
|
186
|
+
WebFacet,
|
|
187
|
+
ServiceFacet,
|
|
188
|
+
Command,
|
|
189
|
+
RouteDescriptor,
|
|
190
|
+
# ... other descriptor types
|
|
191
|
+
)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Versioning
|
|
195
|
+
|
|
196
|
+
This project is pre-1.0 (`0.1.0`). Public APIs may change between minor releases. See [CHANGELOG.md](CHANGELOG.md) for release notes.
|
|
197
|
+
|
|
198
|
+
## Development
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
pip install -e ".[dev]"
|
|
202
|
+
pytest
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Build a wheel locally:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
pip install build
|
|
209
|
+
python -m build
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## License
|
|
213
|
+
|
|
214
|
+
MIT — see [LICENSE](LICENSE).
|
facetkit-0.1.0/README.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# facetkit
|
|
2
|
+
|
|
3
|
+
A composable Python container for application state and passive registries.
|
|
4
|
+
Provides basic functionality for CLI commands, TUI screens, GUI widgets, web routes and background services.
|
|
5
|
+
|
|
6
|
+
## Design
|
|
7
|
+
|
|
8
|
+
- **Container** — shared config, component lifecycle, and a map of mounted facets
|
|
9
|
+
- **Facets** — framework-agnostic registries (commands, routes, widgets, tasks, etc)
|
|
10
|
+
- **Components** — plugins that register into facets on `attach` and clean up on `detach`
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
Container
|
|
14
|
+
├── config
|
|
15
|
+
├── components
|
|
16
|
+
└── facets
|
|
17
|
+
├── cli → commands
|
|
18
|
+
├── tui → screens, keybindings
|
|
19
|
+
├── gui → widgets, menus, toolbars, layouts
|
|
20
|
+
├── web → routes, middleware, error handlers
|
|
21
|
+
└── service → tasks, providers
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Requirements
|
|
25
|
+
|
|
26
|
+
- Python 3.10+
|
|
27
|
+
- [glom](https://github.com/mahmoud/glom) (declared as a dependency)
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install facetkit
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
For local development:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
git clone https://github.com/Dev-DanielR/py_facetkit.git
|
|
39
|
+
cd facetkit
|
|
40
|
+
pip install -e ".[dev]"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick start
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from facetkit import Container, CliFacet, WebFacet
|
|
47
|
+
|
|
48
|
+
app = Container({"app": {"name": "demo"}})
|
|
49
|
+
|
|
50
|
+
app.mount_facet("cli", CliFacet())
|
|
51
|
+
app.mount_facet("web", WebFacet())
|
|
52
|
+
|
|
53
|
+
def hello():
|
|
54
|
+
"""Say hello."""
|
|
55
|
+
return "Hello!"
|
|
56
|
+
|
|
57
|
+
cli = app.facets["cli"]
|
|
58
|
+
cli.add_command("hello", hello)
|
|
59
|
+
|
|
60
|
+
web = app.facets["web"]
|
|
61
|
+
web.add_route("hello", "/hello", lambda: {"message": "Hello!"}, methods=["GET"])
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Your application reads the registries and dispatches however you like — argparse, FastAPI, Textual, Qt, etc.
|
|
65
|
+
|
|
66
|
+
## Components
|
|
67
|
+
|
|
68
|
+
Components implement `attach(ctx)` and `detach(ctx)`. The container is passed as context:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
class StatusComponent:
|
|
72
|
+
def attach(self, ctx):
|
|
73
|
+
ctx.facets["cli"].add_command("status", self.show_status)
|
|
74
|
+
ctx.facets["service"].add_provider("status", {"healthy": True})
|
|
75
|
+
|
|
76
|
+
def detach(self, ctx):
|
|
77
|
+
ctx.facets["cli"].remove_command("status")
|
|
78
|
+
ctx.facets["service"].remove_provider("status")
|
|
79
|
+
|
|
80
|
+
def show_status(self):
|
|
81
|
+
"""Show application status."""
|
|
82
|
+
return "ok"
|
|
83
|
+
|
|
84
|
+
app.mount_facet("cli", CliFacet())
|
|
85
|
+
app.mount_facet("service", ServiceFacet())
|
|
86
|
+
app.add_component("status", StatusComponent())
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Replacing or removing a component calls `detach` on the old instance before the new one attaches.
|
|
90
|
+
|
|
91
|
+
## Facets
|
|
92
|
+
|
|
93
|
+
| Facet | Registries | Purpose |
|
|
94
|
+
|-------|------------|---------|
|
|
95
|
+
| `CliFacet` | `commands` | Named CLI commands. Description is taken from the handler's docstring |
|
|
96
|
+
| `TuiFacet` | `screens`, `keybindings`, `current_screen` | Terminal UI descriptors |
|
|
97
|
+
| `GuiFacet` | `widgets`, `menus`, `toolbars`, `layouts` | Desktop UI descriptors |
|
|
98
|
+
| `WebFacet` | `routes`, `middleware`, `error_handlers` | HTTP/API descriptors |
|
|
99
|
+
| `ServiceFacet` | `tasks`, `providers` | Background work and shared providers |
|
|
100
|
+
|
|
101
|
+
Mount only what you need:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
app.mount_facet("cli", CliFacet())
|
|
105
|
+
app.mount_facet("web", WebFacet())
|
|
106
|
+
# No TUI/GUI facets required
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Unmounting clears the facet's registries:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
app.unmount_facet("cli")
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Registry behavior
|
|
116
|
+
|
|
117
|
+
- **Duplicate keys** — registering the same name again overwrites the previous entry (last wins).
|
|
118
|
+
- **Unmount** — `unmount_facet` clears that facet's registries. Attached components are not automatically detached; call `remove_component` first if you need a full teardown.
|
|
119
|
+
- **Facet names** — the mount name is arbitrary (`app.mount_facet("cli", CliFacet())`). The library does not enforce that the name matches the facet type.
|
|
120
|
+
- **Missing facets** — `app.facets["cli"]` raises `KeyError`; `app.get("facets.cli")` returns `None`.
|
|
121
|
+
|
|
122
|
+
## Introspection with `get()`
|
|
123
|
+
|
|
124
|
+
`Container.get(path)` uses [glom](https://glom.readthedocs.io/) to read nested state:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
app.get("") # the container itself
|
|
128
|
+
app.get("config.app.name") # config values
|
|
129
|
+
app.get("facets") # all mounted facets
|
|
130
|
+
app.get("facets.cli") # CliFacet instance
|
|
131
|
+
app.get("facets.cli.commands") # command registry
|
|
132
|
+
app.get("facets.web.routes.users") # a single route entry
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Missing paths return `default` (or `None`):
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
app.get("facets.missing") # None
|
|
139
|
+
app.get("facets.missing", default={}) # {}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`get()` also returns `default` on any internal error while resolving a path, not only missing keys. That keeps introspection safe but can hide bugs — prefer direct attribute access when you want exceptions to surface.
|
|
143
|
+
|
|
144
|
+
Direct dict access also works when you know a facet is mounted:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
app.facets["cli"].commands["hello"]
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Public API
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from facetkit import (
|
|
154
|
+
Container,
|
|
155
|
+
Component, # protocol
|
|
156
|
+
Facet, # protocol
|
|
157
|
+
CliFacet,
|
|
158
|
+
TuiFacet,
|
|
159
|
+
GuiFacet,
|
|
160
|
+
WebFacet,
|
|
161
|
+
ServiceFacet,
|
|
162
|
+
Command,
|
|
163
|
+
RouteDescriptor,
|
|
164
|
+
# ... other descriptor types
|
|
165
|
+
)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Versioning
|
|
169
|
+
|
|
170
|
+
This project is pre-1.0 (`0.1.0`). Public APIs may change between minor releases. See [CHANGELOG.md](CHANGELOG.md) for release notes.
|
|
171
|
+
|
|
172
|
+
## Development
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
pip install -e ".[dev]"
|
|
176
|
+
pytest
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Build a wheel locally:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
pip install build
|
|
183
|
+
python -m build
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,41 @@
|
|
|
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.1.0"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#===============================================================================
|
|
2
|
+
# DEPENDENCIES
|
|
3
|
+
|
|
4
|
+
import glom
|
|
5
|
+
|
|
6
|
+
from typing import Dict, Any, Optional
|
|
7
|
+
from facetkit.types import Component, Facet
|
|
8
|
+
|
|
9
|
+
#===============================================================================
|
|
10
|
+
# DEFINITIONS
|
|
11
|
+
|
|
12
|
+
class Container:
|
|
13
|
+
|
|
14
|
+
def __init__(self, config: Dict[str, Any]):
|
|
15
|
+
self.config : Dict[str, Any] = config
|
|
16
|
+
self.facets : Dict[str, Facet] = {}
|
|
17
|
+
self.components : Dict[str, Component] = {}
|
|
18
|
+
|
|
19
|
+
# Public API ===============================================================
|
|
20
|
+
|
|
21
|
+
def get(self, path: str, default: Any = None) -> Any:
|
|
22
|
+
"""Retrieve from container using glom."""
|
|
23
|
+
|
|
24
|
+
if not path: return self
|
|
25
|
+
try:
|
|
26
|
+
return glom.glom(self.__dict__, path)
|
|
27
|
+
except (glom.PathAccessError, glom.GlomError):
|
|
28
|
+
return default
|
|
29
|
+
except Exception:
|
|
30
|
+
return default
|
|
31
|
+
|
|
32
|
+
def mount_facet(self, name: str, facet: Facet) -> None:
|
|
33
|
+
if name in self.facets: self.unmount_facet(name)
|
|
34
|
+
self.facets[name] = facet
|
|
35
|
+
|
|
36
|
+
def unmount_facet(self, name: str) -> None:
|
|
37
|
+
facet = self.facets.pop(name, None)
|
|
38
|
+
if facet: facet.clear()
|
|
39
|
+
|
|
40
|
+
def add_component(self, name: str, comp: Component) -> None:
|
|
41
|
+
"""Register a component and attach it to this container."""
|
|
42
|
+
|
|
43
|
+
if name in self.components: self.remove_component(name)
|
|
44
|
+
comp.attach(self)
|
|
45
|
+
self.components[name] = comp
|
|
46
|
+
|
|
47
|
+
def remove_component(self, name: str) -> None:
|
|
48
|
+
"""Remove a component and detach it from this container."""
|
|
49
|
+
|
|
50
|
+
comp = self.components.pop(name, None)
|
|
51
|
+
if comp: comp.detach(self)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from facetkit.facets.cli import CliFacet
|
|
2
|
+
from facetkit.facets.gui import GuiFacet
|
|
3
|
+
from facetkit.facets.service import ServiceFacet
|
|
4
|
+
from facetkit.facets.tui import TuiFacet
|
|
5
|
+
from facetkit.facets.web import WebFacet
|
|
6
|
+
|
|
7
|
+
__all__ = ["CliFacet", "ServiceFacet", "TuiFacet", "GuiFacet", "WebFacet"]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#===============================================================================
|
|
2
|
+
# DEPENDENCIES
|
|
3
|
+
|
|
4
|
+
from textwrap import dedent
|
|
5
|
+
|
|
6
|
+
from typing import Any, Callable, Dict
|
|
7
|
+
from facetkit.types import Command, Facet
|
|
8
|
+
|
|
9
|
+
#===============================================================================
|
|
10
|
+
# DEFINITIONS
|
|
11
|
+
|
|
12
|
+
class CliFacet(Facet):
|
|
13
|
+
|
|
14
|
+
def __init__(self):
|
|
15
|
+
self.name = "cli"
|
|
16
|
+
self.commands : Dict[str, Command] = {}
|
|
17
|
+
|
|
18
|
+
def add_command(self, name: str, handler: Callable[..., Any]) -> None:
|
|
19
|
+
doc = handler.__doc__ or ""
|
|
20
|
+
self.commands[name] = Command(name, handler, dedent(doc).strip())
|
|
21
|
+
|
|
22
|
+
def remove_command(self, name: str) -> None:
|
|
23
|
+
self.commands.pop(name, None)
|
|
24
|
+
|
|
25
|
+
def clear(self) -> None:
|
|
26
|
+
self.commands.clear()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#===============================================================================
|
|
2
|
+
# DEPENDENCIES
|
|
3
|
+
|
|
4
|
+
from typing import Any, Callable, Dict, Optional
|
|
5
|
+
from facetkit.types import Facet, LayoutDescriptor, MenuDescriptor, ToolbarDescriptor, WidgetDescriptor
|
|
6
|
+
|
|
7
|
+
#===============================================================================
|
|
8
|
+
# DEFINITIONS
|
|
9
|
+
|
|
10
|
+
class GuiFacet(Facet):
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self.name = "gui"
|
|
14
|
+
self.widgets : Dict[str, WidgetDescriptor] = {}
|
|
15
|
+
self.menus : Dict[str, MenuDescriptor] = {}
|
|
16
|
+
self.toolbars : Dict[str, ToolbarDescriptor] = {}
|
|
17
|
+
self.layouts : Dict[str, LayoutDescriptor] = {}
|
|
18
|
+
|
|
19
|
+
def add_widget(
|
|
20
|
+
self,
|
|
21
|
+
widget_id: str,
|
|
22
|
+
factory: Callable[..., Any],
|
|
23
|
+
parent: Optional[str] = None,
|
|
24
|
+
layout_hints: Optional[Dict[str, Any]] = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
self.widgets[widget_id] = WidgetDescriptor(widget_id, factory, parent, layout_hints or {})
|
|
27
|
+
|
|
28
|
+
def remove_widget(self, widget_id: str) -> None:
|
|
29
|
+
self.widgets.pop(widget_id, None)
|
|
30
|
+
|
|
31
|
+
def add_menu(self, menu_id: str, factory: Callable[..., Any], parent: Optional[str] = None) -> None:
|
|
32
|
+
self.menus[menu_id] = MenuDescriptor(menu_id, factory, parent)
|
|
33
|
+
|
|
34
|
+
def remove_menu(self, menu_id: str) -> None:
|
|
35
|
+
self.menus.pop(menu_id, None)
|
|
36
|
+
|
|
37
|
+
def add_toolbar(self, toolbar_id: str, factory: Callable[..., Any], parent: Optional[str] = None) -> None:
|
|
38
|
+
self.toolbars[toolbar_id] = ToolbarDescriptor(toolbar_id, factory, parent)
|
|
39
|
+
|
|
40
|
+
def remove_toolbar(self, toolbar_id: str) -> None:
|
|
41
|
+
self.toolbars.pop(toolbar_id, None)
|
|
42
|
+
|
|
43
|
+
def add_layout(self, layout_id: str, factory: Callable[..., Any], hints: Optional[Dict[str, Any]] = None) -> None:
|
|
44
|
+
self.layouts[layout_id] = LayoutDescriptor(layout_id, factory, hints or {})
|
|
45
|
+
|
|
46
|
+
def remove_layout(self, layout_id: str) -> None:
|
|
47
|
+
self.layouts.pop(layout_id, None)
|
|
48
|
+
|
|
49
|
+
def clear(self) -> None:
|
|
50
|
+
self.widgets.clear()
|
|
51
|
+
self.menus.clear()
|
|
52
|
+
self.toolbars.clear()
|
|
53
|
+
self.layouts.clear()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#===============================================================================
|
|
2
|
+
# DEPENDENCIES
|
|
3
|
+
|
|
4
|
+
from typing import Any, Callable, Dict, Optional
|
|
5
|
+
from facetkit.types import Facet, TaskDescriptor
|
|
6
|
+
|
|
7
|
+
#===============================================================================
|
|
8
|
+
# DEFINITIONS
|
|
9
|
+
|
|
10
|
+
class ServiceFacet(Facet):
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self.name = "service"
|
|
14
|
+
self.tasks : Dict[str, TaskDescriptor] = {}
|
|
15
|
+
self.providers : Dict[str, Any] = {}
|
|
16
|
+
|
|
17
|
+
def add_task(self, name: str, factory: Callable[..., Any], interval: Optional[float] = None) -> None:
|
|
18
|
+
self.tasks[name] = TaskDescriptor(name, factory, interval)
|
|
19
|
+
|
|
20
|
+
def remove_task(self, name: str) -> None:
|
|
21
|
+
self.tasks.pop(name, None)
|
|
22
|
+
|
|
23
|
+
def add_provider(self, name: str, provider: Any) -> None:
|
|
24
|
+
self.providers[name] = provider
|
|
25
|
+
|
|
26
|
+
def remove_provider(self, name: str) -> None:
|
|
27
|
+
self.providers.pop(name, None)
|
|
28
|
+
|
|
29
|
+
def clear(self) -> None:
|
|
30
|
+
self.tasks.clear()
|
|
31
|
+
self.providers.clear()
|