u-toolkit 0.1.0__tar.gz

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.
Files changed (39) hide show
  1. u_toolkit-0.1.0/.github/workflows/publish.yml +0 -0
  2. u_toolkit-0.1.0/.gitignore +10 -0
  3. u_toolkit-0.1.0/PKG-INFO +10 -0
  4. u_toolkit-0.1.0/README.md +0 -0
  5. u_toolkit-0.1.0/experimental.py +0 -0
  6. u_toolkit-0.1.0/pyproject.toml +89 -0
  7. u_toolkit-0.1.0/src/u_toolkit/__init__.py +9 -0
  8. u_toolkit-0.1.0/src/u_toolkit/alias_generators.py +49 -0
  9. u_toolkit-0.1.0/src/u_toolkit/datetime.py +30 -0
  10. u_toolkit-0.1.0/src/u_toolkit/decorators.py +46 -0
  11. u_toolkit-0.1.0/src/u_toolkit/enum.py +43 -0
  12. u_toolkit-0.1.0/src/u_toolkit/fastapi/__init__.py +0 -0
  13. u_toolkit-0.1.0/src/u_toolkit/fastapi/cbv.py +342 -0
  14. u_toolkit-0.1.0/src/u_toolkit/fastapi/config.py +9 -0
  15. u_toolkit-0.1.0/src/u_toolkit/fastapi/exception.py +115 -0
  16. u_toolkit-0.1.0/src/u_toolkit/fastapi/helpers.py +18 -0
  17. u_toolkit-0.1.0/src/u_toolkit/fastapi/lifespan.py +67 -0
  18. u_toolkit-0.1.0/src/u_toolkit/fastapi/pagination.py +79 -0
  19. u_toolkit-0.1.0/src/u_toolkit/fastapi/responses.py +66 -0
  20. u_toolkit-0.1.0/src/u_toolkit/function.py +12 -0
  21. u_toolkit-0.1.0/src/u_toolkit/helpers.py +5 -0
  22. u_toolkit-0.1.0/src/u_toolkit/logger.py +11 -0
  23. u_toolkit-0.1.0/src/u_toolkit/merge.py +31 -0
  24. u_toolkit-0.1.0/src/u_toolkit/object.py +0 -0
  25. u_toolkit-0.1.0/src/u_toolkit/path.py +4 -0
  26. u_toolkit-0.1.0/src/u_toolkit/pydantic/__init__.py +0 -0
  27. u_toolkit-0.1.0/src/u_toolkit/pydantic/fields.py +24 -0
  28. u_toolkit-0.1.0/src/u_toolkit/pydantic/models.py +41 -0
  29. u_toolkit-0.1.0/src/u_toolkit/pydantic/type_vars.py +6 -0
  30. u_toolkit-0.1.0/src/u_toolkit/signature.py +74 -0
  31. u_toolkit-0.1.0/src/u_toolkit/sqlalchemy/__init__.py +0 -0
  32. u_toolkit-0.1.0/src/u_toolkit/sqlalchemy/fields.py +0 -0
  33. u_toolkit-0.1.0/src/u_toolkit/sqlalchemy/function.py +12 -0
  34. u_toolkit-0.1.0/src/u_toolkit/sqlalchemy/orm/__init__.py +0 -0
  35. u_toolkit-0.1.0/src/u_toolkit/sqlalchemy/orm/fields.py +20 -0
  36. u_toolkit-0.1.0/src/u_toolkit/sqlalchemy/orm/models.py +23 -0
  37. u_toolkit-0.1.0/src/u_toolkit/sqlalchemy/table_info.py +17 -0
  38. u_toolkit-0.1.0/src/u_toolkit/sqlalchemy/type_vars.py +6 -0
  39. u_toolkit-0.1.0/uv.lock +481 -0
File without changes
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: u-toolkit
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: pydantic>=2.11.3
7
+ Provides-Extra: fastapi
8
+ Requires-Dist: pydantic-settings>=2.9.1; extra == 'fastapi'
9
+ Provides-Extra: sqlalchemy
10
+ Requires-Dist: sqlalchemy>=2.0.40; extra == 'sqlalchemy'
File without changes
File without changes
@@ -0,0 +1,89 @@
1
+ [project]
2
+ name = "u-toolkit"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ dependencies = [
8
+ "pydantic>=2.11.3",
9
+ ]
10
+
11
+ [project.scripts]
12
+ u-toolkit = "u_toolkit:main"
13
+
14
+ [project.optional-dependencies]
15
+ sqlalchemy = [
16
+ "sqlalchemy>=2.0.40",
17
+ ]
18
+ fastapi = [
19
+ "pydantic-settings>=2.9.1",
20
+ ]
21
+
22
+ [build-system]
23
+ requires = ["hatchling"]
24
+ build-backend = "hatchling.build"
25
+
26
+ [dependency-groups]
27
+ dev = [
28
+ "devtools>=0.12.2",
29
+ "pytest>=8.3.5",
30
+ "ruff>=0.11.6",
31
+ "uvicorn>=0.34.2",
32
+ ]
33
+ fastapi = [
34
+ "fastapi>=0.115.12",
35
+ ]
36
+
37
+
38
+
39
+ [tool.ruff]
40
+ line-length = 79
41
+ fix = true
42
+
43
+ [tool.ruff.format]
44
+ quote-style = "double"
45
+ skip-magic-trailing-comma = false
46
+ docstring-code-format = true
47
+ docstring-code-line-length = 72
48
+
49
+ [tool.ruff.lint]
50
+ # select = [
51
+ # "F", # Pyflakes
52
+ # "E", # pycodestyle
53
+ # "I", # isort
54
+ # "N", # pep8-naming
55
+ # "UP", # pyupgrade
56
+ # "B", # flake8-bugbear
57
+ # "A", # flake8-builtins,
58
+ # "DJ", # flake8-django
59
+ # "ISC", # flake8-implicit-str-concat
60
+ # "ICN", # flake8-import-conventions
61
+ # "SIM", # flake8-simplify
62
+ # "PTH", # flake8-use-pathlib
63
+ # ]
64
+ # ignore = ["E111", "E114", "E117", "B008", "ISC001"]
65
+ select = ["ALL"]
66
+ ignore = ["D", "ANN", "E114", "E117", "B008", "ISC001", "PGH003", "FA102", "PLW2901", "DTZ005", "COM812", "TCH", "TD", "FIX001", "FIX002", "S605", "S607", "S101", "RUF001", "EM101", "TRY003", "ERA001", "B010"]
67
+
68
+ [tool.ruff.lint.per-file-ignores]
69
+ "**/__init__.py" = ["F401"]
70
+ "tests/**/*.py" = ["UP031", "E402"]
71
+ "notebook/**/*.ipynb" = ["ALL"]
72
+ "src/main.py" = ["F401"]
73
+
74
+ # https://docs.astral.sh/ruff/settings/#lintflake8-errmsg
75
+ # Maximum string length for string literals in exception messages.
76
+ [tool.ruff.lint.flake8-errmsg]
77
+ max-string-length = 20
78
+
79
+ # https://docs.astral.sh/ruff/settings/#lintisort
80
+ [tool.ruff.lint.isort]
81
+ case-sensitive = true
82
+ lines-after-imports = 2
83
+
84
+ # https://docs.astral.sh/ruff/settings/#lint_pycodestyle_max-doc-length
85
+ [tool.ruff.lint.pycodestyle]
86
+ max-doc-length = 72
87
+
88
+ [tool.ruff.lint.pylint]
89
+ max-args = 8
@@ -0,0 +1,9 @@
1
+ def get(obj: dict, key: str, fallback=None):
2
+ items = key.split(".", 1)
3
+ if len(items) > 1:
4
+ for i in items:
5
+ return get(obj, i, fallback)
6
+ elif items:
7
+ return obj.get(key, fallback)
8
+
9
+ return fallback
@@ -0,0 +1,49 @@
1
+ from typing import TypeVarTuple, overload
2
+
3
+ from pydantic import alias_generators
4
+ from pydantic.fields import ComputedFieldInfo, FieldInfo
5
+
6
+
7
+ Ts = TypeVarTuple("Ts")
8
+
9
+
10
+ @overload
11
+ def to_camel(string: str, _: ComputedFieldInfo | FieldInfo) -> str: ...
12
+
13
+
14
+ @overload
15
+ def to_camel(string: str) -> str: ...
16
+
17
+
18
+ def to_camel(string: str, *_, **__):
19
+ if string.isupper():
20
+ return string
21
+ return alias_generators.to_camel(string)
22
+
23
+
24
+ @overload
25
+ def to_snake(string: str, _: ComputedFieldInfo | FieldInfo) -> str: ...
26
+
27
+
28
+ @overload
29
+ def to_snake(string: str) -> str: ...
30
+
31
+
32
+ def to_snake(string: str, *_, **__):
33
+ if string.isupper():
34
+ return string
35
+ return alias_generators.to_snake(string)
36
+
37
+
38
+ @overload
39
+ def to_pascal(string: str, _: ComputedFieldInfo | FieldInfo) -> str: ...
40
+
41
+
42
+ @overload
43
+ def to_pascal(string: str) -> str: ...
44
+
45
+
46
+ def to_pascal(string: str, *_, **__):
47
+ if string.isupper():
48
+ return string
49
+ return alias_generators.to_pascal(string)
@@ -0,0 +1,30 @@
1
+ from datetime import UTC, datetime
2
+
3
+
4
+ def to_utc(dt: datetime, /) -> datetime:
5
+ """转成 UTC 时间
6
+
7
+ :param dt: 时间
8
+ :return: UTC 时间
9
+ """
10
+ if dt.tzinfo is None:
11
+ return dt.replace(tzinfo=UTC)
12
+ return dt.astimezone(UTC)
13
+
14
+
15
+ def to_naive(dt: datetime, /) -> datetime:
16
+ """去除时区标识
17
+
18
+ :param v: 时间
19
+ :return: 不带时区标识的时间
20
+ """
21
+ return dt.replace(tzinfo=None)
22
+
23
+
24
+ def to_utc_naive(dt: datetime, /) -> datetime:
25
+ """将时间转换成不带时区标识的 UTC 时间
26
+
27
+ :param v: 时间
28
+ :return: 不带时区标识的 UTC 时间
29
+ """
30
+ return to_naive(to_utc(dt))
@@ -0,0 +1,46 @@
1
+ from collections.abc import Callable
2
+ from functools import wraps
3
+ from typing import Generic, NamedTuple, TypeVar
4
+
5
+ from u_toolkit.signature import list_parameters, update_parameters
6
+
7
+
8
+ _FnT = TypeVar("_FnT", bound=Callable)
9
+
10
+ _T = TypeVar("_T")
11
+
12
+
13
+ class DefineMethodParams(NamedTuple, Generic[_T, _FnT]):
14
+ method_class: type[_T]
15
+ method_name: str
16
+ method: _FnT
17
+
18
+
19
+ class DefineMethodDecorator(Generic[_T, _FnT]):
20
+ def __init__(self, fn: _FnT):
21
+ self.fn = fn
22
+ self.name = fn.__name__
23
+
24
+ def register_method(self, params: DefineMethodParams[_T, _FnT]): ...
25
+
26
+ def __set_name__(self, owner_class: type, name: str):
27
+ self.register_method(DefineMethodParams(owner_class, name, self.fn))
28
+
29
+ def __get__(self, instance: _T, owner_class: type[_T]):
30
+ update_parameters(self.fn, *list_parameters(self.fn)[1:])
31
+
32
+ @wraps(self.fn)
33
+ def wrapper(*args, **kwargs):
34
+ return self.fn(instance, *args, **kwargs)
35
+
36
+ return wrapper
37
+
38
+
39
+ def define_method_handler(
40
+ handle: Callable[[DefineMethodParams[_T, _FnT]], None],
41
+ ):
42
+ class Decorator(DefineMethodDecorator):
43
+ def register_method(self, params: DefineMethodParams):
44
+ handle(params)
45
+
46
+ return Decorator
@@ -0,0 +1,43 @@
1
+ from enum import StrEnum, auto
2
+
3
+ from .alias_generators import to_camel, to_pascal, to_snake
4
+
5
+
6
+ __all__ = [
7
+ "CamelEnum",
8
+ "NameEnum",
9
+ "PascalEnum",
10
+ "SnakeEnum",
11
+ "TitleEnum",
12
+ "auto",
13
+ ]
14
+
15
+
16
+ class NameEnum(StrEnum):
17
+ @staticmethod
18
+ def _generate_next_value_(name, *_, **__) -> str:
19
+ return name
20
+
21
+
22
+ class PascalEnum(StrEnum):
23
+ @staticmethod
24
+ def _generate_next_value_(name, *_, **__) -> str:
25
+ return to_pascal(name)
26
+
27
+
28
+ class CamelEnum(StrEnum):
29
+ @staticmethod
30
+ def _generate_next_value_(name, *_, **__) -> str:
31
+ return to_camel(name)
32
+
33
+
34
+ class SnakeEnum(StrEnum):
35
+ @staticmethod
36
+ def _generate_next_value_(name, *_, **__) -> str:
37
+ return to_snake(name)
38
+
39
+
40
+ class TitleEnum(StrEnum):
41
+ @staticmethod
42
+ def _generate_next_value_(name, *_, **__) -> str:
43
+ return name.replace("_", " ").title()
File without changes
@@ -0,0 +1,342 @@
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 Any, Literal, NamedTuple, Protocol, Self, TypeVar, cast
7
+
8
+ from fastapi import APIRouter, Depends
9
+ from pydantic.alias_generators import to_snake
10
+
11
+ from u_toolkit.decorators import DefineMethodParams, define_method_handler
12
+ from u_toolkit.fastapi.helpers import get_depend_from_annotation, is_depend
13
+ from u_toolkit.fastapi.responses import Response, build_responses
14
+ from u_toolkit.helpers import is_annotated
15
+ from u_toolkit.merge import deep_merge_dict
16
+ from u_toolkit.signature import update_parameters, with_parameter
17
+
18
+
19
+ class EndpointsClassInterface(Protocol):
20
+ dependencies: tuple | None = None
21
+ responses: tuple[Response, ...] | None = None
22
+ prefix: str | None = None
23
+ tags: tuple[str | Enum, ...] | None = None
24
+ deprecated: bool | None = None
25
+
26
+ @classmethod
27
+ def build_self(cls) -> Self: ...
28
+
29
+
30
+ _T = TypeVar("_T")
31
+ EndpointsClassInterfaceT = TypeVar(
32
+ "EndpointsClassInterfaceT",
33
+ bound=EndpointsClassInterface,
34
+ )
35
+
36
+
37
+ LiteralUpperMethods = Literal[
38
+ "GET",
39
+ "POST",
40
+ "PATCH",
41
+ "PUT",
42
+ "DELETE",
43
+ "OPTIONS",
44
+ "HEAD",
45
+ "TRACE",
46
+ ]
47
+ LiteralLowerMethods = Literal[
48
+ "get",
49
+ "post",
50
+ "patch",
51
+ "put",
52
+ "delete",
53
+ "options",
54
+ "head",
55
+ "trace",
56
+ ]
57
+
58
+
59
+ class Methods(StrEnum):
60
+ GET = auto()
61
+ POST = auto()
62
+ PATCH = auto()
63
+ PUT = auto()
64
+ DELETE = auto()
65
+ OPTIONS = auto()
66
+ HEAD = auto()
67
+ TRACE = auto()
68
+
69
+
70
+ METHOD_PATTERNS = {
71
+ method: re.compile(f"^{method}", re.IGNORECASE) for method in Methods
72
+ }
73
+
74
+ _FnName = str
75
+
76
+
77
+ class EndpointInfo(NamedTuple):
78
+ fn: Callable
79
+ original_name: str
80
+ method: Methods
81
+ method_pattern: re.Pattern
82
+ path: str
83
+
84
+
85
+ def get_method(name: str):
86
+ for method, method_pattern in METHOD_PATTERNS.items():
87
+ if method_pattern.search(name):
88
+ return method, method_pattern
89
+ return None
90
+
91
+
92
+ def valid_endpoint(name: str):
93
+ if get_method(name) is None:
94
+ raise ValueError("Invalid endpoint function.")
95
+
96
+
97
+ def iter_endpoints(cls: type[_T]):
98
+ prefix = "/"
99
+
100
+ if not cls.__name__.startswith("_"):
101
+ prefix += f"{to_snake(cls.__name__)}"
102
+
103
+ for name, fn in inspect.getmembers(
104
+ cls,
105
+ lambda arg: inspect.ismethoddescriptor(arg) or inspect.isfunction(arg),
106
+ ):
107
+ paths = [prefix]
108
+
109
+ if method := get_method(name):
110
+ path = method[1].sub(name, "").replace("__", "/")
111
+ if path:
112
+ paths.append(path)
113
+
114
+ yield EndpointInfo(
115
+ fn=fn,
116
+ original_name=name,
117
+ path="/".join(paths),
118
+ method=method[0],
119
+ method_pattern=method[1],
120
+ )
121
+
122
+
123
+ def iter_dependencies(cls: type[_T]):
124
+ _split = re.compile(r"\s+|:|=")
125
+ dependencies: dict = dict(inspect.getmembers(cls, is_depend))
126
+ for name, type_ in inspect.get_annotations(cls).items():
127
+ if is_annotated(type_):
128
+ dependency = get_depend_from_annotation(type_)
129
+ dependencies[name] = dependency
130
+
131
+ for line in inspect.getsource(cls).split("\n"):
132
+ token: str = _split.split(line.strip(), 1)[0]
133
+ for name, dep in dependencies.items():
134
+ if name == token:
135
+ yield token, dep
136
+
137
+
138
+ _CBVEndpointParamName = Literal[
139
+ "tags",
140
+ "dependencies",
141
+ "responses",
142
+ "response_model",
143
+ "status",
144
+ "deprecated",
145
+ "methods",
146
+ ]
147
+
148
+
149
+ class CBV:
150
+ def __init__(self, router: APIRouter | None = None) -> None:
151
+ self.router = router or APIRouter()
152
+
153
+ self._state: dict[
154
+ type[EndpointsClassInterface],
155
+ dict[_FnName, dict[_CBVEndpointParamName, Any]],
156
+ ] = {}
157
+
158
+ self._initialed_state: dict[
159
+ type[EndpointsClassInterface], EndpointsClassInterface
160
+ ] = {}
161
+
162
+ def create_route(
163
+ self,
164
+ *,
165
+ cls: type[EndpointsClassInterfaceT],
166
+ path: str,
167
+ method: Methods | LiteralUpperMethods | LiteralLowerMethods,
168
+ method_name: str,
169
+ ):
170
+ class_tags = list(cls.tags) if cls.tags else []
171
+ endpoint_tags: list[str | Enum] = (
172
+ self._state[cls][method_name].get("tags") or []
173
+ )
174
+ tags = class_tags + endpoint_tags
175
+
176
+ class_dependencies = list(cls.dependencies) if cls.dependencies else []
177
+ endpoint_dependencies = (
178
+ self._state[cls][method_name].get("dependencies") or []
179
+ )
180
+ dependencies = class_dependencies + endpoint_dependencies
181
+
182
+ class_responses = cls.responses or []
183
+ endpoint_responses = (
184
+ self._state[cls][method_name].get("responses") or []
185
+ )
186
+ responses = build_responses(*class_responses, *endpoint_responses)
187
+
188
+ status_code = self._state[cls][method_name].get("status")
189
+
190
+ deprecated = self._state[cls][method_name].get(
191
+ "deprecated", cls.deprecated
192
+ )
193
+
194
+ response_model = self._state[cls][method_name].get("response_model")
195
+
196
+ endpoint_methods = self._state[cls][method_name].get("methods") or [
197
+ method
198
+ ]
199
+
200
+ return self.router.api_route(
201
+ path,
202
+ methods=endpoint_methods,
203
+ tags=tags,
204
+ dependencies=dependencies,
205
+ response_model=response_model,
206
+ responses=responses,
207
+ status_code=status_code,
208
+ deprecated=deprecated,
209
+ )
210
+
211
+ def info(
212
+ self,
213
+ *,
214
+ methods: list[Methods | LiteralUpperMethods | LiteralLowerMethods]
215
+ | None = None,
216
+ tags: list[str | Enum] | None = None,
217
+ dependencies: list | None = None,
218
+ responses: list[Response] | None = None,
219
+ response_model: Any | None = None,
220
+ status: int | None = None,
221
+ deprecated: bool | None = None,
222
+ ):
223
+ state = self._state
224
+ initial_state = self._initial_state
225
+ data: dict[_CBVEndpointParamName, Any] = {
226
+ "methods": methods,
227
+ "tags": tags,
228
+ "dependencies": dependencies,
229
+ "responses": responses,
230
+ "response_model": response_model,
231
+ "status": status,
232
+ "deprecated": deprecated,
233
+ }
234
+
235
+ def handle(params: DefineMethodParams):
236
+ initial_state(params.method_class)
237
+ deep_merge_dict(
238
+ state,
239
+ {params.method_class: {params.method_name: data}},
240
+ )
241
+
242
+ return define_method_handler(handle)
243
+
244
+ def _initial_state(self, cls: type[_T]) -> EndpointsClassInterface:
245
+ if result := self._initialed_state.get(cls): # type: ignore
246
+ return result
247
+
248
+ self._update_cls(cls)
249
+ n_cls = cast(type[EndpointsClassInterface], cls)
250
+
251
+ default_data = {}
252
+ for endpoint in iter_endpoints(n_cls):
253
+ default_data[endpoint.original_name] = {}
254
+
255
+ self._state.setdefault(n_cls, default_data)
256
+ result = self._build_cls(n_cls)
257
+ self._initialed_state[n_cls] = result
258
+ return result
259
+
260
+ def _update_cls(self, cls: type[_T]):
261
+ for extra_name in EndpointsClassInterface.__annotations__:
262
+ if not hasattr(cls, extra_name):
263
+ setattr(cls, extra_name, None)
264
+
265
+ # TODO: 加个如果存在属性, 校验属性类型是否是预期的
266
+
267
+ def _build_cls(self, cls: type[_T]) -> _T:
268
+ if inspect.isfunction(cls.__init__) and hasattr(cls, "build_self"):
269
+ return cast(type[EndpointsClassInterface], cls).build_self() # type: ignore
270
+ return cls()
271
+
272
+ def __create_class_dependencies_injector(
273
+ self, cls: type[EndpointsClassInterfaceT]
274
+ ):
275
+ """将类的依赖添加到函数实例上
276
+
277
+ ```python
278
+ @cbv
279
+ class A:
280
+ a = Depends(lambda: id(object()))
281
+
282
+ def get(self):
283
+ # 使得每次 self.a 可以访问到当前请求的依赖
284
+ print(self.a)
285
+ ```
286
+ """
287
+
288
+ def collect_cls_dependencies(**kwargs):
289
+ return kwargs
290
+
291
+ parameters = [
292
+ inspect.Parameter(
293
+ name=name,
294
+ kind=inspect.Parameter.POSITIONAL_OR_KEYWORD,
295
+ default=dep,
296
+ )
297
+ for name, dep in iter_dependencies(cls)
298
+ ]
299
+ update_parameters(collect_cls_dependencies, *parameters)
300
+
301
+ def decorator(method: Callable):
302
+ sign_fn = partial(method)
303
+ update_wrapper(sign_fn, method)
304
+
305
+ parameters, *_ = with_parameter(
306
+ method,
307
+ name=collect_cls_dependencies.__name__,
308
+ default=Depends(collect_cls_dependencies),
309
+ )
310
+ update_parameters(sign_fn, *parameters)
311
+
312
+ @wraps(sign_fn)
313
+ def wrapper(*args, **kwargs):
314
+ instance = self._build_cls(cls)
315
+ dependencies = kwargs.pop(collect_cls_dependencies.__name__)
316
+ for dep_name, dep_value in dependencies.items():
317
+ setattr(instance, dep_name, dep_value)
318
+ fn = getattr(instance, method.__name__)
319
+ return fn(*args, **kwargs)
320
+
321
+ return wrapper
322
+
323
+ return decorator
324
+
325
+ def __call__(self, cls: type[_T]) -> type[_T]:
326
+ instance = self._initial_state(cls)
327
+ cls_ = cast(type[EndpointsClassInterface], cls)
328
+
329
+ decorator = self.__create_class_dependencies_injector(cls_)
330
+
331
+ for endpoint_info in iter_endpoints(cls):
332
+ route = self.create_route(
333
+ cls=cast(type[EndpointsClassInterface], cls),
334
+ path=endpoint_info.path,
335
+ method=endpoint_info.method,
336
+ method_name=endpoint_info.original_name,
337
+ )
338
+ method = getattr(instance, endpoint_info.original_name)
339
+ endpoint = decorator(method)
340
+ route(endpoint)
341
+
342
+ return cls
@@ -0,0 +1,9 @@
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
+ )