jac-scale 0.1.1__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.
- jac_scale/__init__.py +0 -0
- jac_scale/abstractions/config/app_config.jac +30 -0
- jac_scale/abstractions/config/base_config.jac +26 -0
- jac_scale/abstractions/database_provider.jac +51 -0
- jac_scale/abstractions/deployment_target.jac +64 -0
- jac_scale/abstractions/image_registry.jac +54 -0
- jac_scale/abstractions/logger.jac +20 -0
- jac_scale/abstractions/models/deployment_result.jac +27 -0
- jac_scale/abstractions/models/resource_status.jac +38 -0
- jac_scale/config_loader.jac +31 -0
- jac_scale/context.jac +14 -0
- jac_scale/factories/database_factory.jac +43 -0
- jac_scale/factories/deployment_factory.jac +43 -0
- jac_scale/factories/registry_factory.jac +32 -0
- jac_scale/factories/utility_factory.jac +34 -0
- jac_scale/impl/config_loader.impl.jac +131 -0
- jac_scale/impl/context.impl.jac +24 -0
- jac_scale/impl/memory_hierarchy.main.impl.jac +63 -0
- jac_scale/impl/memory_hierarchy.mongo.impl.jac +239 -0
- jac_scale/impl/memory_hierarchy.redis.impl.jac +186 -0
- jac_scale/impl/serve.impl.jac +1785 -0
- jac_scale/jserver/__init__.py +0 -0
- jac_scale/jserver/impl/jfast_api.impl.jac +731 -0
- jac_scale/jserver/impl/jserver.impl.jac +79 -0
- jac_scale/jserver/jfast_api.jac +162 -0
- jac_scale/jserver/jserver.jac +101 -0
- jac_scale/memory_hierarchy.jac +138 -0
- jac_scale/plugin.jac +218 -0
- jac_scale/plugin_config.jac +175 -0
- jac_scale/providers/database/kubernetes_mongo.jac +137 -0
- jac_scale/providers/database/kubernetes_redis.jac +110 -0
- jac_scale/providers/registry/dockerhub.jac +64 -0
- jac_scale/serve.jac +118 -0
- jac_scale/targets/kubernetes/kubernetes_config.jac +215 -0
- jac_scale/targets/kubernetes/kubernetes_target.jac +841 -0
- jac_scale/targets/kubernetes/utils/kubernetes_utils.impl.jac +519 -0
- jac_scale/targets/kubernetes/utils/kubernetes_utils.jac +85 -0
- jac_scale/tests/__init__.py +0 -0
- jac_scale/tests/conftest.py +29 -0
- jac_scale/tests/fixtures/test_api.jac +159 -0
- jac_scale/tests/fixtures/todo_app.jac +68 -0
- jac_scale/tests/test_abstractions.py +88 -0
- jac_scale/tests/test_deploy_k8s.py +265 -0
- jac_scale/tests/test_examples.py +484 -0
- jac_scale/tests/test_factories.py +149 -0
- jac_scale/tests/test_file_upload.py +444 -0
- jac_scale/tests/test_k8s_utils.py +156 -0
- jac_scale/tests/test_memory_hierarchy.py +247 -0
- jac_scale/tests/test_serve.py +1835 -0
- jac_scale/tests/test_sso.py +711 -0
- jac_scale/utilities/loggers/standard_logger.jac +40 -0
- jac_scale/utils.jac +16 -0
- jac_scale-0.1.1.dist-info/METADATA +658 -0
- jac_scale-0.1.1.dist-info/RECORD +57 -0
- jac_scale-0.1.1.dist-info/WHEEL +5 -0
- jac_scale-0.1.1.dist-info/entry_points.txt +3 -0
- jac_scale-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
import logging;
|
|
2
|
+
|
|
3
|
+
"""Helper function to convert TransportResponse to JSONResponse (similar to HTTPTransport.send)."""
|
|
4
|
+
def _convert_transport_response_to_json_response(
|
|
5
|
+
transport_response: TransportResponse
|
|
6
|
+
) -> JSONResponse {
|
|
7
|
+
import json;
|
|
8
|
+
import from fastapi.responses { JSONResponse }
|
|
9
|
+
import from jaclang.runtimelib.transport { TransportResponse }
|
|
10
|
+
# Extract HTTP status from meta.extra or use defaults (same logic as HTTPTransport.send)
|
|
11
|
+
status = 200 if transport_response.ok else 500;
|
|
12
|
+
if transport_response.meta and transport_response.meta.extra {
|
|
13
|
+
status = transport_response.meta.extra.get('http_status', status);
|
|
14
|
+
}
|
|
15
|
+
# Build response body following TransportResponse envelope pattern (same as HTTPTransport.send)
|
|
16
|
+
response_body = {
|
|
17
|
+
'ok': transport_response.ok,
|
|
18
|
+
'type': transport_response.type,
|
|
19
|
+
'data': transport_response.data,
|
|
20
|
+
'error': None
|
|
21
|
+
};
|
|
22
|
+
# Serialize error if present
|
|
23
|
+
if not transport_response.ok and transport_response.error {
|
|
24
|
+
error_obj = {};
|
|
25
|
+
if getattr(transport_response.error, "code", None) {
|
|
26
|
+
error_obj['code'] = transport_response.error.code;
|
|
27
|
+
}
|
|
28
|
+
if getattr(transport_response.error, "message", None) {
|
|
29
|
+
error_obj['message'] = transport_response.error.message;
|
|
30
|
+
}
|
|
31
|
+
if getattr(transport_response.error, "details", None) is not None {
|
|
32
|
+
error_obj['details'] = transport_response.error.details;
|
|
33
|
+
}
|
|
34
|
+
response_body['error'] = error_obj;
|
|
35
|
+
}
|
|
36
|
+
# Include metadata if present
|
|
37
|
+
if transport_response.meta {
|
|
38
|
+
meta_dict = {};
|
|
39
|
+
if getattr(transport_response.meta, "request_id", None) {
|
|
40
|
+
meta_dict['request_id'] = transport_response.meta.request_id;
|
|
41
|
+
}
|
|
42
|
+
if getattr(transport_response.meta, "trace_id", None) {
|
|
43
|
+
meta_dict['trace_id'] = transport_response.meta.trace_id;
|
|
44
|
+
}
|
|
45
|
+
if getattr(transport_response.meta, "timestamp", None) {
|
|
46
|
+
meta_dict['timestamp'] = transport_response.meta.timestamp;
|
|
47
|
+
}
|
|
48
|
+
if getattr(transport_response.meta, "extra", None) {
|
|
49
|
+
meta_dict['extra'] = transport_response.meta.extra;
|
|
50
|
+
}
|
|
51
|
+
if meta_dict {
|
|
52
|
+
response_body['meta'] = meta_dict;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return JSONResponse(status_code=status, content=response_body);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
"""Custom logging handler that routes logs to jac-super console."""
|
|
59
|
+
class ConsoleLogHandler(logging.Handler) {
|
|
60
|
+
"""A logging handler that forwards log messages to jac-super's console."""
|
|
61
|
+
def emit(self: ConsoleLogHandler, record: logging.LogRecord) -> None {
|
|
62
|
+
import from jaclang.cli.console { console }
|
|
63
|
+
message = self.format(record);
|
|
64
|
+
console.print(f" {message}");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
"""Run the FastAPI server using Uvicorn."""
|
|
69
|
+
impl JFastApiServer.run_server(
|
|
70
|
+
self: JFastApiServer, host: str = '0.0.0.0', port: int = 8000
|
|
71
|
+
) -> None {
|
|
72
|
+
app = self.create_server();
|
|
73
|
+
# Create custom handler for console logging
|
|
74
|
+
console_handler = ConsoleLogHandler();
|
|
75
|
+
console_handler.setFormatter(logging.Formatter("%(message)s"));
|
|
76
|
+
# Suppress startup logs but route access logs through jac-super console
|
|
77
|
+
log_config = {
|
|
78
|
+
"version": 1,
|
|
79
|
+
"disable_existing_loggers": False,
|
|
80
|
+
"formatters": {
|
|
81
|
+
"default": {"format": "%(message)s"},
|
|
82
|
+
"access": {"format": "%(message)s"},
|
|
83
|
+
|
|
84
|
+
},
|
|
85
|
+
"handlers": {
|
|
86
|
+
"default": {
|
|
87
|
+
"formatter": "default",
|
|
88
|
+
"class": "logging.StreamHandler",
|
|
89
|
+
"stream": "ext://sys.stderr"
|
|
90
|
+
},
|
|
91
|
+
"access": {"()": lambda : console_handler },
|
|
92
|
+
|
|
93
|
+
},
|
|
94
|
+
"loggers": {
|
|
95
|
+
"uvicorn": {
|
|
96
|
+
"handlers": ["default"],
|
|
97
|
+
"level": "WARNING",
|
|
98
|
+
"propagate": False
|
|
99
|
+
},
|
|
100
|
+
"uvicorn.error": {"level": "WARNING", "propagate": False},
|
|
101
|
+
"uvicorn.access": {
|
|
102
|
+
"handlers": ["access"],
|
|
103
|
+
"level": "INFO",
|
|
104
|
+
"propagate": False
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
};
|
|
110
|
+
uvicorn.run(app, host=host, port=port, log_config=log_config);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
"""Get the underlying FastAPI application instance."""
|
|
114
|
+
impl JFastApiServer.get_app(self: JFastApiServer) -> FastAPI {
|
|
115
|
+
return self.app;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
"""Create a Pydantic response model from configuration."""
|
|
119
|
+
impl JFastApiServer._create_response_model(
|
|
120
|
+
self: JFastApiServer, response_config: (dict[(str, Any)] | None) = None
|
|
121
|
+
) -> (type[BaseModel] | None) {
|
|
122
|
+
if not response_config {
|
|
123
|
+
return None;
|
|
124
|
+
}
|
|
125
|
+
model_name = response_config.get('name', 'ResponseModel');
|
|
126
|
+
fields = response_config.get('fields', {});
|
|
127
|
+
if not fields {
|
|
128
|
+
return None;
|
|
129
|
+
}
|
|
130
|
+
pydantic_fields: dict[(str, Any)] = {};
|
|
131
|
+
for (field_name, field_config) in fields.items() {
|
|
132
|
+
field_type = self._get_python_type(field_config.get('type', 'str'));
|
|
133
|
+
required = field_config.get('required', True);
|
|
134
|
+
description = field_config.get('description', '');
|
|
135
|
+
if required {
|
|
136
|
+
pydantic_fields[field_name] = (
|
|
137
|
+
field_type,
|
|
138
|
+
Field(..., description=description)
|
|
139
|
+
);
|
|
140
|
+
} else {
|
|
141
|
+
default_value = field_config.get('default');
|
|
142
|
+
pydantic_fields[field_name] = (
|
|
143
|
+
(field_type | None),
|
|
144
|
+
Field(default_value, description=description)
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
model = create_model(model_name, **pydantic_fields);
|
|
149
|
+
self._models[model_name] = model;
|
|
150
|
+
return model;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
"""Convert string type to Python type."""
|
|
154
|
+
impl JFastApiServer._get_python_type(
|
|
155
|
+
self: JFastApiServer, type_string: str
|
|
156
|
+
) -> type[Any] {
|
|
157
|
+
if (type_string.startswith("<class '") and type_string.endswith("'>")) {
|
|
158
|
+
type_string = type_string[8:-2];
|
|
159
|
+
}
|
|
160
|
+
type_mapping: dict[(str, type[Any])] = {
|
|
161
|
+
'str': str,
|
|
162
|
+
'string': str,
|
|
163
|
+
'int': int,
|
|
164
|
+
'integer': int,
|
|
165
|
+
'float': float,
|
|
166
|
+
'number': float,
|
|
167
|
+
'bool': bool,
|
|
168
|
+
'boolean': bool,
|
|
169
|
+
'list': <>list,
|
|
170
|
+
'dict': <>dict,
|
|
171
|
+
'object': <>dict
|
|
172
|
+
};
|
|
173
|
+
return type_mapping.get(type_string.lower(), str);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
"""Implementation of PATH parameter handler."""
|
|
177
|
+
impl PathParameterHandler.generate_param_string(param: APIParameter) -> str {
|
|
178
|
+
type_name = self.get_type_name(param);
|
|
179
|
+
description = param.description or '';
|
|
180
|
+
return f"{param.name}: {type_name} = Path(..., description='{description}')";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
"""Implementation of QUERY parameter handler."""
|
|
184
|
+
impl QueryParameterHandler.generate_param_string(param: APIParameter) -> str {
|
|
185
|
+
type_name = self.get_type_name(param);
|
|
186
|
+
description = param.description or '';
|
|
187
|
+
if param.required {
|
|
188
|
+
return f"{param.name}: {type_name} = Query(..., description='{description}')";
|
|
189
|
+
} elif (param.default is None) {
|
|
190
|
+
return f"{param.name}: Optional[{type_name}] = Query(description='{description}')";
|
|
191
|
+
} else {
|
|
192
|
+
return f"{param.name}: Optional[{type_name}] = Query({repr(param.default)}, description='{description}')";
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
"""Implementation of HEADER parameter handler."""
|
|
197
|
+
impl HeaderParameterHandler.generate_param_string(param: APIParameter) -> str {
|
|
198
|
+
type_name = self.get_type_name(param);
|
|
199
|
+
description = param.description or '';
|
|
200
|
+
if param.required {
|
|
201
|
+
return f"{param.name}: {type_name} = Header(..., description='{description}')";
|
|
202
|
+
} elif (param.default is None) {
|
|
203
|
+
return f"{param.name}: Optional[{type_name}] = Header(default=None, description='{description}')";
|
|
204
|
+
} else {
|
|
205
|
+
return f"{param.name}: Optional[{type_name}] = Header({repr(param.default)}, description='{description}')";
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
"""Implementation of FILE parameter handler."""
|
|
210
|
+
impl FileParameterHandler.generate_param_string(param: APIParameter) -> str {
|
|
211
|
+
description = param.description or '';
|
|
212
|
+
is_multiple = 'list' in param.data_type.lower();
|
|
213
|
+
if is_multiple {
|
|
214
|
+
if param.required {
|
|
215
|
+
return f"{param.name}: List[UploadFile] = File(..., description='{description}')";
|
|
216
|
+
} else {
|
|
217
|
+
return f"{param.name}: Optional[List[UploadFile]] = File(default=None, description='{description}')";
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
if param.required {
|
|
221
|
+
return f"{param.name}: UploadFile = File(..., description='{description}')";
|
|
222
|
+
} else {
|
|
223
|
+
return f"{param.name}: Optional[UploadFile] = File(default=None, description='{description}')";
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
"""Implementation of Form BODY parameter handler."""
|
|
229
|
+
impl FormBodyParameterHandler.generate_param_string(param: APIParameter) -> str {
|
|
230
|
+
type_name = self.get_type_name(param);
|
|
231
|
+
description = param.description or '';
|
|
232
|
+
if param.required {
|
|
233
|
+
return f"{param.name}: {type_name} = Form(..., description='{description}')";
|
|
234
|
+
} elif (param.default is None) {
|
|
235
|
+
return f"{param.name}: Optional[{type_name}] = Form(default=None, description='{description}')";
|
|
236
|
+
} else {
|
|
237
|
+
return f"{param.name}: Optional[{type_name}] = Form(default={repr(
|
|
238
|
+
param.default
|
|
239
|
+
)}, description='{description}')";
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
"""Implementation of JSON BODY parameter handler - generate Pydantic model."""
|
|
244
|
+
impl JSONBodyParameterHandler.generate_body_model -> (type[BaseModel] | None) {
|
|
245
|
+
model_fields: dict[(str, Any)] = {};
|
|
246
|
+
for param in self.body_params {
|
|
247
|
+
if not param.name {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
param_type = self.type_converter._get_python_type(param.data_type);
|
|
251
|
+
description = param.description or '';
|
|
252
|
+
if param.required {
|
|
253
|
+
model_fields[param.name] = (
|
|
254
|
+
param_type,
|
|
255
|
+
Field(..., description=description)
|
|
256
|
+
);
|
|
257
|
+
} else {
|
|
258
|
+
if (param.default is None) {
|
|
259
|
+
model_fields[param.name] = (
|
|
260
|
+
(param_type | None),
|
|
261
|
+
Field(description=description)
|
|
262
|
+
);
|
|
263
|
+
} else {
|
|
264
|
+
model_fields[param.name] = (
|
|
265
|
+
(param_type | None),
|
|
266
|
+
Field(param.default, description=description)
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if model_fields {
|
|
272
|
+
return create_model('RequestBody', **model_fields);
|
|
273
|
+
}
|
|
274
|
+
return None;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
"""Create the actual endpoint function with parameter injection."""
|
|
278
|
+
impl JFastApiServer._create_endpoint_function(
|
|
279
|
+
self: JFastApiServer,
|
|
280
|
+
callback: Callable[(..., Any)],
|
|
281
|
+
parameters: list[APIParameter],
|
|
282
|
+
dependencies: list[Any]
|
|
283
|
+
) -> Callable[..., Any] {
|
|
284
|
+
sig = inspect.signature(callback);
|
|
285
|
+
accepts_kwargs = <>any(
|
|
286
|
+
(p.kind == inspect.Parameter.VAR_KEYWORD) for p in sig.parameters.values()
|
|
287
|
+
);
|
|
288
|
+
if not parameters {
|
|
289
|
+
if accepts_kwargs {
|
|
290
|
+
if inspect.iscoroutinefunction(callback) {
|
|
291
|
+
async def async_endpoint_wrapper(request: Request) -> EndpointResponse {
|
|
292
|
+
import from jaclang.runtimelib.transport { TransportResponse }
|
|
293
|
+
try {
|
|
294
|
+
query_params = <>dict(request.query_params);
|
|
295
|
+
result = await callback(**query_params);
|
|
296
|
+
# Automatically convert TransportResponse to JSONResponse (like HTTPTransport.send)
|
|
297
|
+
if isinstance(result, TransportResponse) {
|
|
298
|
+
return _convert_transport_response_to_json_response(result);
|
|
299
|
+
}
|
|
300
|
+
return result;
|
|
301
|
+
} except Exception as e {
|
|
302
|
+
raise e from HTTPException(status_code=500, detail=str(e)) ;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return async_endpoint_wrapper;
|
|
306
|
+
} else {
|
|
307
|
+
def sync_endpoint_wrapper(request: Request) -> EndpointResponse {
|
|
308
|
+
import from jaclang.runtimelib.transport { TransportResponse }
|
|
309
|
+
try {
|
|
310
|
+
query_params = <>dict(request.query_params);
|
|
311
|
+
result = callback(**query_params);
|
|
312
|
+
# Automatically convert TransportResponse to JSONResponse (like HTTPTransport.send)
|
|
313
|
+
if isinstance(result, TransportResponse) {
|
|
314
|
+
return _convert_transport_response_to_json_response(result);
|
|
315
|
+
}
|
|
316
|
+
return result;
|
|
317
|
+
} except Exception as e {
|
|
318
|
+
raise e from HTTPException(status_code=500, detail=str(e)) ;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return sync_endpoint_wrapper;
|
|
322
|
+
}
|
|
323
|
+
} elif inspect.iscoroutinefunction(callback) {
|
|
324
|
+
async def async_endpoint_wrapper__1 -> EndpointResponse {
|
|
325
|
+
import from jaclang.runtimelib.transport { TransportResponse }
|
|
326
|
+
try {
|
|
327
|
+
result = await callback();
|
|
328
|
+
# Automatically convert TransportResponse to JSONResponse (like HTTPTransport.send)
|
|
329
|
+
if isinstance(result, TransportResponse) {
|
|
330
|
+
return _convert_transport_response_to_json_response(result);
|
|
331
|
+
}
|
|
332
|
+
return result;
|
|
333
|
+
} except Exception as e {
|
|
334
|
+
raise e from HTTPException(status_code=500, detail=str(e)) ;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return async_endpoint_wrapper__1;
|
|
338
|
+
} else {
|
|
339
|
+
def sync_endpoint_wrapper__1 -> EndpointResponse {
|
|
340
|
+
import from jaclang.runtimelib.transport { TransportResponse }
|
|
341
|
+
try {
|
|
342
|
+
result = callback();
|
|
343
|
+
# Automatically convert TransportResponse to JSONResponse (like HTTPTransport.send)
|
|
344
|
+
if isinstance(result, TransportResponse) {
|
|
345
|
+
return _convert_transport_response_to_json_response(result);
|
|
346
|
+
}
|
|
347
|
+
return result;
|
|
348
|
+
} except Exception as e {
|
|
349
|
+
raise e from HTTPException(status_code=500, detail=str(e)) ;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return sync_endpoint_wrapper__1;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
# Group parameters by type
|
|
356
|
+
body_params: list[APIParameter] = [];
|
|
357
|
+
path_params: list[APIParameter] = [];
|
|
358
|
+
query_params: list[APIParameter] = [];
|
|
359
|
+
header_params: list[APIParameter] = [];
|
|
360
|
+
file_params: list[APIParameter] = [];
|
|
361
|
+
for param in parameters {
|
|
362
|
+
param_location = param.type;
|
|
363
|
+
if (param_location == ParameterType.BODY) {
|
|
364
|
+
body_params.append(param);
|
|
365
|
+
} elif (param_location == ParameterType.PATH) {
|
|
366
|
+
path_params.append(param);
|
|
367
|
+
} elif (param_location == ParameterType.QUERY) {
|
|
368
|
+
query_params.append(param);
|
|
369
|
+
} elif (param_location == ParameterType.HEADER) {
|
|
370
|
+
header_params.append(param);
|
|
371
|
+
} elif (param_location == ParameterType.FILE) {
|
|
372
|
+
file_params.append(param);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
# Initialize parameter strings and mapping
|
|
376
|
+
param_strs: list[str] = [];
|
|
377
|
+
param_mapping: dict[(str, str)] = {};
|
|
378
|
+
needs_request = 'request' in sig.parameters;
|
|
379
|
+
if accepts_kwargs {
|
|
380
|
+
param_strs.append('request: Request');
|
|
381
|
+
param_mapping['__request__'] = 'request';
|
|
382
|
+
} elif needs_request {
|
|
383
|
+
param_strs.append('request: Request');
|
|
384
|
+
param_mapping['request'] = 'request';
|
|
385
|
+
}
|
|
386
|
+
# Create handler map
|
|
387
|
+
has_file_params = len(file_params) > 0;
|
|
388
|
+
handler_map: dict[(ParameterType, ParameterHandler)] = {
|
|
389
|
+
ParameterType.PATH: PathParameterHandler(type_converter=self),
|
|
390
|
+
ParameterType.QUERY: QueryParameterHandler(type_converter=self),
|
|
391
|
+
ParameterType.HEADER: HeaderParameterHandler(type_converter=self),
|
|
392
|
+
ParameterType.FILE: FileParameterHandler(type_converter=self)
|
|
393
|
+
};
|
|
394
|
+
# Handle body parameters using appropriate handler
|
|
395
|
+
body_model: (type[BaseModel] | None) = None;
|
|
396
|
+
if (len(body_params) >= 1) {
|
|
397
|
+
if has_file_params {
|
|
398
|
+
# Use Form handler when files are present (multipart/form-data)
|
|
399
|
+
form_handler = FormBodyParameterHandler(type_converter=self);
|
|
400
|
+
for param in body_params {
|
|
401
|
+
if param.name {
|
|
402
|
+
param_str = form_handler.generate_param_string(param);
|
|
403
|
+
param_strs.append(param_str);
|
|
404
|
+
param_mapping[param.name] = param.name;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
} else {
|
|
408
|
+
# Use JSON body handler (Pydantic model)
|
|
409
|
+
json_handler = JSONBodyParameterHandler(
|
|
410
|
+
type_converter=self, body_params=body_params
|
|
411
|
+
);
|
|
412
|
+
body_model = json_handler.generate_body_model();
|
|
413
|
+
if body_model {
|
|
414
|
+
param_strs.append('body_data: RequestBody');
|
|
415
|
+
param_mapping['body_data'] = 'body_data';
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
# Process other parameter types using their handlers
|
|
420
|
+
param_groups = [
|
|
421
|
+
(path_params, ParameterType.PATH),
|
|
422
|
+
(query_params, ParameterType.QUERY),
|
|
423
|
+
(header_params, ParameterType.HEADER),
|
|
424
|
+
(file_params, ParameterType.FILE)
|
|
425
|
+
];
|
|
426
|
+
for (param_list, param_type) in param_groups {
|
|
427
|
+
handler = handler_map.get(param_type);
|
|
428
|
+
if handler {
|
|
429
|
+
for param in param_list {
|
|
430
|
+
if param.name {
|
|
431
|
+
param_str = handler.generate_param_string(param);
|
|
432
|
+
param_strs.append(param_str);
|
|
433
|
+
param_mapping[param.name] = param.name;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
params = ', '.join(param_strs);
|
|
439
|
+
callback_args_lines: list[str] = [];
|
|
440
|
+
if body_model {
|
|
441
|
+
for param in body_params {
|
|
442
|
+
param_name = param.name;
|
|
443
|
+
if param_name {
|
|
444
|
+
callback_args_lines.append(
|
|
445
|
+
f" callback_args['{param_name}'] = body_data.{param_name}"
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
for name in param_mapping {
|
|
450
|
+
if (name not in ('body_data', '__request__')) {
|
|
451
|
+
callback_args_lines.append(f" callback_args['{name}'] = {name}");
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
} else {
|
|
455
|
+
callback_args_lines = [
|
|
456
|
+
f" callback_args['{name}'] = {name}"
|
|
457
|
+
for name in param_mapping
|
|
458
|
+
if (name != '__request__')
|
|
459
|
+
];
|
|
460
|
+
}
|
|
461
|
+
callback_args_str = '\n'.join(callback_args_lines);
|
|
462
|
+
extra_query_params_code = '';
|
|
463
|
+
if accepts_kwargs {
|
|
464
|
+
declared_params = [
|
|
465
|
+
p.name
|
|
466
|
+
for p in parameters
|
|
467
|
+
if p.name
|
|
468
|
+
];
|
|
469
|
+
declared_params_str = repr(declared_params);
|
|
470
|
+
extra_query_params_code = f"""
|
|
471
|
+
# Extract additional query parameters not explicitly declared
|
|
472
|
+
declared_params = set({declared_params_str})
|
|
473
|
+
for key, value in request.query_params.items():
|
|
474
|
+
if key not in declared_params:
|
|
475
|
+
callback_args[key] = value
|
|
476
|
+
""";
|
|
477
|
+
}
|
|
478
|
+
if inspect.iscoroutinefunction(callback) {
|
|
479
|
+
func_code = f"""
|
|
480
|
+
async def endpoint_wrapper({params}):
|
|
481
|
+
try:
|
|
482
|
+
callback_args: Dict[str, Any] = {{}}
|
|
483
|
+
{callback_args_str}{extra_query_params_code}
|
|
484
|
+
result = await callback(**callback_args)
|
|
485
|
+
# Automatically convert TransportResponse to JSONResponse (like HTTPTransport.send)
|
|
486
|
+
if isinstance(result, TransportResponse):
|
|
487
|
+
return _convert_transport_response_to_json_response(result)
|
|
488
|
+
return result
|
|
489
|
+
except Exception as e:
|
|
490
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
491
|
+
""";
|
|
492
|
+
} else {
|
|
493
|
+
func_code = f"""
|
|
494
|
+
def endpoint_wrapper({params}):
|
|
495
|
+
try:
|
|
496
|
+
callback_args: Dict[str, Any] = {{}}
|
|
497
|
+
{callback_args_str}{extra_query_params_code}
|
|
498
|
+
result = callback(**callback_args)
|
|
499
|
+
# Automatically convert TransportResponse to JSONResponse (like HTTPTransport.send)
|
|
500
|
+
if isinstance(result, TransportResponse):
|
|
501
|
+
return _convert_transport_response_to_json_response(result)
|
|
502
|
+
return result
|
|
503
|
+
except Exception as e:
|
|
504
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
505
|
+
""";
|
|
506
|
+
}
|
|
507
|
+
exec_globals: dict[(str, Any)] = {
|
|
508
|
+
'callback': callback,
|
|
509
|
+
'HTTPException': HTTPException,
|
|
510
|
+
'Query': Query,
|
|
511
|
+
'Path': Path,
|
|
512
|
+
'Body': Body,
|
|
513
|
+
'Header': Header,
|
|
514
|
+
'File': File,
|
|
515
|
+
'Form': Form,
|
|
516
|
+
'UploadFile': UploadFile,
|
|
517
|
+
'Request': Request,
|
|
518
|
+
'Optional': Optional,
|
|
519
|
+
'Field': Field,
|
|
520
|
+
'create_model': create_model,
|
|
521
|
+
'Dict': <>dict,
|
|
522
|
+
'List': <>list,
|
|
523
|
+
'Any': Any,
|
|
524
|
+
'int': int,
|
|
525
|
+
'str': str,
|
|
526
|
+
'float': float,
|
|
527
|
+
'bool': bool,
|
|
528
|
+
'list': <>list,
|
|
529
|
+
'dict': <>dict,
|
|
530
|
+
'TransportResponse': TransportResponse,
|
|
531
|
+
'_convert_transport_response_to_json_response': _convert_transport_response_to_json_response,
|
|
532
|
+
'isinstance': isinstance
|
|
533
|
+
};
|
|
534
|
+
if body_model {
|
|
535
|
+
exec_globals['RequestBody'] = body_model;
|
|
536
|
+
}
|
|
537
|
+
exec(func_code, exec_globals);
|
|
538
|
+
return exec_globals['endpoint_wrapper'];
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
"""Get the default status code for an HTTP method."""
|
|
542
|
+
impl JFastApiServer._get_default_status_code(
|
|
543
|
+
self: JFastApiServer, method: HTTPMethod
|
|
544
|
+
) -> int {
|
|
545
|
+
status_codes = {
|
|
546
|
+
HTTPMethod.GET: 200,
|
|
547
|
+
HTTPMethod.POST: 200,
|
|
548
|
+
HTTPMethod.PUT: 200,
|
|
549
|
+
HTTPMethod.PATCH: 200,
|
|
550
|
+
HTTPMethod.DELETE: 204
|
|
551
|
+
};
|
|
552
|
+
return status_codes.get(method, 200);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
"""
|
|
556
|
+
Create and register a FastAPI route for the given endpoint.
|
|
557
|
+
Args:method (HTTPMethod): The HTTP method for the route
|
|
558
|
+
method (HTTPMethod): The HTTP method for the route
|
|
559
|
+
endpoint (JEndPoint): The endpoint configuration
|
|
560
|
+
"""
|
|
561
|
+
impl JFastApiServer._create_fastapi_route(
|
|
562
|
+
self: JFastApiServer, method: HTTPMethod, endpoint: JEndPoint
|
|
563
|
+
) -> None {
|
|
564
|
+
endpoint_func = self._create_endpoint_function(
|
|
565
|
+
endpoint.callback, (endpoint.parameters or []), []
|
|
566
|
+
);
|
|
567
|
+
route_kwargs: dict[(str, Any)] = {
|
|
568
|
+
'response_model': endpoint.response_model,
|
|
569
|
+
'status_code': self._get_default_status_code(method),
|
|
570
|
+
'summary': (endpoint.summary or f"{method.value} {endpoint.path}"),
|
|
571
|
+
'description': (
|
|
572
|
+
endpoint.description or endpoint.callback.__doc__
|
|
573
|
+
if endpoint.callback.__doc__
|
|
574
|
+
else ''
|
|
575
|
+
),
|
|
576
|
+
'tags': (endpoint.tags or [])
|
|
577
|
+
};
|
|
578
|
+
try {
|
|
579
|
+
hints = get_type_hints(endpoint.callback);
|
|
580
|
+
return_type = hints.get('return');
|
|
581
|
+
if (
|
|
582
|
+
return_type
|
|
583
|
+
and isinstance(return_type, <>type)
|
|
584
|
+
and issubclass(return_type, Response)
|
|
585
|
+
) {
|
|
586
|
+
route_kwargs['response_class'] = return_type;
|
|
587
|
+
}
|
|
588
|
+
} except Exception {
|
|
589
|
+
;
|
|
590
|
+
}
|
|
591
|
+
if (method == HTTPMethod.GET) {
|
|
592
|
+
self.app.get(endpoint.path, **route_kwargs)(endpoint_func);
|
|
593
|
+
} elif (method == HTTPMethod.POST) {
|
|
594
|
+
self.app.post(endpoint.path, **route_kwargs)(endpoint_func);
|
|
595
|
+
} elif (method == HTTPMethod.PUT) {
|
|
596
|
+
self.app.put(endpoint.path, **route_kwargs)(endpoint_func);
|
|
597
|
+
} elif (method == HTTPMethod.PATCH) {
|
|
598
|
+
self.app.patch(endpoint.path, **route_kwargs)(endpoint_func);
|
|
599
|
+
} elif (method == HTTPMethod.DELETE) {
|
|
600
|
+
self.app.delete(endpoint.path, **route_kwargs)(endpoint_func);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
"""
|
|
605
|
+
Create a complete FastAPI server with all endpoints registered.
|
|
606
|
+
This method executes all registered endpoints to create FastAPI routes
|
|
607
|
+
and returns the configured FastAPI application.
|
|
608
|
+
|
|
609
|
+
Returns:
|
|
610
|
+
FastAPI: The configured FastAPI application instance
|
|
611
|
+
"""
|
|
612
|
+
impl JFastApiServer.create_server(self: JFastApiServer) -> FastAPI {
|
|
613
|
+
if not self.__server_created {
|
|
614
|
+
self.execute();
|
|
615
|
+
self.__server_created = True;
|
|
616
|
+
}
|
|
617
|
+
return self.app;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
"""
|
|
621
|
+
Execute all endpoints by processing them through their respective HTTP method handlers.
|
|
622
|
+
Routes are sorted to ensure more specific paths are registered before generic ones
|
|
623
|
+
to avoid path matching conflicts.
|
|
624
|
+
"""
|
|
625
|
+
impl JFastApiServer.execute(self: JFastApiServer) -> None {
|
|
626
|
+
self._endpoints = sorted(self._endpoints, key=self._route_priority);
|
|
627
|
+
super.execute();
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
"""
|
|
631
|
+
Calculate route priority for sorting. More specific routes get higher priority (lower number).
|
|
632
|
+
Priority rules:
|
|
633
|
+
1. Static paths (no parameters) come first
|
|
634
|
+
2. Paths with fewer parameters come before paths with more parameters
|
|
635
|
+
3. Longer paths come before shorter paths
|
|
636
|
+
4. Alphabetical order for tie-breaking
|
|
637
|
+
"""
|
|
638
|
+
impl JFastApiServer._route_priority(
|
|
639
|
+
self: JFastApiServer, endpoint: JEndPoint
|
|
640
|
+
) -> tuple[int, int, int, str] {
|
|
641
|
+
path = endpoint.path;
|
|
642
|
+
is_catchall = ':path}' in path;
|
|
643
|
+
param_count = path.count('{');
|
|
644
|
+
segment_count = len(
|
|
645
|
+
[
|
|
646
|
+
seg
|
|
647
|
+
for seg in path.split('/')
|
|
648
|
+
if seg
|
|
649
|
+
]
|
|
650
|
+
);
|
|
651
|
+
catchall_priority = 1 if is_catchall else 0;
|
|
652
|
+
priority = param_count;
|
|
653
|
+
return (catchall_priority, priority, -segment_count, path);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
"""
|
|
657
|
+
Handle execution of a DELETE endpoint by registering it with FastAPI.
|
|
658
|
+
Args:
|
|
659
|
+
endpoint (JEndPoint): The DELETE endpoint to execute
|
|
660
|
+
Returns:
|
|
661
|
+
JFastApiServer: Self for method chaining
|
|
662
|
+
"""
|
|
663
|
+
impl JFastApiServer._delete(
|
|
664
|
+
self: JFastApiServer, endpoint: JEndPoint
|
|
665
|
+
) -> 'JFastApiServer' {
|
|
666
|
+
self._create_fastapi_route(HTTPMethod.DELETE, endpoint);
|
|
667
|
+
return self;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
"""
|
|
671
|
+
Handle execution of a PATCH endpoint by registering it with FastAPI.
|
|
672
|
+
Args:
|
|
673
|
+
endpoint (JEndPoint): The PATCH endpoint to execute
|
|
674
|
+
Returns:
|
|
675
|
+
JFastApiServer: Self for method chaining
|
|
676
|
+
"""
|
|
677
|
+
impl JFastApiServer._patch(
|
|
678
|
+
self: JFastApiServer, endpoint: JEndPoint
|
|
679
|
+
) -> 'JFastApiServer' {
|
|
680
|
+
self._create_fastapi_route(HTTPMethod.PATCH, endpoint);
|
|
681
|
+
return self;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
"""
|
|
685
|
+
Handle execution of a PUT endpoint by registering it with FastAPI.
|
|
686
|
+
Args:
|
|
687
|
+
endpoint (JEndPoint): The PUT endpoint to execute
|
|
688
|
+
Returns:
|
|
689
|
+
JFastApiServer: Self for method chaining
|
|
690
|
+
"""
|
|
691
|
+
impl JFastApiServer._put(self: JFastApiServer, endpoint: JEndPoint) -> 'JFastApiServer' {
|
|
692
|
+
self._create_fastapi_route(HTTPMethod.PUT, endpoint);
|
|
693
|
+
return self;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
"""
|
|
697
|
+
Handle execution of a POST endpoint by registering it with FastAPI.
|
|
698
|
+
Args:
|
|
699
|
+
endpoint (JEndPoint): The POST endpoint to execute
|
|
700
|
+
Returns:
|
|
701
|
+
JFastApiServer: Self for method chaining
|
|
702
|
+
"""
|
|
703
|
+
impl JFastApiServer._post(
|
|
704
|
+
self: JFastApiServer, endpoint: JEndPoint
|
|
705
|
+
) -> 'JFastApiServer' {
|
|
706
|
+
self._create_fastapi_route(HTTPMethod.POST, endpoint);
|
|
707
|
+
return self;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
"""
|
|
711
|
+
Handle execution of a GET endpoint by registering it with FastAPI.
|
|
712
|
+
Args:
|
|
713
|
+
endpoint (JEndPoint): The GET endpoint to execute
|
|
714
|
+
Returns:
|
|
715
|
+
JFastApiServer: Self for method chaining
|
|
716
|
+
"""
|
|
717
|
+
impl JFastApiServer._get(self: JFastApiServer, endpoint: JEndPoint) -> 'JFastApiServer' {
|
|
718
|
+
self._create_fastapi_route(HTTPMethod.GET, endpoint);
|
|
719
|
+
return self;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
impl JFastApiServer.init(
|
|
723
|
+
self: JFastApiServer,
|
|
724
|
+
endpoints: (list[JEndPoint] | None) = None,
|
|
725
|
+
app: (FastAPI | None) = None
|
|
726
|
+
) -> None {
|
|
727
|
+
super.init((endpoints or []));
|
|
728
|
+
self.app = app or FastAPI();
|
|
729
|
+
self._models: dict[(str, type[BaseModel])] = {};
|
|
730
|
+
self.__server_created = False;
|
|
731
|
+
}
|