from typing import ( TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Optional, Tuple, Union, ) from django.urls import URLPattern from django.urls import path as django_path from django.utils.module_loading import import_string from ninja.constants import NOT_SET, NOT_SET_TYPE from ninja.errors import ConfigError from ninja.operation import PathView from ninja.throttling import BaseThrottle from ninja.types import TCallable from ninja.utils import normalize_path, replace_path_param_notation if TYPE_CHECKING: from ninja import NinjaAPI # pragma: no cover __all__ = ["Router"] class Router: def __init__( self, *, auth: Any = NOT_SET, throttle: Union[BaseThrottle, List[BaseThrottle], NOT_SET_TYPE] = NOT_SET, tags: Optional[List[str]] = None, by_alias: Optional[bool] = None, exclude_unset: Optional[bool] = None, exclude_defaults: Optional[bool] = None, exclude_none: Optional[bool] = None, ) -> None: self.api: Optional[NinjaAPI] = None self.auth = auth self.throttle = throttle self.tags = tags self.by_alias = by_alias self.exclude_unset = exclude_unset self.exclude_defaults = exclude_defaults self.exclude_none = exclude_none self.path_operations: Dict[str, PathView] = {} self._routers: List[Tuple[str, Router]] = [] def get( self, path: str, *, auth: Any = NOT_SET, throttle: Union[BaseThrottle, List[BaseThrottle], NOT_SET_TYPE] = NOT_SET, response: Any = NOT_SET, operation_id: Optional[str] = None, summary: Optional[str] = None, description: Optional[str] = None, tags: Optional[List[str]] = None, deprecated: Optional[bool] = None, by_alias: Optional[bool] = None, exclude_unset: Optional[bool] = None, exclude_defaults: Optional[bool] = None, exclude_none: Optional[bool] = None, url_name: Optional[str] = None, include_in_schema: bool = True, openapi_extra: Optional[Dict[str, Any]] = None, ) -> Callable[[TCallable], TCallable]: return self.api_operation( ["GET"], path, auth=auth, throttle=throttle, response=response, operation_id=operation_id, summary=summary, description=description, tags=tags, deprecated=deprecated, by_alias=by_alias, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, exclude_none=exclude_none, url_name=url_name, include_in_schema=include_in_schema, openapi_extra=openapi_extra, ) def post( self, path: str, *, auth: Any = NOT_SET, throttle: Union[BaseThrottle, List[BaseThrottle], NOT_SET_TYPE] = NOT_SET, response: Any = NOT_SET, operation_id: Optional[str] = None, summary: Optional[str] = None, description: Optional[str] = None, tags: Optional[List[str]] = None, deprecated: Optional[bool] = None, by_alias: Optional[bool] = None, exclude_unset: Optional[bool] = None, exclude_defaults: Optional[bool] = None, exclude_none: Optional[bool] = None, url_name: Optional[str] = None, include_in_schema: bool = True, openapi_extra: Optional[Dict[str, Any]] = None, ) -> Callable[[TCallable], TCallable]: return self.api_operation( ["POST"], path, auth=auth, throttle=throttle, response=response, operation_id=operation_id, summary=summary, description=description, tags=tags, deprecated=deprecated, by_alias=by_alias, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, exclude_none=exclude_none, url_name=url_name, include_in_schema=include_in_schema, openapi_extra=openapi_extra, ) def delete( self, path: str, *, auth: Any = NOT_SET, throttle: Union[BaseThrottle, List[BaseThrottle], NOT_SET_TYPE] = NOT_SET, response: Any = NOT_SET, operation_id: Optional[str] = None, summary: Optional[str] = None, description: Optional[str] = None, tags: Optional[List[str]] = None, deprecated: Optional[bool] = None, by_alias: Optional[bool] = None, exclude_unset: Optional[bool] = None, exclude_defaults: Optional[bool] = None, exclude_none: Optional[bool] = None, url_name: Optional[str] = None, include_in_schema: bool = True, openapi_extra: Optional[Dict[str, Any]] = None, ) -> Callable[[TCallable], TCallable]: return self.api_operation( ["DELETE"], path, auth=auth, throttle=throttle, response=response, operation_id=operation_id, summary=summary, description=description, tags=tags, deprecated=deprecated, by_alias=by_alias, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, exclude_none=exclude_none, url_name=url_name, include_in_schema=include_in_schema, openapi_extra=openapi_extra, ) def patch( self, path: str, *, auth: Any = NOT_SET, throttle: Union[BaseThrottle, List[BaseThrottle], NOT_SET_TYPE] = NOT_SET, response: Any = NOT_SET, operation_id: Optional[str] = None, summary: Optional[str] = None, description: Optional[str] = None, tags: Optional[List[str]] = None, deprecated: Optional[bool] = None, by_alias: Optional[bool] = None, exclude_unset: Optional[bool] = None, exclude_defaults: Optional[bool] = None, exclude_none: Optional[bool] = None, url_name: Optional[str] = None, include_in_schema: bool = True, openapi_extra: Optional[Dict[str, Any]] = None, ) -> Callable[[TCallable], TCallable]: return self.api_operation( ["PATCH"], path, auth=auth, throttle=throttle, response=response, operation_id=operation_id, summary=summary, description=description, tags=tags, deprecated=deprecated, by_alias=by_alias, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, exclude_none=exclude_none, url_name=url_name, include_in_schema=include_in_schema, openapi_extra=openapi_extra, ) def put( self, path: str, *, auth: Any = NOT_SET, throttle: Union[BaseThrottle, List[BaseThrottle], NOT_SET_TYPE] = NOT_SET, response: Any = NOT_SET, operation_id: Optional[str] = None, summary: Optional[str] = None, description: Optional[str] = None, tags: Optional[List[str]] = None, deprecated: Optional[bool] = None, by_alias: Optional[bool] = None, exclude_unset: Optional[bool] = None, exclude_defaults: Optional[bool] = None, exclude_none: Optional[bool] = None, url_name: Optional[str] = None, include_in_schema: bool = True, openapi_extra: Optional[Dict[str, Any]] = None, ) -> Callable[[TCallable], TCallable]: return self.api_operation( ["PUT"], path, auth=auth, throttle=throttle, response=response, operation_id=operation_id, summary=summary, description=description, tags=tags, deprecated=deprecated, by_alias=by_alias, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, exclude_none=exclude_none, url_name=url_name, include_in_schema=include_in_schema, openapi_extra=openapi_extra, ) def api_operation( self, methods: List[str], path: str, *, auth: Any = NOT_SET, throttle: Union[BaseThrottle, List[BaseThrottle], NOT_SET_TYPE] = NOT_SET, response: Any = NOT_SET, operation_id: Optional[str] = None, summary: Optional[str] = None, description: Optional[str] = None, tags: Optional[List[str]] = None, deprecated: Optional[bool] = None, by_alias: Optional[bool] = None, exclude_unset: Optional[bool] = None, exclude_defaults: Optional[bool] = None, exclude_none: Optional[bool] = None, url_name: Optional[str] = None, include_in_schema: bool = True, openapi_extra: Optional[Dict[str, Any]] = None, ) -> Callable[[TCallable], TCallable]: def decorator(view_func: TCallable) -> TCallable: self.add_api_operation( path, methods, view_func, auth=auth, throttle=throttle, response=response, operation_id=operation_id, summary=summary, description=description, tags=tags, deprecated=deprecated, by_alias=by_alias, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, exclude_none=exclude_none, url_name=url_name, include_in_schema=include_in_schema, openapi_extra=openapi_extra, ) return view_func return decorator def add_api_operation( self, path: str, methods: List[str], view_func: Callable, *, auth: Any = NOT_SET, throttle: Union[BaseThrottle, List[BaseThrottle], NOT_SET_TYPE] = NOT_SET, response: Any = NOT_SET, operation_id: Optional[str] = None, summary: Optional[str] = None, description: Optional[str] = None, tags: Optional[List[str]] = None, deprecated: Optional[bool] = None, by_alias: Optional[bool] = None, exclude_unset: Optional[bool] = None, exclude_defaults: Optional[bool] = None, exclude_none: Optional[bool] = None, url_name: Optional[str] = None, include_in_schema: bool = True, openapi_extra: Optional[Dict[str, Any]] = None, ) -> None: if path not in self.path_operations: path_view = PathView() self.path_operations[path] = path_view else: path_view = self.path_operations[path] by_alias = by_alias is None and self.by_alias or by_alias exclude_unset = exclude_unset is None and self.exclude_unset or exclude_unset exclude_defaults = ( exclude_defaults is None and self.exclude_defaults or exclude_defaults ) exclude_none = exclude_none is None and self.exclude_none or exclude_none path_view.add_operation( path=path, methods=methods, view_func=view_func, auth=auth, throttle=throttle, response=response, operation_id=operation_id, summary=summary, description=description, tags=tags, deprecated=deprecated, by_alias=by_alias, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, exclude_none=exclude_none, url_name=url_name, include_in_schema=include_in_schema, openapi_extra=openapi_extra, ) if self.api: path_view.set_api_instance(self.api, self) return None def set_api_instance( self, api: "NinjaAPI", parent_router: Optional["Router"] = None ) -> None: if self.auth is NOT_SET and parent_router: self.auth = parent_router.auth self.api = api for path_view in self.path_operations.values(): path_view.set_api_instance(self.api, self) for _, router in self._routers: router.set_api_instance(api, self) def urls_paths(self, prefix: str) -> Iterator[URLPattern]: prefix = replace_path_param_notation(prefix) for path, path_view in self.path_operations.items(): for operation in path_view.operations: path = replace_path_param_notation(path) route = "/".join([i for i in (prefix, path) if i]) # to skip lot of checks we simply treat double slash as a mistake: route = normalize_path(route) route = route.lstrip("/") url_name = getattr(operation, "url_name", "") if not url_name and self.api: url_name = self.api.get_operation_url_name(operation, router=self) yield django_path(route, path_view.get_view(), name=url_name) def add_router( self, prefix: str, router: Union["Router", str], *, auth: Any = NOT_SET, throttle: Union[BaseThrottle, List[BaseThrottle], NOT_SET_TYPE] = NOT_SET, tags: Optional[List[str]] = None, ) -> None: if isinstance(router, str): router = import_string(router) assert isinstance(router, Router) if self.api: # we are already attached to an api self.api.add_router( prefix=prefix, router=router, auth=auth, throttle=throttle, tags=tags, parent_router=self, ) else: # we are not attached to an api if auth != NOT_SET: router.auth = auth # TODO: throttle if tags is not None: router.tags = tags self._routers.append((prefix, router)) def build_routers(self, prefix: str) -> List[Tuple[str, "Router"]]: if self.api is not None: from ninja.main import debug_server_url_reimport if not debug_server_url_reimport(): raise ConfigError( f"Router@'{prefix}' has already been attached to API" f" {self.api.title}:{self.api.version} " ) internal_routes = [] for inter_prefix, inter_router in self._routers: _route = normalize_path("/".join((prefix, inter_prefix))).lstrip("/") internal_routes.extend(inter_router.build_routers(_route)) return [(prefix, self), *internal_routes]