Skip to content
Merged
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
71 changes: 53 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,45 +14,80 @@ pip install fastapi-router-controller

## How to use

In a Class module
Here we see a Fastapi CBV (class based view) application
with class wide Basic Auth dependencies.

```python
from fastapi import APIRouter, Depends
import uvicorn

from pydantic import BaseModel
from fastapi_router_controller import Controller
from fastapi import APIRouter, Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials

router = APIRouter()
controller = Controller(router)
security = HTTPBasic()


def verify_auth(credentials: HTTPBasicCredentials = Depends(security)):
correct_username = credentials.username == "john"
correct_password = credentials.password == "silver"
if not (correct_username and correct_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect auth",
headers={"WWW-Authenticate": "Basic"},
)
return credentials.username


class Foo(BaseModel):
bar: str = "wow"


async def amazing_fn():
return 'amazing_variable'
return Foo(bar="amazing_variable")


@controller.resource()
class ExampleController():
class ExampleController:

# add class wide dependencies e.g. auth
dependencies = [Depends(verify_auth)]

# you can define in the Controller init some FastApi Dependency and them are automatically loaded in controller methods
def __init__(self, x: Foo = Depends(amazing_fn)):
self.x = x

@controller.route.get(
'/some_aoi',
summary='A sample description')
"/some_aoi", summary="A sample description", response_model=Foo
)
def sample_api(self):
print(self.x) # -> amazing_variable

return 'A sample response'
```
print(self.x.bar) # -> amazing_variable
return self.x

Load the controller to the main FastAPI app
```python
from fastapi import FastAPI
from fastapi_router_controller import Controller

import ExampleController
# Load the controller to the main FastAPI app

app = FastAPI(
title='A sample application using fastapi_router_controller',
version="0.1.0")
title="A sample application using fastapi_router_controller", version="0.1.0"
)

app.include_router(ExampleController.router())

uvicorn.run(app, host="0.0.0.0", port=9090)
```

### Screenshot

All you expect from Fastapi

![Swagger UI](./swagger_ui.png?raw=true)

Also the login dialog

![Swagger UI Login](./swagger_ui_basic_auth.png?raw=true)


## For some Example use-cases visit the example folder
116 changes: 64 additions & 52 deletions fastapi_router_controller/lib/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,60 +5,62 @@
OPEN_API_TAGS = []
__app_controllers__ = []
__router_params__ = [
'response_model',
'status_code',
'tags',
'dependencies',
'summary',
'description',
'response_description',
'responses',
'deprecated',
'methods',
'operation_id',
'response_model_include',
'response_model_exclude',
'response_model_by_alias',
'response_model_exclude_unset',
'response_model_exclude_defaults',
'response_model_exclude_none',
'include_in_schema',
'response_class',
'name',
'callbacks'
]

class Controller():
'''
"response_model",
"status_code",
"tags",
"dependencies",
"summary",
"description",
"response_description",
"responses",
"deprecated",
"methods",
"operation_id",
"response_model_include",
"response_model_exclude",
"response_model_by_alias",
"response_model_exclude_unset",
"response_model_exclude_defaults",
"response_model_exclude_none",
"include_in_schema",
"response_class",
"name",
"callbacks",
]


class Controller:
"""
The Controller class.

It expose some utilities and decorator functions to define a router controller class
'''
RC_KEY = '__router__'
SIGNATURE_KEY = '__signature__'
"""

RC_KEY = "__router__"
SIGNATURE_KEY = "__signature__"

def __init__(self, router: APIRouter, openapi_tag: dict = None) -> None:
'''
"""
:param router: The FastApi router to link to the Class
:param openapi_tag: An openapi object that will describe your routes in the openapi tamplate
'''
"""
self.router = copy.deepcopy(router)
self.openapi_tag = openapi_tag
self.cls = None

if openapi_tag:
OPEN_API_TAGS.append(openapi_tag)

def __get_parent_routes(self, router: APIRouter):
'''
"""
Private utility to get routes from an extended class
'''
"""
for route in router.routes:
options = {key: getattr(route, key) for key in __router_params__}

# inherits child tags if presents
if len(options['tags']) == 0 and self.openapi_tag:
options['tags'].append(self.openapi_tag['name'])
if len(options["tags"]) == 0 and self.openapi_tag:
options["tags"].append(self.openapi_tag["name"])

self.router.add_api_route(route.path, route.endpoint, **options)

Expand All @@ -74,15 +76,16 @@ def add_resource(self, cls):
return cls

def resource(self):
'''
"""
A decorator function to mark a Class as a Controller
'''
"""
return self.add_resource

def use(_):
'''
"""
A decorator function to mark a Class to be automatically loaded by the Controller
'''
"""

def wrapper(cls):
__app_controllers__.append(cls)
return cls
Expand All @@ -91,47 +94,56 @@ def wrapper(cls):

@staticmethod
def __parse_controller_router(cls):
'''
"""
Private utility to parse the router controller property and extract the correct functions handlers
'''
"""
router = getattr(cls, Controller.RC_KEY)

dependencies = None
if hasattr(cls, "dependencies"):
dependencies = copy.deepcopy(cls.dependencies)
delattr(cls, "dependencies")

for route in router.routes:
# get the signature of the endpoint function
signature = inspect.signature(route.endpoint)
# get the parameters of the endpoint function
signature_parameters = list(signature.parameters.values())
# add class dependencies
if dependencies:
for depends in dependencies[::-1]:
route.dependencies.insert(0, depends)
Copy link
Owner

Choose a reason for hiding this comment

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

@delijati Does route.dependencies always exists?

Copy link
Owner

Choose a reason for hiding this comment

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

@delijati I will move it at line 106, just to keep it separated from rewrite sign

Copy link
Collaborator Author

Choose a reason for hiding this comment

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


# replace the class instance with the itself FastApi Dependecy
signature_parameters[0] = signature_parameters[0].replace(default=Depends(cls))
signature_parameters[0] = signature_parameters[0].replace(
default=Depends(cls)
)
# set self and after it the keyword args
new_parameters = [signature_parameters[0]] + [
parameter.replace(kind=inspect.Parameter.KEYWORD_ONLY)
for parameter in signature_parameters[1:]
]
new_signature = signature.replace(parameters=new_parameters)
setattr(route.endpoint, Controller.SIGNATURE_KEY, new_signature)

return router

@staticmethod
def routers():
'''
"""
It returns all the Classes marked to be used by the "use" decorator
'''
"""
routers = []

for app_controller in __app_controllers__:
routers.append(
app_controller.router()
)

routers.append(app_controller.router())

return routers

@property
def route(self) -> APIRouter:
'''
"""
It returns the FastAPI router.
Use it as if you are using the original one.
'''
"""
return self.router
Binary file added swagger_ui.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added swagger_ui_basic_auth.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
55 changes: 55 additions & 0 deletions tests/test_class_dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import unittest
from fastapi import Depends, FastAPI, HTTPException, APIRouter
from fastapi_router_controller import Controller
from fastapi.testclient import TestClient


router = APIRouter()
controller = Controller(router, openapi_tag={"name": "sample_controller"})


def user_exists(user_id: int):
if user_id <= 5:
raise HTTPException(status_code=400, detail="No User")


def user_is_id(user_id: int):
if user_id == 6:
raise HTTPException(status_code=400, detail="Not exact user")


@controller.resource()
class User:
dependencies = [Depends(user_exists)]

@controller.route.get("/users/{user_id}", dependencies=[Depends(user_is_id)])
def read_users(self, user_id: int):
return {"user_id": user_id}


def create_app():
app = FastAPI()

app.include_router(User.router())
return app


class TestRoutes(unittest.TestCase):
def setUp(self):
app = create_app()
self.client = TestClient(app)

def test_class_dep(self):
response = self.client.get('/users/1')
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json(), {'detail': 'No User'})

def test_func_dep(self):
response = self.client.get('/users/6')
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json(), {'detail': 'Not exact user'})

def test_pass(self):
response = self.client.get('/users/7')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {'user_id': 7})