A composable dependency container with passive UI and service facets.
Project description
facetkit
A composable Python container for application state and passive registries. Provides basic functionality for CLI commands, TUI screens, GUI widgets, web routes and background services.
Design
- Container — shared config, component lifecycle, and a map of mounted facets
- Facets — framework-agnostic registries (commands, routes, widgets, tasks, etc)
- Components — plugins that register into facets on
attachand clean up ondetach
Container
├── config
├── components
└── facets
├── cli → commands
├── tui → screens, keybindings
├── gui → widgets, menus, toolbars, layouts
├── web → routes, middleware, error handlers
└── service → tasks, providers
Requirements
- Python 3.10+
- glom (declared as a dependency)
Installation
pip install facetkit
For local development:
git clone https://github.com/Dev-DanielR/py_facetkit.git
cd facetkit
pip install -e ".[dev]"
Quick start
from facetkit import Container, CliFacet, WebFacet
app = Container({"app": {"name": "demo"}})
app.mount_facet("cli", CliFacet())
app.mount_facet("web", WebFacet())
def hello():
"""Say hello."""
return "Hello!"
cli = app.facets["cli"]
cli.add_command("hello", hello)
web = app.facets["web"]
web.add_route("hello", "/hello", lambda: {"message": "Hello!"}, methods=["GET"])
Your application reads the registries and dispatches however you like — argparse, FastAPI, Textual, Qt, etc.
Components
Components implement attach(ctx) and detach(ctx). The container is passed as context:
class StatusComponent:
def attach(self, ctx):
ctx.facets["cli"].add_command("status", self.show_status)
ctx.facets["service"].add_provider("status", {"healthy": True})
def detach(self, ctx):
ctx.facets["cli"].remove_command("status")
ctx.facets["service"].remove_provider("status")
def show_status(self):
"""Show application status."""
return "ok"
app.mount_facet("cli", CliFacet())
app.mount_facet("service", ServiceFacet())
app.add_component("status", StatusComponent())
Replacing or removing a component calls detach on the old instance before the new one attaches.
Facets
| Facet | Registries | Purpose |
|---|---|---|
CliFacet |
commands |
Named CLI commands. Description is taken from the handler's docstring |
TuiFacet |
screens, keybindings, current_screen |
Terminal UI descriptors |
GuiFacet |
widgets, menus, toolbars, layouts |
Desktop UI descriptors |
WebFacet |
routes, middleware, error_handlers |
HTTP/API descriptors |
ServiceFacet |
tasks, providers |
Background work and shared providers |
Mount only what you need:
app.mount_facet("cli", CliFacet())
app.mount_facet("web", WebFacet())
# No TUI/GUI facets required
Unmounting clears the facet's registries:
app.unmount_facet("cli")
Registry behavior
- Duplicate keys — registering the same name again overwrites the previous entry (last wins).
- Unmount —
unmount_facetclears that facet's registries. Attached components are not automatically detached; callremove_componentfirst if you need a full teardown. - Facet names — the mount name is arbitrary (
app.mount_facet("cli", CliFacet())). The library does not enforce that the name matches the facet type. - Missing facets —
app.facets["cli"]raisesKeyError;app.get("facets.cli")returnsNone.
Introspection with get()
Container.get(path) uses glom to read nested state:
app.get("") # the container itself
app.get("config.app.name") # config values
app.get("facets") # all mounted facets
app.get("facets.cli") # CliFacet instance
app.get("facets.cli.commands") # command registry
app.get("facets.web.routes.users") # a single route entry
Missing paths return default (or None):
app.get("facets.missing") # None
app.get("facets.missing", default={}) # {}
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.
Direct dict access also works when you know a facet is mounted:
app.facets["cli"].commands["hello"]
Public API
from facetkit import (
Container,
Component, # protocol
Facet, # protocol
CliFacet,
TuiFacet,
GuiFacet,
WebFacet,
ServiceFacet,
Command,
RouteDescriptor,
# ... other descriptor types
)
Versioning
This project is pre-1.0 (0.1.0). Public APIs may change between minor releases. See CHANGELOG.md for release notes.
Development
pip install -e ".[dev]"
pytest
Build a wheel locally:
pip install build
python -m build
License
MIT — see LICENSE.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file facetkit-0.1.0.tar.gz.
File metadata
- Download URL: facetkit-0.1.0.tar.gz
- Upload date:
- Size: 9.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fcf43aae22c23b2f6c3fc064b08acf3f6a3f8f088bc8bb6cd8398d52f1e60f9b
|
|
| MD5 |
b40bbc7ab86aaf9fcebebefdfc34372c
|
|
| BLAKE2b-256 |
a30eba0030616a8c7d2ffbc526159a5f9a3494393d6d18a00b195871019ab5dc
|
File details
Details for the file facetkit-0.1.0-py3-none-any.whl.
File metadata
- Download URL: facetkit-0.1.0-py3-none-any.whl
- Upload date:
- Size: 9.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8ddc092be2fe740e0d2023af9aaab6a8d325840a48b99c609894261ba70a73a7
|
|
| MD5 |
6ffa6dd94f72f6340f243e088fad7fef
|
|
| BLAKE2b-256 |
5798d554553d99f486620cd2fb252a348c30d61453364653387f37f02273fca7
|