Files
breakpilot-compliance/backend-compliance/compliance/api/compliance_scope_routes.py
Sharang Parnerkar f1710fdb9e
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 34s
CI/CD / test-python-backend-compliance (push) Successful in 39s
CI/CD / test-python-document-crawler (push) Successful in 24s
CI/CD / test-python-dsms-gateway (push) Successful in 19s
CI/CD / validate-canonical-controls (push) Successful in 13s
CI/CD / Deploy (push) Successful in 2s
fix: migrate deployment from Hetzner to Coolify (#1)
## Summary
- Add Coolify deployment configuration (docker-compose, healthchecks, network setup)
- Replace deploy-hetzner CI job with Coolify webhook deploy
- Externalize postgres, qdrant, S3 for Coolify environment

## All changes since branch creation
- Coolify docker-compose with Traefik labels and healthchecks
- CI pipeline: deploy-hetzner → deploy-coolify (simple webhook curl)
- SQLAlchemy 2.x text() compatibility fixes
- Alpine-compatible Dockerfile fixes

Co-authored-by: Sharang Parnerkar <parnerkarsharang@gmail.com>
Reviewed-on: #1
2026-03-13 10:45:35 +00:00

136 lines
4.3 KiB
Python

"""
FastAPI routes for Compliance Scope persistence.
Stores the tenant's scope decision (frameworks, regulations, industry context)
in sdk_states.state->compliance_scope as JSONB.
Endpoints:
- GET /v1/compliance-scope?tenant_id=... → returns scope or 404
- POST /v1/compliance-scope → UPSERT scope (idempotent)
"""
import json
import logging
from typing import Any, Optional
from fastapi import APIRouter, HTTPException, Header
from pydantic import BaseModel
from sqlalchemy import text
from database import SessionLocal
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/v1/compliance-scope", tags=["compliance-scope"])
# =============================================================================
# REQUEST / RESPONSE MODELS
# =============================================================================
class ComplianceScopeRequest(BaseModel):
"""Scope selection submitted by the frontend wizard."""
scope: dict[str, Any]
tenant_id: Optional[str] = None
class ComplianceScopeResponse(BaseModel):
"""Persisted scope object returned to the frontend."""
tenant_id: str
scope: dict[str, Any]
updated_at: str
created_at: str
# =============================================================================
# HELPERS
# =============================================================================
def _get_tid(
x_tenant_id: Optional[str],
query_tenant_id: str,
) -> str:
return x_tenant_id or query_tenant_id or "default"
def _row_to_response(row) -> ComplianceScopeResponse:
"""Convert a DB row (tenant_id, scope, created_at, updated_at) to response."""
return ComplianceScopeResponse(
tenant_id=row[0],
scope=row[1] if isinstance(row[1], dict) else {},
created_at=str(row[2]),
updated_at=str(row[3]),
)
# =============================================================================
# ROUTES
# =============================================================================
@router.get("", response_model=ComplianceScopeResponse)
async def get_compliance_scope(
tenant_id: str = "default",
x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID"),
):
"""Return the persisted compliance scope for a tenant, or 404 if not set."""
tid = _get_tid(x_tenant_id, tenant_id)
db = SessionLocal()
try:
row = db.execute(
text("""SELECT tenant_id,
state->'compliance_scope' AS scope,
created_at,
updated_at
FROM sdk_states
WHERE tenant_id = :tid
AND state ? 'compliance_scope'"""),
{"tid": tid},
).fetchone()
if not row or row[1] is None:
raise HTTPException(status_code=404, detail="Compliance scope not found")
return _row_to_response(row)
finally:
db.close()
@router.post("", response_model=ComplianceScopeResponse)
async def upsert_compliance_scope(
body: ComplianceScopeRequest,
tenant_id: str = "default",
x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID"),
):
"""Create or update the compliance scope for a tenant (UPSERT)."""
tid = _get_tid(x_tenant_id, body.tenant_id or tenant_id)
scope_json = json.dumps(body.scope)
db = SessionLocal()
try:
db.execute(
text("""INSERT INTO sdk_states (tenant_id, state)
VALUES (:tid, jsonb_build_object('compliance_scope', :scope::jsonb))
ON CONFLICT (tenant_id) DO UPDATE
SET state = sdk_states.state || jsonb_build_object('compliance_scope', :scope::jsonb),
updated_at = NOW()"""),
{"tid": tid, "scope": scope_json},
)
db.commit()
row = db.execute(
text("""SELECT tenant_id,
state->'compliance_scope' AS scope,
created_at,
updated_at
FROM sdk_states
WHERE tenant_id = :tid"""),
{"tid": tid},
).fetchone()
return _row_to_response(row)
except Exception as e:
db.rollback()
logger.error(f"Failed to upsert compliance scope: {e}")
raise HTTPException(status_code=500, detail="Failed to save compliance scope")
finally:
db.close()