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 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 render_with_nested_layouts, string_env, load_template_file, render_page, _runtime_injections, _runtime_metadata
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(url=auth_inst.settings.default_signin_redirect, status_code=303)(scope, receive, send)
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, route_dir='src/app',
403
- page_metadata={'title': "Page Not Found",
404
- 'description': "The page you are looking for does not exist."},
405
- page_layout_props=None, context_data={'request': request}, transform_fn=transform_scripts
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, route_dir='src/app',
429
- page_metadata={'title': 'Application Error',
430
- 'description': 'An unexpected error occurred.'},
431
- page_layout_props=None, context_data=context_data, transform_fn=transform_scripts
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(content=f"<h1>500 - Internal Server Error</h1><p>{error_message}</p>", status_code=500)
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)