diff --git a/README.md b/README.md index 4f3b789..eafdf40 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/fastapi_router_controller/lib/controller.py b/fastapi_router_controller/lib/controller.py index 1276390..e0ca476 100644 --- a/fastapi_router_controller/lib/controller.py +++ b/fastapi_router_controller/lib/controller.py @@ -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) @@ -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 @@ -91,19 +94,30 @@ 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) # 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) @@ -111,27 +125,25 @@ def __parse_controller_router(cls): ] 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 diff --git a/swagger_ui.png b/swagger_ui.png new file mode 100644 index 0000000..0661cf7 Binary files /dev/null and b/swagger_ui.png differ diff --git a/swagger_ui_basic_auth.png b/swagger_ui_basic_auth.png new file mode 100644 index 0000000..cd9f5ff Binary files /dev/null and b/swagger_ui_basic_auth.png differ diff --git a/tests/test_class_dependencies.py b/tests/test_class_dependencies.py new file mode 100644 index 0000000..c7269c9 --- /dev/null +++ b/tests/test_class_dependencies.py @@ -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})