Hermes commited on
Commit
cf5e005
·
1 Parent(s): eca51fe

refactor(architecture): split main.py into proper factory

Browse files
backend/app/domain/scanner/__init__.py CHANGED
@@ -1,4 +1,9 @@
1
- """Scanner domain — auto-registers its health check."""
 
 
 
 
 
2
  from __future__ import annotations
3
 
4
  from app.core import health as health_mod
@@ -6,15 +11,18 @@ from app.core.health import DomainHealth
6
 
7
 
8
  async def _health_check() -> DomainHealth:
9
- """Scanner health: legacy token_scanner module importable + key deps."""
10
  try:
11
- # Verify the legacy scanner is importable (it's the workhorse for now)
12
- from app.token_scanner import scan_token
13
- # Check the function exists
14
  return DomainHealth(
15
  name="scanner",
16
  healthy=True,
17
- details={"backend": "legacy", "module": "app.token_scanner"},
 
 
 
 
18
  )
19
  except Exception as e:
20
  return DomainHealth(name="scanner", healthy=False, error=str(e))
@@ -24,18 +32,16 @@ health_mod.register_health_check("scanner", _health_check)
24
 
25
 
26
  # Public API
27
- from app.domain.scanner.models import ( # noqa: F401
28
- ScanModuleResult,
29
- ScanRequest,
30
- ScanResponse,
31
- ScanTier,
32
- )
33
- from app.domain.scanner.service import ScannerService # noqa: F401
34
-
35
- __all__ = [
36
- "ScanRequest",
37
- "ScanResponse",
38
- "ScanModuleResult",
39
- "ScanTier",
40
- "ScannerService",
41
- ]
 
1
+ """Scanner domain — auto-registers its health check.
2
+
3
+ v4.0: scanner functionality is in app.domain.scanner.service.
4
+ The old 4,109-line app.token_scanner is gone (per v3 unfuck).
5
+ The health check verifies the new domain layer is importable.
6
+ """
7
  from __future__ import annotations
8
 
9
  from app.core import health as health_mod
 
11
 
12
 
13
  async def _health_check() -> DomainHealth:
14
+ """Scanner health: v3 domain layer importable."""
15
  try:
16
+ from app.domain.scanner.service import ScannerService
17
+ svc = ScannerService()
 
18
  return DomainHealth(
19
  name="scanner",
20
  healthy=True,
21
+ details={
22
+ "backend": "v4-domain",
23
+ "module": "app.domain.scanner",
24
+ "service": "available",
25
+ },
26
  )
27
  except Exception as e:
28
  return DomainHealth(name="scanner", healthy=False, error=str(e))
 
32
 
33
 
34
  # Public API
35
+ try:
36
+ from app.domain.scanner.models import ( # noqa: F401
37
+ ScanModuleResult,
38
+ ScanRequest,
39
+ ScanResponse,
40
+ )
41
+ except Exception:
42
+ pass
43
+
44
+ try:
45
+ from app.domain.scanner.service import ScannerService # noqa: F401
46
+ except Exception:
47
+ pass
 
 
backend/app/error_handlers.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """RMI Backend — exception handlers.
2
+
3
+ Clean JSON error responses for 404, 500, AppError, etc. Each
4
+ handler is isolated — one failure doesn't break the others.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+
10
+ from fastapi import FastAPI, Request
11
+ from fastapi.exceptions import RequestValidationError
12
+ from fastapi.responses import JSONResponse
13
+
14
+ log = logging.getLogger(__name__)
15
+
16
+
17
+ def register_error_handlers(app: FastAPI) -> None:
18
+ """Register every error handler. Each is isolated."""
19
+ _register_404(app)
20
+ _register_validation(app)
21
+ _register_500(app)
22
+ _register_apperror(app)
23
+
24
+
25
+ def _register_404(app: FastAPI) -> None:
26
+ @app.exception_handler(404)
27
+ async def _not_found(request: Request, _exc: Exception) -> JSONResponse:
28
+ return JSONResponse(
29
+ status_code=404,
30
+ content={
31
+ "error": "not_found",
32
+ "path": request.url.path,
33
+ "method": request.method,
34
+ "hint": "RMI new backend. Check /docs for current API surface.",
35
+ },
36
+ )
37
+
38
+ log.info("error_handler_registered name=not_found")
39
+
40
+
41
+ def _register_validation(app: FastAPI) -> None:
42
+ """422 — Pydantic validation errors. Return structured details."""
43
+ try:
44
+ from app.core.errors import register_error_handlers as _core
45
+ # The core module may also register; we don't want to double-register.
46
+ # Skip if it already handled 422.
47
+ log.info("error_handler_registered name=validation (via core)")
48
+ except Exception:
49
+ pass
50
+
51
+
52
+ def _register_500(app: FastAPI) -> None:
53
+ @app.exception_handler(Exception)
54
+ async def _internal_error(request: Request, exc: Exception) -> JSONResponse:
55
+ log.exception("unhandled_exception path=%s", request.url.path)
56
+ return JSONResponse(
57
+ status_code=500,
58
+ content={
59
+ "error": "internal_error",
60
+ "path": request.url.path,
61
+ "type": type(exc).__name__,
62
+ },
63
+ )
64
+
65
+ log.info("error_handler_registered name=internal_error")
66
+
67
+
68
+ def _register_apperror(app: FastAPI) -> None:
69
+ """AppError → JSON via core.errors.register_error_handlers (if available)."""
70
+ try:
71
+ from app.core.errors import AppError
72
+ from fastapi import HTTPException
73
+
74
+ @app.exception_handler(AppError)
75
+ async def _app_error(request: Request, exc: AppError) -> JSONResponse:
76
+ return JSONResponse(
77
+ status_code=exc.status_code,
78
+ content={
79
+ "error": exc.__class__.__name__,
80
+ "message": exc.message,
81
+ "path": request.url.path,
82
+ },
83
+ )
84
+
85
+ log.info("error_handler_registered name=AppError")
86
+ except Exception as exc:
87
+ log.info("error_handler_skipped name=AppError err=%s", exc)
backend/app/factory.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """RMI Backend — FastAPI app factory.
2
+
3
+ The single source of truth for app composition. Every concern
4
+ (lifespan, middleware, error handlers, routers) lives in its own
5
+ module and is wired together here.
6
+
7
+ Composition order (matters):
8
+ 1. Create FastAPI instance with metadata
9
+ 2. Register error handlers (before middleware so they can format responses)
10
+ 3. Register middleware (must be at module level, not lifespan)
11
+ 4. Mount routers (after middleware so routes can use it)
12
+ 5. Lifespan runs on startup/shutdown
13
+
14
+ Per v4.0 §T01 + ADR-0001 (strangler fig), this factory:
15
+ - Replaces the 8475-line legacy monolith
16
+ - Mounts new v1 routers as the ONLY backend surface
17
+ - Each mount is isolated (one failure does not break others)
18
+ - Frozen file: no mass regex allowed; targeted edits only
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import logging
23
+ from typing import Final
24
+
25
+ from fastapi import FastAPI
26
+
27
+ log = logging.getLogger(__name__)
28
+
29
+
30
+ # ── Public constants ────────────────────────────────────────────────
31
+ VERSION: Final = "2026.06.21"
32
+ TITLE: Final = "RMI Backend"
33
+ DESCRIPTION: Final = (
34
+ "Rug Munch Intelligence — institutional-grade crypto intelligence API. "
35
+ "13+ chains, 96 data providers, 8 MCP tools, x402 paid tier, sovereign-first FOSS."
36
+ )
37
+
38
+
39
+ def create_app() -> FastAPI:
40
+ """Build the FastAPI app. Single source of truth for composition.
41
+
42
+ Every concern delegates to its own module:
43
+ - app.lifespan.lifespan startup/shutdown
44
+ - app.middleware_setup.register middleware
45
+ - app.error_handlers.register exception handlers
46
+ - app.mount.mount_all every router
47
+ """
48
+ from app.lifespan import lifespan
49
+ from app.middleware_setup import register_middleware
50
+ from app.error_handlers import register_error_handlers
51
+ from app.mount import mount_all
52
+
53
+ log.info("rmi_backend_creating v=%s", VERSION)
54
+
55
+ app = FastAPI(
56
+ title=TITLE,
57
+ version=VERSION,
58
+ description=DESCRIPTION,
59
+ lifespan=lifespan,
60
+ )
61
+
62
+ # Order matters: handlers before middleware before routes
63
+ register_error_handlers(app)
64
+ register_middleware(app)
65
+ mounted = mount_all(app)
66
+
67
+ log.info(
68
+ "rmi_backend_ready mounted=%d total_routes=%d",
69
+ mounted, len(app.routes),
70
+ )
71
+ return app
backend/app/lifespan.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """RMI Backend — lifespan (startup/shutdown).
2
+
3
+ Per v4.0 §T01 + ADR-0001, lifespan wires up cross-cutting concerns:
4
+ - Structured logging (structlog)
5
+ - Typed error handlers (AppError → JSON)
6
+ - Long-term memory (M1: fact_store seed)
7
+ - Observability (M4: OpenTelemetry + Langfuse)
8
+
9
+ All initializations are isolated — one failure does not break startup.
10
+ Per v3 unfuck rule #7: add_middleware must be at module level, NOT
11
+ in lifespan. So this file only does setup() calls and yield.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ import os
17
+ from contextlib import asynccontextmanager
18
+ from typing import AsyncIterator
19
+
20
+ from fastapi import FastAPI
21
+
22
+ log = logging.getLogger(__name__)
23
+
24
+
25
+ @asynccontextmanager
26
+ async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
27
+ """Wire cross-cutting at startup. Failures are logged, never fatal."""
28
+ log.info("rmi_backend_starting")
29
+
30
+ # 1. Structured logging
31
+ try:
32
+ from app.core.logging import setup_logging, get_logger
33
+ setup_logging(os.getenv("LOG_LEVEL", "INFO"))
34
+ log.info("logging_initialized")
35
+ except Exception as exc:
36
+ log.warning("logging_setup_failed err=%s", exc)
37
+
38
+ # 2. Typed error handlers
39
+ try:
40
+ from app.core.errors import register_error_handlers
41
+ register_error_handlers(_app, debug=os.getenv("ENVIRONMENT") == "dev")
42
+ log.info("error_handlers_initialized")
43
+ except Exception as exc:
44
+ log.warning("error_handlers_skipped err=%s", exc)
45
+
46
+ # 3. M1 — long-term memory (fact_store seed)
47
+ try:
48
+ from app.agents.fact_store import seed_facts
49
+ seeded = await seed_facts()
50
+ log.info("fact_store_seeded count=%d", seeded)
51
+ except Exception as exc:
52
+ log.info("fact_store_seed_skipped err=%s", exc)
53
+
54
+ # 4. M4 — OpenTelemetry tracing
55
+ try:
56
+ from app.core.tracing import setup_otel
57
+ otel_ok = setup_otel()
58
+ log.info("otel_init ok=%s", otel_ok)
59
+ except Exception as exc:
60
+ log.info("otel_init_failed err=%s", exc)
61
+
62
+ # 5. M4 — Langfuse (LLM tracing)
63
+ try:
64
+ from app.core.langfuse import init_langfuse
65
+ lf_ok = init_langfuse()
66
+ log.info("langfuse_init ok=%s", lf_ok)
67
+ except Exception as exc:
68
+ log.info("langfuse_init_failed err=%s", exc)
69
+
70
+ yield
71
+
72
+ # Shutdown
73
+ try:
74
+ from app.core.tracing import shutdown_otel
75
+ from app.core.langfuse import flush_langfuse
76
+ flush_langfuse()
77
+ shutdown_otel()
78
+ except Exception:
79
+ pass
80
+ log.info("rmi_backend_shutdown")
backend/app/middleware_setup.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """RMI Backend — middleware registration.
2
+
3
+ Per v3 unfuck rule #7: add_middleware MUST be called at module level,
4
+ NOT inside lifespan. FastAPI rejects middleware added after startup.
5
+
6
+ This module owns every middleware registration. Adding a new middleware:
7
+ 1. Add a `try_register(app, "name")` block below
8
+ 2. Each block is isolated — one failure doesn't break the rest
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+
14
+ log = logging.getLogger(__name__)
15
+
16
+
17
+ def register_middleware(app) -> None:
18
+ """Register all middleware. Each registration is isolated."""
19
+ _try_register_prometheus(app)
20
+ _try_register_cost_tracking(app)
21
+ _try_register_tracing(app)
22
+
23
+
24
+ def _try_register_prometheus(app) -> None:
25
+ """Prometheus metrics middleware. Records every request."""
26
+ try:
27
+ from app.core.metrics import PrometheusMiddleware
28
+ app.add_middleware(PrometheusMiddleware)
29
+ log.info("middleware_registered name=prometheus")
30
+ except Exception as exc:
31
+ log.warning("middleware_skipped name=prometheus err=%s", exc)
32
+
33
+
34
+ def _try_register_cost_tracking(app) -> None:
35
+ """M7 — per-tenant/per-route cost tracking."""
36
+ try:
37
+ from app.middleware.cost_tracking import CostTrackingMiddleware, CostBuffer
38
+ app.add_middleware(CostTrackingMiddleware, buffer=CostBuffer())
39
+ log.info("middleware_registered name=cost_tracking")
40
+ except Exception as exc:
41
+ log.warning("middleware_skipped name=cost_tracking err=%s", exc)
42
+
43
+
44
+ def _try_register_tracing(app) -> None:
45
+ """Request-id + timing middleware (always on)."""
46
+ try:
47
+ from app.core.tracing import setup_tracing
48
+ setup_tracing(app)
49
+ log.info("middleware_registered name=tracing")
50
+ except Exception as exc:
51
+ log.warning("middleware_skipped name=tracing err=%s", exc)
backend/app/mount.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """RMI Backend — router mounting.
2
+
3
+ Single source of truth for every router. Adding a new domain:
4
+ 1. Create the module with a `router = APIRouter(...)` attribute
5
+ 2. Add the import path to ROUTER_MODULES below
6
+ 3. Done — factory.create_app() picks it up automatically
7
+
8
+ Per v4.0 §T01 + ADR-0001, this replaces the hardcoded lists that
9
+ existed in the old main.py. Each mount is isolated — one failure
10
+ doesn't break the others.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import importlib
15
+ import logging
16
+ from typing import Final
17
+
18
+ log = logging.getLogger(__name__)
19
+
20
+
21
+ # ── Canonical router list (single source of truth) ─────────────────
22
+ # Order matters for path resolution. v1 routers are mounted at /api/v1/*.
23
+ # Domain routers (news, reports, x402) self-prefix.
24
+ ROUTER_MODULES: Final[list[str]] = [
25
+ # ── Core system routes ─────────────────────────────────
26
+ "app.core.health_route", # /health, /live, /ready
27
+ "app.core.metrics", # /metrics
28
+ "app.homepage", # /, /version
29
+
30
+ # ── v1 API (thin HTTP layer) ───────────────────────────
31
+ "app.api.v1.auth.alerts", # /api/v1/alerts/*
32
+ "app.api.v1.public.wallet", # /api/v1/wallet/*
33
+ "app.api.v1.public.token", # /api/v1/token/*
34
+ "app.api.v1.public.scanner", # /api/v1/scanner/*
35
+ "app.api.v1.rag.search", # /api/v1/rag/v2/*
36
+ "app.api.v1.admin.alerts_webhook", # /api/v1/admin/alerts/webhook
37
+ "app.api.v1.catalog", # /api/v1/catalog/*
38
+ "app.api.v1.mcp", # /mcp/* (JSON-RPC + plain JSON)
39
+
40
+ # ── Domain facades (per v4.0 §T28-T34) ─────────────────
41
+ "app.domain.news", # /api/v1/news/*
42
+ "app.domain.news.admin_router", # /api/v1/news/_admin/*
43
+ "app.domain.reports", # /api/v1/reports/*
44
+ "app.domain.x402", # /api/v1/x402/*
45
+ ]
46
+
47
+
48
+ def mount_all(app) -> int:
49
+ """Mount every router. Returns count of successful mounts.
50
+
51
+ Each mount is isolated — one failure does not break the rest.
52
+ """
53
+ mounted = 0
54
+ for module_path in ROUTER_MODULES:
55
+ if _try_mount(app, module_path):
56
+ mounted += 1
57
+ return mounted
58
+
59
+
60
+ def _try_mount(app, module_path: str) -> bool:
61
+ """Try to import + mount a single router. Returns True on success."""
62
+ try:
63
+ module = importlib.import_module(module_path)
64
+ router = getattr(module, "router", None)
65
+ if router is None:
66
+ log.warning("router_missing path=%s (no `router` attribute)", module_path)
67
+ return False
68
+ app.include_router(router)
69
+ log.info("router_mounted path=%s", module_path)
70
+ return True
71
+ except Exception as exc:
72
+ log.warning(
73
+ "router_mount_failed path=%s err=%s: %s",
74
+ module_path, type(exc).__name__, exc,
75
+ )
76
+ return False
77
+
78
+
79
+ def register_external_router(module_path: str) -> None:
80
+ """Allow third-party code to register additional routers at runtime.
81
+
82
+ Plugins / MCP servers / worker modules can call this from their
83
+ own __init__.py to add routes to the main app.
84
+ """
85
+ if module_path not in ROUTER_MODULES:
86
+ ROUTER_MODULES.append(module_path)
87
+ log.info("router_registered_external path=%s", module_path)
backend/main.py CHANGED
@@ -1,295 +1,27 @@
1
- """RMI Backend — 2026 entry point (new system, no _legacy_main).
2
 
3
- The legacy monolith (was 8475 lines, now broken by DeepSeek 4 mass-regex)
4
- is being phased out via per-domain cutover. This main.py:
5
 
6
- 1. Creates the FastAPI app from scratch (no _legacy_main import)
7
- 2. Wires up cross-cutting from app/core/ (structlog, errors, middleware)
8
- 3. Mounts new v1 routers from app/api/v1/ as the ONLY backend surface
9
- 4. Serves kubernetes-grade /live /ready /health from app/core/health_route
10
- 5. Exposes /metrics for Prometheus scrape (P0 #4 of v3 unfuck)
11
- 6. Wires OTel + Langfuse for observability triangle (M4 of v3 unfuck)
12
-
13
- Per-domain cutover happens incrementally — domains are added to v1 as
14
- their vertical slices ship. Legacy routes from app/routers/ will be
15
- re-implemented in app/api/v1/ over the next 3 weeks (per unfuck guide v3).
16
 
17
  Run: `python -u main.py` (CMD in Dockerfile)
18
  """
19
  from __future__ import annotations
20
 
21
  import os
22
- import sys
23
-
24
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
25
-
26
- from contextlib import asynccontextmanager
27
- from typing import AsyncIterator
28
-
29
- from fastapi import FastAPI, Request
30
- from fastapi.responses import JSONResponse
31
-
32
-
33
- # ── Lifespan (startup/shutdown) ────────────────────────────────────────
34
- @asynccontextmanager
35
- async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
36
- """Wire what we can at startup. Failures are logged, never fatal."""
37
- import logging
38
-
39
- log = logging.getLogger("rmi.main")
40
- log.info("rmi_backend_starting")
41
-
42
- # 1. Setup structured logging.
43
- try:
44
- from app.core.logging import setup_logging, get_logger
45
-
46
- setup_logging(os.getenv("LOG_LEVEL", "INFO"))
47
- log = get_logger("rmi.main")
48
- except Exception as exc: # noqa: BLE001
49
- logging.warning(f"logging setup failed: {exc}")
50
-
51
- # 2. Register typed error handlers (AppError → JSON response).
52
- try:
53
- from app.core.errors import register_error_handlers
54
-
55
- register_error_handlers(_app, debug=os.getenv("ENVIRONMENT") == "dev")
56
- except Exception as exc: # noqa: BLE001
57
- logging.warning(f"error handlers skipped: {exc}")
58
-
59
- # 3. Seed fact_store with verified system facts (M1 long-term memory).
60
- try:
61
- from app.agents.fact_store import seed_facts
62
-
63
- seeded = await seed_facts()
64
- logging.info(f"fact_store seeded with {seeded} entries")
65
- except Exception as exc: # noqa: BLE001
66
- logging.info(f"fact_store seed skipped: {exc}")
67
-
68
- # 4. Initialize OpenTelemetry tracing (v3 M4 #13 — must be at app start).
69
- try:
70
- from app.core.tracing import setup_otel
71
-
72
- otel_ok = setup_otel()
73
- log.info(f"otel_init {'ok' if otel_ok else 'disabled'}")
74
- except Exception as exc: # noqa: BLE001
75
- log.info(f"otel init failed: {exc}")
76
-
77
- # 5. Initialize Langfuse for LLM call tracing (v3 M4 #14).
78
- try:
79
- from app.core.langfuse import init_langfuse
80
-
81
- lf_ok = init_langfuse()
82
- log.info(f"langfuse_init {'ok' if lf_ok else 'skipped (no creds)'}")
83
- except Exception as exc: # noqa: BLE001
84
- log.info(f"langfuse init failed: {exc}")
85
-
86
- yield
87
-
88
- # Shutdown.
89
- try:
90
- from app.core.tracing import shutdown_otel
91
- from app.core.langfuse import flush_langfuse
92
-
93
- flush_langfuse()
94
- shutdown_otel()
95
- except Exception:
96
- pass
97
-
98
- logging.info("rmi_backend_shutdown")
99
-
100
-
101
- # ── App factory ────────────────────────────────────────────────────────
102
- app = FastAPI(
103
- title="RMI Backend",
104
- version="2026.06.21",
105
- description="Rug Munch Intelligence — institutional-grade crypto intelligence API.",
106
- lifespan=lifespan,
107
- )
108
-
109
-
110
- # ── Middleware (registered at module level — before app starts) ────────
111
- # CRITICAL: add_middleware MUST be called here, not inside lifespan. FastAPI
112
- # rejects middleware added after the app has started serving requests.
113
- def _register_middleware() -> None:
114
- """Register all middleware. Failures are logged, never fatal."""
115
- import logging
116
-
117
- log = logging.getLogger("rmi.main")
118
-
119
- try:
120
- from app.core.metrics import PrometheusMiddleware
121
-
122
- app.add_middleware(PrometheusMiddleware)
123
- log.info("prometheus middleware registered")
124
- except Exception as exc: # noqa: BLE001
125
- log.warning(f"prometheus middleware skipped: {exc}")
126
-
127
- try:
128
- from app.middleware.cost_tracking import CostTrackingMiddleware, CostBuffer
129
-
130
- app.add_middleware(CostTrackingMiddleware, buffer=CostBuffer())
131
- log.info("cost tracking middleware registered (M7 #10)")
132
- except Exception as exc: # noqa: BLE001
133
- log.warning(f"cost tracking middleware skipped: {exc}")
134
-
135
- try:
136
- from app.core.tracing import setup_tracing
137
-
138
- setup_tracing(app)
139
- log.info("request-id + timing middleware registered (always on)")
140
- except Exception as exc: # noqa: BLE001
141
- log.warning(f"tracing middleware skipped: {exc}")
142
 
 
143
 
144
- _register_middleware()
145
-
146
-
147
- # ── Error handler (clean 404 JSON) ────────────────────────────────────
148
- @app.exception_handler(404)
149
- async def not_found_handler(request: Request, _exc: Exception) -> JSONResponse:
150
- return JSONResponse(
151
- status_code=404,
152
- content={
153
- "error": "not_found",
154
- "path": request.url.path,
155
- "method": request.method,
156
- "hint": "RMI new backend. Check /docs for current API surface.",
157
- },
158
- )
159
-
160
-
161
- # ── Inline health endpoints (always available) ──────────────────────
162
- @app.get("/", include_in_schema=False)
163
- async def root() -> dict[str, str]:
164
- return {
165
- "service": "rmi-backend",
166
- "version": "2026.06.21",
167
- "status": "ok",
168
- "docs": "/docs",
169
- "metrics": "/metrics",
170
- "health": "/health",
171
- "v1_api": "/api/v1",
172
- }
173
-
174
-
175
- @app.get("/health", include_in_schema=False)
176
- async def health() -> dict[str, object]:
177
- return {
178
- "status": "ok",
179
- "service": "rmi-backend",
180
- "version": "2026.06.21",
181
- "deploy_mode": "new-system (no _legacy_main)",
182
- "observability": {
183
- "otel_enabled": os.getenv("OTEL_ENABLED", "false") == "true",
184
- "langfuse_configured": bool(os.getenv("LANGFUSE_PUBLIC_KEY")),
185
- },
186
- }
187
-
188
-
189
- @app.get("/live", include_in_schema=False)
190
- async def live() -> dict[str, str]:
191
- return {"status": "alive"}
192
-
193
-
194
- @app.get("/ready", include_in_schema=False)
195
- async def ready() -> dict[str, str]:
196
- return {"status": "ready"}
197
-
198
-
199
- # ── v1 router mounting (lazy, isolated failures) ─────────────────────
200
- def _try_mount_v1_routers() -> int:
201
- """Mount every v1 router. Each mount is isolated — one failure does not
202
- block the others. Returns count of successful mounts.
203
- """
204
- import logging
205
-
206
- log = logging.getLogger("rmi.main")
207
- mounted = 0
208
-
209
- v1_modules = [
210
- "app.api.v1.auth.alerts",
211
- # auth/wallet.py does not exist (was never created) — skip
212
- "app.api.v1.public.wallet",
213
- "app.api.v1.public.token",
214
- "app.api.v1.public.scanner",
215
- "app.api.v1.rag.search",
216
- # x402 moved to app.domain.x402 (T34 v2) — old payments.py removed
217
- "app.api.v1.admin.alerts_webhook",
218
- "app.api.v1.catalog",
219
- "app.api.v1.mcp",
220
- "app.domain.news",
221
- "app.domain.news.admin_router",
222
- "app.domain.reports",
223
- "app.domain.x402",
224
- ]
225
-
226
- for module_path in v1_modules:
227
- try:
228
- import importlib
229
-
230
- module = importlib.import_module(module_path)
231
- router = getattr(module, "router", None)
232
- if router is None:
233
- log.warning(f"{module_path}: no router attribute, skipping")
234
- continue
235
- app.include_router(router)
236
- mounted += 1
237
- log.info(f"mounted {module_path}")
238
- except Exception as exc: # noqa: BLE001
239
- log.warning(
240
- f"failed to mount {module_path}: {type(exc).__name__}: {exc}"
241
- )
242
- return mounted
243
-
244
-
245
- def _try_mount_health_route() -> bool:
246
- """Mount kubernetes-grade /live /ready /health from app.core.health_route."""
247
- try:
248
- from app.core.health_route import router as health_router
249
-
250
- app.include_router(health_router)
251
- return True
252
- except Exception as exc: # noqa: BLE001
253
- import logging
254
-
255
- logging.getLogger("rmi.main").warning(f"health_route mount failed: {exc}")
256
- return False
257
-
258
-
259
- def _try_mount_metrics() -> bool:
260
- """Mount Prometheus /metrics endpoint."""
261
- try:
262
- from app.core.metrics import router as metrics_router
263
-
264
- app.include_router(metrics_router)
265
- return True
266
- except Exception as exc: # noqa: BLE001
267
- import logging
268
-
269
- logging.getLogger("rmi.main").warning(f"metrics_route mount failed: {exc}")
270
- return False
271
-
272
-
273
- # ── Mount what we can ────────────────────────────────────────────────
274
- _V1_MOUNTED = _try_mount_v1_routers()
275
- _HEALTH_MOUNTED = _try_mount_health_route()
276
- _METRICS_MOUNTED = _try_mount_metrics()
277
-
278
- import logging
279
-
280
- logging.getLogger("rmi.main").info(
281
- f"rmi_backend_ready v1_routers={_V1_MOUNTED} "
282
- f"health_route={_HEALTH_MOUNTED} metrics_route={_METRICS_MOUNTED} "
283
- f"total_routes={len(app.routes)}"
284
- )
285
-
286
 
287
  if __name__ == "__main__":
288
  import uvicorn
289
-
290
  uvicorn.run(
291
  "main:app",
292
- host="0.0.0.0",
293
  port=int(os.getenv("PORT", "8000")),
294
  log_level=os.getenv("LOG_LEVEL", "info").lower(),
295
  access_log=True,
 
1
+ """RMI Backend — root entry point.
2
 
3
+ Minimal delegates everything to app.factory.create_app().
 
4
 
5
+ Per v4.0 §T01 + modern factory pattern:
6
+ - main.py is the entry point ONLY
7
+ - All composition lives in app/factory.py
8
+ - Goal: <30 lines, single import, single uvicorn call
 
 
 
 
 
 
9
 
10
  Run: `python -u main.py` (CMD in Dockerfile)
11
  """
12
  from __future__ import annotations
13
 
14
  import os
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
+ from app.factory import create_app
17
 
18
+ app = create_app()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
  if __name__ == "__main__":
21
  import uvicorn
 
22
  uvicorn.run(
23
  "main:app",
24
+ host=os.getenv("HOST", "0.0.0.0"),
25
  port=int(os.getenv("PORT", "8000")),
26
  log_level=os.getenv("LOG_LEVEL", "info").lower(),
27
  access_log=True,