|
1 | | -from typing import Annotated |
| 1 | +import builtins |
| 2 | +from typing import Annotated, Any, Protocol |
2 | 3 |
|
3 | 4 | import pymongo |
4 | 5 | from fastapi import Depends |
5 | 6 | from src.adapters.crud_store.adapter_mongodb import MongoDBCRUDRepository |
6 | | -from src.config.dependencies import DMongoDBDatabase |
| 7 | +from src.config.dependencies import DMongoDBDatabase, GlobalDependencies |
| 8 | +from src.config.environment_variables import EnvironmentVariables, StoragePhase |
7 | 9 | from src.domain.entities.states import StateEntity |
8 | 10 | from src.utils.logging import make_logger |
9 | 11 |
|
10 | 12 | logger = make_logger(__name__) |
11 | 13 |
|
12 | 14 |
|
| 15 | +class TaskStateRepositoryProtocol(Protocol): |
| 16 | + """Contract every task-state storage backend must satisfy. |
| 17 | +
|
| 18 | + Covers everything callers actually invoke through the DTaskStateRepository |
| 19 | + seam: the states use case and authorization shortcuts (create / get / |
| 20 | + update / delete / list), the retention service (find_by_field / |
| 21 | + delete_by_field / batch_create), and get_by_task_and_agent. |
| 22 | +
|
| 23 | + Behavioral requirements beyond the signatures: `.id` is presented as a |
| 24 | + string; `create` honors caller-supplied created_at/updated_at and only |
| 25 | + falls back to server time when absent; missing rows raise ItemDoesNotExist |
| 26 | + and duplicates raise DuplicateItemError; and `list` accepts the |
| 27 | + Mongo-shaped filter dict the states use case builds (plain equality plus |
| 28 | + `{"$in": [...]}` for the authorized-task allow-list). |
| 29 | + """ |
| 30 | + |
| 31 | + async def create(self, item: StateEntity) -> StateEntity: ... |
| 32 | + |
| 33 | + async def batch_create( |
| 34 | + self, items: builtins.list[StateEntity] |
| 35 | + ) -> builtins.list[StateEntity]: ... |
| 36 | + |
| 37 | + async def get( |
| 38 | + self, id: str | None = None, name: str | None = None |
| 39 | + ) -> StateEntity | None: ... |
| 40 | + |
| 41 | + async def update(self, item: StateEntity) -> StateEntity: ... |
| 42 | + |
| 43 | + async def delete(self, id: str | None = None, name: str | None = None) -> None: ... |
| 44 | + |
| 45 | + async def list( |
| 46 | + self, |
| 47 | + filters: dict[str, Any] | None = None, |
| 48 | + limit: int | None = None, |
| 49 | + page_number: int | None = None, |
| 50 | + order_by: str | None = None, |
| 51 | + order_direction: str | None = None, |
| 52 | + ) -> builtins.list[StateEntity]: ... |
| 53 | + |
| 54 | + async def find_by_field( |
| 55 | + self, |
| 56 | + field_name: str, |
| 57 | + field_value: Any, |
| 58 | + limit: int | None = None, |
| 59 | + page_number: int | None = None, |
| 60 | + sort_by: dict[str, int] | None = None, |
| 61 | + filters: dict[str, Any] | None = None, |
| 62 | + ) -> builtins.list[StateEntity]: ... |
| 63 | + |
| 64 | + async def delete_by_field(self, field_name: str, field_value: Any) -> int: ... |
| 65 | + |
| 66 | + async def get_by_task_and_agent( |
| 67 | + self, task_id: str, agent_id: str |
| 68 | + ) -> StateEntity | None: ... |
| 69 | + |
| 70 | + |
13 | 71 | class TaskStateRepository(MongoDBCRUDRepository[StateEntity]): |
14 | 72 | """Repository for managing task states in MongoDB.""" |
15 | 73 |
|
@@ -47,4 +105,27 @@ async def get_by_task_and_agent( |
47 | 105 | return self._deserialize(doc) if doc else None |
48 | 106 |
|
49 | 107 |
|
50 | | -DTaskStateRepository = Annotated[TaskStateRepository, Depends(TaskStateRepository)] |
| 108 | +def get_task_state_repository() -> TaskStateRepositoryProtocol: |
| 109 | + """Select the task-state repository for the configured storage phase. |
| 110 | +
|
| 111 | + This is the single construction point for task-state repositories: the |
| 112 | + FastAPI seam below resolves through it, and so must every site that builds |
| 113 | + the repository by hand outside Depends (the Temporal factories), so that a |
| 114 | + phase switch applies to request handlers and workers alike. |
| 115 | +
|
| 116 | + Each branch constructs its repository lazily — the Postgres phase must |
| 117 | + never touch a Mongo handle, which is what allows the MongoDB connection to |
| 118 | + be absent entirely once no store needs it. |
| 119 | + """ |
| 120 | + phase = EnvironmentVariables.refresh().TASK_STATE_STORAGE_PHASE |
| 121 | + if phase == StoragePhase.MONGODB: |
| 122 | + return TaskStateRepository(GlobalDependencies().mongodb_database) |
| 123 | + raise NotImplementedError( |
| 124 | + f"TASK_STATE_STORAGE_PHASE={phase.value!r} is not implemented yet; " |
| 125 | + "only 'mongodb' is currently supported." |
| 126 | + ) |
| 127 | + |
| 128 | + |
| 129 | +DTaskStateRepository = Annotated[ |
| 130 | + TaskStateRepositoryProtocol, Depends(get_task_state_repository) |
| 131 | +] |
0 commit comments