alms-cli 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.
- alms_cli/__init__.py +3 -0
- alms_cli/__main__.py +5 -0
- alms_cli/commands/__init__.py +6 -0
- alms_cli/commands/info.py +73 -0
- alms_cli/commands/init.py +129 -0
- alms_cli/main.py +41 -0
- alms_cli/templates/__init__.py +1329 -0
- alms_cli/ui/__init__.py +23 -0
- alms_cli/ui/components.py +121 -0
- alms_cli-0.1.0.dist-info/METADATA +108 -0
- alms_cli-0.1.0.dist-info/RECORD +14 -0
- alms_cli-0.1.0.dist-info/WHEEL +4 -0
- alms_cli-0.1.0.dist-info/entry_points.txt +2 -0
- alms_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1329 @@
|
|
|
1
|
+
"""Project template generator."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
import shutil
|
|
6
|
+
|
|
7
|
+
class TemplateGenerator:
|
|
8
|
+
"""Generate ALMS project structure from templates."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, project_name: str, project_path: Path):
|
|
11
|
+
self.project_name = project_name
|
|
12
|
+
self.project_path = project_path
|
|
13
|
+
self.template_dir = Path(__file__).parent / "base"
|
|
14
|
+
|
|
15
|
+
def generate(self, features: Optional[list[str]] = None) -> int:
|
|
16
|
+
"""Generate project structure and return file count."""
|
|
17
|
+
if features is None:
|
|
18
|
+
features = []
|
|
19
|
+
|
|
20
|
+
self._create_base_structure()
|
|
21
|
+
self._create_config_files()
|
|
22
|
+
self._create_source_files()
|
|
23
|
+
|
|
24
|
+
return self._count_files()
|
|
25
|
+
|
|
26
|
+
def _create_base_structure(self):
|
|
27
|
+
"""Create base directory structure."""
|
|
28
|
+
dirs = [
|
|
29
|
+
"src/api/endpoints/v1/schemas",
|
|
30
|
+
"src/api/middlewares",
|
|
31
|
+
"src/api/router",
|
|
32
|
+
"src/execution/actions",
|
|
33
|
+
"src/execution/usecases",
|
|
34
|
+
"src/agents/agent_manager",
|
|
35
|
+
"src/agents/prompts",
|
|
36
|
+
"src/agents/tools",
|
|
37
|
+
"src/agents/workflows",
|
|
38
|
+
"src/providers/ai",
|
|
39
|
+
"src/providers/cache",
|
|
40
|
+
"src/providers/vectordb",
|
|
41
|
+
"src/database/migrations",
|
|
42
|
+
"src/database/repositories",
|
|
43
|
+
"src/core",
|
|
44
|
+
"src/config",
|
|
45
|
+
"src/models",
|
|
46
|
+
"src/utils",
|
|
47
|
+
"src/scripts",
|
|
48
|
+
"src/docs",
|
|
49
|
+
"src/evaluation",
|
|
50
|
+
"src/data",
|
|
51
|
+
"src/tests/v1",
|
|
52
|
+
"src/tests/integration",
|
|
53
|
+
"src/tests/e2e",
|
|
54
|
+
"alembic",
|
|
55
|
+
"rules",
|
|
56
|
+
"docs/changelogs",
|
|
57
|
+
".github/workflows",
|
|
58
|
+
".github/ISSUE_TEMPLATE",
|
|
59
|
+
"assets/images",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
for dir_path in dirs:
|
|
63
|
+
(self.project_path / dir_path).mkdir(parents=True, exist_ok=True)
|
|
64
|
+
|
|
65
|
+
def _create_config_files(self):
|
|
66
|
+
"""Create configuration files."""
|
|
67
|
+
templates = {
|
|
68
|
+
"pyproject.toml": self._pyproject_toml(),
|
|
69
|
+
"README.md": self._readme(),
|
|
70
|
+
".env.example": self._env_example(),
|
|
71
|
+
".gitignore": self._gitignore(),
|
|
72
|
+
"pytest.ini": self._pytest_ini(),
|
|
73
|
+
"alembic.ini": self._alembic_ini(),
|
|
74
|
+
"docker-compose.yml": self._docker_compose(),
|
|
75
|
+
"Dockerfile": self._dockerfile(),
|
|
76
|
+
"rules/project_rules.md": self._project_rules(),
|
|
77
|
+
"docs/01-System-Design.md": "# System Design\n",
|
|
78
|
+
"docs/02-Design-Patterns.md": "# Design Patterns\n",
|
|
79
|
+
"docs/03-Database-Design.md": "# Database Design\n",
|
|
80
|
+
"docs/04-Tech-Stack.md": "# Tech Stack\n",
|
|
81
|
+
"docs/05-Project-Structure.md": "# Project Structure\n",
|
|
82
|
+
"docs/06-API-Documentation.md": "# API Documentation\n",
|
|
83
|
+
"docs/07-Setup-Installation.md": "# Setup & Installation\n",
|
|
84
|
+
"docs/08-Contribution-Guide.md": "# Contribution Guide\n",
|
|
85
|
+
".github/workflows/ci.yml": self._ci_workflow(),
|
|
86
|
+
".github/dependabot.yml": self._dependabot(),
|
|
87
|
+
".github/ISSUE_TEMPLATE/bug_report.yml": self._bug_template(),
|
|
88
|
+
".github/ISSUE_TEMPLATE/feature_request.yml": self._feature_template(),
|
|
89
|
+
".github/pull_request_template.md": self._pr_template(),
|
|
90
|
+
"alembic/README": "Alembic migrations\n",
|
|
91
|
+
"alembic/env.py": self._alembic_env(),
|
|
92
|
+
"alembic/script.py.mako": self._alembic_mako(),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for file_path, content in templates.items():
|
|
96
|
+
full_path = self.project_path / file_path
|
|
97
|
+
full_path.parent.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
full_path.write_text(content)
|
|
99
|
+
|
|
100
|
+
def _create_source_files(self):
|
|
101
|
+
"""Create source code files."""
|
|
102
|
+
templates = {
|
|
103
|
+
"src/__init__.py": "",
|
|
104
|
+
"src/api/__init__.py": "",
|
|
105
|
+
"src/api/main.py": self._main_py(),
|
|
106
|
+
"src/api/endpoints/__init__.py": "",
|
|
107
|
+
"src/api/endpoints/v1/__init__.py": "",
|
|
108
|
+
"src/api/endpoints/v1/dependencies.py": self._dependencies_py(),
|
|
109
|
+
"src/api/endpoints/v1/health.py": self._health_py(),
|
|
110
|
+
"src/api/endpoints/v1/metrics.py": self._metrics_py(),
|
|
111
|
+
"src/api/endpoints/v1/routers.py": self._routers_py(),
|
|
112
|
+
"src/api/endpoints/v1/sample_agent.py": self._sample_agent_py(),
|
|
113
|
+
"src/api/endpoints/v1/sample_di.py": self._sample_di_py(),
|
|
114
|
+
"src/api/endpoints/v1/schemas/__init__.py": "",
|
|
115
|
+
"src/api/endpoints/v1/schemas/base.py": self._schema_base_py(),
|
|
116
|
+
"src/api/endpoints/v1/schemas/sample.py": self._schema_sample_py(),
|
|
117
|
+
"src/api/middlewares/__init__.py": "",
|
|
118
|
+
"src/api/middlewares/error_handler.py": self._error_handler_py(),
|
|
119
|
+
"src/api/middlewares/logging.py": self._logging_middleware_py(),
|
|
120
|
+
"src/api/middlewares/observability.py": self._observability_middleware_py(),
|
|
121
|
+
"src/api/middlewares/security.py": self._security_middleware_py(),
|
|
122
|
+
"src/api/router/__init__.py": "",
|
|
123
|
+
"src/api/router/routers.py": self._router_py(),
|
|
124
|
+
"src/execution/__init__.py": "",
|
|
125
|
+
"src/execution/actions/__init__.py": "",
|
|
126
|
+
"src/execution/actions/sample_action.py": self._sample_action_py(),
|
|
127
|
+
"src/execution/usecases/__init__.py": "",
|
|
128
|
+
"src/execution/usecases/sample_usecase.py": self._sample_usecase_py(),
|
|
129
|
+
"src/agents/__init__.py": "",
|
|
130
|
+
"src/agents/agent_manager/__init__.py": "",
|
|
131
|
+
"src/agents/agent_manager/agent.py": self._agent_py(),
|
|
132
|
+
"src/agents/prompts/__init__.py": "",
|
|
133
|
+
"src/agents/prompts/sample_agent_prompt.py": self._sample_prompt_py(),
|
|
134
|
+
"src/agents/tools/.gitkeep": "",
|
|
135
|
+
"src/agents/workflows/.gitkeep": "",
|
|
136
|
+
"src/providers/__init__.py": "",
|
|
137
|
+
"src/providers/ai/__init__.py": "",
|
|
138
|
+
"src/providers/ai/langchain_model_loader.py": self._model_loader_py(),
|
|
139
|
+
"src/providers/cache/.gitkeep": "",
|
|
140
|
+
"src/providers/vectordb/.gitkeep": "",
|
|
141
|
+
"src/observability/__init__.py": self._observability_init_py(),
|
|
142
|
+
"src/observability/metrics.py": self._metrics_module_py(),
|
|
143
|
+
"src/observability/tracing.py": self._tracing_py(),
|
|
144
|
+
"src/database/__init__.py": "",
|
|
145
|
+
"src/database/connection.py": self._db_connection_py(),
|
|
146
|
+
"src/database/migrations/.gitkeep": "",
|
|
147
|
+
"src/database/repositories/__init__.py": "",
|
|
148
|
+
"src/database/repositories/base.py": self._repository_base_py(),
|
|
149
|
+
"src/database/repositories/sqlalchemy_repository.py": self._sqlalchemy_repo_py(),
|
|
150
|
+
"src/core/__init__.py": "",
|
|
151
|
+
"src/core/exceptions.py": self._exceptions_py(),
|
|
152
|
+
"src/config/__init__.py": "",
|
|
153
|
+
"src/config/settings.py": self._settings_py(),
|
|
154
|
+
"src/config/logs_config.py": self._logs_config_py(),
|
|
155
|
+
"src/models/.gitkeep": "",
|
|
156
|
+
"src/utils/.gitkeep": "",
|
|
157
|
+
"src/scripts/.gitkeep": "",
|
|
158
|
+
"src/docs/.gitkeep": "",
|
|
159
|
+
"src/evaluation/.gitkeep": "",
|
|
160
|
+
"src/data/.gitkeep": "",
|
|
161
|
+
"src/tests/__init__.py": "",
|
|
162
|
+
"src/tests/conftest.py": self._conftest_py(),
|
|
163
|
+
"src/tests/README.md": "# Tests\n",
|
|
164
|
+
"src/tests/v1/__init__.py": "",
|
|
165
|
+
"src/tests/v1/test_health.py": self._test_health_py(),
|
|
166
|
+
"src/tests/v1/test_agent.py": self._test_agent_py(),
|
|
167
|
+
"src/tests/v1/test_metrics.py": self._test_metrics_py(),
|
|
168
|
+
"src/tests/v1/test_metrics_endpoint.py": self._test_metrics_endpoint_py(),
|
|
169
|
+
"src/tests/v1/test_observability_middleware.py": self._test_observability_middleware_py(),
|
|
170
|
+
"src/tests/v1/test_sqlalchemy_repository.py": self._test_sqlalchemy_repository_py(),
|
|
171
|
+
"src/tests/v1/test_tracing.py": self._test_tracing_py(),
|
|
172
|
+
"src/tests/integration/__init__.py": "",
|
|
173
|
+
"src/tests/integration/test_full_stack.py": self._test_full_stack_py(),
|
|
174
|
+
"src/tests/e2e/__init__.py": "",
|
|
175
|
+
"src/tests/e2e/test_workflows.py": self._test_workflows_py(),
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
for file_path, content in templates.items():
|
|
179
|
+
full_path = self.project_path / file_path
|
|
180
|
+
full_path.parent.mkdir(parents=True, exist_ok=True)
|
|
181
|
+
full_path.write_text(content)
|
|
182
|
+
|
|
183
|
+
def _count_files(self) -> int:
|
|
184
|
+
"""Count created files."""
|
|
185
|
+
count = 0
|
|
186
|
+
for path in self.project_path.rglob("*"):
|
|
187
|
+
if path.is_file() and not path.name.startswith('.'):
|
|
188
|
+
count += 1
|
|
189
|
+
return count
|
|
190
|
+
|
|
191
|
+
def _pyproject_toml(self) -> str:
|
|
192
|
+
return f'''[project]
|
|
193
|
+
name = "{self.project_name}"
|
|
194
|
+
version = "0.1.0"
|
|
195
|
+
description = "ALMS AI-first backend starter"
|
|
196
|
+
readme = "README.md"
|
|
197
|
+
requires-python = ">=3.13"
|
|
198
|
+
dependencies = [
|
|
199
|
+
"alembic>=1.17.2",
|
|
200
|
+
"asyncpg>=0.31.0",
|
|
201
|
+
"fastapi[standard]>=0.122.0",
|
|
202
|
+
"langchain>=1.1.0",
|
|
203
|
+
"langchain-community>=0.4.1",
|
|
204
|
+
"langchain-mcp-adapters>=0.1.14",
|
|
205
|
+
"langchain-openai>=1.1.0",
|
|
206
|
+
"langgraph>=1.0.4",
|
|
207
|
+
"openai>=2.8.1",
|
|
208
|
+
"opentelemetry-api>=1.25.0",
|
|
209
|
+
"opentelemetry-sdk>=1.25.0",
|
|
210
|
+
"opentelemetry-instrumentation-fastapi>=0.46b0",
|
|
211
|
+
"opentelemetry-instrumentation-sqlalchemy>=0.46b0",
|
|
212
|
+
"opentelemetry-instrumentation-redis>=0.46b0",
|
|
213
|
+
"opentelemetry-exporter-otlp>=1.25.0",
|
|
214
|
+
"prometheus-client>=0.20.0",
|
|
215
|
+
"pydantic>=2.12.5",
|
|
216
|
+
"pydantic-settings>=2.12.0",
|
|
217
|
+
"redis>=7.1.0",
|
|
218
|
+
"ruff>=0.14.11",
|
|
219
|
+
"scalar-fastapi>=1.6.0",
|
|
220
|
+
"sqlalchemy>=2.0.44",
|
|
221
|
+
"uvicorn>=0.38.0",
|
|
222
|
+
]
|
|
223
|
+
|
|
224
|
+
[dependency-groups]
|
|
225
|
+
dev = [
|
|
226
|
+
"httpx>=0.28.1",
|
|
227
|
+
"pytest>=9.0.2",
|
|
228
|
+
"pytest-asyncio>=1.3.0",
|
|
229
|
+
]
|
|
230
|
+
'''
|
|
231
|
+
|
|
232
|
+
def _readme(self) -> str:
|
|
233
|
+
return f'''# {self.project_name}
|
|
234
|
+
|
|
235
|
+
> **The AI-First Backend for Scalable, Intelligent Applications.**
|
|
236
|
+
|
|
237
|
+
## Quick Start
|
|
238
|
+
|
|
239
|
+
### Prerequisites
|
|
240
|
+
|
|
241
|
+
- Python 3.13+
|
|
242
|
+
- PostgreSQL (optional)
|
|
243
|
+
- Redis (optional)
|
|
244
|
+
- [uv](https://docs.astral.sh/uv/getting-started/installation/) installed
|
|
245
|
+
|
|
246
|
+
### 1. Setup
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
# Copy environment file
|
|
250
|
+
cp .env.example .env
|
|
251
|
+
|
|
252
|
+
# Edit .env with your credentials
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### 2. Install Dependencies
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
uv sync
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### 3. Run Database Migrations
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
alembic revision --autogenerate -m "Initial migration"
|
|
265
|
+
alembic upgrade head
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### 4. Start the Application
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
uv run -m src.api.main
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
The API will be available at:
|
|
275
|
+
- API: http://localhost:3000
|
|
276
|
+
- Docs: http://localhost:3000/docs
|
|
277
|
+
'''
|
|
278
|
+
|
|
279
|
+
def _env_example(self) -> str:
|
|
280
|
+
return '''# OpenAI Configuration
|
|
281
|
+
OPENAI_API_KEY=sk-...
|
|
282
|
+
OPENAI_MODEL_BASIC=gpt-4o-mini
|
|
283
|
+
OPENAI_MODEL_REASONING=gpt-4o
|
|
284
|
+
|
|
285
|
+
# Database Configuration
|
|
286
|
+
DATABASE_HOST=localhost
|
|
287
|
+
DATABASE_PORT=5432
|
|
288
|
+
DATABASE_NAME=mydb
|
|
289
|
+
DATABASE_USER=postgres
|
|
290
|
+
DATABASE_PASSWORD=secret
|
|
291
|
+
|
|
292
|
+
# Redis Configuration
|
|
293
|
+
REDIS_HOST=localhost
|
|
294
|
+
REDIS_PORT=6379
|
|
295
|
+
|
|
296
|
+
# Observability (Optional)
|
|
297
|
+
OTLP_ENDPOINT=http://localhost:4317
|
|
298
|
+
METRICS_ENABLED=true
|
|
299
|
+
TRACING_ENABLED=true
|
|
300
|
+
|
|
301
|
+
# Application
|
|
302
|
+
DEBUG=true
|
|
303
|
+
LOG_LEVEL=info
|
|
304
|
+
SERVER_PORT=3000
|
|
305
|
+
'''
|
|
306
|
+
|
|
307
|
+
def _gitignore(self) -> str:
|
|
308
|
+
return '''# Byte-compiled / optimized / DLL files
|
|
309
|
+
__pycache__/
|
|
310
|
+
*.py[cod]
|
|
311
|
+
*$py.class
|
|
312
|
+
|
|
313
|
+
# Virtual environments
|
|
314
|
+
.venv/
|
|
315
|
+
venv/
|
|
316
|
+
|
|
317
|
+
# Distribution / packaging
|
|
318
|
+
dist/
|
|
319
|
+
build/
|
|
320
|
+
*.egg-info/
|
|
321
|
+
|
|
322
|
+
# IDE
|
|
323
|
+
.vscode/
|
|
324
|
+
.idea/
|
|
325
|
+
*.swp
|
|
326
|
+
*.swo
|
|
327
|
+
|
|
328
|
+
# Environment
|
|
329
|
+
.env
|
|
330
|
+
.env.local
|
|
331
|
+
|
|
332
|
+
# Database
|
|
333
|
+
*.db
|
|
334
|
+
*.sqlite3
|
|
335
|
+
|
|
336
|
+
# Logs
|
|
337
|
+
*.log
|
|
338
|
+
logs/
|
|
339
|
+
|
|
340
|
+
# OS
|
|
341
|
+
.DS_Store
|
|
342
|
+
Thumbs.db
|
|
343
|
+
|
|
344
|
+
# Testing
|
|
345
|
+
.pytest_cache/
|
|
346
|
+
.coverage
|
|
347
|
+
htmlcov/
|
|
348
|
+
|
|
349
|
+
# Alembic
|
|
350
|
+
alembic/versions/*.pyc
|
|
351
|
+
'''
|
|
352
|
+
|
|
353
|
+
def _pytest_ini(self) -> str:
|
|
354
|
+
return '''[pytest]
|
|
355
|
+
testpaths = src/tests
|
|
356
|
+
python_files = test_*.py
|
|
357
|
+
python_classes = Test*
|
|
358
|
+
python_functions = test_*
|
|
359
|
+
asyncio_mode = auto
|
|
360
|
+
'''
|
|
361
|
+
|
|
362
|
+
def _alembic_ini(self) -> str:
|
|
363
|
+
return '''[alembic]
|
|
364
|
+
script_location = alembic
|
|
365
|
+
sqlalchemy.url = postgresql+asyncpg://postgres:secret@localhost:5432/mydb
|
|
366
|
+
|
|
367
|
+
[loggers]
|
|
368
|
+
keys = root,sqlalchemy,alembic
|
|
369
|
+
|
|
370
|
+
[handlers]
|
|
371
|
+
keys = console
|
|
372
|
+
|
|
373
|
+
[formatters]
|
|
374
|
+
keys = generic
|
|
375
|
+
|
|
376
|
+
[logger_root]
|
|
377
|
+
level = WARN
|
|
378
|
+
handlers = console
|
|
379
|
+
|
|
380
|
+
[logger_sqlalchemy]
|
|
381
|
+
level = WARN
|
|
382
|
+
handlers =
|
|
383
|
+
qualname = sqlalchemy.engine
|
|
384
|
+
|
|
385
|
+
[logger_alembic]
|
|
386
|
+
level = INFO
|
|
387
|
+
handlers =
|
|
388
|
+
qualname = alembic
|
|
389
|
+
|
|
390
|
+
[handler_console]
|
|
391
|
+
class = StreamHandler
|
|
392
|
+
args = (sys.stderr,)
|
|
393
|
+
level = NOTSET
|
|
394
|
+
formatter = generic
|
|
395
|
+
|
|
396
|
+
[formatter_generic]
|
|
397
|
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
|
398
|
+
datefmt = %H:%M:%S
|
|
399
|
+
'''
|
|
400
|
+
|
|
401
|
+
def _docker_compose(self) -> str:
|
|
402
|
+
return '''version: '3.8'
|
|
403
|
+
|
|
404
|
+
services:
|
|
405
|
+
postgres:
|
|
406
|
+
image: postgres:16-alpine
|
|
407
|
+
environment:
|
|
408
|
+
POSTGRES_DB: mydb
|
|
409
|
+
POSTGRES_USER: postgres
|
|
410
|
+
POSTGRES_PASSWORD: secret
|
|
411
|
+
ports:
|
|
412
|
+
- "5432:5432"
|
|
413
|
+
volumes:
|
|
414
|
+
- postgres_data:/var/lib/postgresql/data
|
|
415
|
+
|
|
416
|
+
redis:
|
|
417
|
+
image: redis:7-alpine
|
|
418
|
+
ports:
|
|
419
|
+
- "6379:6379"
|
|
420
|
+
volumes:
|
|
421
|
+
- redis_data:/data
|
|
422
|
+
|
|
423
|
+
volumes:
|
|
424
|
+
postgres_data:
|
|
425
|
+
redis_data:
|
|
426
|
+
'''
|
|
427
|
+
|
|
428
|
+
def _dockerfile(self) -> str:
|
|
429
|
+
return '''FROM python:3.13-slim
|
|
430
|
+
|
|
431
|
+
WORKDIR /app
|
|
432
|
+
|
|
433
|
+
RUN pip install uv
|
|
434
|
+
|
|
435
|
+
COPY pyproject.toml uv.lock ./
|
|
436
|
+
RUN uv sync --frozen --no-dev
|
|
437
|
+
|
|
438
|
+
COPY . .
|
|
439
|
+
|
|
440
|
+
EXPOSE 3000
|
|
441
|
+
|
|
442
|
+
CMD ["uv", "run", "-m", "src.api.main", "--host", "0.0.0.0"]
|
|
443
|
+
'''
|
|
444
|
+
|
|
445
|
+
def _project_rules(self) -> str:
|
|
446
|
+
return '''# Project Rules
|
|
447
|
+
|
|
448
|
+
## ALMS Architecture Guidelines
|
|
449
|
+
|
|
450
|
+
1. Never skip layers: API -> Execution -> Agent -> Infrastructure
|
|
451
|
+
2. Use repository pattern for database access
|
|
452
|
+
3. Use custom exceptions, not raw HTTPException
|
|
453
|
+
4. Add type hints to all functions
|
|
454
|
+
5. Write tests for all use cases and actions
|
|
455
|
+
'''
|
|
456
|
+
|
|
457
|
+
def _ci_workflow(self) -> str:
|
|
458
|
+
return '''name: CI
|
|
459
|
+
|
|
460
|
+
on:
|
|
461
|
+
push:
|
|
462
|
+
branches: [main]
|
|
463
|
+
pull_request:
|
|
464
|
+
branches: [main]
|
|
465
|
+
|
|
466
|
+
jobs:
|
|
467
|
+
test:
|
|
468
|
+
runs-on: ubuntu-latest
|
|
469
|
+
steps:
|
|
470
|
+
- uses: actions/checkout@v4
|
|
471
|
+
- uses: astral-sh/setup-uv@v3
|
|
472
|
+
- run: uv sync
|
|
473
|
+
- run: uv run pytest src/tests
|
|
474
|
+
- run: uv run ruff check src/
|
|
475
|
+
'''
|
|
476
|
+
|
|
477
|
+
def _dependabot(self) -> str:
|
|
478
|
+
return '''version: 2
|
|
479
|
+
updates:
|
|
480
|
+
- package-ecosystem: "pip"
|
|
481
|
+
directory: "/"
|
|
482
|
+
schedule:
|
|
483
|
+
interval: "weekly"
|
|
484
|
+
'''
|
|
485
|
+
|
|
486
|
+
def _bug_template(self) -> str:
|
|
487
|
+
return '''name: Bug Report
|
|
488
|
+
description: Create a report to help us improve
|
|
489
|
+
body:
|
|
490
|
+
- type: textarea
|
|
491
|
+
attributes:
|
|
492
|
+
label: Describe the bug
|
|
493
|
+
validations:
|
|
494
|
+
required: true
|
|
495
|
+
'''
|
|
496
|
+
|
|
497
|
+
def _feature_template(self) -> str:
|
|
498
|
+
return '''name: Feature Request
|
|
499
|
+
description: Suggest an idea for this project
|
|
500
|
+
body:
|
|
501
|
+
- type: textarea
|
|
502
|
+
attributes:
|
|
503
|
+
label: Describe the feature
|
|
504
|
+
validations:
|
|
505
|
+
required: true
|
|
506
|
+
'''
|
|
507
|
+
|
|
508
|
+
def _pr_template(self) -> str:
|
|
509
|
+
return '''## Description
|
|
510
|
+
|
|
511
|
+
Please include a summary of the changes.
|
|
512
|
+
|
|
513
|
+
## Type of change
|
|
514
|
+
|
|
515
|
+
- [ ] Bug fix
|
|
516
|
+
- [ ] New feature
|
|
517
|
+
- [ ] Breaking change
|
|
518
|
+
'''
|
|
519
|
+
|
|
520
|
+
def _alembic_env(self) -> str:
|
|
521
|
+
return '''from logging.config import fileConfig
|
|
522
|
+
from sqlalchemy import engine_from_config
|
|
523
|
+
from sqlalchemy import pool
|
|
524
|
+
from alembic import context
|
|
525
|
+
|
|
526
|
+
config = context.config
|
|
527
|
+
|
|
528
|
+
if config.config_file_name is not None:
|
|
529
|
+
fileConfig(config.config_file_name)
|
|
530
|
+
|
|
531
|
+
target_metadata = None
|
|
532
|
+
|
|
533
|
+
def run_migrations_offline() -> None:
|
|
534
|
+
url = config.get_main_option("sqlalchemy.url")
|
|
535
|
+
context.configure(url=url, target_metadata=target_metadata, literal_binds=True)
|
|
536
|
+
with context.begin_transaction():
|
|
537
|
+
context.run_migrations()
|
|
538
|
+
|
|
539
|
+
def run_migrations_online() -> None:
|
|
540
|
+
connectable = engine_from_config(
|
|
541
|
+
config.get_section(config.config_ini_section, {}),
|
|
542
|
+
prefix="sqlalchemy.",
|
|
543
|
+
poolclass=pool.NullPool,
|
|
544
|
+
)
|
|
545
|
+
with connectable.connect() as connection:
|
|
546
|
+
context.configure(connection=connection, target_metadata=target_metadata)
|
|
547
|
+
with context.begin_transaction():
|
|
548
|
+
context.run_migrations()
|
|
549
|
+
|
|
550
|
+
if context.is_offline_mode():
|
|
551
|
+
run_migrations_offline()
|
|
552
|
+
else:
|
|
553
|
+
run_migrations_online()
|
|
554
|
+
'''
|
|
555
|
+
|
|
556
|
+
def _alembic_mako(self) -> str:
|
|
557
|
+
return '''"""${message}
|
|
558
|
+
|
|
559
|
+
Revision ID: ${up_revision}
|
|
560
|
+
Revises: ${down_revision | comma,n}
|
|
561
|
+
Create Date: ${create_date}
|
|
562
|
+
"""
|
|
563
|
+
from typing import Sequence, Union
|
|
564
|
+
from alembic import op
|
|
565
|
+
import sqlalchemy as sa
|
|
566
|
+
|
|
567
|
+
revision: str = ${repr(up_revision)}
|
|
568
|
+
down_revision: Union[str, None] = ${repr(down_revision)}
|
|
569
|
+
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
|
570
|
+
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
|
571
|
+
|
|
572
|
+
def upgrade() -> None:
|
|
573
|
+
${upgrades if upgrades else "pass"}
|
|
574
|
+
|
|
575
|
+
def downgrade() -> None:
|
|
576
|
+
${downgrades if downgrades else "pass"}
|
|
577
|
+
'''
|
|
578
|
+
|
|
579
|
+
def _main_py(self) -> str:
|
|
580
|
+
return '''"""FastAPI application entry point."""
|
|
581
|
+
|
|
582
|
+
import uvicorn
|
|
583
|
+
from fastapi import FastAPI
|
|
584
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
585
|
+
from src.config.settings import settings
|
|
586
|
+
from src.api.router.routers import include_routers
|
|
587
|
+
from src.api.middlewares.security import setup_security_middleware
|
|
588
|
+
from src.api.middlewares.logging import setup_logging_middleware
|
|
589
|
+
from src.api.middlewares.error_handler import setup_error_handler
|
|
590
|
+
from src.api.middlewares.observability import setup_observability_middleware
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def create_app() -> FastAPI:
|
|
594
|
+
"""Create and configure FastAPI application."""
|
|
595
|
+
app = FastAPI(
|
|
596
|
+
title=settings.PROJECT_NAME,
|
|
597
|
+
version="0.1.0",
|
|
598
|
+
docs_url=None,
|
|
599
|
+
redoc_url=None,
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
setup_security_middleware(app)
|
|
603
|
+
setup_logging_middleware(app)
|
|
604
|
+
setup_error_handler(app)
|
|
605
|
+
setup_observability_middleware(app)
|
|
606
|
+
|
|
607
|
+
app.add_middleware(
|
|
608
|
+
CORSMiddleware,
|
|
609
|
+
allow_origins=["*"],
|
|
610
|
+
allow_credentials=True,
|
|
611
|
+
allow_methods=["*"],
|
|
612
|
+
allow_headers=["*"],
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
include_routers(app)
|
|
616
|
+
|
|
617
|
+
try:
|
|
618
|
+
from scalar_fastapi import get_scalar_api_reference
|
|
619
|
+
|
|
620
|
+
@app.get("/docs", include_in_schema=False)
|
|
621
|
+
async def scalar_html():
|
|
622
|
+
return get_scalar_api_reference(
|
|
623
|
+
openapi_url=app.openapi_url,
|
|
624
|
+
title=app.title,
|
|
625
|
+
)
|
|
626
|
+
except ImportError:
|
|
627
|
+
pass
|
|
628
|
+
|
|
629
|
+
return app
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
app = create_app()
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
if __name__ == "__main__":
|
|
636
|
+
uvicorn.run(
|
|
637
|
+
"src.api.main:app",
|
|
638
|
+
host=settings.SERVER_HOST,
|
|
639
|
+
port=settings.SERVER_PORT,
|
|
640
|
+
reload=settings.DEBUG,
|
|
641
|
+
)
|
|
642
|
+
'''
|
|
643
|
+
|
|
644
|
+
def _dependencies_py(self) -> str:
|
|
645
|
+
return '''"""Dependencies for v1 endpoints."""
|
|
646
|
+
|
|
647
|
+
from fastapi import Depends
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def get_db_session():
|
|
651
|
+
"""Get database session."""
|
|
652
|
+
pass
|
|
653
|
+
'''
|
|
654
|
+
|
|
655
|
+
def _health_py(self) -> str:
|
|
656
|
+
return '''"""Health check endpoint."""
|
|
657
|
+
|
|
658
|
+
from fastapi import APIRouter
|
|
659
|
+
from src.api.endpoints.v1.schemas.base import AppResponse
|
|
660
|
+
|
|
661
|
+
router = APIRouter()
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
@router.get("/health")
|
|
665
|
+
async def health_check():
|
|
666
|
+
"""Health check endpoint."""
|
|
667
|
+
return AppResponse(success=True, data={"status": "healthy"})
|
|
668
|
+
'''
|
|
669
|
+
|
|
670
|
+
def _metrics_py(self) -> str:
|
|
671
|
+
return '''"""Prometheus metrics endpoint."""
|
|
672
|
+
|
|
673
|
+
from fastapi import APIRouter, Response
|
|
674
|
+
from src.observability.metrics import generate_metrics
|
|
675
|
+
|
|
676
|
+
router = APIRouter()
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
@router.get("/metrics")
|
|
680
|
+
async def metrics():
|
|
681
|
+
"""Expose Prometheus metrics."""
|
|
682
|
+
metrics_data = generate_metrics()
|
|
683
|
+
return Response(content=metrics_data, media_type="text/plain")
|
|
684
|
+
'''
|
|
685
|
+
|
|
686
|
+
def _routers_py(self) -> str:
|
|
687
|
+
return '''"""Main v1 router aggregation."""
|
|
688
|
+
|
|
689
|
+
from fastapi import APIRouter
|
|
690
|
+
from src.api.endpoints.v1 import health, metrics, sample_agent, sample_di
|
|
691
|
+
|
|
692
|
+
v1_router = APIRouter(prefix="/api/v1")
|
|
693
|
+
|
|
694
|
+
v1_router.include_router(health.router, tags=["Health"])
|
|
695
|
+
v1_router.include_router(metrics.router, tags=["Metrics"])
|
|
696
|
+
v1_router.include_router(sample_agent.router, tags=["Agent"])
|
|
697
|
+
v1_router.include_router(sample_di.router, tags=["DI Example"])
|
|
698
|
+
'''
|
|
699
|
+
|
|
700
|
+
def _sample_agent_py(self) -> str:
|
|
701
|
+
return '''"""Sample agent endpoint."""
|
|
702
|
+
|
|
703
|
+
from fastapi import APIRouter
|
|
704
|
+
from src.api.endpoints.v1.schemas.base import AppResponse
|
|
705
|
+
from src.execution.usecases.sample_usecase import SampleUsecase
|
|
706
|
+
|
|
707
|
+
router = APIRouter()
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
@router.post("/agent/sample")
|
|
711
|
+
async def sample_agent(query: str):
|
|
712
|
+
"""Sample agent endpoint."""
|
|
713
|
+
usecase = SampleUsecase()
|
|
714
|
+
result = await usecase.execute(query)
|
|
715
|
+
return AppResponse(success=True, data=result)
|
|
716
|
+
'''
|
|
717
|
+
|
|
718
|
+
def _sample_di_py(self) -> str:
|
|
719
|
+
return '''"""Sample dependency injection endpoint."""
|
|
720
|
+
|
|
721
|
+
from fastapi import APIRouter, Depends
|
|
722
|
+
from src.api.endpoints.v1.schemas.base import AppResponse
|
|
723
|
+
from src.api.endpoints.v1.dependencies import get_db_session
|
|
724
|
+
|
|
725
|
+
router = APIRouter()
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
@router.get("/di/sample")
|
|
729
|
+
async def sample_di(session=Depends(get_db_session)):
|
|
730
|
+
"""Sample DI endpoint."""
|
|
731
|
+
return AppResponse(success=True, data={"message": "DI working"})
|
|
732
|
+
'''
|
|
733
|
+
|
|
734
|
+
def _schema_base_py(self) -> str:
|
|
735
|
+
return '''"""Base response schemas."""
|
|
736
|
+
|
|
737
|
+
from pydantic import BaseModel
|
|
738
|
+
from typing import Any, Optional
|
|
739
|
+
from uuid import UUID, uuid4
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
class ErrorDetail(BaseModel):
|
|
743
|
+
code: str
|
|
744
|
+
message: str
|
|
745
|
+
details: Optional[dict[str, Any]] = None
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
class AppResponse(BaseModel):
|
|
749
|
+
success: bool
|
|
750
|
+
data: Optional[Any] = None
|
|
751
|
+
error: Optional[ErrorDetail] = None
|
|
752
|
+
request_id: UUID = uuid4()
|
|
753
|
+
'''
|
|
754
|
+
|
|
755
|
+
def _schema_sample_py(self) -> str:
|
|
756
|
+
return '''"""Sample request/response schemas."""
|
|
757
|
+
|
|
758
|
+
from pydantic import BaseModel
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
class SampleRequest(BaseModel):
|
|
762
|
+
query: str
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
class SampleResponse(BaseModel):
|
|
766
|
+
message: str
|
|
767
|
+
result: str
|
|
768
|
+
'''
|
|
769
|
+
|
|
770
|
+
def _error_handler_py(self) -> str:
|
|
771
|
+
return '''"""Error handler middleware."""
|
|
772
|
+
|
|
773
|
+
from fastapi import FastAPI, Request
|
|
774
|
+
from fastapi.responses import JSONResponse
|
|
775
|
+
from src.core.exceptions import AppException
|
|
776
|
+
from src.api.endpoints.v1.schemas.base import AppResponse, ErrorDetail
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
def setup_error_handler(app: FastAPI):
|
|
780
|
+
"""Setup global error handlers."""
|
|
781
|
+
|
|
782
|
+
@app.exception_handler(AppException)
|
|
783
|
+
async def app_exception_handler(request: Request, exc: AppException):
|
|
784
|
+
return JSONResponse(
|
|
785
|
+
status_code=exc.status_code,
|
|
786
|
+
content=AppResponse(
|
|
787
|
+
success=False,
|
|
788
|
+
error=ErrorDetail(
|
|
789
|
+
code=exc.error_code,
|
|
790
|
+
message=exc.message,
|
|
791
|
+
details=exc.details,
|
|
792
|
+
),
|
|
793
|
+
).model_dump(),
|
|
794
|
+
)
|
|
795
|
+
'''
|
|
796
|
+
|
|
797
|
+
def _logging_middleware_py(self) -> str:
|
|
798
|
+
return '''"""Request logging middleware."""
|
|
799
|
+
|
|
800
|
+
import logging
|
|
801
|
+
import time
|
|
802
|
+
from fastapi import FastAPI, Request
|
|
803
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
804
|
+
|
|
805
|
+
logger = logging.getLogger(__name__)
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
class LoggingMiddleware(BaseHTTPMiddleware):
|
|
809
|
+
async def dispatch(self, request: Request, call_next):
|
|
810
|
+
start_time = time.time()
|
|
811
|
+
response = await call_next(request)
|
|
812
|
+
duration = time.time() - start_time
|
|
813
|
+
|
|
814
|
+
logger.info(
|
|
815
|
+
f"{request.method} {request.url.path} - {response.status_code} - {duration:.3f}s"
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
return response
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
def setup_logging_middleware(app: FastAPI):
|
|
822
|
+
"""Setup logging middleware."""
|
|
823
|
+
app.add_middleware(LoggingMiddleware)
|
|
824
|
+
'''
|
|
825
|
+
|
|
826
|
+
def _observability_middleware_py(self) -> str:
|
|
827
|
+
return '''"""Observability middleware for metrics and tracing."""
|
|
828
|
+
|
|
829
|
+
from fastapi import FastAPI, Request
|
|
830
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
831
|
+
from src.observability.metrics import metrics
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
class ObservabilityMiddleware(BaseHTTPMiddleware):
|
|
835
|
+
async def dispatch(self, request: Request, call_next):
|
|
836
|
+
response = await call_next(request)
|
|
837
|
+
|
|
838
|
+
metrics.counter(
|
|
839
|
+
"http_requests_total",
|
|
840
|
+
{"method": request.method, "endpoint": request.url.path, "status": str(response.status_code)},
|
|
841
|
+
)
|
|
842
|
+
|
|
843
|
+
return response
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def setup_observability_middleware(app: FastAPI):
|
|
847
|
+
"""Setup observability middleware."""
|
|
848
|
+
app.add_middleware(ObservabilityMiddleware)
|
|
849
|
+
'''
|
|
850
|
+
|
|
851
|
+
def _security_middleware_py(self) -> str:
|
|
852
|
+
return '''"""Security headers middleware."""
|
|
853
|
+
|
|
854
|
+
from fastapi import FastAPI, Request
|
|
855
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
class SecurityMiddleware(BaseHTTPMiddleware):
|
|
859
|
+
async def dispatch(self, request: Request, call_next):
|
|
860
|
+
response = await call_next(request)
|
|
861
|
+
|
|
862
|
+
response.headers["X-Content-Type-Options"] = "nosniff"
|
|
863
|
+
response.headers["X-Frame-Options"] = "DENY"
|
|
864
|
+
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
|
|
865
|
+
|
|
866
|
+
return response
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
def setup_security_middleware(app: FastAPI):
|
|
870
|
+
"""Setup security middleware."""
|
|
871
|
+
app.add_middleware(SecurityMiddleware)
|
|
872
|
+
'''
|
|
873
|
+
|
|
874
|
+
def _router_py(self) -> str:
|
|
875
|
+
return '''"""Router aggregation."""
|
|
876
|
+
|
|
877
|
+
from fastapi import FastAPI
|
|
878
|
+
from src.api.endpoints.v1.routers import v1_router
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
def include_routers(app: FastAPI):
|
|
882
|
+
"""Include all routers in the application."""
|
|
883
|
+
app.include_router(v1_router)
|
|
884
|
+
'''
|
|
885
|
+
|
|
886
|
+
def _sample_action_py(self) -> str:
|
|
887
|
+
return '''"""Sample action."""
|
|
888
|
+
|
|
889
|
+
from typing import Any
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
class SampleAction:
|
|
893
|
+
"""Sample action that performs a discrete operation."""
|
|
894
|
+
|
|
895
|
+
async def execute(self, query: str) -> dict[str, Any]:
|
|
896
|
+
"""Execute the action."""
|
|
897
|
+
return {"message": f"Processed: {query}", "status": "success"}
|
|
898
|
+
'''
|
|
899
|
+
|
|
900
|
+
def _sample_usecase_py(self) -> str:
|
|
901
|
+
return '''"""Sample usecase."""
|
|
902
|
+
|
|
903
|
+
from typing import Any
|
|
904
|
+
from src.execution.actions.sample_action import SampleAction
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
class SampleUsecase:
|
|
908
|
+
"""Sample usecase that orchestrates business flow."""
|
|
909
|
+
|
|
910
|
+
def __init__(self):
|
|
911
|
+
self.action = SampleAction()
|
|
912
|
+
|
|
913
|
+
async def execute(self, query: str) -> dict[str, Any]:
|
|
914
|
+
"""Execute the usecase."""
|
|
915
|
+
result = await self.action.execute(query)
|
|
916
|
+
return {"usecase_result": result}
|
|
917
|
+
'''
|
|
918
|
+
|
|
919
|
+
def _agent_py(self) -> str:
|
|
920
|
+
return '''"""Agent definitions."""
|
|
921
|
+
|
|
922
|
+
from langchain_openai import ChatOpenAI
|
|
923
|
+
from src.config.settings import settings
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
class SampleAgent:
|
|
927
|
+
"""Sample AI agent."""
|
|
928
|
+
|
|
929
|
+
def __init__(self):
|
|
930
|
+
self.llm = ChatOpenAI(
|
|
931
|
+
model=settings.OPENAI_MODEL_BASIC,
|
|
932
|
+
api_key=settings.OPENAI_API_KEY,
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
async def process(self, query: str) -> str:
|
|
936
|
+
"""Process a query using the agent."""
|
|
937
|
+
response = await self.llm.ainvoke(query)
|
|
938
|
+
return response.content
|
|
939
|
+
'''
|
|
940
|
+
|
|
941
|
+
def _sample_prompt_py(self) -> str:
|
|
942
|
+
return '''"""Sample agent prompt template."""
|
|
943
|
+
|
|
944
|
+
SAMPLE_AGENT_PROMPT = """You are a helpful AI assistant.
|
|
945
|
+
|
|
946
|
+
Query: {query}
|
|
947
|
+
|
|
948
|
+
Please provide a helpful response."""
|
|
949
|
+
'''
|
|
950
|
+
|
|
951
|
+
def _model_loader_py(self) -> str:
|
|
952
|
+
return '''"""AI model loader provider."""
|
|
953
|
+
|
|
954
|
+
from langchain_openai import ChatOpenAI
|
|
955
|
+
from src.config.settings import settings
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
def get_llm(model: str = "basic") -> ChatOpenAI:
|
|
959
|
+
"""Get LLM instance."""
|
|
960
|
+
model_name = (
|
|
961
|
+
settings.OPENAI_MODEL_REASONING
|
|
962
|
+
if model == "reasoning"
|
|
963
|
+
else settings.OPENAI_MODEL_BASIC
|
|
964
|
+
)
|
|
965
|
+
|
|
966
|
+
return ChatOpenAI(
|
|
967
|
+
model=model_name,
|
|
968
|
+
api_key=settings.OPENAI_API_KEY,
|
|
969
|
+
)
|
|
970
|
+
'''
|
|
971
|
+
|
|
972
|
+
def _observability_init_py(self) -> str:
|
|
973
|
+
return '''"""Observability module."""
|
|
974
|
+
|
|
975
|
+
from src.observability.metrics import metrics
|
|
976
|
+
from src.observability.tracing import tracer
|
|
977
|
+
|
|
978
|
+
__all__ = ["metrics", "tracer"]
|
|
979
|
+
'''
|
|
980
|
+
|
|
981
|
+
def _metrics_module_py(self) -> str:
|
|
982
|
+
return '''"""Prometheus metrics."""
|
|
983
|
+
|
|
984
|
+
from prometheus_client import Counter, Histogram, generate_latest
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
metrics_counter = Counter("http_requests_total", "Total HTTP requests", ["method", "endpoint", "status"])
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
class MetricsCollector:
|
|
991
|
+
"""Collect and expose metrics."""
|
|
992
|
+
|
|
993
|
+
def counter(self, name: str, labels: dict[str, str]):
|
|
994
|
+
"""Increment a counter metric."""
|
|
995
|
+
pass
|
|
996
|
+
|
|
997
|
+
def generate(self) -> bytes:
|
|
998
|
+
"""Generate metrics data."""
|
|
999
|
+
return generate_latest()
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
metrics = MetricsCollector()
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
def generate_metrics() -> bytes:
|
|
1006
|
+
"""Generate Prometheus metrics."""
|
|
1007
|
+
return generate_latest()
|
|
1008
|
+
'''
|
|
1009
|
+
|
|
1010
|
+
def _tracing_py(self) -> str:
|
|
1011
|
+
return '''"""OpenTelemetry tracing."""
|
|
1012
|
+
|
|
1013
|
+
from contextlib import contextmanager
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
class Tracer:
|
|
1017
|
+
"""Simple tracer for distributed tracing."""
|
|
1018
|
+
|
|
1019
|
+
@contextmanager
|
|
1020
|
+
def start_as_current_span(self, name: str):
|
|
1021
|
+
"""Start a new span."""
|
|
1022
|
+
yield
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
tracer = Tracer()
|
|
1026
|
+
'''
|
|
1027
|
+
|
|
1028
|
+
def _db_connection_py(self) -> str:
|
|
1029
|
+
return '''"""Database connection setup."""
|
|
1030
|
+
|
|
1031
|
+
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
|
1032
|
+
from sqlalchemy.orm import sessionmaker
|
|
1033
|
+
from src.config.settings import settings
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
engine = create_async_engine(settings.DATABASE_URL, echo=settings.DEBUG)
|
|
1037
|
+
|
|
1038
|
+
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
async def get_db_session() -> AsyncSession:
|
|
1042
|
+
"""Get database session."""
|
|
1043
|
+
async with async_session() as session:
|
|
1044
|
+
yield session
|
|
1045
|
+
'''
|
|
1046
|
+
|
|
1047
|
+
def _repository_base_py(self) -> str:
|
|
1048
|
+
return '''"""Base repository pattern."""
|
|
1049
|
+
|
|
1050
|
+
from typing import Generic, TypeVar, Type, Optional, Sequence
|
|
1051
|
+
from sqlalchemy import select
|
|
1052
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
1053
|
+
|
|
1054
|
+
ModelType = TypeVar("ModelType")
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
class BaseRepository(Generic[ModelType]):
|
|
1058
|
+
"""Base repository with common CRUD operations."""
|
|
1059
|
+
|
|
1060
|
+
def __init__(self, model: Type[ModelType]):
|
|
1061
|
+
self.model = model
|
|
1062
|
+
|
|
1063
|
+
async def get(self, session: AsyncSession, id: int) -> Optional[ModelType]:
|
|
1064
|
+
"""Get by ID."""
|
|
1065
|
+
result = await session.execute(select(self.model).where(self.model.id == id))
|
|
1066
|
+
return result.scalar_one_or_none()
|
|
1067
|
+
|
|
1068
|
+
async def get_multi(
|
|
1069
|
+
self, session: AsyncSession, skip: int = 0, limit: int = 100
|
|
1070
|
+
) -> Sequence[ModelType]:
|
|
1071
|
+
"""Get multiple records."""
|
|
1072
|
+
result = await session.execute(
|
|
1073
|
+
select(self.model).offset(skip).limit(limit)
|
|
1074
|
+
)
|
|
1075
|
+
return result.scalars().all()
|
|
1076
|
+
'''
|
|
1077
|
+
|
|
1078
|
+
def _sqlalchemy_repo_py(self) -> str:
|
|
1079
|
+
return '''"""SQLAlchemy repository implementation."""
|
|
1080
|
+
|
|
1081
|
+
from src.database.repositories.base import BaseRepository
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
class SQLAlchemyRepository(BaseRepository):
|
|
1085
|
+
"""SQLAlchemy specific repository implementation."""
|
|
1086
|
+
pass
|
|
1087
|
+
'''
|
|
1088
|
+
|
|
1089
|
+
def _exceptions_py(self) -> str:
|
|
1090
|
+
return '''"""Custom exceptions."""
|
|
1091
|
+
|
|
1092
|
+
from typing import Optional
|
|
1093
|
+
|
|
1094
|
+
|
|
1095
|
+
class AppException(Exception):
|
|
1096
|
+
"""Base application exception."""
|
|
1097
|
+
|
|
1098
|
+
def __init__(
|
|
1099
|
+
self,
|
|
1100
|
+
message: str,
|
|
1101
|
+
error_code: str,
|
|
1102
|
+
status_code: int = 500,
|
|
1103
|
+
details: Optional[dict] = None,
|
|
1104
|
+
):
|
|
1105
|
+
self.message = message
|
|
1106
|
+
self.error_code = error_code
|
|
1107
|
+
self.status_code = status_code
|
|
1108
|
+
self.details = details or {}
|
|
1109
|
+
super().__init__(message)
|
|
1110
|
+
|
|
1111
|
+
|
|
1112
|
+
class NotFoundException(AppException):
|
|
1113
|
+
"""Resource not found."""
|
|
1114
|
+
|
|
1115
|
+
def __init__(self, message: str, error_code: str = "NOT_FOUND", details: Optional[dict] = None):
|
|
1116
|
+
super().__init__(message, error_code, 404, details)
|
|
1117
|
+
|
|
1118
|
+
|
|
1119
|
+
class ValidationException(AppException):
|
|
1120
|
+
"""Validation error."""
|
|
1121
|
+
|
|
1122
|
+
def __init__(self, message: str, error_code: str = "VALIDATION_ERROR", details: Optional[dict] = None):
|
|
1123
|
+
super().__init__(message, error_code, 422, details)
|
|
1124
|
+
'''
|
|
1125
|
+
|
|
1126
|
+
def _settings_py(self) -> str:
|
|
1127
|
+
return '''"""Application settings."""
|
|
1128
|
+
|
|
1129
|
+
from pydantic_settings import BaseSettings
|
|
1130
|
+
from functools import lru_cache
|
|
1131
|
+
|
|
1132
|
+
|
|
1133
|
+
class Settings(BaseSettings):
|
|
1134
|
+
"""Application settings from environment variables."""
|
|
1135
|
+
|
|
1136
|
+
PROJECT_NAME: str = "ALMS Backend"
|
|
1137
|
+
DEBUG: bool = True
|
|
1138
|
+
SERVER_HOST: str = "0.0.0.0"
|
|
1139
|
+
SERVER_PORT: int = 3000
|
|
1140
|
+
|
|
1141
|
+
OPENAI_API_KEY: str = ""
|
|
1142
|
+
OPENAI_MODEL_BASIC: str = "gpt-4o-mini"
|
|
1143
|
+
OPENAI_MODEL_REASONING: str = "gpt-4o"
|
|
1144
|
+
|
|
1145
|
+
DATABASE_HOST: str = "localhost"
|
|
1146
|
+
DATABASE_PORT: int = 5432
|
|
1147
|
+
DATABASE_NAME: str = "mydb"
|
|
1148
|
+
DATABASE_USER: str = "postgres"
|
|
1149
|
+
DATABASE_PASSWORD: str = "secret"
|
|
1150
|
+
|
|
1151
|
+
REDIS_HOST: str = "localhost"
|
|
1152
|
+
REDIS_PORT: int = 6379
|
|
1153
|
+
|
|
1154
|
+
OTLP_ENDPOINT: str = "http://localhost:4317"
|
|
1155
|
+
METRICS_ENABLED: bool = True
|
|
1156
|
+
TRACING_ENABLED: bool = True
|
|
1157
|
+
|
|
1158
|
+
LOG_LEVEL: str = "info"
|
|
1159
|
+
|
|
1160
|
+
@property
|
|
1161
|
+
def DATABASE_URL(self) -> str:
|
|
1162
|
+
return f"postgresql+asyncpg://{self.DATABASE_USER}:{self.DATABASE_PASSWORD}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}"
|
|
1163
|
+
|
|
1164
|
+
@property
|
|
1165
|
+
def REDIS_URL(self) -> str:
|
|
1166
|
+
return f"redis://{self.REDIS_HOST}:{self.REDIS_PORT}/0"
|
|
1167
|
+
|
|
1168
|
+
class Config:
|
|
1169
|
+
env_file = ".env"
|
|
1170
|
+
case_sensitive = True
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
@lru_cache()
|
|
1174
|
+
def get_settings() -> Settings:
|
|
1175
|
+
"""Get cached settings instance."""
|
|
1176
|
+
return Settings()
|
|
1177
|
+
|
|
1178
|
+
|
|
1179
|
+
settings = get_settings()
|
|
1180
|
+
'''
|
|
1181
|
+
|
|
1182
|
+
def _logs_config_py(self) -> str:
|
|
1183
|
+
return '''"""Logging configuration."""
|
|
1184
|
+
|
|
1185
|
+
import logging
|
|
1186
|
+
import sys
|
|
1187
|
+
from src.config.settings import settings
|
|
1188
|
+
|
|
1189
|
+
|
|
1190
|
+
def setup_logging():
|
|
1191
|
+
"""Configure application logging."""
|
|
1192
|
+
log_level = getattr(logging, settings.LOG_LEVEL.upper(), logging.INFO)
|
|
1193
|
+
|
|
1194
|
+
logging.basicConfig(
|
|
1195
|
+
level=log_level,
|
|
1196
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
1197
|
+
handlers=[logging.StreamHandler(sys.stdout)],
|
|
1198
|
+
)
|
|
1199
|
+
'''
|
|
1200
|
+
|
|
1201
|
+
def _conftest_py(self) -> str:
|
|
1202
|
+
return '''"""Pytest configuration and fixtures."""
|
|
1203
|
+
|
|
1204
|
+
import pytest
|
|
1205
|
+
from httpx import AsyncClient
|
|
1206
|
+
from src.api.main import app
|
|
1207
|
+
|
|
1208
|
+
|
|
1209
|
+
@pytest.fixture
|
|
1210
|
+
async def client():
|
|
1211
|
+
"""Create test client."""
|
|
1212
|
+
async with AsyncClient(app=app, base_url="http://test") as client:
|
|
1213
|
+
yield client
|
|
1214
|
+
'''
|
|
1215
|
+
|
|
1216
|
+
def _test_health_py(self) -> str:
|
|
1217
|
+
return '''"""Health endpoint tests."""
|
|
1218
|
+
|
|
1219
|
+
import pytest
|
|
1220
|
+
|
|
1221
|
+
|
|
1222
|
+
@pytest.mark.asyncio
|
|
1223
|
+
async def test_health_check(client):
|
|
1224
|
+
"""Test health check endpoint."""
|
|
1225
|
+
response = await client.get("/api/v1/health")
|
|
1226
|
+
assert response.status_code == 200
|
|
1227
|
+
data = response.json()
|
|
1228
|
+
assert data["success"] is True
|
|
1229
|
+
'''
|
|
1230
|
+
|
|
1231
|
+
def _test_agent_py(self) -> str:
|
|
1232
|
+
return '''"""Agent endpoint tests."""
|
|
1233
|
+
|
|
1234
|
+
import pytest
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
@pytest.mark.asyncio
|
|
1238
|
+
async def test_sample_agent(client):
|
|
1239
|
+
"""Test sample agent endpoint."""
|
|
1240
|
+
response = await client.post("/api/v1/agent/sample", json={"query": "test"})
|
|
1241
|
+
assert response.status_code == 200
|
|
1242
|
+
'''
|
|
1243
|
+
|
|
1244
|
+
def _test_metrics_py(self) -> str:
|
|
1245
|
+
return '''"""Metrics tests."""
|
|
1246
|
+
|
|
1247
|
+
import pytest
|
|
1248
|
+
|
|
1249
|
+
|
|
1250
|
+
@pytest.mark.asyncio
|
|
1251
|
+
async def test_metrics_collection():
|
|
1252
|
+
"""Test metrics collection."""
|
|
1253
|
+
pass
|
|
1254
|
+
'''
|
|
1255
|
+
|
|
1256
|
+
def _test_metrics_endpoint_py(self) -> str:
|
|
1257
|
+
return '''"""Metrics endpoint tests."""
|
|
1258
|
+
|
|
1259
|
+
import pytest
|
|
1260
|
+
|
|
1261
|
+
|
|
1262
|
+
@pytest.mark.asyncio
|
|
1263
|
+
async def test_metrics_endpoint(client):
|
|
1264
|
+
"""Test metrics endpoint."""
|
|
1265
|
+
response = await client.get("/api/v1/metrics")
|
|
1266
|
+
assert response.status_code == 200
|
|
1267
|
+
'''
|
|
1268
|
+
|
|
1269
|
+
def _test_observability_middleware_py(self) -> str:
|
|
1270
|
+
return '''"""Observability middleware tests."""
|
|
1271
|
+
|
|
1272
|
+
import pytest
|
|
1273
|
+
|
|
1274
|
+
|
|
1275
|
+
@pytest.mark.asyncio
|
|
1276
|
+
async def test_observability_middleware(client):
|
|
1277
|
+
"""Test observability middleware."""
|
|
1278
|
+
response = await client.get("/api/v1/health")
|
|
1279
|
+
assert response.status_code == 200
|
|
1280
|
+
'''
|
|
1281
|
+
|
|
1282
|
+
def _test_sqlalchemy_repository_py(self) -> str:
|
|
1283
|
+
return '''"""SQLAlchemy repository tests."""
|
|
1284
|
+
|
|
1285
|
+
import pytest
|
|
1286
|
+
|
|
1287
|
+
|
|
1288
|
+
@pytest.mark.asyncio
|
|
1289
|
+
async def test_repository_base():
|
|
1290
|
+
"""Test base repository."""
|
|
1291
|
+
pass
|
|
1292
|
+
'''
|
|
1293
|
+
|
|
1294
|
+
def _test_tracing_py(self) -> str:
|
|
1295
|
+
return '''"""Tracing tests."""
|
|
1296
|
+
|
|
1297
|
+
import pytest
|
|
1298
|
+
|
|
1299
|
+
|
|
1300
|
+
@pytest.mark.asyncio
|
|
1301
|
+
async def test_tracing():
|
|
1302
|
+
"""Test tracing setup."""
|
|
1303
|
+
pass
|
|
1304
|
+
'''
|
|
1305
|
+
|
|
1306
|
+
def _test_full_stack_py(self) -> str:
|
|
1307
|
+
return '''"""Full stack integration tests."""
|
|
1308
|
+
|
|
1309
|
+
import pytest
|
|
1310
|
+
|
|
1311
|
+
|
|
1312
|
+
@pytest.mark.asyncio
|
|
1313
|
+
async def test_full_stack(client):
|
|
1314
|
+
"""Test full stack integration."""
|
|
1315
|
+
response = await client.get("/api/v1/health")
|
|
1316
|
+
assert response.status_code == 200
|
|
1317
|
+
'''
|
|
1318
|
+
|
|
1319
|
+
def _test_workflows_py(self) -> str:
|
|
1320
|
+
return '''"""End-to-end workflow tests."""
|
|
1321
|
+
|
|
1322
|
+
import pytest
|
|
1323
|
+
|
|
1324
|
+
|
|
1325
|
+
@pytest.mark.asyncio
|
|
1326
|
+
async def test_workflow(client):
|
|
1327
|
+
"""Test complete workflow."""
|
|
1328
|
+
pass
|
|
1329
|
+
'''
|