aegis-stack 0.1.0__py3-none-any.whl
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.
Potentially problematic release.
This version of aegis-stack might be problematic. Click here for more details.
- aegis/__init__.py +5 -0
- aegis/__main__.py +374 -0
- aegis/core/CLAUDE.md +365 -0
- aegis/core/__init__.py +6 -0
- aegis/core/components.py +115 -0
- aegis/core/dependency_resolver.py +119 -0
- aegis/core/template_generator.py +163 -0
- aegis/templates/CLAUDE.md +306 -0
- aegis/templates/cookiecutter-aegis-project/cookiecutter.json +27 -0
- aegis/templates/cookiecutter-aegis-project/hooks/post_gen_project.py +172 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.dockerignore +71 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.env.example.j2 +70 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.gitignore +127 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Dockerfile +53 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Makefile +211 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/README.md.j2 +196 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/__init__.py +5 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/__init__.py +6 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/health.py +321 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/load_test.py +638 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/main.py +41 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/__init__.py +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/__init__.py +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/health.py +134 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/models.py.j2 +247 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/routing.py.j2 +14 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/tasks.py.j2 +596 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/hooks.py +133 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/main.py +16 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/middleware/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/middleware/cors.py +20 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/shutdown/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/shutdown/cleanup.py +14 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/component_health.py.j2 +190 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/__init__.py +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/core/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/core/theme.py +46 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/main.py +687 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/scheduler/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/scheduler/main.py +138 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/CLAUDE.md +213 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/__init__.py +6 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/constants.py.j2 +30 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/pools.py +78 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/load_test.py +48 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/media.py +41 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/system.py +36 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/registry.py +139 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/__init__.py +119 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/load_tasks.py +526 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/simple_system_tasks.py +32 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/system_tasks.py +279 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/config.py.j2 +119 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/constants.py +60 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/db.py +67 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/log.py +85 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/webserver.py +40 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/{% if cookiecutter.include_scheduler == /"yes/" %}scheduler.py{% endif %}" +21 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/integrations/__init__.py +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/integrations/main.py +61 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/py.typed +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test.py +661 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test_models.py +269 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/shared/__init__.py +15 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/shared/models.py +26 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/__init__.py +52 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/alerts.py +94 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/health.py.j2 +1105 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/models.py +169 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/ui.py +52 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docker-compose.yml.j2 +195 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/api.md +191 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/components/scheduler.md +414 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/development.md +215 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/health.md +240 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/javascripts/mermaid-config.js +62 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/stylesheets/mermaid.css +95 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/mkdocs.yml.j2 +62 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/pyproject.toml.j2 +156 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/entrypoint.sh +87 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/entrypoint.sh.j2 +104 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/gen_docs.py +16 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_health_endpoints.py.j2 +239 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/components/test_scheduler.py +76 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/conftest.py.j2 +81 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_component_integration.py.j2 +376 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_health_logic.py.j2 +633 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_models.py +665 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_service.py +602 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_system_service.py +96 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_worker_health_registration.py.j2 +224 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/test_core.py +50 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/uv.lock +1673 -0
- aegis_stack-0.1.0.dist-info/METADATA +114 -0
- aegis_stack-0.1.0.dist-info/RECORD +103 -0
- aegis_stack-0.1.0.dist-info/WHEEL +4 -0
- aegis_stack-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# {{ cookiecutter.project_name }} 🛡️
|
|
2
|
+
|
|
3
|
+
{{ cookiecutter.project_description }}
|
|
4
|
+
|
|
5
|
+
## Components
|
|
6
|
+
|
|
7
|
+
This Aegis Stack project includes the following components:
|
|
8
|
+
|
|
9
|
+
- **✅ Backend**: FastAPI-based async web server
|
|
10
|
+
- **✅ Frontend**: Flet-based Python UI framework
|
|
11
|
+
{%- if cookiecutter.include_scheduler == "yes" %}
|
|
12
|
+
- **✅ Scheduler**: APScheduler-based async task scheduling
|
|
13
|
+
{%- endif %}
|
|
14
|
+
{%- if cookiecutter.include_database == "yes" %}
|
|
15
|
+
- **✅ Database**: SQLite database with SQLModel ORM
|
|
16
|
+
{%- endif %}
|
|
17
|
+
{%- if cookiecutter.include_cache == "yes" %}
|
|
18
|
+
- **✅ Cache**: Redis-based async caching
|
|
19
|
+
{%- endif %}
|
|
20
|
+
|
|
21
|
+
## Getting Started
|
|
22
|
+
|
|
23
|
+
### Prerequisites
|
|
24
|
+
|
|
25
|
+
- Python {{ cookiecutter.python_version }}+
|
|
26
|
+
- [uv](https://docs.astral.sh/uv/) (recommended) or pip
|
|
27
|
+
|
|
28
|
+
### Installation
|
|
29
|
+
|
|
30
|
+
1. **Install dependencies:**
|
|
31
|
+
```bash
|
|
32
|
+
uv sync # Core dependencies only
|
|
33
|
+
# Or for development: uv sync --all-extras (includes testing, linting, docs)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
2. **Activate virtual environment:**
|
|
37
|
+
```bash
|
|
38
|
+
source .venv/bin/activate
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
3. **Set up environment:**
|
|
42
|
+
```bash
|
|
43
|
+
cp .env.example .env
|
|
44
|
+
# Edit .env with your configuration
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
{%- if cookiecutter.include_database == "yes" %}
|
|
48
|
+
|
|
49
|
+
4. **Set up database:**
|
|
50
|
+
```bash
|
|
51
|
+
# Create data directory for SQLite database
|
|
52
|
+
mkdir -p data
|
|
53
|
+
|
|
54
|
+
# Database will be created automatically on first run
|
|
55
|
+
# Default location: data/app.db
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
5. **Run the application:**
|
|
59
|
+
{%- else %}
|
|
60
|
+
|
|
61
|
+
4. **Run the application:**
|
|
62
|
+
{%- endif %}
|
|
63
|
+
```bash
|
|
64
|
+
make run-local
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The application will be available at `http://127.0.0.1:8000`.
|
|
68
|
+
|
|
69
|
+
## Development
|
|
70
|
+
|
|
71
|
+
### Running Tests
|
|
72
|
+
```bash
|
|
73
|
+
make test
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Code Quality
|
|
77
|
+
```bash
|
|
78
|
+
make check # Run linting, type checking, and tests
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### CLI Commands
|
|
82
|
+
|
|
83
|
+
Your project gets its own CLI command **automatically installed** when first used - no setup required! 🎉
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Health monitoring (auto-installs CLI if needed)
|
|
87
|
+
make health # Basic health check
|
|
88
|
+
make health-detailed # Detailed component status
|
|
89
|
+
make health-json # JSON output for scripts
|
|
90
|
+
make health-quick # Quick healthy/unhealthy check
|
|
91
|
+
|
|
92
|
+
# Direct CLI usage (after first make command)
|
|
93
|
+
{{ cookiecutter.project_slug }} health check # Basic health check
|
|
94
|
+
{{ cookiecutter.project_slug }} health check --detailed # Detailed component status
|
|
95
|
+
{{ cookiecutter.project_slug }} health check --json # JSON output for scripts
|
|
96
|
+
{{ cookiecutter.project_slug }} health quick # Quick healthy/unhealthy check
|
|
97
|
+
|
|
98
|
+
# Explore all commands
|
|
99
|
+
{{ cookiecutter.project_slug }} --help # See all available commands
|
|
100
|
+
{{ cookiecutter.project_slug }} health --help # See health subcommand options
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Environment Issues?** If CLI commands don't work:
|
|
104
|
+
```bash
|
|
105
|
+
# Ensure you're in the project's virtual environment
|
|
106
|
+
unset VIRTUAL_ENV # Clear Docker contamination
|
|
107
|
+
source .venv/bin/activate # Activate local venv
|
|
108
|
+
make health # This will auto-install CLI
|
|
109
|
+
# Or manually: make install-cli
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Documentation
|
|
113
|
+
```bash
|
|
114
|
+
make docs-serve # Serve docs at http://localhost:8001/{{ cookiecutter.project_slug }}/
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Docker
|
|
118
|
+
|
|
119
|
+
### Development
|
|
120
|
+
```bash
|
|
121
|
+
docker compose --profile dev up
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Production
|
|
125
|
+
```bash
|
|
126
|
+
docker compose --profile prod up
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
{%- if cookiecutter.include_database == "yes" %}
|
|
130
|
+
|
|
131
|
+
## Configuration
|
|
132
|
+
|
|
133
|
+
### Database Settings
|
|
134
|
+
|
|
135
|
+
The database component uses SQLite with the following default configuration:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
# Environment variables (add to .env file)
|
|
139
|
+
DATABASE_URL=sqlite:///data/app.db # Database file location
|
|
140
|
+
DATABASE_ENGINE_ECHO=false # Enable SQL query logging
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Database Usage
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from app.core.db import db_session
|
|
147
|
+
from sqlmodel import SQLModel, Field, select
|
|
148
|
+
|
|
149
|
+
# Define a model
|
|
150
|
+
class User(SQLModel, table=True):
|
|
151
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
152
|
+
name: str
|
|
153
|
+
email: str
|
|
154
|
+
|
|
155
|
+
# Use the database
|
|
156
|
+
with db_session() as session:
|
|
157
|
+
user = User(name="John Doe", email="john@example.com")
|
|
158
|
+
session.add(user)
|
|
159
|
+
# Auto-committed by context manager
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
{%- endif %}
|
|
163
|
+
|
|
164
|
+
## Project Structure
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
{{ cookiecutter.project_slug }}/
|
|
168
|
+
├── app/ # Application code
|
|
169
|
+
│ ├── components/ # Component implementations
|
|
170
|
+
│ │ ├── backend/ # FastAPI backend
|
|
171
|
+
│ │ └── frontend/ # Flet frontend
|
|
172
|
+
{%- if cookiecutter.include_scheduler == "yes" %}
|
|
173
|
+
│ │ └── scheduler/ # APScheduler background tasks
|
|
174
|
+
{%- endif %}
|
|
175
|
+
│ ├── cli/ # CLI commands and health monitoring
|
|
176
|
+
│ ├── core/ # Core utilities
|
|
177
|
+
│ │ ├── config.py # Environment-dependent configuration
|
|
178
|
+
│ │ ├── constants.py # Immutable constants (12-Factor App)
|
|
179
|
+
{%- if cookiecutter.include_database == "yes" %}
|
|
180
|
+
│ │ ├── db.py # Database connection and session management
|
|
181
|
+
{%- endif %}
|
|
182
|
+
│ │ └── log.py # Structured logging
|
|
183
|
+
│ ├── entrypoints/ # Execution entry points
|
|
184
|
+
│ ├── integrations/ # App composition layer
|
|
185
|
+
{%- if cookiecutter._has_additional_components == "yes" %}
|
|
186
|
+
│ └── services/ # Business logic services
|
|
187
|
+
{%- endif %}
|
|
188
|
+
├── tests/ # Test suite
|
|
189
|
+
├── docs/ # Documentation
|
|
190
|
+
├── docker-compose.yml # Docker services
|
|
191
|
+
└── pyproject.toml # Project configuration
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
Generated with [Aegis Stack](https://github.com/lbedner/aegis-stack) 🛡️
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Health monitoring CLI commands.
|
|
3
|
+
|
|
4
|
+
Command-line interface for system health checking and monitoring via API endpoints.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
import typer
|
|
16
|
+
|
|
17
|
+
from app.core.config import settings
|
|
18
|
+
from app.core.constants import CLI, APIEndpoints, Defaults
|
|
19
|
+
from app.core.log import setup_logging
|
|
20
|
+
from app.services.system.models import (
|
|
21
|
+
ComponentStatusType,
|
|
22
|
+
DetailedHealthResponse,
|
|
23
|
+
HealthResponse,
|
|
24
|
+
)
|
|
25
|
+
from app.services.system.ui import get_status_icon, get_status_color_name
|
|
26
|
+
|
|
27
|
+
app = typer.Typer(name="health", help="System health monitoring commands")
|
|
28
|
+
console = Console()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _get_status_icon_and_color(status: ComponentStatusType) -> tuple[str, str]:
|
|
32
|
+
"""Get the appropriate icon and color for a component status (shared mapping)."""
|
|
33
|
+
return get_status_icon(status), get_status_color_name(status)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def get_health_data(
|
|
37
|
+
endpoint: str = APIEndpoints.HEALTH_BASIC,
|
|
38
|
+
) -> HealthResponse | DetailedHealthResponse:
|
|
39
|
+
"""Get health data from the API endpoint with Pydantic validation."""
|
|
40
|
+
base_url = getattr(settings, "API_BASE_URL", "http://localhost:8000")
|
|
41
|
+
url = f"{base_url}{endpoint}"
|
|
42
|
+
|
|
43
|
+
timeout = httpx.Timeout(Defaults.API_TIMEOUT)
|
|
44
|
+
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
45
|
+
try:
|
|
46
|
+
response = await client.get(url)
|
|
47
|
+
response.raise_for_status()
|
|
48
|
+
json_data = response.json()
|
|
49
|
+
|
|
50
|
+
# Validate response with appropriate Pydantic model
|
|
51
|
+
if endpoint == APIEndpoints.HEALTH_DETAILED:
|
|
52
|
+
return DetailedHealthResponse.model_validate(json_data)
|
|
53
|
+
else:
|
|
54
|
+
return HealthResponse.model_validate(json_data)
|
|
55
|
+
|
|
56
|
+
except httpx.ConnectError:
|
|
57
|
+
raise ConnectionError(
|
|
58
|
+
f"Cannot connect to API server at {base_url}. "
|
|
59
|
+
"Make sure the application is running with "
|
|
60
|
+
"'make run-local' or 'make run-dev'."
|
|
61
|
+
) from None
|
|
62
|
+
except httpx.TimeoutException:
|
|
63
|
+
raise TimeoutError(
|
|
64
|
+
f"API request to {url} timed out after {Defaults.API_TIMEOUT} seconds."
|
|
65
|
+
) from None
|
|
66
|
+
except httpx.HTTPStatusError as e:
|
|
67
|
+
# Handle structured error responses from health endpoint
|
|
68
|
+
if e.response.status_code == 503:
|
|
69
|
+
try:
|
|
70
|
+
error_data = e.response.json()
|
|
71
|
+
if "detail" in error_data and isinstance(
|
|
72
|
+
error_data["detail"], dict
|
|
73
|
+
):
|
|
74
|
+
detail = error_data["detail"]
|
|
75
|
+
message = detail.get("message", "System is unhealthy")
|
|
76
|
+
unhealthy_components = detail.get("unhealthy_components", [])
|
|
77
|
+
health_percentage = detail.get("health_percentage", 0)
|
|
78
|
+
|
|
79
|
+
error_msg = f"{message}"
|
|
80
|
+
if unhealthy_components:
|
|
81
|
+
components_str = ", ".join(unhealthy_components)
|
|
82
|
+
error_msg += f" (Unhealthy: {components_str})"
|
|
83
|
+
if health_percentage is not None:
|
|
84
|
+
error_msg += f" - Health: {health_percentage:.1f}%"
|
|
85
|
+
|
|
86
|
+
raise RuntimeError(error_msg) from None
|
|
87
|
+
except (ValueError, KeyError, TypeError):
|
|
88
|
+
# Fall back to generic error message if JSON parsing fails
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
raise RuntimeError(
|
|
92
|
+
f"API returned error {e.response.status_code}: {e.response.text}"
|
|
93
|
+
) from None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
async def is_system_healthy() -> bool:
|
|
97
|
+
"""Quick check if system is healthy via API."""
|
|
98
|
+
try:
|
|
99
|
+
health_data = await get_health_data(APIEndpoints.HEALTH_BASIC)
|
|
100
|
+
return health_data.healthy
|
|
101
|
+
except Exception:
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@app.command("status")
|
|
106
|
+
def health_status(
|
|
107
|
+
detailed: bool = typer.Option(
|
|
108
|
+
False, "--detailed", "-d", help="Show detailed component information"
|
|
109
|
+
),
|
|
110
|
+
json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Show system health status (always exits 0 for inspection)."""
|
|
113
|
+
setup_logging()
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
endpoint = (
|
|
117
|
+
APIEndpoints.HEALTH_DETAILED if detailed else APIEndpoints.HEALTH_BASIC
|
|
118
|
+
)
|
|
119
|
+
health_data = asyncio.run(get_health_data(endpoint))
|
|
120
|
+
|
|
121
|
+
if json_output:
|
|
122
|
+
print(json.dumps(health_data.model_dump(), indent=2))
|
|
123
|
+
else:
|
|
124
|
+
_display_health_status(health_data, detailed)
|
|
125
|
+
|
|
126
|
+
# Always exit 0 for status command (informational)
|
|
127
|
+
|
|
128
|
+
except Exception as e:
|
|
129
|
+
if json_output:
|
|
130
|
+
error_data = {"error": str(e), "status": "error"}
|
|
131
|
+
print(json.dumps(error_data, indent=2))
|
|
132
|
+
else:
|
|
133
|
+
console.print(f"[red]❌ Health status failed: {e}[/red]")
|
|
134
|
+
# Exit 1 only on actual errors (connection failures, etc), not unhealthy status
|
|
135
|
+
sys.exit(1)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@app.command("probe")
|
|
139
|
+
def health_probe() -> None:
|
|
140
|
+
"""Health probe for monitoring - exits 1 if unhealthy (like k8s probes)."""
|
|
141
|
+
setup_logging()
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
healthy = asyncio.run(is_system_healthy())
|
|
145
|
+
|
|
146
|
+
if healthy:
|
|
147
|
+
console.print("[green]✅ System is healthy[/green]")
|
|
148
|
+
sys.exit(0)
|
|
149
|
+
else:
|
|
150
|
+
console.print("[red]❌ System is unhealthy[/red]")
|
|
151
|
+
sys.exit(1)
|
|
152
|
+
|
|
153
|
+
except Exception as e:
|
|
154
|
+
console.print(f"[red]💥 Health probe failed: {e}[/red]")
|
|
155
|
+
sys.exit(1)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _display_sub_components(
|
|
159
|
+
sub_components: dict[str, Any], detailed: bool, level: int
|
|
160
|
+
) -> None:
|
|
161
|
+
"""Recursively display sub-components with appropriate tree indentation."""
|
|
162
|
+
sub_items = list(sub_components.items())
|
|
163
|
+
|
|
164
|
+
# Calculate tree indentation based on level
|
|
165
|
+
base_indent = " " * level
|
|
166
|
+
|
|
167
|
+
for i, (sub_name, sub_component) in enumerate(sub_items):
|
|
168
|
+
sub_icon, sub_color = _get_status_icon_and_color(sub_component.status)
|
|
169
|
+
|
|
170
|
+
# Tree connector: ├── for middle items, └── for last item
|
|
171
|
+
is_last = i == len(sub_items) - 1
|
|
172
|
+
tree_connector = f"{base_indent}└── " if is_last else f"{base_indent}├── "
|
|
173
|
+
|
|
174
|
+
sub_line = f"{tree_connector}[{sub_color}]{sub_icon} {sub_name}[/{sub_color}]"
|
|
175
|
+
if detailed and sub_component.response_time_ms is not None:
|
|
176
|
+
sub_line += f" ([dim]{sub_component.response_time_ms:.1f}ms[/dim])"
|
|
177
|
+
sub_line += f" {sub_component.message}"
|
|
178
|
+
console.print(sub_line)
|
|
179
|
+
|
|
180
|
+
# Recursively display sub-sub-components
|
|
181
|
+
if hasattr(sub_component, "sub_components") and sub_component.sub_components:
|
|
182
|
+
_display_sub_components(sub_component.sub_components, detailed, level + 1)
|
|
183
|
+
|
|
184
|
+
# Show metadata for sub-components if detailed and available
|
|
185
|
+
elif detailed and sub_component.metadata:
|
|
186
|
+
metadata_str = json.dumps(sub_component.metadata, separators=(",", ":"))
|
|
187
|
+
max_length = CLI.MAX_METADATA_DISPLAY_LENGTH
|
|
188
|
+
if len(metadata_str) > max_length:
|
|
189
|
+
metadata_str = metadata_str[: max_length - 3] + "..."
|
|
190
|
+
# Adjust tree indent based on whether this is the last item
|
|
191
|
+
tree_indent = f"{base_indent} " if is_last else f"{base_indent}│ "
|
|
192
|
+
console.print(f"{tree_indent}[dim]({metadata_str})[/dim]")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _display_health_status(
|
|
196
|
+
health_data: HealthResponse | DetailedHealthResponse, detailed: bool = False
|
|
197
|
+
) -> None:
|
|
198
|
+
"""Display health status with rich formatting."""
|
|
199
|
+
|
|
200
|
+
# Extract data from Pydantic model
|
|
201
|
+
overall_healthy = health_data.healthy
|
|
202
|
+
components = health_data.components
|
|
203
|
+
timestamp = health_data.timestamp
|
|
204
|
+
|
|
205
|
+
# Use health percentage from API response if available (DetailedHealthResponse)
|
|
206
|
+
# Otherwise calculate from top-level components (HealthResponse)
|
|
207
|
+
if hasattr(health_data, "health_percentage"):
|
|
208
|
+
health_percentage = health_data.health_percentage
|
|
209
|
+
# For detailed response, use the component counts from API
|
|
210
|
+
if hasattr(health_data, "healthy_components"):
|
|
211
|
+
if hasattr(health_data, 'unhealthy_components'):
|
|
212
|
+
healthy_count = len(health_data.healthy_components)
|
|
213
|
+
total_count = len(health_data.healthy_components) + len(
|
|
214
|
+
health_data.unhealthy_components
|
|
215
|
+
)
|
|
216
|
+
else:
|
|
217
|
+
# HealthResponse doesn't have detailed component lists
|
|
218
|
+
healthy_count = 1 if health_data.healthy else 0
|
|
219
|
+
total_count = 1
|
|
220
|
+
else:
|
|
221
|
+
# Fallback for detailed response without component lists
|
|
222
|
+
healthy_count = sum(1 for comp in components.values() if comp.healthy)
|
|
223
|
+
total_count = len(components)
|
|
224
|
+
else:
|
|
225
|
+
# Basic health response - count main sub-components for better overview
|
|
226
|
+
if components and "aegis" in components:
|
|
227
|
+
aegis_component = components["aegis"]
|
|
228
|
+
if (
|
|
229
|
+
hasattr(aegis_component, "sub_components")
|
|
230
|
+
and aegis_component.sub_components
|
|
231
|
+
):
|
|
232
|
+
healthy_count = sum(
|
|
233
|
+
1
|
|
234
|
+
for comp in aegis_component.sub_components.values()
|
|
235
|
+
if comp.healthy
|
|
236
|
+
)
|
|
237
|
+
total_count = len(aegis_component.sub_components)
|
|
238
|
+
health_percentage = (
|
|
239
|
+
(healthy_count / total_count) * 100 if total_count > 0 else 100.0
|
|
240
|
+
)
|
|
241
|
+
else:
|
|
242
|
+
# Fallback to aegis component only
|
|
243
|
+
healthy_count = 1 if aegis_component.healthy else 0
|
|
244
|
+
total_count = 1
|
|
245
|
+
health_percentage = 100.0 if aegis_component.healthy else 0.0
|
|
246
|
+
else:
|
|
247
|
+
# No components or no aegis component
|
|
248
|
+
healthy_count = 0
|
|
249
|
+
total_count = 0
|
|
250
|
+
health_percentage = 0.0
|
|
251
|
+
|
|
252
|
+
overall_color = "green" if overall_healthy else "red"
|
|
253
|
+
overall_icon = "✅" if overall_healthy else "❌"
|
|
254
|
+
|
|
255
|
+
title = f"System Health - {overall_icon}"
|
|
256
|
+
if overall_healthy:
|
|
257
|
+
title += " Healthy"
|
|
258
|
+
else:
|
|
259
|
+
title += " Unhealthy"
|
|
260
|
+
|
|
261
|
+
panel_content = [
|
|
262
|
+
f"Overall Status: [bold {overall_color}]"
|
|
263
|
+
+ ("Healthy" if overall_healthy else "Unhealthy")
|
|
264
|
+
+ f"[/bold {overall_color}]",
|
|
265
|
+
f"Health Percentage: [bold]"
|
|
266
|
+
f"{health_percentage:.{CLI.HEALTH_PERCENTAGE_DECIMALS}f}%[/bold]",
|
|
267
|
+
f"Components: {healthy_count}/" + f"{total_count} healthy",
|
|
268
|
+
f"Timestamp: {timestamp}",
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
console.print(
|
|
272
|
+
Panel("\n".join(panel_content), title=title, border_style=overall_color)
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
# Component Tree Display
|
|
276
|
+
console.print("\n[bold magenta]Component Tree:[/bold magenta]")
|
|
277
|
+
|
|
278
|
+
# Sort components: unhealthy first, then by name
|
|
279
|
+
sorted_components = sorted(components.items(), key=lambda x: (x[1].healthy, x[0]))
|
|
280
|
+
|
|
281
|
+
for name, component in sorted_components:
|
|
282
|
+
status_icon, status_color = _get_status_icon_and_color(component.status)
|
|
283
|
+
|
|
284
|
+
# Display main component
|
|
285
|
+
component_line = f"[{status_color}]{status_icon} {name}[/{status_color}]"
|
|
286
|
+
if detailed and component.response_time_ms is not None:
|
|
287
|
+
component_line += f" ([dim]{component.response_time_ms:.1f}ms[/dim])"
|
|
288
|
+
component_line += f" {component.message}"
|
|
289
|
+
console.print(component_line)
|
|
290
|
+
|
|
291
|
+
# Display sub-components with tree structure (recursive)
|
|
292
|
+
if hasattr(component, "sub_components") and component.sub_components:
|
|
293
|
+
_display_sub_components(component.sub_components, detailed, level=1)
|
|
294
|
+
|
|
295
|
+
# Show metadata for main components if detailed and available
|
|
296
|
+
elif detailed and component.metadata:
|
|
297
|
+
metadata_str = json.dumps(component.metadata, separators=(",", ":"))
|
|
298
|
+
max_length = CLI.MAX_METADATA_DISPLAY_LENGTH
|
|
299
|
+
if len(metadata_str) > max_length:
|
|
300
|
+
metadata_str = metadata_str[: max_length - 3] + "..."
|
|
301
|
+
console.print(f" [dim]({metadata_str})[/dim]")
|
|
302
|
+
|
|
303
|
+
# System information (only in detailed mode)
|
|
304
|
+
if detailed and isinstance(health_data, DetailedHealthResponse):
|
|
305
|
+
system_info = health_data.system_info
|
|
306
|
+
if system_info:
|
|
307
|
+
sys_info_content = []
|
|
308
|
+
for key, value in system_info.items():
|
|
309
|
+
sys_info_content.append(f"{key.replace('_', ' ').title()}: {value}")
|
|
310
|
+
|
|
311
|
+
console.print(
|
|
312
|
+
Panel(
|
|
313
|
+
"\n".join(sys_info_content),
|
|
314
|
+
title="System Information",
|
|
315
|
+
border_style="blue",
|
|
316
|
+
)
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
if __name__ == "__main__":
|
|
321
|
+
app()
|