u-toolkit 0.1.19__py3-none-any.whl → 0.1.20__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
u_toolkit/fastapi/cbv.py DELETED
@@ -1,440 +0,0 @@
1
- import inspect
2
- import re
3
- from collections.abc import Callable
4
- from enum import Enum, StrEnum, auto
5
- from functools import partial, update_wrapper, wraps
6
- from typing import (
7
- Generic,
8
- Literal,
9
- NamedTuple,
10
- NotRequired,
11
- TypeVar,
12
- TypedDict,
13
- cast,
14
- overload,
15
- )
16
-
17
- from fastapi import APIRouter, Depends
18
- from pydantic.alias_generators import to_snake
19
-
20
- from u_toolkit.decorators import DefineMethodParams, define_method_handler
21
- from u_toolkit.fastapi.helpers import get_depend_from_annotation, is_depend
22
- from u_toolkit.fastapi.responses import Response, build_responses
23
- from u_toolkit.helpers import is_annotated
24
- from u_toolkit.merge import deep_merge_dict
25
- from u_toolkit.signature import (
26
- list_parameters,
27
- update_parameters,
28
- with_parameter,
29
- )
30
-
31
-
32
- _T = TypeVar("_T")
33
-
34
-
35
- LiteralUpperMethod = Literal[
36
- "GET",
37
- "POST",
38
- "PATCH",
39
- "PUT",
40
- "DELETE",
41
- "OPTIONS",
42
- "HEAD",
43
- "TRACE",
44
- ]
45
- LiteralLowerMethod = Literal[
46
- "get",
47
- "post",
48
- "patch",
49
- "put",
50
- "delete",
51
- "options",
52
- "head",
53
- "trace",
54
- ]
55
-
56
-
57
- class Methods(StrEnum):
58
- GET = auto()
59
- POST = auto()
60
- PATCH = auto()
61
- PUT = auto()
62
- DELETE = auto()
63
- OPTIONS = auto()
64
- HEAD = auto()
65
- TRACE = auto()
66
-
67
-
68
- RequestMethod = Methods | LiteralLowerMethod | LiteralUpperMethod
69
-
70
-
71
- METHOD_PATTERNS = {
72
- method: re.compile(f"^({method}_|{method})", re.IGNORECASE)
73
- for method in Methods
74
- }
75
-
76
-
77
- _FnName = str
78
-
79
-
80
- _MethodInfo = tuple[RequestMethod, re.Pattern[str]]
81
-
82
-
83
- def get_method(name: str) -> _MethodInfo | None:
84
- for method, method_pattern in METHOD_PATTERNS.items():
85
- if method_pattern.search(name):
86
- return method, method_pattern
87
- return None
88
-
89
-
90
- class EndpointInfo(NamedTuple):
91
- handle: Callable
92
- original_handle_name: str
93
- handle_name: str
94
- method: RequestMethod
95
- method_pattern: re.Pattern
96
- path: str
97
-
98
-
99
- def iter_endpoints(
100
- cls: type[_T],
101
- valid_method: Callable[[str, Callable], list[_MethodInfo] | None]
102
- | None = None,
103
- ):
104
- prefix = ""
105
-
106
- if not cls.__name__.startswith("_"):
107
- prefix += f"/{to_snake(cls.__name__)}"
108
-
109
- for name, handle in inspect.getmembers(
110
- cls,
111
- lambda arg: inspect.ismethoddescriptor(arg) or inspect.isfunction(arg),
112
- ):
113
- paths = [prefix]
114
-
115
- methods: list[_MethodInfo] = []
116
- from_valid_method = False
117
-
118
- if valid_method:
119
- if results := valid_method(name, handle):
120
- methods.extend(results)
121
- if methods:
122
- from_valid_method = True
123
-
124
- if not methods and (method := get_method(name)):
125
- methods.append(method)
126
-
127
- for method, pattern in methods:
128
- handle_name = name if from_valid_method else pattern.sub("", name)
129
- path = handle_name.replace("__", "/")
130
- if path:
131
- paths.append(path)
132
-
133
- yield EndpointInfo(
134
- handle=handle,
135
- original_handle_name=name,
136
- handle_name=handle_name,
137
- path="/".join(paths),
138
- method=method,
139
- method_pattern=pattern,
140
- )
141
-
142
-
143
- def iter_dependencies(cls: type[_T]):
144
- _split = re.compile(r"\s+|:|=")
145
- dependencies: dict = dict(inspect.getmembers(cls, is_depend))
146
- for name, type_ in inspect.get_annotations(cls).items():
147
- if is_annotated(type_):
148
- dependency = get_depend_from_annotation(type_)
149
- dependencies[name] = dependency
150
-
151
- for line in inspect.getsource(cls).split("\n"):
152
- token: str = _split.split(line.strip(), 1)[0]
153
- for name, dep in dependencies.items():
154
- if name == token:
155
- yield token, dep
156
-
157
-
158
- class CBVRoutesInfo(TypedDict):
159
- path: NotRequired[str | None]
160
- tags: NotRequired[list[str | Enum] | None]
161
- dependencies: NotRequired[list | None]
162
- responses: NotRequired[list[Response] | None]
163
- deprecated: NotRequired[bool | None]
164
-
165
-
166
- CBVRoutesInfoT = TypeVar("CBVRoutesInfoT", bound=CBVRoutesInfo)
167
-
168
-
169
- class CBVRouteInfo(CBVRoutesInfo, Generic[_T]):
170
- methods: NotRequired[list[RequestMethod] | None]
171
- response_model: NotRequired[type[_T] | None]
172
- status: NotRequired[int | None]
173
- summary: NotRequired[str | None]
174
- description: NotRequired[str | None]
175
- name: NotRequired[str | None]
176
-
177
-
178
- class CBV(Generic[CBVRoutesInfoT]):
179
- def __init__(self, router: APIRouter | None = None) -> None:
180
- self.router = router or APIRouter()
181
-
182
- self.state: dict[type, dict[_FnName, CBVRouteInfo]] = {}
183
- self.routes_extra: dict[
184
- type,
185
- tuple[
186
- CBVRoutesInfoT | None,
187
- Callable[[type[_T]], _T] | None, # type: ignore
188
- ],
189
- ] = {}
190
- self.initialed_state: dict[type[_T], _T] = {} # type: ignore
191
-
192
- def create_route(
193
- self,
194
- *,
195
- cls: type[_T],
196
- path: str,
197
- method: RequestMethod,
198
- method_name: str,
199
- ):
200
- class_routes_info = self.routes_extra[cls][0] or {}
201
-
202
- class_tags = class_routes_info.get("tags") or []
203
- endpoint_tags: list[str | Enum] = (
204
- self.state[cls][method_name].get("tags") or []
205
- )
206
- tags = class_tags + endpoint_tags
207
-
208
- class_dependencies = class_routes_info.get("dependencies") or []
209
- endpoint_dependencies = (
210
- self.state[cls][method_name].get("dependencies") or []
211
- )
212
- dependencies = class_dependencies + endpoint_dependencies
213
-
214
- class_responses = class_routes_info.get("responses") or []
215
- endpoint_responses = (
216
- self.state[cls][method_name].get("responses") or []
217
- )
218
- responses = build_responses(*class_responses, *endpoint_responses)
219
-
220
- status_code = self.state[cls][method_name].get("status")
221
-
222
- deprecated = self.state[cls][method_name].get(
223
- "deprecated", class_routes_info.get("deprecated")
224
- )
225
-
226
- response_model = self.state[cls][method_name].get("response_model")
227
-
228
- endpoint_methods = [
229
- i.upper()
230
- for i in (self.state[cls][method_name].get("methods") or [method])
231
- ]
232
-
233
- path = self.state[cls][method_name].get("path") or path
234
-
235
- summary = self.state[cls][method_name].get("summary")
236
- description = self.state[cls][method_name].get("description")
237
- name = self.state[cls][method_name].get("name")
238
- return self.router.api_route(
239
- path,
240
- methods=endpoint_methods,
241
- tags=tags,
242
- dependencies=dependencies,
243
- response_model=response_model,
244
- responses=responses,
245
- status_code=status_code,
246
- deprecated=deprecated,
247
- summary=summary,
248
- description=description,
249
- name=name,
250
- )
251
-
252
- def info( # noqa: PLR0913
253
- self,
254
- *,
255
- path: str | None = None,
256
- methods: list[RequestMethod] | None = None,
257
- tags: list[str | Enum] | None = None,
258
- dependencies: list | None = None,
259
- responses: list[Response] | None = None,
260
- response_model: type[_T] | None = None,
261
- summary: str | None = None,
262
- description: str | None = None,
263
- name: str | None = None,
264
- status: int | None = None,
265
- deprecated: bool | None = None,
266
- ):
267
- state = self.state
268
- initial_state = self._initial_state
269
- data = CBVRouteInfo(
270
- path=path,
271
- methods=methods,
272
- tags=tags,
273
- dependencies=dependencies,
274
- responses=responses,
275
- response_model=response_model,
276
- status=status,
277
- deprecated=deprecated,
278
- summary=summary,
279
- description=description,
280
- name=name,
281
- )
282
-
283
- def handle(params: DefineMethodParams):
284
- initial_state(params.method_class)
285
- deep_merge_dict(
286
- state,
287
- {params.method_class: {params.method_name: data}},
288
- )
289
-
290
- return define_method_handler(handle)
291
-
292
- def _initial_state(self, cls: type[_T]) -> _T:
293
- if result := self.initialed_state.get(cls):
294
- return cast(_T, result)
295
-
296
- default_data = {}
297
- for endpoint in iter_endpoints(cls):
298
- default_data[endpoint.original_handle_name] = {}
299
-
300
- self.state.setdefault(cls, default_data)
301
- result = self._build_cls(cls)
302
- self.initialed_state[cls] = result
303
- return result
304
-
305
- def _build_cls(self, cls: type[_T]) -> _T:
306
- if cls in self.routes_extra and (build := self.routes_extra[cls][1]):
307
- return build(cls) # type: ignore
308
- return cls()
309
-
310
- def __create_class_dependencies_injector(self, cls: type[_T]):
311
- """将类的依赖添加到函数实例上
312
-
313
- ```python
314
- @cbv
315
- class A:
316
- a = Depends(lambda: id(object()))
317
-
318
- def get(self):
319
- # 使得每次 self.a 可以访问到当前请求的依赖
320
- print(self.a)
321
- ```
322
- """
323
-
324
- def collect_cls_dependencies(**kwargs):
325
- return kwargs
326
-
327
- parameters = [
328
- inspect.Parameter(
329
- name=name,
330
- kind=inspect.Parameter.POSITIONAL_OR_KEYWORD,
331
- default=dep,
332
- )
333
- for name, dep in iter_dependencies(cls)
334
- ]
335
-
336
- has_cls_deps = bool(parameters)
337
- if has_cls_deps:
338
- update_parameters(collect_cls_dependencies, *parameters)
339
-
340
- def new_fn(method_name, kwargs):
341
- instance = self._build_cls(cls)
342
- dependencies = kwargs.pop(collect_cls_dependencies.__name__, {})
343
- for dep_name, dep_value in dependencies.items():
344
- setattr(instance, dep_name, dep_value)
345
- return getattr(instance, method_name)
346
-
347
- def decorator(method: Callable):
348
- method_name = method.__name__
349
-
350
- cls_fn = getattr(cls, method_name)
351
- sign_cls_fn = partial(cls_fn)
352
- update_wrapper(sign_cls_fn, cls_fn)
353
-
354
- if has_cls_deps:
355
- parameters, *_ = with_parameter(
356
- sign_cls_fn,
357
- name=collect_cls_dependencies.__name__,
358
- default=Depends(collect_cls_dependencies),
359
- )
360
- else:
361
- parameters = list_parameters(sign_cls_fn)
362
-
363
- update_parameters(sign_cls_fn, *(parameters[1:]))
364
-
365
- if inspect.iscoroutinefunction(method):
366
-
367
- @wraps(sign_cls_fn)
368
- async def awrapper(*args, **kwargs):
369
- fn = new_fn(method_name, kwargs)
370
- return await fn(*args, **kwargs)
371
-
372
- return awrapper
373
-
374
- @wraps(sign_cls_fn)
375
- def wrapper(*args, **kwargs):
376
- fn = new_fn(method_name, kwargs)
377
- return fn(*args, **kwargs)
378
-
379
- return wrapper
380
-
381
- return decorator
382
-
383
- @overload
384
- def __call__(self, cls: type[_T], /) -> type[_T]: ...
385
- @overload
386
- def __call__(
387
- self,
388
- *,
389
- info: CBVRoutesInfoT | None = None,
390
- build: Callable[[type[_T]], _T] | None = None,
391
- ) -> Callable[[type[_T]], type[_T]]: ...
392
-
393
- def __call__(self, *args, **kwargs):
394
- info = None
395
- build: Callable | None = None
396
-
397
- def decorator(cls: type[_T]) -> type[_T]:
398
- instance = self._initial_state(cls)
399
- self.routes_extra[cls] = info, build
400
-
401
- decorator = self.__create_class_dependencies_injector(cls)
402
-
403
- def valid_method(
404
- name: str, _handle: Callable
405
- ) -> None | list[_MethodInfo]:
406
- if (cls_state := self.state.get(cls)) and (
407
- method_state := cls_state.get(name)
408
- ):
409
- methods: list[RequestMethod] = (
410
- method_state.get("methods") or []
411
- )
412
- result: list[_MethodInfo] = []
413
- for i in methods:
414
- method = Methods(i.lower())
415
- result.append((method, METHOD_PATTERNS[method]))
416
- return result
417
-
418
- return None
419
-
420
- for endpoint_info in iter_endpoints(cls, valid_method):
421
- route = self.create_route(
422
- cls=cls,
423
- path=endpoint_info.path,
424
- method=endpoint_info.method,
425
- method_name=endpoint_info.original_handle_name,
426
- )
427
- method = getattr(instance, endpoint_info.original_handle_name)
428
- endpoint = decorator(method)
429
- endpoint.__name__ = endpoint_info.handle_name
430
- route(endpoint)
431
-
432
- return cls
433
-
434
- if args:
435
- return decorator(args[0])
436
-
437
- info = kwargs.get("info") or None
438
- build = kwargs.get("build")
439
-
440
- return decorator
@@ -1,9 +0,0 @@
1
- from pydantic_settings import BaseSettings, SettingsConfigDict
2
-
3
-
4
- class FastAPISettings(BaseSettings):
5
- model_config = SettingsConfigDict(
6
- env_prefix="FASTAPI_",
7
- env_file=(".env", ".env.prod", ".env.dev", ".env.test"),
8
- extra="ignore",
9
- )
@@ -1,163 +0,0 @@
1
- import inspect
2
- from collections.abc import Callable, Sequence
3
- from typing import Any, Generic, Literal, Protocol, TypeVar, cast
4
-
5
- from fastapi import Response, status
6
- from fastapi.responses import JSONResponse, ORJSONResponse
7
- from fastapi.utils import is_body_allowed_for_status_code
8
- from pydantic import BaseModel, create_model
9
-
10
- from u_toolkit.pydantic.type_vars import BaseModelT
11
-
12
-
13
- try:
14
- import orjson # type: ignore
15
- except ImportError: # pragma: nocover
16
- orjson = None # type: ignore
17
-
18
-
19
- class WrapperError(BaseModel, Generic[BaseModelT]):
20
- @classmethod
21
- def create(
22
- cls: type["WrapperError[BaseModelT]"],
23
- model: BaseModelT,
24
- ) -> "WrapperError[BaseModelT]":
25
- raise NotImplementedError
26
-
27
-
28
- WrapperErrorT = TypeVar("WrapperErrorT", bound=WrapperError)
29
-
30
-
31
- class EndpointError(WrapperError[BaseModelT], Generic[BaseModelT]):
32
- error: BaseModelT
33
-
34
- @classmethod
35
- def create(cls, model: BaseModelT):
36
- return cls(error=model)
37
-
38
-
39
- class HTTPErrorInterface(Protocol):
40
- status: int
41
-
42
- @classmethod
43
- def response_class(cls) -> type[BaseModel]: ...
44
-
45
-
46
- class NamedHTTPError(Exception, Generic[WrapperErrorT, BaseModelT]):
47
- status: int = status.HTTP_400_BAD_REQUEST
48
- code: str | None = None
49
- targets: Sequence[Any] | None = None
50
- target_transform: Callable[[Any], Any] | None = None
51
- message: str | None = None
52
- wrapper: (
53
- type[WrapperError[BaseModelT]]
54
- | tuple[WrapperErrorT, Callable[[BaseModelT], WrapperErrorT]]
55
- | None
56
- ) = None
57
-
58
- @classmethod
59
- def error_name(cls):
60
- return cls.__name__.removesuffix("Error")
61
-
62
- @classmethod
63
- def model_class(cls) -> type[BaseModelT]:
64
- type_ = cls.error_name()
65
- error_code = cls.code or type_
66
- kwargs = {
67
- "code": (Literal[error_code], ...),
68
- "message": (Literal[cls.message] if cls.message else str, ...),
69
- }
70
- if cls.targets:
71
- kwargs["target"] = (Literal[*cls.transformed_targets()], ...)
72
-
73
- return cast(type[BaseModelT], create_model(f"{type_}Model", **kwargs))
74
-
75
- @classmethod
76
- def error_code(cls):
77
- return cls.code or cls.error_name()
78
-
79
- @classmethod
80
- def transformed_targets(cls) -> list[str]:
81
- if cls.targets:
82
- result = []
83
- for i in cls.targets:
84
- if cls.target_transform:
85
- result.append(cls.target_transform(i))
86
- else:
87
- result.append(i)
88
- return result
89
- return []
90
-
91
- def __init__(
92
- self,
93
- *,
94
- message: str | None = None,
95
- target: str | None = None,
96
- headers: dict[str, str] | None = None,
97
- ) -> None:
98
- kwargs: dict[str, Any] = {
99
- "code": self.error_code(),
100
- "message": message or self.message or "operation failed",
101
- }
102
-
103
- if target:
104
- if self.target_transform:
105
- target = self.target_transform(target)
106
- kwargs["target"] = target
107
- kwargs["message"] = kwargs["message"].format(target=target)
108
-
109
- self.model = self.model_class()(**kwargs)
110
- create: Callable[[BaseModelT], BaseModel] | None = None
111
- if inspect.isclass(self.wrapper):
112
- create = self.wrapper.create
113
- elif isinstance(self.wrapper, tuple):
114
- create = self.wrapper[1]
115
- self.data: BaseModel = (
116
- create(self.model) if create is not None else self.model
117
- )
118
-
119
- self.headers = headers
120
-
121
- def __str__(self) -> str:
122
- return f"{self.status}: {self.data.error.code}" # type: ignore
123
-
124
- def __repr__(self) -> str:
125
- return f"{self.model_class: str(self.error)}"
126
-
127
- @classmethod
128
- def response_class(cls):
129
- model = cls.model_class()
130
-
131
- if cls.wrapper:
132
- wrapper: Any
133
- if inspect.isclass(cls.wrapper):
134
- wrapper = cls.wrapper
135
- else:
136
- wrapper = cls.wrapper[0]
137
- return wrapper[model]
138
-
139
- return model
140
-
141
- @classmethod
142
- def response_schema(cls):
143
- return {cls.status: {"model": cls.response_class()}}
144
-
145
-
146
- def named_http_error_handler(_, exc: NamedHTTPError):
147
- headers = exc.headers
148
-
149
- if not is_body_allowed_for_status_code(exc.status):
150
- return Response(status_code=exc.status, headers=headers)
151
-
152
- if orjson:
153
- return ORJSONResponse(
154
- exc.data.model_dump(exclude_none=True),
155
- status_code=exc.status,
156
- headers=headers,
157
- )
158
-
159
- return JSONResponse(
160
- exc.data.model_dump(exclude_none=True),
161
- status_code=exc.status,
162
- headers=headers,
163
- )
@@ -1,18 +0,0 @@
1
- from typing import Annotated, Any, get_args
2
-
3
- from fastapi.params import Depends
4
-
5
-
6
- def is_depend(value: Any):
7
- return isinstance(value, Depends)
8
-
9
-
10
- def get_depend_from_annotation(annotation: Annotated):
11
- args = list(get_args(annotation))
12
- # 因为 FastAPI 好像也是取最后的依赖运行的, 所以这里也将参数反转
13
- args.reverse()
14
- for arg in args:
15
- if is_depend(arg):
16
- return arg
17
-
18
- raise ValueError
@@ -1,67 +0,0 @@
1
- import asyncio
2
- from collections.abc import Awaitable, Callable, Coroutine
3
- from contextlib import (
4
- AbstractAsyncContextManager,
5
- AbstractContextManager,
6
- AsyncExitStack,
7
- asynccontextmanager,
8
- )
9
- from typing import TypeVar
10
-
11
- from fastapi import FastAPI
12
-
13
-
14
- Hook = Callable[
15
- [FastAPI],
16
- Awaitable[None] | Coroutine[None, None, None] | None,
17
- ]
18
-
19
- HookT = TypeVar("HookT", bound=Hook)
20
-
21
- ContextManager = Callable[
22
- [FastAPI],
23
- AbstractContextManager | AbstractAsyncContextManager,
24
- ]
25
-
26
- ContextManagerT = TypeVar("ContextManagerT", bound=ContextManager)
27
-
28
-
29
- class Lifespan:
30
- def __init__(self) -> None:
31
- self._startup_hooks: list[Hook] = []
32
- self._shutdown_hooks: list[Hook] = []
33
- self._context_managers: list[ContextManager] = []
34
-
35
- def on_startup(self, fn: HookT) -> HookT:
36
- self._startup_hooks.append(fn)
37
- return fn
38
-
39
- def on_shutdown(self, fn: HookT) -> HookT:
40
- self._shutdown_hooks.append(fn)
41
- return fn
42
-
43
- def on_context(self, fn: ContextManagerT) -> ContextManagerT:
44
- self._context_managers.append(fn)
45
- return fn
46
-
47
- @asynccontextmanager
48
- async def __call__(self, _app: FastAPI):
49
- for hook in self._startup_hooks:
50
- ret = hook(_app)
51
- if asyncio.iscoroutine(ret):
52
- await ret
53
-
54
- async with AsyncExitStack() as stack:
55
- for ctx in self._context_managers:
56
- i = ctx(_app)
57
- if isinstance(i, AbstractContextManager):
58
- stack.enter_context(i)
59
- elif isinstance(i, AbstractAsyncContextManager):
60
- await stack.enter_async_context(i)
61
-
62
- yield
63
-
64
- for hook in self._shutdown_hooks:
65
- ret = hook(_app)
66
- if asyncio.iscoroutine(ret):
67
- await ret