swadhinbiswas

[email protected]
2026-07-25 15:37:12 +0600 committed NaN years ago
7c023f8
commit 7c023f8
2 files changed
+120 -36
▼crates/justapi-py/python/justapi/app.py
+118-36
517
517
allow_methods = options.get("allow_methods", ["*"])
518
518
allow_headers = options.get("allow_headers", ["*"])
519
519
allow_credentials = options.get("allow_credentials", False)
520
+ expose_headers = options.get("expose_headers")
521
+ max_age = options.get("max_age")
520
522
self.add_cors(
521
523
allow_origins=allow_origins,
522
524
allow_methods=allow_methods,
523
525
allow_headers=allow_headers,
524
526
allow_credentials=allow_credentials,
527
+ expose_headers=expose_headers,
528
+ max_age=max_age,
525
529
)
526
530
else:
527
531
try:
536
540
allow_methods=None,
537
541
allow_headers=None,
538
542
allow_credentials=False,
543
+ expose_headers=None,
544
+ max_age=None,
539
545
):
540
- """Configure CORS origins, methods, and headers for the application."""
541
- origins = allow_origins or ["*"]
542
- methods = allow_methods or ["*"]
543
- headers = allow_headers or ["*"]
544
-
545
- def cors_middleware(request, call_next):
546
- response = call_next(request)
547
- if isinstance(response, dict):
548
- response = JSONResponse(response)
549
- if hasattr(response, "headers"):
550
- response.headers["Access-Control-Allow-Origin"] = (
551
- origins[0] if len(origins) == 1 else "*"
552
- )
553
- response.headers["Access-Control-Allow-Methods"] = ", ".join(methods)
554
- response.headers["Access-Control-Allow-Headers"] = ", ".join(headers)
555
- if allow_credentials:
556
- response.headers["Access-Control-Allow-Credentials"] = "true"
557
- return response
558
-
559
- self.middlewares.append(cors_middleware)
546
+ """Configure CORS via the Rust middleware chain.
547
+
548
+ Handles both OPTIONS preflight (returning 204) and per-response
549
+ ``Access-Control-*`` header injection. This is a bridge to the
550
+ Rust ``Cors`` middleware (not a Python-level middleware), so it
551
+ covers ALL routes including Rust-native fast-path and 404s.
552
+
553
+ Args:
554
+ allow_origins: List of allowed origins, or ``["*"]`` for all.
555
+ allow_methods: List of allowed HTTP methods.
556
+ allow_headers: List of allowed request headers.
557
+ allow_credentials: When True, ``Access-Control-Allow-Credentials``
558
+ is set and a concrete origin is echoed instead of ``*``.
559
+ expose_headers: List of headers the browser is allowed to access.
560
+ max_age: ``Access-Control-Max-Age`` in seconds (int).
561
+ """
562
+ self._app._set_cors(
563
+ allow_origins or ["*"],
564
+ allow_methods,
565
+ allow_headers,
566
+ allow_credentials,
567
+ expose_headers,
568
+ max_age,
569
+ )
560
570
561
571
562
572
def add_exception_handler(self, exc_class, handler):
572
582
async def _resolve_dependencies_list(self, deps: typing.List[Depends], request: dict):
573
583
if "_dep_cache" not in request:
574
584
request["_dep_cache"] = {}
585
+ if "_dep_cleanup" not in request:
586
+ request["_dep_cleanup"] = []
575
587
576
588
for dep in deps:
577
589
dep_sig = inspect.signature(dep.dependency)
580
592
if dep.use_cache and dep.dependency in request["_dep_cache"]:
581
593
continue
582
594
583
- if inspect.iscoroutinefunction(dep.dependency):
595
+ is_gen = inspect.isasyncgenfunction(dep.dependency)
596
+ is_sync_gen = inspect.isgeneratorfunction(dep.dependency)
597
+
598
+ if is_gen:
599
+ gen = dep.dependency(**dep_kwargs)
600
+ res = await gen.__anext__()
601
+ request["_dep_cleanup"].append(gen.aclose)
602
+ elif is_sync_gen:
603
+ gen = dep.dependency(**dep_kwargs)
604
+ res = next(gen)
605
+ request["_dep_cleanup"].append(gen.close)
606
+ elif inspect.iscoroutinefunction(dep.dependency):
584
607
res = await dep.dependency(**dep_kwargs)
585
608
else:
586
609
res = dep.dependency(**dep_kwargs)
598
621
599
622
if "_dep_cache" not in request:
600
623
request["_dep_cache"] = {}
624
+ if "_dep_cleanup" not in request:
625
+ request["_dep_cleanup"] = []
601
626
602
627
kwargs = {}
603
628
for name, param in sig.parameters.items():
608
633
609
634
dep_sig = inspect.signature(param.default.dependency)
610
635
dep_kwargs = await self._resolve_kwargs(dep_sig, request)
611
- if inspect.iscoroutinefunction(param.default.dependency):
636
+
637
+ is_gen = inspect.isasyncgenfunction(param.default.dependency)
638
+ is_sync_gen = inspect.isgeneratorfunction(param.default.dependency)
639
+
640
+ if is_gen:
641
+ gen = param.default.dependency(**dep_kwargs)
642
+ res = await gen.__anext__()
643
+ request["_dep_cleanup"].append(gen.aclose)
644
+ elif is_sync_gen:
645
+ gen = param.default.dependency(**dep_kwargs)
646
+ res = next(gen)
647
+ request["_dep_cleanup"].append(gen.close)
648
+ elif inspect.iscoroutinefunction(param.default.dependency):
612
649
res = await param.default.dependency(**dep_kwargs)
613
650
else:
614
651
res = param.default.dependency(**dep_kwargs)
698
735
def _resolve_dependencies_list_sync(self, deps: typing.List[Depends], request: dict):
699
736
if "_dep_cache" not in request:
700
737
request["_dep_cache"] = {}
738
+ if "_dep_cleanup" not in request:
739
+ request["_dep_cleanup"] = []
701
740
702
741
for dep in deps:
703
742
dep_sig = inspect.signature(dep.dependency)
706
745
if dep.use_cache and dep.dependency in request["_dep_cache"]:
707
746
continue
708
747
709
- res = dep.dependency(**dep_kwargs)
748
+ is_sync_gen = inspect.isgeneratorfunction(dep.dependency)
749
+
750
+ if is_sync_gen:
751
+ gen = dep.dependency(**dep_kwargs)
752
+ res = next(gen)
753
+ request["_dep_cleanup"].append(gen.close)
754
+ else:
755
+ res = dep.dependency(**dep_kwargs)
756
+
710
757
if dep.use_cache:
711
758
request["_dep_cache"][dep.dependency] = res
712
759
719
766
720
767
if "_dep_cache" not in request:
721
768
request["_dep_cache"] = {}
769
+ if "_dep_cleanup" not in request:
770
+ request["_dep_cleanup"] = []
722
771
723
772
kwargs = {}
724
773
for name, param in sig.parameters.items():
729
778
730
779
dep_sig = inspect.signature(param.default.dependency)
731
780
dep_kwargs = self._resolve_kwargs_sync(dep_sig, request)
732
- res = param.default.dependency(**dep_kwargs)
781
+
782
+ is_sync_gen = inspect.isgeneratorfunction(param.default.dependency)
783
+
784
+ if is_sync_gen:
785
+ gen = param.default.dependency(**dep_kwargs)
786
+ res = next(gen)
787
+ request["_dep_cleanup"].append(gen.close)
788
+ else:
789
+ res = param.default.dependency(**dep_kwargs)
733
790
734
791
if param.default.use_cache:
735
792
request["_dep_cache"][param.default.dependency] = res
883
940
884
941
if is_async:
885
942
async def async_wrapper(request):
886
- try:
887
- bg_tasks = None
888
- if has_bg:
889
- from .background import BackgroundTasks
890
- bg_tasks = BackgroundTasks()
891
- request["background_tasks"] = bg_tasks
943
+ bg_tasks = None
944
+ if has_bg:
945
+ from .background import BackgroundTasks
946
+ bg_tasks = BackgroundTasks()
947
+ request["background_tasks"] = bg_tasks
892
948
949
+ try:
893
950
await self._resolve_dependencies_list(all_deps, request)
894
951
kwargs = await self._resolve_kwargs(sig, request)
895
952
902
959
return result
903
960
except Exception as e:
904
961
return self._handle_exception(request, e)
962
+ finally:
963
+ for c in reversed(request.get("_dep_cleanup", [])):
964
+ try:
965
+ await c()
966
+ except Exception:
967
+ pass
905
968
wrapper = self._apply_middlewares(async_wrapper, route_middlewares)
906
969
else:
907
970
def sync_wrapper(request):
908
- try:
909
- bg_tasks = None
910
- if has_bg:
911
- from .background import BackgroundTasks
912
- bg_tasks = BackgroundTasks()
913
- request["background_tasks"] = bg_tasks
971
+ bg_tasks = None
972
+ if has_bg:
973
+ from .background import BackgroundTasks
974
+ bg_tasks = BackgroundTasks()
975
+ request["background_tasks"] = bg_tasks
914
976
977
+ try:
915
978
self._resolve_dependencies_list_sync(all_deps, request)
916
979
kwargs = self._resolve_kwargs_sync(sig, request)
917
980
result = handler(**kwargs)
919
982
return result
920
983
except Exception as e:
921
984
return self._handle_exception(request, e)
985
+ finally:
986
+ for c in reversed(request.get("_dep_cleanup", [])):
987
+ try:
988
+ c()
989
+ except Exception:
990
+ pass
922
991
923
992
if self.middlewares or route_middlewares:
924
993
import asyncio
1326
1395
"""
1327
1396
self._app.enable_secure_headers(with_hsts)
1328
1397
1398
+ def set_jwt_auth(self, secret: str, algorithm: str = "HS256"):
1399
+ """
1400
+ Enable Rust-native JWT authentication middleware.
1401
+
1402
+ Validates every incoming request using the Rust JwtAuth middleware.
1403
+ Decoded claims are automatically injected into ``request["auth"]``.
1404
+
1405
+ Args:
1406
+ secret: The HMAC shared secret.
1407
+ algorithm: Signing algorithm (default ``"HS256"``).
1408
+ """
1409
+ self._app._set_jwt_auth(secret, algorithm)
1410
+
1329
1411
def register_health_check(self, name: str, check: typing.Callable[[], bool]):
1330
1412
"""
1331
1413
Register a dependency readiness probe used by the ``/ready`` endpoint.
1332
1414
▼crates/justapi-py/python/justapi/app.pyi
+2-0
125
125
middlewares: Incomplete
126
126
def __init__(self, dependencies: list[Depends] = None) -> None: ...
127
127
def middleware(self, middleware_type: str = 'http'): ...
128
+ def add_cors(self, allow_origins: list[str] = None, allow_methods: list[str] = None, allow_headers: list[str] = None, allow_credentials: bool = False, expose_headers: list[str] = None, max_age: int = None) -> None: ...
129
+ def set_jwt_auth(self, secret: str, algorithm: str = "HS256") -> None: ...
128
130
def add_exception_handler(self, exc_class, handler) -> None: ...
129
131
def get(self, path: str, handler=None, dependencies: list[Depends] = None, middlewares: list[typing.Callable] = None, tags: list[str] = None, summary: str = None, description: str = None, deprecated: bool = False, status_code: int = None, responses: dict = None, operation_id: str = None, openapi_extra: dict = None, include_in_schema: bool = True, name: str = None): ...
130
132
def post(self, path: str, handler=None, body_schema=None, schema=None, dependencies: list[Depends] = None, middlewares: list[typing.Callable] = None, tags: list[str] = None, summary: str = None, description: str = None, deprecated: bool = False, status_code: int = None, responses: dict = None, operation_id: str = None, openapi_extra: dict = None, include_in_schema: bool = True, name: str = None): ...
131
133