Spaces:
Running
Running
Download sinetti_api/errors.py from Couscouskabyle/iscc-app: direct link, hf CLI and curl.
- Browser
- Download file 6.93 kB
-
https://huggingface.co/spaces/Couscouskabyle/iscc-app/resolve/main/sinetti_api/errors.py
- Command line
-
hf download hf://spaces/Couscouskabyle/iscc-app/sinetti_api/errors.py
-
curl -L -o errors.py https://huggingface.co/spaces/Couscouskabyle/iscc-app/resolve/main/sinetti_api/errors.py
6.93 kB
| from __future__ import annotations | |
| import uuid | |
| from fastapi import Request | |
| from fastapi.exceptions import RequestValidationError | |
| from fastapi.responses import JSONResponse | |
| from starlette.exceptions import HTTPException as StarletteHTTPException | |
| import obs | |
| from sinetti_services import diagnostics_contract | |
| from sinetti_services.tenant_context import TenantRequired | |
| _ACTION_UNAVAILABLE_BODY = { | |
| "ok": False, | |
| "code": "action_unavailable", | |
| "message": "We couldn’t complete this action. Please try again later or contact support.", | |
| } | |
| def request_trace_id(request: Request) -> str: | |
| trace_id = str(getattr(request.state, "trace_id", "") or "") | |
| if trace_id: | |
| return trace_id | |
| trace_id = str(uuid.uuid4()) | |
| request.state.trace_id = trace_id | |
| return trace_id | |
| async def http_exception_handler(request: Request, exc: StarletteHTTPException) -> JSONResponse: | |
| code = { | |
| 400: "invalid_request", | |
| 401: "unauthorized", | |
| 403: "forbidden", | |
| 404: "not_found", | |
| 409: "conflict", | |
| 422: "validation_error", | |
| 503: "service_unavailable", | |
| }.get(exc.status_code, "http_error") | |
| trace_id = request_trace_id(request) | |
| headers = {**(exc.headers or {}), "X-Request-ID": trace_id} | |
| return JSONResponse( | |
| status_code=exc.status_code, | |
| headers=headers, | |
| content={ | |
| "error": { | |
| "code": code, | |
| "message": str(exc.detail or "Request failed"), | |
| "trace_id": trace_id, | |
| } | |
| }, | |
| ) | |
| async def request_validation_exception_handler( | |
| request: Request, | |
| exc: RequestValidationError, | |
| ) -> JSONResponse: | |
| trace_id = request_trace_id(request) | |
| # Locations and error types only — never the rejected input, which carries user content. | |
| try: | |
| fields = ",".join( | |
| sorted( | |
| { | |
| ".".join(str(part) for part in (error.get("loc") or ())[:4]) | |
| + ":" | |
| + str(error.get("type") or "") | |
| for error in (exc.errors() or ()) | |
| } | |
| ) | |
| )[:400] | |
| except Exception: # noqa: BLE001 — diagnostics must never mask the 422 | |
| fields = "" | |
| obs.error( | |
| "request_validation_failed", | |
| trace_id=trace_id, | |
| method=request.method, | |
| path=diagnostics_contract.log_path(request), | |
| fields=fields, | |
| ) | |
| return JSONResponse( | |
| status_code=422, | |
| headers={"X-Request-ID": trace_id}, | |
| content={ | |
| "error": { | |
| "code": "validation_error", | |
| "message": "Request validation failed.", | |
| "trace_id": trace_id, | |
| } | |
| }, | |
| ) | |
| async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONResponse: | |
| trace_id = request_trace_id(request) | |
| obs.error( | |
| "request_failed", | |
| trace_id=trace_id, | |
| method=request.method, | |
| path=diagnostics_contract.log_path(request), | |
| exception_class=type(exc).__name__, | |
| ) | |
| # Best-effort ops ledger append — never fail the HTTP 500 path if the write fails. | |
| try: | |
| from sinetti_services import ops_error_ledger | |
| # Class name only — never str(exc); exception text can carry secrets. | |
| ops_error_ledger.record_error( | |
| source="server", | |
| trace_id=trace_id, | |
| message=type(exc).__name__, | |
| path=diagnostics_contract.log_path(request), | |
| ) | |
| except Exception as ledger_exc: # noqa: BLE001 — ledger must not mask the original 500 | |
| obs.error( | |
| "ops_error_ledger_write_failed", | |
| trace_id=trace_id, | |
| error_type=type(ledger_exc).__name__, | |
| error=str(ledger_exc), | |
| ) | |
| return JSONResponse( | |
| status_code=500, | |
| headers={"X-Request-ID": trace_id}, | |
| content={ | |
| "error": { | |
| "code": "internal_error", | |
| "message": "The request could not be completed.", | |
| "trace_id": trace_id, | |
| } | |
| }, | |
| ) | |
| class ActionUnavailable(Exception): | |
| """A protected action could not safely be completed; its reason remains internal.""" | |
| def __init__(self, internal_reason: str) -> None: | |
| self.internal_reason = internal_reason | |
| super().__init__(internal_reason) | |
| async def action_unavailable_handler(request: Request, exc: ActionUnavailable) -> JSONResponse: | |
| trace_id = request_trace_id(request) | |
| obs.error( | |
| "action_unavailable", | |
| internal_reason=exc.internal_reason, | |
| trace_id=trace_id, | |
| method=request.method, | |
| path=diagnostics_contract.log_path(request), | |
| ) | |
| return JSONResponse(status_code=429, content=_ACTION_UNAVAILABLE_BODY) | |
| # Human-readable names for the plan-blocked message. Keep in sync with store_billing.FEATURE_KEYS. | |
| _FEATURE_DISPLAY_NAMES = { | |
| "counterparty_dd": "Counterparty Due Diligence", | |
| "business_footprint": "Company research", | |
| "csv_export": "CSV Export", | |
| "pdf_deep_dives": "PDF Deep-Dives", | |
| "market_balances": "Market Balances", | |
| "batch_check": "Batch check", | |
| "research_data": "Research data", | |
| "plant_table": "Plant table", | |
| } | |
| class FeatureBlocked(Exception): | |
| """Raised by sinetti_api.deps.require_feature when the caller's plan lacks `feature`. | |
| Handled as a graceful HTTP 200 envelope (never a bare 403) — see feature_blocked_handler. | |
| """ | |
| def __init__(self, feature: str, *, reason: str = "plan", message: str = "") -> None: | |
| self.feature = feature | |
| self.reason = reason | |
| self.message = message | |
| super().__init__(f"feature not entitled: {feature}") | |
| async def feature_blocked_handler(request: Request, exc: FeatureBlocked) -> JSONResponse: | |
| name = _FEATURE_DISPLAY_NAMES.get(exc.feature, exc.feature) | |
| return JSONResponse( | |
| status_code=200, | |
| content={ | |
| "ok": False, | |
| "blocked": True, | |
| "reason": exc.reason, | |
| "feature": exc.feature, | |
| "message": exc.message | |
| or f"{name} is not included in your plan — contact your administrator.", | |
| }, | |
| ) | |
| async def tenant_required_handler(request: Request, exc: TenantRequired) -> JSONResponse: | |
| trace_id = request_trace_id(request) | |
| message = str(exc) or "Organisation access must be assigned before using this feature." | |
| return JSONResponse( | |
| status_code=403, | |
| headers={"X-Request-ID": trace_id}, | |
| content={ | |
| "error": { | |
| "code": "tenant_required", | |
| "message": message, | |
| "trace_id": trace_id, | |
| } | |
| }, | |
| ) | |