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/openapi.py ADDED
@@ -0,0 +1,233 @@
1
+ # OpenAPI schema registry + route factory for docs UIs.
2
+
3
+
4
+ import re
5
+
6
+ from future.routing import Get
7
+ from future.settings import APP_DESCRIPTION, APP_NAME, APP_VERSION
8
+
9
+
10
+ _DEFAULT_UIS = ["swagger", "redoc", "scalar", "rapidoc"]
11
+ _DOC_PATH_ENDS = ("/openapi.json", "/swagger-config", "/docs", "/redoc", "/scalar", "/rapidoc")
12
+
13
+ _SPEC = {
14
+ "openapi": "3.0.0",
15
+ "info": {
16
+ "title": APP_NAME,
17
+ "version": APP_VERSION,
18
+ "description": APP_DESCRIPTION,
19
+ },
20
+ "paths": {},
21
+ }
22
+
23
+ _CONFIG = {
24
+ "enabled": True,
25
+ "uis": list(_DEFAULT_UIS),
26
+ "auto_routes": False,
27
+ "path_prefix": "",
28
+ "redocly_license_key": "", # paid Redocly Reference Docs (Try it); empty = OSS ReDoc
29
+ "servers": None, # list of {"url": "...", "description": "..."} or None → filled at serve time from request + RouteGroup hosts
30
+ "security_schemes": None, # OpenAPI components.securitySchemes map
31
+ "security": None, # global security requirements
32
+ "models": [], # model classes → components.schemas via Model.openapi_schema()
33
+ }
34
+
35
+
36
+ def get_openapi_config(config=None):
37
+ if config and isinstance(config.get("OPENAPI"), dict):
38
+ merged = dict(_CONFIG)
39
+ merged.update(config["OPENAPI"])
40
+ if "uis" not in config["OPENAPI"]:
41
+ merged["uis"] = list(_DEFAULT_UIS)
42
+ return merged
43
+ return dict(_CONFIG)
44
+
45
+
46
+ def set_openapi_config(config):
47
+ global _CONFIG
48
+ if not config:
49
+ return
50
+ openapi = config.get("OPENAPI") if isinstance(config, dict) else None
51
+ if not isinstance(openapi, dict):
52
+ return
53
+ _CONFIG = get_openapi_config(config)
54
+
55
+
56
+ def get_spec():
57
+ return _SPEC
58
+
59
+
60
+ def path_prefix():
61
+ return _CONFIG.get("path_prefix") or ""
62
+
63
+
64
+ def spec_path():
65
+ return f"{path_prefix()}/openapi.json"
66
+
67
+
68
+ def is_docs_path(path):
69
+ for ending in _DOC_PATH_ENDS:
70
+ if path == ending or path.endswith(ending):
71
+ return True
72
+ return False
73
+
74
+
75
+ def rebuild_spec_from_routes(routes_by_domain, app_config=None):
76
+ global _SPEC
77
+ cfg = get_openapi_config(app_config)
78
+ info_title = APP_NAME
79
+ info_version = APP_VERSION
80
+ info_description = APP_DESCRIPTION
81
+ if app_config:
82
+ info_title = app_config.get("APP_NAME", info_title)
83
+ info_version = app_config.get("APP_VERSION", info_version)
84
+ info_description = app_config.get("APP_DESCRIPTION", info_description)
85
+ paths = {}
86
+ schemas = {}
87
+ for model in cfg.get("models") or []:
88
+ schemas[model.__name__] = model().openapi_schema()
89
+ if cfg.get("enabled", True):
90
+ for domain_key, route_map in routes_by_domain.items():
91
+ for route_key, route_config in route_map.items():
92
+ route = route_config.get("route")
93
+ path = getattr(route, "path", None) or (route_key.split(" ", 1)[-1] if " " in str(route_key) else route_key)
94
+ if is_docs_path(path):
95
+ continue
96
+ operation_id = getattr(route, "name", None) or path
97
+ handler = route_config.get("handler") or getattr(route, "endpoint", None)
98
+ doc = ""
99
+ if handler is not None:
100
+ doc = (getattr(handler, "__doc__", None) or "").strip()
101
+ summary = None
102
+ description = None
103
+ if doc:
104
+ # FastAPI-style: first line = summary, remainder = description (Markdown OK)
105
+ lines = doc.split("\n", 1)
106
+ summary = lines[0].strip() or None
107
+ if len(lines) > 1:
108
+ description = "\n".join(line.rstrip() for line in lines[1].splitlines()).strip() or None
109
+ # Drop common leading indent from the body
110
+ if description:
111
+ body_lines = description.splitlines()
112
+ indents = [len(line) - len(line.lstrip()) for line in body_lines if line.strip()]
113
+ if indents:
114
+ pad = min(indents)
115
+ description = "\n".join(line[pad:] if len(line) >= pad else line for line in body_lines).strip()
116
+ param_schemas = {}
117
+ for match in re.finditer(r"/<(?:([^:>]+):)?([^>]+)>", path):
118
+ type_name = match.group(1) or "string"
119
+ param_name = match.group(2)
120
+ if type_name == "int":
121
+ param_schemas[param_name] = {"type": "integer"}
122
+ elif type_name == "float":
123
+ param_schemas[param_name] = {"type": "number"}
124
+ elif type_name == "uuid":
125
+ param_schemas[param_name] = {"type": "string", "format": "uuid"}
126
+ else:
127
+ param_schemas[param_name] = {"type": "string"}
128
+ for match in re.finditer(r"/\{(?:([^:}]+):)?([^}]+)\}", path):
129
+ type_name = match.group(1) or "string"
130
+ param_name = match.group(2)
131
+ if type_name == "int":
132
+ param_schemas[param_name] = {"type": "integer"}
133
+ elif type_name == "float":
134
+ param_schemas[param_name] = {"type": "number"}
135
+ elif type_name == "uuid":
136
+ param_schemas[param_name] = {"type": "string", "format": "uuid"}
137
+ else:
138
+ param_schemas[param_name] = {"type": "string"}
139
+ path = re.sub(r"/<(?:[^:>]+:)?([^>]+)>", r"/{\1}", path)
140
+ path = re.sub(r"/\{(?:[^:}]+:)?([^}]+)\}", r"/{\1}", path)
141
+ path = re.sub(r"/:([A-Za-z_][A-Za-z0-9_]*)", r"/{\1}", path)
142
+ entry = paths.setdefault(path, {})
143
+ # Host for this RouteGroup (e.g. api.example.com); scheme applied when serving /openapi.json.
144
+ if domain_key:
145
+ hosts = entry.setdefault("x-future-hosts", [])
146
+ if domain_key not in hosts:
147
+ hosts.append(domain_key)
148
+ sub = (route_config.get("group") or {}).get("subdomain") or ""
149
+ if sub:
150
+ entry["x-future-subdomain"] = sub
151
+ for method in route_config.get("methods") or ["GET"]:
152
+ operation = {
153
+ "operationId": operation_id,
154
+ "responses": {"200": {"description": "OK"}},
155
+ }
156
+ if summary:
157
+ operation["summary"] = summary
158
+ if description:
159
+ operation["description"] = description
160
+ group_name = (route_config.get("group") or {}).get("name")
161
+ if group_name:
162
+ operation["tags"] = [group_name]
163
+ parameters = []
164
+ for param_name in getattr(route, "param_names", None) or []:
165
+ parameters.append({
166
+ "name": param_name,
167
+ "in": "path",
168
+ "required": True,
169
+ "schema": param_schemas.get(param_name) or {"type": "string"},
170
+ })
171
+ if parameters:
172
+ operation["parameters"] = parameters
173
+ if method.upper() in ("POST", "PUT", "PATCH"):
174
+ operation["requestBody"] = {
175
+ "required": False,
176
+ "content": {
177
+ "application/json": {"schema": {"type": "object"}},
178
+ "application/x-www-form-urlencoded": {"schema": {"type": "object"}},
179
+ "multipart/form-data": {"schema": {"type": "object"}},
180
+ },
181
+ }
182
+ entry[method.lower()] = operation
183
+ # Explicit OPENAPI.servers only at rebuild; default servers use request.scheme + registered hosts when serving.
184
+ servers = cfg.get("servers")
185
+ components = {}
186
+ security_schemes = cfg.get("security_schemes")
187
+ if security_schemes:
188
+ components["securitySchemes"] = security_schemes
189
+ if schemas:
190
+ components["schemas"] = schemas
191
+ _SPEC = {
192
+ "openapi": "3.0.0",
193
+ "info": {
194
+ "title": info_title,
195
+ "version": str(info_version),
196
+ "description": info_description,
197
+ },
198
+ "paths": paths,
199
+ }
200
+ if servers:
201
+ _SPEC["servers"] = servers
202
+ if components:
203
+ _SPEC["components"] = components
204
+ if cfg.get("security"):
205
+ _SPEC["security"] = cfg["security"]
206
+ return _SPEC
207
+
208
+
209
+ def openapi_routes(uis=None, path_prefix=None, config=None):
210
+ # Paths are root-relative (`/scalar`, …). Mount with RouteGroup(prefix=...); path_prefix only affects HTML spec URLs.
211
+ from future.controllers import OpenAPIController
212
+ global _CONFIG
213
+ cfg = get_openapi_config(config)
214
+ if not cfg.get("enabled", True):
215
+ return []
216
+ selected = uis if uis is not None else cfg.get("uis") or _DEFAULT_UIS
217
+ prefix = path_prefix if path_prefix is not None else (cfg.get("path_prefix") or "")
218
+ _CONFIG["path_prefix"] = prefix
219
+ if uis is not None:
220
+ _CONFIG["uis"] = list(selected)
221
+ routes = [
222
+ Get("/openapi.json", OpenAPIController.openapi, "openapi"),
223
+ ]
224
+ if "swagger" in selected:
225
+ routes.append(Get("/swagger-config", OpenAPIController.swagger_config, "swagger-config"))
226
+ routes.append(Get("/docs", OpenAPIController.swagger, "docs"))
227
+ if "redoc" in selected:
228
+ routes.append(Get("/redoc", OpenAPIController.redoc, "redoc"))
229
+ if "scalar" in selected:
230
+ routes.append(Get("/scalar", OpenAPIController.scalar, "scalar"))
231
+ if "rapidoc" in selected:
232
+ routes.append(Get("/rapidoc", OpenAPIController.rapidoc, "rapidoc"))
233
+ return routes