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.
Files changed (64) hide show
  1. future/__init__.py +3 -0
  2. future/application.py +656 -0
  3. future/authentication/Auth0Authentication.py +9 -0
  4. future/authentication/Authentication.py +11 -0
  5. future/authentication/AzureADAuthentication.py +9 -0
  6. future/authentication/BasicAuthentication.py +9 -0
  7. future/authentication/KerberosAuthentication.py +9 -0
  8. future/authentication/KeycloakAuthentication.py +9 -0
  9. future/authentication/OAuth2Authentication.py +9 -0
  10. future/authentication/OpenIdConnectAuthentication.py +9 -0
  11. future/authentication/SAMLAuthentication.py +9 -0
  12. future/cli/__init__.py +1 -0
  13. future/cli/main.py +608 -0
  14. future/cli/stubs.py +114 -0
  15. future/controllers/__init__.py +4 -0
  16. future/controllers/base.py +8 -0
  17. future/controllers/builtins.py +41 -0
  18. future/controllers/graphql.py +23 -0
  19. future/controllers/openapi.py +224 -0
  20. future/databases/Clickhouse.py +176 -0
  21. future/databases/Connections.py +20 -0
  22. future/databases/Database.py +66 -0
  23. future/databases/Elasticsearch.py +146 -0
  24. future/databases/MongoDB.py +177 -0
  25. future/databases/MySQL.py +215 -0
  26. future/databases/Postgres.py +214 -0
  27. future/databases/Redis.py +146 -0
  28. future/databases/SQLite.py +219 -0
  29. future/exceptions.py +27 -0
  30. future/graphql/__init__.py +1 -0
  31. future/graphql/schema.py +148 -0
  32. future/lifespan.py +70 -0
  33. future/logger.py +53 -0
  34. future/middleware/Middleware.py +201 -0
  35. future/middleware/SessionMiddleware.py +77 -0
  36. future/middleware/__init__.py +21 -0
  37. future/migrations/Blueprint.py +54 -0
  38. future/migrations/Column.py +33 -0
  39. future/migrations/Migration.py +11 -0
  40. future/migrations/MigrationGenerator.py +137 -0
  41. future/migrations/Migrator.py +72 -0
  42. future/migrations/Schema.py +21 -0
  43. future/models/__init__.py +1 -0
  44. future/models/model.py +224 -0
  45. future/openapi.py +233 -0
  46. future/plugins/ElasticsearchPlugin.py +496 -0
  47. future/plugins/__init__.py +9 -0
  48. future/request.py +143 -0
  49. future/response.py +201 -0
  50. future/routing.py +342 -0
  51. future/seeds/SeedGenerator.py +95 -0
  52. future/seeds/SeedRunner.py +42 -0
  53. future/seeds/Seeder.py +9 -0
  54. future/settings.py +94 -0
  55. future/tasks/__init__.py +14 -0
  56. future/tasks/scheduler.py +267 -0
  57. future/testing/__init__.py +1 -0
  58. future/testing/client.py +135 -0
  59. future/types.py +47 -0
  60. future_framework-1.1.0.dist-info/METADATA +68 -0
  61. future_framework-1.1.0.dist-info/RECORD +64 -0
  62. future_framework-1.1.0.dist-info/WHEEL +4 -0
  63. future_framework-1.1.0.dist-info/entry_points.txt +3 -0
  64. 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
future/seeds/Seeder.py ADDED
@@ -0,0 +1,9 @@
1
+ # Base class for database seeders (Orator/Masonite-style).
2
+
3
+
4
+ class Seeder:
5
+ def run(self):
6
+ raise NotImplementedError
7
+
8
+ def call(self, seeder_class):
9
+ seeder_class().run()