pulice 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.
Files changed (38) hide show
  1. pulice-0.1.0/PKG-INFO +128 -0
  2. pulice-0.1.0/README.md +101 -0
  3. pulice-0.1.0/pyproject.toml +112 -0
  4. pulice-0.1.0/src/pulice/__init__.py +19 -0
  5. pulice-0.1.0/src/pulice/admin/__init__.py +13 -0
  6. pulice-0.1.0/src/pulice/admin/__main__.py +8 -0
  7. pulice-0.1.0/src/pulice/admin/app.py +90 -0
  8. pulice-0.1.0/src/pulice/admin/data.py +112 -0
  9. pulice-0.1.0/src/pulice/admin/screens/__init__.py +15 -0
  10. pulice-0.1.0/src/pulice/admin/screens/dashboard.py +61 -0
  11. pulice-0.1.0/src/pulice/admin/screens/stacks.py +86 -0
  12. pulice-0.1.0/src/pulice/admin/screens/system.py +67 -0
  13. pulice-0.1.0/src/pulice/admin/screens/tasks.py +92 -0
  14. pulice-0.1.0/src/pulice/admin/screens/tenants.py +78 -0
  15. pulice-0.1.0/src/pulice/admin/serve.py +45 -0
  16. pulice-0.1.0/src/pulice/admin/styles/app.tcss +39 -0
  17. pulice-0.1.0/src/pulice/admin/widgets/__init__.py +6 -0
  18. pulice-0.1.0/src/pulice/admin/widgets/confirm.py +48 -0
  19. pulice-0.1.0/src/pulice/admin/widgets/stat_card.py +49 -0
  20. pulice-0.1.0/src/pulice/api/__init__.py +25 -0
  21. pulice-0.1.0/src/pulice/api/models.py +82 -0
  22. pulice-0.1.0/src/pulice/api/routes_stacks.py +95 -0
  23. pulice-0.1.0/src/pulice/api/routes_tasks.py +48 -0
  24. pulice-0.1.0/src/pulice/api/routes_tenants.py +65 -0
  25. pulice-0.1.0/src/pulice/cli/__init__.py +15 -0
  26. pulice-0.1.0/src/pulice/cli/app.py +82 -0
  27. pulice-0.1.0/src/pulice/cli/registry.py +558 -0
  28. pulice-0.1.0/src/pulice/core/__init__.py +50 -0
  29. pulice-0.1.0/src/pulice/core/base.py +49 -0
  30. pulice-0.1.0/src/pulice/core/celery_backend.py +89 -0
  31. pulice-0.1.0/src/pulice/core/controllers.py +130 -0
  32. pulice-0.1.0/src/pulice/core/managed.py +172 -0
  33. pulice-0.1.0/src/pulice/core/protocol.py +49 -0
  34. pulice-0.1.0/src/pulice/core/stack.py +743 -0
  35. pulice-0.1.0/src/pulice/core/task_definitions.py +211 -0
  36. pulice-0.1.0/src/pulice/core/tasks.py +222 -0
  37. pulice-0.1.0/src/pulice/py.typed +0 -0
  38. pulice-0.1.0/src/pulice/worker.py +12 -0
pulice-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,128 @@
1
+ Metadata-Version: 2.4
2
+ Name: pulice
3
+ Version: 0.1.0
4
+ Summary: A framework for managing infrastructure-as-code components via Pulumi with tenant isolation, async execution, and pluggable backends.
5
+ Author: mk.fshr
6
+ License-Expression: MIT
7
+ Classifier: Intended Audience :: Developers
8
+ Classifier: Programming Language :: Python :: 3.13
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Requires-Dist: pulumi>=3.215.0
12
+ Requires-Dist: pydantic>=2.12.5
13
+ Requires-Dist: typer>=0.21.1
14
+ Requires-Dist: textual>=3.0.0 ; extra == 'admin'
15
+ Requires-Dist: textual-serve>=1.1.0 ; extra == 'admin'
16
+ Requires-Dist: fastapi>=0.115.0 ; extra == 'api'
17
+ Requires-Dist: uvicorn[standard]>=0.30.0 ; extra == 'api'
18
+ Requires-Dist: huey>=2.5.0 ; extra == 'api'
19
+ Requires-Dist: pulumi-aws>=7.15.0 ; extra == 'aws'
20
+ Requires-Dist: celery[redis]>=5.4.0 ; extra == 'celery'
21
+ Requires-Python: >=3.13
22
+ Provides-Extra: admin
23
+ Provides-Extra: api
24
+ Provides-Extra: aws
25
+ Provides-Extra: celery
26
+ Description-Content-Type: text/markdown
27
+
28
+ # Pulice
29
+
30
+ A Python framework for managing infrastructure-as-code components via [Pulumi](https://www.pulumi.com/) with tenant isolation, async execution, and pluggable backends.
31
+
32
+ ## Features
33
+
34
+ - **Component model** — Define cloud resources as `ManagedComponent` subclasses with Pydantic-validated inputs
35
+ - **Tenant isolation** — Named boundaries ensure stacks belonging to different environments never collide
36
+ - **9 lifecycle operations** — create, read, update, delete, refresh, list, status, export, import
37
+ - **CLI + HTTP API** — Synchronous CLI for interactive use; async FastAPI server for automation
38
+ - **Pluggable task backends** — Huey (SQLite, zero-dep) or Celery (Redis, distributed)
39
+ - **Passphrase-protected stacks** — scrypt-hashed passphrases validated before any Pulumi call
40
+ - **Advisory locking** — Prevents concurrent mutating operations on the same stack
41
+ - **Provider-agnostic** — Works with any Pulumi provider (AWS, GCP, Azure, Kubernetes, etc.)
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ pip install pulice
47
+ ```
48
+
49
+ With optional extras:
50
+
51
+ ```bash
52
+ pip install pulice[api] # FastAPI server + Huey worker
53
+ pip install pulice[celery] # Celery backend for distributed deployments
54
+ pip install pulice[aws] # AWS provider
55
+ ```
56
+
57
+ ## Quick Start
58
+
59
+ Define a component:
60
+
61
+ ```python
62
+ from pulice import ComponentArgs, ManagedComponent
63
+ from pydantic import Field
64
+ import pulumi
65
+ import pulumi_aws as aws
66
+
67
+ class BucketArgs(ComponentArgs):
68
+ region: str = Field(description="AWS region")
69
+
70
+ class Bucket(ManagedComponent):
71
+ args_model = BucketArgs
72
+
73
+ def __init__(self, name: str, args: BucketArgs, opts=None, **kwargs):
74
+ super().__init__("pulice:aws:Bucket", name, {}, opts)
75
+ aws.s3.BucketV2(f"{name}-bucket", opts=pulumi.ResourceOptions(parent=self))
76
+ ```
77
+
78
+ Register and run:
79
+
80
+ ```python
81
+ from typer import Typer
82
+ from pulice import PuliceCLI
83
+
84
+ app = Typer()
85
+ cli = PuliceCLI(app)
86
+ cli.register_component(Bucket, name="bucket")
87
+
88
+ if __name__ == "__main__":
89
+ cli()
90
+ ```
91
+
92
+ Use it:
93
+
94
+ ```bash
95
+ pulice tenant create --name dev
96
+ pulice bucket create --name my-data --region eu-west-1 --tenant dev --passphrase secret
97
+ pulice bucket list --tenant dev
98
+ pulice bucket delete --stack-reference <ref> --tenant dev --passphrase secret
99
+ ```
100
+
101
+ ## HTTP API
102
+
103
+ ```bash
104
+ pip install pulice[api]
105
+ uvicorn pulice.api:create_api --factory --host 0.0.0.0 --port 8000
106
+ huey_consumer pulice.worker.huey -w 2 -k process
107
+ ```
108
+
109
+ Submit operations asynchronously:
110
+
111
+ ```bash
112
+ curl -X POST http://localhost:8000/stacks/operations \
113
+ -H "Content-Type: application/json" \
114
+ -d '{"component_class": "myapp.Bucket", "operation": "create", "tenant": "dev", "passphrase": "secret", "args": {"name": "my-data", "region": "eu-west-1"}}'
115
+ ```
116
+
117
+ ## Documentation
118
+
119
+ Full documentation is available at the [project site](https://your-org.github.io/pulice/) or can be built locally:
120
+
121
+ ```bash
122
+ pip install pulice[docs]
123
+ zensical serve
124
+ ```
125
+
126
+ ## License
127
+
128
+ [MIT](LICENSE) — Mike Fischer and Dani Vela Calderón
pulice-0.1.0/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # Pulice
2
+
3
+ A Python framework for managing infrastructure-as-code components via [Pulumi](https://www.pulumi.com/) with tenant isolation, async execution, and pluggable backends.
4
+
5
+ ## Features
6
+
7
+ - **Component model** — Define cloud resources as `ManagedComponent` subclasses with Pydantic-validated inputs
8
+ - **Tenant isolation** — Named boundaries ensure stacks belonging to different environments never collide
9
+ - **9 lifecycle operations** — create, read, update, delete, refresh, list, status, export, import
10
+ - **CLI + HTTP API** — Synchronous CLI for interactive use; async FastAPI server for automation
11
+ - **Pluggable task backends** — Huey (SQLite, zero-dep) or Celery (Redis, distributed)
12
+ - **Passphrase-protected stacks** — scrypt-hashed passphrases validated before any Pulumi call
13
+ - **Advisory locking** — Prevents concurrent mutating operations on the same stack
14
+ - **Provider-agnostic** — Works with any Pulumi provider (AWS, GCP, Azure, Kubernetes, etc.)
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pip install pulice
20
+ ```
21
+
22
+ With optional extras:
23
+
24
+ ```bash
25
+ pip install pulice[api] # FastAPI server + Huey worker
26
+ pip install pulice[celery] # Celery backend for distributed deployments
27
+ pip install pulice[aws] # AWS provider
28
+ ```
29
+
30
+ ## Quick Start
31
+
32
+ Define a component:
33
+
34
+ ```python
35
+ from pulice import ComponentArgs, ManagedComponent
36
+ from pydantic import Field
37
+ import pulumi
38
+ import pulumi_aws as aws
39
+
40
+ class BucketArgs(ComponentArgs):
41
+ region: str = Field(description="AWS region")
42
+
43
+ class Bucket(ManagedComponent):
44
+ args_model = BucketArgs
45
+
46
+ def __init__(self, name: str, args: BucketArgs, opts=None, **kwargs):
47
+ super().__init__("pulice:aws:Bucket", name, {}, opts)
48
+ aws.s3.BucketV2(f"{name}-bucket", opts=pulumi.ResourceOptions(parent=self))
49
+ ```
50
+
51
+ Register and run:
52
+
53
+ ```python
54
+ from typer import Typer
55
+ from pulice import PuliceCLI
56
+
57
+ app = Typer()
58
+ cli = PuliceCLI(app)
59
+ cli.register_component(Bucket, name="bucket")
60
+
61
+ if __name__ == "__main__":
62
+ cli()
63
+ ```
64
+
65
+ Use it:
66
+
67
+ ```bash
68
+ pulice tenant create --name dev
69
+ pulice bucket create --name my-data --region eu-west-1 --tenant dev --passphrase secret
70
+ pulice bucket list --tenant dev
71
+ pulice bucket delete --stack-reference <ref> --tenant dev --passphrase secret
72
+ ```
73
+
74
+ ## HTTP API
75
+
76
+ ```bash
77
+ pip install pulice[api]
78
+ uvicorn pulice.api:create_api --factory --host 0.0.0.0 --port 8000
79
+ huey_consumer pulice.worker.huey -w 2 -k process
80
+ ```
81
+
82
+ Submit operations asynchronously:
83
+
84
+ ```bash
85
+ curl -X POST http://localhost:8000/stacks/operations \
86
+ -H "Content-Type: application/json" \
87
+ -d '{"component_class": "myapp.Bucket", "operation": "create", "tenant": "dev", "passphrase": "secret", "args": {"name": "my-data", "region": "eu-west-1"}}'
88
+ ```
89
+
90
+ ## Documentation
91
+
92
+ Full documentation is available at the [project site](https://your-org.github.io/pulice/) or can be built locally:
93
+
94
+ ```bash
95
+ pip install pulice[docs]
96
+ zensical serve
97
+ ```
98
+
99
+ ## License
100
+
101
+ [MIT](LICENSE) — Mike Fischer and Dani Vela Calderón
@@ -0,0 +1,112 @@
1
+ [project]
2
+ name = "pulice"
3
+ version = "0.1.0"
4
+ description = "A framework for managing infrastructure-as-code components via Pulumi with tenant isolation, async execution, and pluggable backends."
5
+ readme = "README.md"
6
+ authors = [
7
+ {name = "mk.fshr"},
8
+ ]
9
+ license = "MIT"
10
+ requires-python = ">=3.13"
11
+ dependencies = [
12
+ "pulumi>=3.215.0",
13
+ "pydantic>=2.12.5",
14
+ "typer>=0.21.1",
15
+ ]
16
+ classifiers = [
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "License :: OSI Approved :: MIT License",
21
+ ]
22
+
23
+ [tool.pyrefly]
24
+ project-includes = ["src"]
25
+ project-excludes = ["**/*venv/**/*", "examples/", "tests/"]
26
+
27
+ [project.optional-dependencies]
28
+ aws = ["pulumi-aws>=7.15.0"]
29
+ api = [
30
+ "fastapi>=0.115.0",
31
+ "uvicorn[standard]>=0.30.0",
32
+ "huey>=2.5.0",
33
+ ]
34
+ celery = [
35
+ "celery[redis]>=5.4.0",
36
+ ]
37
+ admin = [
38
+ "textual>=3.0.0",
39
+ "textual-serve>=1.1.0",
40
+ ]
41
+
42
+ [dependency-groups]
43
+ dev = [
44
+ "pytest>=8.3.4",
45
+ "pytest-cov>=6.0.0",
46
+ "fastapi>=0.115.0",
47
+ "huey>=2.5.0",
48
+ "httpx>=0.28.0",
49
+ ]
50
+ docs = [
51
+ "zensical>=0.0.37",
52
+ "mkdocstrings[python]>=0.29.0",
53
+ ]
54
+
55
+ [build-system]
56
+ requires = ["uv_build>=0.9.21,<0.10.0"]
57
+ build-backend = "uv_build"
58
+
59
+ [tool.pytest.ini_options]
60
+ minversion = "8.0"
61
+ addopts = ["-ra", "-q"]
62
+ testpaths = ["tests"]
63
+
64
+ [tool.uv.workspace]
65
+ members = [
66
+ "examples/bootstrap-usecase",
67
+ "examples/static-website",
68
+ "examples/api-postgres",
69
+ "examples/kubernetes-deploy",
70
+ ]
71
+
72
+ [tool.ruff]
73
+ line-length = 100
74
+ indent-width = 4
75
+
76
+ [tool.ruff.format]
77
+ quote-style = "single"
78
+ indent-style = "space"
79
+ docstring-code-format = true
80
+ exclude = [
81
+ "build",
82
+ "dist",
83
+ ".eggs",
84
+ ]
85
+
86
+ [tool.ruff.lint]
87
+ ignore = [
88
+ "N801", # class names should use CapWords convention (class names)
89
+ "N812", # lowercase imported as non-lowercase (pyspark.sql functions as F)
90
+ "N818" # Exception name should have Error suffix
91
+ ]
92
+
93
+ select = [
94
+ "E",
95
+ "F",
96
+ "N",
97
+ "W",
98
+ "I001", # isort configuration for ruff "--profile", "black"
99
+ ]
100
+
101
+
102
+ [tool.ruff.lint.isort.sections]
103
+ typing = ["typing"]
104
+
105
+
106
+ [tool.ruff.lint.isort]
107
+ no-lines-before = ["future", "standard-library", "third-party", "first-party", "local-folder", "typing"]
108
+ section-order = ["future", "standard-library", "third-party", "first-party", "local-folder", "typing"]
109
+
110
+ [tool.bandit]
111
+ skips = ["B601", "B608", "B101", "B110"] # B101 asserts used
112
+ exclude_dirs = ["tests/", "examples/"]
@@ -0,0 +1,19 @@
1
+ __version__ = '0.1.0'
2
+
3
+ from pulice.cli.app import PuliceCLI
4
+ from pulice.core.base import ComponentArgs, ManagedComponent
5
+ from pulice.core.controllers import ComponentController, WorkspaceController
6
+ from pulice.core.protocol import PuliceApp
7
+ from pulice.core.tasks import TaskBackend, TaskResult, TaskStatus
8
+
9
+ __all__ = [
10
+ 'ComponentArgs',
11
+ 'ComponentController',
12
+ 'ManagedComponent',
13
+ 'PuliceApp',
14
+ 'PuliceCLI',
15
+ 'TaskBackend',
16
+ 'TaskResult',
17
+ 'TaskStatus',
18
+ 'WorkspaceController',
19
+ ]
@@ -0,0 +1,13 @@
1
+ """Pulice Admin TUI — terminal and browser dashboard."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def create_admin_app(
7
+ state_dir: str | None = None,
8
+ refresh_interval: int = 5,
9
+ ) -> 'PuliceAdmin': # noqa: F821 # pyrefly: ignore
10
+ """Create and return the admin TUI application."""
11
+ from pulice.admin.app import PuliceAdmin
12
+
13
+ return PuliceAdmin(state_dir=state_dir, refresh_interval=refresh_interval)
@@ -0,0 +1,8 @@
1
+ """Allow running the admin TUI with: python -m pulice.admin"""
2
+
3
+ import sys
4
+ from pulice.admin.app import PuliceAdmin
5
+
6
+ state_dir = sys.argv[1] if len(sys.argv) > 1 else None
7
+ app = PuliceAdmin(state_dir=state_dir)
8
+ app.run()
@@ -0,0 +1,90 @@
1
+ """PuliceAdmin — main Textual application for the admin TUI."""
2
+
3
+ from __future__ import annotations
4
+ from pathlib import Path
5
+ from textual.app import App, ComposeResult
6
+ from textual.binding import Binding
7
+ from textual.containers import Vertical
8
+ from textual.widgets import Footer, Header, TabbedContent, TabPane
9
+ from pulice.admin.data import AdminDataSource
10
+ from pulice.admin.screens.dashboard import DashboardScreen
11
+ from pulice.admin.screens.stacks import StacksScreen
12
+ from pulice.admin.screens.system import SystemScreen
13
+ from pulice.admin.screens.tasks import TasksScreen
14
+ from pulice.admin.screens.tenants import TenantsScreen
15
+
16
+ CSS_PATH = Path(__file__).parent / 'styles' / 'app.tcss'
17
+
18
+
19
+ class PuliceAdmin(App):
20
+ """Pulice administrative dashboard TUI."""
21
+
22
+ TITLE = 'Pulice Admin'
23
+ CSS_PATH = CSS_PATH
24
+ BINDINGS = [
25
+ Binding('1', "switch_tab('dashboard')", 'Dashboard', show=False),
26
+ Binding('2', "switch_tab('tenants')", 'Tenants', show=False),
27
+ Binding('3', "switch_tab('stacks')", 'Stacks', show=False),
28
+ Binding('4', "switch_tab('tasks')", 'Tasks', show=False),
29
+ Binding('5', "switch_tab('system')", 'System', show=False),
30
+ Binding('r', 'force_refresh', 'Refresh'),
31
+ Binding('q', 'quit', 'Quit'),
32
+ ]
33
+
34
+ def __init__(
35
+ self,
36
+ state_dir: str | None = None,
37
+ refresh_interval: int = 5,
38
+ **kwargs,
39
+ ) -> None:
40
+ super().__init__(**kwargs)
41
+ self._state_dir = state_dir
42
+ self._refresh_interval = refresh_interval
43
+ self._data = AdminDataSource(state_dir=state_dir)
44
+
45
+ def compose(self) -> ComposeResult:
46
+ yield Header()
47
+ with Vertical(id='main'):
48
+ with TabbedContent(id='tabs'):
49
+ with TabPane('Dashboard', id='dashboard'):
50
+ yield DashboardScreen(self._data)
51
+ with TabPane('Tenants', id='tenants'):
52
+ yield TenantsScreen(self._data)
53
+ with TabPane('Stacks', id='stacks'):
54
+ yield StacksScreen(self._data)
55
+ with TabPane('Tasks', id='tasks'):
56
+ yield TasksScreen(self._data)
57
+ with TabPane('System', id='system'):
58
+ yield SystemScreen(self._data)
59
+ yield Footer()
60
+
61
+ def on_mount(self) -> None:
62
+ self._do_refresh()
63
+ if self._refresh_interval > 0:
64
+ self.set_interval(self._refresh_interval, self._do_refresh)
65
+
66
+ def _do_refresh(self) -> None:
67
+ self.run_worker(self._refresh_worker, exclusive=True)
68
+
69
+ async def _refresh_worker(self) -> None:
70
+ for screen_cls in (
71
+ DashboardScreen,
72
+ TenantsScreen,
73
+ StacksScreen,
74
+ TasksScreen,
75
+ SystemScreen,
76
+ ):
77
+ try:
78
+ widget = self.query_one(screen_cls) # pyrefly: ignore
79
+ widget.refresh_data()
80
+ except Exception: # noseq
81
+ pass
82
+
83
+ def action_switch_tab(self, tab_id: str) -> None:
84
+ tabs = self.query_one('#tabs', TabbedContent)
85
+ tabs.active = tab_id # pyrefly: ignore
86
+ self._do_refresh()
87
+
88
+ def action_force_refresh(self) -> None:
89
+ self._do_refresh()
90
+ self.notify('Refreshed')
@@ -0,0 +1,112 @@
1
+ """Read-only data provider for the admin TUI."""
2
+
3
+ from __future__ import annotations
4
+ import os
5
+ import sys
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+ import pulice
9
+ from pulice.core.stack import (
10
+ LocalStackReferenceStore,
11
+ SqliteBackendStorage,
12
+ StackReference,
13
+ Tenant,
14
+ )
15
+ from pulice.core.tasks import TaskBackend, TaskResult
16
+ from typing import Any
17
+
18
+
19
+ class AdminDataSource:
20
+ """Read-only data provider for the admin TUI.
21
+
22
+ Wraps existing storage APIs to provide data for all admin screens.
23
+ """
24
+
25
+ def __init__(self, state_dir: str | None = None) -> None:
26
+ self._state_dir = state_dir or os.getenv('PULICE_STATE_DIR')
27
+ self._storage = SqliteBackendStorage(root_dir=self._state_dir)
28
+ self._references = LocalStackReferenceStore(root_dir=self._state_dir)
29
+ self._task_backend: TaskBackend | None = None
30
+
31
+ def _get_task_backend(self) -> TaskBackend:
32
+ if self._task_backend is None:
33
+ from pulice.core.tasks import get_task_backend
34
+
35
+ self._task_backend = get_task_backend(state_dir=self._state_dir)
36
+ return self._task_backend
37
+
38
+ @property
39
+ def state_dir(self) -> str:
40
+ return str(self._storage._root)
41
+
42
+ def get_tenants(self) -> list[Tenant]:
43
+ return self._storage.list_tenants()
44
+
45
+ def get_stacks(self, tenant_id: str | None = None) -> list[dict[str, Any]]:
46
+ return self._storage.list_stacks(tenant_id=tenant_id)
47
+
48
+ def get_stack_references(self, tenant_id: str | None = None) -> list[StackReference]:
49
+ return self._references.list(tenant_id=tenant_id)
50
+
51
+ def get_locks(self) -> list[dict[str, Any]]:
52
+ with self._storage._connect() as conn:
53
+ rows = conn.execute(
54
+ 'SELECT stack_name, holder, operation, locked_at FROM stack_locks'
55
+ ).fetchall()
56
+ now = datetime.now(timezone.utc)
57
+ results = []
58
+ for r in rows:
59
+ locked_at = datetime.fromisoformat(r[3])
60
+ age_seconds = (now - locked_at).total_seconds()
61
+ results.append(
62
+ {
63
+ 'stack_name': r[0],
64
+ 'holder': r[1],
65
+ 'operation': r[2],
66
+ 'locked_at': r[3],
67
+ 'age_seconds': age_seconds,
68
+ }
69
+ )
70
+ return results
71
+
72
+ def get_task_status(self, task_id: str) -> TaskResult:
73
+ return self._get_task_backend().get_status(task_id)
74
+
75
+ def get_system_info(self) -> dict[str, Any]:
76
+ state_path = Path(self.state_dir)
77
+ db_path = state_path / 'pulice_stacks.sqlite3'
78
+
79
+ total_size = sum(f.stat().st_size for f in state_path.rglob('*') if f.is_file())
80
+ db_size = db_path.stat().st_size if db_path.exists() else 0
81
+
82
+ tenants = self.get_tenants()
83
+ stacks = self.get_stacks()
84
+ locks = self.get_locks()
85
+
86
+ backend_type = os.getenv('PULICE_TASK_BACKEND', 'huey')
87
+
88
+ return {
89
+ 'version': pulice.__version__,
90
+ 'python': sys.version,
91
+ 'state_dir': self.state_dir,
92
+ 'state_dir_size': total_size,
93
+ 'db_size': db_size,
94
+ 'task_backend': backend_type,
95
+ 'tenant_count': len(tenants),
96
+ 'stack_count': len(stacks),
97
+ 'lock_count': len(locks),
98
+ 'locks': locks,
99
+ }
100
+
101
+ def delete_tenant(self, name: str) -> None:
102
+ self._storage.delete_tenant(name)
103
+
104
+ def release_lock(self, stack_name: str) -> None:
105
+ with self._storage._connect() as conn:
106
+ conn.execute('DELETE FROM stack_locks WHERE stack_name = ?', (stack_name,))
107
+
108
+ def cancel_task(self, task_id: str) -> bool:
109
+ return self._get_task_backend().cancel(task_id)
110
+
111
+ def retry_task(self, task_id: str) -> str:
112
+ return self._get_task_backend().retry(task_id)
@@ -0,0 +1,15 @@
1
+ """Admin TUI screens."""
2
+
3
+ from pulice.admin.screens.dashboard import DashboardScreen
4
+ from pulice.admin.screens.stacks import StacksScreen
5
+ from pulice.admin.screens.system import SystemScreen
6
+ from pulice.admin.screens.tasks import TasksScreen
7
+ from pulice.admin.screens.tenants import TenantsScreen
8
+
9
+ __all__ = [
10
+ 'DashboardScreen',
11
+ 'TenantsScreen',
12
+ 'StacksScreen',
13
+ 'TasksScreen',
14
+ 'SystemScreen',
15
+ ]
@@ -0,0 +1,61 @@
1
+ """Dashboard screen — summary view with key metrics."""
2
+
3
+ from __future__ import annotations
4
+ from textual.app import ComposeResult
5
+ from textual.containers import Horizontal, Vertical
6
+ from textual.widgets import Static
7
+ from pulice.admin.widgets.stat_card import StatCard
8
+
9
+ if __name__ != '__main__':
10
+ from typing import TYPE_CHECKING
11
+
12
+ if TYPE_CHECKING:
13
+ from pulice.admin.data import AdminDataSource
14
+
15
+
16
+ class DashboardScreen(Static):
17
+ """Summary dashboard showing key metrics at a glance."""
18
+
19
+ DEFAULT_CSS = """
20
+ DashboardScreen {
21
+ height: 1fr;
22
+ padding: 1 2;
23
+ }
24
+ DashboardScreen .metrics-row {
25
+ height: 7;
26
+ margin-bottom: 1;
27
+ }
28
+ DashboardScreen .info-section {
29
+ height: auto;
30
+ padding: 1;
31
+ }
32
+ """
33
+
34
+ def __init__(self, data_source: 'AdminDataSource') -> None:
35
+ super().__init__()
36
+ self._data = data_source
37
+
38
+ def compose(self) -> ComposeResult:
39
+ with Horizontal(classes='metrics-row'):
40
+ yield StatCard('Tenants', '0', id='stat-tenants')
41
+ yield StatCard('Stacks', '0', id='stat-stacks')
42
+ yield StatCard('Active Locks', '0', id='stat-locks')
43
+ with Horizontal(classes='metrics-row'):
44
+ yield StatCard('Pending Tasks', '—', id='stat-pending')
45
+ yield StatCard('Running Tasks', '—', id='stat-running')
46
+ yield StatCard('Failed Tasks', '—', id='stat-failed')
47
+ with Vertical(classes='info-section'):
48
+ yield Static(id='system-info')
49
+
50
+ def refresh_data(self) -> None:
51
+ info = self._data.get_system_info()
52
+ self.query_one('#stat-tenants', StatCard).update_value(str(info['tenant_count']))
53
+ self.query_one('#stat-stacks', StatCard).update_value(str(info['stack_count']))
54
+ self.query_one('#stat-locks', StatCard).update_value(str(info['lock_count']))
55
+
56
+ info_text = (
57
+ f'[bold]Pulice[/bold] v{info["version"]} | '
58
+ f'Backend: {info["task_backend"]} | '
59
+ f'State: {info["state_dir"]}'
60
+ )
61
+ self.query_one('#system-info', Static).update(info_text)