"""AwoooP Phase 2.4: Project ID Context Variable ================================================ 2026-05-04 ogt + Claude Sonnet 4.6(ADR-123 background loop tagging) 設計原則: - Python asyncio.create_task() 自動繼承父任務的 ContextVar 值 - 起始流程不再在 lifespan 強制寫入固定 PROJECT_ID;呼叫端需明確提供 project_id - get_db_context() 僅接受明確參數或已注入的 contextvar 作為 tenant 來源 - 多租戶未來:呼叫端傳入不同 project_id 即可隔離,無需改 loop 本體 """ from __future__ import annotations from contextvars import ContextVar, Token # 追蹤當前非同步任務的 project_id # Fail-Closed: 移除 default="awoooi",進 DB 路徑需要明確租戶標籤 PROJECT_ID: ContextVar[str | None] = ContextVar("project_id") PROJECT_ID_SOURCE: ContextVar[str | None] = ContextVar("project_id_source") PROJECT_ID_REQUEST_ID: ContextVar[str | None] = ContextVar("project_id_request_id") def set_project_context( project_id: str | None, source: str = "runtime", request_id: str | None = None, ) -> tuple[Token[str | None], Token[str | None], Token[str | None]]: """ 設定當前 request/context 的 project 上下文,並回傳 ContextVar token 供 restore。 """ return ( PROJECT_ID.set(project_id), PROJECT_ID_SOURCE.set(source), PROJECT_ID_REQUEST_ID.set(request_id), ) def clear_project_context(tokens: tuple[Token[str | None], Token[str | None], Token[str | None]]) -> None: """清除 request 上下文,回復前一個 ContextVar 狀態。""" PROJECT_ID_REQUEST_ID.reset(tokens[2]) PROJECT_ID_SOURCE.reset(tokens[1]) PROJECT_ID.reset(tokens[0]) def get_project_context() -> dict[str, str | None]: """取得目前上下文快照(可直接寫入 audit log)。""" return { "project_id": PROJECT_ID.get(None), "source": PROJECT_ID_SOURCE.get(None), "request_id": PROJECT_ID_REQUEST_ID.get(None), } def get_current_project_id() -> str | None: """取得當前任務的 project_id(給 service 層使用)""" return PROJECT_ID.get(None) def get_current_project_context() -> dict[str, str | None]: """取得可追溯上下文(同 get_project_context,保留 API 命名)。""" return get_project_context()