Coverage for src/extratools_api/crudl.py: 0%
72 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-13 19:09 -0700
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-13 19:09 -0700
1import json
2from collections.abc import Awaitable, Callable, Iterable, Mapping, MutableMapping
3from http import HTTPStatus
4from typing import Annotated, Any, cast
6from extratools_core.crudl import CRUDLWrapper, RLWrapper
7from extratools_core.json import JsonDict
8from fastapi import Body, FastAPI, HTTPException
9from pydantic import BaseModel, ConfigDict, ValidationError
12def add_crudl_endpoints[KT: str, VT: JsonDict | BaseModel](
13 app: FastAPI,
14 path_prefix: str,
15 *,
16 create_func: Callable[[KT, JsonDict], Awaitable[VT | None]] | None = None,
17 read_func: Callable[[KT], Awaitable[VT]] | None = None,
18 update_func: Callable[[KT, JsonDict], Awaitable[VT | None]] | None = None,
19 delete_func: Callable[[KT], Awaitable[VT | None]] | None = None,
20 list_func: Callable[[JsonDict | None], Awaitable[Iterable[tuple[KT, Any]]]] | None = None,
21) -> None:
22 path_prefix = path_prefix.rstrip("/")
24 if create_func:
25 @app.put(path_prefix + "/{identifier}")
26 async def create_endpoint(
27 identifier: KT,
28 put_body: Annotated[JsonDict, Body()],
29 ) -> VT | None:
30 return await create_func(
31 identifier,
32 put_body,
33 )
35 if read_func:
36 @app.get(path_prefix + "/{identifier}")
37 async def read_endpoint(identifier: KT) -> VT:
38 return await read_func(
39 identifier,
40 )
42 if update_func:
43 @app.patch(path_prefix + "/{identifier}")
44 async def update_endpoint(
45 identifier: KT,
46 patch_body: Annotated[JsonDict, Body()],
47 ) -> VT | None:
48 return await update_func(
49 identifier,
50 patch_body,
51 )
53 if delete_func:
54 @app.delete(path_prefix + "/{identifier}")
55 async def delete_endpoint(identifier: KT) -> VT | None:
56 return await delete_func(identifier)
58 if list_func:
59 @app.get(path_prefix + "/")
60 async def list_endpoint(filter_body: str | None = None) -> dict[KT, Any]:
61 try:
62 return dict(await list_func(
63 json.loads(filter_body) if filter_body
64 else None,
65 ))
66 except json.JSONDecodeError as e:
67 raise HTTPException(HTTPStatus.BAD_REQUEST) from e
70class FilterKeys(BaseModel):
71 model_config = ConfigDict(extra="forbid")
73 includes: list[str] | None = None
74 excludes: list[str] | None = None
77def add_crudl_endpoints_for_mapping[KT: str, VT: JsonDict](
78 app: FastAPI,
79 path_prefix: str,
80 mapping: Mapping[KT, VT],
81 *,
82 values_in_list: bool = False,
83 readonly: bool | None = None,
84) -> None:
85 mutable: bool = isinstance(mapping, MutableMapping)
86 if readonly is None:
87 readonly = not mutable
89 async def read_func(key: KT) -> VT:
90 try:
91 return crudl_store.read(key)
92 except KeyError as e:
93 raise HTTPException(HTTPStatus.NOT_FOUND) from e
95 async def list_func(filter_keys: JsonDict | None) -> Iterable[tuple[KT, VT | None]]:
96 try:
97 filter_keys_model: FilterKeys | None = (
98 FilterKeys.model_validate(filter_keys, strict=True) if filter_keys
99 else None
100 )
101 except ValidationError as e:
102 raise HTTPException(HTTPStatus.BAD_REQUEST) from e
104 return crudl_store.list(
105 None if filter_keys_model is None
106 else lambda key: (
107 (filter_keys_model.includes is None or key in (filter_keys_model.includes))
108 and key not in (filter_keys_model.excludes or [])
109 ),
110 )
112 if mutable and not readonly:
113 crudl_store = CRUDLWrapper[KT, VT](
114 mapping,
115 values_in_list=values_in_list,
116 )
118 async def create_func(key: KT, value: JsonDict) -> None:
119 try:
120 crudl_store.create(key, cast("VT", value))
121 except KeyError as e:
122 raise HTTPException(HTTPStatus.CONFLICT) from e
124 async def update_func(key: KT, value: JsonDict) -> None:
125 try:
126 crudl_store.update(key, cast("VT", value))
127 except KeyError as e:
128 raise HTTPException(HTTPStatus.NOT_FOUND) from e
130 async def delete_func(key: KT) -> None:
131 try:
132 crudl_store.delete(key)
133 except KeyError as e:
134 raise HTTPException(HTTPStatus.NOT_FOUND) from e
136 add_crudl_endpoints(
137 app,
138 path_prefix,
139 read_func=read_func,
140 list_func=list_func,
141 create_func=create_func,
142 update_func=update_func,
143 delete_func=delete_func,
144 )
145 else:
146 crudl_store = RLWrapper[KT, VT](
147 mapping,
148 values_in_list=values_in_list,
149 )
151 add_crudl_endpoints(
152 app,
153 path_prefix,
154 read_func=read_func,
155 list_func=list_func,
156 )