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.
- u_toolkit-0.1.0/.github/workflows/publish.yml +0 -0
- u_toolkit-0.1.0/.gitignore +10 -0
- u_toolkit-0.1.0/PKG-INFO +10 -0
- u_toolkit-0.1.0/README.md +0 -0
- u_toolkit-0.1.0/experimental.py +0 -0
- u_toolkit-0.1.0/pyproject.toml +89 -0
- u_toolkit-0.1.0/src/u_toolkit/__init__.py +9 -0
- u_toolkit-0.1.0/src/u_toolkit/alias_generators.py +49 -0
- u_toolkit-0.1.0/src/u_toolkit/datetime.py +30 -0
- u_toolkit-0.1.0/src/u_toolkit/decorators.py +46 -0
- u_toolkit-0.1.0/src/u_toolkit/enum.py +43 -0
- u_toolkit-0.1.0/src/u_toolkit/fastapi/__init__.py +0 -0
- u_toolkit-0.1.0/src/u_toolkit/fastapi/cbv.py +342 -0
- u_toolkit-0.1.0/src/u_toolkit/fastapi/config.py +9 -0
- u_toolkit-0.1.0/src/u_toolkit/fastapi/exception.py +115 -0
- u_toolkit-0.1.0/src/u_toolkit/fastapi/helpers.py +18 -0
- u_toolkit-0.1.0/src/u_toolkit/fastapi/lifespan.py +67 -0
- u_toolkit-0.1.0/src/u_toolkit/fastapi/pagination.py +79 -0
- u_toolkit-0.1.0/src/u_toolkit/fastapi/responses.py +66 -0
- u_toolkit-0.1.0/src/u_toolkit/function.py +12 -0
- u_toolkit-0.1.0/src/u_toolkit/helpers.py +5 -0
- u_toolkit-0.1.0/src/u_toolkit/logger.py +11 -0
- u_toolkit-0.1.0/src/u_toolkit/merge.py +31 -0
- u_toolkit-0.1.0/src/u_toolkit/object.py +0 -0
- u_toolkit-0.1.0/src/u_toolkit/path.py +4 -0
- u_toolkit-0.1.0/src/u_toolkit/pydantic/__init__.py +0 -0
- u_toolkit-0.1.0/src/u_toolkit/pydantic/fields.py +24 -0
- u_toolkit-0.1.0/src/u_toolkit/pydantic/models.py +41 -0
- u_toolkit-0.1.0/src/u_toolkit/pydantic/type_vars.py +6 -0
- u_toolkit-0.1.0/src/u_toolkit/signature.py +74 -0
- u_toolkit-0.1.0/src/u_toolkit/sqlalchemy/__init__.py +0 -0
- u_toolkit-0.1.0/src/u_toolkit/sqlalchemy/fields.py +0 -0
- u_toolkit-0.1.0/src/u_toolkit/sqlalchemy/function.py +12 -0
- u_toolkit-0.1.0/src/u_toolkit/sqlalchemy/orm/__init__.py +0 -0
- u_toolkit-0.1.0/src/u_toolkit/sqlalchemy/orm/fields.py +20 -0
- u_toolkit-0.1.0/src/u_toolkit/sqlalchemy/orm/models.py +23 -0
- u_toolkit-0.1.0/src/u_toolkit/sqlalchemy/table_info.py +17 -0
- u_toolkit-0.1.0/src/u_toolkit/sqlalchemy/type_vars.py +6 -0
- u_toolkit-0.1.0/uv.lock +481 -0
File without changes
|
u_toolkit-0.1.0/PKG-INFO
ADDED
@@ -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,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
|