refactor(backend)!: separate algorithm and data layers
Reorganize algorithm packages by business responsibility, move orchestration into services, and keep database access behind pooled repositories. Harden analysis API validation, remove unsafe legacy simulation endpoints, and add regression and architecture boundary coverage. BREAKING CHANGE: legacy algorithm module paths and obsolete simulation endpoints are removed.
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
"""Executable dependency rules for the service/algorithm/database layers."""
|
||||
|
||||
import ast
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
APP_ROOT = Path(__file__).parents[2] / "app"
|
||||
|
||||
|
||||
def _app_imports(path: Path) -> set[str]:
|
||||
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
||||
imports: set[str] = set()
|
||||
package_parts = ("app", *path.relative_to(APP_ROOT).parent.parts)
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import):
|
||||
imports.update(alias.name for alias in node.names if alias.name.startswith("app."))
|
||||
elif isinstance(node, ast.ImportFrom):
|
||||
if node.level:
|
||||
parent_count = node.level - 1
|
||||
if parent_count >= len(package_parts):
|
||||
continue
|
||||
base_parts = package_parts[: len(package_parts) - parent_count]
|
||||
if node.module:
|
||||
imports.add(".".join((*base_parts, *node.module.split("."))))
|
||||
else:
|
||||
imports.update(
|
||||
".".join((*base_parts, alias.name.split(".")[0]))
|
||||
for alias in node.names
|
||||
)
|
||||
elif node.module and node.module.startswith("app."):
|
||||
imports.add(node.module)
|
||||
return imports
|
||||
|
||||
|
||||
def _forbidden_imports(root: Path, prefixes: tuple[str, ...]) -> list[str]:
|
||||
violations: list[str] = []
|
||||
for path in sorted(root.rglob("*.py")):
|
||||
for module in sorted(_app_imports(path)):
|
||||
if module.startswith(prefixes):
|
||||
violations.append(f"{path.relative_to(APP_ROOT)} -> {module}")
|
||||
return violations
|
||||
|
||||
|
||||
def test_algorithms_are_independent_of_application_and_io_layers():
|
||||
violations = _forbidden_imports(
|
||||
APP_ROOT / "algorithms",
|
||||
("app.api", "app.auth", "app.infra", "app.native", "app.services"),
|
||||
)
|
||||
|
||||
assert violations == []
|
||||
|
||||
|
||||
def test_infrastructure_does_not_depend_on_application_or_algorithms():
|
||||
violations = _forbidden_imports(
|
||||
APP_ROOT / "infra",
|
||||
("app.api", "app.algorithms", "app.services"),
|
||||
)
|
||||
|
||||
assert violations == []
|
||||
|
||||
|
||||
def test_services_do_not_execute_sql_directly():
|
||||
violations = []
|
||||
for path in sorted((APP_ROOT / "services").rglob("*.py")):
|
||||
source = path.read_text(encoding="utf-8")
|
||||
if ".cursor(" in source or ".execute(" in source:
|
||||
violations.append(str(path.relative_to(APP_ROOT)))
|
||||
|
||||
assert violations == []
|
||||
|
||||
|
||||
def test_services_do_not_depend_on_the_http_network_facade():
|
||||
violations = _forbidden_imports(
|
||||
APP_ROOT / "services",
|
||||
("app.services.tjnetwork",),
|
||||
)
|
||||
|
||||
assert violations == []
|
||||
|
||||
|
||||
def test_algorithm_packages_use_explicit_business_names():
|
||||
legacy_names = {
|
||||
"burst_location",
|
||||
"cleaning",
|
||||
"health",
|
||||
"isolation",
|
||||
"leakage",
|
||||
"sensor",
|
||||
"simulation",
|
||||
"water_demand",
|
||||
}
|
||||
present_names = {
|
||||
path.name
|
||||
for path in (APP_ROOT / "algorithms").iterdir()
|
||||
if path.is_dir() and any(path.glob("*.py"))
|
||||
}
|
||||
|
||||
assert present_names.isdisjoint(legacy_names)
|
||||
|
||||
|
||||
def test_application_code_does_not_open_direct_psycopg_connections():
|
||||
violations = []
|
||||
direct_calls = ("psycopg.connect(", "Connection.connect(", "AsyncConnection.connect(")
|
||||
for path in sorted(APP_ROOT.rglob("*.py")):
|
||||
source = path.read_text(encoding="utf-8")
|
||||
if any(call in source for call in direct_calls):
|
||||
violations.append(str(path.relative_to(APP_ROOT)))
|
||||
|
||||
assert violations == []
|
||||
Reference in New Issue
Block a user