Skip to content

feat(core): build protocol layer to make pynest framework agnostic #98

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions examples/BlankApp/main.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import uvicorn
from examples.BlankApp.src.app_module import app

if __name__ == "__main__":
uvicorn.run("src.app_module:http_server", host="0.0.0.0", port=8010, reload=True)
app.adapter.run()
2 changes: 0 additions & 2 deletions examples/BlankApp/src/app_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,3 @@ class AppModule:
version="1.0.0",
debug=True,
)

http_server: FastAPI = app.get_server()
63 changes: 54 additions & 9 deletions examples/BlankApp/src/user/user_controller.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,63 @@
from nest.core import Controller, Depends, Get, Post
# app/controllers/user_controller.py

from .user_model import User
import uuid
from typing import Optional, Any
from .user_service import UserService
from nest.core import Controller, Get, Post, Param, Query, Body, Form, File
from .user_model import UserCreateDTO, UserDTO


@Controller("user")
@Controller(prefix="/users", tag="Users")
class UserController:
def __init__(self, service: UserService):
self.service = service
def __init__(self, user_service: UserService):
self.user_service = user_service

@Get("/{user_id}")
def get_user_by_id(self, user_id: Param[uuid.UUID]) -> dict:
"""
Retrieve a user by their UUID.
"""
user = self.user_service.get_user_by_id(str(user_id))
return {"user": user}

@Get("/")
def get_user(self):
return self.service.get_user()
def list_users(
self,
page: Query[int] = 1,
limit: Query[int] = 50,
search: Optional[Query[str]] = None,
) -> dict:
"""
List users with pagination and optional search.
"""
# Implement pagination and search logic here
return {
"message": f"Listing users on page={page}, limit={limit}, search={search}"
}

@Post("/")
def add_user(self, user: User):
return self.service.add_user(user)
def create_user(self, user: Body[UserCreateDTO]) -> dict:
"""
Create a new user.
"""
user_data = self.user_service.add_user(user)
return {"message": "User created", "user": user_data}

#
@Post("/{user_id}/upload-avatar")
def upload_avatar(
self,
user_id: Param[uuid.UUID],
file: File[bytes],
description: Optional[Form[str]] = None,
) -> dict:
"""
Upload an avatar for a user.
"""
# avatar_url = self.user_service.upload_avatar(user_id, file, description)
print(f"Uploaded avatar for user {user_id}: {file}")
print(f"Description: {description}")
return {
"message": "Avatar uploaded",
"avatar_url": "http://example.com/avatar.jpg",
}
12 changes: 11 additions & 1 deletion examples/BlankApp/src/user/user_model.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import uuid
from pydantic import BaseModel


class User(BaseModel):
class UserDTO(BaseModel):
id: uuid.UUID
name: str
email: str
age: int


class UserCreateDTO(BaseModel):
name: str
email: str
age: int
14 changes: 9 additions & 5 deletions examples/BlankApp/src/user/user_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,23 @@

from nest.core.decorators import Injectable

from .user_model import User
from .user_model import UserDTO, UserCreateDTO


@Injectable
class UserService:
def __init__(self):
self.database = []
time.sleep(5)
print("UserService initialized")

def get_user(self):
def get_user(self) -> list[UserDTO]:
return self.database

def add_user(self, user: User):
def add_user(self, user: UserCreateDTO):
self.database.append(user)
return user

def get_user_by_id(self, user_id: str) -> UserDTO:
return next((user for user in self.database if user.id == user_id), None)

def log_access(self, user_id):
print(user_id, " Access app")
2 changes: 1 addition & 1 deletion nest/common/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def has(self, token: str):
return True if self.get(token) is not None else False


class Module:
class NestModule:
def __init__(self, metatype: Type[object], container):
self._id = str(uuid.uuid4())
self._metatype = metatype
Expand Down
16 changes: 0 additions & 16 deletions nest/common/route_resolver.py

This file was deleted.

13 changes: 11 additions & 2 deletions nest/core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from fastapi import Depends

from nest.core.decorators import (
Controller,
Delete,
Expand All @@ -11,6 +9,17 @@
Post,
Put,
)
from nest.core.protocols import (
Param,
Query,
Header,
Body,
Cookie,
RequestProtocol,
ResponseProtocol,
File,
Form,
)
from nest.core.pynest_application import PyNestApp
from nest.core.pynest_container import PyNestContainer
from nest.core.pynest_factory import PyNestFactory
Empty file added nest/core/adapters/__init__.py
Empty file.
Empty file.
Empty file.
Empty file.
145 changes: 145 additions & 0 deletions nest/core/adapters/fastapi/fastapi_adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
from typing import Any, Callable, List, Optional
import uvicorn
from fastapi import FastAPI, APIRouter
from fastapi.middleware import Middleware

from nest.core.protocols import (
WebFrameworkAdapterProtocol,
RouterProtocol,
Container,
)

from nest.core.adapters.fastapi.utils import wrap_instance_method


class FastAPIRouterAdapter(RouterProtocol):
"""
An adapter for registering routes in FastAPI.
"""

def __init__(self, base_path: str = "") -> None:
"""
Initialize with an optional base path.
"""
print("Initializing FastAPIRouterAdapter")
self._base_path = base_path
self._router = APIRouter(prefix=self._base_path)

def add_route(
self,
path: str,
endpoint: Callable[..., Any],
methods: List[str],
*,
name: Optional[str] = None,
) -> None:
"""
Register an HTTP route with FastAPI's APIRouter.
"""
self._router.add_api_route(path, endpoint, methods=methods, name=name)

def get_router(self) -> APIRouter:
"""
Return the underlying FastAPI APIRouter.
"""
return self._router


###############################################################################
# FastAPI Adapter
###############################################################################


class FastAPIAdapter(WebFrameworkAdapterProtocol):
"""
A FastAPI-based implementation of WebFrameworkAdapterProtocol.
"""

def __init__(self) -> None:
self._app: Optional[FastAPI] = None
self._router_adapter = FastAPIRouterAdapter()
self._middlewares: List[Middleware] = []
self._initialized = False

def create_app(self, **kwargs: Any) -> FastAPI:
"""
Create and configure the FastAPI application.
"""
print("Creating FastAPI app")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove print

self._app = FastAPI(**kwargs)
self._app.include_router(self._router_adapter.get_router())
# Add any pre-collected middlewares
for mw in self._middlewares:
self._app.add_middleware(mw.cls, **mw.options)

self._initialized = True
return self._app

def get_router(self) -> RouterProtocol:
"""
Return the RouterProtocol implementation.
"""
return self._router_adapter

def add_middleware(
self,
middleware_cls: Any,
**options: Any,
) -> None:
"""
Add middleware to the FastAPI application.
"""
if not self._app:
# Collect middlewares before app creation
self._middlewares.append(Middleware(middleware_cls, **options))
else:
# Add middleware directly if app is already created
self._app.add_middleware(middleware_cls, **options)

def run(self, host: str = "127.0.0.1", port: int = 8000, **kwargs) -> None:
"""
Run the FastAPI application using Uvicorn.
"""
if not self._initialized or not self._app:
raise RuntimeError("FastAPI app not created yet. Call create_app() first.")

uvicorn.run(self._app, host=host, port=port, **kwargs)

async def startup(self) -> None:
"""
Handle any startup tasks if necessary.
"""
if self._app:
await self._app.router.startup()

async def shutdown(self) -> None:
"""
Handle any shutdown tasks if necessary.
"""
if self._app:
await self._app.router.shutdown()

def register_routes(self, container: Container) -> None:
"""
Register multiple routes at once.
"""
for module in container.modules.values():
for controller_cls in module.controllers.values():
instance = container.get_instance(controller_cls)

route_definitions = getattr(controller_cls, "__pynest_routes__", [])
for route_definition in route_definitions:
path = route_definition["path"]
method = route_definition["method"]
original_method = route_definition["endpoint"]

final_endpoint = wrap_instance_method(
instance, controller_cls, original_method
)

self._router_adapter.add_route(
path=path,
endpoint=final_endpoint,
methods=[method],
name=f"{controller_cls.__name__}.{original_method.__name__}",
)
Loading
Loading