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/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
|