7c023f8
commit 7c023f8
2 files changed
+120 -36
Showing 2 changed files+120-36
▼crates/justapi-py/python/justapi/app.py
+118-36
| @@ -517,11 +517,15 @@ class JustAPIApp: | ||
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,27 +540,33 @@ class JustAPIApp: | ||
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,6 +582,8 @@ class JustAPIApp: | ||
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,7 +592,18 @@ class JustAPIApp: | ||
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,6 +621,8 @@ class JustAPIApp: | ||
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,7 +633,19 @@ class JustAPIApp: | ||
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,6 +735,8 @@ class JustAPIApp: | ||
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,7 +745,15 @@ class JustAPIApp: | ||
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,6 +766,8 @@ class JustAPIApp: | ||
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,7 +778,15 @@ class JustAPIApp: | ||
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,13 +940,13 @@ class JustAPIApp: | ||
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,16 +959,22 @@ class JustAPIApp: | ||
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,6 +982,12 @@ class JustAPIApp: | ||
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,6 +1395,19 @@ class JustAPIApp: | ||
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,6 +125,8 @@ class JustAPIApp: | ||
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 | |