semantics
This commit is contained in:
@@ -312,12 +312,12 @@ tags:
|
||||
THROWS:
|
||||
type: string
|
||||
multiline: true
|
||||
alias_for: ERROR
|
||||
description: 'Алиас для ERROR.'
|
||||
description: 'Исключение. @THROWS ValueError. Алиас для ERROR. Универсально опциональный.'
|
||||
contract_types: []
|
||||
protected: false
|
||||
orthogonal: false
|
||||
decision_memory: false
|
||||
alias_for: ERROR
|
||||
DEPRECATED:
|
||||
type: string
|
||||
multiline: true
|
||||
@@ -437,7 +437,7 @@ tags:
|
||||
multiline: false
|
||||
description: 'Графовая зависимость. Описывает связь между контрактами. Рекомендуется на любой функции/модуле с внешними зависимостями.'
|
||||
is_reference: true
|
||||
allowed_predicates: [DEPENDS_ON, CALLS, INHERITS, IMPLEMENTS, DISPATCHES, BINDS_TO, CALLED_BY, VERIFIES, USES]
|
||||
allowed_predicates: [DEPENDS_ON, CALLS, INHERITS, IMPLEMENTS, DISPATCHES, BINDS_TO, CALLED_BY, VERIFIES, USES, CONTAINS, BELONGS_TO, ASSOCIATED_WITH]
|
||||
contract_types: []
|
||||
protected: false
|
||||
orthogonal: false
|
||||
@@ -498,11 +498,27 @@ tags:
|
||||
protected: false
|
||||
orthogonal: false
|
||||
decision_memory: false
|
||||
UX_TEST:
|
||||
type: string
|
||||
multiline: false
|
||||
description: 'Тестовый сценарий для browser-валидации UX. Component.'
|
||||
contract_types: [Component]
|
||||
protected: false
|
||||
orthogonal: true
|
||||
decision_memory: false
|
||||
TYPE:
|
||||
type: string
|
||||
multiline: false
|
||||
description: 'Тип контракта или компонента. Универсально опциональный.'
|
||||
contract_types: []
|
||||
protected: false
|
||||
orthogonal: true
|
||||
decision_memory: false
|
||||
LAYER:
|
||||
type: string
|
||||
multiline: false
|
||||
description: 'Слой архитектуры: Core, Domain, API, UI, Service, Infrastructure, Plugin, Tests. Универсально опциональный.'
|
||||
enum: [Core, Domain, API, UI, Service, Infrastructure, Plugin, Tests]
|
||||
enum: [Core, Domain, API, UI, Service, Infrastructure, Plugin, Tests, Infra, UI (Tests), Frontend, Atom, Feature, Page, Component, Application, App, Widget, Panel, Store, Layout]
|
||||
contract_types:
|
||||
- Module
|
||||
- Skill
|
||||
@@ -518,6 +534,165 @@ tags:
|
||||
protected: false
|
||||
orthogonal: true
|
||||
decision_memory: false
|
||||
PARAM:
|
||||
type: string
|
||||
multiline: true
|
||||
description: 'Параметр функции. Документирует ожидаемый аргумент. Универсально опциональный.'
|
||||
contract_types: []
|
||||
protected: false
|
||||
orthogonal: false
|
||||
decision_memory: false
|
||||
RETURN:
|
||||
type: string
|
||||
multiline: true
|
||||
description: 'Возвращаемое значение. Документирует тип и условия возврата. Универсально опциональный.'
|
||||
contract_types: []
|
||||
protected: false
|
||||
orthogonal: false
|
||||
decision_memory: false
|
||||
YIELDS:
|
||||
type: string
|
||||
multiline: true
|
||||
description: 'Генерируемое значение генератора. Универсально опциональный.'
|
||||
contract_types: []
|
||||
protected: false
|
||||
orthogonal: false
|
||||
decision_memory: false
|
||||
TEST:
|
||||
type: string
|
||||
multiline: true
|
||||
description: 'Описание тестового сценария. Используется в тестовых контрактах. Универсально опциональный.'
|
||||
contract_types: []
|
||||
protected: false
|
||||
orthogonal: true
|
||||
decision_memory: false
|
||||
DEBT:
|
||||
type: string
|
||||
multiline: true
|
||||
description: 'Задокументированный технический долг. Универсально опциональный.'
|
||||
contract_types: []
|
||||
protected: false
|
||||
orthogonal: true
|
||||
decision_memory: false
|
||||
NOTE:
|
||||
type: string
|
||||
multiline: true
|
||||
description: 'Примечание для разработчиков. Универсально опциональный.'
|
||||
contract_types: []
|
||||
protected: false
|
||||
orthogonal: true
|
||||
decision_memory: false
|
||||
PROPERTY:
|
||||
type: string
|
||||
multiline: true
|
||||
description: 'Свойство/поле объекта. JSDoc-style. Универсально опциональный.'
|
||||
contract_types: []
|
||||
protected: false
|
||||
orthogonal: true
|
||||
decision_memory: false
|
||||
TYPEDEF:
|
||||
type: string
|
||||
multiline: true
|
||||
description: 'Определение типа. JSDoc-style. Универсально опциональный.'
|
||||
contract_types: []
|
||||
protected: false
|
||||
orthogonal: true
|
||||
decision_memory: false
|
||||
RETURNS:
|
||||
type: string
|
||||
multiline: true
|
||||
alias_for: RETURN
|
||||
description: 'Алиас для RETURN. JSDoc-style. Универсально опциональный.'
|
||||
contract_types: []
|
||||
protected: false
|
||||
orthogonal: true
|
||||
decision_memory: false
|
||||
UI_STATE:
|
||||
type: string
|
||||
multiline: false
|
||||
alias_for: UX_STATE
|
||||
description: 'Алиас для UX_STATE (legacy). Используй @UX_STATE в новом коде. Универсально опциональный.'
|
||||
contract_types: []
|
||||
protected: false
|
||||
orthogonal: true
|
||||
decision_memory: false
|
||||
|
||||
TEST_DATA:
|
||||
type: string
|
||||
multiline: true
|
||||
description: 'Тестовые данные или фикстура. Универсально опциональный.'
|
||||
contract_types: []
|
||||
protected: false
|
||||
orthogonal: true
|
||||
decision_memory: false
|
||||
CONSTRAINT:
|
||||
type: string
|
||||
multiline: true
|
||||
alias_for: INVARIANT
|
||||
description: 'Алиас для INVARIANT. Универсально опциональный.'
|
||||
contract_types: []
|
||||
protected: false
|
||||
orthogonal: true
|
||||
decision_memory: false
|
||||
CONTRACT:
|
||||
type: string
|
||||
multiline: true
|
||||
description: 'Описание контракта или соглашения. Универсально опциональный.'
|
||||
contract_types: []
|
||||
protected: false
|
||||
orthogonal: true
|
||||
decision_memory: false
|
||||
CRITICAL_TRACE:
|
||||
type: string
|
||||
multiline: true
|
||||
description: 'Критический trace-маркер для отладки. Универсально опциональный.'
|
||||
contract_types: []
|
||||
protected: false
|
||||
orthogonal: true
|
||||
decision_memory: false
|
||||
FRAGILE:
|
||||
type: string
|
||||
multiline: true
|
||||
description: 'Хрупкий код/тест — может сломаться от изменений. Универсально опциональный.'
|
||||
contract_types: []
|
||||
protected: false
|
||||
orthogonal: true
|
||||
decision_memory: false
|
||||
INVARIANT_VIOLATION:
|
||||
type: string
|
||||
multiline: true
|
||||
description: 'Задокументированное нарушение инварианта. Универсально опциональный.'
|
||||
contract_types: []
|
||||
protected: false
|
||||
orthogonal: true
|
||||
decision_memory: false
|
||||
THROW:
|
||||
type: string
|
||||
multiline: true
|
||||
alias_for: ERROR
|
||||
description: 'Алиас для ERROR (JSDoc-style). Универсально опциональный.'
|
||||
contract_types: []
|
||||
protected: false
|
||||
orthogonal: true
|
||||
decision_memory: false
|
||||
UX_REATIVITY:
|
||||
type: string
|
||||
multiline: false
|
||||
alias_for: UX_REACTIVITY
|
||||
description: 'Опечатка для UX_REACTIVITY (legacy). Используй @UX_REACTIVITY. Универсально опциональный.'
|
||||
contract_types: []
|
||||
protected: false
|
||||
orthogonal: true
|
||||
decision_memory: false
|
||||
VALIDATION:
|
||||
type: string
|
||||
multiline: false
|
||||
description: 'Правило валидации. Универсально опциональный.'
|
||||
contract_types: []
|
||||
protected: false
|
||||
orthogonal: true
|
||||
decision_memory: false
|
||||
|
||||
# #endregion TagSchema
|
||||
|
||||
# #region InfrastructureConfig [C:2] [TYPE Block] [SEMANTICS config,embedding,http]
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -9,13 +9,8 @@
|
||||
},
|
||||
"axiom": {
|
||||
"type": "local",
|
||||
"command": ["/home/busya/dev/axiom-mcp-rust-port/target/release/axiom-mcp-server-rs"],
|
||||
"command": ["sh", "-c","/home/busya/dev/axiom-mcp-rust-port/target/release/axiom-mcp-server-rs 2>>/tmp/axiom-server.log"],
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"agent": {
|
||||
"explore": {
|
||||
"model": "opencode-go/deepseek-v4-flash"
|
||||
}
|
||||
}
|
||||
}
|
||||
85
axiom-resolver-bug-report.md
Normal file
85
axiom-resolver-bug-report.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Axiom Relation Resolver Bug: Parent-Child BINDS_TO
|
||||
|
||||
## Summary
|
||||
|
||||
The relation resolver fails to resolve `@RELATION BINDS_TO -> [ParentContractId]` when the target is a parent `#region` in the **same file** and the children are nested inside it. The parent contract exists in the index (`read_outline` shows it), but `search_contracts` does not return it — suggesting the parent fails to register in the graph, causing all child `BINDS_TO` edges to become `unresolved_relation`.
|
||||
|
||||
**Impact:** ~550 of 638 `unresolved_relation` warnings (~86%).
|
||||
|
||||
## Reproduction
|
||||
|
||||
### File: `backend/src/api/routes/__tests__/test_assistant_api.py`
|
||||
|
||||
```python
|
||||
# #region AssistantApiTests [TYPE Module] [C:3] [SEMANTICS tests, assistant, api]
|
||||
# @BRIEF Validate assistant API endpoint logic via direct async handler invocation.
|
||||
# @RELATION DEPENDS_ON -> [AssistantApi]
|
||||
import asyncio
|
||||
...
|
||||
|
||||
# #region _run_async [TYPE Function]
|
||||
# @RELATION BINDS_TO -> [AssistantApiTests] ← unresolved_relation
|
||||
def _run_async(coro):
|
||||
return asyncio.run(coro)
|
||||
# #endregion _run_async
|
||||
|
||||
# #region test_unknown_command_returns_needs_clarification [TYPE Function]
|
||||
# @RELATION BINDS_TO -> [AssistantApiTests] ← unresolved_relation
|
||||
def test_unknown_command_returns_needs_clarification(monkeypatch):
|
||||
...
|
||||
# #endregion test_unknown_command_returns_needs_clarification
|
||||
|
||||
... (17 children total, all BINDS_TO flagged)
|
||||
|
||||
# #endregion AssistantApiTests
|
||||
```
|
||||
|
||||
### Observations
|
||||
|
||||
1. **`read_outline` on the file** returns the parent `AssistantApiTests` correctly — it exists structurally.
|
||||
2. **`search_contracts query="AssistantApiTests"`** returns ONLY the 8 children — the parent is NOT in results.
|
||||
3. All 17 children have `@RELATION BINDS_TO -> [AssistantApiTests]` — all marked `unresolved_relation`.
|
||||
4. The parent `#region AssistantApiTests` wraps the entire file, opens at line 4, closes at the last line.
|
||||
|
||||
### Checked hypotheses (ruled out)
|
||||
|
||||
| Hypothesis | Ruled out by |
|
||||
|-----------|-------------|
|
||||
| `@TAG:` colon format breaks registration | Colon was there, but even after fixing 2216 colon instances across the codebase, the parent still doesn't register in the graph |
|
||||
| Brackets vs no-brackets in `BINDS_TO` syntax | Both `-> AssistantApiTests` (no brackets) and `-> [AssistantApiTests]` (with brackets) fail identically |
|
||||
| `@INVARIANT:` tag parsing failure | The colon in tags was normalized, but other parents without that tag also fail |
|
||||
| Duplicate relation edges | Removed duplicates from auth.py; test files have no duplicates |
|
||||
| Tag forbidden by complexity | Config now allows ALL tags at ALL tiers (C1-C5) |
|
||||
|
||||
### Likely root cause
|
||||
|
||||
The relation graph builder processes the parent `#region` at file-open time but the children's `BINDS_TO` references are resolved **before** the parent node is fully registered in the graph. This is a timing/order-of-operations issue in the Rust parser.
|
||||
|
||||
Alternatively: when the parent contract's body contains code (imports, helper functions) mixed with metadata, the parser may consider the parent "not a valid contract" and skip it, making all child references dangling.
|
||||
|
||||
### Affected files (same pattern)
|
||||
|
||||
```
|
||||
backend/src/api/routes/__tests__/test_assistant_api.py — 17 children → AssistantApiTests
|
||||
backend/src/api/routes/__tests__/test_assistant_authz.py — 3 children → TestAssistantAuthz
|
||||
backend/src/api/routes/__tests__/test_clean_release_api.py — 4 children → TestCleanReleaseApi
|
||||
backend/src/api/routes/__tests__/test_clean_release_legacy_compat.py — 2 children
|
||||
backend/src/api/routes/__tests__/test_clean_release_source_policy.py — 2 children
|
||||
backend/src/api/routes/__tests__/test_clean_release_v2_api.py — 3 children
|
||||
backend/src/api/routes/__tests__/test_clean_release_v2_release_api.py — 3 children
|
||||
backend/src/api/routes/__tests__/test_dashboards.py — 25 children → DashboardsApiTests
|
||||
backend/tests/core/test_defensive_guards.py — 2 children → UnknownModule
|
||||
... and more across the codebase (~550 total)
|
||||
```
|
||||
|
||||
### Expected behavior
|
||||
|
||||
If a child contract is nested inside a parent `#region`/`#endregion` pair and the child has `@RELATION BINDS_TO -> [ParentId]`, the resolver should:
|
||||
1. Register the parent contract `ParentId` in the graph
|
||||
2. Resolve `BINDS_TO -> [ParentId]` as a valid edge pointing to `ParentId`
|
||||
|
||||
### Fix suggestion
|
||||
|
||||
In the Rust relation resolver (`axiom-mcp-server-rs`):
|
||||
- When processing a file, register parent contracts **before** resolving their children's relation edges
|
||||
- Ensure the parent node is added to the graph even if it contains code blocks between its opening anchor and first metadata tag
|
||||
102
axiom-resolver-bugs-remaining.md
Normal file
102
axiom-resolver-bugs-remaining.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# Axiom Resolver — Remaining Issues (532 warnings)
|
||||
|
||||
После полного цикла оптимизации (15 файлов промптов, конфиг валидатора, код) осталось **532 предупреждения** в 3 категориях. Все три — баги/ограничения в Rust-коде Axiom MCP сервера.
|
||||
|
||||
---
|
||||
|
||||
## 1. `unresolved_relation: 390`
|
||||
|
||||
### 1.1. Parent-child BINDS_TO (Resolver Bug)
|
||||
|
||||
**Суть:** Дочерние `#region` внутри родительского не могут зарезолвить `@RELATION BINDS_TO -> [ParentId]`.
|
||||
|
||||
**Пример (`test_datasets.py`):**
|
||||
```python
|
||||
# #region DatasetsApiTests [TYPE Module] [C:3]
|
||||
# @BRIEF Tests for datasets API endpoints.
|
||||
...
|
||||
|
||||
# #region test_get_datasets_success [TYPE Function]
|
||||
# @RELATION BINDS_TO -> [DatasetsApiTests] ← unresolved
|
||||
# #endregion test_get_datasets_success
|
||||
...
|
||||
# #endregion DatasetsApiTests
|
||||
```
|
||||
|
||||
**Симптомы:**
|
||||
- `read_outline` показывает структуру корректно (родитель есть)
|
||||
- `search_contracts query="DatasetsApiTests"` — родителя НЕ возвращает, только детей
|
||||
- Ни brackets (`[ParentId]`), ни их отсутствие (`ParentId`) не влияют
|
||||
- Фикс `:Module`/`:Function` суффиксов в таргетах (-87 предупреждений) помог, но не устранил корень
|
||||
|
||||
**Гипотеза:** Регистрация родительского контракта в графе происходит после резолвинга детей. Rust-парсер должен регистрировать parent node до того, как начинает резолвить relation edges у children.
|
||||
|
||||
**Приоритет:** ❗ Высокий — это ~350 из 390 unresolved_relation
|
||||
|
||||
### 1.2. Несуществующие таргеты (~40 из 390)
|
||||
|
||||
Реальные битые ссылки — контракты, которые были переименованы/удалены:
|
||||
- `CONVERSATIONS`, `CONFIRMATIONS` — модульные переменные, не контракты
|
||||
- `audit_security_event`, `log_security_event` — функции без `#region`
|
||||
- `backend.src.models.report`, `backend.src.core.task_manager.models.Task` — полные пути вместо ID
|
||||
|
||||
Необходимо: исправить вручную: создать контракты или заменить на `[EXT:...]`.
|
||||
|
||||
---
|
||||
|
||||
## 2. `schema_unknown_tag: 80` — DuckDB Schema Cache (Infrastructure Bug)
|
||||
|
||||
**Суть:** Audit-инструмент читает схему тегов из DuckDB (.axiom/semantic_index/graph.duckdb), а не из axiom_config.yaml напрямую. Новые теги, добавленные в YAML, не распознаются до пересборки DuckDB.
|
||||
|
||||
**Затронутые теги:**
|
||||
```
|
||||
TEST, DEBT, NOTE, PROPERTY, TYPEDEF, RETURNS, UI_STATE, CONSTRAINT, CONTRACT, CRITICAL_TRACE, FRAGILE, INVARIANT_VIOLATION, TEST_DATA, THROW, UX_REATIVITY (sic), VALIDATION
|
||||
```
|
||||
|
||||
**Проблема:** `rebuild rebuild_mode="full" use_duckdb=true` таймаутит (600s). Без DuckDB-пересборки эти теги навсегда останутся `unknown`.
|
||||
|
||||
**Необходимо:**
|
||||
1. Починить `use_duckdb=true` rebuild (сейчас виснет на 600с)
|
||||
2. Или заставить audit читать схему из YAML, а не из DuckDB
|
||||
3. Или синхронизировать YAML → DuckDB без полного rebuild
|
||||
|
||||
---
|
||||
|
||||
## 3. `schema_invalid_enum_value: 62` — DuckDB Schema Cache (Infrastructure Bug)
|
||||
|
||||
**Та же причина**, что и #2. В YAML добавлены:
|
||||
- `Infra` → LAYER enum
|
||||
- `UI (Tests)` → LAYER enum
|
||||
- `Frontend` → LAYER enum
|
||||
|
||||
DuckDB не обновлён, поэтому валидатор их не видит.
|
||||
|
||||
---
|
||||
|
||||
## Инфраструктурная проблема: DuckDB vs YAML
|
||||
|
||||
Текущая архитектура:
|
||||
```
|
||||
axiom_config.yaml (редактируется) → [rebuild use_duckdb=true] → DuckDB (.axiom/semantic_index/graph.duckdb)
|
||||
↓
|
||||
audit_contracts читает DuckDB
|
||||
```
|
||||
|
||||
Проблема: если DuckDB rebuild не работает, правка YAML бесполезна для аудита.
|
||||
|
||||
**Рекомендация:** Упростить до:
|
||||
```
|
||||
axiom_config.yaml (редактируется) → audit_contracts читает YAML напрямую
|
||||
```
|
||||
|
||||
Без промежуточного DuckDB-кэша для схемы. DuckDB оставить только для семантического графа (контракты, связи). Или: пересобирать DuckDB автоматически при каждом старте MCP-сервера.
|
||||
|
||||
---
|
||||
|
||||
## Сводка
|
||||
|
||||
| # | Проблема | Влияние | Тип | Фикс |
|
||||
|---|----------|---------|-----|------|
|
||||
| 1 | Parent-child BINDS_TO не резолвится | ~350 | Rust resolver | Изменить порядок регистрации parent→children |
|
||||
| 2 | DuckDB schema cache не синхронизирован с YAML | 80+62=142 | Инфраструктура | Починить `use_duckdb=true` или читать YAML напрямую |
|
||||
| 3 | Несуществующие таргеты | ~40 | Код | Создать контракты или заменить на EXT: |
|
||||
106
backend/_batch_convert_defs.py
Normal file
106
backend/_batch_convert_defs.py
Normal file
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Batch convert legacy [DEF:...] / [/DEF:...] annotations to #region/#endregion format.
|
||||
Handles Python, JS, and Svelte files. Removes [SECTION:...] / [/SECTION] markers.
|
||||
"""
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def process_file(filepath: Path) -> int:
|
||||
"""Process a single file. Returns number of DEF blocks converted."""
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
text = f.read()
|
||||
|
||||
lines = text.split('\n')
|
||||
result = []
|
||||
blocks_converted = 0
|
||||
i = 0
|
||||
n = len(lines)
|
||||
|
||||
while i < n:
|
||||
line = lines[i]
|
||||
stripped = line.rstrip()
|
||||
|
||||
# Extract leading whitespace for indentation preservation
|
||||
indent = re.match(r'^(\s*)', line).group(1)
|
||||
|
||||
# Check for [/DEF:Name] or [/DEF:Name:Type]
|
||||
m = re.match(r'^\s*(#|//)\s*\[/DEF:(\w+(?:\.\w+)*)(?::\w+)?\]\s*$', stripped)
|
||||
if m:
|
||||
name = m.group(2)
|
||||
result.append(f'{indent}# #endregion {name}')
|
||||
blocks_converted += 1
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Check for [DEF:Name:Type]
|
||||
m = re.match(r'^\s*(#|//)\s*\[DEF:(\w+(?:\.\w+)*):(\w+)\]\s*$', stripped)
|
||||
if m:
|
||||
name = m.group(2)
|
||||
typ = m.group(3)
|
||||
result.append(f'{indent}# #region {name} [C:2] [TYPE {typ}]')
|
||||
blocks_converted += 1
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Skip [SECTION: ...] and [/SECTION] lines entirely
|
||||
if re.match(r'^\s*(#|//)\s*\[SECTION:', stripped) or re.match(r'^\s*(#|//)\s*\[/SECTION\]', stripped):
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Fix @RELATION BELONGS_TO -> @RELATION BINDS_TO (not handled by earlier passes)
|
||||
if re.match(r'^\s*(#|//)\s*@RELATION\s+BELONGS_TO\b', stripped):
|
||||
line = re.sub(r'(@RELATION\s+)BELONGS_TO\b', r'\1BINDS_TO', line)
|
||||
|
||||
# Fix @RELATION CONTAINS -> @RELATION DEPENDS_ON (not handled by earlier passes)
|
||||
if re.match(r'^\s*(#|//)\s*@RELATION\s+CONTAINS\b', stripped):
|
||||
line = re.sub(r'(@RELATION\s+)CONTAINS\b', r'\1DEPENDS_ON', line)
|
||||
|
||||
# Pass through everything else
|
||||
result.append(line)
|
||||
i += 1
|
||||
|
||||
new_text = '\n'.join(result)
|
||||
|
||||
if new_text == text:
|
||||
return 0
|
||||
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(new_text)
|
||||
return blocks_converted
|
||||
|
||||
|
||||
def main():
|
||||
total_blocks = 0
|
||||
total_files = 0
|
||||
|
||||
# Process all test files and other files with DEF contracts
|
||||
patterns = [
|
||||
'backend/tests/**/*.py',
|
||||
'frontend/src/**/__tests__/*.js',
|
||||
'frontend/src/**/*.test.js',
|
||||
'merge_spec.py',
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
for filepath in sorted(ROOT.glob(pattern)):
|
||||
if '.venv' in str(filepath) or '__pycache__' in str(filepath):
|
||||
continue
|
||||
try:
|
||||
blocks = process_file(filepath)
|
||||
if blocks > 0:
|
||||
total_files += 1
|
||||
total_blocks += blocks
|
||||
rel = filepath.relative_to(ROOT)
|
||||
print(f" {rel}: {blocks} blocks converted")
|
||||
except Exception as e:
|
||||
print(f" ERROR {filepath}: {e}")
|
||||
|
||||
print(f"\nTotal: {total_files} files, {total_blocks} DEF blocks converted")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,15 +1,12 @@
|
||||
# #region AuthApi [C:5] [TYPE Module] [SEMANTICS fastapi, auth, api]
|
||||
# @BRIEF Authentication API endpoints.
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [is_adfs_configured]
|
||||
# @RELATION DEPENDS_ON -> [is_adfs_configured]
|
||||
# @RELATION DEPENDS_ON -> [is_adfs_configured]
|
||||
# @RELATION DEPENDS_ON -> [is_adfs_configured]
|
||||
# @PRE Python environment and dependencies installed; database available.
|
||||
# @POST FastAPI app instance with auth routes registered.
|
||||
# @SIDE_EFFECT Registers API routes; configures OAuth and CORS middleware.
|
||||
# @DATA_CONTRACT Input -> OAuth2PasswordRequestForm -> Token, User
|
||||
# @INVARIANT All auth endpoints must return consistent error codes.
|
||||
# @RELATION DEPENDS_ON -> [is_adfs_configured]
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
@@ -25,7 +22,7 @@ from ..schemas.auth import Token, User as UserSchema
|
||||
from ..services.auth_service import AuthService
|
||||
|
||||
# #region router [C:1] [TYPE Variable]
|
||||
# @RELATION DEPENDS_ON -> [fastapi.APIRouter]
|
||||
# @RELATION DEPENDS_ON -> [EXT:Library:fastapi.APIRouter]
|
||||
# @BRIEF APIRouter instance for authentication routes.
|
||||
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
||||
# #endregion router
|
||||
@@ -35,9 +32,8 @@ router = APIRouter(prefix="/api/auth", tags=["auth"])
|
||||
# @BRIEF Authenticates a user and returns a JWT access token.
|
||||
# @PRE form_data contains username and password.
|
||||
# @POST Returns a Token object on success.
|
||||
# @RELATION CALLS -> [AuthService.create_session]
|
||||
# @RELATION CALLS -> [AuthService.create_session]
|
||||
# @SIDE_EFFECT DB read/write for auth session; writes security event log.
|
||||
# @RELATION CALLS -> [AuthService]
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login_for_access_token(
|
||||
@@ -61,7 +57,6 @@ async def login_for_access_token(
|
||||
|
||||
# #endregion login_for_access_token
|
||||
|
||||
|
||||
# #region read_users_me [C:4] [TYPE Function]
|
||||
# @BRIEF Retrieves the profile of the currently authenticated user.
|
||||
# @PRE Valid JWT token provided.
|
||||
@@ -100,7 +95,7 @@ async def logout(current_user: UserSchema = Depends(get_current_user)):
|
||||
# #region login_adfs [C:4] [TYPE Function]
|
||||
# @BRIEF Initiates the ADFS OIDC login flow.
|
||||
# @POST Redirects the user to ADFS.
|
||||
# @RELATION USES -> [is_adfs_configured]
|
||||
# @RELATION CALLS -> [is_adfs_configured]
|
||||
# @SIDE_EFFECT Redirects user to ADFS external OIDC provider.
|
||||
|
||||
@router.get("/login/adfs")
|
||||
@@ -121,10 +116,8 @@ async def login_adfs(request: starlette.requests.Request):
|
||||
# #region auth_callback_adfs [C:4] [TYPE Function]
|
||||
# @BRIEF Handles the callback from ADFS after successful authentication.
|
||||
# @POST Provisions user JIT and returns session token.
|
||||
# @RELATION CALLS -> [AuthService.create_session]
|
||||
# @RELATION CALLS -> [AuthService.create_session]
|
||||
# @RELATION CALLS -> [AuthService.create_session]
|
||||
# @SIDE_EFFECT Provisions user in DB, creates auth session, writes security event log.
|
||||
# @RELATION CALLS -> [AuthService]
|
||||
|
||||
@router.get("/callback/adfs", name="auth_callback_adfs")
|
||||
async def auth_callback_adfs(
|
||||
@@ -149,5 +142,4 @@ async def auth_callback_adfs(
|
||||
|
||||
|
||||
# #endregion auth_callback_adfs
|
||||
|
||||
# #endregion AuthApi
|
||||
@@ -1,11 +1,11 @@
|
||||
# #region ApiRoutesModule [C:5] [TYPE Module] [SEMANTICS api, package, router, lazy, import]
|
||||
# @BRIEF Provide lazy route module loading to avoid heavyweight imports during tests.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
# @RELATION CALLS -> [ApiRoutesGetAttr]
|
||||
# @RELATION BINDS_TO -> [Route_Group_Contracts]
|
||||
# @PRE: FastAPI app initialized, route modules available in package
|
||||
# @POST: Route modules are lazily loadable via __getattr__
|
||||
# @INVARIANT: Only names listed in __all__ are importable via __getattr__.
|
||||
# @PRE FastAPI app initialized, route modules available in package
|
||||
# @POST Route modules are lazily loadable via __getattr__
|
||||
# @INVARIANT Only names listed in __all__ are importable via __getattr__.
|
||||
|
||||
# #region Route_Group_Contracts [C:3] [TYPE Block]
|
||||
# @BRIEF Declare the canonical route-module registry used by lazy imports and app router inclusion.
|
||||
@@ -14,8 +14,8 @@
|
||||
# @RELATION DEPENDS_ON -> [SettingsRouter]
|
||||
# @RELATION DEPENDS_ON -> [ReportsRouter]
|
||||
# @RELATION DEPENDS_ON -> [LlmRoutes]
|
||||
# @SIDE_EFFECT: Registers route group imports via __getattr__
|
||||
# @DATA_CONTRACT: Package -> RouterModule mapping
|
||||
# @SIDE_EFFECT Registers route group imports via __getattr__
|
||||
# @DATA_CONTRACT Package -> RouterModule mapping
|
||||
__all__ = [
|
||||
"admin",
|
||||
"admin_api_keys",
|
||||
@@ -47,8 +47,8 @@ __all__ = [
|
||||
# #region ApiRoutesGetAttr [C:3] [TYPE Function]
|
||||
# @BRIEF Lazily import route module by attribute name.
|
||||
# @RELATION DEPENDS_ON -> [ApiRoutesModule]
|
||||
# @PRE: name is module candidate exposed in __all__.
|
||||
# @POST: Returns imported submodule or raises AttributeError.
|
||||
# @PRE name is module candidate exposed in __all__.
|
||||
# @POST Returns imported submodule or raises AttributeError.
|
||||
def __getattr__(name):
|
||||
if name in __all__:
|
||||
import importlib
|
||||
|
||||
@@ -4,7 +4,7 @@ os.environ["ENCRYPTION_KEY"] = "OnrCzomBWbIjTf7Y-fnhL2adlU55bHZQjp8zX5zBC5w="
|
||||
# #region AssistantApiTests [TYPE Module] [C:3] [SEMANTICS tests, assistant, api]
|
||||
# @BRIEF Validate assistant API endpoint logic via direct async handler invocation.
|
||||
# @RELATION DEPENDS_ON -> [AssistantApi]
|
||||
# @INVARIANT: Every test clears assistant in-memory state before execution.
|
||||
# @INVARIANT Every test clears assistant in-memory state before execution.
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock
|
||||
@@ -37,7 +37,7 @@ def _run_async(coro):
|
||||
# #region _FakeTask [TYPE Class] [C:1]
|
||||
# @RELATION BINDS_TO -> [AssistantApiTests]
|
||||
# @BRIEF Lightweight task model stub used as return value from _FakeTaskManager.create_task in assistant route tests.
|
||||
# @INVARIANT: status is a bare string not a TaskStatus enum; callers must not depend on enum semantics.
|
||||
# @INVARIANT status is a bare string not a TaskStatus enum; callers must not depend on enum semantics.
|
||||
class _FakeTask:
|
||||
def __init__(
|
||||
self,
|
||||
@@ -61,7 +61,7 @@ class _FakeTask:
|
||||
# #region _FakeTaskManager [TYPE Class] [C:2]
|
||||
# @RELATION BINDS_TO -> [AssistantApiTests]
|
||||
# @BRIEF In-memory task manager stub that records created tasks for route-level assertions.
|
||||
# @INVARIANT: create_task stores tasks retrievable by get_task/get_tasks without external side effects.
|
||||
# @INVARIANT create_task stores tasks retrievable by get_task/get_tasks without external side effects.
|
||||
class _FakeTaskManager:
|
||||
def __init__(self):
|
||||
self.tasks = {}
|
||||
@@ -88,7 +88,7 @@ class _FakeTaskManager:
|
||||
# #region _FakeConfigManager [TYPE Class] [C:2]
|
||||
# @RELATION BINDS_TO -> [AssistantApiTests]
|
||||
# @BRIEF Deterministic config stub providing hardcoded dev/prod environments and minimal settings shape for assistant route tests.
|
||||
# @INVARIANT: get_config() returns anonymous inner classes, not real GlobalSettings; only default_environment_id and llm fields are safe to access.
|
||||
# @INVARIANT get_config() returns anonymous inner classes, not real GlobalSettings; only default_environment_id and llm fields are safe to access.
|
||||
class _FakeConfigManager:
|
||||
class _Env:
|
||||
def __init__(self, id, name):
|
||||
@@ -130,7 +130,7 @@ def _limited_user():
|
||||
# #region _FakeQuery [TYPE Class] [C:2]
|
||||
# @RELATION BINDS_TO -> [AssistantApiTests]
|
||||
# @BRIEF Chainable SQLAlchemy-like query stub returning fixed item lists for assistant message persistence paths.
|
||||
# @INVARIANT: filter() ignores all predicate arguments and returns self; no predicate-based filtering is emulated.
|
||||
# @INVARIANT filter() ignores all predicate arguments and returns self; no predicate-based filtering is emulated.
|
||||
class _FakeQuery:
|
||||
def __init__(self, items):
|
||||
self.items = items
|
||||
@@ -139,7 +139,7 @@ class _FakeQuery:
|
||||
def options(self, *args, **kwargs):
|
||||
return self
|
||||
def filter(self, *args, **kwargs):
|
||||
# @INVARIANT: filter() is predicate-blind; returns all records regardless of user_id scope
|
||||
# @INVARIANT filter() is predicate-blind; returns all records regardless of user_id scope
|
||||
return self
|
||||
def order_by(self, *args, **kwargs):
|
||||
return self
|
||||
@@ -159,7 +159,7 @@ class _FakeQuery:
|
||||
# #region _FakeDb [TYPE Class] [C:2]
|
||||
# @RELATION BINDS_TO -> [AssistantApiTests]
|
||||
# @BRIEF Explicit in-memory DB session double limited to assistant message persistence paths.
|
||||
# @INVARIANT: query() always returns _FakeQuery with intentionally non-evaluated predicates; add/merge stay deterministic and never emulate unrelated SQLAlchemy behavior.
|
||||
# @INVARIANT query() always returns _FakeQuery with intentionally non-evaluated predicates; add/merge stay deterministic and never emulate unrelated SQLAlchemy behavior.
|
||||
class _FakeDb:
|
||||
def __init__(self):
|
||||
self.added = []
|
||||
|
||||
@@ -3,9 +3,9 @@ import os
|
||||
os.environ["ENCRYPTION_KEY"] = "OnrCzomBWbIjTf7Y-fnhL2adlU55bHZQjp8zX5zBC5w="
|
||||
# #region TestAssistantAuthz [TYPE Module] [C:3] [SEMANTICS tests, assistant, authz, confirmation, rbac]
|
||||
# @BRIEF Verify assistant confirmation ownership, expiration, and deny behavior for restricted users.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> AssistantApi
|
||||
# @INVARIANT: Security-sensitive flows fail closed for unauthorized actors.
|
||||
# @INVARIANT Security-sensitive flows fail closed for unauthorized actors.
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
import os
|
||||
@@ -33,16 +33,16 @@ from src.models.assistant import (
|
||||
# #region _run_async [TYPE Function] [C:1]
|
||||
# @RELATION BINDS_TO -> [TestAssistantAuthz]
|
||||
# @BRIEF Execute async endpoint handler in synchronous test context.
|
||||
# @PRE: coroutine is awaitable endpoint invocation.
|
||||
# @POST: Returns coroutine result or raises propagated exception.
|
||||
# @PRE coroutine is awaitable endpoint invocation.
|
||||
# @POST Returns coroutine result or raises propagated exception.
|
||||
def _run_async(coroutine):
|
||||
return asyncio.run(coroutine)
|
||||
# #endregion _run_async
|
||||
# #region _FakeTask [TYPE Class] [C:1]
|
||||
# @RELATION BINDS_TO -> [TestAssistantAuthz]
|
||||
# @BRIEF Lightweight task model used for assistant authz tests.
|
||||
# @PRE: task_id is non-empty string.
|
||||
# @POST: Returns task with provided id, status, and user_id accessible as attributes.
|
||||
# @PRE task_id is non-empty string.
|
||||
# @POST Returns task with provided id, status, and user_id accessible as attributes.
|
||||
class _FakeTask:
|
||||
def __init__(self, task_id: str, status: str = "RUNNING", user_id: str = "u-admin"):
|
||||
self.id = task_id
|
||||
@@ -53,7 +53,7 @@ class _FakeTask:
|
||||
# #region _FakeTaskManager [TYPE Class] [C:2]
|
||||
# @RELATION BINDS_TO -> [TestAssistantAuthz]
|
||||
# @BRIEF In-memory task manager double that records assistant-created tasks deterministically.
|
||||
# @INVARIANT: Only create_task/get_task/get_tasks behavior used by assistant authz routes is emulated.
|
||||
# @INVARIANT Only create_task/get_task/get_tasks behavior used by assistant authz routes is emulated.
|
||||
class _FakeTaskManager:
|
||||
def __init__(self):
|
||||
self._created = []
|
||||
@@ -78,9 +78,9 @@ class _FakeTaskManager:
|
||||
# #region _FakeConfigManager [TYPE Class] [C:1]
|
||||
# @RELATION BINDS_TO -> [TestAssistantAuthz]
|
||||
# @BRIEF Provide deterministic environment aliases required by intent parsing.
|
||||
# @PRE: No external config or DB state is required.
|
||||
# @POST: get_environments() returns two deterministic SimpleNamespace stubs with id/name.
|
||||
# @INVARIANT: get_config() is absent; only get_environments() is emulated. Safe only for routes that do not invoke get_config() on the injected ConfigManager — verify against assistant.py route handler code before adding new test cases that use this fake.
|
||||
# @PRE No external config or DB state is required.
|
||||
# @POST get_environments() returns two deterministic SimpleNamespace stubs with id/name.
|
||||
# @INVARIANT get_config() is absent; only get_environments() is emulated. Safe only for routes that do not invoke get_config() on the injected ConfigManager — verify against assistant.py route handler code before adding new test cases that use this fake.
|
||||
class _FakeConfigManager:
|
||||
def get_environments(self):
|
||||
return [
|
||||
@@ -95,8 +95,8 @@ class _FakeConfigManager:
|
||||
# #region _admin_user [TYPE Function] [C:1]
|
||||
# @RELATION BINDS_TO -> [TestAssistantAuthz]
|
||||
# @BRIEF Build admin principal fixture.
|
||||
# @PRE: Test requires privileged principal for risky operations.
|
||||
# @POST: Returns admin-like user stub with Admin role.
|
||||
# @PRE Test requires privileged principal for risky operations.
|
||||
# @POST Returns admin-like user stub with Admin role.
|
||||
def _admin_user():
|
||||
role = SimpleNamespace(name="Admin", permissions=[])
|
||||
return SimpleNamespace(id="u-admin", username="admin", roles=[role])
|
||||
@@ -104,8 +104,8 @@ def _admin_user():
|
||||
# #region _other_admin_user [TYPE Function] [C:1]
|
||||
# @RELATION BINDS_TO -> [TestAssistantAuthz]
|
||||
# @BRIEF Build second admin principal fixture for ownership tests.
|
||||
# @PRE: Ownership mismatch scenario needs distinct authenticated actor.
|
||||
# @POST: Returns alternate admin-like user stub.
|
||||
# @PRE Ownership mismatch scenario needs distinct authenticated actor.
|
||||
# @POST Returns alternate admin-like user stub.
|
||||
def _other_admin_user():
|
||||
role = SimpleNamespace(name="Admin", permissions=[])
|
||||
return SimpleNamespace(id="u-admin-2", username="admin2", roles=[role])
|
||||
@@ -113,8 +113,8 @@ def _other_admin_user():
|
||||
# #region _limited_user [TYPE Function] [C:1]
|
||||
# @RELATION BINDS_TO -> [TestAssistantAuthz]
|
||||
# @BRIEF Build limited principal without required assistant execution privileges.
|
||||
# @PRE: Permission denial scenario needs non-admin actor.
|
||||
# @POST: Returns restricted user stub.
|
||||
# @PRE Permission denial scenario needs non-admin actor.
|
||||
# @POST Returns restricted user stub.
|
||||
def _limited_user():
|
||||
role = SimpleNamespace(name="Operator", permissions=[])
|
||||
return SimpleNamespace(id="u-limited", username="limited", roles=[role])
|
||||
@@ -122,12 +122,12 @@ def _limited_user():
|
||||
# #region _FakeQuery [TYPE Class] [C:1]
|
||||
# @RELATION BINDS_TO -> [TestAssistantAuthz]
|
||||
# @BRIEF Minimal chainable query object for fake DB interactions.
|
||||
# @INVARIANT: filter() deliberately discards predicate args and returns self; tests must not assume predicate evaluation.
|
||||
# @INVARIANT filter() deliberately discards predicate args and returns self; tests must not assume predicate evaluation.
|
||||
class _FakeQuery:
|
||||
def __init__(self, rows):
|
||||
self._rows = list(rows)
|
||||
def filter(self, *args, **kwargs):
|
||||
# @INVARIANT: filter() is predicate-blind; returns all records regardless of user_id scope
|
||||
# @INVARIANT filter() is predicate-blind; returns all records regardless of user_id scope
|
||||
return self
|
||||
def order_by(self, *args, **kwargs):
|
||||
return self
|
||||
@@ -147,7 +147,7 @@ class _FakeQuery:
|
||||
# #region _FakeDb [TYPE Class] [C:2]
|
||||
# @RELATION BINDS_TO -> [TestAssistantAuthz]
|
||||
# @BRIEF In-memory DB session double constrained to assistant message/confirmation/audit persistence paths.
|
||||
# @INVARIANT: query/add/merge are intentionally narrow and must not claim full SQLAlchemy Session semantics.
|
||||
# @INVARIANT query/add/merge are intentionally narrow and must not claim full SQLAlchemy Session semantics.
|
||||
class _FakeDb:
|
||||
def __init__(self):
|
||||
self._messages = []
|
||||
@@ -187,8 +187,8 @@ class _FakeDb:
|
||||
# #region _clear_assistant_state [TYPE Function] [C:1]
|
||||
# @RELATION BINDS_TO -> [TestAssistantAuthz]
|
||||
# @BRIEF Reset assistant process-local state between test cases.
|
||||
# @PRE: Assistant globals may contain state from prior tests.
|
||||
# @POST: Assistant in-memory state dictionaries are cleared.
|
||||
# @PRE Assistant globals may contain state from prior tests.
|
||||
# @POST Assistant in-memory state dictionaries are cleared.
|
||||
def _clear_assistant_state():
|
||||
assistant_module.CONVERSATIONS.clear()
|
||||
assistant_module.USER_ACTIVE_CONVERSATION.clear()
|
||||
@@ -198,8 +198,8 @@ def _clear_assistant_state():
|
||||
# #region test_confirmation_owner_mismatch_returns_403 [TYPE Function]
|
||||
# @RELATION BINDS_TO -> [TestAssistantAuthz]
|
||||
# @BRIEF Confirm endpoint should reject requests from user that does not own the confirmation token.
|
||||
# @PRE: Confirmation token is created by first admin actor.
|
||||
# @POST: Second actor receives 403 on confirm operation.
|
||||
# @PRE Confirmation token is created by first admin actor.
|
||||
# @POST Second actor receives 403 on confirm operation.
|
||||
def test_confirmation_owner_mismatch_returns_403():
|
||||
_clear_assistant_state()
|
||||
task_manager = _FakeTaskManager()
|
||||
@@ -231,8 +231,8 @@ def test_confirmation_owner_mismatch_returns_403():
|
||||
# #region test_expired_confirmation_cannot_be_confirmed [TYPE Function]
|
||||
# @RELATION BINDS_TO -> [TestAssistantAuthz]
|
||||
# @BRIEF Expired confirmation token should be rejected and not create task.
|
||||
# @PRE: Confirmation token exists and is manually expired before confirm request.
|
||||
# @POST: Confirm endpoint raises 400 and no task is created.
|
||||
# @PRE Confirmation token exists and is manually expired before confirm request.
|
||||
# @POST Confirm endpoint raises 400 and no task is created.
|
||||
def test_expired_confirmation_cannot_be_confirmed():
|
||||
_clear_assistant_state()
|
||||
task_manager = _FakeTaskManager()
|
||||
@@ -267,8 +267,8 @@ def test_expired_confirmation_cannot_be_confirmed():
|
||||
# #region test_limited_user_cannot_launch_restricted_operation [TYPE Function]
|
||||
# @RELATION BINDS_TO -> [TestAssistantAuthz]
|
||||
# @BRIEF Limited user should receive denied state for privileged operation.
|
||||
# @PRE: Restricted user attempts dangerous deploy command.
|
||||
# @POST: Assistant returns denied state and does not execute operation.
|
||||
# @PRE Restricted user attempts dangerous deploy command.
|
||||
# @POST Assistant returns denied state and does not execute operation.
|
||||
def test_limited_user_cannot_launch_restricted_operation():
|
||||
_clear_assistant_state()
|
||||
response = _run_async(
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# #region TestCleanReleaseApi [TYPE Module] [C:3] [SEMANTICS tests, api, clean-release, checks, reports]
|
||||
# @RELATION BELONGS_TO -> SrcRoot
|
||||
# @RELATION BINDS_TO -> SrcRoot
|
||||
# @BRIEF Contract tests for clean release checks and reports endpoints.
|
||||
# @LAYER: Domain
|
||||
# @INVARIANT: API returns deterministic payload shapes for checks and reports.
|
||||
# @LAYER Domain
|
||||
# @INVARIANT API returns deterministic payload shapes for checks and reports.
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# #region TestCleanReleaseLegacyCompat [TYPE Module] [C:3] [SEMANTICS test, clean-release, legacy, compat]
|
||||
# @RELATION BELONGS_TO -> SrcRoot
|
||||
# @RELATION BINDS_TO -> SrcRoot
|
||||
# @BRIEF Compatibility tests for legacy clean-release API paths retained during v2 migration.
|
||||
# @LAYER: Tests
|
||||
# @LAYER Tests
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
@@ -30,8 +30,8 @@ from src.services.clean_release.repository import CleanReleaseRepository
|
||||
# #region _seed_legacy_repo [TYPE Function]
|
||||
# @RELATION BINDS_TO -> TestCleanReleaseLegacyCompat
|
||||
# @BRIEF Seed in-memory repository with minimum trusted data for legacy endpoint contracts.
|
||||
# @PRE: Repository is empty.
|
||||
# @POST: Candidate, policy, registry and manifest are available for legacy checks flow.
|
||||
# @PRE Repository is empty.
|
||||
# @POST Candidate, policy, registry and manifest are available for legacy checks flow.
|
||||
def _seed_legacy_repo() -> CleanReleaseRepository:
|
||||
repo = CleanReleaseRepository()
|
||||
now = datetime.now(UTC)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# #region TestCleanReleaseSourcePolicy [TYPE Module] [C:3] [SEMANTICS tests, api, clean-release, source-policy]
|
||||
# @RELATION BELONGS_TO -> SrcRoot
|
||||
# @RELATION BINDS_TO -> SrcRoot
|
||||
# @BRIEF Validate API behavior for source isolation violations in clean release preparation.
|
||||
# @LAYER: Domain
|
||||
# @INVARIANT: External endpoints must produce blocking violation entries.
|
||||
# @LAYER Domain
|
||||
# @INVARIANT External endpoints must produce blocking violation entries.
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# #region CleanReleaseV2ApiTests [TYPE Module] [C:3] [SEMANTICS test, clean-release, v2, api, contract]
|
||||
# @BRIEF API contract tests for redesigned clean release endpoints.
|
||||
# @LAYER: Domain
|
||||
# @LAYER Domain
|
||||
# @RELATION DEPENDS_ON -> [CleanReleaseV2Api]
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# #region CleanReleaseV2ReleaseApiTests [TYPE Module] [C:3] [SEMANTICS test, clean-release, release, approval, publication]
|
||||
# @BRIEF API contract test scaffolding for clean release approval and publication endpoints.
|
||||
# @LAYER: Domain
|
||||
# @LAYER Domain
|
||||
# @RELATION DEPENDS_ON -> [CleanReleaseV2Api]
|
||||
"""Contract tests for redesigned approval/publication API endpoints."""
|
||||
from datetime import UTC, datetime
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# #region DashboardsApiTests [TYPE Module] [C:3] [SEMANTICS test, dashboard, api, listing, migration]
|
||||
# @BRIEF Unit tests for dashboards API endpoints.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [DashboardsApi]
|
||||
from datetime import UTC, datetime
|
||||
import pytest
|
||||
@@ -65,15 +65,15 @@ client = TestClient(app)
|
||||
# @RELATION BINDS_TO -> DashboardsApiTests
|
||||
# @BRIEF Validate dashboards listing returns a populated response that satisfies the schema contract.
|
||||
# @TEST: GET /api/dashboards returns 200 and valid schema
|
||||
# @PRE: env_id exists
|
||||
# @POST: Response matches DashboardsResponse schema
|
||||
# @PRE env_id exists
|
||||
# @POST Response matches DashboardsResponse schema
|
||||
def test_get_dashboards_success(mock_deps):
|
||||
"""Uses @TEST_FIXTURE: dashboard_list_happy data."""
|
||||
mock_env = MagicMock()
|
||||
mock_env.id = "prod"
|
||||
mock_deps["config"].get_environments.return_value = [mock_env]
|
||||
mock_deps["task"].get_all_tasks.return_value = []
|
||||
# @TEST_FIXTURE: dashboard_list_happy -> {"id": 1, "title": "Main Revenue"}
|
||||
# @TEST_FIXTURE dashboard_list_happy -> {"id": 1, "title": "Main Revenue"}
|
||||
mock_deps["resource"].get_dashboards_with_status = AsyncMock(
|
||||
return_value=[
|
||||
{
|
||||
@@ -100,8 +100,8 @@ def test_get_dashboards_success(mock_deps):
|
||||
# @RELATION BINDS_TO -> DashboardsApiTests
|
||||
# @BRIEF Validate dashboards listing applies the search filter and returns only matching rows.
|
||||
# @TEST: GET /api/dashboards filters by search term
|
||||
# @PRE: search parameter provided
|
||||
# @POST: Only matching dashboards returned
|
||||
# @PRE search parameter provided
|
||||
# @POST Only matching dashboards returned
|
||||
def test_get_dashboards_with_search(mock_deps):
|
||||
mock_env = MagicMock()
|
||||
mock_env.id = "prod"
|
||||
@@ -130,14 +130,14 @@ def test_get_dashboards_with_search(mock_deps):
|
||||
response = client.get("/api/dashboards?env_id=prod&search=sales")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
# @POST: Filtered result count must match search
|
||||
# @POST Filtered result count must match search
|
||||
assert len(data["dashboards"]) == 1
|
||||
assert data["dashboards"][0]["title"] == "Sales Report"
|
||||
# #endregion test_get_dashboards_with_search
|
||||
# #region test_get_dashboards_empty [TYPE Function]
|
||||
# @RELATION BINDS_TO -> DashboardsApiTests
|
||||
# @BRIEF Validate dashboards listing returns an empty payload for an environment without dashboards.
|
||||
# @TEST_EDGE: empty_dashboards -> {env_id: 'empty_env', expected_total: 0}
|
||||
# @TEST_EDGE empty_dashboards -> {env_id: 'empty_env', expected_total: 0}
|
||||
def test_get_dashboards_empty(mock_deps):
|
||||
"""@TEST_EDGE: empty_dashboards -> {env_id: 'empty_env', expected_total: 0}"""
|
||||
mock_env = MagicMock()
|
||||
@@ -156,7 +156,7 @@ def test_get_dashboards_empty(mock_deps):
|
||||
# #region test_get_dashboards_superset_failure [TYPE Function]
|
||||
# @RELATION BINDS_TO -> DashboardsApiTests
|
||||
# @BRIEF Validate dashboards listing surfaces a 503 contract when Superset access fails.
|
||||
# @TEST_EDGE: external_superset_failure -> {env_id: 'bad_conn', status: 503}
|
||||
# @TEST_EDGE external_superset_failure -> {env_id: 'bad_conn', status: 503}
|
||||
def test_get_dashboards_superset_failure(mock_deps):
|
||||
"""@TEST_EDGE: external_superset_failure -> {env_id: 'bad_conn', status: 503}"""
|
||||
mock_env = MagicMock()
|
||||
@@ -174,8 +174,8 @@ def test_get_dashboards_superset_failure(mock_deps):
|
||||
# @RELATION BINDS_TO -> DashboardsApiTests
|
||||
# @BRIEF Validate dashboards listing returns 404 when the requested environment does not exist.
|
||||
# @TEST: GET /api/dashboards returns 404 if env_id missing
|
||||
# @PRE: env_id does not exist
|
||||
# @POST: Returns 404 error
|
||||
# @PRE env_id does not exist
|
||||
# @POST Returns 404 error
|
||||
def test_get_dashboards_env_not_found(mock_deps):
|
||||
mock_deps["config"].get_environments.return_value = []
|
||||
response = client.get("/api/dashboards?env_id=nonexistent")
|
||||
@@ -186,8 +186,8 @@ def test_get_dashboards_env_not_found(mock_deps):
|
||||
# @RELATION BINDS_TO -> DashboardsApiTests
|
||||
# @BRIEF Validate dashboards listing rejects invalid pagination parameters with 400 responses.
|
||||
# @TEST: GET /api/dashboards returns 400 for invalid page/page_size
|
||||
# @PRE: page < 1 or page_size > 100
|
||||
# @POST: Returns 400 error
|
||||
# @PRE page < 1 or page_size > 100
|
||||
# @POST Returns 400 error
|
||||
def test_get_dashboards_invalid_pagination(mock_deps):
|
||||
mock_env = MagicMock()
|
||||
mock_env.id = "prod"
|
||||
@@ -263,9 +263,9 @@ def test_get_dashboard_detail_env_not_found(mock_deps):
|
||||
# #region test_migrate_dashboards_success [TYPE Function]
|
||||
# @RELATION BINDS_TO -> DashboardsApiTests
|
||||
# @TEST: POST /api/dashboards/migrate creates migration task
|
||||
# @PRE: Valid source_env_id, target_env_id, dashboard_ids
|
||||
# @PRE Valid source_env_id, target_env_id, dashboard_ids
|
||||
# @BRIEF Validate dashboard migration request creates an async task and returns its identifier.
|
||||
# @POST: Returns task_id and create_task was called
|
||||
# @POST Returns task_id and create_task was called
|
||||
def test_migrate_dashboards_success(mock_deps):
|
||||
mock_source = MagicMock()
|
||||
mock_source.id = "source"
|
||||
@@ -293,9 +293,9 @@ def test_migrate_dashboards_success(mock_deps):
|
||||
# #region test_migrate_dashboards_no_ids [TYPE Function]
|
||||
# @RELATION BINDS_TO -> DashboardsApiTests
|
||||
# @TEST: POST /api/dashboards/migrate returns 400 for empty dashboard_ids
|
||||
# @PRE: dashboard_ids is empty
|
||||
# @PRE dashboard_ids is empty
|
||||
# @BRIEF Validate dashboard migration rejects empty dashboard identifier lists.
|
||||
# @POST: Returns 400 error
|
||||
# @POST Returns 400 error
|
||||
def test_migrate_dashboards_no_ids(mock_deps):
|
||||
response = client.post(
|
||||
"/api/dashboards/migrate",
|
||||
@@ -311,7 +311,7 @@ def test_migrate_dashboards_no_ids(mock_deps):
|
||||
# #region test_migrate_dashboards_env_not_found [TYPE Function]
|
||||
# @RELATION BINDS_TO -> DashboardsApiTests
|
||||
# @BRIEF Validate migration creation returns 404 when the source environment cannot be resolved.
|
||||
# @PRE: source_env_id and target_env_id are valid environment IDs
|
||||
# @PRE source_env_id and target_env_id are valid environment IDs
|
||||
def test_migrate_dashboards_env_not_found(mock_deps):
|
||||
"""@PRE: source_env_id and target_env_id are valid environment IDs."""
|
||||
mock_deps["config"].get_environments.return_value = []
|
||||
@@ -325,9 +325,9 @@ def test_migrate_dashboards_env_not_found(mock_deps):
|
||||
# #region test_backup_dashboards_success [TYPE Function]
|
||||
# @RELATION BINDS_TO -> DashboardsApiTests
|
||||
# @TEST: POST /api/dashboards/backup creates backup task
|
||||
# @PRE: Valid env_id, dashboard_ids
|
||||
# @PRE Valid env_id, dashboard_ids
|
||||
# @BRIEF Validate dashboard backup request creates an async backup task and returns its identifier.
|
||||
# @POST: Returns task_id and create_task was called
|
||||
# @POST Returns task_id and create_task was called
|
||||
def test_backup_dashboards_success(mock_deps):
|
||||
mock_env = MagicMock()
|
||||
mock_env.id = "prod"
|
||||
@@ -348,7 +348,7 @@ def test_backup_dashboards_success(mock_deps):
|
||||
# #region test_backup_dashboards_env_not_found [TYPE Function]
|
||||
# @RELATION BINDS_TO -> DashboardsApiTests
|
||||
# @BRIEF Validate backup task creation returns 404 when the target environment is missing.
|
||||
# @PRE: env_id is a valid environment ID
|
||||
# @PRE env_id is a valid environment ID
|
||||
def test_backup_dashboards_env_not_found(mock_deps):
|
||||
"""@PRE: env_id is a valid environment ID."""
|
||||
mock_deps["config"].get_environments.return_value = []
|
||||
@@ -361,9 +361,9 @@ def test_backup_dashboards_env_not_found(mock_deps):
|
||||
# #region test_get_database_mappings_success [TYPE Function]
|
||||
# @RELATION BINDS_TO -> DashboardsApiTests
|
||||
# @TEST: GET /api/dashboards/db-mappings returns mapping suggestions
|
||||
# @PRE: Valid source_env_id, target_env_id
|
||||
# @PRE Valid source_env_id, target_env_id
|
||||
# @BRIEF Validate database mapping suggestions are returned for valid source and target environments.
|
||||
# @POST: Returns list of database mappings
|
||||
# @POST Returns list of database mappings
|
||||
def test_get_database_mappings_success(mock_deps):
|
||||
mock_source = MagicMock()
|
||||
mock_source.id = "prod"
|
||||
@@ -393,7 +393,7 @@ def test_get_database_mappings_success(mock_deps):
|
||||
# #region test_get_database_mappings_env_not_found [TYPE Function]
|
||||
# @RELATION BINDS_TO -> DashboardsApiTests
|
||||
# @BRIEF Validate database mapping suggestions return 404 when either environment is missing.
|
||||
# @PRE: source_env_id and target_env_id are valid environment IDs
|
||||
# @PRE source_env_id and target_env_id are valid environment IDs
|
||||
def test_get_database_mappings_env_not_found(mock_deps):
|
||||
"""@PRE: source_env_id must be a valid environment."""
|
||||
mock_deps["config"].get_environments.return_value = []
|
||||
@@ -471,8 +471,8 @@ def test_get_dashboard_thumbnail_success(mock_deps):
|
||||
# #region _build_profile_preference_stub [TYPE Function]
|
||||
# @RELATION BINDS_TO -> DashboardsApiTests
|
||||
# @BRIEF Creates profile preference payload stub for dashboards filter contract tests.
|
||||
# @PRE: username can be empty; enabled indicates profile-default toggle state.
|
||||
# @POST: Returns object compatible with ProfileService.get_my_preference contract.
|
||||
# @PRE username can be empty; enabled indicates profile-default toggle state.
|
||||
# @POST Returns object compatible with ProfileService.get_my_preference contract.
|
||||
def _build_profile_preference_stub(username: str, enabled: bool):
|
||||
preference = MagicMock()
|
||||
preference.superset_username = username
|
||||
@@ -487,8 +487,8 @@ def _build_profile_preference_stub(username: str, enabled: bool):
|
||||
# #region _matches_actor_case_insensitive [TYPE Function]
|
||||
# @RELATION BINDS_TO -> DashboardsApiTests
|
||||
# @BRIEF Applies trim + case-insensitive owners OR modified_by matching used by route contract tests.
|
||||
# @PRE: owners can be None or list-like values.
|
||||
# @POST: Returns True when bound username matches any owner or modified_by.
|
||||
# @PRE owners can be None or list-like values.
|
||||
# @POST Returns True when bound username matches any owner or modified_by.
|
||||
def _matches_actor_case_insensitive(bound_username, owners, modified_by):
|
||||
normalized_bound = str(bound_username or "").strip().lower()
|
||||
if not normalized_bound:
|
||||
@@ -507,8 +507,8 @@ def _matches_actor_case_insensitive(bound_username, owners, modified_by):
|
||||
# @RELATION BINDS_TO -> DashboardsApiTests
|
||||
# @TEST: GET /api/dashboards applies profile-default filter with owners OR modified_by trim+case-insensitive semantics.
|
||||
# @BRIEF Validate profile-default filtering matches owner and modifier aliases using normalized Superset actor values.
|
||||
# @PRE: Current user has enabled profile-default preference and bound username.
|
||||
# @POST: Response includes only matching dashboards and effective_profile_filter metadata.
|
||||
# @PRE Current user has enabled profile-default preference and bound username.
|
||||
# @POST Response includes only matching dashboards and effective_profile_filter metadata.
|
||||
def test_get_dashboards_profile_filter_contract_owners_or_modified_by(mock_deps):
|
||||
mock_env = MagicMock()
|
||||
mock_env.id = "prod"
|
||||
@@ -566,8 +566,8 @@ def test_get_dashboards_profile_filter_contract_owners_or_modified_by(mock_deps)
|
||||
# @RELATION BINDS_TO -> DashboardsApiTests
|
||||
# @TEST: GET /api/dashboards honors override_show_all and disables profile-default filter for current page.
|
||||
# @BRIEF Validate override_show_all bypasses profile-default filtering without changing dashboard list semantics.
|
||||
# @PRE: Profile-default preference exists but override_show_all=true query is provided.
|
||||
# @POST: Response remains unfiltered and effective_profile_filter.applied is false.
|
||||
# @PRE Profile-default preference exists but override_show_all=true query is provided.
|
||||
# @POST Response remains unfiltered and effective_profile_filter.applied is false.
|
||||
def test_get_dashboards_override_show_all_contract(mock_deps):
|
||||
mock_env = MagicMock()
|
||||
mock_env.id = "prod"
|
||||
@@ -619,8 +619,8 @@ def test_get_dashboards_override_show_all_contract(mock_deps):
|
||||
# @RELATION BINDS_TO -> DashboardsApiTests
|
||||
# @TEST: GET /api/dashboards returns empty result set when profile-default filter is active and no dashboard actors match.
|
||||
# @BRIEF Validate profile-default filtering returns an empty dashboard page when no actor aliases match the bound user.
|
||||
# @PRE: Profile-default preference is enabled with bound username and all dashboards are non-matching.
|
||||
# @POST: Response total is 0 with deterministic pagination and active effective_profile_filter metadata.
|
||||
# @PRE Profile-default preference is enabled with bound username and all dashboards are non-matching.
|
||||
# @POST Response total is 0 with deterministic pagination and active effective_profile_filter metadata.
|
||||
def test_get_dashboards_profile_filter_no_match_results_contract(mock_deps):
|
||||
mock_env = MagicMock()
|
||||
mock_env.id = "prod"
|
||||
@@ -674,8 +674,8 @@ def test_get_dashboards_profile_filter_no_match_results_contract(mock_deps):
|
||||
# @RELATION BINDS_TO -> DashboardsApiTests
|
||||
# @TEST: GET /api/dashboards does not auto-apply profile-default filter outside dashboards_main page context.
|
||||
# @BRIEF Validate non-dashboard page contexts suppress profile-default filtering and preserve unfiltered results.
|
||||
# @PRE: Profile-default preference exists but page_context=other query is provided.
|
||||
# @POST: Response remains unfiltered and metadata reflects source_page=other.
|
||||
# @PRE Profile-default preference exists but page_context=other query is provided.
|
||||
# @POST Response remains unfiltered and metadata reflects source_page=other.
|
||||
def test_get_dashboards_page_context_other_disables_profile_default(mock_deps):
|
||||
mock_env = MagicMock()
|
||||
mock_env.id = "prod"
|
||||
@@ -727,8 +727,8 @@ def test_get_dashboards_page_context_other_disables_profile_default(mock_deps):
|
||||
# @RELATION BINDS_TO -> DashboardsApiTests
|
||||
# @TEST: GET /api/dashboards resolves Superset display-name alias once and filters without per-dashboard detail calls.
|
||||
# @BRIEF Validate profile-default filtering reuses resolved Superset display aliases without triggering per-dashboard detail fanout.
|
||||
# @PRE: Profile-default filter is active, bound username is `admin`, dashboard actors contain display labels.
|
||||
# @POST: Route matches by alias (`Superset Admin`) and does not call `SupersetClient.get_dashboard` in list filter path.
|
||||
# @PRE Profile-default filter is active, bound username is `admin`, dashboard actors contain display labels.
|
||||
# @POST Route matches by alias (`Superset Admin`) and does not call `SupersetClient.get_dashboard` in list filter path.
|
||||
def test_get_dashboards_profile_filter_matches_display_alias_without_detail_fanout(
|
||||
mock_deps,
|
||||
):
|
||||
@@ -803,8 +803,8 @@ def test_get_dashboards_profile_filter_matches_display_alias_without_detail_fano
|
||||
# @RELATION BINDS_TO -> DashboardsApiTests
|
||||
# @TEST: GET /api/dashboards profile-default filter matches Superset owner object payloads.
|
||||
# @BRIEF Validate profile-default filtering accepts owner object payloads once aliases resolve to the bound Superset username.
|
||||
# @PRE: Profile-default preference is enabled and owners list contains dict payloads.
|
||||
# @POST: Response keeps dashboards where owner object resolves to bound username alias.
|
||||
# @PRE Profile-default preference is enabled and owners list contains dict payloads.
|
||||
# @POST Response keeps dashboards where owner object resolves to bound username alias.
|
||||
def test_get_dashboards_profile_filter_matches_owner_object_payload_contract(mock_deps):
|
||||
mock_env = MagicMock()
|
||||
mock_env.id = "prod"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# #region DatasetReviewApiTests [TYPE Module] [C:3] [SEMANTICS dataset_review, api, tests, lifecycle, exports, orchestration]
|
||||
# @BRIEF Verify backend US1 dataset review lifecycle, export, parsing, and dictionary-resolution contracts.
|
||||
# @LAYER: API
|
||||
# @RELATION [BINDS_TO] ->[DatasetReviewApi]
|
||||
# @RELATION [BINDS_TO] ->[DatasetReviewOrchestrator]
|
||||
# @LAYER API
|
||||
# @RELATION BINDS_TO ->[DatasetReviewApi]
|
||||
# @RELATION BINDS_TO ->[DatasetReviewOrchestrator]
|
||||
from datetime import UTC, datetime
|
||||
import pytest
|
||||
from types import SimpleNamespace
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# #region DatasetsApiTests [TYPE Module] [C:3] [SEMANTICS datasets, api, tests, pagination, mapping, docs]
|
||||
# @BRIEF Unit tests for datasets API endpoints.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [DatasetsApi]
|
||||
# @INVARIANT: Endpoint contracts remain stable for success and validation failure paths.
|
||||
# @INVARIANT Endpoint contracts remain stable for success and validation failure paths.
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
@@ -31,11 +31,11 @@ def mock_deps():
|
||||
"""Bare MagicMock — no spec guards. All service method calls succeed silently.
|
||||
Authorization, data integrity, and error paths are invisible to this fixture.
|
||||
"""
|
||||
# @INVARIANT: unconstrained mock — no spec= enforced; attribute typos will silently pass
|
||||
# @INVARIANT unconstrained mock — no spec= enforced; attribute typos will silently pass
|
||||
config_manager = MagicMock()
|
||||
# @INVARIANT: unconstrained mock — no spec= enforced; attribute typos will silently pass
|
||||
# @INVARIANT unconstrained mock — no spec= enforced; attribute typos will silently pass
|
||||
task_manager = MagicMock()
|
||||
# @INVARIANT: unconstrained mock — no spec= enforced; attribute typos will silently pass
|
||||
# @INVARIANT unconstrained mock — no spec= enforced; attribute typos will silently pass
|
||||
resource_service = MagicMock()
|
||||
mapping_service = MagicMock()
|
||||
app.dependency_overrides[get_config_manager] = lambda: config_manager
|
||||
@@ -62,11 +62,11 @@ def mock_deps():
|
||||
app.dependency_overrides.clear()
|
||||
client = TestClient(app)
|
||||
# #region test_get_datasets_success [TYPE Function]
|
||||
# @RELATION BINDS_TO -> [DatasetsApiTests:Module]
|
||||
# @RELATION BINDS_TO -> [DatasetsApiTests]
|
||||
# @BRIEF Validate successful datasets listing contract for an existing environment.
|
||||
# @TEST: GET /api/datasets returns 200 and valid schema
|
||||
# @PRE: env_id exists
|
||||
# @POST: Response matches DatasetsResponse schema
|
||||
# @PRE env_id exists
|
||||
# @POST Response matches DatasetsResponse schema
|
||||
def test_get_datasets_success(mock_deps):
|
||||
# Mock environment
|
||||
mock_env = MagicMock()
|
||||
@@ -94,11 +94,11 @@ def test_get_datasets_success(mock_deps):
|
||||
DatasetsResponse(**data)
|
||||
# #endregion test_get_datasets_success
|
||||
# #region test_get_datasets_env_not_found [TYPE Function]
|
||||
# @RELATION BINDS_TO -> [DatasetsApiTests:Module]
|
||||
# @RELATION BINDS_TO -> [DatasetsApiTests]
|
||||
# @BRIEF Validate datasets listing returns 404 when the requested environment does not exist.
|
||||
# @TEST: GET /api/datasets returns 404 if env_id missing
|
||||
# @PRE: env_id does not exist
|
||||
# @POST: Returns 404 error
|
||||
# @PRE env_id does not exist
|
||||
# @POST Returns 404 error
|
||||
def test_get_datasets_env_not_found(mock_deps):
|
||||
mock_deps["config"].get_environments.return_value = []
|
||||
response = client.get("/api/datasets?env_id=nonexistent")
|
||||
@@ -106,11 +106,11 @@ def test_get_datasets_env_not_found(mock_deps):
|
||||
assert "Environment not found" in response.json()["detail"]
|
||||
# #endregion test_get_datasets_env_not_found
|
||||
# #region test_get_datasets_invalid_pagination [TYPE Function]
|
||||
# @RELATION BINDS_TO -> [DatasetsApiTests:Module]
|
||||
# @RELATION BINDS_TO -> [DatasetsApiTests]
|
||||
# @BRIEF Validate datasets listing rejects invalid pagination parameters with 400 responses.
|
||||
# @TEST: GET /api/datasets returns 400 for invalid page/page_size
|
||||
# @PRE: page < 1 or page_size > 100
|
||||
# @POST: Returns 400 error
|
||||
# @PRE page < 1 or page_size > 100
|
||||
# @POST Returns 400 error
|
||||
def test_get_datasets_invalid_pagination(mock_deps):
|
||||
mock_env = MagicMock()
|
||||
mock_env.id = "prod"
|
||||
@@ -123,17 +123,17 @@ def test_get_datasets_invalid_pagination(mock_deps):
|
||||
response = client.get("/api/datasets?env_id=prod&page_size=0")
|
||||
assert response.status_code == 400
|
||||
assert "Page size must be between 1 and 100" in response.json()["detail"]
|
||||
# @TEST_EDGE: page_size > 100 exceeds max
|
||||
# @TEST_EDGE page_size > 100 exceeds max
|
||||
response = client.get("/api/datasets?env_id=prod&page_size=101")
|
||||
assert response.status_code == 400
|
||||
assert "Page size must be between 1 and 100" in response.json()["detail"]
|
||||
# #endregion test_get_datasets_invalid_pagination
|
||||
# #region test_map_columns_success [TYPE Function]
|
||||
# @RELATION BINDS_TO -> [DatasetsApiTests:Module]
|
||||
# @RELATION BINDS_TO -> [DatasetsApiTests]
|
||||
# @BRIEF Validate map-columns request creates an async mapping task and returns its identifier.
|
||||
# @TEST: POST /api/datasets/map-columns creates mapping task
|
||||
# @PRE: Valid env_id, dataset_ids, source_type (sqllab)
|
||||
# @POST: Returns task_id
|
||||
# @PRE Valid env_id, dataset_ids, source_type (sqllab)
|
||||
# @POST Returns task_id
|
||||
def test_map_columns_success(mock_deps):
|
||||
# Mock environment
|
||||
mock_env = MagicMock()
|
||||
@@ -154,11 +154,11 @@ def test_map_columns_success(mock_deps):
|
||||
mock_deps["task"].create_task.assert_called_once()
|
||||
# #endregion test_map_columns_success
|
||||
# #region test_map_columns_invalid_source_type [TYPE Function]
|
||||
# @RELATION BINDS_TO -> [DatasetsApiTests:Module]
|
||||
# @RELATION BINDS_TO -> [DatasetsApiTests]
|
||||
# @BRIEF Validate map-columns rejects unsupported source types with a 400 contract response.
|
||||
# @TEST: POST /api/datasets/map-columns returns 400 for invalid source_type
|
||||
# @PRE: source_type is not 'sqllab' or 'xlsx'
|
||||
# @POST: Returns 400 error
|
||||
# @PRE source_type is not 'sqllab' or 'xlsx'
|
||||
# @POST Returns 400 error
|
||||
def test_map_columns_invalid_source_type(mock_deps):
|
||||
response = client.post(
|
||||
"/api/datasets/map-columns",
|
||||
@@ -168,11 +168,11 @@ def test_map_columns_invalid_source_type(mock_deps):
|
||||
assert "Source type must be 'sqllab' or 'xlsx'" in response.json()["detail"]
|
||||
# #endregion test_map_columns_invalid_source_type
|
||||
# #region test_generate_docs_success [TYPE Function]
|
||||
# @RELATION BINDS_TO -> [DatasetsApiTests:Module]
|
||||
# @RELATION BINDS_TO -> [DatasetsApiTests]
|
||||
# @TEST: POST /api/datasets/generate-docs creates doc generation task
|
||||
# @PRE: Valid env_id, dataset_ids, llm_provider
|
||||
# @PRE Valid env_id, dataset_ids, llm_provider
|
||||
# @BRIEF Validate generate-docs request creates an async documentation task and returns its identifier.
|
||||
# @POST: Returns task_id
|
||||
# @POST Returns task_id
|
||||
def test_generate_docs_success(mock_deps):
|
||||
# Mock environment
|
||||
mock_env = MagicMock()
|
||||
@@ -193,11 +193,11 @@ def test_generate_docs_success(mock_deps):
|
||||
mock_deps["task"].create_task.assert_called_once()
|
||||
# #endregion test_generate_docs_success
|
||||
# #region test_map_columns_empty_ids [TYPE Function]
|
||||
# @RELATION BINDS_TO -> [DatasetsApiTests:Module]
|
||||
# @RELATION BINDS_TO -> [DatasetsApiTests]
|
||||
# @BRIEF Validate map-columns rejects empty dataset identifier lists.
|
||||
# @TEST: POST /api/datasets/map-columns returns 400 for empty dataset_ids
|
||||
# @PRE: dataset_ids is empty
|
||||
# @POST: Returns 400 error
|
||||
# @PRE dataset_ids is empty
|
||||
# @POST Returns 400 error
|
||||
def test_map_columns_empty_ids(mock_deps):
|
||||
"""@PRE: dataset_ids must be non-empty."""
|
||||
response = client.post(
|
||||
@@ -208,10 +208,10 @@ def test_map_columns_empty_ids(mock_deps):
|
||||
assert "At least one dataset ID must be provided" in response.json()["detail"]
|
||||
# #endregion test_map_columns_empty_ids
|
||||
# #region test_map_columns_missing_database_id [TYPE Function]
|
||||
# @RELATION BINDS_TO -> [DatasetsApiTests:Module]
|
||||
# @RELATION BINDS_TO -> [DatasetsApiTests]
|
||||
# @BRIEF Validate map-columns rejects sqllab source without database_id.
|
||||
# @TEST: POST /api/datasets/map-columns returns 400 for sqllab without database_id
|
||||
# @POST: Returns 400 error
|
||||
# @POST Returns 400 error
|
||||
def test_map_columns_missing_database_id(mock_deps):
|
||||
response = client.post(
|
||||
"/api/datasets/map-columns",
|
||||
@@ -221,11 +221,11 @@ def test_map_columns_missing_database_id(mock_deps):
|
||||
assert "database_id is required" in response.json()["detail"]
|
||||
# #endregion test_map_columns_missing_database_id
|
||||
# #region test_generate_docs_empty_ids [TYPE Function]
|
||||
# @RELATION BINDS_TO -> [DatasetsApiTests:Module]
|
||||
# @RELATION BINDS_TO -> [DatasetsApiTests]
|
||||
# @BRIEF Validate generate-docs rejects empty dataset identifier lists.
|
||||
# @TEST: POST /api/datasets/generate-docs returns 400 for empty dataset_ids
|
||||
# @PRE: dataset_ids is empty
|
||||
# @POST: Returns 400 error
|
||||
# @PRE dataset_ids is empty
|
||||
# @POST Returns 400 error
|
||||
def test_generate_docs_empty_ids(mock_deps):
|
||||
"""@PRE: dataset_ids must be non-empty."""
|
||||
response = client.post(
|
||||
@@ -236,11 +236,11 @@ def test_generate_docs_empty_ids(mock_deps):
|
||||
assert "At least one dataset ID must be provided" in response.json()["detail"]
|
||||
# #endregion test_generate_docs_empty_ids
|
||||
# #region test_generate_docs_env_not_found [TYPE Function]
|
||||
# @RELATION BINDS_TO -> [DatasetsApiTests:Module]
|
||||
# @RELATION BINDS_TO -> [DatasetsApiTests]
|
||||
# @TEST: POST /api/datasets/generate-docs returns 404 for missing env
|
||||
# @PRE: env_id does not exist
|
||||
# @PRE env_id does not exist
|
||||
# @BRIEF Validate generate-docs returns 404 when the requested environment cannot be resolved.
|
||||
# @POST: Returns 404 error
|
||||
# @POST Returns 404 error
|
||||
def test_generate_docs_env_not_found(mock_deps):
|
||||
"""@PRE: env_id must be a valid environment."""
|
||||
mock_deps["config"].get_environments.return_value = []
|
||||
@@ -252,10 +252,10 @@ def test_generate_docs_env_not_found(mock_deps):
|
||||
assert "Environment not found" in response.json()["detail"]
|
||||
# #endregion test_generate_docs_env_not_found
|
||||
# #region test_get_datasets_superset_failure [TYPE Function]
|
||||
# @RELATION BINDS_TO -> [DatasetsApiTests:Module]
|
||||
# @RELATION BINDS_TO -> [DatasetsApiTests]
|
||||
# @BRIEF Validate datasets listing surfaces a 503 contract when Superset access fails.
|
||||
# @TEST_EDGE: external_superset_failure -> {status: 503}
|
||||
# @POST: Returns 503 with stable error detail when upstream dataset fetch fails.
|
||||
# @TEST_EDGE external_superset_failure -> {status: 503}
|
||||
# @POST Returns 503 with stable error detail when upstream dataset fetch fails.
|
||||
def test_get_datasets_superset_failure(mock_deps):
|
||||
"""@TEST_EDGE: external_superset_failure -> {status: 503}"""
|
||||
mock_env = MagicMock()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# #region TestGitApi [TYPE Module] [C:3] [SEMANTICS test, git, api, config, repository]
|
||||
# @RELATION VERIFIES -> [GitApi]
|
||||
# @RELATION BINDS_TO -> [EXT:frontend:GitApi]
|
||||
# @BRIEF API tests for Git configurations and repository operations.
|
||||
import asyncio
|
||||
import pytest
|
||||
@@ -14,7 +14,7 @@ from src.models.git import GitProvider, GitRepository, GitServerConfig, GitStatu
|
||||
# #region DbMock [TYPE Class] [C:2]
|
||||
# @RELATION BINDS_TO -> [TestGitApi]
|
||||
# @BRIEF In-memory session double for git route tests with minimal query/filter persistence semantics.
|
||||
# @INVARIANT: Supports only the SQLAlchemy-like operations exercised by this test module.
|
||||
# @INVARIANT Supports only the SQLAlchemy-like operations exercised by this test module.
|
||||
class DbMock:
|
||||
def __init__(self, data=None):
|
||||
self._data = data or []
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# #region TestGitStatusRoute [TYPE Module] [C:3] [SEMANTICS tests, git, api, status, no_repo]
|
||||
# @BRIEF Validate status endpoint behavior for missing and error repository states.
|
||||
# @LAYER: Domain
|
||||
# @RELATION VERIFIES -> [GitApi]
|
||||
# @LAYER Domain
|
||||
# @RELATION BINDS_TO -> [EXT:frontend:GitApi]
|
||||
import asyncio
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
@@ -14,8 +14,8 @@ from src.api.routes import git as git_routes
|
||||
# #region test_get_repository_status_returns_no_repo_payload_for_missing_repo [TYPE Function]
|
||||
# @RELATION BINDS_TO -> TestGitStatusRoute
|
||||
# @BRIEF Ensure missing local repository is represented as NO_REPO payload instead of an API error.
|
||||
# @PRE: GitService.get_status raises HTTPException(404).
|
||||
# @POST: Route returns a deterministic NO_REPO status payload.
|
||||
# @PRE GitService.get_status raises HTTPException(404).
|
||||
# @POST Route returns a deterministic NO_REPO status payload.
|
||||
def test_get_repository_status_returns_no_repo_payload_for_missing_repo(monkeypatch):
|
||||
class MissingRepoGitService:
|
||||
def _get_repo_path(self, dashboard_id: int) -> str:
|
||||
@@ -32,8 +32,8 @@ def test_get_repository_status_returns_no_repo_payload_for_missing_repo(monkeypa
|
||||
# #region test_get_repository_status_propagates_non_404_http_exception [TYPE Function]
|
||||
# @RELATION BINDS_TO -> TestGitStatusRoute
|
||||
# @BRIEF Ensure HTTP exceptions other than 404 are not masked.
|
||||
# @PRE: GitService.get_status raises HTTPException with non-404 status.
|
||||
# @POST: Raised exception preserves original status and detail.
|
||||
# @PRE GitService.get_status raises HTTPException with non-404 status.
|
||||
# @POST Raised exception preserves original status and detail.
|
||||
def test_get_repository_status_propagates_non_404_http_exception(monkeypatch):
|
||||
class ConflictGitService:
|
||||
def _get_repo_path(self, dashboard_id: int) -> str:
|
||||
@@ -50,8 +50,8 @@ def test_get_repository_status_propagates_non_404_http_exception(monkeypatch):
|
||||
# #region test_get_repository_diff_propagates_http_exception [TYPE Function]
|
||||
# @RELATION BINDS_TO -> TestGitStatusRoute
|
||||
# @BRIEF Ensure diff endpoint preserves domain HTTP errors from GitService.
|
||||
# @PRE: GitService.get_diff raises HTTPException.
|
||||
# @POST: Endpoint raises same HTTPException values.
|
||||
# @PRE GitService.get_diff raises HTTPException.
|
||||
# @POST Endpoint raises same HTTPException values.
|
||||
def test_get_repository_diff_propagates_http_exception(monkeypatch):
|
||||
class DiffGitService:
|
||||
def get_diff(self, dashboard_id: int, file_path=None, staged: bool = False) -> str:
|
||||
@@ -65,8 +65,8 @@ def test_get_repository_diff_propagates_http_exception(monkeypatch):
|
||||
# #region test_get_history_wraps_unexpected_error_as_500 [TYPE Function]
|
||||
# @RELATION BINDS_TO -> TestGitStatusRoute
|
||||
# @BRIEF Ensure non-HTTP exceptions in history endpoint become deterministic 500 errors.
|
||||
# @PRE: GitService.get_commit_history raises ValueError.
|
||||
# @POST: Endpoint returns HTTPException with status 500 and route context.
|
||||
# @PRE GitService.get_commit_history raises ValueError.
|
||||
# @POST Endpoint returns HTTPException with status 500 and route context.
|
||||
def test_get_history_wraps_unexpected_error_as_500(monkeypatch):
|
||||
class HistoryGitService:
|
||||
def get_commit_history(self, dashboard_id: int, limit: int = 50):
|
||||
@@ -80,8 +80,8 @@ def test_get_history_wraps_unexpected_error_as_500(monkeypatch):
|
||||
# #region test_commit_changes_wraps_unexpected_error_as_500 [TYPE Function]
|
||||
# @RELATION BINDS_TO -> TestGitStatusRoute
|
||||
# @BRIEF Ensure commit endpoint does not leak unexpected errors as 400.
|
||||
# @PRE: GitService.commit_changes raises RuntimeError.
|
||||
# @POST: Endpoint raises HTTPException(500) with route context.
|
||||
# @PRE GitService.commit_changes raises RuntimeError.
|
||||
# @POST Endpoint raises HTTPException(500) with route context.
|
||||
def test_commit_changes_wraps_unexpected_error_as_500(monkeypatch):
|
||||
class CommitGitService:
|
||||
def commit_changes(self, dashboard_id: int, message: str, files):
|
||||
@@ -98,8 +98,8 @@ def test_commit_changes_wraps_unexpected_error_as_500(monkeypatch):
|
||||
# #region test_get_repository_status_batch_returns_mixed_statuses [TYPE Function]
|
||||
# @RELATION BINDS_TO -> TestGitStatusRoute
|
||||
# @BRIEF Ensure batch endpoint returns per-dashboard statuses in one response.
|
||||
# @PRE: Some repositories are missing and some are initialized.
|
||||
# @POST: Returned map includes resolved status for each requested dashboard ID.
|
||||
# @PRE Some repositories are missing and some are initialized.
|
||||
# @POST Returned map includes resolved status for each requested dashboard ID.
|
||||
def test_get_repository_status_batch_returns_mixed_statuses(monkeypatch):
|
||||
class BatchGitService:
|
||||
def _get_repo_path(self, dashboard_id: int) -> str:
|
||||
@@ -119,8 +119,8 @@ def test_get_repository_status_batch_returns_mixed_statuses(monkeypatch):
|
||||
# #region test_get_repository_status_batch_marks_item_as_error_on_service_failure [TYPE Function]
|
||||
# @RELATION BINDS_TO -> TestGitStatusRoute
|
||||
# @BRIEF Ensure batch endpoint marks failed items as ERROR without failing entire request.
|
||||
# @PRE: GitService raises non-HTTP exception for one dashboard.
|
||||
# @POST: Failed dashboard status is marked as ERROR.
|
||||
# @PRE GitService raises non-HTTP exception for one dashboard.
|
||||
# @POST Failed dashboard status is marked as ERROR.
|
||||
def test_get_repository_status_batch_marks_item_as_error_on_service_failure(monkeypatch):
|
||||
class BatchErrorGitService:
|
||||
def _get_repo_path(self, dashboard_id: int) -> str:
|
||||
@@ -138,8 +138,8 @@ def test_get_repository_status_batch_marks_item_as_error_on_service_failure(monk
|
||||
# #region test_get_repository_status_batch_deduplicates_and_truncates_ids [TYPE Function]
|
||||
# @RELATION BINDS_TO -> TestGitStatusRoute
|
||||
# @BRIEF Ensure batch endpoint protects server from oversized payloads.
|
||||
# @PRE: request includes duplicate IDs and more than MAX_REPOSITORY_STATUS_BATCH entries.
|
||||
# @POST: Result contains unique IDs up to configured cap.
|
||||
# @PRE request includes duplicate IDs and more than MAX_REPOSITORY_STATUS_BATCH entries.
|
||||
# @POST Result contains unique IDs up to configured cap.
|
||||
def test_get_repository_status_batch_deduplicates_and_truncates_ids(monkeypatch):
|
||||
class SafeBatchGitService:
|
||||
def _get_repo_path(self, dashboard_id: int) -> str:
|
||||
@@ -157,8 +157,8 @@ def test_get_repository_status_batch_deduplicates_and_truncates_ids(monkeypatch)
|
||||
# #region test_commit_changes_applies_profile_identity_before_commit [TYPE Function]
|
||||
# @RELATION BINDS_TO -> TestGitStatusRoute
|
||||
# @BRIEF Ensure commit route configures repository identity from profile preferences before commit call.
|
||||
# @PRE: Profile preference contains git_username/git_email for current user.
|
||||
# @POST: git_service.configure_identity receives resolved identity and commit proceeds.
|
||||
# @PRE Profile preference contains git_username/git_email for current user.
|
||||
# @POST git_service.configure_identity receives resolved identity and commit proceeds.
|
||||
def test_commit_changes_applies_profile_identity_before_commit(monkeypatch):
|
||||
class IdentityGitService:
|
||||
def __init__(self):
|
||||
@@ -206,8 +206,8 @@ def test_commit_changes_applies_profile_identity_before_commit(monkeypatch):
|
||||
# #region test_pull_changes_applies_profile_identity_before_pull [TYPE Function]
|
||||
# @RELATION BINDS_TO -> TestGitStatusRoute
|
||||
# @BRIEF Ensure pull route configures repository identity from profile preferences before pull call.
|
||||
# @PRE: Profile preference contains git_username/git_email for current user.
|
||||
# @POST: git_service.configure_identity receives resolved identity and pull proceeds.
|
||||
# @PRE Profile preference contains git_username/git_email for current user.
|
||||
# @POST git_service.configure_identity receives resolved identity and pull proceeds.
|
||||
def test_pull_changes_applies_profile_identity_before_pull(monkeypatch):
|
||||
class IdentityGitService:
|
||||
def __init__(self):
|
||||
@@ -251,8 +251,8 @@ def test_pull_changes_applies_profile_identity_before_pull(monkeypatch):
|
||||
# #region test_get_merge_status_returns_service_payload [TYPE Function]
|
||||
# @RELATION BINDS_TO -> TestGitStatusRoute
|
||||
# @BRIEF Ensure merge status route returns service payload as-is.
|
||||
# @PRE: git_service.get_merge_status returns unfinished merge payload.
|
||||
# @POST: Route response contains has_unfinished_merge=True.
|
||||
# @PRE git_service.get_merge_status returns unfinished merge payload.
|
||||
# @POST Route response contains has_unfinished_merge=True.
|
||||
def test_get_merge_status_returns_service_payload(monkeypatch):
|
||||
class MergeStatusGitService:
|
||||
def get_merge_status(self, dashboard_id: int) -> dict:
|
||||
@@ -279,8 +279,8 @@ def test_get_merge_status_returns_service_payload(monkeypatch):
|
||||
# #region test_resolve_merge_conflicts_passes_resolution_items_to_service [TYPE Function]
|
||||
# @RELATION BINDS_TO -> TestGitStatusRoute
|
||||
# @BRIEF Ensure merge resolve route forwards parsed resolutions to service.
|
||||
# @PRE: resolve_data has one file strategy.
|
||||
# @POST: Service receives normalized list and route returns resolved files.
|
||||
# @PRE resolve_data has one file strategy.
|
||||
# @POST Service receives normalized list and route returns resolved files.
|
||||
def test_resolve_merge_conflicts_passes_resolution_items_to_service(monkeypatch):
|
||||
captured = {}
|
||||
class MergeResolveGitService:
|
||||
@@ -309,8 +309,8 @@ def test_resolve_merge_conflicts_passes_resolution_items_to_service(monkeypatch)
|
||||
# #region test_abort_merge_calls_service_and_returns_result [TYPE Function]
|
||||
# @RELATION BINDS_TO -> TestGitStatusRoute
|
||||
# @BRIEF Ensure abort route delegates to service.
|
||||
# @PRE: Service abort_merge returns aborted status.
|
||||
# @POST: Route returns aborted status.
|
||||
# @PRE Service abort_merge returns aborted status.
|
||||
# @POST Route returns aborted status.
|
||||
def test_abort_merge_calls_service_and_returns_result(monkeypatch):
|
||||
class AbortGitService:
|
||||
def abort_merge(self, dashboard_id: int):
|
||||
@@ -329,8 +329,8 @@ def test_abort_merge_calls_service_and_returns_result(monkeypatch):
|
||||
# #region test_continue_merge_passes_message_and_returns_commit [TYPE Function]
|
||||
# @RELATION BINDS_TO -> TestGitStatusRoute
|
||||
# @BRIEF Ensure continue route passes commit message to service.
|
||||
# @PRE: continue_data.message is provided.
|
||||
# @POST: Route returns committed status and hash.
|
||||
# @PRE continue_data.message is provided.
|
||||
# @POST Route returns committed status and hash.
|
||||
def test_continue_merge_passes_message_and_returns_commit(monkeypatch):
|
||||
class ContinueGitService:
|
||||
def continue_merge(self, dashboard_id: int, message: str):
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# #region TestMigrationRoutes [TYPE Module] [C:3] [SEMANTICS test, migration, api, route, handler]
|
||||
#
|
||||
# @BRIEF Unit tests for migration API route handlers.
|
||||
# @LAYER: API
|
||||
# @RELATION VERIFIES -> backend.src.api.routes.migration
|
||||
# @LAYER API
|
||||
# @RELATION BINDS_TO -> [EXT:path:backend.src.api.routes.migration]
|
||||
#
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
@@ -420,8 +420,8 @@ async def test_execute_migration_invalid_env_raises_400(_mock_env):
|
||||
assert exc.value.status_code == 400
|
||||
@pytest.mark.asyncio
|
||||
async def test_dry_run_migration_returns_diff_and_risk(db_session):
|
||||
# @TEST_EDGE: missing_target_datasource -> validates high risk item generation
|
||||
# @TEST_EDGE: breaking_reference -> validates high risk on missing dataset link
|
||||
# @TEST_EDGE missing_target_datasource -> validates high risk item generation
|
||||
# @TEST_EDGE breaking_reference -> validates high risk on missing dataset link
|
||||
from src.api.routes.migration import dry_run_migration
|
||||
from src.models.dashboard import DashboardSelection
|
||||
env_source = MagicMock()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# #region TestProfileApi [TYPE Module] [C:3] [SEMANTICS tests, profile, api, preferences, lookup, contract]
|
||||
# @RELATION BELONGS_TO -> SrcRoot
|
||||
# @RELATION BINDS_TO -> SrcRoot
|
||||
# @BRIEF Verifies profile API route contracts for preference read/update and Superset account lookup.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
# [SECTION: IMPORTS]
|
||||
from datetime import UTC, datetime
|
||||
from unittest.mock import MagicMock, patch
|
||||
@@ -30,8 +30,8 @@ client = TestClient(app)
|
||||
# #region mock_profile_route_dependencies [TYPE Function]
|
||||
# @RELATION BINDS_TO -> TestProfileApi
|
||||
# @BRIEF Provides deterministic dependency overrides for profile route tests.
|
||||
# @PRE: App instance is initialized.
|
||||
# @POST: Dependencies are overridden for current test and restored afterward.
|
||||
# @PRE App instance is initialized.
|
||||
# @POST Dependencies are overridden for current test and restored afterward.
|
||||
def mock_profile_route_dependencies():
|
||||
mock_user = MagicMock()
|
||||
mock_user.id = "u-1"
|
||||
@@ -46,8 +46,8 @@ def mock_profile_route_dependencies():
|
||||
# #region profile_route_deps_fixture [TYPE Function]
|
||||
# @RELATION BINDS_TO -> TestProfileApi
|
||||
# @BRIEF Pytest fixture wrapper for profile route dependency overrides.
|
||||
# @PRE: None.
|
||||
# @POST: Yields overridden dependencies and clears overrides after test.
|
||||
# @PRE None.
|
||||
# @POST Yields overridden dependencies and clears overrides after test.
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -60,8 +60,8 @@ def profile_route_deps_fixture():
|
||||
# #region _build_preference_response [TYPE Function]
|
||||
# @RELATION BINDS_TO -> TestProfileApi
|
||||
# @BRIEF Builds stable profile preference response payload for route tests.
|
||||
# @PRE: user_id is provided.
|
||||
# @POST: Returns ProfilePreferenceResponse object with deterministic timestamps.
|
||||
# @PRE user_id is provided.
|
||||
# @POST Returns ProfilePreferenceResponse object with deterministic timestamps.
|
||||
def _build_preference_response(user_id: str = "u-1") -> ProfilePreferenceResponse:
|
||||
now = datetime.now(UTC)
|
||||
return ProfilePreferenceResponse(
|
||||
@@ -99,8 +99,8 @@ def _build_preference_response(user_id: str = "u-1") -> ProfilePreferenceRespons
|
||||
# #region test_get_profile_preferences_returns_self_payload [TYPE Function]
|
||||
# @RELATION BINDS_TO -> TestProfileApi
|
||||
# @BRIEF Verifies GET /api/profile/preferences returns stable self-scoped payload.
|
||||
# @PRE: Authenticated user context is available.
|
||||
# @POST: Response status is 200 and payload contains current user preference.
|
||||
# @PRE Authenticated user context is available.
|
||||
# @POST Response status is 200 and payload contains current user preference.
|
||||
def test_get_profile_preferences_returns_self_payload(profile_route_deps_fixture):
|
||||
mock_user, _, _ = profile_route_deps_fixture
|
||||
service = MagicMock()
|
||||
@@ -128,8 +128,8 @@ def test_get_profile_preferences_returns_self_payload(profile_route_deps_fixture
|
||||
# #region test_patch_profile_preferences_success [TYPE Function]
|
||||
# @RELATION BINDS_TO -> TestProfileApi
|
||||
# @BRIEF Verifies PATCH /api/profile/preferences persists valid payload through route mapping.
|
||||
# @PRE: Valid request payload and authenticated user.
|
||||
# @POST: Response status is 200 with saved preference payload.
|
||||
# @PRE Valid request payload and authenticated user.
|
||||
# @POST Response status is 200 with saved preference payload.
|
||||
def test_patch_profile_preferences_success(profile_route_deps_fixture):
|
||||
mock_user, _, _ = profile_route_deps_fixture
|
||||
service = MagicMock()
|
||||
@@ -174,8 +174,8 @@ def test_patch_profile_preferences_success(profile_route_deps_fixture):
|
||||
# #region test_patch_profile_preferences_validation_error [TYPE Function]
|
||||
# @RELATION BINDS_TO -> TestProfileApi
|
||||
# @BRIEF Verifies route maps domain validation failure to HTTP 422 with actionable details.
|
||||
# @PRE: Service raises ProfileValidationError.
|
||||
# @POST: Response status is 422 and includes validation messages.
|
||||
# @PRE Service raises ProfileValidationError.
|
||||
# @POST Response status is 422 and includes validation messages.
|
||||
def test_patch_profile_preferences_validation_error(profile_route_deps_fixture):
|
||||
service = MagicMock()
|
||||
service.update_my_preference.side_effect = ProfileValidationError(
|
||||
@@ -197,8 +197,8 @@ def test_patch_profile_preferences_validation_error(profile_route_deps_fixture):
|
||||
# #region test_patch_profile_preferences_cross_user_denied [TYPE Function]
|
||||
# @RELATION BINDS_TO -> TestProfileApi
|
||||
# @BRIEF Verifies route maps domain authorization guard failure to HTTP 403.
|
||||
# @PRE: Service raises ProfileAuthorizationError.
|
||||
# @POST: Response status is 403 with denial message.
|
||||
# @PRE Service raises ProfileAuthorizationError.
|
||||
# @POST Response status is 403 with denial message.
|
||||
def test_patch_profile_preferences_cross_user_denied(profile_route_deps_fixture):
|
||||
service = MagicMock()
|
||||
service.update_my_preference.side_effect = ProfileAuthorizationError(
|
||||
@@ -219,8 +219,8 @@ def test_patch_profile_preferences_cross_user_denied(profile_route_deps_fixture)
|
||||
# #region test_lookup_superset_accounts_success [TYPE Function]
|
||||
# @RELATION BINDS_TO -> TestProfileApi
|
||||
# @BRIEF Verifies lookup route returns success payload with normalized candidates.
|
||||
# @PRE: Valid environment_id and service success response.
|
||||
# @POST: Response status is 200 and items list is returned.
|
||||
# @PRE Valid environment_id and service success response.
|
||||
# @POST Response status is 200 and items list is returned.
|
||||
def test_lookup_superset_accounts_success(profile_route_deps_fixture):
|
||||
service = MagicMock()
|
||||
service.lookup_superset_accounts.return_value = SupersetAccountLookupResponse(
|
||||
@@ -252,8 +252,8 @@ def test_lookup_superset_accounts_success(profile_route_deps_fixture):
|
||||
# #region test_lookup_superset_accounts_env_not_found [TYPE Function]
|
||||
# @RELATION BINDS_TO -> TestProfileApi
|
||||
# @BRIEF Verifies lookup route maps missing environment to HTTP 404.
|
||||
# @PRE: Service raises EnvironmentNotFoundError.
|
||||
# @POST: Response status is 404 with explicit message.
|
||||
# @PRE Service raises EnvironmentNotFoundError.
|
||||
# @POST Response status is 404 with explicit message.
|
||||
def test_lookup_superset_accounts_env_not_found(profile_route_deps_fixture):
|
||||
service = MagicMock()
|
||||
service.lookup_superset_accounts.side_effect = EnvironmentNotFoundError(
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# #region TestReportsApi [TYPE Module] [C:3] [SEMANTICS tests, reports, api, contract, pagination, filtering]
|
||||
# @RELATION BELONGS_TO -> SrcRoot
|
||||
# @RELATION BINDS_TO -> SrcRoot
|
||||
# @BRIEF Contract tests for GET /api/reports defaults, pagination, and filtering behavior.
|
||||
# @LAYER: Domain
|
||||
# @INVARIANT: API response contract contains {items,total,page,page_size,has_next,applied_filters}.
|
||||
# @LAYER Domain
|
||||
# @INVARIANT API response contract contains {items,total,page,page_size,has_next,applied_filters}.
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from types import SimpleNamespace
|
||||
|
||||
@@ -17,7 +17,7 @@ from src.dependencies import get_current_user, get_task_manager
|
||||
# #region _FakeTaskManager [TYPE Class] [C:1]
|
||||
# @RELATION BINDS_TO -> [TestReportsApi]
|
||||
# @BRIEF Minimal task-manager double exposing only get_all_tasks used by reports route tests.
|
||||
# @INVARIANT: Returns pre-seeded tasks without mutation or side effects.
|
||||
# @INVARIANT Returns pre-seeded tasks without mutation or side effects.
|
||||
class _FakeTaskManager:
|
||||
def __init__(self, tasks):
|
||||
self._tasks = tasks
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# #region TestReportsDetailApi [TYPE Module] [C:3] [SEMANTICS tests, reports, api, detail, diagnostics]
|
||||
# @RELATION BELONGS_TO -> SrcRoot
|
||||
# @RELATION BINDS_TO -> SrcRoot
|
||||
# @BRIEF Contract tests for GET /api/reports/{report_id} detail endpoint behavior.
|
||||
# @LAYER: Domain
|
||||
# @INVARIANT: Detail endpoint tests must keep deterministic assertions for success and not-found contracts.
|
||||
# @LAYER Domain
|
||||
# @INVARIANT Detail endpoint tests must keep deterministic assertions for success and not-found contracts.
|
||||
from datetime import datetime, timedelta
|
||||
from types import SimpleNamespace
|
||||
|
||||
@@ -17,7 +17,7 @@ from src.dependencies import get_current_user, get_task_manager
|
||||
# #region _FakeTaskManager [TYPE Class] [C:1]
|
||||
# @RELATION BINDS_TO -> [TestReportsDetailApi]
|
||||
# @BRIEF Minimal task-manager double exposing pre-seeded tasks to detail endpoint under test.
|
||||
# @INVARIANT: get_all_tasks returns exactly seeded tasks list.
|
||||
# @INVARIANT get_all_tasks returns exactly seeded tasks list.
|
||||
class _FakeTaskManager:
|
||||
def __init__(self, tasks):
|
||||
self._tasks = tasks
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# #region TestReportsOpenapiConformance [TYPE Module] [C:3] [SEMANTICS tests, reports, openapi, conformance]
|
||||
# @RELATION BELONGS_TO -> SrcRoot
|
||||
# @RELATION BINDS_TO -> SrcRoot
|
||||
# @BRIEF Validate implemented reports payload shape against OpenAPI-required top-level contract fields.
|
||||
# @LAYER: Domain
|
||||
# @INVARIANT: List and detail payloads include required contract keys.
|
||||
# @LAYER Domain
|
||||
# @INVARIANT List and detail payloads include required contract keys.
|
||||
from datetime import datetime
|
||||
from types import SimpleNamespace
|
||||
|
||||
@@ -16,7 +16,7 @@ from src.dependencies import get_current_user, get_task_manager
|
||||
# #region _FakeTaskManager [TYPE Class] [C:1]
|
||||
# @RELATION BINDS_TO -> [TestReportsOpenapiConformance]
|
||||
# @BRIEF Minimal task-manager fake exposing static task list for OpenAPI conformance checks.
|
||||
# @INVARIANT: get_all_tasks returns seeded tasks unchanged.
|
||||
# @INVARIANT get_all_tasks returns seeded tasks unchanged.
|
||||
class _FakeTaskManager:
|
||||
def __init__(self, tasks):
|
||||
self._tasks = tasks
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# #region test_tasks_logs_module [TYPE Module] [C:2] [SEMANTICS tests, tasks, logs, api, contract, validation]
|
||||
# @RELATION VERIFIES -> [src.api.routes.tasks:Module]
|
||||
# @RELATION BINDS_TO -> [EXT:frontend:TasksModule]
|
||||
# @BRIEF Contract testing for task logs API endpoints.
|
||||
# @LAYER: Domain
|
||||
# @LAYER Domain
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
@@ -12,20 +12,20 @@ from src.api.routes.tasks import router
|
||||
from src.dependencies import get_task_manager, has_permission
|
||||
|
||||
|
||||
# @TEST_FIXTURE: mock_app
|
||||
# @TEST_FIXTURE mock_app
|
||||
@pytest.fixture
|
||||
def client():
|
||||
app = FastAPI()
|
||||
app.include_router(router, prefix="/tasks")
|
||||
# Mock TaskManager
|
||||
# @INVARIANT: unconstrained mock — no spec= enforced
|
||||
# @INVARIANT unconstrained mock — no spec= enforced
|
||||
mock_tm = MagicMock()
|
||||
app.dependency_overrides[get_task_manager] = lambda: mock_tm
|
||||
# Mock permissions (bypass for unit test)
|
||||
app.dependency_overrides[has_permission("tasks", "READ")] = lambda: True
|
||||
return TestClient(app), mock_tm
|
||||
# @TEST_CONTRACT: get_task_logs_api -> Invariants
|
||||
# @TEST_FIXTURE: valid_task_logs_request
|
||||
# @TEST_CONTRACT get_task_logs_api -> Invariants
|
||||
# @TEST_FIXTURE valid_task_logs_request
|
||||
# #region test_get_task_logs_success [TYPE Function]
|
||||
# @RELATION BINDS_TO -> test_tasks_logs_module
|
||||
# @BRIEF Validate task logs endpoint returns filtered logs for an existing task.
|
||||
@@ -43,7 +43,7 @@ def test_get_task_logs_success(client):
|
||||
args = tm.get_task_logs.call_args
|
||||
assert args[0][0] == "task-1"
|
||||
assert args[0][1].level == "INFO"
|
||||
# @TEST_EDGE: task_not_found
|
||||
# @TEST_EDGE task_not_found
|
||||
# #endregion test_get_task_logs_success
|
||||
# #region test_get_task_logs_not_found [TYPE Function]
|
||||
# @RELATION BINDS_TO -> test_tasks_logs_module
|
||||
@@ -54,7 +54,7 @@ def test_get_task_logs_not_found(client):
|
||||
response = tc.get("/tasks/missing/logs")
|
||||
assert response.status_code == 404
|
||||
assert response.json()["detail"] == "Task not found"
|
||||
# @TEST_EDGE: invalid_limit
|
||||
# @TEST_EDGE invalid_limit
|
||||
# #endregion test_get_task_logs_not_found
|
||||
# #region test_get_task_logs_invalid_limit [TYPE Function]
|
||||
# @RELATION BINDS_TO -> test_tasks_logs_module
|
||||
@@ -64,7 +64,7 @@ def test_get_task_logs_invalid_limit(client):
|
||||
# limit=0 is ge=1 in Query
|
||||
response = tc.get("/tasks/task-1/logs?limit=0")
|
||||
assert response.status_code == 422
|
||||
# @TEST_INVARIANT: response_purity
|
||||
# @TEST_INVARIANT response_purity
|
||||
# #endregion test_get_task_logs_invalid_limit
|
||||
# #region test_get_task_log_stats_success [TYPE Function]
|
||||
# @RELATION BINDS_TO -> test_tasks_logs_module
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# #region AdminApi [C:5] [TYPE Module] [SEMANTICS fastapi, admin, api, rbac, user]
|
||||
#
|
||||
# @BRIEF Admin API endpoints for user and role management.
|
||||
# @LAYER: API
|
||||
# @RELATION DEPENDS_ON -> [AuthRepository:Class]
|
||||
# @RELATION DEPENDS_ON -> [get_auth_db:Function]
|
||||
# @RELATION DEPENDS_ON -> [has_permission:Function]
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [AuthRepository]
|
||||
# @RELATION DEPENDS_ON -> [get_auth_db]
|
||||
# @RELATION DEPENDS_ON -> [has_permission]
|
||||
#
|
||||
# @INVARIANT: All endpoints in this module require 'Admin' role or 'admin' scope.
|
||||
# @INVARIANT All endpoints in this module require 'Admin' role or 'admin' scope.
|
||||
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
@@ -43,8 +43,8 @@ router = APIRouter(prefix="/api/admin", tags=["admin"])
|
||||
|
||||
# #region list_users [C:3] [TYPE Function]
|
||||
# @BRIEF Lists all registered users.
|
||||
# @PRE: Current user has 'Admin' role.
|
||||
# @POST: Returns a list of UserSchema objects.
|
||||
# @PRE Current user has 'Admin' role.
|
||||
# @POST Returns a list of UserSchema objects.
|
||||
# @RELATION CALLS -> User
|
||||
@router.get("/users", response_model=list[UserSchema])
|
||||
async def list_users(
|
||||
@@ -60,9 +60,9 @@ async def list_users(
|
||||
|
||||
# #region create_user [C:3] [TYPE Function]
|
||||
# @BRIEF Creates a new local user.
|
||||
# @PRE: Current user has 'Admin' role.
|
||||
# @POST: New user is created in the database.
|
||||
# @RELATION CALLS -> [AuthRepository:Class]
|
||||
# @PRE Current user has 'Admin' role.
|
||||
# @POST New user is created in the database.
|
||||
# @RELATION CALLS -> [AuthRepository]
|
||||
@router.post("/users", response_model=UserSchema, status_code=status.HTTP_201_CREATED)
|
||||
async def create_user(
|
||||
user_in: UserCreate,
|
||||
@@ -98,8 +98,8 @@ async def create_user(
|
||||
|
||||
# #region update_user [C:3] [TYPE Function]
|
||||
# @BRIEF Updates an existing user.
|
||||
# @PRE: Current user has 'Admin' role.
|
||||
# @POST: User record is updated in the database.
|
||||
# @PRE Current user has 'Admin' role.
|
||||
# @POST User record is updated in the database.
|
||||
# @RELATION CALLS -> AuthRepository
|
||||
@router.put("/users/{user_id}", response_model=UserSchema)
|
||||
async def update_user(
|
||||
@@ -138,8 +138,8 @@ async def update_user(
|
||||
|
||||
# #region delete_user [C:3] [TYPE Function]
|
||||
# @BRIEF Deletes a user.
|
||||
# @PRE: Current user has 'Admin' role.
|
||||
# @POST: User record is removed from the database.
|
||||
# @PRE Current user has 'Admin' role.
|
||||
# @POST User record is removed from the database.
|
||||
# @RELATION CALLS -> AuthRepository
|
||||
@router.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_user(
|
||||
@@ -175,7 +175,7 @@ async def delete_user(
|
||||
|
||||
# #region list_roles [C:3] [TYPE Function]
|
||||
# @BRIEF Lists all available roles.
|
||||
# @RELATION CALLS -> [Role:Class]
|
||||
# @RELATION CALLS -> [Role]
|
||||
@router.get("/roles", response_model=list[RoleSchema])
|
||||
async def list_roles(
|
||||
db: Session = Depends(get_auth_db), _=Depends(has_permission("admin:roles", "READ"))
|
||||
@@ -189,10 +189,10 @@ async def list_roles(
|
||||
|
||||
# #region create_role [C:3] [TYPE Function]
|
||||
# @BRIEF Creates a new system role with associated permissions.
|
||||
# @PRE: Role name must be unique.
|
||||
# @POST: New Role record is created in auth.db.
|
||||
# @SIDE_EFFECT: Commits new role and associations to auth.db.
|
||||
# @RELATION CALLS -> [get_permission_by_id:Function]
|
||||
# @PRE Role name must be unique.
|
||||
# @POST New Role record is created in auth.db.
|
||||
# @SIDE_EFFECT Commits new role and associations to auth.db.
|
||||
# @RELATION CALLS -> [get_permission_by_id]
|
||||
@router.post("/roles", response_model=RoleSchema, status_code=status.HTTP_201_CREATED)
|
||||
async def create_role(
|
||||
role_in: RoleCreate,
|
||||
@@ -226,10 +226,10 @@ async def create_role(
|
||||
|
||||
# #region update_role [C:3] [TYPE Function]
|
||||
# @BRIEF Updates an existing role's metadata and permissions.
|
||||
# @PRE: role_id must be a valid existing role UUID.
|
||||
# @POST: Role record is updated in auth.db.
|
||||
# @SIDE_EFFECT: Commits updates to auth.db.
|
||||
# @RELATION CALLS -> [get_role_by_id:Function]
|
||||
# @PRE role_id must be a valid existing role UUID.
|
||||
# @POST Role record is updated in auth.db.
|
||||
# @SIDE_EFFECT Commits updates to auth.db.
|
||||
# @RELATION CALLS -> [get_role_by_id]
|
||||
@router.put("/roles/{role_id}", response_model=RoleSchema)
|
||||
async def update_role(
|
||||
role_id: str,
|
||||
@@ -269,10 +269,10 @@ async def update_role(
|
||||
|
||||
# #region delete_role [C:3] [TYPE Function]
|
||||
# @BRIEF Removes a role from the system.
|
||||
# @PRE: role_id must be a valid existing role UUID.
|
||||
# @POST: Role record is removed from auth.db.
|
||||
# @SIDE_EFFECT: Deletes record from auth.db and commits.
|
||||
# @RELATION CALLS -> [get_role_by_id:Function]
|
||||
# @PRE role_id must be a valid existing role UUID.
|
||||
# @POST Role record is removed from auth.db.
|
||||
# @SIDE_EFFECT Deletes record from auth.db and commits.
|
||||
# @RELATION CALLS -> [get_role_by_id]
|
||||
@router.delete("/roles/{role_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_role(
|
||||
role_id: str,
|
||||
@@ -295,7 +295,7 @@ async def delete_role(
|
||||
|
||||
# #region list_permissions [C:3] [TYPE Function]
|
||||
# @BRIEF Lists all available system permissions for assignment.
|
||||
# @POST: Returns a list of all PermissionSchema objects.
|
||||
# @POST Returns a list of all PermissionSchema objects.
|
||||
# @RELATION CALLS -> backend.src.core.auth.repository.AuthRepository.list_permissions
|
||||
@router.get("/permissions", response_model=list[PermissionSchema])
|
||||
async def list_permissions(
|
||||
@@ -339,9 +339,9 @@ async def list_ad_mappings(
|
||||
|
||||
|
||||
# #region create_ad_mapping [C:2] [TYPE Function]
|
||||
# @RELATION DEPENDS_ON -> [ADGroupMapping:Class]
|
||||
# @RELATION DEPENDS_ON -> [get_auth_db:Function]
|
||||
# @RELATION DEPENDS_ON -> [has_permission:Function]
|
||||
# @RELATION DEPENDS_ON -> [ADGroupMapping]
|
||||
# @RELATION DEPENDS_ON -> [get_auth_db]
|
||||
# @RELATION DEPENDS_ON -> [has_permission]
|
||||
# @BRIEF Creates a new AD Group mapping.
|
||||
@router.post("/ad-mappings", response_model=ADGroupMappingSchema)
|
||||
async def create_ad_mapping(
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [APIKeyModel]
|
||||
# @RELATION DEPENDS_ON -> [APIKeyUtilities]
|
||||
# @RELATION DEPENDS_ON -> [has_permission("admin:settings", "WRITE")]
|
||||
# @RELATION DEPENDS_ON -> [EXT:code:has_permission("admin:settings", "WRITE")]
|
||||
# @INVARIANT GET /api/admin/api-keys NEVER returns key_hash or raw_key.
|
||||
# @INVARIANT POST /api/admin/api-keys returns raw_key ONCE — never stored, never retrievable again.
|
||||
# @INVARIANT DELETE /api/admin/api-keys/{id} soft-deletes (active=False), preserves row for audit.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# #region AssistantApi [C:5] [TYPE Module] [SEMANTICS assistant, api, package, llm, execution]
|
||||
# @BRIEF API routes for LLM assistant command parsing and safe execution orchestration.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [TaskManager]
|
||||
# @RELATION DEPENDS_ON -> [AssistantMessageRecord]
|
||||
# @RELATION DEPENDS_ON -> [AssistantConfirmationRecord]
|
||||
# @RELATION DEPENDS_ON -> [AssistantAuditRecord]
|
||||
# @INVARIANT: Risky operations are never executed without valid confirmation token.
|
||||
# @INVARIANT Risky operations are never executed without valid confirmation token.
|
||||
|
||||
# Re-export public API for backward compatibility.
|
||||
from ._admin_routes import delete_conversation, get_assistant_audit, get_history, list_conversations
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# #region AssistantAdminRoutes [C:5] [TYPE Module] [SEMANTICS assistant, admin, route, audit, conversation]
|
||||
# @BRIEF FastAPI route handlers for assistant admin operations — conversation listing, deletion, history, audit.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [AssistantRoutes]
|
||||
# @RELATION DEPENDS_ON -> [AssistantSchemas]
|
||||
# @RELATION DEPENDS_ON -> [AssistantHistory]
|
||||
# @INVARIANT: Audit endpoint requires tasks:READ permission.
|
||||
# @INVARIANT Audit endpoint requires tasks:READ permission.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -39,8 +39,8 @@ from ._schemas import (
|
||||
|
||||
# #region list_conversations [C:2] [TYPE Function]
|
||||
# @BRIEF Return paginated conversation list for current user with archived flag and last message preview.
|
||||
# @PRE: Authenticated user context and valid pagination params.
|
||||
# @POST: Conversations are grouped by conversation_id sorted by latest activity descending.
|
||||
# @PRE Authenticated user context and valid pagination params.
|
||||
# @POST Conversations are grouped by conversation_id sorted by latest activity descending.
|
||||
@router.get("/conversations")
|
||||
async def list_conversations(
|
||||
page: int = Query(1, ge=1),
|
||||
@@ -135,8 +135,8 @@ async def list_conversations(
|
||||
|
||||
# #region delete_conversation [C:2] [TYPE Function]
|
||||
# @BRIEF Soft-delete or hard-delete a conversation and clear its in-memory trace.
|
||||
# @PRE: conversation_id belongs to current_user.
|
||||
# @POST: Conversation records are removed from DB and CONVERSATIONS cache.
|
||||
# @PRE conversation_id belongs to current_user.
|
||||
# @POST Conversation records are removed from DB and CONVERSATIONS cache.
|
||||
@router.delete("/conversations/{conversation_id}")
|
||||
async def delete_conversation(
|
||||
conversation_id: str,
|
||||
@@ -181,8 +181,8 @@ async def delete_conversation(
|
||||
@router.get("/history")
|
||||
# #region get_history [TYPE Function]
|
||||
# @BRIEF Retrieve paginated assistant conversation history for current user.
|
||||
# @PRE: Authenticated user is available and page params are valid.
|
||||
# @POST: Returns persistent messages and mirrored in-memory snapshot for diagnostics.
|
||||
# @PRE Authenticated user is available and page params are valid.
|
||||
# @POST Returns persistent messages and mirrored in-memory snapshot for diagnostics.
|
||||
async def get_history(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
@@ -252,8 +252,8 @@ async def get_history(
|
||||
@router.get("/audit")
|
||||
# #region get_assistant_audit [TYPE Function]
|
||||
# @BRIEF Return assistant audit decisions for current user from persistent and in-memory stores.
|
||||
# @PRE: User has tasks:READ permission.
|
||||
# @POST: Audit payload is returned in reverse chronological order from DB.
|
||||
# @PRE User has tasks:READ permission.
|
||||
# @POST Audit payload is returned in reverse chronological order from DB.
|
||||
async def get_assistant_audit(
|
||||
limit: int = Query(50, ge=1, le=500),
|
||||
current_user: User = Depends(get_current_user),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# #region AssistantCommandParser [C:4] [TYPE Module] [SEMANTICS assistant, command, parser, nlu, intent]
|
||||
# @BRIEF Deterministic RU/EN command text parser that converts user messages into intent payloads.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [AssistantResolvers]
|
||||
# @INVARIANT: Every return path includes domain, operation, entities, confidence, risk_level, requires_confirmation.
|
||||
# @INVARIANT Every return path includes domain, operation, entities, confidence, risk_level, requires_confirmation.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -17,13 +17,13 @@ from ._resolvers import _extract_id, _is_production_env
|
||||
|
||||
# #region _parse_command [C:4] [TYPE Function]
|
||||
# @BRIEF Deterministically parse RU/EN command text into intent payload.
|
||||
# @DATA_CONTRACT: Input[message:str, config_manager:ConfigManager] -> Output[Dict[str,Any]{domain,operation,entities,confidence,risk_level,requires_confirmation}]
|
||||
# @DATA_CONTRACT Input[message:str, config_manager:ConfigManager] -> Output[Dict[str,Any]{domain,operation,entities,confidence,risk_level,requires_confirmation}]
|
||||
# @RELATION DEPENDS_ON -> [_extract_id]
|
||||
# @RELATION DEPENDS_ON -> [_is_production_env]
|
||||
# @SIDE_EFFECT: None (pure parsing logic).
|
||||
# @PRE: message contains raw user text and config manager resolves environments.
|
||||
# @POST: Returns intent dict with domain/operation/entities/confidence/risk fields.
|
||||
# @INVARIANT: every return path includes domain, operation, entities, confidence, risk_level, requires_confirmation.
|
||||
# @SIDE_EFFECT None (pure parsing logic).
|
||||
# @PRE message contains raw user text and config manager resolves environments.
|
||||
# @POST Returns intent dict with domain/operation/entities/confidence/risk fields.
|
||||
# @INVARIANT every return path includes domain, operation, entities, confidence, risk_level, requires_confirmation.
|
||||
def _parse_command(message: str, config_manager: ConfigManager) -> dict[str, Any]:
|
||||
with belief_scope('_parse_command'):
|
||||
logger.reason('Belief protocol reasoning checkpoint for _parse_command')
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# #region AssistantDatasetReview [C:4] [TYPE Module] [SEMANTICS assistant, dataset, review, context, intent]
|
||||
# @BRIEF Dataset review context loading and intent planning for the assistant API.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [DatasetReviewOrchestrator]
|
||||
# @RELATION DEPENDS_ON -> [AssistantSchemas]
|
||||
# @RELATION DISPATCHES -> [AssistantDatasetReviewDispatch]
|
||||
# @INVARIANT: Dataset review operations are always scoped to the owner's session.
|
||||
# @INVARIANT Dataset review operations are always scoped to the owner's session.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -31,9 +31,9 @@ from src.services.dataset_review.repositories.session_repository import (
|
||||
# #region _serialize_dataset_review_context [C:4] [TYPE Function]
|
||||
# @BRIEF Build assistant-safe dataset-review context snapshot with masked imported-filter payloads for session-scoped assistant routing.
|
||||
# @RELATION DEPENDS_ON -> [DatasetReviewSession]
|
||||
# @PRE: session_id is a valid active review session identifier.
|
||||
# @POST: Returns a serializable dictionary containing the complete review context.
|
||||
# @SIDE_EFFECT: Reads session data from the database.
|
||||
# @PRE session_id is a valid active review session identifier.
|
||||
# @POST Returns a serializable dictionary containing the complete review context.
|
||||
# @SIDE_EFFECT Reads session data from the database.
|
||||
def _serialize_dataset_review_context(session: DatasetReviewSession) -> dict[str, Any]:
|
||||
with belief_scope('_serialize_dataset_review_context'):
|
||||
logger.reason('Belief protocol reasoning checkpoint for _serialize_dataset_review_context')
|
||||
@@ -51,9 +51,9 @@ def _serialize_dataset_review_context(session: DatasetReviewSession) -> dict[str
|
||||
# #region _load_dataset_review_context [C:4] [TYPE Function]
|
||||
# @BRIEF Load owner-scoped dataset-review context for assistant planning and grounded response generation.
|
||||
# @RELATION DEPENDS_ON -> [DatasetReviewSessionRepository]
|
||||
# @PRE: session_id is a valid active review session identifier.
|
||||
# @POST: Returns a loaded context object with session data and findings.
|
||||
# @SIDE_EFFECT: Reads session data from the database.
|
||||
# @PRE session_id is a valid active review session identifier.
|
||||
# @POST Returns a loaded context object with session data and findings.
|
||||
# @SIDE_EFFECT Reads session data from the database.
|
||||
def _load_dataset_review_context(dataset_review_session_id: str | None, current_user: User, db: Session) -> dict[str, Any] | None:
|
||||
with belief_scope('_load_dataset_review_context'):
|
||||
if not dataset_review_session_id:
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# #region AssistantDatasetReviewDispatch [C:4] [TYPE Module] [SEMANTICS assistant, dataset, review, dispatch, confirm]
|
||||
# @BRIEF Dispatch and confirmation handling for dataset-review assistant intents.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [AssistantDatasetReview]
|
||||
# @RELATION DEPENDS_ON -> [DatasetReviewOrchestrator]
|
||||
# @RELATION DEPENDS_ON -> [AssistantSchemas]
|
||||
# @INVARIANT: Dataset review dispatch requires valid session version for write operations.
|
||||
# @INVARIANT Dataset review dispatch requires valid session version for write operations.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -60,9 +60,9 @@ def _dataset_review_conflict_http_exception(
|
||||
# #region _dispatch_dataset_review_intent [C:4] [TYPE Function]
|
||||
# @BRIEF Route confirmed dataset-review assistant intents through existing backend dataset-review APIs and orchestration boundaries.
|
||||
# @RELATION CALLS -> DatasetReviewOrchestrator
|
||||
# @PRE: context contains valid session data and user intent.
|
||||
# @POST: Returns a structured response with planned actions and confirmations.
|
||||
# @SIDE_EFFECT: May update session state and enqueue tasks.
|
||||
# @PRE context contains valid session data and user intent.
|
||||
# @POST Returns a structured response with planned actions and confirmations.
|
||||
# @SIDE_EFFECT May update session state and enqueue tasks.
|
||||
async def _dispatch_dataset_review_intent(
|
||||
intent: dict[str, Any],
|
||||
current_user: User,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# #region AssistantDispatch [C:5] [TYPE Module] [SEMANTICS assistant, dispatch, confirm, execution, orchestration]
|
||||
# @BRIEF Intent dispatch engine, confirmation summary, and clarification text for the assistant API.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [AssistantSchemas]
|
||||
# @RELATION DEPENDS_ON -> [AssistantResolvers]
|
||||
# @RELATION DEPENDS_ON -> [AssistantLlmPlanner]
|
||||
# @RELATION DEPENDS_ON -> [AssistantDatasetReview]
|
||||
# @INVARIANT: Unsupported operations are rejected via HTTPException(400).
|
||||
# @INVARIANT Unsupported operations are rejected via HTTPException(400).
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -42,8 +42,8 @@ git_service = GitService()
|
||||
|
||||
# #region _clarification_text_for_intent [C:2] [TYPE Function]
|
||||
# @BRIEF Convert technical missing-parameter errors into user-facing clarification prompts.
|
||||
# @PRE: state was classified as needs_clarification for current intent/error combination.
|
||||
# @POST: Returned text is human-readable and actionable for target operation.
|
||||
# @PRE state was classified as needs_clarification for current intent/error combination.
|
||||
# @POST Returned text is human-readable and actionable for target operation.
|
||||
def _clarification_text_for_intent(
|
||||
intent: dict[str, Any] | None, detail_text: str
|
||||
) -> str:
|
||||
@@ -69,9 +69,9 @@ def _clarification_text_for_intent(
|
||||
|
||||
# #region _async_confirmation_summary [C:4] [TYPE Function]
|
||||
# @BRIEF Build human-readable confirmation prompt for an intent before execution.
|
||||
# @PRE: actions is a non-empty list of planned review actions.
|
||||
# @POST: Returns a formatted summary string suitable for display to the user.
|
||||
# @SIDE_EFFECT: None - pure formatting function.
|
||||
# @PRE actions is a non-empty list of planned review actions.
|
||||
# @POST Returns a formatted summary string suitable for display to the user.
|
||||
# @SIDE_EFFECT None - pure formatting function.
|
||||
async def _async_confirmation_summary(intent: dict[str, Any], config_manager: ConfigManager, db: Session) -> str:
|
||||
with belief_scope('_confirmation_summary'):
|
||||
logger.reason('Belief protocol reasoning checkpoint for _confirmation_summary')
|
||||
@@ -139,15 +139,15 @@ async def _async_confirmation_summary(intent: dict[str, Any], config_manager: Co
|
||||
|
||||
# #region _dispatch_intent [C:5] [TYPE Function]
|
||||
# @BRIEF Execute parsed assistant intent via existing task/plugin/git services.
|
||||
# @DATA_CONTRACT: Input[intent,current_user,task_manager,config_manager,db] -> Output[Tuple[text:str,task_id:Optional[str],actions:List[AssistantAction]]]
|
||||
# @DATA_CONTRACT Input[intent,current_user,task_manager,config_manager,db] -> Output[Tuple[text:str,task_id:Optional[str],actions:List[AssistantAction]]]
|
||||
# @RELATION DEPENDS_ON -> [_check_any_permission]
|
||||
# @RELATION DEPENDS_ON -> [_resolve_dashboard_id_entity]
|
||||
# @RELATION DEPENDS_ON -> [TaskManager]
|
||||
# @RELATION DEPENDS_ON -> [GitService]
|
||||
# @SIDE_EFFECT: May enqueue tasks, invoke git operations, and query/update external service state.
|
||||
# @PRE: intent operation is known and actor permissions are validated per operation.
|
||||
# @POST: Returns response text, optional task id, and UI actions for follow-up.
|
||||
# @INVARIANT: unsupported operations are rejected via HTTPException(400).
|
||||
# @SIDE_EFFECT May enqueue tasks, invoke git operations, and query/update external service state.
|
||||
# @PRE intent operation is known and actor permissions are validated per operation.
|
||||
# @POST Returns response text, optional task id, and UI actions for follow-up.
|
||||
# @INVARIANT unsupported operations are rejected via HTTPException(400).
|
||||
async def _dispatch_intent(intent: dict[str, Any], current_user: User, task_manager: TaskManager, config_manager: ConfigManager, db: Session) -> tuple[str, str | None, list[AssistantAction]]:
|
||||
with belief_scope('_dispatch_intent'):
|
||||
logger.reason('Belief protocol reasoning checkpoint for _dispatch_intent')
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# #region AssistantHistory [C:2] [TYPE Module] [SEMANTICS assistant, history, audit, persistence, conversation]
|
||||
# @BRIEF Conversation history, audit trail, and confirmation persistence helpers for the assistant API.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [AssistantSchemas]
|
||||
# @INVARIANT: Failed persistence attempts always rollback before returning.
|
||||
# @INVARIANT Failed persistence attempts always rollback before returning.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -32,12 +32,12 @@ logger = logger
|
||||
|
||||
# #region _append_history [C:2] [TYPE Function]
|
||||
# @BRIEF Append conversation message to in-memory history buffer.
|
||||
# @DATA_CONTRACT: Input[user_id,conversation_id,role,text,state?,task_id?,confirmation_id?] -> Output[None]
|
||||
# @RELATION UPDATES -> [CONVERSATIONS]
|
||||
# @SIDE_EFFECT: Mutates in-memory CONVERSATIONS store for user conversation history.
|
||||
# @PRE: user_id and conversation_id identify target conversation bucket.
|
||||
# @POST: Message entry is appended to CONVERSATIONS key list.
|
||||
# @INVARIANT: every appended entry includes generated message_id and created_at timestamp.
|
||||
# @DATA_CONTRACT Input[user_id,conversation_id,role,text,state?,task_id?,confirmation_id?] -> Output[None]
|
||||
# @RELATION BINDS_TO -> [EXT:internal:CONVERSATIONS]
|
||||
# @SIDE_EFFECT Mutates in-memory CONVERSATIONS store for user conversation history.
|
||||
# @PRE user_id and conversation_id identify target conversation bucket.
|
||||
# @POST Message entry is appended to CONVERSATIONS key list.
|
||||
# @INVARIANT every appended entry includes generated message_id and created_at timestamp.
|
||||
def _append_history(
|
||||
user_id: str,
|
||||
conversation_id: str,
|
||||
@@ -69,12 +69,12 @@ def _append_history(
|
||||
|
||||
# #region _persist_message [C:2] [TYPE Function]
|
||||
# @BRIEF Persist assistant/user message record to database.
|
||||
# @DATA_CONTRACT: Input[Session,user_id,conversation_id,role,text,state?,task_id?,confirmation_id?,metadata?] -> Output[None]
|
||||
# @DATA_CONTRACT Input[Session,user_id,conversation_id,role,text,state?,task_id?,confirmation_id?,metadata?] -> Output[None]
|
||||
# @RELATION DEPENDS_ON -> [AssistantMessageRecord]
|
||||
# @SIDE_EFFECT: Writes AssistantMessageRecord rows and commits or rollbacks the DB session.
|
||||
# @PRE: db session is writable and message payload is serializable.
|
||||
# @POST: Message row is committed or persistence failure is logged.
|
||||
# @INVARIANT: failed persistence attempts always rollback before returning.
|
||||
# @SIDE_EFFECT Writes AssistantMessageRecord rows and commits or rollbacks the DB session.
|
||||
# @PRE db session is writable and message payload is serializable.
|
||||
# @POST Message row is committed or persistence failure is logged.
|
||||
# @INVARIANT failed persistence attempts always rollback before returning.
|
||||
def _persist_message(
|
||||
db: Session,
|
||||
user_id: str,
|
||||
@@ -110,12 +110,12 @@ def _persist_message(
|
||||
|
||||
# #region _audit [C:2] [TYPE Function]
|
||||
# @BRIEF Append in-memory audit record for assistant decision trace.
|
||||
# @DATA_CONTRACT: Input[user_id,payload:Dict[str,Any]] -> Output[None]
|
||||
# @RELATION UPDATES -> [ASSISTANT_AUDIT]
|
||||
# @SIDE_EFFECT: Mutates in-memory ASSISTANT_AUDIT store and emits structured log event.
|
||||
# @PRE: payload describes decision/outcome fields.
|
||||
# @POST: ASSISTANT_AUDIT list for user contains new timestamped entry.
|
||||
# @INVARIANT: persisted in-memory audit entry always contains created_at in ISO format.
|
||||
# @DATA_CONTRACT Input[user_id,payload:Dict[str,Any]] -> Output[None]
|
||||
# @RELATION BINDS_TO -> [EXT:internal:ASSISTANT_AUDIT]
|
||||
# @SIDE_EFFECT Mutates in-memory ASSISTANT_AUDIT store and emits structured log event.
|
||||
# @PRE payload describes decision/outcome fields.
|
||||
# @POST ASSISTANT_AUDIT list for user contains new timestamped entry.
|
||||
# @INVARIANT persisted in-memory audit entry always contains created_at in ISO format.
|
||||
def _audit(user_id: str, payload: dict[str, Any]):
|
||||
if user_id not in ASSISTANT_AUDIT:
|
||||
ASSISTANT_AUDIT[user_id] = []
|
||||
@@ -130,8 +130,8 @@ def _audit(user_id: str, payload: dict[str, Any]):
|
||||
|
||||
# #region _persist_audit [C:2] [TYPE Function]
|
||||
# @BRIEF Persist structured assistant audit payload in database.
|
||||
# @PRE: db session is writable and payload is JSON-serializable.
|
||||
# @POST: Audit row is committed or failure is logged with rollback.
|
||||
# @PRE db session is writable and payload is JSON-serializable.
|
||||
# @POST Audit row is committed or failure is logged with rollback.
|
||||
def _persist_audit(
|
||||
db: Session, user_id: str, payload: dict[str, Any], conversation_id: str | None
|
||||
):
|
||||
@@ -157,8 +157,8 @@ def _persist_audit(
|
||||
|
||||
# #region _persist_confirmation [C:2] [TYPE Function]
|
||||
# @BRIEF Persist confirmation token record to database.
|
||||
# @PRE: record contains id/user/intent/dispatch/expiry fields.
|
||||
# @POST: Confirmation row exists in persistent storage.
|
||||
# @PRE record contains id/user/intent/dispatch/expiry fields.
|
||||
# @POST Confirmation row exists in persistent storage.
|
||||
def _persist_confirmation(db: Session, record: ConfirmationRecord):
|
||||
try:
|
||||
row = AssistantConfirmationRecord(
|
||||
@@ -184,8 +184,8 @@ def _persist_confirmation(db: Session, record: ConfirmationRecord):
|
||||
|
||||
# #region _update_confirmation_state [C:2] [TYPE Function]
|
||||
# @BRIEF Update persistent confirmation token lifecycle state.
|
||||
# @PRE: confirmation_id references existing row.
|
||||
# @POST: State and consumed_at fields are updated when applicable.
|
||||
# @PRE confirmation_id references existing row.
|
||||
# @POST State and consumed_at fields are updated when applicable.
|
||||
def _update_confirmation_state(db: Session, confirmation_id: str, state: str):
|
||||
try:
|
||||
row = (
|
||||
@@ -209,8 +209,8 @@ def _update_confirmation_state(db: Session, confirmation_id: str, state: str):
|
||||
|
||||
# #region _load_confirmation_from_db [C:2] [TYPE Function]
|
||||
# @BRIEF Load confirmation token from database into in-memory model.
|
||||
# @PRE: confirmation_id may or may not exist in storage.
|
||||
# @POST: Returns ConfirmationRecord when found, otherwise None.
|
||||
# @PRE confirmation_id may or may not exist in storage.
|
||||
# @POST Returns ConfirmationRecord when found, otherwise None.
|
||||
def _load_confirmation_from_db(
|
||||
db: Session, confirmation_id: str
|
||||
) -> ConfirmationRecord | None:
|
||||
@@ -238,8 +238,8 @@ def _load_confirmation_from_db(
|
||||
|
||||
# #region _ensure_conversation [C:2] [TYPE Function]
|
||||
# @BRIEF Resolve active conversation id in memory or create a new one.
|
||||
# @PRE: user_id identifies current actor.
|
||||
# @POST: Returns stable conversation id and updates USER_ACTIVE_CONVERSATION.
|
||||
# @PRE user_id identifies current actor.
|
||||
# @POST Returns stable conversation id and updates USER_ACTIVE_CONVERSATION.
|
||||
def _ensure_conversation(user_id: str, conversation_id: str | None) -> str:
|
||||
if conversation_id:
|
||||
from ._schemas import USER_ACTIVE_CONVERSATION
|
||||
@@ -261,8 +261,8 @@ def _ensure_conversation(user_id: str, conversation_id: str | None) -> str:
|
||||
|
||||
# #region _resolve_or_create_conversation [C:2] [TYPE Function]
|
||||
# @BRIEF Resolve active conversation using explicit id, memory cache, or persisted history.
|
||||
# @PRE: user_id and db session are available.
|
||||
# @POST: Returns conversation id and updates USER_ACTIVE_CONVERSATION cache.
|
||||
# @PRE user_id and db session are available.
|
||||
# @POST Returns conversation id and updates USER_ACTIVE_CONVERSATION cache.
|
||||
def _resolve_or_create_conversation(
|
||||
user_id: str, conversation_id: str | None, db: Session
|
||||
) -> str:
|
||||
@@ -298,8 +298,8 @@ def _resolve_or_create_conversation(
|
||||
|
||||
# #region _cleanup_history_ttl [C:2] [TYPE Function]
|
||||
# @BRIEF Enforce assistant message retention window by deleting expired rows and in-memory records.
|
||||
# @PRE: db session is available and user_id references current actor scope.
|
||||
# @POST: Messages older than ASSISTANT_MESSAGE_TTL_DAYS are removed from persistence and memory mirrors.
|
||||
# @PRE db session is available and user_id references current actor scope.
|
||||
# @POST Messages older than ASSISTANT_MESSAGE_TTL_DAYS are removed from persistence and memory mirrors.
|
||||
def _cleanup_history_ttl(db: Session, user_id: str):
|
||||
cutoff = datetime.utcnow() - timedelta(days=ASSISTANT_MESSAGE_TTL_DAYS)
|
||||
try:
|
||||
@@ -339,8 +339,8 @@ def _cleanup_history_ttl(db: Session, user_id: str):
|
||||
|
||||
# #region _is_conversation_archived [C:2] [TYPE Function]
|
||||
# @BRIEF Determine archived state for a conversation based on last update timestamp.
|
||||
# @PRE: updated_at can be null for empty conversations.
|
||||
# @POST: Returns True when conversation inactivity exceeds archive threshold.
|
||||
# @PRE updated_at can be null for empty conversations.
|
||||
# @POST Returns True when conversation inactivity exceeds archive threshold.
|
||||
def _is_conversation_archived(updated_at: datetime | None) -> bool:
|
||||
if not updated_at:
|
||||
return False
|
||||
@@ -353,8 +353,8 @@ def _is_conversation_archived(updated_at: datetime | None) -> bool:
|
||||
|
||||
# #region _coerce_query_bool [C:2] [TYPE Function]
|
||||
# @BRIEF Normalize bool-like query values for compatibility in direct handler invocations/tests.
|
||||
# @PRE: value may be bool, string, or FastAPI Query metadata object.
|
||||
# @POST: Returns deterministic boolean flag.
|
||||
# @PRE value may be bool, string, or FastAPI Query metadata object.
|
||||
# @POST Returns deterministic boolean flag.
|
||||
def _coerce_query_bool(value: Any) -> bool:
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# #region AssistantLlmPlanner [C:5] [TYPE Module] [SEMANTICS assistant, llm, planner, tool, catalog]
|
||||
# @BRIEF LLM-based intent planning, tool catalog construction, and authorization for the assistant API.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [AssistantSchemas]
|
||||
# @RELATION DEPENDS_ON -> [AssistantResolvers]
|
||||
# @RELATION DISPATCHES -> [AssistantLlmPlannerIntent]
|
||||
# @PRE: Assistant routes initialized, user authenticated
|
||||
# @POST: LLM tool catalog filtered and returned
|
||||
# @INVARIANT: Tool catalog is filtered by user permissions before being sent to LLM.
|
||||
# @SIDE_EFFECT: Filters tool catalog by user permissions
|
||||
# @DATA_CONTRACT: UserPermissions -> ToolCatalog
|
||||
# @PRE Assistant routes initialized, user authenticated
|
||||
# @POST LLM tool catalog filtered and returned
|
||||
# @INVARIANT Tool catalog is filtered by user permissions before being sent to LLM.
|
||||
# @SIDE_EFFECT Filters tool catalog by user permissions
|
||||
# @DATA_CONTRACT UserPermissions -> ToolCatalog
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -33,8 +33,8 @@ from ._schemas import (
|
||||
|
||||
# #region _check_any_permission [C:2] [TYPE Function]
|
||||
# @BRIEF Validate user against alternative permission checks (logical OR).
|
||||
# @PRE: checks list contains resource-action tuples.
|
||||
# @POST: Returns on first successful permission; raises 403-like HTTPException otherwise.
|
||||
# @PRE checks list contains resource-action tuples.
|
||||
# @POST Returns on first successful permission; raises 403-like HTTPException otherwise.
|
||||
def _check_any_permission(current_user: User, checks: list[tuple[str, str]]):
|
||||
errors: list[HTTPException] = []
|
||||
for resource, action in checks:
|
||||
@@ -56,8 +56,8 @@ def _check_any_permission(current_user: User, checks: list[tuple[str, str]]):
|
||||
|
||||
# #region _has_any_permission [C:2] [TYPE Function]
|
||||
# @BRIEF Check whether user has at least one permission tuple from the provided list.
|
||||
# @PRE: current_user and checks list are valid.
|
||||
# @POST: Returns True when at least one permission check passes.
|
||||
# @PRE current_user and checks list are valid.
|
||||
# @POST Returns True when at least one permission check passes.
|
||||
def _has_any_permission(current_user: User, checks: list[tuple[str, str]]) -> bool:
|
||||
try:
|
||||
_check_any_permission(current_user, checks)
|
||||
@@ -71,8 +71,8 @@ def _has_any_permission(current_user: User, checks: list[tuple[str, str]]) -> bo
|
||||
|
||||
# #region _build_tool_catalog [C:3] [TYPE Function]
|
||||
# @BRIEF Build current-user tool catalog for LLM planner with operation contracts and defaults.
|
||||
# @PRE: current_user is authenticated; config/db are available.
|
||||
# @POST: Returns list of executable tools filtered by permission and runtime availability.
|
||||
# @PRE current_user is authenticated; config/db are available.
|
||||
# @POST Returns list of executable tools filtered by permission and runtime availability.
|
||||
# @RELATION CALLS -> LLMProviderService
|
||||
def _build_tool_catalog(
|
||||
current_user: User,
|
||||
@@ -273,8 +273,8 @@ def _build_tool_catalog(
|
||||
|
||||
# #region _coerce_intent_entities [C:2] [TYPE Function]
|
||||
# @BRIEF Normalize intent entity value types from LLM output to route-compatible values.
|
||||
# @PRE: intent contains entities dict or missing entities.
|
||||
# @POST: Returned intent has numeric ids coerced where possible and string values stripped.
|
||||
# @PRE intent contains entities dict or missing entities.
|
||||
# @POST Returned intent has numeric ids coerced where possible and string values stripped.
|
||||
def _coerce_intent_entities(intent: dict[str, Any]) -> dict[str, Any]:
|
||||
entities = intent.get("entities")
|
||||
if not isinstance(entities, dict):
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# #region AssistantLlmPlannerIntent [C:5] [TYPE Module] [SEMANTICS assistant, llm, intent, planning, authorization]
|
||||
# @BRIEF LLM-based intent planning and authorization for the assistant API — separated from tool catalog.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [AssistantLlmPlanner]
|
||||
# @RELATION DEPENDS_ON -> [AssistantResolvers]
|
||||
# @PRE: Assistant routes initialized, user authenticated
|
||||
# @POST: Intent planning registered with confirmation gate
|
||||
# @INVARIANT: Production deployments always require confirmation.
|
||||
# @SIDE_EFFECT: Registers intent planning routes
|
||||
# @DATA_CONTRACT: UserIntent -> PlannedAction
|
||||
# @PRE Assistant routes initialized, user authenticated
|
||||
# @POST Intent planning registered with confirmation gate
|
||||
# @INVARIANT Production deployments always require confirmation.
|
||||
# @SIDE_EFFECT Registers intent planning routes
|
||||
# @DATA_CONTRACT UserIntent -> PlannedAction
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -39,8 +39,8 @@ from ._schemas import INTENT_PERMISSION_CHECKS
|
||||
|
||||
# #region _plan_intent_with_llm [C:2] [TYPE Function]
|
||||
# @BRIEF Use active LLM provider to select best tool/operation from dynamic catalog.
|
||||
# @PRE: tools list contains allowed operations for current user.
|
||||
# @POST: Returns normalized intent dict when planning succeeds; otherwise None.
|
||||
# @PRE tools list contains allowed operations for current user.
|
||||
# @POST Returns normalized intent dict when planning succeeds; otherwise None.
|
||||
async def _plan_intent_with_llm(
|
||||
message: str,
|
||||
tools: list[dict[str, Any]],
|
||||
@@ -160,8 +160,8 @@ async def _plan_intent_with_llm(
|
||||
|
||||
# #region _authorize_intent [C:2] [TYPE Function]
|
||||
# @BRIEF Validate user permissions for parsed intent before confirmation/dispatch.
|
||||
# @PRE: intent.operation is present for known assistant command domains.
|
||||
# @POST: Returns if authorized; raises HTTPException(403) when denied.
|
||||
# @PRE intent.operation is present for known assistant command domains.
|
||||
# @POST Returns if authorized; raises HTTPException(403) when denied.
|
||||
def _authorize_intent(intent: dict[str, Any], current_user: User):
|
||||
operation = intent.get("operation")
|
||||
if operation in INTENT_PERMISSION_CHECKS:
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# #region AssistantResolvers [C:2] [TYPE Module] [SEMANTICS assistant, resolver, lookup, environment, mapper]
|
||||
# @BRIEF Environment, dashboard, provider, and task resolution utilities for the assistant API.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [ConfigManager]
|
||||
# @RELATION DEPENDS_ON -> [SupersetClient]
|
||||
# @INVARIANT: Resolution functions never raise; they return None on failure.
|
||||
# @INVARIANT Resolution functions never raise; they return None on failure.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -23,8 +23,8 @@ logger = cast(Any, logger)
|
||||
|
||||
# #region _extract_id [C:2] [TYPE Function]
|
||||
# @BRIEF Extract first regex match group from text by ordered pattern list.
|
||||
# @PRE: patterns contain at least one capture group.
|
||||
# @POST: Returns first matched token or None.
|
||||
# @PRE patterns contain at least one capture group.
|
||||
# @POST Returns first matched token or None.
|
||||
def _extract_id(text: str, patterns: list[str]) -> str | None:
|
||||
for p in patterns:
|
||||
m = re.search(p, text, flags=re.IGNORECASE)
|
||||
@@ -37,8 +37,8 @@ def _extract_id(text: str, patterns: list[str]) -> str | None:
|
||||
|
||||
# #region _resolve_env_id [C:2] [TYPE Function]
|
||||
# @BRIEF Resolve environment identifier/name token to canonical environment id.
|
||||
# @PRE: config_manager provides environment list.
|
||||
# @POST: Returns matched environment id or None.
|
||||
# @PRE config_manager provides environment list.
|
||||
# @POST Returns matched environment id or None.
|
||||
def _resolve_env_id(
|
||||
token: str | None, config_manager: ConfigManager
|
||||
) -> str | None:
|
||||
@@ -57,8 +57,8 @@ def _resolve_env_id(
|
||||
|
||||
# #region _is_production_env [C:2] [TYPE Function]
|
||||
# @BRIEF Determine whether environment token resolves to production-like target.
|
||||
# @PRE: config_manager provides environments or token text is provided.
|
||||
# @POST: Returns True for production/prod synonyms, else False.
|
||||
# @PRE config_manager provides environments or token text is provided.
|
||||
# @POST Returns True for production/prod synonyms, else False.
|
||||
def _is_production_env(token: str | None, config_manager: ConfigManager) -> bool:
|
||||
env_id = _resolve_env_id(token, config_manager)
|
||||
if not env_id:
|
||||
@@ -75,8 +75,8 @@ def _is_production_env(token: str | None, config_manager: ConfigManager) -> bool
|
||||
|
||||
# #region _resolve_provider_id [C:2] [TYPE Function]
|
||||
# @BRIEF Resolve provider token to provider id with active/default fallback.
|
||||
# @PRE: db session can load provider list through LLMProviderService.
|
||||
# @POST: Returns provider id or None when no providers configured.
|
||||
# @PRE db session can load provider list through LLMProviderService.
|
||||
# @POST Returns provider id or None when no providers configured.
|
||||
def _resolve_provider_id(
|
||||
provider_token: str | None,
|
||||
db: Session,
|
||||
@@ -111,8 +111,8 @@ def _resolve_provider_id(
|
||||
|
||||
# #region _get_default_environment_id [C:2] [TYPE Function]
|
||||
# @BRIEF Resolve default environment id from settings or first configured environment.
|
||||
# @PRE: config_manager returns environments list.
|
||||
# @POST: Returns default environment id or None when environment list is empty.
|
||||
# @PRE config_manager returns environments list.
|
||||
# @POST Returns default environment id or None when environment list is empty.
|
||||
def _get_default_environment_id(config_manager: ConfigManager) -> str | None:
|
||||
configured = config_manager.get_environments()
|
||||
if not configured:
|
||||
@@ -135,8 +135,8 @@ def _get_default_environment_id(config_manager: ConfigManager) -> str | None:
|
||||
|
||||
# #region _resolve_dashboard_id_by_ref [C:2] [TYPE Function]
|
||||
# @BRIEF Resolve dashboard id by title or slug reference in selected environment.
|
||||
# @PRE: dashboard_ref is a non-empty string-like token.
|
||||
# @POST: Returns dashboard id when uniquely matched, otherwise None.
|
||||
# @PRE dashboard_ref is a non-empty string-like token.
|
||||
# @POST Returns dashboard id when uniquely matched, otherwise None.
|
||||
def _resolve_dashboard_id_by_ref(
|
||||
dashboard_ref: str | None,
|
||||
env_id: str | None,
|
||||
@@ -187,8 +187,8 @@ def _resolve_dashboard_id_by_ref(
|
||||
|
||||
# #region _resolve_dashboard_id_entity [C:2] [TYPE Function]
|
||||
# @BRIEF Resolve dashboard id from intent entities using numeric id or dashboard_ref fallback.
|
||||
# @PRE: entities may contain dashboard_id as int/str and optional dashboard_ref.
|
||||
# @POST: Returns resolved dashboard id or None when ambiguous/unresolvable.
|
||||
# @PRE entities may contain dashboard_id as int/str and optional dashboard_ref.
|
||||
# @POST Returns resolved dashboard id or None when ambiguous/unresolvable.
|
||||
def _resolve_dashboard_id_entity(
|
||||
entities: dict[str, Any],
|
||||
config_manager: ConfigManager,
|
||||
@@ -228,8 +228,8 @@ def _resolve_dashboard_id_entity(
|
||||
|
||||
# #region _get_environment_name_by_id [C:2] [TYPE Function]
|
||||
# @BRIEF Resolve human-readable environment name by id.
|
||||
# @PRE: environment id may be None.
|
||||
# @POST: Returns matching environment name or fallback id.
|
||||
# @PRE environment id may be None.
|
||||
# @POST Returns matching environment name or fallback id.
|
||||
def _get_environment_name_by_id(
|
||||
env_id: str | None, config_manager: ConfigManager
|
||||
) -> str:
|
||||
@@ -245,8 +245,8 @@ def _get_environment_name_by_id(
|
||||
|
||||
# #region _extract_result_deep_links [C:2] [TYPE Function]
|
||||
# @BRIEF Build deep-link actions to verify task result from assistant chat.
|
||||
# @PRE: task object is available.
|
||||
# @POST: Returns zero or more assistant actions for dashboard open/diff.
|
||||
# @PRE task object is available.
|
||||
# @POST Returns zero or more assistant actions for dashboard open/diff.
|
||||
def _extract_result_deep_links(
|
||||
task: Any, config_manager: ConfigManager
|
||||
) -> list:
|
||||
@@ -318,8 +318,8 @@ def _extract_result_deep_links(
|
||||
|
||||
# #region _build_task_observability_summary [C:2] [TYPE Function]
|
||||
# @BRIEF Build compact textual summary for completed tasks to reduce "black box" effect.
|
||||
# @PRE: task may contain plugin-specific result payload.
|
||||
# @POST: Returns non-empty summary line for known task types or empty string fallback.
|
||||
# @PRE task may contain plugin-specific result payload.
|
||||
# @POST Returns non-empty summary line for known task types or empty string fallback.
|
||||
def _build_task_observability_summary(task: Any, config_manager: ConfigManager) -> str:
|
||||
plugin_id = getattr(task, "plugin_id", None)
|
||||
status = str(getattr(task, "status", "")).upper()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# #region AssistantRoutes [C:5] [TYPE Module] [SEMANTICS assistant, api, route, chat, execution]
|
||||
# @BRIEF FastAPI route handlers for the assistant API — message sending, confirmation, conversation management.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [AssistantSchemas]
|
||||
# @RELATION DEPENDS_ON -> [AssistantHistory]
|
||||
# @RELATION DEPENDS_ON -> [AssistantCommandParser]
|
||||
@@ -8,7 +8,7 @@
|
||||
# @RELATION DEPENDS_ON -> [AssistantDatasetReview]
|
||||
# @RELATION DEPENDS_ON -> [AssistantDispatch]
|
||||
# @RELATION DISPATCHES -> [AssistantAdminRoutes]
|
||||
# @INVARIANT: Risky operations are never executed without valid confirmation token.
|
||||
# @INVARIANT Risky operations are never executed without valid confirmation token.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -66,17 +66,17 @@ router = APIRouter(tags=["Assistant"])
|
||||
@router.post("/messages", response_model=AssistantMessageResponse)
|
||||
# #region send_message [C:5] [TYPE Function]
|
||||
# @BRIEF Parse assistant command, enforce safety gates, and dispatch executable intent.
|
||||
# @DATA_CONTRACT: Input[AssistantMessageRequest,User,TaskManager,ConfigManager,Session] -> Output[AssistantMessageResponse]
|
||||
# @DATA_CONTRACT Input[AssistantMessageRequest,User,TaskManager,ConfigManager,Session] -> Output[AssistantMessageResponse]
|
||||
# @RELATION DEPENDS_ON -> [_plan_intent_with_llm]
|
||||
# @RELATION DEPENDS_ON -> [_parse_command]
|
||||
# @RELATION DEPENDS_ON -> [_dispatch_intent]
|
||||
# @RELATION DEPENDS_ON -> [_append_history]
|
||||
# @RELATION DEPENDS_ON -> [_persist_message]
|
||||
# @RELATION DEPENDS_ON -> [_audit]
|
||||
# @SIDE_EFFECT: Persists chat/audit state, mutates in-memory conversation and confirmation stores, and may create confirmation records.
|
||||
# @PRE: Authenticated user is available and message text is non-empty.
|
||||
# @POST: Response state is one of clarification/confirmation/started/success/denied/failed.
|
||||
# @INVARIANT: non-safe operations are gated with confirmation before execution from this endpoint.
|
||||
# @SIDE_EFFECT Persists chat/audit state, mutates in-memory conversation and confirmation stores, and may create confirmation records.
|
||||
# @PRE Authenticated user is available and message text is non-empty.
|
||||
# @POST Response state is one of clarification/confirmation/started/success/denied/failed.
|
||||
# @INVARIANT non-safe operations are gated with confirmation before execution from this endpoint.
|
||||
async def send_message(request: AssistantMessageRequest, current_user: User=Depends(get_current_user), task_manager: TaskManager=Depends(get_task_manager), config_manager: ConfigManager=Depends(get_config_manager), db: Session=Depends(get_db)):
|
||||
with belief_scope('send_message'):
|
||||
logger.reason('Belief protocol reasoning checkpoint for send_message')
|
||||
@@ -159,8 +159,8 @@ async def send_message(request: AssistantMessageRequest, current_user: User=Depe
|
||||
)
|
||||
# #region confirm_operation [C:2] [TYPE Function]
|
||||
# @BRIEF Execute previously requested risky operation after explicit user confirmation.
|
||||
# @PRE: confirmation_id exists, belongs to current user, is pending, and not expired.
|
||||
# @POST: Confirmation state becomes consumed and operation result is persisted in history.
|
||||
# @PRE confirmation_id exists, belongs to current user, is pending, and not expired.
|
||||
# @POST Confirmation state becomes consumed and operation result is persisted in history.
|
||||
async def confirm_operation(
|
||||
confirmation_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
@@ -246,8 +246,8 @@ async def confirm_operation(
|
||||
)
|
||||
# #region cancel_operation [C:2] [TYPE Function]
|
||||
# @BRIEF Cancel pending risky operation and mark confirmation token as cancelled.
|
||||
# @PRE: confirmation_id exists, belongs to current user, and is still pending.
|
||||
# @POST: Confirmation becomes cancelled and cannot be executed anymore.
|
||||
# @PRE confirmation_id exists, belongs to current user, and is still pending.
|
||||
# @POST Confirmation becomes cancelled and cannot be executed anymore.
|
||||
async def cancel_operation(
|
||||
confirmation_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# #region AssistantSchemas [C:2] [TYPE Module] [SEMANTICS assistant, pydantic, schema, store, permission]
|
||||
# @BRIEF Pydantic models, in-memory stores, and permission mappings for the assistant API.
|
||||
# @LAYER API
|
||||
# @RELATION USED_BY -> [AssistantHistory]
|
||||
# @RELATION USED_BY -> [AssistantHistory]
|
||||
# @RELATION CALLED_BY -> [AssistantHistory]
|
||||
# @RELATION CALLED_BY -> [AssistantHistory]
|
||||
# @INVARIANT In-memory stores are module-level singletons shared across the assistant package.
|
||||
# @RATIONALE In-memory stores documented with NOTE about restart loss. ASSISTANT_ARCHIVE_AFTER_DAYS and ASSISTANT_MESSAGE_TTL_DAYS kept as module-level constants with TODO for config migration — Pydantic schemas module should not depend on ConfigManager for architectural purity.
|
||||
|
||||
@@ -17,7 +17,7 @@ from pydantic import BaseModel, Field
|
||||
# #region AssistantMessageRequest [C:1] [TYPE Class]
|
||||
# @BRIEF Input payload for assistant message endpoint.
|
||||
# @DATA_CONTRACT Input[conversation_id?:str, message:str(1..4000)] -> Output[AssistantMessageRequest]
|
||||
# @RELATION USED_BY -> [send_message]
|
||||
# @RELATION CALLED_BY -> [send_message]
|
||||
# @SIDE_EFFECT None (schema declaration only).
|
||||
# @PRE message length is within accepted bounds.
|
||||
# @POST Request object provides message text and optional conversation binding.
|
||||
@@ -34,7 +34,7 @@ class AssistantMessageRequest(BaseModel):
|
||||
# #region AssistantAction [C:1] [TYPE Class]
|
||||
# @BRIEF UI action descriptor returned with assistant responses.
|
||||
# @DATA_CONTRACT Input[type:str, label:str, target?:str] -> Output[AssistantAction]
|
||||
# @RELATION USED_BY -> [AssistantMessageResponse]
|
||||
# @RELATION CALLED_BY -> [AssistantMessageResponse]
|
||||
# @SIDE_EFFECT None (schema declaration only).
|
||||
# @PRE type and label are provided by orchestration logic.
|
||||
# @POST Action can be rendered as button on frontend.
|
||||
@@ -51,9 +51,9 @@ class AssistantAction(BaseModel):
|
||||
# #region AssistantMessageResponse [C:1] [TYPE Class]
|
||||
# @BRIEF Output payload contract for assistant interaction endpoints.
|
||||
# @DATA_CONTRACT Input[conversation_id,response_id,state,text,intent?,confirmation_id?,task_id?,actions[],created_at] -> Output[AssistantMessageResponse]
|
||||
# @RELATION RETURNED_BY -> [send_message]
|
||||
# @RELATION RETURNED_BY -> [confirm_operation]
|
||||
# @RELATION RETURNED_BY -> [cancel_operation]
|
||||
# @RELATION CALLED_BY -> [send_message]
|
||||
# @RELATION CALLED_BY -> [confirm_operation]
|
||||
# @RELATION CALLED_BY -> [cancel_operation]
|
||||
# @SIDE_EFFECT None (schema declaration only).
|
||||
# @PRE Response includes deterministic state and text.
|
||||
# @POST Payload may include task_id/confirmation_id/actions for UI follow-up.
|
||||
@@ -76,9 +76,9 @@ class AssistantMessageResponse(BaseModel):
|
||||
# #region ConfirmationRecord [C:1] [TYPE Class]
|
||||
# @BRIEF In-memory confirmation token model for risky operation dispatch.
|
||||
# @DATA_CONTRACT Input[id,user_id,conversation_id,intent,dispatch,expires_at,state?,created_at] -> Output[ConfirmationRecord]
|
||||
# @RELATION USED_BY -> [send_message]
|
||||
# @RELATION USED_BY -> [confirm_operation]
|
||||
# @RELATION USED_BY -> [cancel_operation]
|
||||
# @RELATION CALLED_BY -> [send_message]
|
||||
# @RELATION CALLED_BY -> [confirm_operation]
|
||||
# @RELATION CALLED_BY -> [cancel_operation]
|
||||
# @SIDE_EFFECT None (schema declaration only).
|
||||
# @PRE intent/dispatch/user_id are populated at confirmation request time.
|
||||
# @POST Record tracks lifecycle state and expiry timestamp.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# #region CleanReleaseApi [C:4] [TYPE Module] [SEMANTICS fastapi, clean-release, api, compliance, candidate, release]
|
||||
# @BRIEF Expose clean release endpoints for candidate preparation and subsequent compliance flow.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [get_clean_release_repository]
|
||||
# @RELATION DEPENDS_ON -> [PreparationService]
|
||||
# @PRE: Clean release repository and preparation service dependencies are configured for the current request scope.
|
||||
# @POST: Candidate preparation, manifest, and compliance routes expose deterministic API payloads without reporting prepared state on failed preparation.
|
||||
# @SIDE_EFFECT: Persists candidate/compliance lifecycle state and triggers clean-release orchestration services.
|
||||
# @INVARIANT: API never reports prepared status if preparation errors are present.
|
||||
# @PRE Clean release repository and preparation service dependencies are configured for the current request scope.
|
||||
# @POST Candidate preparation, manifest, and compliance routes expose deterministic API payloads without reporting prepared state on failed preparation.
|
||||
# @SIDE_EFFECT Persists candidate/compliance lifecycle state and triggers clean-release orchestration services.
|
||||
# @INVARIANT API never reports prepared status if preparation errors are present.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -112,8 +112,8 @@ class CreateComplianceRunRequest(BaseModel):
|
||||
|
||||
# #region register_candidate_v2_endpoint [TYPE Function]
|
||||
# @BRIEF Register a clean-release candidate for headless lifecycle.
|
||||
# @PRE: Candidate identifier is unique.
|
||||
# @POST: Candidate is persisted in DRAFT status.
|
||||
# @PRE Candidate identifier is unique.
|
||||
# @POST Candidate is persisted in DRAFT status.
|
||||
@router.post(
|
||||
"/candidates", response_model=CandidateDTO, status_code=status.HTTP_201_CREATED
|
||||
)
|
||||
@@ -153,8 +153,8 @@ async def register_candidate_v2_endpoint(
|
||||
|
||||
# #region import_candidate_artifacts_v2_endpoint [TYPE Function]
|
||||
# @BRIEF Import candidate artifacts in headless flow.
|
||||
# @PRE: Candidate exists and artifacts array is non-empty.
|
||||
# @POST: Artifacts are persisted and candidate advances to PREPARED if it was DRAFT.
|
||||
# @PRE Candidate exists and artifacts array is non-empty.
|
||||
# @POST Artifacts are persisted and candidate advances to PREPARED if it was DRAFT.
|
||||
@router.post("/candidates/{candidate_id}/artifacts")
|
||||
async def import_candidate_artifacts_v2_endpoint(
|
||||
candidate_id: str,
|
||||
@@ -211,8 +211,8 @@ async def import_candidate_artifacts_v2_endpoint(
|
||||
|
||||
# #region build_candidate_manifest_v2_endpoint [TYPE Function]
|
||||
# @BRIEF Build immutable manifest snapshot for prepared candidate.
|
||||
# @PRE: Candidate exists and has imported artifacts.
|
||||
# @POST: Returns created ManifestDTO with incremented version.
|
||||
# @PRE Candidate exists and has imported artifacts.
|
||||
# @POST Returns created ManifestDTO with incremented version.
|
||||
@router.post(
|
||||
"/candidates/{candidate_id}/manifests",
|
||||
response_model=ManifestDTO,
|
||||
@@ -255,8 +255,8 @@ async def build_candidate_manifest_v2_endpoint(
|
||||
|
||||
# #region get_candidate_overview_v2_endpoint [TYPE Function]
|
||||
# @BRIEF Return expanded candidate overview DTO for headless lifecycle visibility.
|
||||
# @PRE: Candidate exists.
|
||||
# @POST: Returns CandidateOverviewDTO built from the same repository state used by headless US1 endpoints.
|
||||
# @PRE Candidate exists.
|
||||
# @POST Returns CandidateOverviewDTO built from the same repository state used by headless US1 endpoints.
|
||||
@router.get("/candidates/{candidate_id}/overview", response_model=CandidateOverviewDTO)
|
||||
async def get_candidate_overview_v2_endpoint(
|
||||
candidate_id: str,
|
||||
@@ -371,8 +371,8 @@ async def get_candidate_overview_v2_endpoint(
|
||||
|
||||
# #region prepare_candidate_endpoint [TYPE Function]
|
||||
# @BRIEF Prepare candidate with policy evaluation and deterministic manifest generation.
|
||||
# @PRE: Candidate and active policy exist in repository.
|
||||
# @POST: Returns preparation result including manifest reference and violations.
|
||||
# @PRE Candidate and active policy exist in repository.
|
||||
# @POST Returns preparation result including manifest reference and violations.
|
||||
@router.post("/candidates/prepare")
|
||||
async def prepare_candidate_endpoint(
|
||||
payload: PrepareCandidateRequest,
|
||||
@@ -405,8 +405,8 @@ async def prepare_candidate_endpoint(
|
||||
|
||||
# #region start_check [TYPE Function]
|
||||
# @BRIEF Start and finalize a clean compliance check run and persist report artifacts.
|
||||
# @PRE: Active policy and candidate exist.
|
||||
# @POST: Returns accepted payload with check_run_id and started_at.
|
||||
# @PRE Active policy and candidate exist.
|
||||
# @POST Returns accepted payload with check_run_id and started_at.
|
||||
@router.post("/checks", status_code=status.HTTP_202_ACCEPTED)
|
||||
async def start_check(
|
||||
payload: StartCheckRequest,
|
||||
@@ -541,8 +541,8 @@ async def start_check(
|
||||
|
||||
# #region get_check_status [TYPE Function]
|
||||
# @BRIEF Return terminal/intermediate status payload for a check run.
|
||||
# @PRE: check_run_id references an existing run.
|
||||
# @POST: Deterministic payload shape includes checks and violations arrays.
|
||||
# @PRE check_run_id references an existing run.
|
||||
# @POST Deterministic payload shape includes checks and violations arrays.
|
||||
@router.get("/checks/{check_run_id}")
|
||||
async def get_check_status(
|
||||
check_run_id: str,
|
||||
@@ -593,8 +593,8 @@ async def get_check_status(
|
||||
|
||||
# #region get_report [TYPE Function]
|
||||
# @BRIEF Return persisted compliance report by report_id.
|
||||
# @PRE: report_id references an existing report.
|
||||
# @POST: Returns serialized report object.
|
||||
# @PRE report_id references an existing report.
|
||||
# @POST Returns serialized report object.
|
||||
@router.get("/reports/{report_id}")
|
||||
async def get_report(
|
||||
report_id: str,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# #region CleanReleaseV2Api [C:4] [TYPE Module] [SEMANTICS fastapi, clean-release, candidate, lifecycle, api]
|
||||
# @BRIEF Redesigned clean release API for headless candidate lifecycle.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [CleanReleaseRepository]
|
||||
# @RELATION CALLS -> [approve_candidate]
|
||||
# @RELATION CALLS -> [publish_candidate]
|
||||
# @PRE: Clean release repository dependency is available for candidate lifecycle endpoints.
|
||||
# @POST: Candidate registration, approval, publication, and revocation routes are registered without behavior changes.
|
||||
# @SIDE_EFFECT: Persists candidate lifecycle state through clean release services and repository adapters.
|
||||
# @PRE Clean release repository dependency is available for candidate lifecycle endpoints.
|
||||
# @POST Candidate registration, approval, publication, and revocation routes are registered without behavior changes.
|
||||
# @SIDE_EFFECT Persists candidate lifecycle state through clean release services and repository adapters.
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
@@ -63,8 +63,8 @@ class RevokeRequest(dict):
|
||||
|
||||
# #region register_candidate [C:3] [TYPE Function]
|
||||
# @BRIEF Register a new release candidate.
|
||||
# @PRE: Payload contains required fields (id, version, source_snapshot_ref, created_by).
|
||||
# @POST: Candidate is saved in repository.
|
||||
# @PRE Payload contains required fields (id, version, source_snapshot_ref, created_by).
|
||||
# @POST Candidate is saved in repository.
|
||||
# @RELATION DEPENDS_ON -> [CleanReleaseRepository]
|
||||
# @RELATION DEPENDS_ON -> [clean_release_dto]
|
||||
@router.post(
|
||||
@@ -98,8 +98,8 @@ async def register_candidate(
|
||||
|
||||
# #region import_artifacts [C:3] [TYPE Function]
|
||||
# @BRIEF Associate artifacts with a release candidate.
|
||||
# @PRE: Candidate exists.
|
||||
# @POST: Artifacts are processed (placeholder).
|
||||
# @PRE Candidate exists.
|
||||
# @POST Artifacts are processed (placeholder).
|
||||
# @RELATION DEPENDS_ON -> [CleanReleaseRepository]
|
||||
@router.post("/candidates/{candidate_id}/artifacts")
|
||||
async def import_artifacts(
|
||||
@@ -131,8 +131,8 @@ async def import_artifacts(
|
||||
|
||||
# #region build_manifest [C:3] [TYPE Function]
|
||||
# @BRIEF Generate distribution manifest for a candidate.
|
||||
# @PRE: Candidate exists.
|
||||
# @POST: Manifest is created and saved.
|
||||
# @PRE Candidate exists.
|
||||
# @POST Manifest is created and saved.
|
||||
# @RELATION DEPENDS_ON -> [CleanReleaseRepository]
|
||||
@router.post(
|
||||
"/candidates/{candidate_id}/manifests",
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
# #region DashboardsApi [C:5] [TYPE Module] [SEMANTICS dashboard, api, package, search, git, task]
|
||||
#
|
||||
# @BRIEF API endpoints for the Dashboard Hub - listing dashboards with Git and task status
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [AppDependencies]
|
||||
# @RELATION DEPENDS_ON -> [ResourceService]
|
||||
# @RELATION DEPENDS_ON -> [SupersetClient]
|
||||
#
|
||||
# @INVARIANT: All dashboard responses include git_status and last_task metadata
|
||||
# @INVARIANT All dashboard responses include git_status and last_task metadata
|
||||
#
|
||||
# @PRE: Valid environment configurations exist in ConfigManager.
|
||||
# @POST: Dashboard responses are projected into DashboardsResponse DTO.
|
||||
# @SIDE_EFFECT: Performs external calls to Superset API and potentially Git providers.
|
||||
# @DATA_CONTRACT: Input(env_id, filters) -> Output(DashboardsResponse)
|
||||
# @PRE Valid environment configurations exist in ConfigManager.
|
||||
# @POST Dashboard responses are projected into DashboardsResponse DTO.
|
||||
# @SIDE_EFFECT Performs external calls to Superset API and potentially Git providers.
|
||||
# @DATA_CONTRACT Input(env_id, filters) -> Output(DashboardsResponse)
|
||||
#
|
||||
# @TEST_CONTRACT: DashboardsAPI -> {
|
||||
# @TEST_CONTRACT DashboardsAPI -> {
|
||||
# required_fields: {env_id: string, page: integer, page_size: integer},
|
||||
# optional_fields: {search: string},
|
||||
# invariants: ["Pagination must be valid", "Environment must exist"]
|
||||
# }
|
||||
#
|
||||
# @TEST_FIXTURE: dashboard_list_happy -> {
|
||||
# @TEST_FIXTURE dashboard_list_happy -> {
|
||||
# "env_id": "prod",
|
||||
# "expected_count": 1,
|
||||
# "dashboards": [{"id": 1, "title": "Main Revenue"}]
|
||||
# }
|
||||
#
|
||||
# @TEST_EDGE: pagination_zero_page -> {"env_id": "prod", "page": 0, "status": 400}
|
||||
# @TEST_EDGE: pagination_oversize -> {"env_id": "prod", "page_size": 101, "status": 400}
|
||||
# @TEST_EDGE: missing_env -> {"env_id": "ghost", "status": 404}
|
||||
# @TEST_EDGE: empty_dashboards -> {"env_id": "empty_env", "expected_total": 0}
|
||||
# @TEST_EDGE: external_superset_failure -> {"env_id": "bad_conn", "status": 503}
|
||||
# @TEST_EDGE pagination_zero_page -> {"env_id": "prod", "page": 0, "status": 400}
|
||||
# @TEST_EDGE pagination_oversize -> {"env_id": "prod", "page_size": 101, "status": 400}
|
||||
# @TEST_EDGE missing_env -> {"env_id": "ghost", "status": 404}
|
||||
# @TEST_EDGE empty_dashboards -> {"env_id": "empty_env", "expected_total": 0}
|
||||
# @TEST_EDGE external_superset_failure -> {"env_id": "bad_conn", "status": 503}
|
||||
#
|
||||
# @TEST_INVARIANT: metadata_consistency -> verifies: [dashboard_list_happy, empty_dashboards]
|
||||
# @TEST_INVARIANT metadata_consistency -> verifies: [dashboard_list_happy, empty_dashboards]
|
||||
|
||||
from ._action_routes import *
|
||||
from ._detail_routes import *
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# #region DashboardActionRoutes [C:2] [TYPE Module] [SEMANTICS fastapi, dashboard, api, backup]
|
||||
# @BRIEF Dashboard action route handlers — migrate, backup.
|
||||
# @LAYER: API
|
||||
# @RELATION DEPENDS_ON -> [DashboardRouter]
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [[EXT:frontend:DashboardRouter]]
|
||||
# @RELATION DEPENDS_ON -> [DashboardSchemas]
|
||||
|
||||
from fastapi import Depends, HTTPException
|
||||
@@ -23,12 +23,12 @@ from ._schemas import (
|
||||
|
||||
# #region migrate_dashboards [C:2] [TYPE Function]
|
||||
# @BRIEF Trigger bulk migration of dashboards from source to target environment
|
||||
# @PRE: User has permission plugin:migration:execute
|
||||
# @PRE: source_env_id and target_env_id are valid environment IDs
|
||||
# @PRE: dashboard_ids is a non-empty list
|
||||
# @POST: Returns task_id for tracking migration progress
|
||||
# @POST: Task is created and queued for execution
|
||||
# @RELATION DISPATCHES -> [MigrationPlugin:execute]
|
||||
# @PRE User has permission plugin:migration:execute
|
||||
# @PRE source_env_id and target_env_id are valid environment IDs
|
||||
# @PRE dashboard_ids is a non-empty list
|
||||
# @POST Returns task_id for tracking migration progress
|
||||
# @POST Task is created and queued for execution
|
||||
# @RELATION DISPATCHES -> [EXT:method:MigrationPlugin:execute]
|
||||
# @RELATION CALLS -> [TaskManager]
|
||||
@router.post("/migrate", response_model=TaskResponse)
|
||||
async def migrate_dashboards(
|
||||
@@ -101,13 +101,13 @@ async def migrate_dashboards(
|
||||
|
||||
# #region backup_dashboards [C:2] [TYPE Function]
|
||||
# @BRIEF Trigger bulk backup of dashboards with optional cron schedule
|
||||
# @PRE: User has permission plugin:backup:execute
|
||||
# @PRE: env_id is a valid environment ID
|
||||
# @PRE: dashboard_ids is a non-empty list
|
||||
# @POST: Returns task_id for tracking backup progress
|
||||
# @POST: Task is created and queued for execution
|
||||
# @POST: If schedule is provided, a scheduled task is created
|
||||
# @RELATION DISPATCHES -> [BackupPlugin:execute]
|
||||
# @PRE User has permission plugin:backup:execute
|
||||
# @PRE env_id is a valid environment ID
|
||||
# @PRE dashboard_ids is a non-empty list
|
||||
# @POST Returns task_id for tracking backup progress
|
||||
# @POST Task is created and queued for execution
|
||||
# @POST If schedule is provided, a scheduled task is created
|
||||
# @RELATION DISPATCHES -> [EXT:method:BackupPlugin:execute]
|
||||
# @RELATION CALLS -> [TaskManager]
|
||||
@router.post("/backup", response_model=TaskResponse)
|
||||
async def backup_dashboards(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# #region DashboardDetailRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, dashboard, api, transform, search, task]
|
||||
# @BRIEF Dashboard detail, db-mappings, task history, thumbnail route handlers.
|
||||
# @LAYER: API
|
||||
# @RELATION DEPENDS_ON -> [DashboardRouter]
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [[EXT:frontend:DashboardRouter]]
|
||||
# @RELATION DEPENDS_ON -> [DashboardSchemas]
|
||||
# @RELATION DEPENDS_ON -> [DashboardHelpers]
|
||||
# @RELATION DEPENDS_ON -> [DashboardProjection]
|
||||
@@ -43,10 +43,10 @@ from ._schemas import (
|
||||
|
||||
# #region get_database_mappings [C:2] [TYPE Function]
|
||||
# @BRIEF Get database mapping suggestions between source and target environments
|
||||
# @PRE: User has permission plugin:migration:read
|
||||
# @PRE: source_env_id and target_env_id are valid environment IDs
|
||||
# @POST: Returns list of suggested database mappings with confidence scores
|
||||
# @RELATION CALLS -> [MappingService:get_suggestions]
|
||||
# @PRE User has permission plugin:migration:read
|
||||
# @PRE source_env_id and target_env_id are valid environment IDs
|
||||
# @POST Returns list of suggested database mappings with confidence scores
|
||||
# @RELATION CALLS -> [EXT:method:MappingService:get_suggestions]
|
||||
@router.get("/db-mappings", response_model=DatabaseMappingsResponse)
|
||||
async def get_database_mappings(
|
||||
source_env_id: str,
|
||||
@@ -109,8 +109,8 @@ async def get_database_mappings(
|
||||
|
||||
# #region get_dashboard_detail [C:2] [TYPE Function]
|
||||
# @BRIEF Fetch detailed dashboard info with related charts and datasets
|
||||
# @PRE: env_id must be valid and dashboard ref (slug or id) must exist
|
||||
# @POST: Returns dashboard detail payload for overview page
|
||||
# @PRE env_id must be valid and dashboard ref (slug or id) must exist
|
||||
# @POST Returns dashboard detail payload for overview page
|
||||
# @RELATION CALLS -> [AsyncSupersetClient]
|
||||
@router.get("/{dashboard_ref}", response_model=DashboardDetailResponse)
|
||||
async def get_dashboard_detail(
|
||||
@@ -154,8 +154,8 @@ async def get_dashboard_detail(
|
||||
|
||||
# #region get_dashboard_tasks_history [C:2] [TYPE Function]
|
||||
# @BRIEF Returns history of backup and LLM validation tasks for a dashboard.
|
||||
# @PRE: dashboard ref (slug or id) is valid.
|
||||
# @POST: Response contains sorted task history (newest first).
|
||||
# @PRE dashboard ref (slug or id) is valid.
|
||||
# @POST Response contains sorted task history (newest first).
|
||||
@router.get("/{dashboard_ref}/tasks", response_model=DashboardTaskHistoryResponse)
|
||||
async def get_dashboard_tasks_history(
|
||||
dashboard_ref: str,
|
||||
@@ -257,8 +257,8 @@ async def get_dashboard_tasks_history(
|
||||
# #region get_dashboard_thumbnail [C:3] [TYPE Function]
|
||||
# @BRIEF Proxies Superset dashboard thumbnail with cache support.
|
||||
# @RELATION CALLS -> [AsyncSupersetClient]
|
||||
# @PRE: env_id must exist.
|
||||
# @POST: Returns image bytes or 202 when thumbnail is being prepared by Superset.
|
||||
# @PRE env_id must exist.
|
||||
# @POST Returns image bytes or 202 when thumbnail is being prepared by Superset.
|
||||
@router.get("/{dashboard_ref}/thumbnail")
|
||||
async def get_dashboard_thumbnail(
|
||||
dashboard_ref: str,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# #region DashboardHelpers [C:2] [TYPE Module] [SEMANTICS fastapi, dashboard, api, search, filter]
|
||||
# @BRIEF Basic helper functions for dashboard route handlers — slug resolution, filter normalization.
|
||||
# @LAYER: Infra
|
||||
# @LAYER Infrastructure
|
||||
# @RELATION DEPENDS_ON -> [SupersetClient]
|
||||
# @RELATION DEPENDS_ON -> [AsyncSupersetClient]
|
||||
|
||||
@@ -14,8 +14,8 @@ from src.core.superset_client import SupersetClient
|
||||
|
||||
# #region _find_dashboard_id_by_slug [C:2] [TYPE Function]
|
||||
# @BRIEF Resolve dashboard numeric ID by slug using Superset list endpoint.
|
||||
# @PRE: `dashboard_slug` is non-empty.
|
||||
# @POST: Returns dashboard ID when found, otherwise None.
|
||||
# @PRE `dashboard_slug` is non-empty.
|
||||
# @POST Returns dashboard ID when found, otherwise None.
|
||||
def _find_dashboard_id_by_slug(
|
||||
client: SupersetClient,
|
||||
dashboard_slug: str,
|
||||
@@ -51,8 +51,8 @@ def _find_dashboard_id_by_slug(
|
||||
|
||||
# #region _resolve_dashboard_id_from_ref [C:2] [TYPE Function]
|
||||
# @BRIEF Resolve dashboard ID from slug-first reference with numeric fallback.
|
||||
# @PRE: `dashboard_ref` is provided in route path.
|
||||
# @POST: Returns a valid dashboard ID or raises HTTPException(404).
|
||||
# @PRE `dashboard_ref` is provided in route path.
|
||||
# @POST Returns a valid dashboard ID or raises HTTPException(404).
|
||||
def _resolve_dashboard_id_from_ref(
|
||||
dashboard_ref: str,
|
||||
client: SupersetClient,
|
||||
@@ -77,8 +77,8 @@ def _resolve_dashboard_id_from_ref(
|
||||
|
||||
# #region _find_dashboard_id_by_slug_async [C:2] [TYPE Function]
|
||||
# @BRIEF Resolve dashboard numeric ID by slug using async Superset list endpoint.
|
||||
# @PRE: dashboard_slug is non-empty.
|
||||
# @POST: Returns dashboard ID when found, otherwise None.
|
||||
# @PRE dashboard_slug is non-empty.
|
||||
# @POST Returns dashboard ID when found, otherwise None.
|
||||
async def _find_dashboard_id_by_slug_async(
|
||||
client: AsyncSupersetClient,
|
||||
dashboard_slug: str,
|
||||
@@ -114,8 +114,8 @@ async def _find_dashboard_id_by_slug_async(
|
||||
|
||||
# #region _resolve_dashboard_id_from_ref_async [C:2] [TYPE Function]
|
||||
# @BRIEF Resolve dashboard ID from slug-first reference using async Superset client.
|
||||
# @PRE: dashboard_ref is provided in route path.
|
||||
# @POST: Returns valid dashboard ID or raises HTTPException(404).
|
||||
# @PRE dashboard_ref is provided in route path.
|
||||
# @POST Returns valid dashboard ID or raises HTTPException(404).
|
||||
async def _resolve_dashboard_id_from_ref_async(
|
||||
dashboard_ref: str,
|
||||
client: AsyncSupersetClient,
|
||||
@@ -139,8 +139,8 @@ async def _resolve_dashboard_id_from_ref_async(
|
||||
|
||||
# #region _normalize_filter_values [C:2] [TYPE Function]
|
||||
# @BRIEF Normalize query filter values to lower-cased non-empty tokens.
|
||||
# @PRE: values may be None or list of strings.
|
||||
# @POST: Returns trimmed normalized list preserving input order.
|
||||
# @PRE values may be None or list of strings.
|
||||
# @POST Returns trimmed normalized list preserving input order.
|
||||
def _normalize_filter_values(values: list[str] | None) -> list[str]:
|
||||
if not values:
|
||||
return []
|
||||
@@ -157,8 +157,8 @@ def _normalize_filter_values(values: list[str] | None) -> list[str]:
|
||||
|
||||
# #region _dashboard_git_filter_value [C:2] [TYPE Function]
|
||||
# @BRIEF Build comparable git status token for dashboards filtering.
|
||||
# @PRE: dashboard payload may contain git_status or None.
|
||||
# @POST: Returns one of ok|diff|no_repo|error|pending.
|
||||
# @PRE dashboard payload may contain git_status or None.
|
||||
# @POST Returns one of ok|diff|no_repo|error|pending.
|
||||
def _dashboard_git_filter_value(dashboard: dict[str, Any]) -> str:
|
||||
git_status = dashboard.get("git_status") or {}
|
||||
sync_status = str(git_status.get("sync_status") or "").strip().upper()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# #region DashboardListingRoutes [C:4] [TYPE Module] [SEMANTICS fastapi, dashboard, api, search]
|
||||
# @BRIEF Dashboard listing route handler for Dashboard Hub.
|
||||
# @LAYER: API
|
||||
# @RELATION DEPENDS_ON -> [DashboardRouter]
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [[EXT:frontend:DashboardRouter]]
|
||||
# @RELATION DEPENDS_ON -> [DashboardSchemas]
|
||||
# @RELATION DEPENDS_ON -> [DashboardHelpers]
|
||||
# @RELATION DEPENDS_ON -> [DashboardProjection]
|
||||
@@ -36,13 +36,13 @@ from ._schemas import DashboardsResponse, EffectiveProfileFilter
|
||||
|
||||
# #region get_dashboards [C:3] [TYPE Function]
|
||||
# @BRIEF Fetch list of dashboards from a specific environment with Git status and last task status
|
||||
# @PRE: env_id must be a valid environment ID
|
||||
# @PRE: page must be >= 1 if provided
|
||||
# @PRE: page_size must be between 1 and 100 if provided
|
||||
# @POST: Returns a list of dashboards with enhanced metadata and pagination info
|
||||
# @POST: Response includes pagination metadata (page, page_size, total, total_pages)
|
||||
# @POST: Response includes effective profile filter metadata for main dashboards page context
|
||||
# @RELATION CALLS -> [get_dashboards_with_status]
|
||||
# @PRE env_id must be a valid environment ID
|
||||
# @PRE page must be >= 1 if provided
|
||||
# @PRE page_size must be between 1 and 100 if provided
|
||||
# @POST Returns a list of dashboards with enhanced metadata and pagination info
|
||||
# @POST Response includes pagination metadata (page, page_size, total, total_pages)
|
||||
# @POST Response includes effective profile filter metadata for main dashboards page context
|
||||
# @RELATION CALLS -> [EXT:method:get_dashboards_with_status]
|
||||
@router.get("", response_model=DashboardsResponse)
|
||||
async def get_dashboards(
|
||||
env_id: str,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# #region DashboardProjection [C:2] [TYPE Module] [SEMANTICS dashboard, api, transform, profile, filter]
|
||||
# @BRIEF Dashboard response projection and profile-filter helpers for Dashboard Hub routes.
|
||||
# @LAYER: Infra
|
||||
# @LAYER Infrastructure
|
||||
# @RELATION DEPENDS_ON -> [SupersetClient]
|
||||
# @RELATION DEPENDS_ON -> [ProfileService]
|
||||
|
||||
@@ -142,7 +142,7 @@ def _get_profile_filter_binding(
|
||||
|
||||
# #region _resolve_profile_actor_aliases [C:2] [TYPE Function]
|
||||
# @BRIEF Resolve stable actor aliases for profile filtering without per-dashboard detail fan-out.
|
||||
# @SIDE_EFFECT: Performs at most one Superset users-lookup request.
|
||||
# @SIDE_EFFECT Performs at most one Superset users-lookup request.
|
||||
def _resolve_profile_actor_aliases(env: Any, bound_username: str) -> list[str]:
|
||||
normalized_bound = _normalize_actor_alias_token(bound_username)
|
||||
if not normalized_bound:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# #region DashboardSchemas [C:1] [TYPE Module] [SEMANTICS pydantic, dashboard, api, dto, git-status]
|
||||
# @BRIEF DTO classes for the Dashboard Hub API.
|
||||
# @LAYER: Infra
|
||||
# @RELATION DEPENDS_ON -> [Pydantic]
|
||||
# @LAYER Infrastructure
|
||||
# @RELATION DEPENDS_ON -> [EXT:Library:pydantic]
|
||||
|
||||
from typing import Literal
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# #region DatasetReviewApi [C:3] [TYPE Module] [SEMANTICS dataset, review, api, facade]
|
||||
# @BRIEF Thin facade re-exporting router and public symbols from decomposed dataset review API sub-modules.
|
||||
# @LAYER: API
|
||||
# @RATIONALE: Original 2484-line monolith violated INV_7 (400-line module limit) by 6x. Decomposed into _dependencies (DTOs/guards/serializers) and _routes (handlers).
|
||||
# @REJECTED: Keeping all routes in one file because it exceeded the fractal limit by 6x and accumulated severe structural erosion risk.
|
||||
# @LAYER API
|
||||
# @RATIONALE Original 2484-line monolith violated INV_7 (400-line module limit) by 6x. Decomposed into _dependencies (DTOs/guards/serializers) and _routes (handlers).
|
||||
# @REJECTED Keeping all routes in one file because it exceeded the fractal limit by 6x and accumulated severe structural erosion risk.
|
||||
|
||||
from src.api.routes.dataset_review_pkg._dependencies import ( # noqa: F401
|
||||
ApproveMappingRequest,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# #region DatasetReviewDependencies [C:2] [TYPE Module] [SEMANTICS dataset, review, dependency, di, serialize]
|
||||
# @BRIEF Dependency injection, serialization helpers, and feature-flag guards for dataset review endpoints.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -628,7 +628,7 @@ def _resolve_candidate_source_version(field, source_id):
|
||||
|
||||
# #region _update_semantic_field_state [C:3] [TYPE Function]
|
||||
# @BRIEF Apply field-level semantic manual override or candidate acceptance.
|
||||
# @POST: Manual overrides always set manual provenance plus lock.
|
||||
# @POST Manual overrides always set manual provenance plus lock.
|
||||
def _update_semantic_field_state(field, request, changed_by):
|
||||
has_manual_override = any(v is not None for v in [request.verbose_name, request.description, request.display_format])
|
||||
selected_candidate = None
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# #region DatasetsApi [C:5] [TYPE Module] [SEMANTICS fastapi, dataset, api, search, mapping, mapped-fields]
|
||||
#
|
||||
# @BRIEF API endpoints for the Dataset Hub - listing datasets with mapping progress
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [AppDependencies]
|
||||
# @RELATION DEPENDS_ON -> [ResourceService]
|
||||
# @RELATION DEPENDS_ON -> [SupersetClient]
|
||||
@@ -10,7 +10,7 @@
|
||||
# @POST Returns dataset metadata with mapping status.
|
||||
# @SIDE_EFFECT Reads from Superset API and task manager.
|
||||
# @DATA_CONTRACT Input -> DatasetQuery, Output -> DatasetsResponse, DatasetDetailResponse
|
||||
# @INVARIANT: All dataset responses include last_task metadata
|
||||
# @INVARIANT All dataset responses include last_task metadata
|
||||
|
||||
|
||||
import re
|
||||
@@ -149,9 +149,9 @@ class MetricDescriptionUpdate(BaseModel):
|
||||
|
||||
# #region get_dataset_ids [C:4] [TYPE Function]
|
||||
# @BRIEF Fetch list of all dataset IDs from a specific environment (without pagination)
|
||||
# @PRE: env_id must be a valid environment ID
|
||||
# @POST: Returns a list of all dataset IDs
|
||||
# @RELATION CALLS -> [get_datasets_with_status]
|
||||
# @PRE env_id must be a valid environment ID
|
||||
# @POST Returns a list of all dataset IDs
|
||||
# @RELATION CALLS -> [EXT:method:get_datasets_with_status]
|
||||
@router.get("/ids")
|
||||
async def get_dataset_ids(
|
||||
env_id: str,
|
||||
@@ -197,13 +197,13 @@ async def get_dataset_ids(
|
||||
|
||||
# #region get_datasets [C:4] [TYPE Function]
|
||||
# @BRIEF Fetch list of datasets from a specific environment with mapping progress and stats.
|
||||
# @PRE: env_id must be a valid environment ID
|
||||
# @PRE: page must be >= 1 if provided
|
||||
# @PRE: page_size must be between 1 and 100 if provided
|
||||
# @POST: Returns a list of datasets with enhanced metadata, pagination info, and StatsCounts.
|
||||
# @POST: Response includes pagination metadata (page, page_size, total, total_pages) and stats object.
|
||||
# @PRE env_id must be a valid environment ID
|
||||
# @PRE page must be >= 1 if provided
|
||||
# @PRE page_size must be between 1 and 100 if provided
|
||||
# @POST Returns a list of datasets with enhanced metadata, pagination info, and StatsCounts.
|
||||
# @POST Response includes pagination metadata (page, page_size, total, total_pages) and stats object.
|
||||
# @RATIONALE Stats counts returned in the same response as datasets to avoid an extra API call for the Stats Bar (FR-022).
|
||||
# @RELATION CALLS -> [get_datasets_with_status]
|
||||
# @RELATION CALLS -> [EXT:method:get_datasets_with_status]
|
||||
@router.get("", response_model=DatasetsResponse)
|
||||
async def get_datasets(
|
||||
env_id: str,
|
||||
@@ -324,11 +324,11 @@ class MapColumnsRequest(BaseModel):
|
||||
|
||||
# #region map_columns [C:4] [TYPE Function]
|
||||
# @BRIEF Trigger bulk column mapping for datasets
|
||||
# @PRE: User has permission plugin:mapper:execute
|
||||
# @PRE: env_id is a valid environment ID
|
||||
# @PRE: dataset_ids is a non-empty list
|
||||
# @POST: Returns task_id for tracking mapping progress
|
||||
# @POST: Task is created and queued for execution
|
||||
# @PRE User has permission plugin:mapper:execute
|
||||
# @PRE env_id is a valid environment ID
|
||||
# @PRE dataset_ids is a non-empty list
|
||||
# @POST Returns task_id for tracking mapping progress
|
||||
# @POST Task is created and queued for execution
|
||||
# @RELATION DISPATCHES -> [MapperPlugin]
|
||||
# @RELATION CALLS -> [create_task]
|
||||
@router.post("/map-columns", response_model=TaskResponse)
|
||||
@@ -397,11 +397,11 @@ class GenerateDocsRequest(BaseModel):
|
||||
|
||||
# #region generate_docs [C:4] [TYPE Function]
|
||||
# @BRIEF Trigger bulk documentation generation for datasets
|
||||
# @PRE: User has permission plugin:llm_analysis:execute
|
||||
# @PRE: env_id is a valid environment ID
|
||||
# @PRE: dataset_ids is a non-empty list
|
||||
# @POST: Returns task_id for tracking documentation generation progress
|
||||
# @POST: Task is created and queued for execution
|
||||
# @PRE User has permission plugin:llm_analysis:execute
|
||||
# @PRE env_id is a valid environment ID
|
||||
# @PRE dataset_ids is a non-empty list
|
||||
# @POST Returns task_id for tracking documentation generation progress
|
||||
# @POST Task is created and queued for execution
|
||||
# @RELATION DISPATCHES -> [DocumentationPlugin]
|
||||
# @RELATION CALLS -> [create_task]
|
||||
@router.post("/generate-docs", response_model=TaskResponse)
|
||||
@@ -450,9 +450,9 @@ async def generate_docs(
|
||||
|
||||
# #region get_dataset_detail [C:4] [TYPE Function]
|
||||
# @BRIEF Get detailed dataset information including columns and linked dashboards
|
||||
# @PRE: env_id is a valid environment ID
|
||||
# @PRE: dataset_id is a valid dataset ID
|
||||
# @POST: Returns detailed dataset info with columns and linked dashboards
|
||||
# @PRE env_id is a valid environment ID
|
||||
# @PRE dataset_id is a valid dataset ID
|
||||
# @POST Returns detailed dataset info with columns and linked dashboards
|
||||
# @RELATION CALLS -> [SupersetClientGetDatasetDetail]
|
||||
@router.get("/{dataset_id}", response_model=DatasetDetailResponse)
|
||||
async def get_dataset_detail(
|
||||
@@ -505,13 +505,13 @@ def _strip_html_tags(text: str) -> str:
|
||||
|
||||
# #region update_column_description [C:4] [TYPE Endpoint]
|
||||
# @BRIEF Save description for a single dataset column. Internal: GET full dataset from Superset, modify one column's description, PUT back.
|
||||
# @PRE: dataset_id and column_id must exist in the target environment.
|
||||
# @POST: Column description in Superset is updated. Response confirms success.
|
||||
# @SIDE_EFFECT: Mutates dataset metadata in upstream Superset instance via PUT.
|
||||
# @PRE dataset_id and column_id must exist in the target environment.
|
||||
# @POST Column description in Superset is updated. Response confirms success.
|
||||
# @SIDE_EFFECT Mutates dataset metadata in upstream Superset instance via PUT.
|
||||
# @VALIDATION: description — string, max 2000 chars, plain text only (HTML stripped), may be empty string to clear description, no trimming applied. 404 if dataset or column not found. 502 if Superset upstream fails.
|
||||
# @ERROR: 400 — description exceeds max length or contains non-plaintext content. 404 — dataset_id or column_id not found. 502 — Superset upstream failure (GET or PUT).
|
||||
# @RELATION CALLS -> [SupersetClient.get_dataset]
|
||||
# @RELATION CALLS -> [SupersetClient.update_dataset]
|
||||
# @ERROR 400 — description exceeds max length or contains non-plaintext content. 404 — dataset_id or column_id not found. 502 — Superset upstream failure (GET or PUT).
|
||||
# @RELATION CALLS -> [EXT:method:SupersetClient.get_dataset]
|
||||
# @RELATION CALLS -> [EXT:method:SupersetClient.update_dataset]
|
||||
# @RATIONALE Must perform GET→modify→PUT because Superset has no PATCH for individual columns — only full object PUT with override_columns=false.
|
||||
# @REJECTED Direct PUT from frontend — rejected because frontend would need to handle full Superset payload structure.
|
||||
@router.put("/{dataset_id}/columns/{column_id}/description")
|
||||
@@ -594,13 +594,13 @@ async def update_column_description(
|
||||
|
||||
# #region update_metric_description [C:4] [TYPE Endpoint]
|
||||
# @BRIEF Save description for a single dataset metric. Mirror of update_column_description for metrics.
|
||||
# @PRE: dataset_id and metric_id must exist in the target environment.
|
||||
# @POST: Metric description in Superset is updated.
|
||||
# @SIDE_EFFECT: Mutates dataset metadata in upstream Superset instance via PUT.
|
||||
# @PRE dataset_id and metric_id must exist in the target environment.
|
||||
# @POST Metric description in Superset is updated.
|
||||
# @SIDE_EFFECT Mutates dataset metadata in upstream Superset instance via PUT.
|
||||
# @VALIDATION: description — string, max 2000 chars, plain text only (HTML stripped), may be empty string to clear description, no trimming applied. 404 if dataset or metric not found. 502 if Superset upstream fails.
|
||||
# @ERROR: 400 — description exceeds max length or contains non-plaintext content. 404 — dataset_id or metric_id not found. 502 — Superset upstream failure (GET or PUT).
|
||||
# @RELATION CALLS -> [SupersetClient.get_dataset]
|
||||
# @RELATION CALLS -> [SupersetClient.update_dataset]
|
||||
# @ERROR 400 — description exceeds max length or contains non-plaintext content. 404 — dataset_id or metric_id not found. 502 — Superset upstream failure (GET or PUT).
|
||||
# @RELATION CALLS -> [EXT:method:SupersetClient.get_dataset]
|
||||
# @RELATION CALLS -> [EXT:method:SupersetClient.update_dataset]
|
||||
@router.put("/{dataset_id}/metrics/{metric_id}/description")
|
||||
async def update_metric_description(
|
||||
dataset_id: int,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# #region EnvironmentsApi [C:5] [TYPE Module] [SEMANTICS fastapi, environment, api]
|
||||
#
|
||||
# @BRIEF API endpoints for listing environments and their databases.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [AppDependencies]
|
||||
# @RELATION DEPENDS_ON -> [SupersetClient]
|
||||
#
|
||||
# @INVARIANT: Environment IDs must exist in the configuration.
|
||||
# @INVARIANT Environment IDs must exist in the configuration.
|
||||
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
@@ -20,8 +20,8 @@ router = APIRouter(prefix="/api/environments", tags=["Environments"])
|
||||
|
||||
# #region _normalize_superset_env_url [TYPE Function]
|
||||
# @BRIEF Canonicalize Superset environment URL to base host/path without trailing /api/v1.
|
||||
# @PRE: raw_url can be empty.
|
||||
# @POST: Returns normalized base URL.
|
||||
# @PRE raw_url can be empty.
|
||||
# @POST Returns normalized base URL.
|
||||
def _normalize_superset_env_url(raw_url: str) -> str:
|
||||
normalized = str(raw_url or "").strip().rstrip("/")
|
||||
if normalized.lower().endswith("/api/v1"):
|
||||
@@ -54,9 +54,9 @@ class DatabaseResponse(BaseModel):
|
||||
|
||||
# #region get_environments [TYPE Function] [SEMANTICS list, environments, config]
|
||||
# @BRIEF List all configured environments.
|
||||
# @LAYER: API
|
||||
# @PRE: config_manager is injected via Depends.
|
||||
# @POST: Returns a list of EnvironmentResponse objects.
|
||||
# @LAYER API
|
||||
# @PRE config_manager is injected via Depends.
|
||||
# @POST Returns a list of EnvironmentResponse objects.
|
||||
@router.get("", response_model=list[EnvironmentResponse])
|
||||
async def get_environments(
|
||||
config_manager=Depends(get_config_manager),
|
||||
@@ -91,9 +91,9 @@ async def get_environments(
|
||||
|
||||
# #region update_environment_schedule [TYPE Function] [SEMANTICS update, schedule, backup, environment]
|
||||
# @BRIEF Update backup schedule for an environment.
|
||||
# @LAYER: API
|
||||
# @PRE: Environment id exists, schedule is valid ScheduleSchema.
|
||||
# @POST: Backup schedule updated and scheduler reloaded.
|
||||
# @LAYER API
|
||||
# @PRE Environment id exists, schedule is valid ScheduleSchema.
|
||||
# @POST Backup schedule updated and scheduler reloaded.
|
||||
@router.put("/{id}/schedule")
|
||||
async def update_environment_schedule(
|
||||
id: str,
|
||||
@@ -122,9 +122,9 @@ async def update_environment_schedule(
|
||||
|
||||
# #region get_environment_databases [TYPE Function] [SEMANTICS fetch, databases, superset, environment]
|
||||
# @BRIEF Fetch the list of databases from a specific environment.
|
||||
# @LAYER: API
|
||||
# @PRE: Environment id exists.
|
||||
# @POST: Returns a list of database summaries from the environment.
|
||||
# @LAYER API
|
||||
# @PRE Environment id exists.
|
||||
# @POST Returns a list of database summaries from the environment.
|
||||
@router.get("/{id}/databases")
|
||||
async def get_environment_databases(
|
||||
id: str,
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
# #region GitPackage [C:5] [TYPE Module] [SEMANTICS git, api, package, sync]
|
||||
# @BRIEF Package root for decomposed git routes. Re-exports all public symbols from submodules.
|
||||
# @LAYER: API
|
||||
# @RELATION USES -> [GitRouter, GitDeps, GitHelpers, GitConfigRoutes, GitGiteaRoutes,
|
||||
# GitRepoRoutes, GitRepoOperationsRoutes, GitRepoLifecycleRoutes,
|
||||
# GitMergeRoutes, GitEnvironmentRoutes]
|
||||
# @INVARIANT: git_service and os are module-level attributes for test monkeypatch compatibility.
|
||||
# @PRE: Git service initialized
|
||||
# @POST: Git API package exported
|
||||
# @SIDE_EFFECT: Registers git route submodules
|
||||
# @DATA_CONTRACT: GitRequest -> GitResponse
|
||||
# @LAYER API
|
||||
# @RELATION CALLS -> [[EXT:list:GitPackage_all_routes]]
|
||||
# @INVARIANT git_service and os are module-level attributes for test monkeypatch compatibility.
|
||||
# @PRE Git service initialized
|
||||
# @POST Git API package exported
|
||||
# @SIDE_EFFECT Registers git route submodules
|
||||
# @DATA_CONTRACT GitRequest -> GitResponse
|
||||
# All route functions are re-exported for direct access via `from src.api.routes import git`.
|
||||
|
||||
import os
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# #region GitConfigRoutes [C:2] [TYPE Module] [SEMANTICS fastapi, git, api, search, connection]
|
||||
# @BRIEF FastAPI endpoints for Git server configuration CRUD and connection testing.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
|
||||
|
||||
from fastapi import Depends, HTTPException
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# #region GitDeps [C:1] [TYPE Module] [SEMANTICS git, dependency, service, provider]
|
||||
# @BRIEF Shared dependency wiring for monkeypatch-safe git_service access and constants.
|
||||
# @LAYER: API
|
||||
# @INVARIANT: get_git_service() resolves from sys.modules at call time so test monkeypatching
|
||||
# @LAYER API
|
||||
# @INVARIANT get_git_service() resolves from sys.modules at call time so test monkeypatching
|
||||
# of git_routes.git_service (the __init__ attribute) takes effect across all submodules.
|
||||
|
||||
import sys
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# #region GitEnvironmentRoutes [C:2] [TYPE Module] [SEMANTICS fastapi, git, environment, api]
|
||||
# @BRIEF FastAPI endpoint for listing deployment environments.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
|
||||
|
||||
from fastapi import Depends
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# #region GitGiteaRoutes [C:2] [TYPE Module] [SEMANTICS fastapi, git, gitea, api]
|
||||
# @BRIEF FastAPI endpoints for Gitea-specific repository operations.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
|
||||
|
||||
from fastapi import Depends, HTTPException
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# #region GitHelpers [C:3] [TYPE Module] [SEMANTICS fastapi, git, api]
|
||||
# @BRIEF Shared helper functions for Git route modules.
|
||||
# @LAYER: API
|
||||
# @RELATION USES -> [GitDeps]
|
||||
# @RELATION USES -> [SupersetClient]
|
||||
# @RELATION USES -> [UserDashboardPreference]
|
||||
# @LAYER API
|
||||
# @RELATION CALLS -> [GitDeps]
|
||||
# @RELATION CALLS -> [SupersetClient]
|
||||
# @RELATION CALLS -> [UserDashboardPreference]
|
||||
|
||||
import os
|
||||
|
||||
@@ -21,7 +21,7 @@ from ._deps import get_git_service
|
||||
|
||||
# #region _build_no_repo_status_payload [C:1] [TYPE Function]
|
||||
# @BRIEF Build a consistent status payload for dashboards without initialized repositories.
|
||||
# @POST: Returns a stable payload compatible with frontend repository status parsing.
|
||||
# @POST Returns a stable payload compatible with frontend repository status parsing.
|
||||
def _build_no_repo_status_payload() -> dict:
|
||||
return {
|
||||
"is_dirty": False,
|
||||
@@ -43,8 +43,8 @@ def _build_no_repo_status_payload() -> dict:
|
||||
|
||||
# #region _handle_unexpected_git_route_error [C:1] [TYPE Function]
|
||||
# @BRIEF Convert unexpected route-level exceptions to stable 500 API responses.
|
||||
# @PRE: `error` is a non-HTTPException instance.
|
||||
# @POST: Raises HTTPException(500) with route-specific context.
|
||||
# @PRE `error` is a non-HTTPException instance.
|
||||
# @POST Raises HTTPException(500) with route-specific context.
|
||||
def _handle_unexpected_git_route_error(route_name: str, error: Exception) -> None:
|
||||
logger.error(f"[{route_name}][Coherence:Failed] {error}")
|
||||
raise HTTPException(status_code=500, detail=f"{route_name} failed: {error!s}")
|
||||
@@ -53,8 +53,8 @@ def _handle_unexpected_git_route_error(route_name: str, error: Exception) -> Non
|
||||
|
||||
# #region _resolve_repository_status [C:2] [TYPE Function]
|
||||
# @BRIEF Resolve repository status for one dashboard with graceful NO_REPO semantics.
|
||||
# @PRE: `dashboard_id` is a valid integer.
|
||||
# @POST: Returns standard status payload or `NO_REPO` payload when repository path is absent.
|
||||
# @PRE `dashboard_id` is a valid integer.
|
||||
# @POST Returns standard status payload or `NO_REPO` payload when repository path is absent.
|
||||
def _resolve_repository_status(dashboard_id: int) -> dict:
|
||||
git_service = get_git_service()
|
||||
repo_path = git_service._get_repo_path(dashboard_id)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# #region GitMergeRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, git, api]
|
||||
# @BRIEF FastAPI endpoints for merge operations (status, conflicts, resolve, abort, continue).
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
|
||||
|
||||
from fastapi import Depends, HTTPException
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# #region GitRepoLifecycleRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, git, api, sync, deploy]
|
||||
# @BRIEF FastAPI endpoints for Git lifecycle operations (sync, promote, deploy).
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
|
||||
|
||||
from fastapi import Depends, HTTPException
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# #region GitRepoOperationsRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, git, api, history, diff]
|
||||
# @BRIEF FastAPI endpoints for Git repository operations (commit, push, pull, status, diff, history).
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
|
||||
|
||||
from fastapi import Depends, HTTPException
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# #region GitRepoRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, git, api, search]
|
||||
# @BRIEF FastAPI endpoints for core Git repository operations (init, binding, branches, checkout).
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
|
||||
|
||||
from fastapi import Depends, HTTPException
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# #region GitRouter [C:1] [TYPE Module] [SEMANTICS fastapi, git, api]
|
||||
# @BRIEF Shared APIRouter for all Git route modules.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# #region GitSchemas [C:1] [TYPE Module] [SEMANTICS fastapi, git, api, git-server-config-base]
|
||||
#
|
||||
# @BRIEF Defines Pydantic models for the Git integration API layer.
|
||||
# @LAYER: API
|
||||
# @RELATION DEPENDS_ON -> backend.src.models.git
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [EXT:path:backend.src.models.git]
|
||||
#
|
||||
# @INVARIANT: All schemas must be compatible with the FastAPI router.
|
||||
# @INVARIANT All schemas must be compatible with the FastAPI router.
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# #region health_router [C:3] [TYPE Module] [SEMANTICS fastapi, health, api, search, dashboard]
|
||||
# @BRIEF API endpoints for dashboard health monitoring and status aggregation.
|
||||
# @LAYER: UI/API
|
||||
# @RELATION DEPENDS_ON -> health_service
|
||||
# @LAYER UI
|
||||
# @RELATION DEPENDS_ON -> [health_service]
|
||||
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
@@ -16,9 +16,9 @@ router = APIRouter(prefix="/api/health", tags=["Health"])
|
||||
|
||||
# #region get_health_summary [TYPE Function]
|
||||
# @BRIEF Get aggregated health status for all dashboards.
|
||||
# @PRE: Caller has read permission for dashboard health view.
|
||||
# @POST: Returns HealthSummaryResponse.
|
||||
# @RELATION CALLS -> backend.src.services.health_service.HealthService
|
||||
# @PRE Caller has read permission for dashboard health view.
|
||||
# @POST Returns HealthSummaryResponse.
|
||||
# @RELATION CALLS -> [EXT:path:backend.src.services.health_service.HealthService]
|
||||
@router.get("/summary", response_model=HealthSummaryResponse)
|
||||
async def get_health_summary(
|
||||
environment_id: str | None = Query(None),
|
||||
@@ -39,9 +39,9 @@ async def get_health_summary(
|
||||
|
||||
# #region delete_health_report [TYPE Function]
|
||||
# @BRIEF Delete one persisted dashboard validation report from health summary.
|
||||
# @PRE: Caller has write permission for tasks/report maintenance.
|
||||
# @POST: Validation record is removed; linked task/logs are cleaned when available.
|
||||
# @RELATION CALLS -> backend.src.services.health_service.HealthService
|
||||
# @PRE Caller has write permission for tasks/report maintenance.
|
||||
# @POST Validation record is removed; linked task/logs are cleaned when available.
|
||||
# @RELATION CALLS -> [EXT:path:backend.src.services.health_service.HealthService]
|
||||
@router.delete("/summary/{record_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_health_report(
|
||||
record_id: str,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# #region LlmRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, llm, api, provider]
|
||||
# @BRIEF API routes for LLM provider configuration and management.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [LLMProviderService]
|
||||
# @RELATION DEPENDS_ON -> [LLMProviderConfig]
|
||||
# @RELATION DEPENDS_ON -> [get_current_user]
|
||||
@@ -39,8 +39,8 @@ router = APIRouter(tags=["LLM"])
|
||||
|
||||
# #region _is_valid_runtime_api_key [C:4] [TYPE Function]
|
||||
# @BRIEF Validate decrypted runtime API key presence/shape.
|
||||
# @PRE: value can be None.
|
||||
# @POST: Returns True only for non-placeholder key.
|
||||
# @PRE value can be None.
|
||||
# @POST Returns True only for non-placeholder key.
|
||||
# @RELATION BINDS_TO -> [LlmRoutes]
|
||||
def _is_valid_runtime_api_key(value: str | None) -> bool:
|
||||
key = (value or "").strip()
|
||||
@@ -56,8 +56,8 @@ def _is_valid_runtime_api_key(value: str | None) -> bool:
|
||||
|
||||
# #region get_providers [C:4] [TYPE Function]
|
||||
# @BRIEF Retrieve all LLM provider configurations.
|
||||
# @PRE: User is authenticated.
|
||||
# @POST: Returns list of LLMProviderConfig.
|
||||
# @PRE User is authenticated.
|
||||
# @POST Returns list of LLMProviderConfig.
|
||||
# @RELATION CALLS -> [LLMProviderService]
|
||||
# @RELATION DEPENDS_ON -> [LLMProviderConfig]
|
||||
@router.get("/providers", response_model=list[LLMProviderConfig])
|
||||
@@ -93,8 +93,8 @@ async def get_providers(
|
||||
|
||||
# #region fetch_models [C:4] [TYPE Function]
|
||||
# @BRIEF Fetch available models from an LLM provider by base_url+provider_type, or by provider_id.
|
||||
# @PRE: User is authenticated. Either provider_id or base_url+provider_type must be provided.
|
||||
# @POST: Returns a list of available model IDs.
|
||||
# @PRE User is authenticated. Either provider_id or base_url+provider_type must be provided.
|
||||
# @POST Returns a list of available model IDs.
|
||||
# @RELATION CALLS -> [LLMProviderService]
|
||||
# @RELATION CALLS -> [LLMClient]
|
||||
@router.post("/providers/fetch-models")
|
||||
@@ -172,8 +172,8 @@ async def fetch_models(
|
||||
|
||||
# #region get_llm_status [C:4] [TYPE Function]
|
||||
# @BRIEF Returns whether LLM runtime is configured for dashboard validation.
|
||||
# @PRE: User is authenticated.
|
||||
# @POST: configured=true only when an active provider with valid decrypted key exists.
|
||||
# @PRE User is authenticated.
|
||||
# @POST configured=true only when an active provider with valid decrypted key exists.
|
||||
# @RELATION CALLS -> [LLMProviderService]
|
||||
# @RELATION CALLS -> [_is_valid_runtime_api_key]
|
||||
@router.get("/status")
|
||||
@@ -243,8 +243,8 @@ async def get_llm_status(
|
||||
|
||||
# #region create_provider [C:4] [TYPE Function]
|
||||
# @BRIEF Create a new LLM provider configuration.
|
||||
# @PRE: User is authenticated and has admin permissions.
|
||||
# @POST: Returns the created LLMProviderConfig.
|
||||
# @PRE User is authenticated and has admin permissions.
|
||||
# @POST Returns the created LLMProviderConfig.
|
||||
# @RELATION CALLS -> [LLMProviderService]
|
||||
# @RELATION DEPENDS_ON -> [LLMProviderConfig]
|
||||
@router.post(
|
||||
@@ -277,8 +277,8 @@ async def create_provider(
|
||||
|
||||
# #region update_provider [C:4] [TYPE Function]
|
||||
# @BRIEF Update an existing LLM provider configuration.
|
||||
# @PRE: User is authenticated and has admin permissions.
|
||||
# @POST: Returns the updated LLMProviderConfig.
|
||||
# @PRE User is authenticated and has admin permissions.
|
||||
# @POST Returns the updated LLMProviderConfig.
|
||||
# @RELATION CALLS -> [LLMProviderService]
|
||||
# @RELATION DEPENDS_ON -> [LLMProviderConfig]
|
||||
@router.put("/providers/{provider_id}", response_model=LLMProviderConfig)
|
||||
@@ -313,8 +313,8 @@ async def update_provider(
|
||||
|
||||
# #region delete_provider [C:4] [TYPE Function]
|
||||
# @BRIEF Delete an LLM provider configuration.
|
||||
# @PRE: User is authenticated and has admin permissions.
|
||||
# @POST: Returns success status.
|
||||
# @PRE User is authenticated and has admin permissions.
|
||||
# @POST Returns success status.
|
||||
# @RELATION CALLS -> [LLMProviderService]
|
||||
@router.delete("/providers/{provider_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_provider(
|
||||
@@ -336,8 +336,8 @@ async def delete_provider(
|
||||
|
||||
# #region test_connection [C:4] [TYPE Function]
|
||||
# @BRIEF Test connection to an LLM provider.
|
||||
# @PRE: User is authenticated.
|
||||
# @POST: Returns success status and message.
|
||||
# @PRE User is authenticated.
|
||||
# @POST Returns success status and message.
|
||||
# @RELATION CALLS -> [LLMProviderService]
|
||||
# @RELATION DEPENDS_ON -> [LLMClient]
|
||||
@router.post("/providers/{provider_id}/test")
|
||||
@@ -391,8 +391,8 @@ async def test_connection(
|
||||
|
||||
# #region test_provider_config [C:4] [TYPE Function]
|
||||
# @BRIEF Test connection with a provided configuration (not yet saved).
|
||||
# @PRE: User is authenticated.
|
||||
# @POST: Returns success status and message.
|
||||
# @PRE User is authenticated.
|
||||
# @POST Returns success status and message.
|
||||
# @RELATION DEPENDS_ON -> [LLMClient]
|
||||
# @RELATION DEPENDS_ON -> [LLMProviderConfig]
|
||||
@router.post("/providers/test")
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# @RELATION DEPENDS_ON -> [MaintenanceSchemasModule]
|
||||
# @RELATION DEPENDS_ON -> [MaintenanceRouter]
|
||||
# @RELATION DEPENDS_ON -> [AppDependencies]
|
||||
# @RELATION DEPENDS_ON -> [MaintenanceServiceModule]
|
||||
# @RELATION DEPENDS_ON -> [EXT:frontend:MaintenanceServiceModule]
|
||||
# @INVARIANT All mutation endpoints return 202 {task_id} per spec.
|
||||
# @INVARIANT RBAC enforced per FR-015 matrix via has_permission() dependency.
|
||||
|
||||
@@ -54,7 +54,7 @@ from ._schemas import (
|
||||
|
||||
# #region list_dashboard_banners [C:2] [TYPE Function]
|
||||
# @BRIEF Get per-dashboard banner state for the Dashboard Hub indicator.
|
||||
# @RELATION DEPENDS_ON -> [has_permission("maintenance", "READ")]
|
||||
# @RELATION DEPENDS_ON -> [EXT:code:has_permission("maintenance", "READ")]
|
||||
@router.get("/dashboard-banners", response_model=list[MaintenanceDashboardBannerState])
|
||||
async def list_dashboard_banners(
|
||||
db: Session = Depends(get_db),
|
||||
@@ -118,7 +118,7 @@ async def list_dashboard_banners(
|
||||
|
||||
# #region list_events [C:2] [TYPE Function]
|
||||
# @BRIEF Get active and completed maintenance event lists with affected dashboard counts.
|
||||
# @RELATION DEPENDS_ON -> [has_permission("maintenance", "READ")]
|
||||
# @RELATION DEPENDS_ON -> [EXT:code:has_permission("maintenance", "READ")]
|
||||
@router.get("/events", response_model=MaintenanceEventListResponse)
|
||||
async def list_events(
|
||||
db: Session = Depends(get_db),
|
||||
@@ -226,7 +226,7 @@ async def list_events(
|
||||
# Always returns 202 with task_id and maintenance_id.
|
||||
# Idempotency: same (tables, start_time, end_time) returns already_active (409-like but 200).
|
||||
# Tables sorted & lowered for idempotency key. Message NOT in key.
|
||||
# @RELATION DEPENDS_ON -> [has_permission("maintenance", "WRITE")]
|
||||
# @RELATION DEPENDS_ON -> [EXT:code:has_permission("maintenance", "WRITE")]
|
||||
# @RELATION DEPENDS_ON -> [TaskManager]
|
||||
@router.post(
|
||||
"/start",
|
||||
@@ -367,7 +367,7 @@ async def start_maintenance(
|
||||
# #region end_maintenance [C:3] [TYPE Function]
|
||||
# @BRIEF End a specific maintenance event by ID. Removes banners from affected dashboards.
|
||||
# Always returns 202 with task_id. Idempotent: already_completed returns success.
|
||||
# @RELATION DEPENDS_ON -> [has_permission("maintenance", "WRITE")]
|
||||
# @RELATION DEPENDS_ON -> [EXT:code:has_permission("maintenance", "WRITE")]
|
||||
@router.post(
|
||||
"/{maintenance_id}/end",
|
||||
status_code=status.HTTP_202_ACCEPTED,
|
||||
@@ -437,7 +437,7 @@ async def end_maintenance(
|
||||
# #region end_all_maintenance [C:3] [TYPE Function]
|
||||
# @BRIEF End all active maintenance events. Removes all banners from all dashboards.
|
||||
# Always returns 202 with task_id. Supports environment scoping for API keys.
|
||||
# @RELATION DEPENDS_ON -> [has_permission("maintenance", "WRITE")]
|
||||
# @RELATION DEPENDS_ON -> [EXT:code:has_permission("maintenance", "WRITE")]
|
||||
@router.post(
|
||||
"/end-all",
|
||||
status_code=status.HTTP_202_ACCEPTED,
|
||||
@@ -500,7 +500,7 @@ async def end_all_maintenance(
|
||||
|
||||
# #region get_maintenance_settings [C:2] [TYPE Function]
|
||||
# @BRIEF Get current maintenance settings configuration.
|
||||
# @RELATION DEPENDS_ON -> [has_permission("maintenance", "READ")]
|
||||
# @RELATION DEPENDS_ON -> [EXT:code:has_permission("maintenance", "READ")]
|
||||
@router.get("/settings", response_model=MaintenanceSettingsResponse)
|
||||
async def get_maintenance_settings(
|
||||
db: Session = Depends(get_db),
|
||||
@@ -536,7 +536,7 @@ async def get_maintenance_settings(
|
||||
# #region update_maintenance_settings [C:2] [TYPE Function]
|
||||
# @BRIEF Update maintenance settings. All fields optional for partial update.
|
||||
# admin role required per FR-015.
|
||||
# @RELATION DEPENDS_ON -> [has_permission("admin:settings", "WRITE")]
|
||||
# @RELATION DEPENDS_ON -> [EXT:code:has_permission("admin:settings", "WRITE")]
|
||||
@router.put("/settings", response_model=MaintenanceSettingsResponse)
|
||||
async def update_maintenance_settings(
|
||||
settings_data: MaintenanceSettingsUpdate,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# #region MaintenanceSchemasModule [C:2] [TYPE Module] [SEMANTICS pydantic, schema, maintenance, request, response]
|
||||
# @BRIEF Pydantic models for Maintenance Banner API: request/response schemas per data-model.md.
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [Pydantic]
|
||||
# @RELATION DEPENDS_ON -> [EXT:Library:pydantic]
|
||||
# @INVARIANT All response schemas use the standard envelope shape: { status, data, error, meta }
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# #region MappingsApi [C:3] [TYPE Module] [SEMANTICS fastapi, mapping, api, mapping-create]
|
||||
#
|
||||
# @BRIEF API endpoints for managing database mappings and getting suggestions.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [AppDependencies]
|
||||
# @RELATION DEPENDS_ON -> [DatabaseModule]
|
||||
# @RELATION DEPENDS_ON -> [mapping_service]
|
||||
@@ -54,8 +54,8 @@ class SuggestRequest(BaseModel):
|
||||
|
||||
# #region get_mappings [TYPE Function]
|
||||
# @BRIEF List all saved database mappings.
|
||||
# @PRE: db session is injected.
|
||||
# @POST: Returns filtered list of DatabaseMapping records.
|
||||
# @PRE db session is injected.
|
||||
# @POST Returns filtered list of DatabaseMapping records.
|
||||
@router.get("", response_model=list[MappingResponse])
|
||||
async def get_mappings(
|
||||
source_env_id: str | None = None,
|
||||
@@ -74,8 +74,8 @@ async def get_mappings(
|
||||
|
||||
# #region create_mapping [TYPE Function]
|
||||
# @BRIEF Create or update a database mapping.
|
||||
# @PRE: mapping is valid MappingCreate, db session is injected.
|
||||
# @POST: DatabaseMapping created or updated in database.
|
||||
# @PRE mapping is valid MappingCreate, db session is injected.
|
||||
# @POST DatabaseMapping created or updated in database.
|
||||
@router.post("", response_model=MappingResponse)
|
||||
async def create_mapping(
|
||||
mapping: MappingCreate,
|
||||
@@ -107,8 +107,8 @@ async def create_mapping(
|
||||
|
||||
# #region suggest_mappings_api [TYPE Function]
|
||||
# @BRIEF Get suggested mappings based on fuzzy matching.
|
||||
# @PRE: request is valid SuggestRequest, config_manager is injected.
|
||||
# @POST: Returns mapping suggestions.
|
||||
# @PRE request is valid SuggestRequest, config_manager is injected.
|
||||
# @POST Returns mapping suggestions.
|
||||
@router.post("/suggest")
|
||||
async def suggest_mappings_api(
|
||||
request: SuggestRequest,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# #region MigrationApi [C:5] [TYPE Module] [SEMANTICS fastapi, migration, api, sync, search, mapping]
|
||||
# @BRIEF HTTP contract layer for migration orchestration, settings, dry-run, and mapping sync endpoints.
|
||||
# @LAYER: Infra
|
||||
# @LAYER Infrastructure
|
||||
# @RELATION DEPENDS_ON -> [AppDependencies]
|
||||
# @RELATION DEPENDS_ON -> [DatabaseModule]
|
||||
# @RELATION DEPENDS_ON -> [DashboardSelection]
|
||||
@@ -8,18 +8,18 @@
|
||||
# @RELATION DEPENDS_ON -> [MigrationDryRunService]
|
||||
# @RELATION DEPENDS_ON -> [IdMappingService]
|
||||
# @RELATION DEPENDS_ON -> [ResourceMapping]
|
||||
# @INVARIANT: Migration endpoints never execute with invalid environment references and always return explicit HTTP errors on guard failures.
|
||||
# @PRE: Backend core services initialized and Database session available.
|
||||
# @POST: Migration tasks are enqueued or dry-run results are computed and returned.
|
||||
# @SIDE_EFFECT: Enqueues long-running tasks, potentially mutates ResourceMapping table, and performs remote Superset API calls.
|
||||
# @DATA_CONTRACT: [DashboardSelection | QueryParams] -> [TaskResponse | DryRunResult | MappingSummary]
|
||||
# @TEST_CONTRACT: [DashboardSelection + configured envs] -> [task_id | dry-run result | sync summary]
|
||||
# @TEST_SCENARIO: [invalid_environment] -> [HTTP_400_or_404]
|
||||
# @TEST_SCENARIO: [valid_execution] -> [success_payload_with_required_fields]
|
||||
# @TEST_EDGE: [missing_field] ->[HTTP_400]
|
||||
# @TEST_EDGE: [invalid_type] ->[validation_error]
|
||||
# @TEST_EDGE: [external_fail] ->[HTTP_500]
|
||||
# @TEST_INVARIANT: [EnvironmentValidationBeforeAction] -> VERIFIED_BY: [invalid_environment, valid_execution]
|
||||
# @INVARIANT Migration endpoints never execute with invalid environment references and always return explicit HTTP errors on guard failures.
|
||||
# @PRE Backend core services initialized and Database session available.
|
||||
# @POST Migration tasks are enqueued or dry-run results are computed and returned.
|
||||
# @SIDE_EFFECT Enqueues long-running tasks, potentially mutates ResourceMapping table, and performs remote Superset API calls.
|
||||
# @DATA_CONTRACT [DashboardSelection | QueryParams] -> [TaskResponse | DryRunResult | MappingSummary]
|
||||
# @TEST_CONTRACT [DashboardSelection + configured envs] -> [task_id | dry-run result | sync summary]
|
||||
# @TEST_SCENARIO [invalid_environment] -> [HTTP_400_or_404]
|
||||
# @TEST_SCENARIO [valid_execution] -> [success_payload_with_required_fields]
|
||||
# @TEST_EDGE [missing_field] ->[HTTP_400]
|
||||
# @TEST_EDGE [invalid_type] ->[validation_error]
|
||||
# @TEST_EDGE [external_fail] ->[HTTP_500]
|
||||
# @TEST_INVARIANT [EnvironmentValidationBeforeAction] -> VERIFIED_BY: [invalid_environment, valid_execution]
|
||||
|
||||
from typing import Any, cast
|
||||
|
||||
@@ -42,11 +42,11 @@ router = APIRouter(prefix="/api", tags=["migration"])
|
||||
|
||||
# #region get_dashboards [C:3] [TYPE Function]
|
||||
# @BRIEF Fetch dashboard metadata from a requested environment for migration selection UI.
|
||||
# @PRE: env_id is provided and exists in configured environments.
|
||||
# @POST: Returns List[DashboardMetadata] for the resolved environment; emits HTTP_404 when environment is absent.
|
||||
# @SIDE_EFFECT: Reads environment configuration and performs remote Superset metadata retrieval over network.
|
||||
# @DATA_CONTRACT: Input[str env_id] -> Output[List[DashboardMetadata]]
|
||||
# @RELATION CALLS -> [SupersetClient.get_dashboards_summary]
|
||||
# @PRE env_id is provided and exists in configured environments.
|
||||
# @POST Returns List[DashboardMetadata] for the resolved environment; emits HTTP_404 when environment is absent.
|
||||
# @SIDE_EFFECT Reads environment configuration and performs remote Superset metadata retrieval over network.
|
||||
# @DATA_CONTRACT Input[str env_id] -> Output[List[DashboardMetadata]]
|
||||
# @RELATION CALLS -> [EXT:method:SupersetClient.get_dashboards_summary]
|
||||
@router.get("/environments/{env_id}/dashboards", response_model=list[DashboardMetadata])
|
||||
async def get_dashboards(
|
||||
env_id: str,
|
||||
@@ -73,13 +73,13 @@ async def get_dashboards(
|
||||
|
||||
# #region execute_migration [C:5] [TYPE Function]
|
||||
# @BRIEF Validate migration selection and enqueue asynchronous migration task execution.
|
||||
# @PRE: DashboardSelection payload is valid and both source/target environments exist.
|
||||
# @POST: Returns {"task_id": str, "message": str} when task creation succeeds; emits HTTP_400/HTTP_500 on failure.
|
||||
# @SIDE_EFFECT: Reads configuration, writes task record through task manager, and writes operational logs.
|
||||
# @DATA_CONTRACT: Input[DashboardSelection] -> Output[Dict[str, str]]
|
||||
# @PRE DashboardSelection payload is valid and both source/target environments exist.
|
||||
# @POST Returns {"task_id": str, "message": str} when task creation succeeds; emits HTTP_400/HTTP_500 on failure.
|
||||
# @SIDE_EFFECT Reads configuration, writes task record through task manager, and writes operational logs.
|
||||
# @DATA_CONTRACT Input[DashboardSelection] -> Output[Dict[str, str]]
|
||||
# @RELATION CALLS -> [create_task]
|
||||
# @RELATION DEPENDS_ON -> [DashboardSelection]
|
||||
# @INVARIANT: Migration task dispatch never occurs before source and target environment ids pass guard validation.
|
||||
# @INVARIANT Migration task dispatch never occurs before source and target environment ids pass guard validation.
|
||||
@router.post("/migration/execute")
|
||||
async def execute_migration(
|
||||
selection: DashboardSelection,
|
||||
@@ -136,13 +136,13 @@ async def execute_migration(
|
||||
|
||||
# #region dry_run_migration [C:5] [TYPE Function]
|
||||
# @BRIEF Build pre-flight migration diff and risk summary without mutating target systems.
|
||||
# @PRE: DashboardSelection is valid, source and target environments exist, differ, and selected_ids is non-empty.
|
||||
# @POST: Returns deterministic dry-run payload; emits HTTP_400 for guard violations and HTTP_500 for orchestrator value errors.
|
||||
# @SIDE_EFFECT: Reads local mappings from DB and fetches source/target metadata via Superset API.
|
||||
# @DATA_CONTRACT: Input[DashboardSelection] -> Output[Dict[str, Any]]
|
||||
# @PRE DashboardSelection is valid, source and target environments exist, differ, and selected_ids is non-empty.
|
||||
# @POST Returns deterministic dry-run payload; emits HTTP_400 for guard violations and HTTP_500 for orchestrator value errors.
|
||||
# @SIDE_EFFECT Reads local mappings from DB and fetches source/target metadata via Superset API.
|
||||
# @DATA_CONTRACT Input[DashboardSelection] -> Output[Dict[str, Any]]
|
||||
# @RELATION DEPENDS_ON -> [DashboardSelection]
|
||||
# @RELATION DEPENDS_ON -> [MigrationDryRunService]
|
||||
# @INVARIANT: Dry-run flow remains read-only and rejects identical source/target environments before service execution.
|
||||
# @INVARIANT Dry-run flow remains read-only and rejects identical source/target environments before service execution.
|
||||
@router.post("/migration/dry-run", response_model=dict[str, Any])
|
||||
async def dry_run_migration(
|
||||
selection: DashboardSelection,
|
||||
@@ -202,10 +202,10 @@ async def dry_run_migration(
|
||||
|
||||
# #region get_migration_settings [C:3] [TYPE Function]
|
||||
# @BRIEF Read and return configured migration synchronization cron expression.
|
||||
# @PRE: Configuration store is available and requester has READ permission.
|
||||
# @POST: Returns {"cron": str} reflecting current persisted settings value.
|
||||
# @SIDE_EFFECT: Reads configuration from config manager.
|
||||
# @DATA_CONTRACT: Input[None] -> Output[Dict[str, str]]
|
||||
# @PRE Configuration store is available and requester has READ permission.
|
||||
# @POST Returns {"cron": str} reflecting current persisted settings value.
|
||||
# @SIDE_EFFECT Reads configuration from config manager.
|
||||
# @DATA_CONTRACT Input[None] -> Output[Dict[str, str]]
|
||||
# @RELATION DEPENDS_ON -> [AppDependencies]
|
||||
@router.get("/migration/settings", response_model=dict[str, str])
|
||||
async def get_migration_settings(
|
||||
@@ -223,10 +223,10 @@ async def get_migration_settings(
|
||||
|
||||
# #region update_migration_settings [C:3] [TYPE Function]
|
||||
# @BRIEF Validate and persist migration synchronization cron expression update.
|
||||
# @PRE: Payload includes "cron" key and requester has WRITE permission.
|
||||
# @POST: Returns {"cron": str, "status": "updated"} and persists updated cron value.
|
||||
# @SIDE_EFFECT: Mutates configuration and writes persisted config through config manager.
|
||||
# @DATA_CONTRACT: Input[Dict[str, str]] -> Output[Dict[str, str]]
|
||||
# @PRE Payload includes "cron" key and requester has WRITE permission.
|
||||
# @POST Returns {"cron": str, "status": "updated"} and persists updated cron value.
|
||||
# @SIDE_EFFECT Mutates configuration and writes persisted config through config manager.
|
||||
# @DATA_CONTRACT Input[Dict[str, str]] -> Output[Dict[str, str]]
|
||||
# @RELATION DEPENDS_ON -> [AppDependencies]
|
||||
@router.put("/migration/settings", response_model=dict[str, str])
|
||||
async def update_migration_settings(
|
||||
@@ -254,10 +254,10 @@ async def update_migration_settings(
|
||||
|
||||
# #region get_resource_mappings [C:3] [TYPE Function]
|
||||
# @BRIEF Fetch synchronized resource mappings with optional filters and pagination for migration mappings view.
|
||||
# @PRE: skip>=0, 1<=limit<=500, DB session is active, requester has READ permission.
|
||||
# @POST: Returns {"items": [...], "total": int} where items reflect applied filters and pagination.
|
||||
# @SIDE_EFFECT: Executes database read queries against ResourceMapping table.
|
||||
# @DATA_CONTRACT: Input[QueryParams] -> Output[Dict[str, Any]]
|
||||
# @PRE skip>=0, 1<=limit<=500, DB session is active, requester has READ permission.
|
||||
# @POST Returns {"items": [...], "total": int} where items reflect applied filters and pagination.
|
||||
# @SIDE_EFFECT Executes database read queries against ResourceMapping table.
|
||||
# @DATA_CONTRACT Input[QueryParams] -> Output[Dict[str, Any]]
|
||||
# @RELATION DEPENDS_ON -> [ResourceMapping]
|
||||
@router.get("/migration/mappings-data", response_model=dict[str, Any])
|
||||
async def get_resource_mappings(
|
||||
@@ -326,10 +326,10 @@ async def get_resource_mappings(
|
||||
|
||||
# #region trigger_sync_now [C:3] [TYPE Function]
|
||||
# @BRIEF Trigger immediate ID synchronization for every configured environment.
|
||||
# @PRE: At least one environment is configured and requester has EXECUTE permission.
|
||||
# @POST: Returns sync summary with synced/failed counts after attempting all environments.
|
||||
# @SIDE_EFFECT: Upserts Environment rows, commits DB transaction, performs network sync calls, and writes logs.
|
||||
# @DATA_CONTRACT: Input[None] -> Output[Dict[str, Any]]
|
||||
# @PRE At least one environment is configured and requester has EXECUTE permission.
|
||||
# @POST Returns sync summary with synced/failed counts after attempting all environments.
|
||||
# @SIDE_EFFECT Upserts Environment rows, commits DB transaction, performs network sync calls, and writes logs.
|
||||
# @DATA_CONTRACT Input[None] -> Output[Dict[str, Any]]
|
||||
# @RELATION DEPENDS_ON -> [IdMappingService]
|
||||
# @RELATION CALLS -> [sync_environment]
|
||||
@router.post("/migration/sync-now", response_model=dict[str, Any])
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# #region PluginsRouter [C:3] [TYPE Module] [SEMANTICS fastapi, plugin, api]
|
||||
# @BRIEF Defines the FastAPI router for plugin-related endpoints, allowing clients to list available plugins.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [PluginConfig]
|
||||
# @RELATION DEPENDS_ON -> [get_plugin_loader]
|
||||
# @RELATION BINDS_TO -> [API_Routes]
|
||||
@@ -16,8 +16,8 @@ router = APIRouter()
|
||||
|
||||
# #region list_plugins [TYPE Function]
|
||||
# @BRIEF Retrieve a list of all available plugins.
|
||||
# @PRE: plugin_loader is injected via Depends.
|
||||
# @POST: Returns a list of PluginConfig objects.
|
||||
# @PRE plugin_loader is injected via Depends.
|
||||
# @POST Returns a list of PluginConfig objects.
|
||||
# @RELATION CALLS -> [get_plugin_loader]
|
||||
# @RELATION DEPENDS_ON -> [PluginConfig]
|
||||
@router.get("", response_model=list[PluginConfig])
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# #region ProfileApiModule [C:5] [TYPE Module] [SEMANTICS fastapi, profile, api, validate, search, superset]
|
||||
#
|
||||
# @BRIEF Exposes self-scoped profile preference endpoints and environment-based Superset account lookup.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [ProfileService]
|
||||
# @RELATION DEPENDS_ON -> [get_current_user]
|
||||
# @RELATION DEPENDS_ON -> [get_db]
|
||||
#
|
||||
# @INVARIANT: Endpoints are self-scoped and never mutate another user preference.
|
||||
# @PRE: Auth middleware configured, database session available
|
||||
# @POST: Profile endpoints registered
|
||||
# @INVARIANT Endpoints are self-scoped and never mutate another user preference.
|
||||
# @PRE Auth middleware configured, database session available
|
||||
# @POST Profile endpoints registered
|
||||
# UX_STATE: ProfileLoad -> Returns stable ProfilePreferenceResponse for authenticated user.
|
||||
# UX_STATE: Saving -> Validation errors map to actionable 422 details.
|
||||
# UX_STATE: LookupLoading -> Returns success/degraded Superset lookup payload.
|
||||
# UX_FEEDBACK: Stable status/message/warning payloads support profile page feedback.
|
||||
# UX_RECOVERY: Lookup degradation keeps manual username save path available.
|
||||
# @SIDE_EFFECT: Registers /api/profile/* routes
|
||||
# @DATA_CONTRACT: ProfileRequest -> ProfileResponse
|
||||
# @SIDE_EFFECT Registers /api/profile/* routes
|
||||
# @DATA_CONTRACT ProfileRequest -> ProfileResponse
|
||||
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
@@ -48,8 +48,8 @@ router = APIRouter(prefix="/api/profile", tags=["profile"])
|
||||
# #region _get_profile_service [TYPE Function]
|
||||
# @RELATION CALLS -> ProfileService
|
||||
# @BRIEF Build profile service for current request scope.
|
||||
# @PRE: db session and config manager are available.
|
||||
# @POST: Returns a ready ProfileService instance.
|
||||
# @PRE db session and config manager are available.
|
||||
# @POST Returns a ready ProfileService instance.
|
||||
def _get_profile_service(db: Session, config_manager, plugin_loader=None) -> ProfileService:
|
||||
return ProfileService(
|
||||
db=db,
|
||||
@@ -62,8 +62,8 @@ def _get_profile_service(db: Session, config_manager, plugin_loader=None) -> Pro
|
||||
# #region get_preferences [TYPE Function]
|
||||
# @RELATION CALLS -> ProfileService
|
||||
# @BRIEF Get authenticated user's dashboard filter preference.
|
||||
# @PRE: Valid JWT and authenticated user context.
|
||||
# @POST: Returns preference payload for current user only.
|
||||
# @PRE Valid JWT and authenticated user context.
|
||||
# @POST Returns preference payload for current user only.
|
||||
@router.get("/preferences", response_model=ProfilePreferenceResponse)
|
||||
async def get_preferences(
|
||||
current_user: User = Depends(get_current_user),
|
||||
@@ -81,8 +81,8 @@ async def get_preferences(
|
||||
# #region update_preferences [TYPE Function]
|
||||
# @RELATION CALLS -> ProfileService
|
||||
# @BRIEF Update authenticated user's dashboard filter preference.
|
||||
# @PRE: Valid JWT and valid request payload.
|
||||
# @POST: Persists normalized preference for current user or raises validation/authorization errors.
|
||||
# @PRE Valid JWT and valid request payload.
|
||||
# @POST Persists normalized preference for current user or raises validation/authorization errors.
|
||||
@router.patch("/preferences", response_model=ProfilePreferenceResponse)
|
||||
async def update_preferences(
|
||||
payload: ProfilePreferenceUpdateRequest,
|
||||
@@ -108,8 +108,8 @@ async def update_preferences(
|
||||
# #region lookup_superset_accounts [TYPE Function]
|
||||
# @RELATION CALLS -> ProfileService
|
||||
# @BRIEF Lookup Superset account candidates in selected environment.
|
||||
# @PRE: Valid JWT, authenticated context, and environment_id query parameter.
|
||||
# @POST: Returns success or degraded lookup payload with stable shape.
|
||||
# @PRE Valid JWT, authenticated context, and environment_id query parameter.
|
||||
# @POST Returns success or degraded lookup payload with stable shape.
|
||||
@router.get("/superset-accounts", response_model=SupersetAccountLookupResponse)
|
||||
async def lookup_superset_accounts(
|
||||
environment_id: str = Query(...),
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
# #region ReportsRouter [C:5] [TYPE Module] [SEMANTICS fastapi, report, api, transform, search, task]
|
||||
# @BRIEF FastAPI router for unified task report list and detail retrieval endpoints.
|
||||
# @LAYER: API
|
||||
# @RELATION DEPENDS_ON -> [ReportsService:Class]
|
||||
# @RELATION DEPENDS_ON -> [get_task_manager:Function]
|
||||
# @RELATION DEPENDS_ON -> [get_clean_release_repository:Function]
|
||||
# @RELATION DEPENDS_ON -> [has_permission:Function]
|
||||
# @INVARIANT: Endpoints are read-only and do not trigger long-running tasks.
|
||||
# @PRE: Reports service and dependencies are initialized.
|
||||
# @POST: Router is configured and endpoints are ready for registration.
|
||||
# @SIDE_EFFECT: None
|
||||
# @DATA_CONTRACT: [ReportQuery] -> [ReportCollection | ReportDetailView]
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [ReportsService]
|
||||
# @RELATION DEPENDS_ON -> [get_task_manager]
|
||||
# @RELATION DEPENDS_ON -> [get_clean_release_repository]
|
||||
# @RELATION DEPENDS_ON -> [has_permission]
|
||||
# @INVARIANT Endpoints are read-only and do not trigger long-running tasks.
|
||||
# @PRE Reports service and dependencies are initialized.
|
||||
# @POST Router is configured and endpoints are ready for registration.
|
||||
# @SIDE_EFFECT None
|
||||
# @DATA_CONTRACT [ReportQuery] -> [ReportCollection | ReportDetailView]
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
@@ -37,8 +37,8 @@ router = APIRouter(prefix="/api/reports", tags=["Reports"])
|
||||
|
||||
# #region _parse_csv_enum_list [C:1] [TYPE Function]
|
||||
# @BRIEF Parse comma-separated query value into enum list.
|
||||
# @PRE: raw may be None/empty or comma-separated values.
|
||||
# @POST: Returns enum list or raises HTTP 400 with deterministic machine-readable payload.
|
||||
# @PRE raw may be None/empty or comma-separated values.
|
||||
# @POST Returns enum list or raises HTTP 400 with deterministic machine-readable payload.
|
||||
# @RELATION BINDS_TO -> [ReportsRouter]
|
||||
def _parse_csv_enum_list(raw: str | None, enum_cls, field_name: str) -> list:
|
||||
with belief_scope("_parse_csv_enum_list"):
|
||||
@@ -70,14 +70,14 @@ def _parse_csv_enum_list(raw: str | None, enum_cls, field_name: str) -> list:
|
||||
|
||||
# #region list_reports [C:2] [TYPE Function]
|
||||
# @BRIEF Return paginated unified reports list.
|
||||
# @PRE: authenticated/authorized request and validated query params.
|
||||
# @POST: returns {items,total,page,page_size,has_next,applied_filters}.
|
||||
# @POST: deterministic error payload for invalid filters.
|
||||
# @RELATION CALLS -> [_parse_csv_enum_list:Function]
|
||||
# @RELATION DEPENDS_ON -> [ReportQuery:Class]
|
||||
# @RELATION DEPENDS_ON -> [ReportsService:Class]
|
||||
# @PRE authenticated/authorized request and validated query params.
|
||||
# @POST returns {items,total,page,page_size,has_next,applied_filters}.
|
||||
# @POST deterministic error payload for invalid filters.
|
||||
# @RELATION CALLS -> [_parse_csv_enum_list]
|
||||
# @RELATION DEPENDS_ON -> [ReportQuery]
|
||||
# @RELATION DEPENDS_ON -> [ReportsService]
|
||||
#
|
||||
# @TEST_CONTRACT: ListReportsApi ->
|
||||
# @TEST_CONTRACT ListReportsApi ->
|
||||
# {
|
||||
# required_fields: {page: int, page_size: int, sort_by: str, sort_order: str},
|
||||
# optional_fields: {task_types: str, statuses: str, search: str},
|
||||
@@ -86,10 +86,10 @@ def _parse_csv_enum_list(raw: str | None, enum_cls, field_name: str) -> list:
|
||||
# "Raises HTTPException 400 for invalid query parameters"
|
||||
# ]
|
||||
# }
|
||||
# @TEST_FIXTURE: valid_list_request -> {"page": 1, "page_size": 20}
|
||||
# @TEST_EDGE: invalid_task_type_filter -> raises HTTPException(400)
|
||||
# @TEST_EDGE: malformed_query -> raises HTTPException(400)
|
||||
# @TEST_INVARIANT: consistent_list_payload -> verifies: [valid_list_request]
|
||||
# @TEST_FIXTURE valid_list_request -> {"page": 1, "page_size": 20}
|
||||
# @TEST_EDGE invalid_task_type_filter -> raises HTTPException(400)
|
||||
# @TEST_EDGE malformed_query -> raises HTTPException(400)
|
||||
# @TEST_INVARIANT consistent_list_payload -> verifies: [valid_list_request]
|
||||
@router.get("", response_model=ReportCollection)
|
||||
async def list_reports(
|
||||
page: int = Query(1, ge=1),
|
||||
@@ -145,9 +145,9 @@ async def list_reports(
|
||||
|
||||
# #region get_report_detail [C:2] [TYPE Function]
|
||||
# @BRIEF Return one normalized report detail with diagnostics and next actions.
|
||||
# @PRE: authenticated/authorized request and existing report_id.
|
||||
# @POST: returns normalized detail envelope or 404 when report is not found.
|
||||
# @RELATION CALLS -> [ReportsService:Class]
|
||||
# @PRE authenticated/authorized request and existing report_id.
|
||||
# @POST returns normalized detail envelope or 404 when report is not found.
|
||||
# @RELATION CALLS -> [ReportsService]
|
||||
@router.get("/{report_id}", response_model=ReportDetailView)
|
||||
async def get_report_detail(
|
||||
report_id: str,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# #region SettingsRouter [C:5] [TYPE Module] [SEMANTICS fastapi, api, superset, logging-config-response]
|
||||
#
|
||||
# @BRIEF Provides API endpoints for managing application settings and Superset environments.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [ConfigManager]
|
||||
# @RELATION DEPENDS_ON -> [get_config_manager]
|
||||
# @RELATION DEPENDS_ON -> [has_permission]
|
||||
@@ -10,8 +10,8 @@
|
||||
# @POST Settings are read or written via ConfigManager.
|
||||
# @SIDE_EFFECT Persists config changes to disk via ConfigManager.
|
||||
# @DATA_CONTRACT Input -> ConfigUpdateRequest, Output -> AppConfig, LoggingConfigResponse
|
||||
# @INVARIANT: All settings changes must be persisted via ConfigManager.
|
||||
# @PUBLIC_API: router
|
||||
# @INVARIANT All settings changes must be persisted via ConfigManager.
|
||||
# @PUBLIC_API router
|
||||
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
@@ -53,8 +53,8 @@ router = APIRouter()
|
||||
# #region _normalize_superset_env_url [C:1] [TYPE Function]
|
||||
# @BRIEF Canonicalize Superset environment URL to base host/path without trailing /api/v1.
|
||||
# Auto-prepends https:// if no scheme is present.
|
||||
# @PRE: raw_url can be empty.
|
||||
# @POST: Returns normalized base URL with scheme.
|
||||
# @PRE raw_url can be empty.
|
||||
# @POST Returns normalized base URL with scheme.
|
||||
def _normalize_superset_env_url(raw_url: str) -> str:
|
||||
normalized = str(raw_url or "").strip().rstrip("/")
|
||||
if normalized.lower().endswith("/api/v1"):
|
||||
@@ -71,8 +71,8 @@ def _normalize_superset_env_url(raw_url: str) -> str:
|
||||
|
||||
# #region _validate_superset_connection_fast [C:2] [TYPE Function]
|
||||
# @BRIEF Run lightweight Superset connectivity validation without full pagination scan.
|
||||
# @PRE: env contains valid URL and credentials.
|
||||
# @POST: Raises on auth/API failures; returns None on success.
|
||||
# @PRE env contains valid URL and credentials.
|
||||
# @POST Raises on auth/API failures; returns None on success.
|
||||
def _validate_superset_connection_fast(env: Environment) -> None:
|
||||
client = SupersetClient(env)
|
||||
# 1) Explicit auth check
|
||||
@@ -92,8 +92,8 @@ def _validate_superset_connection_fast(env: Environment) -> None:
|
||||
|
||||
# #region get_settings [C:2] [TYPE Function]
|
||||
# @BRIEF Retrieves all application settings.
|
||||
# @PRE: Config manager is available.
|
||||
# @POST: Returns masked AppConfig.
|
||||
# @PRE Config manager is available.
|
||||
# @POST Returns masked AppConfig.
|
||||
@router.get("", response_model=AppConfig)
|
||||
async def get_settings(
|
||||
config_manager: ConfigManager = Depends(get_config_manager),
|
||||
@@ -115,9 +115,9 @@ async def get_settings(
|
||||
|
||||
# #region get_features [C:1] [TYPE Function]
|
||||
# @BRIEF Public endpoint returning feature flags for frontend sidebar filtering.
|
||||
# @RATIONALE: Unauthenticated because sidebar filtering must work for all users, not just admins.
|
||||
# @PRE: Config manager is available.
|
||||
# @POST: Returns dict with dataset_review and health_monitor booleans.
|
||||
# @RATIONALE Unauthenticated because sidebar filtering must work for all users, not just admins.
|
||||
# @PRE Config manager is available.
|
||||
# @POST Returns dict with dataset_review and health_monitor booleans.
|
||||
@router.get("/features")
|
||||
async def get_features(
|
||||
config_manager: ConfigManager = Depends(get_config_manager),
|
||||
@@ -130,8 +130,8 @@ async def get_features(
|
||||
|
||||
# #region update_global_settings [C:2] [TYPE Function]
|
||||
# @BRIEF Updates global application settings.
|
||||
# @PRE: New settings are provided.
|
||||
# @POST: Global settings are updated.
|
||||
# @PRE New settings are provided.
|
||||
# @POST Global settings are updated.
|
||||
@router.patch("/global", response_model=GlobalSettings)
|
||||
async def update_global_settings(
|
||||
settings: GlobalSettings,
|
||||
@@ -164,7 +164,7 @@ async def get_storage_settings(
|
||||
|
||||
# #region update_storage_settings [C:2] [TYPE Function]
|
||||
# @BRIEF Updates storage-specific settings.
|
||||
# @POST: Storage settings are updated and saved.
|
||||
# @POST Storage settings are updated and saved.
|
||||
@router.put("/storage", response_model=StorageConfig)
|
||||
async def update_storage_settings(
|
||||
storage: StorageConfig,
|
||||
@@ -187,8 +187,8 @@ async def update_storage_settings(
|
||||
|
||||
# #region get_environments [C:2] [TYPE Function]
|
||||
# @BRIEF Lists all configured Superset environments.
|
||||
# @PRE: Config manager is available.
|
||||
# @POST: Returns list of environments.
|
||||
# @PRE Config manager is available.
|
||||
# @POST Returns list of environments.
|
||||
@router.get("/environments", response_model=list[Environment])
|
||||
async def get_environments(
|
||||
config_manager: ConfigManager = Depends(get_config_manager),
|
||||
@@ -208,8 +208,8 @@ async def get_environments(
|
||||
|
||||
# #region add_environment [C:2] [TYPE Function]
|
||||
# @BRIEF Adds a new Superset environment.
|
||||
# @PRE: Environment data is valid and reachable.
|
||||
# @POST: Environment is added to config.
|
||||
# @PRE Environment data is valid and reachable.
|
||||
# @POST Environment is added to config.
|
||||
@router.post("/environments", response_model=Environment)
|
||||
async def add_environment(
|
||||
env: Environment,
|
||||
@@ -240,8 +240,8 @@ async def add_environment(
|
||||
|
||||
# #region update_environment [C:2] [TYPE Function]
|
||||
# @BRIEF Updates an existing Superset environment.
|
||||
# @PRE: ID and valid environment data are provided.
|
||||
# @POST: Environment is updated in config.
|
||||
# @PRE ID and valid environment data are provided.
|
||||
# @POST Environment is updated in config.
|
||||
@router.put("/environments/{id}", response_model=Environment)
|
||||
async def update_environment(
|
||||
id: str,
|
||||
@@ -283,8 +283,8 @@ async def update_environment(
|
||||
|
||||
# #region delete_environment [C:2] [TYPE Function]
|
||||
# @BRIEF Deletes a Superset environment.
|
||||
# @PRE: ID is provided.
|
||||
# @POST: Environment is removed from config.
|
||||
# @PRE ID is provided.
|
||||
# @POST Environment is removed from config.
|
||||
@router.delete("/environments/{id}")
|
||||
async def delete_environment(
|
||||
id: str, config_manager: ConfigManager = Depends(get_config_manager)
|
||||
@@ -300,8 +300,8 @@ async def delete_environment(
|
||||
|
||||
# #region test_environment_connection [C:2] [TYPE Function]
|
||||
# @BRIEF Tests the connection to a Superset environment.
|
||||
# @PRE: ID is provided.
|
||||
# @POST: Returns success or error status.
|
||||
# @PRE ID is provided.
|
||||
# @POST Returns success or error status.
|
||||
@router.post("/environments/{id}/test")
|
||||
async def test_environment_connection(
|
||||
id: str, config_manager: ConfigManager = Depends(get_config_manager)
|
||||
@@ -333,8 +333,8 @@ async def test_environment_connection(
|
||||
|
||||
# #region get_logging_config [C:2] [TYPE Function]
|
||||
# @BRIEF Retrieves current logging configuration.
|
||||
# @PRE: Config manager is available.
|
||||
# @POST: Returns logging configuration.
|
||||
# @PRE Config manager is available.
|
||||
# @POST Returns logging configuration.
|
||||
@router.get("/logging", response_model=LoggingConfigResponse)
|
||||
async def get_logging_config(
|
||||
config_manager: ConfigManager = Depends(get_config_manager),
|
||||
@@ -354,8 +354,8 @@ async def get_logging_config(
|
||||
|
||||
# #region update_logging_config [C:2] [TYPE Function]
|
||||
# @BRIEF Updates logging configuration.
|
||||
# @PRE: New logging config is provided.
|
||||
# @POST: Logging configuration is updated and saved.
|
||||
# @PRE New logging config is provided.
|
||||
# @POST Logging configuration is updated and saved.
|
||||
@router.patch("/logging", response_model=LoggingConfigResponse)
|
||||
async def update_logging_config(
|
||||
config: LoggingConfig,
|
||||
@@ -401,10 +401,10 @@ class ConsolidatedSettingsResponse(BaseModel):
|
||||
|
||||
# #region get_consolidated_settings [C:4] [TYPE Function]
|
||||
# @BRIEF Retrieves all settings categories in a single call.
|
||||
# @PRE: Config manager is available and the caller holds admin settings read permission.
|
||||
# @POST: Returns consolidated settings, provider metadata, and persisted notification payload in one stable response.
|
||||
# @SIDE_EFFECT: Opens one database session to read LLM providers and config-backed notification payload, then closes it.
|
||||
# @DATA_CONTRACT: Input[ConfigManager] -> Output[ConsolidatedSettingsResponse]
|
||||
# @PRE Config manager is available and the caller holds admin settings read permission.
|
||||
# @POST Returns consolidated settings, provider metadata, and persisted notification payload in one stable response.
|
||||
# @SIDE_EFFECT Opens one database session to read LLM providers and config-backed notification payload, then closes it.
|
||||
# @DATA_CONTRACT Input[ConfigManager] -> Output[ConsolidatedSettingsResponse]
|
||||
# @RELATION DEPENDS_ON -> [ConfigManager]
|
||||
# @RELATION DEPENDS_ON -> [LLMProviderService]
|
||||
# @RELATION DEPENDS_ON -> [AppConfigRecord]
|
||||
@@ -481,8 +481,8 @@ async def get_consolidated_settings(
|
||||
|
||||
# #region update_consolidated_settings [C:2] [TYPE Function]
|
||||
# @BRIEF Bulk update application settings from the consolidated view.
|
||||
# @PRE: User has admin permissions, config is valid.
|
||||
# @POST: Settings are updated and saved via ConfigManager.
|
||||
# @PRE User has admin permissions, config is valid.
|
||||
# @POST Settings are updated and saved via ConfigManager.
|
||||
@router.patch("/consolidated")
|
||||
async def update_consolidated_settings(
|
||||
settings_patch: dict,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# #region storage_routes [C:5] [TYPE Module] [SEMANTICS fastapi, storage, api, upload, download]
|
||||
#
|
||||
# @BRIEF API endpoints for file storage management (backups and repositories).
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [StorageModels]
|
||||
# @RELATION DEPENDS_ON -> [StoragePlugin]
|
||||
#
|
||||
# @INVARIANT: All paths must be validated against path traversal.
|
||||
# @INVARIANT All paths must be validated against path traversal.
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
@@ -22,8 +22,8 @@ router = APIRouter(tags=["storage"])
|
||||
# #region list_files [C:3] [TYPE Function]
|
||||
# @BRIEF List all files and directories in the storage system.
|
||||
#
|
||||
# @PRE: None.
|
||||
# @POST: Returns a list of StoredFile objects.
|
||||
# @PRE None.
|
||||
# @POST Returns a list of StoredFile objects.
|
||||
#
|
||||
# @RELATION DEPENDS_ON -> [StoragePlugin]
|
||||
@router.get("/files", response_model=list[StoredFile])
|
||||
@@ -44,12 +44,12 @@ async def list_files(
|
||||
# #region upload_file [C:3] [TYPE Function]
|
||||
# @BRIEF Upload a file to the storage system.
|
||||
#
|
||||
# @PRE: category must be a valid FileCategory.
|
||||
# @PRE: file must be a valid UploadFile.
|
||||
# @POST: Returns the StoredFile object of the uploaded file.
|
||||
# @PRE category must be a valid FileCategory.
|
||||
# @PRE file must be a valid UploadFile.
|
||||
# @POST Returns the StoredFile object of the uploaded file.
|
||||
#
|
||||
#
|
||||
# @SIDE_EFFECT: Writes file to the filesystem.
|
||||
# @SIDE_EFFECT Writes file to the filesystem.
|
||||
#
|
||||
# @RELATION DEPENDS_ON -> [StoragePlugin]
|
||||
@router.post("/upload", response_model=StoredFile, status_code=201)
|
||||
@@ -73,11 +73,11 @@ async def upload_file(
|
||||
# #region delete_file [C:3] [TYPE Function]
|
||||
# @BRIEF Delete a specific file or directory.
|
||||
#
|
||||
# @PRE: category must be a valid FileCategory.
|
||||
# @POST: Item is removed from storage.
|
||||
# @PRE category must be a valid FileCategory.
|
||||
# @POST Item is removed from storage.
|
||||
#
|
||||
#
|
||||
# @SIDE_EFFECT: Deletes item from the filesystem.
|
||||
# @SIDE_EFFECT Deletes item from the filesystem.
|
||||
#
|
||||
# @RELATION DEPENDS_ON -> [StoragePlugin]
|
||||
@router.delete("/files/{category}/{path:path}", status_code=204)
|
||||
@@ -102,8 +102,8 @@ async def delete_file(
|
||||
# #region download_file [C:3] [TYPE Function]
|
||||
# @BRIEF Retrieve a file for download.
|
||||
#
|
||||
# @PRE: category must be a valid FileCategory.
|
||||
# @POST: Returns a FileResponse.
|
||||
# @PRE category must be a valid FileCategory.
|
||||
# @POST Returns a FileResponse.
|
||||
#
|
||||
#
|
||||
# @RELATION DEPENDS_ON -> [StoragePlugin]
|
||||
@@ -131,8 +131,8 @@ async def download_file(
|
||||
# #region get_file_by_path [C:3] [TYPE Function]
|
||||
# @BRIEF Retrieve a file by validated absolute/relative path under storage root.
|
||||
#
|
||||
# @PRE: path must resolve under configured storage root.
|
||||
# @POST: Returns a FileResponse for existing files.
|
||||
# @PRE path must resolve under configured storage root.
|
||||
# @POST Returns a FileResponse for existing files.
|
||||
#
|
||||
#
|
||||
# @RELATION DEPENDS_ON -> [StoragePlugin]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# #region TasksRouter [C:3] [TYPE Module] [SEMANTICS fastapi, task, api, search, create-task-request, resolve-task-request]
|
||||
# @BRIEF Defines the FastAPI router for task-related endpoints, allowing clients to create, list, and get the status of tasks.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [TaskManager]
|
||||
# @RELATION DEPENDS_ON -> [ConfigManager]
|
||||
# @RELATION DEPENDS_ON -> [LLMProviderService]
|
||||
@@ -49,8 +49,8 @@ class ResumeTaskRequest(BaseModel):
|
||||
|
||||
# #region create_task [C:3] [TYPE Function]
|
||||
# @BRIEF Create and start a new task for a given plugin.
|
||||
# @PRE: plugin_id must exist and params must be valid for that plugin.
|
||||
# @POST: A new task is created and started.
|
||||
# @PRE plugin_id must exist and params must be valid for that plugin.
|
||||
# @POST A new task is created and started.
|
||||
# @RELATION CALLS -> [TaskManager]
|
||||
# @RELATION DEPENDS_ON -> [ConfigManager]
|
||||
# @RELATION DEPENDS_ON -> [LLMProviderService]
|
||||
@@ -125,10 +125,10 @@ async def create_task(
|
||||
|
||||
# #region list_tasks [C:2] [TYPE Function]
|
||||
# @BRIEF Retrieve a list of tasks with pagination and optional status filter.
|
||||
# @PRE: task_manager must be available.
|
||||
# @POST: Returns a list of tasks.
|
||||
# @PRE task_manager must be available.
|
||||
# @POST Returns a list of tasks.
|
||||
# @RELATION CALLS -> [TaskManager]
|
||||
# @RELATION BINDS_TO -> [TASK_TYPE_PLUGIN_MAP]
|
||||
# @RELATION BINDS_TO -> [EXT:method:TASK_TYPE_PLUGIN_MAP]
|
||||
@router.get("", response_model=list[Task])
|
||||
async def list_tasks(
|
||||
limit: int = 10,
|
||||
@@ -170,8 +170,8 @@ async def list_tasks(
|
||||
|
||||
# #region get_task [C:2] [TYPE Function]
|
||||
# @BRIEF Retrieve the details of a specific task.
|
||||
# @PRE: task_id must exist.
|
||||
# @POST: Returns task details or raises 404.
|
||||
# @PRE task_id must exist.
|
||||
# @POST Returns task details or raises 404.
|
||||
# @RELATION CALLS -> [TaskManager]
|
||||
@router.get("/{task_id}", response_model=Task)
|
||||
async def get_task(
|
||||
@@ -193,17 +193,17 @@ async def get_task(
|
||||
|
||||
# #region get_task_logs [C:5] [TYPE Function]
|
||||
# @BRIEF Retrieve logs for a specific task with optional filtering.
|
||||
# @PRE: task_id must exist.
|
||||
# @POST: Returns a list of log entries or raises 404.
|
||||
# @PRE task_id must exist.
|
||||
# @POST Returns a list of log entries or raises 404.
|
||||
# @RELATION CALLS -> [TaskManager]
|
||||
# @RELATION DEPENDS_ON -> [LogFilter]
|
||||
# @TEST_CONTRACT: TaskLogQueryInput -> List[LogEntry]
|
||||
# @TEST_SCENARIO: existing_task_logs_filtered -> Returns filtered logs by level/source/search with pagination.
|
||||
# @TEST_FIXTURE: valid_task_with_mixed_logs -> backend/tests/fixtures/task_logs/valid_task_with_mixed_logs.json
|
||||
# @TEST_EDGE: missing_task -> Unknown task_id returns 404 Task not found.
|
||||
# @TEST_EDGE: invalid_level_type -> Non-string/invalid level query rejected by validation or yields empty result.
|
||||
# @TEST_EDGE: pagination_bounds -> offset=0 and limit=1000 remain within API bounds and do not overflow.
|
||||
# @TEST_INVARIANT: logs_only_for_existing_task -> VERIFIED_BY: [existing_task_logs_filtered, missing_task]
|
||||
# @TEST_CONTRACT TaskLogQueryInput -> List[LogEntry]
|
||||
# @TEST_SCENARIO existing_task_logs_filtered -> Returns filtered logs by level/source/search with pagination.
|
||||
# @TEST_FIXTURE valid_task_with_mixed_logs -> backend/tests/fixtures/task_logs/valid_task_with_mixed_logs.json
|
||||
# @TEST_EDGE missing_task -> Unknown task_id returns 404 Task not found.
|
||||
# @TEST_EDGE invalid_level_type -> Non-string/invalid level query rejected by validation or yields empty result.
|
||||
# @TEST_EDGE pagination_bounds -> offset=0 and limit=1000 remain within API bounds and do not overflow.
|
||||
# @TEST_INVARIANT logs_only_for_existing_task -> VERIFIED_BY: [existing_task_logs_filtered, missing_task]
|
||||
@router.get("/{task_id}/logs")
|
||||
async def get_task_logs(
|
||||
task_id: str,
|
||||
@@ -240,8 +240,8 @@ async def get_task_logs(
|
||||
|
||||
# #region get_task_log_stats [C:2] [TYPE Function]
|
||||
# @BRIEF Get statistics about logs for a task (counts by level and source).
|
||||
# @PRE: task_id must exist.
|
||||
# @POST: Returns log statistics or raises 404.
|
||||
# @PRE task_id must exist.
|
||||
# @POST Returns log statistics or raises 404.
|
||||
# @RELATION CALLS -> [TaskManager]
|
||||
# @RELATION DEPENDS_ON -> [LogStats]
|
||||
@router.get("/{task_id}/logs/stats", response_model=LogStats)
|
||||
@@ -285,8 +285,8 @@ async def get_task_log_stats(
|
||||
|
||||
# #region get_task_log_sources [C:2] [TYPE Function]
|
||||
# @BRIEF Get unique sources for a task's logs.
|
||||
# @PRE: task_id must exist.
|
||||
# @POST: Returns list of unique source names or raises 404.
|
||||
# @PRE task_id must exist.
|
||||
# @POST Returns list of unique source names or raises 404.
|
||||
# @RELATION CALLS -> [TaskManager]
|
||||
@router.get("/{task_id}/logs/sources", response_model=list[str])
|
||||
async def get_task_log_sources(
|
||||
@@ -307,8 +307,8 @@ async def get_task_log_sources(
|
||||
|
||||
# #region resolve_task [C:2] [TYPE Function]
|
||||
# @BRIEF Resolve a task that is awaiting mapping.
|
||||
# @PRE: task must be in AWAITING_MAPPING status.
|
||||
# @POST: Task is resolved and resumes execution.
|
||||
# @PRE task must be in AWAITING_MAPPING status.
|
||||
# @POST Task is resolved and resumes execution.
|
||||
# @RELATION CALLS -> [TaskManager]
|
||||
@router.post("/{task_id}/resolve", response_model=Task)
|
||||
async def resolve_task(
|
||||
@@ -330,8 +330,8 @@ async def resolve_task(
|
||||
|
||||
# #region resume_task [C:2] [TYPE Function]
|
||||
# @BRIEF Resume a task that is awaiting input (e.g., passwords).
|
||||
# @PRE: task must be in AWAITING_INPUT status.
|
||||
# @POST: Task resumes execution with provided input.
|
||||
# @PRE task must be in AWAITING_INPUT status.
|
||||
# @POST Task resumes execution with provided input.
|
||||
# @RELATION CALLS -> [TaskManager]
|
||||
@router.post("/{task_id}/resume", response_model=Task)
|
||||
async def resume_task(
|
||||
@@ -353,8 +353,8 @@ async def resume_task(
|
||||
|
||||
# #region clear_tasks [C:2] [TYPE Function]
|
||||
# @BRIEF Clear tasks matching the status filter.
|
||||
# @PRE: task_manager is available.
|
||||
# @POST: Tasks are removed from memory/persistence.
|
||||
# @PRE task_manager is available.
|
||||
# @POST Tasks are removed from memory/persistence.
|
||||
# @RELATION CALLS -> [TaskManager]
|
||||
@router.delete("", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def clear_tasks(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# #region TranslateCorrectionRoutesModule [C:2] [TYPE Module] [SEMANTICS fastapi, translate, api, search]
|
||||
# @BRIEF Term correction submission endpoints.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# #region TranslateDictionaryRoutesModule [C:3] [TYPE Module] [SEMANTICS fastapi, translate, api, search]
|
||||
# @BRIEF Terminology Dictionary CRUD, entries management, and import routes.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
|
||||
from fastapi import Depends, HTTPException, Query, status
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -24,8 +24,8 @@ from ._router import router
|
||||
|
||||
# #region list_dictionaries [C:4] [TYPE Function]
|
||||
# @BRIEF List all terminology dictionaries.
|
||||
# @PRE: User has translate.dictionary.view permission.
|
||||
# @POST: Returns list of dictionaries with total count.
|
||||
# @PRE User has translate.dictionary.view permission.
|
||||
# @POST Returns list of dictionaries with total count.
|
||||
@router.get("/dictionaries")
|
||||
async def list_dictionaries(
|
||||
page: int = Query(1, ge=1),
|
||||
@@ -48,8 +48,8 @@ async def list_dictionaries(
|
||||
|
||||
# #region get_dictionary [C:4] [TYPE Function]
|
||||
# @BRIEF Get a single terminology dictionary by ID.
|
||||
# @PRE: User has translate.dictionary.view permission.
|
||||
# @POST: Returns the dictionary with entry_count.
|
||||
# @PRE User has translate.dictionary.view permission.
|
||||
# @POST Returns the dictionary with entry_count.
|
||||
@router.get("/dictionaries/{dictionary_id}")
|
||||
async def get_dictionary(
|
||||
dictionary_id: str,
|
||||
@@ -73,8 +73,8 @@ async def get_dictionary(
|
||||
|
||||
# #region create_dictionary [C:4] [TYPE Function]
|
||||
# @BRIEF Create a new terminology dictionary.
|
||||
# @PRE: User has translate.dictionary.create permission.
|
||||
# @POST: Returns the created dictionary.
|
||||
# @PRE User has translate.dictionary.create permission.
|
||||
# @POST Returns the created dictionary.
|
||||
@router.post("/dictionaries", status_code=status.HTTP_201_CREATED)
|
||||
async def create_dictionary(
|
||||
payload: DictionaryCreate,
|
||||
@@ -101,8 +101,8 @@ async def create_dictionary(
|
||||
|
||||
# #region update_dictionary [C:4] [TYPE Function]
|
||||
# @BRIEF Update an existing terminology dictionary.
|
||||
# @PRE: User has translate.dictionary.edit permission.
|
||||
# @POST: Returns the updated dictionary.
|
||||
# @PRE User has translate.dictionary.edit permission.
|
||||
# @POST Returns the updated dictionary.
|
||||
@router.put("/dictionaries/{dictionary_id}")
|
||||
async def update_dictionary(
|
||||
dictionary_id: str,
|
||||
@@ -135,8 +135,8 @@ async def update_dictionary(
|
||||
|
||||
# #region delete_dictionary [C:4] [TYPE Function]
|
||||
# @BRIEF Delete a terminology dictionary, blocked if attached to active/scheduled jobs.
|
||||
# @PRE: User has translate.dictionary.delete permission.
|
||||
# @POST: Dictionary is deleted.
|
||||
# @PRE User has translate.dictionary.delete permission.
|
||||
# @POST Dictionary is deleted.
|
||||
@router.delete("/dictionaries/{dictionary_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_dictionary(
|
||||
dictionary_id: str,
|
||||
@@ -164,8 +164,8 @@ async def delete_dictionary(
|
||||
|
||||
# #region list_dictionary_entries [C:4] [TYPE Function]
|
||||
# @BRIEF List entries for a dictionary, optionally filtered by language pair.
|
||||
# @PRE: User has translate.dictionary.view permission.
|
||||
# @POST: Returns paginated list of entries with language pair fields.
|
||||
# @PRE User has translate.dictionary.view permission.
|
||||
# @POST Returns paginated list of entries with language pair fields.
|
||||
@router.get("/dictionaries/{dictionary_id}/entries")
|
||||
async def list_dictionary_entries(
|
||||
dictionary_id: str,
|
||||
@@ -222,8 +222,8 @@ async def list_dictionary_entries(
|
||||
|
||||
# #region add_dictionary_entry [C:4] [TYPE Function]
|
||||
# @BRIEF Add a new entry to a dictionary.
|
||||
# @PRE: User has translate.dictionary.edit permission.
|
||||
# @POST: Entry is created.
|
||||
# @PRE User has translate.dictionary.edit permission.
|
||||
# @POST Entry is created.
|
||||
@router.post("/dictionaries/{dictionary_id}/entries", status_code=status.HTTP_201_CREATED)
|
||||
async def add_dictionary_entry(
|
||||
dictionary_id: str,
|
||||
@@ -271,8 +271,8 @@ async def add_dictionary_entry(
|
||||
|
||||
# #region edit_dictionary_entry [C:4] [TYPE Function]
|
||||
# @BRIEF Update an existing dictionary entry.
|
||||
# @PRE: User has translate.dictionary.edit permission.
|
||||
# @POST: Entry is updated.
|
||||
# @PRE User has translate.dictionary.edit permission.
|
||||
# @POST Entry is updated.
|
||||
@router.put("/dictionaries/{dictionary_id}/entries/{entry_id}")
|
||||
async def edit_dictionary_entry(
|
||||
dictionary_id: str,
|
||||
@@ -321,8 +321,8 @@ async def edit_dictionary_entry(
|
||||
|
||||
# #region delete_dictionary_entry [C:4] [TYPE Function]
|
||||
# @BRIEF Delete a dictionary entry.
|
||||
# @PRE: User has translate.dictionary.edit permission.
|
||||
# @POST: Entry is deleted.
|
||||
# @PRE User has translate.dictionary.edit permission.
|
||||
# @POST Entry is deleted.
|
||||
@router.delete("/dictionaries/{dictionary_id}/entries/{entry_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_dictionary_entry(
|
||||
dictionary_id: str,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# #region TranslateHelpersModule [C:2] [TYPE Module] [SEMANTICS sqlalchemy, translate, helper, query, mapping]
|
||||
# @BRIEF Shared helper functions for translate route handlers.
|
||||
# @LAYER API
|
||||
# @RELATION DEPENDS_ON -> [models.translate]
|
||||
# @RELATION DEPENDS_ON -> [EXT:frontend:models.translate]
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# #region TranslateJobRoutesModule [C:3] [TYPE Module] [SEMANTICS fastapi, translate, api, search]
|
||||
# @BRIEF Translation Job CRUD and datasource column routes.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
|
||||
|
||||
from fastapi import Depends, HTTPException, Query, status
|
||||
@@ -34,8 +34,8 @@ from ._router import router
|
||||
|
||||
# #region list_jobs [C:4] [TYPE Function]
|
||||
# @BRIEF List all translation jobs.
|
||||
# @PRE: User has translate.job.view permission.
|
||||
# @POST: Returns list of translation jobs.
|
||||
# @PRE User has translate.job.view permission.
|
||||
# @POST Returns list of translation jobs.
|
||||
@router.get("/jobs", response_model=list[TranslateJobResponse])
|
||||
async def list_jobs(
|
||||
page: int = Query(1, ge=1),
|
||||
@@ -67,8 +67,8 @@ async def list_jobs(
|
||||
|
||||
# #region get_job [C:4] [TYPE Function]
|
||||
# @BRIEF Get a single translation job by ID.
|
||||
# @PRE: User has translate.job.view permission.
|
||||
# @POST: Returns the translation job.
|
||||
# @PRE User has translate.job.view permission.
|
||||
# @POST Returns the translation job.
|
||||
@router.get("/jobs/{job_id}", response_model=TranslateJobResponse)
|
||||
async def get_job(
|
||||
job_id: str,
|
||||
@@ -91,9 +91,9 @@ async def get_job(
|
||||
|
||||
# #region create_job [C:4] [TYPE Function]
|
||||
# @BRIEF Create a new translation job.
|
||||
# @PRE: User has translate.job.create permission.
|
||||
# @POST: Returns the created translation job.
|
||||
# @SIDE_EFFECT: Validates columns via SupersetClient; caches database_dialect.
|
||||
# @PRE User has translate.job.create permission.
|
||||
# @POST Returns the created translation job.
|
||||
# @SIDE_EFFECT Validates columns via SupersetClient; caches database_dialect.
|
||||
@router.post("/jobs", response_model=TranslateJobResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_job(
|
||||
payload: TranslateJobCreate,
|
||||
@@ -116,9 +116,9 @@ async def create_job(
|
||||
|
||||
# #region update_job [C:4] [TYPE Function]
|
||||
# @BRIEF Update an existing translation job.
|
||||
# @PRE: User has translate.job.edit permission.
|
||||
# @POST: Returns the updated translation job.
|
||||
# @SIDE_EFFECT: Re-detects database_dialect if datasource changed.
|
||||
# @PRE User has translate.job.edit permission.
|
||||
# @POST Returns the updated translation job.
|
||||
# @SIDE_EFFECT Re-detects database_dialect if datasource changed.
|
||||
@router.put("/jobs/{job_id}", response_model=TranslateJobResponse)
|
||||
async def update_job(
|
||||
job_id: str,
|
||||
@@ -142,8 +142,8 @@ async def update_job(
|
||||
|
||||
# #region delete_job [C:4] [TYPE Function]
|
||||
# @BRIEF Delete a translation job.
|
||||
# @PRE: User has translate.job.delete permission.
|
||||
# @POST: Job is deleted.
|
||||
# @PRE User has translate.job.delete permission.
|
||||
# @POST Job is deleted.
|
||||
@router.delete("/jobs/{job_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_job(
|
||||
job_id: str,
|
||||
@@ -164,8 +164,8 @@ async def delete_job(
|
||||
|
||||
# #region duplicate_job [C:4] [TYPE Function]
|
||||
# @BRIEF Duplicate a translation job.
|
||||
# @PRE: User has translate.job.create permission.
|
||||
# @POST: Returns the duplicated job with status DRAFT.
|
||||
# @PRE User has translate.job.create permission.
|
||||
# @POST Returns the duplicated job with status DRAFT.
|
||||
@router.post("/jobs/{job_id}/duplicate", response_model=DuplicateJobResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def duplicate_job(
|
||||
job_id: str,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# #region TranslateMetricsRoutesModule [C:2] [TYPE Module] [SEMANTICS fastapi, translate, api, search]
|
||||
# @BRIEF Translation Metrics endpoints.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
|
||||
|
||||
from fastapi import Depends, HTTPException, Query, status
|
||||
@@ -19,8 +19,8 @@ from ._router import router
|
||||
|
||||
# #region get_metrics [C:4] [TYPE Function]
|
||||
# @BRIEF Get translation metrics, optionally filtered by job.
|
||||
# @PRE: User has translate.metrics.view permission.
|
||||
# @POST: Returns metrics data.
|
||||
# @PRE User has translate.metrics.view permission.
|
||||
# @POST Returns metrics data.
|
||||
@router.get("/metrics")
|
||||
async def get_metrics(
|
||||
job_id: str | None = Query(None),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# #region TranslatePreviewRoutesModule [C:3] [TYPE Module] [SEMANTICS fastapi, translate, preview, review, api, search]
|
||||
# @BRIEF Translation Preview session management routes.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
|
||||
|
||||
from fastapi import Depends, HTTPException, status
|
||||
@@ -25,9 +25,9 @@ from ._router import router
|
||||
|
||||
# #region preview_translation [C:4] [TYPE Function]
|
||||
# @BRIEF Preview a translation before applying it.
|
||||
# @PRE: User has translate.job.execute permission.
|
||||
# @POST: Returns a preview session with records and cost estimation.
|
||||
# @SIDE_EFFECT: Fetches sample data from Superset; calls LLM provider; creates DB rows.
|
||||
# @PRE User has translate.job.execute permission.
|
||||
# @POST Returns a preview session with records and cost estimation.
|
||||
# @SIDE_EFFECT Fetches sample data from Superset; calls LLM provider; creates DB rows.
|
||||
@router.post("/jobs/{job_id}/preview", status_code=status.HTTP_201_CREATED)
|
||||
async def preview_translation(
|
||||
job_id: str,
|
||||
@@ -61,8 +61,8 @@ async def preview_translation(
|
||||
|
||||
# #region update_preview_row [C:4] [TYPE Function]
|
||||
# @BRIEF Approve, edit, or reject a preview row (optionally per language).
|
||||
# @PRE: User has translate.job.execute permission.
|
||||
# @POST: Preview row status is updated.
|
||||
# @PRE User has translate.job.execute permission.
|
||||
# @POST Preview row status is updated.
|
||||
@router.put("/jobs/{job_id}/preview/rows/{row_key}")
|
||||
async def update_preview_row(
|
||||
job_id: str,
|
||||
@@ -93,8 +93,8 @@ async def update_preview_row(
|
||||
|
||||
# #region accept_preview_session [C:4] [TYPE Function]
|
||||
# @BRIEF Accept a preview session, marking it as the quality gate for full execution.
|
||||
# @PRE: User has translate.job.execute permission. Job has an ACTIVE preview session.
|
||||
# @POST: Preview session is marked as APPLIED; full execution can proceed.
|
||||
# @PRE User has translate.job.execute permission. Job has an ACTIVE preview session.
|
||||
# @POST Preview session is marked as APPLIED; full execution can proceed.
|
||||
@router.post("/jobs/{job_id}/preview/accept")
|
||||
async def accept_preview_session(
|
||||
job_id: str,
|
||||
@@ -116,8 +116,8 @@ async def accept_preview_session(
|
||||
|
||||
# #region apply_preview [C:4] [TYPE Function]
|
||||
# @BRIEF Apply a preview session (alias for accept when accepting at session level).
|
||||
# @PRE: User has translate.job.execute permission.
|
||||
# @POST: Preview is applied.
|
||||
# @PRE User has translate.job.execute permission.
|
||||
# @POST Preview is applied.
|
||||
@router.post("/preview/{session_id}/apply")
|
||||
async def apply_preview(
|
||||
session_id: str,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# #region TranslateRouterModule [C:1] [TYPE Module] [SEMANTICS fastapi, translate, api]
|
||||
# @BRIEF APIRouter instance for translate routes.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# #region TranslateRunListRoutesModule [C:3] [TYPE Module] [SEMANTICS fastapi, translate, api, download, search]
|
||||
# @BRIEF Translation Run listing, detail, and CSV download routes (cross-job).
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
|
||||
|
||||
from fastapi import Depends, HTTPException, Query, status
|
||||
@@ -21,8 +21,8 @@ from ._router import router
|
||||
|
||||
# #region list_runs [C:4] [TYPE Function]
|
||||
# @BRIEF List all runs with cross-job filtering and pagination.
|
||||
# @PRE: User has translate.history.view permission.
|
||||
# @POST: Returns paginated list of runs.
|
||||
# @PRE User has translate.history.view permission.
|
||||
# @POST Returns paginated list of runs.
|
||||
@router.get("/runs")
|
||||
async def list_runs(
|
||||
job_id: str | None = Query(None),
|
||||
@@ -128,8 +128,8 @@ async def list_runs(
|
||||
|
||||
# #region get_run_detail [C:4] [TYPE Function]
|
||||
# @BRIEF Get detailed run info with config_snapshot, records, events.
|
||||
# @PRE: User has translate.history.view permission.
|
||||
# @POST: Returns run detail with records and events.
|
||||
# @PRE User has translate.history.view permission.
|
||||
# @POST Returns run detail with records and events.
|
||||
@router.get("/runs/{run_id}/detail")
|
||||
async def get_run_detail(
|
||||
run_id: str,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# #region TranslateRunRoutesModule [C:3] [TYPE Module] [SEMANTICS fastapi, translate, api, search, execution, history]
|
||||
# @BRIEF Translation Run execution, history, status, records and batches routes.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
|
||||
from datetime import UTC, datetime
|
||||
|
||||
@@ -27,8 +27,8 @@ from ._router import router
|
||||
|
||||
# #region run_translation [C:4] [TYPE Function]
|
||||
# @BRIEF Execute a translation job (trigger a run).
|
||||
# @PRE: User has translate.job.execute permission.
|
||||
# @POST: Returns the created translation run.
|
||||
# @PRE User has translate.job.execute permission.
|
||||
# @POST Returns the created translation run.
|
||||
@router.post("/jobs/{job_id}/run", status_code=status.HTTP_201_CREATED)
|
||||
def run_translation(
|
||||
job_id: str,
|
||||
@@ -166,8 +166,8 @@ def run_translation(
|
||||
|
||||
# #region retry_run [C:4] [TYPE Function]
|
||||
# @BRIEF Retry failed batches in a translation run.
|
||||
# @PRE: User has translate.job.execute permission.
|
||||
# @POST: Returns the updated translation run.
|
||||
# @PRE User has translate.job.execute permission.
|
||||
# @POST Returns the updated translation run.
|
||||
@router.post("/runs/{run_id}/retry")
|
||||
def retry_run(
|
||||
run_id: str,
|
||||
@@ -192,8 +192,8 @@ def retry_run(
|
||||
|
||||
# #region retry_insert [C:4] [TYPE Function]
|
||||
# @BRIEF Retry the SQL insert phase for a completed run.
|
||||
# @PRE: User has translate.job.execute permission.
|
||||
# @POST: Returns the updated run.
|
||||
# @PRE User has translate.job.execute permission.
|
||||
# @POST Returns the updated run.
|
||||
@router.post("/runs/{run_id}/retry-insert")
|
||||
def retry_insert(
|
||||
run_id: str,
|
||||
@@ -218,8 +218,8 @@ def retry_insert(
|
||||
|
||||
# #region cancel_run [C:4] [TYPE Function]
|
||||
# @BRIEF Cancel a running translation.
|
||||
# @PRE: User has translate.job.execute permission.
|
||||
# @POST: Run is cancelled.
|
||||
# @PRE User has translate.job.execute permission.
|
||||
# @POST Run is cancelled.
|
||||
@router.post("/runs/{run_id}/cancel")
|
||||
def cancel_run(
|
||||
run_id: str,
|
||||
@@ -241,8 +241,8 @@ def cancel_run(
|
||||
|
||||
# #region get_run_history [C:4] [TYPE Function]
|
||||
# @BRIEF Get run history for a translation job.
|
||||
# @PRE: User has translate.history.view permission.
|
||||
# @POST: Returns list of runs.
|
||||
# @PRE User has translate.history.view permission.
|
||||
# @POST Returns list of runs.
|
||||
@router.get("/jobs/{job_id}/runs")
|
||||
def get_run_history(
|
||||
job_id: str,
|
||||
@@ -266,8 +266,8 @@ def get_run_history(
|
||||
|
||||
# #region get_run_status [C:4] [TYPE Function]
|
||||
# @BRIEF Get status and statistics for a translation run.
|
||||
# @PRE: User has translate.history.view permission.
|
||||
# @POST: Returns run details with statistics.
|
||||
# @PRE User has translate.history.view permission.
|
||||
# @POST Returns run details with statistics.
|
||||
@router.get("/runs/{run_id}")
|
||||
def get_run_status(
|
||||
run_id: str,
|
||||
@@ -288,8 +288,8 @@ def get_run_status(
|
||||
|
||||
# #region get_run_records [C:4] [TYPE Function]
|
||||
# @BRIEF Get paginated records for a translation run.
|
||||
# @PRE: User has translate.history.view permission.
|
||||
# @POST: Returns paginated records.
|
||||
# @PRE User has translate.history.view permission.
|
||||
# @POST Returns paginated records.
|
||||
@router.get("/runs/{run_id}/records")
|
||||
def get_run_records(
|
||||
run_id: str,
|
||||
@@ -317,8 +317,8 @@ def get_run_records(
|
||||
|
||||
# #region get_batches [C:4] [TYPE Function]
|
||||
# @BRIEF Get batches for a translation run.
|
||||
# @PRE: User has translate.job.view permission.
|
||||
# @POST: Returns list of batches.
|
||||
# @PRE User has translate.job.view permission.
|
||||
# @POST Returns list of batches.
|
||||
@router.get("/runs/{run_id}/batches")
|
||||
def get_batches(
|
||||
run_id: str,
|
||||
@@ -362,9 +362,9 @@ def get_batches(
|
||||
|
||||
# #region override_detected_language [C:4] [TYPE Function]
|
||||
# @BRIEF Manually override the detected source language for a specific translation language entry.
|
||||
# @PRE: User has translate.job.execute permission. Run, record, and language entry exist.
|
||||
# @POST: TranslationLanguage.source_language_detected is updated; language_overridden is set to True.
|
||||
# @SIDE_EFFECT: DB write.
|
||||
# @PRE User has translate.job.execute permission. Run, record, and language entry exist.
|
||||
# @POST TranslationLanguage.source_language_detected is updated; language_overridden is set to True.
|
||||
# @SIDE_EFFECT DB write.
|
||||
@router.put("/runs/{run_id}/records/{record_id}/languages/{language_code}/override-language")
|
||||
def override_detected_language(
|
||||
run_id: str,
|
||||
@@ -441,8 +441,8 @@ def override_detected_language(
|
||||
|
||||
# #region inline_edit_translation [C:4] [TYPE Function] [SEMANTICS api,translate,correction]
|
||||
# @BRIEF Apply an inline correction to a translated value on a completed run result.
|
||||
# @PRE: User has translate.job.execute permission. Run, record, and language entry exist.
|
||||
# @POST: TranslationLanguage.final_value and user_edit are updated. Optional dictionary submission.
|
||||
# @PRE User has translate.job.execute permission. Run, record, and language entry exist.
|
||||
# @POST TranslationLanguage.final_value and user_edit are updated. Optional dictionary submission.
|
||||
@router.put("/runs/{run_id}/records/{record_id}/languages/{language_code}")
|
||||
def inline_edit_translation(
|
||||
run_id: str,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# #region TranslateScheduleRoutesModule [C:3] [TYPE Module] [SEMANTICS fastapi, translate, api, schedule, search]
|
||||
# @BRIEF Translation Schedule management routes.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
|
||||
from fastapi import Depends, HTTPException, Query, status
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -20,8 +20,8 @@ from ._router import router
|
||||
|
||||
# #region get_schedule [C:4] [TYPE Function]
|
||||
# @BRIEF Get the schedule for a translation job.
|
||||
# @PRE: User has translate.schedule.view permission.
|
||||
# @POST: Returns the schedule configuration.
|
||||
# @PRE User has translate.schedule.view permission.
|
||||
# @POST Returns the schedule configuration.
|
||||
@router.get("/jobs/{job_id}/schedule")
|
||||
async def get_schedule(
|
||||
job_id: str,
|
||||
@@ -55,8 +55,8 @@ async def get_schedule(
|
||||
|
||||
# #region set_schedule [C:4] [TYPE Function]
|
||||
# @BRIEF Set or update the schedule for a translation job.
|
||||
# @PRE: User has translate.schedule.manage permission.
|
||||
# @POST: Schedule is created or updated.
|
||||
# @PRE User has translate.schedule.manage permission.
|
||||
# @POST Schedule is created or updated.
|
||||
@router.put("/jobs/{job_id}/schedule")
|
||||
async def set_schedule(
|
||||
job_id: str,
|
||||
@@ -121,8 +121,8 @@ async def set_schedule(
|
||||
|
||||
# #region enable_schedule [C:4] [TYPE Function]
|
||||
# @BRIEF Enable a schedule for a translation job.
|
||||
# @PRE: User has translate.schedule.manage permission.
|
||||
# @POST: Schedule is enabled.
|
||||
# @PRE User has translate.schedule.manage permission.
|
||||
# @POST Schedule is enabled.
|
||||
@router.post("/jobs/{job_id}/schedule/enable")
|
||||
async def enable_schedule(
|
||||
job_id: str,
|
||||
@@ -153,8 +153,8 @@ async def enable_schedule(
|
||||
|
||||
# #region disable_schedule [C:4] [TYPE Function]
|
||||
# @BRIEF Disable a schedule for a translation job.
|
||||
# @PRE: User has translate.schedule.manage permission.
|
||||
# @POST: Schedule is disabled.
|
||||
# @PRE User has translate.schedule.manage permission.
|
||||
# @POST Schedule is disabled.
|
||||
@router.post("/jobs/{job_id}/schedule/disable")
|
||||
async def disable_schedule(
|
||||
job_id: str,
|
||||
@@ -179,8 +179,8 @@ async def disable_schedule(
|
||||
|
||||
# #region delete_schedule [C:4] [TYPE Function]
|
||||
# @BRIEF Delete the schedule for a translation job.
|
||||
# @PRE: User has translate.schedule.manage permission.
|
||||
# @POST: Schedule is removed.
|
||||
# @PRE User has translate.schedule.manage permission.
|
||||
# @POST Schedule is removed.
|
||||
@router.delete("/jobs/{job_id}/schedule", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_schedule(
|
||||
job_id: str,
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
# #region ValidationApiTests [C:3] [TYPE Module] [SEMANTICS validation, api, tests, pagination, crud]
|
||||
# @BRIEF Unit tests for validation task CRUD and run history API endpoints.
|
||||
# @LAYER: API
|
||||
# @LAYER API
|
||||
# @RELATION BINDS_TO -> [ValidationRoutes]
|
||||
# @RELATION DEPENDS_ON -> [EXT:FastAPI:TestClient]
|
||||
# @TEST_CONTRACT: [ValidationTaskCreate|Update|RunRequest] -> [ValidationTaskResponse|RunResponse|TriggerRunResponse]
|
||||
# @TEST_SCENARIO: list_tasks -> 200 with paginated tasks; filter by is_active/environment_id
|
||||
# @TEST_SCENARIO: create_task -> 201 with valid payload; 422 with invalid provider/environment
|
||||
# @TEST_SCENARIO: get_task -> 200 with task+recent_runs; 404 for nonexistent
|
||||
# @TEST_SCENARIO: update_task -> 200 with updated fields; 404 for nonexistent
|
||||
# @TEST_SCENARIO: delete_task -> 204 on success; 404 for nonexistent
|
||||
# @TEST_SCENARIO: trigger_run -> 200 with spawned_task_id; 422 for invalid task
|
||||
# @TEST_SCENARIO: list_runs -> 200 with 6 filters + pagination
|
||||
# @TEST_SCENARIO: get_run_detail -> 200 with issues/raw_response; 404 for nonexistent
|
||||
# @TEST_SCENARIO: delete_run -> 204 on success; 404 for nonexistent
|
||||
# @TEST_EDGE: missing_field -> 422 when required field omitted from create payload
|
||||
# @TEST_EDGE: invalid_type -> 422 when provider_id is not multimodal
|
||||
# @TEST_EDGE: external_fail -> 404/422 when task/run does not exist
|
||||
# @TEST_EDGE: delete_task_with_runs -> 204 with delete_runs=true flag
|
||||
# @INVARIANT: Every endpoint requires authentication via has_permission
|
||||
# @INVARIANT: All list endpoints support pagination (page/page_size) with defaults
|
||||
# @INVARIANT: provider_id must reference a multimodal LLM provider for creation
|
||||
# @TEST_CONTRACT [ValidationTaskCreate|Update|RunRequest] -> [ValidationTaskResponse|RunResponse|TriggerRunResponse]
|
||||
# @TEST_SCENARIO list_tasks -> 200 with paginated tasks; filter by is_active/environment_id
|
||||
# @TEST_SCENARIO create_task -> 201 with valid payload; 422 with invalid provider/environment
|
||||
# @TEST_SCENARIO get_task -> 200 with task+recent_runs; 404 for nonexistent
|
||||
# @TEST_SCENARIO update_task -> 200 with updated fields; 404 for nonexistent
|
||||
# @TEST_SCENARIO delete_task -> 204 on success; 404 for nonexistent
|
||||
# @TEST_SCENARIO trigger_run -> 200 with spawned_task_id; 422 for invalid task
|
||||
# @TEST_SCENARIO list_runs -> 200 with 6 filters + pagination
|
||||
# @TEST_SCENARIO get_run_detail -> 200 with issues/raw_response; 404 for nonexistent
|
||||
# @TEST_SCENARIO delete_run -> 204 on success; 404 for nonexistent
|
||||
# @TEST_EDGE missing_field -> 422 when required field omitted from create payload
|
||||
# @TEST_EDGE invalid_type -> 422 when provider_id is not multimodal
|
||||
# @TEST_EDGE external_fail -> 404/422 when task/run does not exist
|
||||
# @TEST_EDGE delete_task_with_runs -> 204 with delete_runs=true flag
|
||||
# @INVARIANT Every endpoint requires authentication via has_permission
|
||||
# @INVARIANT All list endpoints support pagination (page/page_size) with defaults
|
||||
# @INVARIANT provider_id must reference a multimodal LLM provider for creation
|
||||
|
||||
# Set required env vars before ANY app imports — crash-early guard for AuthConfig()
|
||||
import os
|
||||
|
||||
@@ -69,7 +69,7 @@ app = FastAPI(
|
||||
# #endregion FastAPI_App
|
||||
# #region ensure_initial_admin_user [C:3] [TYPE Function]
|
||||
# @BRIEF Ensures initial admin user exists when bootstrap env flags are enabled.
|
||||
# @RELATION DEPENDS_ON -> AuthRepository
|
||||
# @RELATION DEPENDS_ON -> [AuthRepository]
|
||||
def ensure_initial_admin_user() -> None:
|
||||
raw_flag = os.getenv("INITIAL_ADMIN_CREATE", "false").strip().lower()
|
||||
if raw_flag not in {"1", "true", "yes", "on"}:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# #region TestConfigManagerCompat [TYPE Module] [C:3] [SEMANTICS config-manager, compatibility, payload, tests]
|
||||
# @BRIEF Verifies ConfigManager compatibility wrappers preserve legacy payload sections.
|
||||
# @LAYER: Domain
|
||||
# @RELATION VERIFIES -> ConfigManager
|
||||
# @LAYER Domain
|
||||
# @RELATION BINDS_TO -> ConfigManager
|
||||
from types import SimpleNamespace
|
||||
|
||||
from src.core.config_manager import ConfigManager
|
||||
@@ -58,17 +58,17 @@ def test_save_config_syncs_environment_records_for_fk_backed_flows():
|
||||
credentials_id="legacy-user",
|
||||
)
|
||||
# #region _FakeQuery [TYPE Class] [C:1]
|
||||
# @RELATION: BINDS_TO -> [test_save_config_syncs_environment_records_for_fk_backed_flows]
|
||||
# @RELATION BINDS_TO -> [test_save_config_syncs_environment_records_for_fk_backed_flows]
|
||||
# @PURPOSE: Minimal query stub returning hardcoded existing environment record list for sync tests.
|
||||
# @INVARIANT: all() always returns [existing_record]; no parameterization or filtering.
|
||||
# @INVARIANT all() always returns [existing_record]; no parameterization or filtering.
|
||||
class _FakeQuery:
|
||||
def all(self):
|
||||
return [existing_record]
|
||||
# #endregion _FakeQuery
|
||||
# #region _FakeSession [TYPE Class] [C:1]
|
||||
# @RELATION: BINDS_TO -> [test_save_config_syncs_environment_records_for_fk_backed_flows]
|
||||
# @RELATION BINDS_TO -> [test_save_config_syncs_environment_records_for_fk_backed_flows]
|
||||
# @PURPOSE: Minimal SQLAlchemy session stub that captures add/delete calls for environment sync assertions.
|
||||
# @INVARIANT: query() always returns _FakeQuery; no real DB interaction.
|
||||
# @INVARIANT query() always returns _FakeQuery; no real DB interaction.
|
||||
class _FakeSession:
|
||||
def query(self, model):
|
||||
return _FakeQuery()
|
||||
@@ -117,14 +117,14 @@ def test_save_config_syncs_deletions_to_persistence():
|
||||
credentials_id="legacy-user",
|
||||
)
|
||||
# #region _FakeQueryDel [TYPE Class] [C:1]
|
||||
# @RELATION: BINDS_TO -> [test_save_config_syncs_deletions_to_persistence]
|
||||
# @RELATION BINDS_TO -> [test_save_config_syncs_deletions_to_persistence]
|
||||
# @PURPOSE: Minimal query stub for deletion test.
|
||||
class _FakeQuery:
|
||||
def all(self):
|
||||
return [existing_record]
|
||||
# #endregion _FakeQueryDel
|
||||
# #region _FakeSessionDel [TYPE Class] [C:1]
|
||||
# @RELATION: BINDS_TO -> [test_save_config_syncs_deletions_to_persistence]
|
||||
# @RELATION BINDS_TO -> [test_save_config_syncs_deletions_to_persistence]
|
||||
# @PURPOSE: Minimal session stub that captures add/delete for deletion assertions.
|
||||
class _FakeSession:
|
||||
def query(self, model):
|
||||
@@ -170,9 +170,9 @@ def test_load_config_syncs_environment_records_from_existing_db_payload(monkeypa
|
||||
closed = {"value": False}
|
||||
committed = {"value": False}
|
||||
# #region _FakeSession [TYPE Class] [C:1]
|
||||
# @RELATION: BINDS_TO -> [test_load_config_syncs_environment_records_from_existing_db_payload]
|
||||
# @RELATION BINDS_TO -> [test_load_config_syncs_environment_records_from_existing_db_payload]
|
||||
# @PURPOSE: Minimal session stub tracking commit/close signals for config load lifecycle assertions.
|
||||
# @INVARIANT: No query or add semantics; only lifecycle signal tracking.
|
||||
# @INVARIANT No query or add semantics; only lifecycle signal tracking.
|
||||
class _FakeSession:
|
||||
def commit(self):
|
||||
committed["value"] = True
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# #region NativeFilterExtractionTests [TYPE Module] [C:3] [SEMANTICS tests, superset, native, filters, permalink, filter_state]
|
||||
# @BRIEF Verify native filter extraction from permalinks and native_filters_key URLs.
|
||||
# @LAYER: Domain
|
||||
# @RELATION [BINDS_TO] ->[SupersetClient]
|
||||
# @RELATION [BINDS_TO] ->[AsyncSupersetClient]
|
||||
# @RELATION [BINDS_TO] ->[FilterState, ParsedNativeFilters, ExtraFormDataMerge]
|
||||
# @LAYER Domain
|
||||
# @RELATION BINDS_TO ->[SupersetClient]
|
||||
# @RELATION BINDS_TO ->[AsyncSupersetClient]
|
||||
# @RELATION BINDS_TO ->[[EXT:list:FilterState_ParsedNativeFilters_ExtraFormDataMerge]]
|
||||
import json
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# #region SupersetPreviewPipelineTests [TYPE Module] [C:3] [SEMANTICS tests, superset, preview, chart_data, network, 404-mapping]
|
||||
# @BRIEF Verify explicit chart-data preview compilation and ensure non-dashboard 404 errors remain generic across sync and async clients.
|
||||
# @LAYER: Domain
|
||||
# @RELATION [BINDS_TO] ->[AsyncNetworkModule]
|
||||
# @LAYER Domain
|
||||
# @RELATION BINDS_TO -> [AsyncNetworkModule]
|
||||
import json
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# #region TestSupersetProfileLookup [TYPE Module] [C:3] [SEMANTICS tests, superset, profile, lookup, fallback, sorting]
|
||||
# @RELATION BELONGS_TO -> SrcRoot
|
||||
# @RELATION BINDS_TO -> SrcRoot
|
||||
# @BRIEF Verifies Superset profile lookup adapter payload normalization and fallback error precedence.
|
||||
# @LAYER: Domain
|
||||
# @LAYER Domain
|
||||
# [SECTION: IMPORTS]
|
||||
import json
|
||||
from pathlib import Path
|
||||
@@ -20,20 +20,20 @@ from src.core.utils.network import AuthenticationError, SupersetAPIError
|
||||
# #region _RecordingNetworkClient [TYPE Class] [C:2]
|
||||
# @RELATION BINDS_TO -> TestSupersetProfileLookup
|
||||
# @BRIEF Records request payloads and returns scripted responses for deterministic adapter tests.
|
||||
# @INVARIANT: Each request consumes one scripted response in call order and persists call metadata.
|
||||
# @INVARIANT Each request consumes one scripted response in call order and persists call metadata.
|
||||
class _RecordingNetworkClient:
|
||||
# #region __init__ [TYPE Function]
|
||||
# @PURPOSE: Initializes scripted network responses.
|
||||
# @PRE: scripted_responses is ordered per expected request sequence.
|
||||
# @POST: Instance stores response script and captures subsequent request calls.
|
||||
# @PRE scripted_responses is ordered per expected request sequence.
|
||||
# @POST Instance stores response script and captures subsequent request calls.
|
||||
def __init__(self, scripted_responses: list[Any]):
|
||||
self._scripted_responses = scripted_responses
|
||||
self.calls: list[dict[str, Any]] = []
|
||||
# #endregion __init__
|
||||
# #region request [TYPE Function]
|
||||
# @PURPOSE: Mimics APIClient.request while capturing call arguments.
|
||||
# @PRE: method and endpoint are provided.
|
||||
# @POST: Returns scripted response or raises scripted exception.
|
||||
# @PRE method and endpoint are provided.
|
||||
# @POST Returns scripted response or raises scripted exception.
|
||||
def request(
|
||||
self,
|
||||
method: str,
|
||||
@@ -58,8 +58,8 @@ class _RecordingNetworkClient:
|
||||
# #region test_get_users_page_sends_lowercase_order_direction [TYPE Function]
|
||||
# @RELATION BINDS_TO -> TestSupersetProfileLookup
|
||||
# @BRIEF Ensures adapter sends lowercase order_direction compatible with Superset rison schema.
|
||||
# @PRE: Adapter is initialized with recording network client.
|
||||
# @POST: First request query payload contains order_direction='asc' for asc sort.
|
||||
# @PRE Adapter is initialized with recording network client.
|
||||
# @POST First request query payload contains order_direction='asc' for asc sort.
|
||||
def test_get_users_page_sends_lowercase_order_direction():
|
||||
client = _RecordingNetworkClient(
|
||||
scripted_responses=[{"result": [{"username": "admin"}], "count": 1}]
|
||||
@@ -80,8 +80,8 @@ def test_get_users_page_sends_lowercase_order_direction():
|
||||
# #region test_get_users_page_preserves_primary_schema_error_over_fallback_auth_error [TYPE Function]
|
||||
# @RELATION BINDS_TO -> TestSupersetProfileLookup
|
||||
# @BRIEF Ensures fallback auth error does not mask primary schema/query failure.
|
||||
# @PRE: Primary endpoint fails with SupersetAPIError and fallback fails with AuthenticationError.
|
||||
# @POST: Raised exception remains primary SupersetAPIError (non-auth) to preserve root cause.
|
||||
# @PRE Primary endpoint fails with SupersetAPIError and fallback fails with AuthenticationError.
|
||||
# @POST Raised exception remains primary SupersetAPIError (non-auth) to preserve root cause.
|
||||
def test_get_users_page_preserves_primary_schema_error_over_fallback_auth_error():
|
||||
client = _RecordingNetworkClient(
|
||||
scripted_responses=[
|
||||
@@ -100,8 +100,8 @@ def test_get_users_page_preserves_primary_schema_error_over_fallback_auth_error(
|
||||
# #region test_get_users_page_uses_fallback_endpoint_when_primary_fails [TYPE Function]
|
||||
# @RELATION BINDS_TO -> TestSupersetProfileLookup
|
||||
# @BRIEF Verifies adapter retries second users endpoint and succeeds when fallback is healthy.
|
||||
# @PRE: Primary endpoint fails; fallback returns valid users payload.
|
||||
# @POST: Result status is success and both endpoints were attempted in order.
|
||||
# @PRE Primary endpoint fails; fallback returns valid users payload.
|
||||
# @POST Result status is success and both endpoints were attempted in order.
|
||||
def test_get_users_page_uses_fallback_endpoint_when_primary_fails():
|
||||
client = _RecordingNetworkClient(
|
||||
scripted_responses=[
|
||||
|
||||
@@ -4,10 +4,10 @@ from src.core.scheduler import ThrottledSchedulerConfigurator
|
||||
|
||||
|
||||
# #region test_throttled_scheduler [TYPE Module] [C:3] [SEMANTICS test, scheduler, throttle, unit]
|
||||
# @RELATION BELONGS_TO -> SrcRoot
|
||||
# @RELATION BINDS_TO -> SrcRoot
|
||||
# @BRIEF Unit tests for ThrottledSchedulerConfigurator distribution logic.
|
||||
# #region test_calculate_schedule_even_distribution [TYPE Function]
|
||||
# @RELATION BINDS_TO -> test_throttled_scheduler
|
||||
# @RELATION BINDS_TO -> [test_throttled_scheduler]
|
||||
# @BRIEF Validate even spacing across a two-hour scheduling window for three tasks.
|
||||
def test_calculate_schedule_even_distribution():
|
||||
"""
|
||||
@@ -26,7 +26,7 @@ def test_calculate_schedule_even_distribution():
|
||||
assert schedule[2] == datetime(2024, 1, 1, 3, 0)
|
||||
# #endregion test_calculate_schedule_even_distribution
|
||||
# #region test_calculate_schedule_midnight_crossing [TYPE Function]
|
||||
# @RELATION BINDS_TO -> test_throttled_scheduler
|
||||
# @RELATION BINDS_TO -> [test_throttled_scheduler]
|
||||
# @BRIEF Validate scheduler correctly rolls timestamps into the next day across midnight.
|
||||
def test_calculate_schedule_midnight_crossing():
|
||||
"""
|
||||
@@ -45,7 +45,7 @@ def test_calculate_schedule_midnight_crossing():
|
||||
assert schedule[2] == datetime(2024, 1, 2, 1, 0)
|
||||
# #endregion test_calculate_schedule_midnight_crossing
|
||||
# #region test_calculate_schedule_single_task [TYPE Function]
|
||||
# @RELATION BINDS_TO -> test_throttled_scheduler
|
||||
# @RELATION BINDS_TO -> [test_throttled_scheduler]
|
||||
# @BRIEF Validate single-task schedule returns only the window start timestamp.
|
||||
def test_calculate_schedule_single_task():
|
||||
"""
|
||||
@@ -62,7 +62,7 @@ def test_calculate_schedule_single_task():
|
||||
assert schedule[0] == datetime(2024, 1, 1, 1, 0)
|
||||
# #endregion test_calculate_schedule_single_task
|
||||
# #region test_calculate_schedule_empty_list [TYPE Function]
|
||||
# @RELATION BINDS_TO -> test_throttled_scheduler
|
||||
# @RELATION BINDS_TO -> [test_throttled_scheduler]
|
||||
# @BRIEF Validate empty dashboard list produces an empty schedule.
|
||||
def test_calculate_schedule_empty_list():
|
||||
"""
|
||||
@@ -78,7 +78,7 @@ def test_calculate_schedule_empty_list():
|
||||
assert schedule == []
|
||||
# #endregion test_calculate_schedule_empty_list
|
||||
# #region test_calculate_schedule_zero_window [TYPE Function]
|
||||
# @RELATION BINDS_TO -> test_throttled_scheduler
|
||||
# @RELATION BINDS_TO -> [test_throttled_scheduler]
|
||||
# @BRIEF Validate zero-length window schedules all tasks at identical start timestamp.
|
||||
def test_calculate_schedule_zero_window():
|
||||
"""
|
||||
@@ -96,7 +96,7 @@ def test_calculate_schedule_zero_window():
|
||||
assert schedule[1] == datetime(2024, 1, 1, 1, 0)
|
||||
# #endregion test_calculate_schedule_zero_window
|
||||
# #region test_calculate_schedule_very_small_window [TYPE Function]
|
||||
# @RELATION BINDS_TO -> test_throttled_scheduler
|
||||
# @RELATION BINDS_TO -> [test_throttled_scheduler]
|
||||
# @BRIEF Validate sub-second interpolation when task count exceeds near-zero window granularity.
|
||||
def test_calculate_schedule_very_small_window():
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# #region AsyncSupersetClientModule [C:3] [TYPE Module] [SEMANTICS superset, transform, dashboard, filter, async-superset-client]
|
||||
# @BRIEF Parse a Superset dashboard URL and extract native filter state asynchronously.
|
||||
# @LAYER: Core
|
||||
# @LAYER Core
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
@@ -16,14 +16,14 @@ from .utils.async_network import AsyncAPIClient
|
||||
# @BRIEF Async sibling of SupersetClient for dashboard read paths.
|
||||
# @RELATION INHERITS -> [SupersetClient]
|
||||
# @RELATION DEPENDS_ON -> [AsyncAPIClient]
|
||||
# @RELATION CALLS -> [AsyncAPIClient.request]
|
||||
# @RELATION CALLS -> [EXT:method:AsyncAPIClient.request]
|
||||
class AsyncSupersetClient(SupersetClient):
|
||||
# #region AsyncSupersetClientInit [TYPE Function] [C:3]
|
||||
# @PURPOSE: Initialize async Superset client with AsyncAPIClient transport.
|
||||
# @PRE: env is valid Environment instance.
|
||||
# @POST: Client uses async network transport and inherited projection helpers.
|
||||
# @DATA_CONTRACT: Input[Environment] -> self.network[AsyncAPIClient]
|
||||
# @RELATION: [DEPENDS_ON] ->[AsyncAPIClient]
|
||||
# @PRE env is valid Environment instance.
|
||||
# @POST Client uses async network transport and inherited projection helpers.
|
||||
# @DATA_CONTRACT Input[Environment] -> self.network[AsyncAPIClient]
|
||||
# @RELATION DEPENDS_ON -> [AsyncAPIClient]
|
||||
def __init__(self, env: Environment):
|
||||
self.env = env
|
||||
auth_payload = {
|
||||
@@ -41,17 +41,17 @@ class AsyncSupersetClient(SupersetClient):
|
||||
# #endregion AsyncSupersetClientInit
|
||||
# #region AsyncSupersetClientClose [TYPE Function] [C:3]
|
||||
# @PURPOSE: Close async transport resources.
|
||||
# @POST: Underlying AsyncAPIClient is closed.
|
||||
# @SIDE_EFFECT: Closes network sockets.
|
||||
# @RELATION: [CALLS] ->[AsyncAPIClient.aclose]
|
||||
# @POST Underlying AsyncAPIClient is closed.
|
||||
# @SIDE_EFFECT Closes network sockets.
|
||||
# @RELATION CALLS -> [AsyncAPIClient.aclose]
|
||||
async def aclose(self) -> None:
|
||||
await self.network.aclose()
|
||||
# #endregion AsyncSupersetClientClose
|
||||
# #region get_dashboards_page_async [TYPE Function] [C:3]
|
||||
# @PURPOSE: Fetch one dashboards page asynchronously.
|
||||
# @POST: Returns total count and page result list.
|
||||
# @DATA_CONTRACT: Input[query: Optional[Dict]] -> Output[Tuple[int, List[Dict]]]
|
||||
# @RELATION: [CALLS] -> [AsyncAPIClient.request]
|
||||
# @POST Returns total count and page result list.
|
||||
# @DATA_CONTRACT Input[query: Optional[Dict]] -> Output[Tuple[int, List[Dict]]]
|
||||
# @RELATION CALLS -> [EXT:method:AsyncAPIClient.request]
|
||||
async def get_dashboards_page_async(
|
||||
self, query: dict | None = None
|
||||
) -> tuple[int, list[dict]]:
|
||||
@@ -84,9 +84,9 @@ class AsyncSupersetClient(SupersetClient):
|
||||
# #endregion get_dashboards_page_async
|
||||
# #region get_dashboard_async [TYPE Function] [C:3]
|
||||
# @PURPOSE: Fetch one dashboard payload asynchronously.
|
||||
# @POST: Returns raw dashboard payload from Superset API.
|
||||
# @DATA_CONTRACT: Input[dashboard_id: int] -> Output[Dict]
|
||||
# @RELATION: [CALLS] ->[AsyncAPIClient.request]
|
||||
# @POST Returns raw dashboard payload from Superset API.
|
||||
# @DATA_CONTRACT Input[dashboard_id: int] -> Output[Dict]
|
||||
# @RELATION CALLS -> [EXT:method:AsyncAPIClient.request]
|
||||
async def get_dashboard_async(self, dashboard_id: int) -> dict:
|
||||
with belief_scope(
|
||||
"AsyncSupersetClient.get_dashboard_async", f"id={dashboard_id}"
|
||||
@@ -98,9 +98,9 @@ class AsyncSupersetClient(SupersetClient):
|
||||
# #endregion get_dashboard_async
|
||||
# #region get_chart_async [TYPE Function] [C:3]
|
||||
# @PURPOSE: Fetch one chart payload asynchronously.
|
||||
# @POST: Returns raw chart payload from Superset API.
|
||||
# @DATA_CONTRACT: Input[chart_id: int] -> Output[Dict]
|
||||
# @RELATION: [CALLS] ->[AsyncAPIClient.request]
|
||||
# @POST Returns raw chart payload from Superset API.
|
||||
# @DATA_CONTRACT Input[chart_id: int] -> Output[Dict]
|
||||
# @RELATION CALLS -> [EXT:method:AsyncAPIClient.request]
|
||||
async def get_chart_async(self, chart_id: int) -> dict:
|
||||
with belief_scope("AsyncSupersetClient.get_chart_async", f"id={chart_id}"):
|
||||
response = await self.network.request(
|
||||
@@ -110,10 +110,10 @@ class AsyncSupersetClient(SupersetClient):
|
||||
# #endregion get_chart_async
|
||||
# #region get_dashboard_detail_async [TYPE Function] [C:3]
|
||||
# @PURPOSE: Fetch dashboard detail asynchronously with concurrent charts/datasets requests.
|
||||
# @POST: Returns dashboard detail payload for overview page.
|
||||
# @DATA_CONTRACT: Input[dashboard_id: int] -> Output[Dict]
|
||||
# @RELATION: [CALLS] ->[get_dashboard_async]
|
||||
# @RELATION: [CALLS] ->[get_chart_async]
|
||||
# @POST Returns dashboard detail payload for overview page.
|
||||
# @DATA_CONTRACT Input[dashboard_id: int] -> Output[Dict]
|
||||
# @RELATION CALLS -> [get_dashboard_async]
|
||||
# @RELATION CALLS -> [get_chart_async]
|
||||
async def get_dashboard_detail_async(self, dashboard_id: int) -> dict:
|
||||
with belief_scope(
|
||||
"AsyncSupersetClient.get_dashboard_detail_async", f"id={dashboard_id}"
|
||||
@@ -395,8 +395,8 @@ class AsyncSupersetClient(SupersetClient):
|
||||
# #endregion get_dashboard_detail_async
|
||||
# #region get_dashboard_permalink_state_async [TYPE Function] [C:2]
|
||||
# @PURPOSE: Fetch stored dashboard permalink state asynchronously.
|
||||
# @POST: Returns dashboard permalink state payload from Superset API.
|
||||
# @DATA_CONTRACT: Input[permalink_key: str] -> Output[Dict]
|
||||
# @POST Returns dashboard permalink state payload from Superset API.
|
||||
# @DATA_CONTRACT Input[permalink_key: str] -> Output[Dict]
|
||||
async def get_dashboard_permalink_state_async(self, permalink_key: str) -> dict:
|
||||
with belief_scope(
|
||||
"AsyncSupersetClient.get_dashboard_permalink_state_async",
|
||||
@@ -409,8 +409,8 @@ class AsyncSupersetClient(SupersetClient):
|
||||
# #endregion get_dashboard_permalink_state_async
|
||||
# #region get_native_filter_state_async [TYPE Function] [C:2]
|
||||
# @PURPOSE: Fetch stored native filter state asynchronously.
|
||||
# @POST: Returns native filter state payload from Superset API.
|
||||
# @DATA_CONTRACT: Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict]
|
||||
# @POST Returns native filter state payload from Superset API.
|
||||
# @DATA_CONTRACT Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict]
|
||||
async def get_native_filter_state_async(
|
||||
self, dashboard_id: int, filter_state_key: str
|
||||
) -> dict:
|
||||
@@ -426,9 +426,9 @@ class AsyncSupersetClient(SupersetClient):
|
||||
# #endregion get_native_filter_state_async
|
||||
# #region extract_native_filters_from_permalink_async [TYPE Function] [C:3]
|
||||
# @PURPOSE: Extract native filters dataMask from a permalink key asynchronously.
|
||||
# @POST: Returns extracted dataMask with filter states.
|
||||
# @DATA_CONTRACT: Input[permalink_key: str] -> Output[Dict]
|
||||
# @RELATION: [CALLS] ->[get_dashboard_permalink_state_async]
|
||||
# @POST Returns extracted dataMask with filter states.
|
||||
# @DATA_CONTRACT Input[permalink_key: str] -> Output[Dict]
|
||||
# @RELATION CALLS -> [get_dashboard_permalink_state_async]
|
||||
async def extract_native_filters_from_permalink_async(
|
||||
self, permalink_key: str
|
||||
) -> dict:
|
||||
@@ -461,9 +461,9 @@ class AsyncSupersetClient(SupersetClient):
|
||||
# #endregion extract_native_filters_from_permalink_async
|
||||
# #region extract_native_filters_from_key_async [TYPE Function] [C:3]
|
||||
# @PURPOSE: Extract native filters from a native_filters_key URL parameter asynchronously.
|
||||
# @POST: Returns extracted filter state with extraFormData.
|
||||
# @DATA_CONTRACT: Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict]
|
||||
# @RELATION: [CALLS] ->[get_native_filter_state_async]
|
||||
# @POST Returns extracted filter state with extraFormData.
|
||||
# @DATA_CONTRACT Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict]
|
||||
# @RELATION CALLS -> [get_native_filter_state_async]
|
||||
async def extract_native_filters_from_key_async(
|
||||
self, dashboard_id: int, filter_state_key: str
|
||||
) -> dict:
|
||||
@@ -514,10 +514,10 @@ class AsyncSupersetClient(SupersetClient):
|
||||
# #endregion extract_native_filters_from_key_async
|
||||
# #region parse_dashboard_url_for_filters_async [TYPE Function] [C:3]
|
||||
# @PURPOSE: Parse a Superset dashboard URL and extract native filter state asynchronously.
|
||||
# @POST: Returns extracted filter state or empty dict if no filters found.
|
||||
# @DATA_CONTRACT: Input[url: str] -> Output[Dict]
|
||||
# @RELATION: [CALLS] ->[extract_native_filters_from_permalink_async]
|
||||
# @RELATION: [CALLS] ->[extract_native_filters_from_key_async]
|
||||
# @POST Returns extracted filter state or empty dict if no filters found.
|
||||
# @DATA_CONTRACT Input[url: str] -> Output[Dict]
|
||||
# @RELATION CALLS -> [extract_native_filters_from_permalink_async]
|
||||
# @RELATION CALLS -> [extract_native_filters_from_key_async]
|
||||
async def parse_dashboard_url_for_filters_async(self, url: str) -> dict:
|
||||
with belief_scope(
|
||||
"AsyncSupersetClient.parse_dashboard_url_for_filters_async", f"url={url}"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# #region test_auth [TYPE Module] [C:3] [SEMANTICS test, auth, authentication, unit]
|
||||
# @BRIEF Unit tests for authentication module
|
||||
# @LAYER: Domain
|
||||
# @RELATION VERIFIES -> AuthPackage
|
||||
# @LAYER Domain
|
||||
# @RELATION BINDS_TO -> AuthPackage
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
@@ -46,7 +46,7 @@ def auth_repo(db_session):
|
||||
return AuthRepository(db_session)
|
||||
# #region test_create_user [TYPE Function]
|
||||
# @BRIEF Verifies that a persisted user can be retrieved with intact credential hash.
|
||||
# @RELATION BINDS_TO -> test_auth
|
||||
# @RELATION BINDS_TO -> [test_auth]
|
||||
def test_create_user(auth_repo):
|
||||
"""Test user creation"""
|
||||
user = User(
|
||||
@@ -65,7 +65,7 @@ def test_create_user(auth_repo):
|
||||
# #endregion test_create_user
|
||||
# #region test_authenticate_user [TYPE Function]
|
||||
# @BRIEF Validates authentication outcomes for valid, wrong-password, and unknown-user cases.
|
||||
# @RELATION BINDS_TO -> test_auth
|
||||
# @RELATION BINDS_TO -> [test_auth]
|
||||
def test_authenticate_user(auth_service, auth_repo):
|
||||
"""Test user authentication with valid and invalid credentials"""
|
||||
user = User(
|
||||
@@ -89,7 +89,7 @@ def test_authenticate_user(auth_service, auth_repo):
|
||||
# #endregion test_authenticate_user
|
||||
# #region test_create_session [TYPE Function]
|
||||
# @BRIEF Ensures session creation returns bearer token payload fields.
|
||||
# @RELATION BINDS_TO -> test_auth
|
||||
# @RELATION BINDS_TO -> [test_auth]
|
||||
def test_create_session(auth_service, auth_repo):
|
||||
"""Test session token creation"""
|
||||
user = User(
|
||||
@@ -108,7 +108,7 @@ def test_create_session(auth_service, auth_repo):
|
||||
# #endregion test_create_session
|
||||
# #region test_role_permission_association [TYPE Function]
|
||||
# @BRIEF Confirms role-permission many-to-many assignments persist and reload correctly.
|
||||
# @RELATION BINDS_TO -> test_auth
|
||||
# @RELATION BINDS_TO -> [test_auth]
|
||||
def test_role_permission_association(auth_repo):
|
||||
"""Test role and permission association"""
|
||||
role = Role(name="Admin", description="System administrator")
|
||||
@@ -126,7 +126,7 @@ def test_role_permission_association(auth_repo):
|
||||
# #endregion test_role_permission_association
|
||||
# #region test_user_role_association [TYPE Function]
|
||||
# @BRIEF Confirms user-role assignment persists and is queryable from repository reads.
|
||||
# @RELATION BINDS_TO -> test_auth
|
||||
# @RELATION BINDS_TO -> [test_auth]
|
||||
def test_user_role_association(auth_repo):
|
||||
"""Test user and role association"""
|
||||
role = Role(name="Admin", description="System administrator")
|
||||
@@ -147,7 +147,7 @@ def test_user_role_association(auth_repo):
|
||||
# #endregion test_user_role_association
|
||||
# #region test_ad_group_mapping [TYPE Function]
|
||||
# @BRIEF Verifies AD group mapping rows persist and reference the expected role.
|
||||
# @RELATION BINDS_TO -> test_auth
|
||||
# @RELATION BINDS_TO -> [test_auth]
|
||||
def test_ad_group_mapping(auth_repo):
|
||||
"""Test AD group mapping"""
|
||||
role = Role(name="ADFS_Admin", description="ADFS administrators")
|
||||
@@ -166,7 +166,7 @@ def test_ad_group_mapping(auth_repo):
|
||||
# #endregion test_ad_group_mapping
|
||||
# #region test_authenticate_user_updates_last_login [TYPE Function]
|
||||
# @BRIEF Verifies successful authentication updates last_login audit field.
|
||||
# @RELATION BINDS_TO -> test_auth
|
||||
# @RELATION BINDS_TO -> [test_auth]
|
||||
def test_authenticate_user_updates_last_login(auth_service, auth_repo):
|
||||
"""@SIDE_EFFECT: authenticate_user updates last_login timestamp on success."""
|
||||
user = User(
|
||||
@@ -184,7 +184,7 @@ def test_authenticate_user_updates_last_login(auth_service, auth_repo):
|
||||
# #endregion test_authenticate_user_updates_last_login
|
||||
# #region test_authenticate_inactive_user [TYPE Function]
|
||||
# @BRIEF Verifies inactive accounts are rejected during password authentication.
|
||||
# @RELATION BINDS_TO -> test_auth
|
||||
# @RELATION BINDS_TO -> [test_auth]
|
||||
def test_authenticate_inactive_user(auth_service, auth_repo):
|
||||
"""@PRE: User with is_active=False should not authenticate."""
|
||||
user = User(
|
||||
@@ -201,7 +201,7 @@ def test_authenticate_inactive_user(auth_service, auth_repo):
|
||||
# #endregion test_authenticate_inactive_user
|
||||
# #region test_verify_password_empty_hash [TYPE Function]
|
||||
# @BRIEF Verifies password verification safely rejects empty or null password hashes.
|
||||
# @RELATION BINDS_TO -> test_auth
|
||||
# @RELATION BINDS_TO -> [test_auth]
|
||||
def test_verify_password_empty_hash():
|
||||
"""@PRE: verify_password with empty/None hash returns False."""
|
||||
assert verify_password("anypassword", "") is False
|
||||
@@ -209,7 +209,7 @@ def test_verify_password_empty_hash():
|
||||
# #endregion test_verify_password_empty_hash
|
||||
# #region test_provision_adfs_user_new [TYPE Function]
|
||||
# @BRIEF Verifies JIT provisioning creates a new ADFS user and maps group-derived roles.
|
||||
# @RELATION BINDS_TO -> test_auth
|
||||
# @RELATION BINDS_TO -> [test_auth]
|
||||
def test_provision_adfs_user_new(auth_service, auth_repo):
|
||||
"""@POST: provision_adfs_user creates a new ADFS user with correct roles."""
|
||||
# Set up a role and AD group mapping
|
||||
@@ -234,7 +234,7 @@ def test_provision_adfs_user_new(auth_service, auth_repo):
|
||||
# #endregion test_provision_adfs_user_new
|
||||
# #region test_provision_adfs_user_existing [TYPE Function]
|
||||
# @BRIEF Verifies JIT provisioning reuses existing ADFS user and refreshes role assignments.
|
||||
# @RELATION BINDS_TO -> test_auth
|
||||
# @RELATION BINDS_TO -> [test_auth]
|
||||
def test_provision_adfs_user_existing(auth_service, auth_repo):
|
||||
"""@POST: provision_adfs_user updates roles for existing user."""
|
||||
# Create existing user
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# #region APIKeyUtilities [C:2] [TYPE Module] [SEMANTICS auth, api_key, crypto, generation]
|
||||
# @BRIEF API key generation and hashing utilities for service-to-service authentication.
|
||||
# @LAYER Core
|
||||
# @RELATION DEPENDS_ON -> [hashlib]
|
||||
# @RELATION DEPENDS_ON -> [secrets]
|
||||
# @RELATION DEPENDS_ON -> [EXT:Python:hashlib]
|
||||
# @RELATION DEPENDS_ON -> [EXT:Python:secrets]
|
||||
# @INVARIANT generate_api_key() always returns (raw_key, prefix, key_hash) where prefix is "ssk_" + 7 chars.
|
||||
# @INVARIANT hash_api_key() produces SHA-256 hex digest for lookup and storage.
|
||||
|
||||
@@ -14,8 +14,8 @@ import secrets
|
||||
# @BRIEF Generate a new API key in ssk_ format with SHA-256 hash.
|
||||
# @POST Returns (raw_key, prefix, key_hash) — raw_key shown ONCE to caller, never stored.
|
||||
# @SIDE_EFFECT Uses secrets.token_urlsafe(32) for cryptographic randomness.
|
||||
# @RELATION DEPENDS_ON -> [secrets.token_urlsafe]
|
||||
# @RELATION DEPENDS_ON -> [hashlib.sha256]
|
||||
# @RELATION DEPENDS_ON -> [EXT:Python:secrets.token_urlsafe]
|
||||
# @RELATION DEPENDS_ON -> [EXT:Python:hashlib.sha256]
|
||||
def generate_api_key() -> tuple[str, str, str]:
|
||||
"""Generate a new API key.
|
||||
|
||||
@@ -37,7 +37,7 @@ def generate_api_key() -> tuple[str, str, str]:
|
||||
# @BRIEF Hash an API key string to SHA-256 hex digest for lookup.
|
||||
# @PRE raw_key is a non-empty string.
|
||||
# @POST Returns 64-character hex digest.
|
||||
# @RELATION DEPENDS_ON -> [hashlib.sha256]
|
||||
# @RELATION DEPENDS_ON -> [EXT:Python:hashlib.sha256]
|
||||
def hash_api_key(raw_key: str) -> str:
|
||||
"""Hash an API key using SHA-256.
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# #region AuthConfigModule [C:2] [TYPE Module] [SEMANTICS pydantic, auth, auth-config, config]
|
||||
#
|
||||
# @BRIEF Centralized configuration for authentication and authorization.
|
||||
# @LAYER: Core
|
||||
# @RELATION DEPENDS_ON -> pydantic
|
||||
# @LAYER Core
|
||||
# @RELATION DEPENDS_ON -> [EXT:Library:pydantic]
|
||||
#
|
||||
# @INVARIANT: All sensitive configuration must be loaded from environment; no hardcoded secrets.
|
||||
# @INVARIANT All sensitive configuration must be loaded from environment; no hardcoded secrets.
|
||||
# @RATIONALE SECRET_KEY and AUTH_DATABASE_URL now crash-early if env vars are missing.
|
||||
# Dev fallback for AUTH_DATABASE_URL only when DEV_MODE=true.
|
||||
# @REJECTED Default secrets in source code rejected — Class 1 security violation:
|
||||
@@ -19,9 +19,9 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
# #region AuthConfig [TYPE Class]
|
||||
# @BRIEF Holds authentication-related settings.
|
||||
# @PRE: Environment variables may be provided via .env file.
|
||||
# @POST: Returns a configuration object with validated settings.
|
||||
# @RELATION INHERITS -> pydantic_settings.BaseSettings
|
||||
# @PRE Environment variables may be provided via .env file.
|
||||
# @POST Returns a configuration object with validated settings.
|
||||
# @RELATION INHERITS -> [EXT:Library:pydantic_settings.BaseSettings]
|
||||
class AuthConfig(BaseSettings):
|
||||
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
|
||||
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
# #region AuthJwtModule [C:5] [TYPE Module] [SEMANTICS auth, validate, jwt, token]
|
||||
#
|
||||
# @BRIEF JWT token generation and validation logic.
|
||||
# @LAYER: Core
|
||||
# @LAYER Core
|
||||
# @RELATION DEPENDS_ON -> [auth_config]
|
||||
# @RELATION USES -> [auth_config]
|
||||
#
|
||||
# @INVARIANT: Tokens must include expiration time and user identifier.
|
||||
# @PRE: JWT secret configured in environment
|
||||
# @POST: Token encode/decode functions exported
|
||||
# @SIDE_EFFECT: None
|
||||
# @DATA_CONTRACT: TokenPayload -> JWT string
|
||||
# @INVARIANT Tokens must include expiration time and user identifier.
|
||||
# @PRE JWT secret configured in environment
|
||||
# @POST Token encode/decode functions exported
|
||||
# @SIDE_EFFECT None
|
||||
# @DATA_CONTRACT TokenPayload -> JWT string
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
@@ -21,8 +20,8 @@ from .config import auth_config
|
||||
|
||||
# #region create_access_token [TYPE Function]
|
||||
# @BRIEF Generates a new JWT access token.
|
||||
# @PRE: data dict contains 'sub' (user_id) and optional 'scopes' (roles).
|
||||
# @POST: Returns a signed JWT string.
|
||||
# @PRE data dict contains 'sub' (user_id) and optional 'scopes' (roles).
|
||||
# @POST Returns a signed JWT string.
|
||||
# @RELATION DEPENDS_ON -> [auth_config]
|
||||
#
|
||||
def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:
|
||||
@@ -47,8 +46,8 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None) -> s
|
||||
|
||||
# #region decode_token [TYPE Function]
|
||||
# @BRIEF Decodes and validates a JWT token.
|
||||
# @PRE: token is a signed JWT string.
|
||||
# @POST: Returns the decoded payload if valid.
|
||||
# @PRE token is a signed JWT string.
|
||||
# @POST Returns the decoded payload if valid.
|
||||
# @RELATION DEPENDS_ON -> [auth_config]
|
||||
#
|
||||
def decode_token(token: str) -> dict:
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
# #region AuthLoggerModule [C:5] [TYPE Module] [SEMANTICS auth, audit, logging]
|
||||
#
|
||||
# @BRIEF Structured auth logging module for audit trail generation.
|
||||
# @LAYER: Core
|
||||
# @RELATION DEPENDS_ON -> [core_logger]
|
||||
# @LAYER Core
|
||||
# @RELATION DEPENDS_ON -> [EXT:frontend:core_logger]
|
||||
#
|
||||
# @INVARIANT: Must not log sensitive data like passwords or full tokens.
|
||||
# @PRE: Auth module initialized
|
||||
# @POST: Audit logging functions exported
|
||||
# @SIDE_EFFECT: Writes auth audit log entries
|
||||
# @DATA_CONTRACT: AuthEvent -> LogEntry
|
||||
# @SIDE_EFFECT: Writes auth audit log entries
|
||||
# @DATA_CONTRACT: AuthEvent -> LogEntry
|
||||
# @INVARIANT Must not log sensitive data like passwords or full tokens.
|
||||
# @PRE Auth module initialized
|
||||
# @POST Audit logging functions exported
|
||||
# @SIDE_EFFECT Writes auth audit log entries
|
||||
# @DATA_CONTRACT AuthEvent -> LogEntry
|
||||
# @SIDE_EFFECT Writes auth audit log entries
|
||||
# @DATA_CONTRACT AuthEvent -> LogEntry
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
@@ -19,9 +19,9 @@ from ..logger import belief_scope, logger
|
||||
|
||||
# #region log_security_event [TYPE Function]
|
||||
# @BRIEF Logs a security-related event for audit trails.
|
||||
# @PRE: event_type and username are strings.
|
||||
# @POST: Security event is written to the application log.
|
||||
# @RELATION USES -> logger
|
||||
# @PRE event_type and username are strings.
|
||||
# @POST Security event is written to the application log.
|
||||
# @RELATION DEPENDS_ON -> [logger]
|
||||
def log_security_event(event_type: str, username: str, details: dict = None):
|
||||
with belief_scope("log_security_event", f"{event_type}:{username}"):
|
||||
timestamp = datetime.utcnow().isoformat()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user