create-caspian-app 0.0.28 → 0.0.30
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.
- package/dist/main.py +121 -11
- package/dist/public/js/pp-reactive-v1.js +1 -1
- package/package.json +1 -1
package/dist/main.py
CHANGED
|
@@ -25,9 +25,17 @@ from casp.auth import (
|
|
|
25
25
|
configure_auth,
|
|
26
26
|
)
|
|
27
27
|
from casp.rpc import register_rpc_routes
|
|
28
|
-
from casp.layout import
|
|
28
|
+
from casp.layout import (
|
|
29
|
+
render_with_nested_layouts,
|
|
30
|
+
string_env,
|
|
31
|
+
load_template_file,
|
|
32
|
+
render_page,
|
|
33
|
+
_runtime_injections,
|
|
34
|
+
_runtime_metadata,
|
|
35
|
+
)
|
|
29
36
|
import hashlib
|
|
30
37
|
from casp.streaming import SSE
|
|
38
|
+
from typing import Any, Optional, get_args, get_origin, Union
|
|
31
39
|
|
|
32
40
|
load_dotenv()
|
|
33
41
|
cfg = get_config()
|
|
@@ -192,6 +200,7 @@ class AuthMiddleware:
|
|
|
192
200
|
Auth.set_request(request)
|
|
193
201
|
auth_inst = Auth.get_instance()
|
|
194
202
|
providers = Auth.get_providers()
|
|
203
|
+
|
|
195
204
|
if providers:
|
|
196
205
|
oauth_response = auth_inst.auth_providers(*providers)
|
|
197
206
|
if oauth_response:
|
|
@@ -202,10 +211,14 @@ class AuthMiddleware:
|
|
|
202
211
|
return
|
|
203
212
|
if auth_inst.is_auth_route(path):
|
|
204
213
|
if auth_inst.is_authenticated():
|
|
205
|
-
await RedirectResponse(
|
|
214
|
+
await RedirectResponse(
|
|
215
|
+
url=auth_inst.settings.default_signin_redirect,
|
|
216
|
+
status_code=303
|
|
217
|
+
)(scope, receive, send)
|
|
206
218
|
return
|
|
207
219
|
await self.app(scope, receive, send)
|
|
208
220
|
return
|
|
221
|
+
|
|
209
222
|
if auth_inst.settings.is_role_based:
|
|
210
223
|
required_roles = auth_inst.get_required_roles(path)
|
|
211
224
|
if required_roles:
|
|
@@ -215,6 +228,7 @@ class AuthMiddleware:
|
|
|
215
228
|
if not auth_inst.check_role(auth_inst.get_payload(), required_roles):
|
|
216
229
|
await RedirectResponse(url='/unauthorized', status_code=303)(scope, receive, send)
|
|
217
230
|
return
|
|
231
|
+
|
|
218
232
|
if auth_inst.is_private_route(path):
|
|
219
233
|
if not auth_inst.is_authenticated():
|
|
220
234
|
if auth_inst.settings.on_auth_failure:
|
|
@@ -222,6 +236,7 @@ class AuthMiddleware:
|
|
|
222
236
|
return
|
|
223
237
|
await RedirectResponse(url=f'/signin?next={path}', status_code=303)(scope, receive, send)
|
|
224
238
|
return
|
|
239
|
+
|
|
225
240
|
await self.app(scope, receive, send)
|
|
226
241
|
|
|
227
242
|
|
|
@@ -260,6 +275,75 @@ def load_route_module(file_path: str):
|
|
|
260
275
|
return module
|
|
261
276
|
|
|
262
277
|
|
|
278
|
+
def _unwrap_optional(annotation: Any) -> Any:
|
|
279
|
+
"""
|
|
280
|
+
Optional[T] is Union[T, NoneType]. Return T when applicable.
|
|
281
|
+
"""
|
|
282
|
+
origin = get_origin(annotation)
|
|
283
|
+
if origin is Union:
|
|
284
|
+
args = [a for a in get_args(annotation) if a is not type(None)]
|
|
285
|
+
if len(args) == 1:
|
|
286
|
+
return args[0]
|
|
287
|
+
return annotation
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _coerce_scalar(value: Optional[str], annotation: Any) -> Any:
|
|
291
|
+
"""
|
|
292
|
+
Coerce a single query value based on annotation (best-effort).
|
|
293
|
+
If value is None -> returns None.
|
|
294
|
+
If coercion fails -> returns original string.
|
|
295
|
+
"""
|
|
296
|
+
if value is None:
|
|
297
|
+
return None
|
|
298
|
+
|
|
299
|
+
ann = _unwrap_optional(annotation)
|
|
300
|
+
|
|
301
|
+
try:
|
|
302
|
+
if ann is inspect._empty or ann is str or ann is Any:
|
|
303
|
+
return value
|
|
304
|
+
if ann is int:
|
|
305
|
+
return int(value)
|
|
306
|
+
if ann is float:
|
|
307
|
+
return float(value)
|
|
308
|
+
if ann is bool:
|
|
309
|
+
v = value.strip().lower()
|
|
310
|
+
if v in ("1", "true", "t", "yes", "y", "on"):
|
|
311
|
+
return True
|
|
312
|
+
if v in ("0", "false", "f", "no", "n", "off"):
|
|
313
|
+
return False
|
|
314
|
+
return bool(value)
|
|
315
|
+
return value
|
|
316
|
+
except Exception:
|
|
317
|
+
return value
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _coerce_query_param(request: Request, name: str, param: inspect.Parameter) -> Any:
|
|
321
|
+
"""
|
|
322
|
+
Supports:
|
|
323
|
+
- scalar types: str/int/float/bool/Optional[...]
|
|
324
|
+
- list types: list[str], list[int], etc. via ?x=a&x=b
|
|
325
|
+
- Optional[list[T]]
|
|
326
|
+
"""
|
|
327
|
+
ann = param.annotation
|
|
328
|
+
origin = get_origin(ann)
|
|
329
|
+
|
|
330
|
+
# list[T]
|
|
331
|
+
if origin is list:
|
|
332
|
+
inner = get_args(ann)[0] if get_args(ann) else str
|
|
333
|
+
values = request.query_params.getlist(name)
|
|
334
|
+
return [_coerce_scalar(v, inner) for v in values]
|
|
335
|
+
|
|
336
|
+
# Optional[list[T]] -> Union[list[T], None]
|
|
337
|
+
unwrapped = _unwrap_optional(ann)
|
|
338
|
+
if get_origin(unwrapped) is list:
|
|
339
|
+
inner = get_args(unwrapped)[0] if get_args(unwrapped) else str
|
|
340
|
+
values = request.query_params.getlist(name)
|
|
341
|
+
return [_coerce_scalar(v, inner) for v in values]
|
|
342
|
+
|
|
343
|
+
# scalar
|
|
344
|
+
return _coerce_scalar(request.query_params.get(name), ann)
|
|
345
|
+
|
|
346
|
+
|
|
263
347
|
def register_routes():
|
|
264
348
|
idx = get_files_index()
|
|
265
349
|
for route in idx.routes:
|
|
@@ -299,11 +383,23 @@ def register_single_route(url_pattern: str, file_path: str):
|
|
|
299
383
|
sig = inspect.signature(module.page)
|
|
300
384
|
call_kwargs = {}
|
|
301
385
|
call_args = []
|
|
386
|
+
|
|
302
387
|
if kwargs:
|
|
303
388
|
call_args.append(kwargs)
|
|
304
389
|
if 'request' in sig.parameters:
|
|
305
390
|
call_kwargs['request'] = request
|
|
306
391
|
|
|
392
|
+
for name, param in sig.parameters.items():
|
|
393
|
+
if name in call_kwargs:
|
|
394
|
+
continue
|
|
395
|
+
if name in ("kwargs",):
|
|
396
|
+
continue
|
|
397
|
+
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
398
|
+
continue
|
|
399
|
+
if name in request.query_params:
|
|
400
|
+
call_kwargs[name] = _coerce_query_param(
|
|
401
|
+
request, name, param)
|
|
402
|
+
|
|
307
403
|
if inspect.iscoroutinefunction(module.page):
|
|
308
404
|
result = await module.page(*call_args, **call_kwargs)
|
|
309
405
|
else:
|
|
@@ -341,6 +437,7 @@ def register_single_route(url_pattern: str, file_path: str):
|
|
|
341
437
|
if obj.extra:
|
|
342
438
|
d.update(obj.extra)
|
|
343
439
|
return d
|
|
440
|
+
|
|
344
441
|
page_metadata.update(extract_meta(static_meta))
|
|
345
442
|
page_metadata.update(extract_meta(dynamic_meta))
|
|
346
443
|
else:
|
|
@@ -399,10 +496,15 @@ async def custom_404_handler(request: Request, exc: StarletteHTTPException):
|
|
|
399
496
|
with open(not_found_path, 'r', encoding='utf-8') as f:
|
|
400
497
|
content = f.read()
|
|
401
498
|
html_output, root_layout_id = render_with_nested_layouts(
|
|
402
|
-
children=content,
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
499
|
+
children=content,
|
|
500
|
+
route_dir='src/app',
|
|
501
|
+
page_metadata={
|
|
502
|
+
'title': "Page Not Found",
|
|
503
|
+
'description': "The page you are looking for does not exist."
|
|
504
|
+
},
|
|
505
|
+
page_layout_props=None,
|
|
506
|
+
context_data={'request': request},
|
|
507
|
+
transform_fn=transform_scripts
|
|
406
508
|
)
|
|
407
509
|
resp = HTMLResponse(content=html_output, status_code=404)
|
|
408
510
|
resp.headers['X-PP-Root-Layout'] = root_layout_id
|
|
@@ -425,17 +527,25 @@ async def custom_general_exception_handler(request: Request, exc: Exception):
|
|
|
425
527
|
rendered_content = string_env.from_string(
|
|
426
528
|
raw_content).render(**context_data)
|
|
427
529
|
html_output, root_layout_id = render_with_nested_layouts(
|
|
428
|
-
children=rendered_content,
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
530
|
+
children=rendered_content,
|
|
531
|
+
route_dir='src/app',
|
|
532
|
+
page_metadata={
|
|
533
|
+
'title': 'Application Error',
|
|
534
|
+
'description': 'An unexpected error occurred.'
|
|
535
|
+
},
|
|
536
|
+
page_layout_props=None,
|
|
537
|
+
context_data=context_data,
|
|
538
|
+
transform_fn=transform_scripts
|
|
432
539
|
)
|
|
433
540
|
resp = HTMLResponse(content=html_output, status_code=500)
|
|
434
541
|
resp.headers['X-PP-Root-Layout'] = root_layout_id
|
|
435
542
|
return resp
|
|
436
543
|
except Exception as render_exc:
|
|
437
544
|
print("Error rendering error.html:", render_exc)
|
|
438
|
-
return HTMLResponse(
|
|
545
|
+
return HTMLResponse(
|
|
546
|
+
content=f"<h1>500 - Internal Server Error</h1><p>{error_message}</p>",
|
|
547
|
+
status_code=500
|
|
548
|
+
)
|
|
439
549
|
|
|
440
550
|
# ====
|
|
441
551
|
# Middleware Order (LAST added runs FIRST)
|