diff --git a/nest/common/route_resolver.py b/nest/common/route_resolver.py index 9d545c4..0fda4a1 100644 --- a/nest/common/route_resolver.py +++ b/nest/common/route_resolver.py @@ -1,8 +1,8 @@ -from fastapi import APIRouter, FastAPI +from nest.engine.proto import App, Router class RoutesResolver: - def __init__(self, container, app_ref: FastAPI): + def __init__(self, container, app_ref: App): self.container = container self.app_ref = app_ref @@ -12,5 +12,6 @@ def register_routes(self): self.register_route(controller) def register_route(self, controller): - router: APIRouter = controller.get_router() + router: Router = controller.get_router() self.app_ref.include_router(router) + diff --git a/nest/core/cli_factory.py b/nest/core/cli_factory.py index 99284be..e0338ee 100644 --- a/nest/core/cli_factory.py +++ b/nest/core/cli_factory.py @@ -3,10 +3,10 @@ import click from nest.core.pynest_container import PyNestContainer -from nest.core.pynest_factory import AbstractPyNestFactory, ModuleType +from nest.core.pynest_factory import PyNestFactory, ModuleType -class CLIAppFactory(AbstractPyNestFactory): +class CLIAppFactory(PyNestFactory): def __init__(self): super().__init__() diff --git a/nest/core/decorators/class_based_view.py b/nest/core/decorators/class_based_view.py index 18cf0b8..aadf378 100644 --- a/nest/core/decorators/class_based_view.py +++ b/nest/core/decorators/class_based_view.py @@ -11,13 +11,14 @@ List, Type, TypeVar, - Union, get_origin, get_type_hints, ) -from fastapi import APIRouter, Depends -from starlette.routing import Route, WebSocketRoute + +from nest.engine.proto import Router, RouteProtocol, Route +from fastapi import Depends # TODO: remove in future release + T = TypeVar("T") K = TypeVar("K", bound=Callable[..., Any]) @@ -25,19 +26,19 @@ CBV_CLASS_KEY = "__cbv_class__" -def class_based_view(router: APIRouter, cls: Type[T]) -> Type[T]: +def class_based_view(router: Router, cls: Type[T]) -> Type[T]: """ Replaces any methods of the provided class `cls` that are endpoints of routes in `router` with updated function calls that will properly inject an instance of `cls`. """ _init_cbv(cls) - cbv_router = APIRouter() + cbv_router = router.__class__() function_members = inspect.getmembers(cls, inspect.isfunction) functions_set = set(func for _, func in function_members) cbv_routes = [ route for route in router.routes - if isinstance(route, (Route, WebSocketRoute)) + if isinstance(route, RouteProtocol) and route.endpoint in functions_set ] for route in cbv_routes: @@ -95,7 +96,7 @@ def new_init(self: Any, *args: Any, **kwargs: Any) -> None: def _update_cbv_route_endpoint_signature( - cls: Type[Any], route: Union[Route, WebSocketRoute] + cls: Type[Any], route: Route ) -> None: """ Fixes the endpoint signature for a cbv route to ensure FastAPI performs dependency injection properly. @@ -110,4 +111,4 @@ def _update_cbv_route_endpoint_signature( for parameter in old_parameters[1:] ] new_signature = old_signature.replace(parameters=new_parameters) - setattr(route.endpoint, "__signature__", new_signature) \ No newline at end of file + setattr(route.endpoint, "__signature__", new_signature) diff --git a/nest/core/decorators/controller.py b/nest/core/decorators/controller.py index 8073083..edb2da0 100644 --- a/nest/core/decorators/controller.py +++ b/nest/core/decorators/controller.py @@ -1,7 +1,7 @@ from typing import Optional, Type -from fastapi.routing import APIRouter - +from nest.engine.proto import Router +from nest.engine.fastapi import FastAPIRouter from nest.core.decorators.class_based_view import class_based_view as ClassBasedView from nest.core.decorators.http_method import HTTPMethod from nest.core.decorators.utils import get_instance_variables, parse_dependencies @@ -24,7 +24,9 @@ def Controller(prefix: Optional[str] = None, tag: Optional[str] = None): route_prefix = process_prefix(prefix, tag) def wrapper(cls: Type) -> Type[ClassBasedView]: - router = APIRouter(tags=[tag] if tag else None) + tags = [tag] if tag else None + # TODO: replace with factory + router: Router = FastAPIRouter(tags=tags) # Process class dependencies process_dependencies(cls) @@ -86,7 +88,7 @@ def ensure_init_method(cls: Type) -> None: pass -def add_routes(cls: Type, router: APIRouter, route_prefix: str) -> None: +def add_routes(cls: Type, router: Router, route_prefix: str) -> None: """Add routes from class methods to the router.""" for method_name, method_function in cls.__dict__.items(): if callable(method_function) and hasattr(method_function, "__http_method__"): @@ -127,7 +129,7 @@ def configure_method_route(method_function: callable, route_prefix: str) -> None method_function.__route_path__ = method_function.__route_path__.rstrip("/") -def add_route_to_router(router: APIRouter, method_function: callable) -> None: +def add_route_to_router(router: Router, method_function: callable) -> None: """Add the configured route to the router.""" route_kwargs = { "path": method_function.__route_path__, diff --git a/nest/core/pynest_application.py b/nest/core/pynest_application.py index 22e2669..ae70ee4 100644 --- a/nest/core/pynest_application.py +++ b/nest/core/pynest_application.py @@ -1,10 +1,8 @@ from typing import Any - -from fastapi import FastAPI - from nest.common.route_resolver import RoutesResolver from nest.core.pynest_app_context import PyNestApplicationContext from nest.core.pynest_container import PyNestContainer +from nest.engine.proto import App class PyNestApp(PyNestApplicationContext): @@ -19,13 +17,13 @@ class PyNestApp(PyNestApplicationContext): def is_listening(self) -> bool: return self._is_listening - def __init__(self, container: PyNestContainer, http_server: FastAPI): + def __init__(self, container: PyNestContainer, http_server: App): """ Initialize the PyNestApp with the given container and HTTP server. Args: container (PyNestContainer): The PyNestContainer container instance. - http_server (FastAPI): The FastAPI server instance. + http_server (App): The App server instance. """ self.container = container self.http_server = http_server @@ -48,12 +46,12 @@ def use(self, middleware: type, **options: Any) -> "PyNestApp": self.http_server.add_middleware(middleware, **options) return self - def get_server(self) -> FastAPI: + def get_server(self) -> App: """ - Get the FastAPI server instance. + Get the App server instance. Returns: - FastAPI: The FastAPI server instance. + App: The App server instance. """ return self.http_server diff --git a/nest/core/pynest_factory.py b/nest/core/pynest_factory.py index 2d988ae..1271edf 100644 --- a/nest/core/pynest_factory.py +++ b/nest/core/pynest_factory.py @@ -1,25 +1,47 @@ -from abc import ABC, abstractmethod -from typing import Type, TypeVar - -from fastapi import FastAPI +from typing import Type, TypeVar, overload, Union, Optional from nest.core.pynest_application import PyNestApp from nest.core.pynest_container import PyNestContainer +from nest.engine.proto import App +from nest.engine.fastapi import FastAPIApp + ModuleType = TypeVar("ModuleType") -class AbstractPyNestFactory(ABC): - @abstractmethod - def create(self, main_module: Type[ModuleType], **kwargs): - raise NotImplementedError +class PyNestFactory: + """Factory class for creating PyNest applications.""" + @staticmethod + @overload + def create( + main_module: Type[ModuleType], + app_cls: Type[FastAPIApp], + title: str = "", + description: str = "", + version: Optional[Union[str, int, float]] = None, + debug: bool = False, + ) -> PyNestApp: + """ + Create a PyNest application of FastAPIApp kind. + """ -class PyNestFactory(AbstractPyNestFactory): - """Factory class for creating PyNest applications.""" + @staticmethod + @overload + def create( + main_module: Type[ModuleType], + app_cls: Type[App], + ) -> PyNestApp: + """ + Create a PyNest application of FastAPIApp kind. + """ @staticmethod - def create(main_module: Type[ModuleType], **kwargs) -> PyNestApp: + def create( + main_module: Type[ModuleType], + app_cls: Type[App] = FastAPIApp, + **kwargs + ) -> PyNestApp: """ Create a PyNest application with the specified main module class. @@ -32,11 +54,11 @@ def create(main_module: Type[ModuleType], **kwargs) -> PyNestApp: """ container = PyNestContainer() container.add_module(main_module) - http_server = PyNestFactory._create_server(**kwargs) + http_server = PyNestFactory._create_server(app_cls, **kwargs) return PyNestApp(container, http_server) @staticmethod - def _create_server(**kwargs) -> FastAPI: + def _create_server(app_cls: Type[App], **kwargs) -> App: """ Create a FastAPI server. @@ -46,4 +68,4 @@ def _create_server(**kwargs) -> FastAPI: Returns: FastAPI: The created FastAPI server. """ - return FastAPI(**kwargs) + return app_cls(**kwargs) diff --git a/nest/engine/fastapi/__init__.py b/nest/engine/fastapi/__init__.py new file mode 100644 index 0000000..08d4cb9 --- /dev/null +++ b/nest/engine/fastapi/__init__.py @@ -0,0 +1,2 @@ +from fastapi import APIRouter as FastAPIRouter +from fastapi import FastAPI as FastAPIApp \ No newline at end of file diff --git a/nest/engine/proto/__init__.py b/nest/engine/proto/__init__.py new file mode 100644 index 0000000..8c39bb3 --- /dev/null +++ b/nest/engine/proto/__init__.py @@ -0,0 +1,3 @@ +from .app import App +from .router import Router +from .route import Route, RouteProtocol \ No newline at end of file diff --git a/nest/engine/proto/app.py b/nest/engine/proto/app.py new file mode 100644 index 0000000..4a1929e --- /dev/null +++ b/nest/engine/proto/app.py @@ -0,0 +1,20 @@ +from typing import Protocol, TypeVar, List, Optional, Iterable + +from nest.engine.proto.router import Router +from nest.engine.proto.route import Route +from nest.engine.types import Endpoint + +Middleware = TypeVar('Middleware') + + +class AppProtocol(Protocol): + + def __init__(self, **kwargs) -> None: ... + def add_middleware(self, middleware: Middleware, **options) -> None: ... + def include_router(self, router: Router, *, prefix: str = "", tags: Optional[List[str]] = None, **options) -> None: ... + def add_api_route(self, *, path: str, endpoint: Endpoint, methods: Optional[Iterable[str]]) -> None: ... + @property + def routes(self) -> List[Route]: ... + + +App = TypeVar('App', bound=AppProtocol) diff --git a/nest/engine/proto/route.py b/nest/engine/proto/route.py new file mode 100644 index 0000000..d586b74 --- /dev/null +++ b/nest/engine/proto/route.py @@ -0,0 +1,12 @@ +from typing import Protocol, TypeVar, runtime_checkable + +from nest.engine.types import Endpoint + + +@runtime_checkable +class RouteProtocol(Protocol): + endpoint: Endpoint + """Function representing the route endpoint logic.""" + + +Route = TypeVar('Route', bound=RouteProtocol) diff --git a/nest/engine/proto/router.py b/nest/engine/proto/router.py new file mode 100644 index 0000000..07147ec --- /dev/null +++ b/nest/engine/proto/router.py @@ -0,0 +1,14 @@ +from typing import Protocol, List, Optional, TypeVar, Iterable, runtime_checkable +from .route import Route +from ..types import Endpoint + + +@runtime_checkable +class RouterProtocol(Protocol): + routes: List[Route] + def __init__(self, tags: Optional[List[str]] = None, **kwargs) -> None: ... + def include_router(self, router: 'RouterProtocol', **kwargs) -> None: ... + def add_api_route(self, *, path: str, endpoint: Endpoint, methods: Optional[Iterable[str]]) -> None: ... + + +Router = TypeVar('Router', bound=RouterProtocol) diff --git a/nest/engine/types.py b/nest/engine/types.py new file mode 100644 index 0000000..ffde65e --- /dev/null +++ b/nest/engine/types.py @@ -0,0 +1,4 @@ +from typing import Callable, Any +from typing_extensions import TypeAlias + +Endpoint: TypeAlias = Callable[..., Any] diff --git a/tests/test_core/__init__.py b/tests/test_core/__init__.py index f5303b4..e0d78e1 100644 --- a/tests/test_core/__init__.py +++ b/tests/test_core/__init__.py @@ -46,6 +46,7 @@ def test_module(): @pytest.fixture def test_server() -> FastAPI: server = PyNestFactory._create_server( + app_cls=FastAPI, title="Test Server", description="This is a test server", version="1.0.0",