future-framework 1.1.0__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.
- future/__init__.py +3 -0
- future/application.py +656 -0
- future/authentication/Auth0Authentication.py +9 -0
- future/authentication/Authentication.py +11 -0
- future/authentication/AzureADAuthentication.py +9 -0
- future/authentication/BasicAuthentication.py +9 -0
- future/authentication/KerberosAuthentication.py +9 -0
- future/authentication/KeycloakAuthentication.py +9 -0
- future/authentication/OAuth2Authentication.py +9 -0
- future/authentication/OpenIdConnectAuthentication.py +9 -0
- future/authentication/SAMLAuthentication.py +9 -0
- future/cli/__init__.py +1 -0
- future/cli/main.py +608 -0
- future/cli/stubs.py +114 -0
- future/controllers/__init__.py +4 -0
- future/controllers/base.py +8 -0
- future/controllers/builtins.py +41 -0
- future/controllers/graphql.py +23 -0
- future/controllers/openapi.py +224 -0
- future/databases/Clickhouse.py +176 -0
- future/databases/Connections.py +20 -0
- future/databases/Database.py +66 -0
- future/databases/Elasticsearch.py +146 -0
- future/databases/MongoDB.py +177 -0
- future/databases/MySQL.py +215 -0
- future/databases/Postgres.py +214 -0
- future/databases/Redis.py +146 -0
- future/databases/SQLite.py +219 -0
- future/exceptions.py +27 -0
- future/graphql/__init__.py +1 -0
- future/graphql/schema.py +148 -0
- future/lifespan.py +70 -0
- future/logger.py +53 -0
- future/middleware/Middleware.py +201 -0
- future/middleware/SessionMiddleware.py +77 -0
- future/middleware/__init__.py +21 -0
- future/migrations/Blueprint.py +54 -0
- future/migrations/Column.py +33 -0
- future/migrations/Migration.py +11 -0
- future/migrations/MigrationGenerator.py +137 -0
- future/migrations/Migrator.py +72 -0
- future/migrations/Schema.py +21 -0
- future/models/__init__.py +1 -0
- future/models/model.py +224 -0
- future/openapi.py +233 -0
- future/plugins/ElasticsearchPlugin.py +496 -0
- future/plugins/__init__.py +9 -0
- future/request.py +143 -0
- future/response.py +201 -0
- future/routing.py +342 -0
- future/seeds/SeedGenerator.py +95 -0
- future/seeds/SeedRunner.py +42 -0
- future/seeds/Seeder.py +9 -0
- future/settings.py +94 -0
- future/tasks/__init__.py +14 -0
- future/tasks/scheduler.py +267 -0
- future/testing/__init__.py +1 -0
- future/testing/client.py +135 -0
- future/types.py +47 -0
- future_framework-1.1.0.dist-info/METADATA +68 -0
- future_framework-1.1.0.dist-info/RECORD +64 -0
- future_framework-1.1.0.dist-info/WHEEL +4 -0
- future_framework-1.1.0.dist-info/entry_points.txt +3 -0
- future_framework-1.1.0.dist-info/licenses/LICENSE +21 -0
future/response.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from collections.abc import Awaitable
|
|
4
|
+
from typing import Any, Callable, Optional, Union
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Response:
|
|
8
|
+
def __init__(self, body: Union[str, bytes] = "", status: int = 200, headers: dict[str, str] | None = None, content_type: Optional[str] = None) -> None:
|
|
9
|
+
self.status = status
|
|
10
|
+
self.context: dict[str, Any] = {}
|
|
11
|
+
self.body = b""
|
|
12
|
+
self.headers: list[list[bytes]] = []
|
|
13
|
+
if body or content_type or headers:
|
|
14
|
+
self._set(body=body, status=status, headers=headers, content_type=content_type)
|
|
15
|
+
|
|
16
|
+
def _set(self, body: Union[str, bytes] = "", status: int = 200, headers: dict[str, str] | None = None, content_type: Optional[str] = None) -> "Response":
|
|
17
|
+
self.status = status
|
|
18
|
+
if isinstance(body, str):
|
|
19
|
+
self.body = body.encode("utf-8")
|
|
20
|
+
if content_type is None:
|
|
21
|
+
content_type = "text/plain"
|
|
22
|
+
elif isinstance(body, bytes):
|
|
23
|
+
self.body = body
|
|
24
|
+
if content_type is None and body:
|
|
25
|
+
content_type = "application/octet-stream"
|
|
26
|
+
else:
|
|
27
|
+
raise TypeError(f"Response body must be str or bytes, got {type(body)}")
|
|
28
|
+
header_map = dict(headers) if headers else {}
|
|
29
|
+
if content_type and not any(key.lower() == "content-type" for key in header_map):
|
|
30
|
+
header_map["content-type"] = content_type
|
|
31
|
+
# Preserve Set-Cookie headers already appended via set_cookie
|
|
32
|
+
cookies = [pair for pair in self.headers if pair[0].lower() == b"set-cookie"]
|
|
33
|
+
self.headers = [[key.encode(), value.encode()] for key, value in header_map.items()]
|
|
34
|
+
self.headers.extend(cookies)
|
|
35
|
+
return self
|
|
36
|
+
|
|
37
|
+
def json(self, data: Any, status: int = 200, headers: dict[str, str] | None = None) -> "Response":
|
|
38
|
+
return self._set(body=json.dumps(data, default=str, ensure_ascii=False), status=status, headers=headers, content_type="application/json")
|
|
39
|
+
|
|
40
|
+
def html(self, html: str, status: int = 200, headers: dict[str, str] | None = None) -> "Response":
|
|
41
|
+
return self._set(body=html, status=status, headers=headers, content_type="text/html")
|
|
42
|
+
|
|
43
|
+
def text(self, body: str = "", status: int = 200, headers: dict[str, str] | None = None) -> "Response":
|
|
44
|
+
return self._set(body=body, status=status, headers=headers, content_type="text/plain")
|
|
45
|
+
|
|
46
|
+
def empty(self, status: int = 204, headers: dict[str, str] | None = None) -> "Response":
|
|
47
|
+
return self._set(body="", status=status, headers=headers, content_type=None)
|
|
48
|
+
|
|
49
|
+
def redirect(self, url: str, status: int = 302, headers: dict[str, str] | None = None) -> "Response":
|
|
50
|
+
final = dict(headers) if headers else {}
|
|
51
|
+
final["location"] = url
|
|
52
|
+
return self._set(body="", status=status, headers=final, content_type=None)
|
|
53
|
+
|
|
54
|
+
def file(self, body: bytes = b"", status: int = 200, headers: dict[str, str] | None = None, content_type: str = "application/octet-stream") -> "Response":
|
|
55
|
+
return self._set(body=body, status=status, headers=headers, content_type=content_type)
|
|
56
|
+
|
|
57
|
+
def image(self, body: bytes = b"", status: int = 200, headers: dict[str, str] | None = None, content_type: str = "image/png", file_path: str | None = None) -> "Response":
|
|
58
|
+
if file_path:
|
|
59
|
+
with open(file_path, "rb") as f:
|
|
60
|
+
body = f.read()
|
|
61
|
+
return self._set(body=body, status=status, headers=headers, content_type=content_type)
|
|
62
|
+
|
|
63
|
+
def set_cookie(self, name: str, value: str, path: str = "/", httponly: bool = True, samesite: str = "Lax", secure: bool = False, max_age: int | None = None, domain: str | None = None, expires: str | None = None) -> "Response":
|
|
64
|
+
cookie = f"{name}={value}; Path={path}"
|
|
65
|
+
if domain:
|
|
66
|
+
cookie += f"; Domain={domain}"
|
|
67
|
+
if max_age is not None:
|
|
68
|
+
cookie += f"; Max-Age={int(max_age)}"
|
|
69
|
+
if expires:
|
|
70
|
+
cookie += f"; Expires={expires}"
|
|
71
|
+
if httponly:
|
|
72
|
+
cookie += "; HttpOnly"
|
|
73
|
+
cookie += f"; SameSite={samesite}"
|
|
74
|
+
if secure:
|
|
75
|
+
cookie += "; Secure"
|
|
76
|
+
self.headers.append([b"set-cookie", cookie.encode("utf-8")])
|
|
77
|
+
return self
|
|
78
|
+
|
|
79
|
+
def delete_cookie(self, name: str, path: str = "/") -> "Response":
|
|
80
|
+
self.headers.append([b"set-cookie", f"{name}=; Path={path}; Max-Age=0".encode("utf-8")])
|
|
81
|
+
return self
|
|
82
|
+
|
|
83
|
+
async def __call__(self, send: Callable[[dict[str, Any]], Awaitable[None]]) -> None:
|
|
84
|
+
start_message = {
|
|
85
|
+
"type": "http.response.start",
|
|
86
|
+
"status": self.status,
|
|
87
|
+
"headers": self.headers,
|
|
88
|
+
}
|
|
89
|
+
await send(start_message)
|
|
90
|
+
body_message = {
|
|
91
|
+
"type": "http.response.body",
|
|
92
|
+
"body": self.body,
|
|
93
|
+
}
|
|
94
|
+
await send(body_message)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class WebSocketResponse:
|
|
98
|
+
"""ASGI WebSocket session: accept, send/receive loop, then exit on disconnect."""
|
|
99
|
+
|
|
100
|
+
def __init__(self, receive: Any, message: str = "") -> None:
|
|
101
|
+
self._asgi_receive = receive
|
|
102
|
+
self.message = message
|
|
103
|
+
self._send: Any = None
|
|
104
|
+
|
|
105
|
+
async def accept(self) -> None:
|
|
106
|
+
await self._send({"type": "websocket.accept"})
|
|
107
|
+
|
|
108
|
+
async def send_text(self, text: str) -> None:
|
|
109
|
+
await self._send({"type": "websocket.send", "text": text})
|
|
110
|
+
|
|
111
|
+
async def send_bytes(self, data: bytes) -> None:
|
|
112
|
+
await self._send({"type": "websocket.send", "bytes": data})
|
|
113
|
+
|
|
114
|
+
async def close(self, code: int = 1000, reason: str = "") -> None:
|
|
115
|
+
await self._send({"type": "websocket.close", "code": code, "reason": reason})
|
|
116
|
+
|
|
117
|
+
async def receive(self) -> dict[str, Any] | None:
|
|
118
|
+
while True:
|
|
119
|
+
message = await self._asgi_receive()
|
|
120
|
+
if message["type"] == "websocket.disconnect":
|
|
121
|
+
return None
|
|
122
|
+
if message["type"] == "websocket.receive":
|
|
123
|
+
return message
|
|
124
|
+
|
|
125
|
+
async def receive_text(self) -> str | None:
|
|
126
|
+
message = await self.receive()
|
|
127
|
+
if message is None:
|
|
128
|
+
return None
|
|
129
|
+
if "text" in message and message["text"] is not None:
|
|
130
|
+
return message["text"]
|
|
131
|
+
if "bytes" in message and message["bytes"] is not None:
|
|
132
|
+
return message["bytes"].decode("utf-8")
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
async def __call__(self, send: Any) -> None:
|
|
136
|
+
self._send = send
|
|
137
|
+
await self.accept()
|
|
138
|
+
if self.message:
|
|
139
|
+
await self.send_text(f"Echo: {self.message}")
|
|
140
|
+
while True:
|
|
141
|
+
text = await self.receive_text()
|
|
142
|
+
if text is None:
|
|
143
|
+
break
|
|
144
|
+
await self.send_text(f"Echo: {text}")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# Temporary wrappers for migration
|
|
148
|
+
class PlainTextResponse(Response):
|
|
149
|
+
def __init__(self, body: str = "", status: int = 200, headers: dict[str, str] | None = None) -> None:
|
|
150
|
+
super().__init__()
|
|
151
|
+
self.text(body, status=status, headers=headers)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class JSONResponse(Response):
|
|
155
|
+
def __init__(self, data: Any = None, status: int = 200, headers: dict[str, str] | None = None) -> None:
|
|
156
|
+
super().__init__()
|
|
157
|
+
self.json(data, status=status, headers=headers)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class EmptyResponse(Response):
|
|
161
|
+
def __init__(self, status: int = 204, headers: dict[str, str] | None = None) -> None:
|
|
162
|
+
super().__init__()
|
|
163
|
+
self.empty(status=status, headers=headers)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class HTMLResponse(Response):
|
|
167
|
+
def __init__(self, html: str = "", status: int = 200, headers: dict[str, str] | None = None) -> None:
|
|
168
|
+
super().__init__()
|
|
169
|
+
self.html(html, status=status, headers=headers)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class PNGResponse(Response):
|
|
173
|
+
def __init__(self, body: bytes = b"", status: int = 200, headers: dict[str, str] | None = None) -> None:
|
|
174
|
+
super().__init__()
|
|
175
|
+
self.image(body, status=status, headers=headers, content_type="image/png")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class ImageResponse(Response):
|
|
179
|
+
def __init__(
|
|
180
|
+
self, body: bytes = b"", status: int = 200, headers: dict[str, str] | None = None, image_content_type: str = "image/png", file_path: str | None = None
|
|
181
|
+
) -> None:
|
|
182
|
+
super().__init__()
|
|
183
|
+
self.image(body, status=status, headers=headers, content_type=image_content_type, file_path=file_path)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class RedirectResponse(Response):
|
|
187
|
+
def __init__(self, url: str = "", status: int = 302, headers: dict[str, str] | None = None) -> None:
|
|
188
|
+
super().__init__()
|
|
189
|
+
if url:
|
|
190
|
+
self.redirect(url, status=status, headers=headers)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class FileResponse(Response):
|
|
194
|
+
def __init__(self, body: bytes = b"", status: int = 200, headers: dict[str, str] | None = None) -> None:
|
|
195
|
+
super().__init__()
|
|
196
|
+
self.file(body, status=status, headers=headers)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class StreamingResponse(Response):
|
|
200
|
+
async def __call__(self, send: Any) -> None:
|
|
201
|
+
raise NotImplementedError("streaming is not implemented yet")
|
future/routing.py
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Optional, TypedDict
|
|
4
|
+
|
|
5
|
+
from future.logger import log
|
|
6
|
+
from future.middleware import Middleware
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RegexConfig(TypedDict):
|
|
10
|
+
paths: list[re.Pattern[str]]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RouteException(Exception):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class InvalidValuePatternName(RouteException):
|
|
18
|
+
def __init__(self, pattern_name: str, matched_parameter: str) -> None:
|
|
19
|
+
self.pattern_name = pattern_name
|
|
20
|
+
self.matched_parameter = matched_parameter
|
|
21
|
+
super().__init__(f"Invalid value pattern name: {pattern_name} in {matched_parameter}")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RouteMatch:
|
|
25
|
+
def __init__(self, route: "Route", params: Optional[dict[str, str]]) -> None:
|
|
26
|
+
self.route = route
|
|
27
|
+
self.params = params
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Route:
|
|
31
|
+
value_patterns = {
|
|
32
|
+
"string": r"[^\/]+",
|
|
33
|
+
"str": r"[^\/]+",
|
|
34
|
+
"path": r".*",
|
|
35
|
+
"int": r"\d+",
|
|
36
|
+
"float": r"\d+(?:\.\d+)?",
|
|
37
|
+
"uuid": r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
methods: list[str],
|
|
43
|
+
path: str,
|
|
44
|
+
endpoint: Callable[..., Any],
|
|
45
|
+
name: str,
|
|
46
|
+
strict_slashes: bool = False,
|
|
47
|
+
middlewares: Optional[list[Middleware]] = None,
|
|
48
|
+
scopes: Optional[list[str]] = None,
|
|
49
|
+
) -> None:
|
|
50
|
+
self.methods = methods
|
|
51
|
+
self.path = path
|
|
52
|
+
self.endpoint = endpoint
|
|
53
|
+
self.name = name
|
|
54
|
+
self.strict_slashes = strict_slashes
|
|
55
|
+
self.middlewares = middlewares or []
|
|
56
|
+
self.scopes = scopes or []
|
|
57
|
+
|
|
58
|
+
# Huge credits to BlackSheep for the code below.
|
|
59
|
+
def compile_pattern(self) -> None:
|
|
60
|
+
_route_all_rx = re.compile(b"\\*")
|
|
61
|
+
_route_param_rx = re.compile(b"/:([^/]+)")
|
|
62
|
+
|
|
63
|
+
_mustache_route_param_rx = re.compile(b"/{([^}]+)}")
|
|
64
|
+
_angle_bracket_route_param_rx = re.compile(b"/<([^>]+)>")
|
|
65
|
+
|
|
66
|
+
_named_group_rx = re.compile(b"\\?P<([^>]+)>")
|
|
67
|
+
_escaped_chars = {
|
|
68
|
+
b".",
|
|
69
|
+
b"[",
|
|
70
|
+
b"]",
|
|
71
|
+
b"(",
|
|
72
|
+
b")",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
def _get_parameter_pattern_fragment(name: str, pattern: Optional[str] = None) -> bytes:
|
|
76
|
+
if pattern is None:
|
|
77
|
+
pattern = Route.value_patterns["string"]
|
|
78
|
+
return rb"/(?P<" + name.encode() + rb">" + pattern.encode() + rb")"
|
|
79
|
+
|
|
80
|
+
def _handle_rich_parameter(match: re.Match[bytes]) -> bytes:
|
|
81
|
+
matched_parameter = next(iter(match.groups()))
|
|
82
|
+
assert isinstance(matched_parameter, bytes)
|
|
83
|
+
|
|
84
|
+
if b":" in matched_parameter:
|
|
85
|
+
raw_pattern_name, parameter_name = matched_parameter.split(b":")
|
|
86
|
+
parameter_pattern_name = raw_pattern_name.decode()
|
|
87
|
+
parameter_pattern = Route.value_patterns.get(parameter_pattern_name)
|
|
88
|
+
|
|
89
|
+
if not parameter_pattern:
|
|
90
|
+
raise InvalidValuePatternName(parameter_pattern_name, matched_parameter.decode("utf8"))
|
|
91
|
+
|
|
92
|
+
return _get_parameter_pattern_fragment(parameter_name.decode(), parameter_pattern)
|
|
93
|
+
|
|
94
|
+
return _get_parameter_pattern_fragment(matched_parameter.decode())
|
|
95
|
+
|
|
96
|
+
pattern = self.path.encode()
|
|
97
|
+
for c in _escaped_chars:
|
|
98
|
+
if c in pattern:
|
|
99
|
+
pattern = pattern.replace(c, b"\\" + c)
|
|
100
|
+
|
|
101
|
+
if b"*" in pattern:
|
|
102
|
+
if pattern.count(b"*") > 1:
|
|
103
|
+
raise RouteException("A route pattern cannot contain more than one star sign *.")
|
|
104
|
+
if b"/*" in pattern:
|
|
105
|
+
pattern = _route_all_rx.sub(rb"?(?P<tail>.*)", pattern)
|
|
106
|
+
else:
|
|
107
|
+
pattern = _route_all_rx.sub(rb"(?P<tail>.*)", pattern)
|
|
108
|
+
|
|
109
|
+
if b"<" in pattern:
|
|
110
|
+
pattern = _angle_bracket_route_param_rx.sub(_handle_rich_parameter, pattern)
|
|
111
|
+
|
|
112
|
+
if b"{" in pattern:
|
|
113
|
+
pattern = _mustache_route_param_rx.sub(_handle_rich_parameter, pattern)
|
|
114
|
+
|
|
115
|
+
if b"/:" in pattern:
|
|
116
|
+
pattern = _route_param_rx.sub(rb"/(?P<\1>[^\/]+)", pattern)
|
|
117
|
+
|
|
118
|
+
param_names = []
|
|
119
|
+
for p in _named_group_rx.finditer(pattern):
|
|
120
|
+
param_name = p.group(1).decode()
|
|
121
|
+
|
|
122
|
+
if param_name in param_names:
|
|
123
|
+
raise ValueError(f"cannot have multiple parameters with name: {param_name}")
|
|
124
|
+
|
|
125
|
+
param_names.append(param_name)
|
|
126
|
+
|
|
127
|
+
if len(pattern) > 1 and not pattern.endswith(b"*") and not self.strict_slashes:
|
|
128
|
+
pattern = pattern + b"/?"
|
|
129
|
+
|
|
130
|
+
self._rx = re.compile(b"^" + pattern + b"$", re.IGNORECASE)
|
|
131
|
+
|
|
132
|
+
self.param_names = param_names
|
|
133
|
+
|
|
134
|
+
def match(self, request_method: str, request_path: bytes) -> Optional[RouteMatch]:
|
|
135
|
+
log.debug("Matching on request type %s for path: %s using regex: %s", self.methods, request_path, self._rx)
|
|
136
|
+
# Check if the HTTP method matches the allowed methods for this route
|
|
137
|
+
if request_method not in self.methods:
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
# Check if the request path matches the compiled regex pattern for this route
|
|
141
|
+
match = self._rx.match(request_path)
|
|
142
|
+
if not match:
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
# Convert bytes values to strings in groupdict
|
|
146
|
+
params = None
|
|
147
|
+
if self.param_names:
|
|
148
|
+
groupdict = match.groupdict()
|
|
149
|
+
params = {key: value.decode("utf-8") if isinstance(value, bytes) else str(value) for key, value in groupdict.items()}
|
|
150
|
+
|
|
151
|
+
route_match = RouteMatch(self, params)
|
|
152
|
+
return route_match
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class Get(Route):
|
|
156
|
+
def __init__(
|
|
157
|
+
self,
|
|
158
|
+
path: str,
|
|
159
|
+
endpoint: Callable[..., Any],
|
|
160
|
+
name: str,
|
|
161
|
+
strict_slashes: bool = False,
|
|
162
|
+
middlewares: Optional[list[Middleware]] = None,
|
|
163
|
+
scopes: Optional[list[str]] = None,
|
|
164
|
+
) -> None:
|
|
165
|
+
super().__init__(
|
|
166
|
+
methods=["GET"],
|
|
167
|
+
path=path,
|
|
168
|
+
endpoint=endpoint,
|
|
169
|
+
name=name,
|
|
170
|
+
strict_slashes=strict_slashes,
|
|
171
|
+
middlewares=middlewares,
|
|
172
|
+
scopes=scopes,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class Post(Route):
|
|
177
|
+
def __init__(
|
|
178
|
+
self,
|
|
179
|
+
path: str,
|
|
180
|
+
endpoint: Callable[..., Any],
|
|
181
|
+
name: str,
|
|
182
|
+
strict_slashes: bool = False,
|
|
183
|
+
middlewares: Optional[list[Middleware]] = None,
|
|
184
|
+
scopes: Optional[list[str]] = None,
|
|
185
|
+
) -> None:
|
|
186
|
+
super().__init__(
|
|
187
|
+
methods=["POST"],
|
|
188
|
+
path=path,
|
|
189
|
+
endpoint=endpoint,
|
|
190
|
+
name=name,
|
|
191
|
+
strict_slashes=strict_slashes,
|
|
192
|
+
middlewares=middlewares,
|
|
193
|
+
scopes=scopes,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class Put(Route):
|
|
198
|
+
def __init__(
|
|
199
|
+
self,
|
|
200
|
+
path: str,
|
|
201
|
+
endpoint: Callable[..., Any],
|
|
202
|
+
name: str,
|
|
203
|
+
strict_slashes: bool = False,
|
|
204
|
+
middlewares: Optional[list[Middleware]] = None,
|
|
205
|
+
scopes: Optional[list[str]] = None,
|
|
206
|
+
) -> None:
|
|
207
|
+
super().__init__(
|
|
208
|
+
methods=["PUT"],
|
|
209
|
+
path=path,
|
|
210
|
+
endpoint=endpoint,
|
|
211
|
+
name=name,
|
|
212
|
+
strict_slashes=strict_slashes,
|
|
213
|
+
middlewares=middlewares,
|
|
214
|
+
scopes=scopes,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class Head(Route):
|
|
219
|
+
def __init__(
|
|
220
|
+
self,
|
|
221
|
+
path: str,
|
|
222
|
+
endpoint: Callable[..., Any],
|
|
223
|
+
name: str,
|
|
224
|
+
strict_slashes: bool = False,
|
|
225
|
+
middlewares: Optional[list[Middleware]] = None,
|
|
226
|
+
scopes: Optional[list[str]] = None,
|
|
227
|
+
) -> None:
|
|
228
|
+
super().__init__(
|
|
229
|
+
methods=["HEAD"],
|
|
230
|
+
path=path,
|
|
231
|
+
endpoint=endpoint,
|
|
232
|
+
name=name,
|
|
233
|
+
strict_slashes=strict_slashes,
|
|
234
|
+
middlewares=middlewares,
|
|
235
|
+
scopes=scopes,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class Options(Route):
|
|
240
|
+
def __init__(
|
|
241
|
+
self,
|
|
242
|
+
path: str,
|
|
243
|
+
endpoint: Callable[..., Any],
|
|
244
|
+
name: str,
|
|
245
|
+
strict_slashes: bool = False,
|
|
246
|
+
middlewares: Optional[list[Middleware]] = None,
|
|
247
|
+
scopes: Optional[list[str]] = None,
|
|
248
|
+
) -> None:
|
|
249
|
+
super().__init__(
|
|
250
|
+
methods=["OPTIONS"],
|
|
251
|
+
path=path,
|
|
252
|
+
endpoint=endpoint,
|
|
253
|
+
name=name,
|
|
254
|
+
strict_slashes=strict_slashes,
|
|
255
|
+
middlewares=middlewares,
|
|
256
|
+
scopes=scopes,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class Patch(Route):
|
|
261
|
+
def __init__(
|
|
262
|
+
self,
|
|
263
|
+
path: str,
|
|
264
|
+
endpoint: Callable[..., Any],
|
|
265
|
+
name: str,
|
|
266
|
+
strict_slashes: bool = False,
|
|
267
|
+
middlewares: Optional[list[Middleware]] = None,
|
|
268
|
+
scopes: Optional[list[str]] = None,
|
|
269
|
+
) -> None:
|
|
270
|
+
super().__init__(
|
|
271
|
+
methods=["PATCH"],
|
|
272
|
+
path=path,
|
|
273
|
+
endpoint=endpoint,
|
|
274
|
+
name=name,
|
|
275
|
+
strict_slashes=strict_slashes,
|
|
276
|
+
middlewares=middlewares,
|
|
277
|
+
scopes=scopes,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class Delete(Route):
|
|
282
|
+
def __init__(
|
|
283
|
+
self,
|
|
284
|
+
path: str,
|
|
285
|
+
endpoint: Callable[..., Any],
|
|
286
|
+
name: str,
|
|
287
|
+
strict_slashes: bool = False,
|
|
288
|
+
middlewares: Optional[list[Middleware]] = None,
|
|
289
|
+
scopes: Optional[list[str]] = None,
|
|
290
|
+
) -> None:
|
|
291
|
+
super().__init__(
|
|
292
|
+
methods=["DELETE"],
|
|
293
|
+
path=path,
|
|
294
|
+
endpoint=endpoint,
|
|
295
|
+
name=name,
|
|
296
|
+
strict_slashes=strict_slashes,
|
|
297
|
+
middlewares=middlewares,
|
|
298
|
+
scopes=scopes,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class WebSocket(Route):
|
|
303
|
+
def __init__(
|
|
304
|
+
self,
|
|
305
|
+
path: str,
|
|
306
|
+
endpoint: Callable[..., Any],
|
|
307
|
+
name: str,
|
|
308
|
+
strict_slashes: bool = False,
|
|
309
|
+
middlewares: Optional[list[Middleware]] = None,
|
|
310
|
+
scopes: Optional[list[str]] = None,
|
|
311
|
+
) -> None:
|
|
312
|
+
super().__init__(
|
|
313
|
+
methods=["WEBSOCKET"],
|
|
314
|
+
path=path,
|
|
315
|
+
endpoint=endpoint,
|
|
316
|
+
name=name,
|
|
317
|
+
strict_slashes=strict_slashes,
|
|
318
|
+
middlewares=middlewares,
|
|
319
|
+
scopes=scopes,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class RouteGroup:
|
|
324
|
+
def __init__(
|
|
325
|
+
self,
|
|
326
|
+
routes: list[Route],
|
|
327
|
+
name: str = "",
|
|
328
|
+
prefix: str = "",
|
|
329
|
+
subdomain: str = "",
|
|
330
|
+
middlewares: Optional[list[Middleware]] = None,
|
|
331
|
+
) -> None:
|
|
332
|
+
self.name = name
|
|
333
|
+
self.prefix = prefix
|
|
334
|
+
self.routes = routes
|
|
335
|
+
self.subdomain = subdomain
|
|
336
|
+
self.middlewares = middlewares or []
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
class EndpointConfig(TypedDict):
|
|
340
|
+
middleware_before: list[Middleware]
|
|
341
|
+
middleware_after: list[Middleware]
|
|
342
|
+
route: Route
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Generate seeder stubs from Model annotations.
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from types import UnionType
|
|
6
|
+
from typing import get_args, get_origin, Union
|
|
7
|
+
|
|
8
|
+
from future.migrations.MigrationGenerator import MigrationGenerator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SeedGenerator:
|
|
12
|
+
def __init__(self, models_path="app/models", seeds_path="database/seeds"):
|
|
13
|
+
self.models_path = models_path
|
|
14
|
+
self.seeds_path = seeds_path
|
|
15
|
+
|
|
16
|
+
def find_model(self, name):
|
|
17
|
+
for model in MigrationGenerator(models_path=self.models_path).discover_models():
|
|
18
|
+
if model.__name__ == name:
|
|
19
|
+
return model
|
|
20
|
+
raise ValueError(f"Model not found: {name}")
|
|
21
|
+
|
|
22
|
+
def value_expr(self, field, annotation):
|
|
23
|
+
origin = get_origin(annotation)
|
|
24
|
+
if origin is Union or origin is UnionType:
|
|
25
|
+
args = [arg for arg in get_args(annotation) if arg is not type(None)]
|
|
26
|
+
annotation = args[0] if args else str
|
|
27
|
+
type_name = getattr(annotation, "__name__", str(annotation))
|
|
28
|
+
if field == "id":
|
|
29
|
+
return "fake.uuid4()"
|
|
30
|
+
if type_name == "datetime":
|
|
31
|
+
return "fake.date_time_this_year()"
|
|
32
|
+
if type_name == "int":
|
|
33
|
+
return "fake.random_int(1, 9999)"
|
|
34
|
+
if type_name == "float":
|
|
35
|
+
return "float(fake.pyfloat(min_value=1, max_value=100))"
|
|
36
|
+
if type_name == "bool":
|
|
37
|
+
return "fake.boolean()"
|
|
38
|
+
if field.endswith("_id"):
|
|
39
|
+
return "str(fake.random_int(1000, 9999))"
|
|
40
|
+
if "email" in field:
|
|
41
|
+
return "fake.email()"
|
|
42
|
+
if "name" in field or "username" in field:
|
|
43
|
+
return "fake.user_name()"
|
|
44
|
+
if field in ("description", "text", "body", "content") or field.endswith("_original") or field.endswith("_translated"):
|
|
45
|
+
return "fake.paragraph()"
|
|
46
|
+
return "fake.word()"
|
|
47
|
+
|
|
48
|
+
def render(self, model):
|
|
49
|
+
name = model.__name__
|
|
50
|
+
annotations = dict(getattr(model, "__annotations__", {}))
|
|
51
|
+
args = []
|
|
52
|
+
for field, annotation in annotations.items():
|
|
53
|
+
args.append(f" {field}={self.value_expr(field, annotation)},")
|
|
54
|
+
if args:
|
|
55
|
+
construct = f" {name}(\n" + "\n".join(args) + f"\n ).save()"
|
|
56
|
+
else:
|
|
57
|
+
construct = f" {name}().save()"
|
|
58
|
+
return (
|
|
59
|
+
"from faker import Faker\n"
|
|
60
|
+
"from future.seeds.Seeder import Seeder\n"
|
|
61
|
+
f"from app.models.{name} import {name}\n"
|
|
62
|
+
"\n"
|
|
63
|
+
"\n"
|
|
64
|
+
f"class {name}Seeder(Seeder):\n"
|
|
65
|
+
" def run(self):\n"
|
|
66
|
+
" fake = Faker()\n"
|
|
67
|
+
" for _ in range(10):\n"
|
|
68
|
+
f"{construct}\n"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def make(self, model_name=None):
|
|
72
|
+
if model_name is None:
|
|
73
|
+
return self.make_all()
|
|
74
|
+
model = self.find_model(model_name)
|
|
75
|
+
return [self.write(model)]
|
|
76
|
+
|
|
77
|
+
def make_all(self):
|
|
78
|
+
paths = []
|
|
79
|
+
for model in MigrationGenerator(models_path=self.models_path).discover_models():
|
|
80
|
+
path = self.write(model, skip_existing=True)
|
|
81
|
+
if path is not None:
|
|
82
|
+
paths.append(path)
|
|
83
|
+
return paths
|
|
84
|
+
|
|
85
|
+
def write(self, model, skip_existing=False):
|
|
86
|
+
class_name = f"{model.__name__}Seeder"
|
|
87
|
+
directory = Path(self.seeds_path)
|
|
88
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
89
|
+
path = directory / f"{class_name}.py"
|
|
90
|
+
if path.exists():
|
|
91
|
+
if skip_existing:
|
|
92
|
+
return None
|
|
93
|
+
raise FileExistsError(f"File already exists: {path}")
|
|
94
|
+
path.write_text(self.render(model))
|
|
95
|
+
return str(path)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Discovers and runs seeder classes from a seeds directory.
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import importlib.util
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .Seeder import Seeder
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SeedRunner:
|
|
11
|
+
def __init__(self, path):
|
|
12
|
+
self.path = path
|
|
13
|
+
|
|
14
|
+
def discover(self):
|
|
15
|
+
seeders = {}
|
|
16
|
+
directory = Path(self.path)
|
|
17
|
+
if not directory.exists():
|
|
18
|
+
return seeders
|
|
19
|
+
for file_path in sorted(directory.glob("*.py")):
|
|
20
|
+
if file_path.name.startswith("_"):
|
|
21
|
+
continue
|
|
22
|
+
module_name = f"seeder_{file_path.stem}"
|
|
23
|
+
spec = importlib.util.spec_from_file_location(module_name, str(file_path))
|
|
24
|
+
module = importlib.util.module_from_spec(spec)
|
|
25
|
+
spec.loader.exec_module(module)
|
|
26
|
+
for value in module.__dict__.values():
|
|
27
|
+
if isinstance(value, type) and issubclass(value, Seeder) and value is not Seeder:
|
|
28
|
+
seeders[value.__name__] = value
|
|
29
|
+
return seeders
|
|
30
|
+
|
|
31
|
+
def run(self, name=None):
|
|
32
|
+
seeders = self.discover()
|
|
33
|
+
if name is not None:
|
|
34
|
+
if name not in seeders:
|
|
35
|
+
raise ValueError(f"Seeder not found: {name}")
|
|
36
|
+
seeders[name]().run()
|
|
37
|
+
return [name]
|
|
38
|
+
ran = []
|
|
39
|
+
for seeder_name in sorted(seeders.keys()):
|
|
40
|
+
seeders[seeder_name]().run()
|
|
41
|
+
ran.append(seeder_name)
|
|
42
|
+
return ran
|