hexastack-cli 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.
- hexastack_cli-0.1.0/PKG-INFO +145 -0
- hexastack_cli-0.1.0/README.md +129 -0
- hexastack_cli-0.1.0/pyproject.toml +36 -0
- hexastack_cli-0.1.0/pyproject.toml.orig +35 -0
- hexastack_cli-0.1.0/src/hexastack_cli/__init__.py +6 -0
- hexastack_cli-0.1.0/src/hexastack_cli/adapters/__init__.py +13 -0
- hexastack_cli-0.1.0/src/hexastack_cli/adapters/app.py +77 -0
- hexastack_cli-0.1.0/src/hexastack_cli/adapters/presenter.py +137 -0
- hexastack_cli-0.1.0/src/hexastack_cli/adapters/routing.py +396 -0
- hexastack_cli-0.1.0/src/hexastack_cli/infra/__init__.py +29 -0
- hexastack_cli-0.1.0/src/hexastack_cli/infra/autodiscovery.py +215 -0
- hexastack_cli-0.1.0/src/hexastack_cli/infra/bootstrap.py +80 -0
- hexastack_cli-0.1.0/src/hexastack_cli/infra/config.py +43 -0
- hexastack_cli-0.1.0/src/hexastack_cli/infra/decorators.py +219 -0
- hexastack_cli-0.1.0/src/hexastack_cli/py.typed +0 -0
- hexastack_cli-0.1.0/src/hexastack_cli/testing/__init__.py +7 -0
- hexastack_cli-0.1.0/src/hexastack_cli/testing/narrator.py +195 -0
- hexastack_cli-0.1.0/src/hexastack_cli/testing/terminal.py +328 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hexastack-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Hexastack CLI presentation adapter with Typer and Rich
|
|
5
|
+
Author: Richard West
|
|
6
|
+
Author-email: Richard West <dopplereffect.us@gmail.com>
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
Requires-Dist: hexastack-core
|
|
9
|
+
Requires-Dist: hexastack-cqrs
|
|
10
|
+
Requires-Dist: rich>=13.8.0
|
|
11
|
+
Requires-Dist: typer>=0.27.1
|
|
12
|
+
Requires-Dist: playwright>=1.49.0 ; extra == 'testing'
|
|
13
|
+
Requires-Python: >=3.13
|
|
14
|
+
Provides-Extra: testing
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
|
|
19
|
+
# hexastack-cli
|
|
20
|
+
|
|
21
|
+
> Typer and Rich presentation adapter for Hexastack: nested commands, aliases, piped outputs, and CQRS dispatching.
|
|
22
|
+
|
|
23
|
+
[](https://pypi.org/project/hexastack-cli/)
|
|
24
|
+
[](https://www.python.org/downloads/)
|
|
25
|
+
[](https://codecov.io/github/TheTrueSCU/hexastack)
|
|
26
|
+
[](../../LICENSE)
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 1. Overview & Capabilities
|
|
30
|
+
|
|
31
|
+
`hexastack-cli` turns Hexastack CQRS commands and queries into intuitive, modern CLI applications:
|
|
32
|
+
|
|
33
|
+
- **Nested Command Hierarchies**: Nest subcommands naturally (`app user create`, `app db migrate`) using `@cli_group`.
|
|
34
|
+
- **Command Aliases**: Register multiple aliases for the same action (`app user new == app user create`).
|
|
35
|
+
- **Feature Flag Gating**: Gate CLI commands dynamically with `@feature_flag_command(...)` and `@cli_command(..., feature_flag=...)`.
|
|
36
|
+
- **Rich Formatted & CI-Friendly Output**: Beautiful tables, panels, and spinners for interactive terminals; clean text/JSON streaming for CI/CD pipelines.
|
|
37
|
+
- **Direct CQRS Dispatching**: Declaratively expose domain commands (`@cli_command`) and queries (`@cli_query`) with automatic parameter parsing and validation.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 2. Package Anatomy & Key Components
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
hexastack_cli/
|
|
45
|
+
├── domain/ # CliContext, OutputFormat enum
|
|
46
|
+
├── adapters/ # create_cli_app, Rich presenters, Typer command runners
|
|
47
|
+
└── infra/ # CliBootstrapper (order=30), @cli_command, @cli_query, @cli_group, @feature_flag_command
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Key Exports
|
|
51
|
+
|
|
52
|
+
| Category | Exports |
|
|
53
|
+
|---|---|
|
|
54
|
+
| **Application Factory** | `create_cli_app`, `CliBootstrapper` (order=30) |
|
|
55
|
+
| **Decorators** | `@cli_command`, `@cli_query`, `@cli_group`, `@feature_flag_command` |
|
|
56
|
+
| **Presenters** | `RichTerminalPresenter`, `ConsolePresenter`, `TablePresenter`, `JsonPresenter` |
|
|
57
|
+
| **Testing & Demo Narration** | `CliNarrator`, `TerminalEvent` |
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 3. Monorepo & Sibling Relationships
|
|
62
|
+
|
|
63
|
+
```mermaid
|
|
64
|
+
graph TD
|
|
65
|
+
subgraph UserInvocation ["CLI Invocations"]
|
|
66
|
+
INV["Terminal Commands & Scripts"]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
subgraph CliLayer ["hexastack-cli"]
|
|
70
|
+
TYPER["Typer Application (Nested Groups & Aliases)"]
|
|
71
|
+
RICH["Rich Presenters (Tables, Panels, JSON)"]
|
|
72
|
+
SCAN["CLI Decorator Scanner (@cli_command, @cli_query)"]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
subgraph CQRSExecution ["hexastack-cqrs"]
|
|
76
|
+
CBUS["CommandBusPort"]
|
|
77
|
+
QBUS["QueryBusPort"]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
subgraph Kernel ["hexastack-core"]
|
|
81
|
+
DI["rodi.Container"]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
INV --> TYPER
|
|
85
|
+
TYPER --> SCAN
|
|
86
|
+
SCAN -->|dispatches to| CBUS
|
|
87
|
+
SCAN -->|dispatches to| QBUS
|
|
88
|
+
SCAN --> RICH
|
|
89
|
+
|
|
90
|
+
TYPER -. resolves buses from DI .-> DI
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Explicit Dependencies (Direct)
|
|
94
|
+
- `hexastack-core`: Core kernel, DI container, and ports.
|
|
95
|
+
- `hexastack-cqrs`: `CommandBusPort` and `QueryBusPort` for message dispatching.
|
|
96
|
+
- `typer>=0.27.1`: CLI command parser and shell completion.
|
|
97
|
+
- `rich>=15.0.0`: Terminal formatting, tables, and colors.
|
|
98
|
+
|
|
99
|
+
### Implied / Behavioral Relationships (DI-Mediated)
|
|
100
|
+
- **CQRS Integration**: Dispatches CLI argument payloads directly into the application's command and query buses.
|
|
101
|
+
- **Umbrella CLI**: Consumed by the `hexastack` umbrella package to power diagnostic commands (`hexastack info`, `hexastack inspect registry`, `hexastack demo ping`).
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 4. Installation
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# Standalone install
|
|
109
|
+
pip install hexastack-cli
|
|
110
|
+
|
|
111
|
+
# Via umbrella package
|
|
112
|
+
pip install "hexastack[cli]"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 5. Quickstart Example
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from dataclasses import dataclass
|
|
121
|
+
from hexastack_core.infra.bootstrap import bootstrap
|
|
122
|
+
from hexastack_cqrs.domain.query import Query
|
|
123
|
+
from hexastack_cqrs.infra.decorators import query_handler
|
|
124
|
+
from hexastack_cli.infra.decorators import cli_query, cli_group
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass(frozen=True)
|
|
128
|
+
class CheckStatusQuery(Query):
|
|
129
|
+
service_name: str
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@query_handler(CheckStatusQuery)
|
|
133
|
+
class CheckStatusHandler:
|
|
134
|
+
def __call__(self, qry: CheckStatusQuery) -> dict:
|
|
135
|
+
return {"service": qry.service_name, "status": "ONLINE"}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# Expose query as a CLI command
|
|
139
|
+
cli_query("status", aliases=["st", "health"], help="Check status of a service")(
|
|
140
|
+
CheckStatusQuery
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
runtime = bootstrap(packages_to_scan=[__name__])
|
|
144
|
+
cli_app = runtime.get("cli_app")
|
|
145
|
+
```
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# hexastack-cli
|
|
4
|
+
|
|
5
|
+
> Typer and Rich presentation adapter for Hexastack: nested commands, aliases, piped outputs, and CQRS dispatching.
|
|
6
|
+
|
|
7
|
+
[](https://pypi.org/project/hexastack-cli/)
|
|
8
|
+
[](https://www.python.org/downloads/)
|
|
9
|
+
[](https://codecov.io/github/TheTrueSCU/hexastack)
|
|
10
|
+
[](../../LICENSE)
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 1. Overview & Capabilities
|
|
14
|
+
|
|
15
|
+
`hexastack-cli` turns Hexastack CQRS commands and queries into intuitive, modern CLI applications:
|
|
16
|
+
|
|
17
|
+
- **Nested Command Hierarchies**: Nest subcommands naturally (`app user create`, `app db migrate`) using `@cli_group`.
|
|
18
|
+
- **Command Aliases**: Register multiple aliases for the same action (`app user new == app user create`).
|
|
19
|
+
- **Feature Flag Gating**: Gate CLI commands dynamically with `@feature_flag_command(...)` and `@cli_command(..., feature_flag=...)`.
|
|
20
|
+
- **Rich Formatted & CI-Friendly Output**: Beautiful tables, panels, and spinners for interactive terminals; clean text/JSON streaming for CI/CD pipelines.
|
|
21
|
+
- **Direct CQRS Dispatching**: Declaratively expose domain commands (`@cli_command`) and queries (`@cli_query`) with automatic parameter parsing and validation.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 2. Package Anatomy & Key Components
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
hexastack_cli/
|
|
29
|
+
├── domain/ # CliContext, OutputFormat enum
|
|
30
|
+
├── adapters/ # create_cli_app, Rich presenters, Typer command runners
|
|
31
|
+
└── infra/ # CliBootstrapper (order=30), @cli_command, @cli_query, @cli_group, @feature_flag_command
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Key Exports
|
|
35
|
+
|
|
36
|
+
| Category | Exports |
|
|
37
|
+
|---|---|
|
|
38
|
+
| **Application Factory** | `create_cli_app`, `CliBootstrapper` (order=30) |
|
|
39
|
+
| **Decorators** | `@cli_command`, `@cli_query`, `@cli_group`, `@feature_flag_command` |
|
|
40
|
+
| **Presenters** | `RichTerminalPresenter`, `ConsolePresenter`, `TablePresenter`, `JsonPresenter` |
|
|
41
|
+
| **Testing & Demo Narration** | `CliNarrator`, `TerminalEvent` |
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 3. Monorepo & Sibling Relationships
|
|
46
|
+
|
|
47
|
+
```mermaid
|
|
48
|
+
graph TD
|
|
49
|
+
subgraph UserInvocation ["CLI Invocations"]
|
|
50
|
+
INV["Terminal Commands & Scripts"]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
subgraph CliLayer ["hexastack-cli"]
|
|
54
|
+
TYPER["Typer Application (Nested Groups & Aliases)"]
|
|
55
|
+
RICH["Rich Presenters (Tables, Panels, JSON)"]
|
|
56
|
+
SCAN["CLI Decorator Scanner (@cli_command, @cli_query)"]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
subgraph CQRSExecution ["hexastack-cqrs"]
|
|
60
|
+
CBUS["CommandBusPort"]
|
|
61
|
+
QBUS["QueryBusPort"]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
subgraph Kernel ["hexastack-core"]
|
|
65
|
+
DI["rodi.Container"]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
INV --> TYPER
|
|
69
|
+
TYPER --> SCAN
|
|
70
|
+
SCAN -->|dispatches to| CBUS
|
|
71
|
+
SCAN -->|dispatches to| QBUS
|
|
72
|
+
SCAN --> RICH
|
|
73
|
+
|
|
74
|
+
TYPER -. resolves buses from DI .-> DI
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Explicit Dependencies (Direct)
|
|
78
|
+
- `hexastack-core`: Core kernel, DI container, and ports.
|
|
79
|
+
- `hexastack-cqrs`: `CommandBusPort` and `QueryBusPort` for message dispatching.
|
|
80
|
+
- `typer>=0.27.1`: CLI command parser and shell completion.
|
|
81
|
+
- `rich>=15.0.0`: Terminal formatting, tables, and colors.
|
|
82
|
+
|
|
83
|
+
### Implied / Behavioral Relationships (DI-Mediated)
|
|
84
|
+
- **CQRS Integration**: Dispatches CLI argument payloads directly into the application's command and query buses.
|
|
85
|
+
- **Umbrella CLI**: Consumed by the `hexastack` umbrella package to power diagnostic commands (`hexastack info`, `hexastack inspect registry`, `hexastack demo ping`).
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 4. Installation
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Standalone install
|
|
93
|
+
pip install hexastack-cli
|
|
94
|
+
|
|
95
|
+
# Via umbrella package
|
|
96
|
+
pip install "hexastack[cli]"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 5. Quickstart Example
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from dataclasses import dataclass
|
|
105
|
+
from hexastack_core.infra.bootstrap import bootstrap
|
|
106
|
+
from hexastack_cqrs.domain.query import Query
|
|
107
|
+
from hexastack_cqrs.infra.decorators import query_handler
|
|
108
|
+
from hexastack_cli.infra.decorators import cli_query, cli_group
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass(frozen=True)
|
|
112
|
+
class CheckStatusQuery(Query):
|
|
113
|
+
service_name: str
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@query_handler(CheckStatusQuery)
|
|
117
|
+
class CheckStatusHandler:
|
|
118
|
+
def __call__(self, qry: CheckStatusQuery) -> dict:
|
|
119
|
+
return {"service": qry.service_name, "status": "ONLINE"}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# Expose query as a CLI command
|
|
123
|
+
cli_query("status", aliases=["st", "health"], help="Check status of a service")(
|
|
124
|
+
CheckStatusQuery
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
runtime = bootstrap(packages_to_scan=[__name__])
|
|
128
|
+
cli_app = runtime.get("cli_app")
|
|
129
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hexastack-cli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Hexastack CLI presentation adapter with Typer and Rich"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
requires-python = ">=3.13"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"hexastack-core",
|
|
10
|
+
"hexastack-cqrs",
|
|
11
|
+
"rich>=13.8.0",
|
|
12
|
+
"typer>=0.27.1",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[[project.authors]]
|
|
16
|
+
name = "Richard West"
|
|
17
|
+
email = "dopplereffect.us@gmail.com"
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
testing = ["playwright>=1.49.0"]
|
|
21
|
+
|
|
22
|
+
[project.entry-points."hexastack.bootstrappers"]
|
|
23
|
+
cli = "hexastack_cli.infra.bootstrap:CliBootstrapper"
|
|
24
|
+
|
|
25
|
+
[build-system]
|
|
26
|
+
requires = ["uv_build>=0.12.3,<0.13.0"]
|
|
27
|
+
build-backend = "uv_build"
|
|
28
|
+
|
|
29
|
+
[tool.uv.sources.hexastack-core]
|
|
30
|
+
workspace = true
|
|
31
|
+
|
|
32
|
+
[tool.uv.sources.hexastack-cqrs]
|
|
33
|
+
workspace = true
|
|
34
|
+
|
|
35
|
+
[tool.importlinter]
|
|
36
|
+
root_packages = ["hexastack_cli"]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hexastack-cli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Hexastack CLI presentation adapter with Typer and Rich"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Richard West", email = "dopplereffect.us@gmail.com" }
|
|
9
|
+
]
|
|
10
|
+
requires-python = ">=3.13"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"hexastack-core",
|
|
13
|
+
"hexastack-cqrs",
|
|
14
|
+
"rich>=13.8.0",
|
|
15
|
+
"typer>=0.27.1",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
testing = [
|
|
20
|
+
"playwright>=1.49.0",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.entry-points."hexastack.bootstrappers"]
|
|
24
|
+
cli = "hexastack_cli.infra.bootstrap:CliBootstrapper"
|
|
25
|
+
|
|
26
|
+
[build-system]
|
|
27
|
+
requires = ["uv_build>=0.12.3,<0.13.0"]
|
|
28
|
+
build-backend = "uv_build"
|
|
29
|
+
|
|
30
|
+
[tool.uv.sources]
|
|
31
|
+
hexastack-core = { workspace = true }
|
|
32
|
+
hexastack-cqrs = { workspace = true }
|
|
33
|
+
|
|
34
|
+
[tool.importlinter]
|
|
35
|
+
root_packages = ["hexastack_cli"]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from hexastack_cli.adapters.app import create_cli_app
|
|
2
|
+
from hexastack_cli.adapters.presenter import RichTerminalPresenter
|
|
3
|
+
from hexastack_cli.adapters.routing import (
|
|
4
|
+
register_cqrs_command,
|
|
5
|
+
register_cqrs_query,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"create_cli_app",
|
|
10
|
+
"register_cqrs_command",
|
|
11
|
+
"register_cqrs_query",
|
|
12
|
+
"RichTerminalPresenter",
|
|
13
|
+
]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rodi import Container
|
|
4
|
+
|
|
5
|
+
from hexastack_cli.adapters.presenter import RichTerminalPresenter
|
|
6
|
+
from hexastack_cli.infra.config import HexastackCliConfig
|
|
7
|
+
from hexastack_core.domain import Generic
|
|
8
|
+
from hexastack_cqrs.infra.pipeline import ExecutionPipeline
|
|
9
|
+
from hexastack_cqrs.infra.registries.presenter import PresenterRegistry
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"create_cli_app",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def create_cli_app(
|
|
17
|
+
config: HexastackCliConfig | None = None,
|
|
18
|
+
container: Container | None = None,
|
|
19
|
+
pipeline: ExecutionPipeline | None = None,
|
|
20
|
+
console: Console | None = None,
|
|
21
|
+
) -> typer.Typer:
|
|
22
|
+
"""Factory creating and configuring a Typer CLI application integrated with Hexastack.
|
|
23
|
+
|
|
24
|
+
Notes/Architectural Intent:
|
|
25
|
+
Assembles a Typer CLI instance configured with Rich formatting, binds container
|
|
26
|
+
and pipeline references, supports top-level --version, ensures multi-command dispatching,
|
|
27
|
+
and registers terminal presenters into the presenter registry.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
config: Optional HexastackCliConfig instance.
|
|
31
|
+
container: Optional rodi Container instance.
|
|
32
|
+
pipeline: Optional ExecutionPipeline instance.
|
|
33
|
+
console: Optional rich Console instance.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Configured Typer application instance.
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
None.
|
|
40
|
+
"""
|
|
41
|
+
cfg = config or HexastackCliConfig()
|
|
42
|
+
active_console = console or Console()
|
|
43
|
+
|
|
44
|
+
app = typer.Typer(
|
|
45
|
+
name=cfg.app_name,
|
|
46
|
+
help=cfg.help_text,
|
|
47
|
+
rich_markup_mode="rich" if cfg.rich_markup else None,
|
|
48
|
+
no_args_is_help=True,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def _version_callback(value: bool) -> None:
|
|
52
|
+
if value:
|
|
53
|
+
active_console.print(f"{cfg.app_name} {cfg.version}")
|
|
54
|
+
raise typer.Exit()
|
|
55
|
+
|
|
56
|
+
# Establish root callback to enforce multi-command structure and handle --version
|
|
57
|
+
@app.callback()
|
|
58
|
+
def _main(
|
|
59
|
+
version: bool | None = typer.Option(
|
|
60
|
+
None,
|
|
61
|
+
"--version",
|
|
62
|
+
"-v",
|
|
63
|
+
help="Show the application version and exit.",
|
|
64
|
+
callback=_version_callback,
|
|
65
|
+
is_eager=True,
|
|
66
|
+
),
|
|
67
|
+
) -> None:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
# Register RichTerminalPresenter if presenter registry is present in DI
|
|
71
|
+
if container is not None and PresenterRegistry in container:
|
|
72
|
+
pres_reg = container.resolve(PresenterRegistry)
|
|
73
|
+
terminal_presenter = RichTerminalPresenter(console=active_console)
|
|
74
|
+
pres_reg.register(Generic, "rich", terminal_presenter)
|
|
75
|
+
container.add_instance(terminal_presenter, declared_class=RichTerminalPresenter)
|
|
76
|
+
|
|
77
|
+
return app
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from hexastack_core.domain import Generic
|
|
12
|
+
from hexastack_core.ports.presenter import PresenterPort
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RichTerminalPresenter(PresenterPort):
|
|
16
|
+
"""Terminal presenter formatting domain models into stylized Rich panels, JSON, or plain text.
|
|
17
|
+
|
|
18
|
+
Notes/Architectural Intent:
|
|
19
|
+
Implements Presenter port for terminal and CI/pipe environments.
|
|
20
|
+
Supports structured JSON, plain line-oriented text, and interactive Rich tables.
|
|
21
|
+
Automatically respects NO_COLOR and non-TTY stdout streams.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
console: Console | None = None,
|
|
27
|
+
stderr_console: Console | None = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Initialize RichTerminalPresenter with optional stdout and stderr Console instances.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
console: Optional rich.console.Console instance for stdout.
|
|
33
|
+
stderr_console: Optional rich.console.Console instance for stderr.
|
|
34
|
+
"""
|
|
35
|
+
no_color = bool(os.environ.get("NO_COLOR"))
|
|
36
|
+
self._console = console or Console(no_color=no_color, highlight=not no_color)
|
|
37
|
+
self._stderr = stderr_console or Console(
|
|
38
|
+
stderr=True, no_color=no_color, highlight=not no_color
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def _present_json(self, data: Any) -> Any:
|
|
42
|
+
"""Render raw, pipe-friendly JSON to stdout."""
|
|
43
|
+
json_str = json.dumps(data, indent=2, default=str)
|
|
44
|
+
sys.stdout.write(json_str + "\n")
|
|
45
|
+
sys.stdout.flush()
|
|
46
|
+
return data
|
|
47
|
+
|
|
48
|
+
def _present_plain(self, data: Any) -> Any:
|
|
49
|
+
"""Render TSV/newline-delimited text to stdout for Unix pipeline processing."""
|
|
50
|
+
if isinstance(data, dict):
|
|
51
|
+
for k, v in data.items():
|
|
52
|
+
sys.stdout.write(f"{k}\t{v}\n")
|
|
53
|
+
elif isinstance(data, list):
|
|
54
|
+
for item in data:
|
|
55
|
+
sys.stdout.write(f"{item}\n")
|
|
56
|
+
else:
|
|
57
|
+
sys.stdout.write(f"{data}\n")
|
|
58
|
+
sys.stdout.flush()
|
|
59
|
+
return data
|
|
60
|
+
|
|
61
|
+
def _present_table(self, instance: Generic, data: Any) -> Any:
|
|
62
|
+
"""Render colorized Rich table/panel to stdout."""
|
|
63
|
+
if isinstance(data, dict):
|
|
64
|
+
table = Table(show_header=True, header_style="bold magenta")
|
|
65
|
+
table.add_column("Field", style="cyan")
|
|
66
|
+
table.add_column("Value", style="green")
|
|
67
|
+
for k, v in data.items():
|
|
68
|
+
val_str = (
|
|
69
|
+
json.dumps(v, indent=2) if isinstance(v, dict | list) else str(v)
|
|
70
|
+
)
|
|
71
|
+
table.add_row(str(k), val_str)
|
|
72
|
+
self._console.print(
|
|
73
|
+
Panel(table, title=type(instance).__name__, border_style="blue")
|
|
74
|
+
)
|
|
75
|
+
elif isinstance(data, list):
|
|
76
|
+
for item in data:
|
|
77
|
+
self._console.print(f"[cyan]•[/cyan] {item}")
|
|
78
|
+
else:
|
|
79
|
+
self._console.print(f"[bold green]{data}[/bold green]")
|
|
80
|
+
return data
|
|
81
|
+
|
|
82
|
+
def present(
|
|
83
|
+
self,
|
|
84
|
+
instance: Generic,
|
|
85
|
+
format_mode: str | None = None,
|
|
86
|
+
) -> Any:
|
|
87
|
+
"""Format and print a domain Generic instance to stdout based on requested format mode.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
instance: Domain Generic or DTO model instance.
|
|
91
|
+
format_mode: Optional format mode ('table', 'json', 'plain').
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
The presented raw data representation.
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
None.
|
|
98
|
+
"""
|
|
99
|
+
data = instance.model_dump() if isinstance(instance, BaseModel) else instance
|
|
100
|
+
|
|
101
|
+
mode = (format_mode or "table").lower()
|
|
102
|
+
|
|
103
|
+
if mode == "json":
|
|
104
|
+
return self._present_json(data)
|
|
105
|
+
if mode == "plain":
|
|
106
|
+
return self._present_plain(data)
|
|
107
|
+
return self._present_table(instance, data)
|
|
108
|
+
|
|
109
|
+
def print_error(self, message: str) -> None:
|
|
110
|
+
"""Print an error message to stderr.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
message: The error message string.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
None.
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
None.
|
|
120
|
+
"""
|
|
121
|
+
self._stderr.print(f"[bold red]Error:[/bold red] {message}")
|
|
122
|
+
|
|
123
|
+
def print_exception(self) -> None:
|
|
124
|
+
"""Print a rich formatted traceback to stderr for debugging.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
None.
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
None.
|
|
131
|
+
"""
|
|
132
|
+
self._stderr.print_exception(show_locals=False)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
__all__ = [
|
|
136
|
+
"RichTerminalPresenter",
|
|
137
|
+
]
|