├── .github └── workflows │ └── publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── examples ├── example_Crud_app │ └── main.py ├── example_async_db_app │ └── main.py ├── example_basic │ └── main.py ├── example_bigger_app │ ├── api │ │ └── user.py │ ├── db │ │ └── user.py │ ├── depends │ │ └── depends.py │ ├── main.py │ └── models │ │ └── user.py └── example_paginate_dependency │ └── main.py ├── extviews ├── __init__.py ├── connections.py ├── crudset.py └── viewset.py ├── pyproject.toml ├── requirements.txt ├── scripts └── publish.sh └── test └── test_viewset.py /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | deploy: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Set up Python 15 | uses: actions/setup-python@v3 16 | with: 17 | python-version: '3.7' 18 | - name: Install dependencies 19 | run: | 20 | python -m pip install --upgrade pip 21 | pip install poetry 22 | - name: Build package 23 | run: python -m build 24 | - name: Build and publish package 25 | run: | 26 | poetry version $(git describe --tags --abbrev=0) 27 | poetry build 28 | poetry publish --username ${{ secrets.PYPI_USERNAME }} --password ${{ secrets.PYPI_PASSWORD }} 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | 141 | # pytype static type analyzer 142 | .pytype/ 143 | 144 | # Cython debug symbols 145 | cython_debug/ 146 | 147 | # PyCharm 148 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can 149 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 150 | # and can be added to the global gitignore or merged into this file. For a more nuclear 151 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 152 | #.idea/ 153 | 154 | TODO -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 bilal alpaslan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fastapi-ExtViews 2 | 3 | extviews is a fastapi library for creating RESTful APIs with classes. 4 | -------------------------------------------------------------------------------- /examples/example_Crud_app/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from pydantic import BaseModel 3 | 4 | from extviews.connections import PymongoConnection 5 | from extviews.crudset import PymongoCrudSet 6 | from extviews import CrudViewSet 7 | 8 | app = FastAPI() 9 | 10 | conn = PymongoConnection() 11 | 12 | #------------------------------------------------------------------------------ 13 | # Models 14 | class User(BaseModel): 15 | id: int 16 | name: str 17 | age: int 18 | 19 | #------------------------------------------------------------------------------ 20 | # CRUD SET 21 | 22 | class UserCrudSet(PymongoCrudSet): 23 | connection = conn 24 | collection = "users" 25 | model = User 26 | 27 | #------------------------------------------------------------------------------ 28 | # VIEW SET 29 | 30 | class UserCrudViewSet(CrudViewSet): 31 | base_path = '/users' 32 | class_tag = 'user' 33 | crud = UserCrudSet 34 | model = User 35 | 36 | 37 | app.include_router(UserCrudViewSet().router) 38 | 39 | 40 | if __name__ == '__main__': 41 | import uvicorn 42 | uvicorn.run("main:app", port=8001 ,reload=True, workers=4, ) 43 | 44 | -------------------------------------------------------------------------------- /examples/example_async_db_app/main.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from fastapi import FastAPI 3 | from pydantic import BaseModel 4 | 5 | from extviews import PymongoConnection, MotorConnection, CrudViewSet, BaseCrudSet 6 | 7 | app = FastAPI() 8 | 9 | conn = PymongoConnection() 10 | conn2 = MotorConnection() 11 | 12 | #------------------------------------------------------------------------------ MODEL 13 | class User(BaseModel): 14 | id: int 15 | name: str 16 | age: int 17 | 18 | #------------------------------------------------------------------------------ CRUD SET 19 | class UserCrudSet(BaseCrudSet): 20 | # db = conn.get_db() 21 | db = conn2.get_db() 22 | 23 | async def list(self): 24 | users = [] 25 | async for u in self.db.users.find(): 26 | users.append(User(**u)) 27 | print(users) 28 | return users 29 | 30 | async def create(self, data: User): 31 | await self.db.users.insert_one(data.dict()) 32 | return data 33 | 34 | #------------------------------------------------------------------------------ VIEW SET 35 | class UserCrudViewSet(CrudViewSet): 36 | base_path = '/users' 37 | class_tag = 'user' 38 | crud = UserCrudSet 39 | model = User 40 | 41 | async_db = True 42 | 43 | 44 | app.include_router(UserCrudViewSet().router) 45 | 46 | 47 | if __name__ == '__main__': 48 | import uvicorn 49 | uvicorn.run("main:app", port=8001 ,reload=True, workers=4, ) 50 | 51 | -------------------------------------------------------------------------------- /examples/example_basic/main.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from fastapi import FastAPI, Depends, Header, HTTPException 4 | from pydantic import BaseModel 5 | 6 | from extviews import ViewSet 7 | 8 | app = FastAPI() 9 | 10 | 11 | class User(BaseModel): 12 | id: int 13 | name: str 14 | 15 | async def get_users(): 16 | return [User(id=1, name='John Doe'), User(id=2, name='Jane Doe')] 17 | 18 | 19 | async def verify_token(x_token: str = Header(...)): 20 | if x_token != "fake-super-secret-token": 21 | raise HTTPException(status_code=400, detail="X-Token header invalid") 22 | 23 | @app.get('/') 24 | async def root(): 25 | return {'message': 'Hello World'} 26 | 27 | 28 | class UserViewSet(ViewSet): 29 | base_path = '/users' 30 | # class_tag = 'user' 31 | # response_model = User 32 | # dependencies = [Depends(verify_token)] 33 | 34 | def get_response_model(action: str): 35 | if action == 'list': 36 | return List[User] # TODO: generic function for all actions 37 | 38 | def get_dependencies(action: str): 39 | if action in ["create", "update", "delete"]: 40 | return [Depends(verify_token)] 41 | return None 42 | 43 | def list(self): 44 | return get_users() 45 | 46 | def retrieve(self, id: int): 47 | return {'message': f'Hello World {id}'} 48 | 49 | def create(self, user: User): 50 | return {'message': f'Hello World create {user.id}'} 51 | 52 | def update(self, id: int): 53 | return {'message': f'Hello World update {id}'} 54 | 55 | def delete(self, id: int): 56 | return {'message': f'Hello World delete {id}'} 57 | 58 | 59 | app.include_router(UserViewSet().router) 60 | 61 | if __name__ == '__main__': 62 | import uvicorn 63 | uvicorn.run("main:app", port=8001 ,reload=True, workers=4, ) 64 | -------------------------------------------------------------------------------- /examples/example_bigger_app/api/user.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from fastapi import Depends 3 | 4 | from extviews import ViewSet 5 | 6 | from db.user import get_users 7 | from depends.depends import verify_token 8 | from models.user import User 9 | 10 | class UserViewSet(ViewSet): 11 | base_path = '/users' 12 | # class_tag = 'user' 13 | # response_model = User 14 | # dependencies = [Depends(verify_token)] 15 | 16 | def get_response_model(action: str): 17 | if action == 'list': 18 | return List[User] # TODO: generic function for all actions 19 | 20 | def get_dependencies(action: str): 21 | if action in ["create", "update", "delete"]: 22 | return [Depends(verify_token)] 23 | return None 24 | 25 | def list(self): # TODO: if we use "self" can we remove this ? viewset ViewSet._register_route function 26 | return get_users() 27 | 28 | def retrieve(self, id: int): 29 | return {'message': f'Hello World {id}'} 30 | 31 | def create(self, user: User): 32 | return {'message': f'Hello World create {user.id}'} 33 | 34 | def update(self, id: int): 35 | return {'message': f'Hello World update {id}'} 36 | 37 | def delete(self, id: int): 38 | return {'message': f'Hello World delete {id}'} 39 | -------------------------------------------------------------------------------- /examples/example_bigger_app/db/user.py: -------------------------------------------------------------------------------- 1 | 2 | from models.user import User 3 | 4 | 5 | async def get_users(): 6 | return [User(id=1, name='John Doe'), User(id=2, name='Jane Doe')] -------------------------------------------------------------------------------- /examples/example_bigger_app/depends/depends.py: -------------------------------------------------------------------------------- 1 | 2 | from fastapi import Header, HTTPException 3 | 4 | 5 | async def verify_token(x_token: str = Header(...)): 6 | if x_token != "fake-super-secret-token": 7 | raise HTTPException(status_code=400, detail="X-Token header invalid") -------------------------------------------------------------------------------- /examples/example_bigger_app/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | from api.user import UserViewSet 4 | 5 | app = FastAPI() 6 | 7 | 8 | @app.get('/') 9 | async def root(): 10 | return {'message': 'Hello World'} 11 | 12 | 13 | app.include_router(UserViewSet().router) 14 | 15 | if __name__ == '__main__': 16 | import uvicorn 17 | uvicorn.run("main:app", port=8001 ,reload=True, workers=4, ) 18 | -------------------------------------------------------------------------------- /examples/example_bigger_app/models/user.py: -------------------------------------------------------------------------------- 1 | 2 | from pydantic import BaseModel 3 | 4 | 5 | class User(BaseModel): 6 | id: int 7 | name: str -------------------------------------------------------------------------------- /examples/example_paginate_dependency/main.py: -------------------------------------------------------------------------------- 1 | from typing import Sequence 2 | from fastapi import FastAPI, Depends 3 | from pydantic import BaseModel 4 | 5 | from extviews.connections import PymongoConnection 6 | from extviews.crudset import PymongoCrudSet 7 | from extviews.depends import pagination_depends 8 | from extviews import CrudViewSet 9 | 10 | app = FastAPI() 11 | 12 | conn =PymongoConnection() 13 | 14 | #------------------------------------------------------------------------------ 15 | # Models 16 | class User(BaseModel): 17 | id: int 18 | name: str 19 | age: int 20 | 21 | #------------------------------------------------------------------------------ 22 | # CRUD SET 23 | 24 | class UserCrudSet(PymongoCrudSet): 25 | connection = conn 26 | collection = "users" 27 | model = User 28 | 29 | def list(self, skip: int = 0, limit: int = 10) -> Sequence[User]: 30 | models = [] 31 | for _model in self._collection.find().skip(skip).limit(limit): 32 | models.append(self.model(**_model)) 33 | return models 34 | 35 | #------------------------------------------------------------------------------ 36 | # VIEW SET 37 | 38 | class UserCrudViewSet(CrudViewSet): 39 | base_path = '/users' 40 | class_tag = 'user' 41 | crud = UserCrudSet 42 | model = User 43 | 44 | def get_dependencies(self, action: str) -> Sequence[Depends]: 45 | if action in ['list']: 46 | return [pagination_depends(max_limit=10)] 47 | return None 48 | 49 | def list(self, skip: int = 0, limit: int = 10) -> Sequence[User]: 50 | return self._crud.list(skip=skip, limit=limit) 51 | 52 | app.include_router(UserCrudViewSet().router) 53 | 54 | 55 | if __name__ == '__main__': 56 | import uvicorn 57 | uvicorn.run("main:app", port=8001 ,reload=True, workers=4, ) 58 | 59 | -------------------------------------------------------------------------------- /extviews/__init__.py: -------------------------------------------------------------------------------- 1 | """ extviews is a fastapi library for creating RESTful APIs with a single class. """ 2 | 3 | __version__ = "0.1.5" 4 | 5 | from .viewset import ViewSet, CrudViewSet 6 | from .crudset import BaseCrudSet, ModelCrudSet, PymongoCrudSet, MotorCrudSet 7 | from .connections import MotorConnection, PymongoConnection 8 | 9 | __all__ = [ 10 | 'ViewSet', 11 | 'CrudViewSet', 12 | 'BaseCrudSet', 13 | 'ModelCrudSet', 14 | 'PymongoCrudSet', 15 | 'MotorCrudSet', # !: not yet implemented 16 | 'MotorConnection', 17 | 'PymongoConnection' 18 | ] 19 | 20 | 21 | -------------------------------------------------------------------------------- /extviews/connections.py: -------------------------------------------------------------------------------- 1 | 2 | from motor.motor_asyncio import AsyncIOMotorClient 3 | from pymongo import MongoClient 4 | 5 | 6 | __all__ = ['PymongoConnection', 'MotorConnection'] 7 | 8 | class PymongoConnection: 9 | def __init__(self, host="127.0.0.1", port="27017", db="default", user=None, password=None): 10 | """Create database connection.""" 11 | if user and password: 12 | self.db_client = MongoClient(f"mongodb://{user}:{password}@{host}:{port}") 13 | else: 14 | self.db_client = MongoClient(f"mongodb://{host}:{port}") 15 | self.db_name = db 16 | 17 | def get_db_client(self) -> MongoClient: 18 | """Return database client instance.""" 19 | return self.db_client 20 | 21 | def get_db(self): 22 | """Return database instance.""" 23 | return self.get_db_client()[self.db_name] 24 | 25 | def close_db(self): 26 | """Close database connection.""" 27 | self.db_client.close() 28 | 29 | 30 | class MotorConnection: 31 | def __init__(self, host="127.0.0.1", port="27017", db="default", user=None, password=None): 32 | """Create database connection.""" 33 | if user and password: 34 | self.db_client = AsyncIOMotorClient(f"mongodb://{user}:{password}@{host}:{port}") 35 | else: 36 | self.db_client = AsyncIOMotorClient(f"mongodb://{host}:{port}") 37 | self.db_name = db 38 | 39 | def get_db_client(self) -> AsyncIOMotorClient: 40 | """Return database client instance.""" 41 | return self.db_client 42 | 43 | def get_db(self): 44 | """Return database instance.""" 45 | return self.get_db_client()[self.db_name] 46 | 47 | def close_db(self): 48 | """Close database connection.""" 49 | self.db_client.close() -------------------------------------------------------------------------------- /extviews/crudset.py: -------------------------------------------------------------------------------- 1 | 2 | from pydantic import BaseModel 3 | 4 | from .connections import MotorConnection, PymongoConnection 5 | 6 | __all__ = ['BaseCrudSet', 'ModelCrudSet', 'PymongoCrudSet', 'MotorCrudSet'] 7 | 8 | class BaseCrudSet(object): 9 | """ 10 | Base class for CRUD operations. 11 | """ 12 | def __init__(self) -> None: 13 | super().__init__() 14 | 15 | def list(self): 16 | assert False, "You must implement list method" 17 | 18 | def retrieve(self, id : int): 19 | assert False, "You must implement retrieve method" 20 | 21 | def create(self, data : dict): 22 | assert False, "You must implement create method" 23 | 24 | def update(self, id : int, data : dict): 25 | assert False, "You must implement update method" 26 | 27 | def partial_update(self, id : int, data : dict): 28 | assert False, "You must implement partial_update method" 29 | 30 | def destroy(self, id : int): 31 | assert False, "You must implement destroy method" 32 | 33 | 34 | class ModelCrudSet(BaseCrudSet): 35 | """ 36 | Generic CRUD set. 37 | 38 | model : BaseModel = None 39 | """ 40 | model : BaseModel = None 41 | 42 | def __init__(self): 43 | assert self.model is not None, "model is not defined" 44 | super().__init__() 45 | 46 | 47 | class PymongoCrudSet(ModelCrudSet): 48 | """MongoDB CRUD set. 49 | 50 | model : BaseModel = None 51 | connection : PymongoConnection = None 52 | collecttion: str = None 53 | """ 54 | connection : PymongoConnection = None 55 | collecttion: str = None 56 | 57 | def __init__(self): 58 | assert self.connection is not None, "connenciton is not defined" 59 | if self.collecttion is None: 60 | self.collecttion = self.model.__name__.lower() 61 | 62 | self.db = self.connection.get_db() 63 | self._collection = self.db[self.collecttion] 64 | super().__init__() 65 | 66 | def list(self): 67 | models=[] 68 | for _model in self._collection.find(): 69 | models.append(self.model(**_model)) 70 | return models 71 | 72 | def retrieve(self, id : int): 73 | _model = self._collection.find_one({'id': id}) 74 | return self.model(**_model) 75 | 76 | def create(self, model : BaseModel): 77 | self._collection.insert_one(model.dict()) 78 | return model 79 | 80 | def update(self, id : int, model : BaseModel): 81 | self._collection.update_one({'id': id}, {'$set': model.dict()}) 82 | return model 83 | 84 | def partial_update(self, id : int, model : BaseModel): 85 | self._collection.update_one({'id': id}, {'$set': model.dict()}) 86 | return model 87 | 88 | def destroy(self, id : int): 89 | self._collection.delete_one({'id': id}) 90 | return {'message': f'destroy {id}'} 91 | 92 | 93 | class MotorCrudSet(ModelCrudSet): 94 | 95 | 96 | # !: not implemented yet 97 | 98 | """MongoDB CRUD set. 99 | 100 | model : BaseModel = None 101 | collecttion: str = None 102 | """ 103 | conn : MotorConnection = None 104 | collecttion: str = None 105 | 106 | async def __new__(cls): 107 | instance = super().__new__(cls) 108 | await instance.__init__() 109 | return instance 110 | 111 | async def __init__(self): 112 | assert self.conn is not None, "conn is not defined" 113 | if self.collecttion is None: 114 | self.collecttion = self.model.__name__.lower() 115 | 116 | self._conn = await self.conn.get_db_client() 117 | self._collection = self._conn["default"][self.collecttion] 118 | super().__init__() 119 | 120 | async def list(self): 121 | data = await self._collection.find_one() 122 | print(data) 123 | return self.model(**data) 124 | 125 | async def retrieve(self, id : int): 126 | return {'message': f'Hello World mongo {id}'} 127 | 128 | async def create(self, data : dict): 129 | return {'message': f'Hello World mongo create {data}'} 130 | 131 | async def update(self, id : int, data : dict): 132 | return {'message': f'Hello World mongo update {id}'} 133 | 134 | async def partial_update(self, id : int, data : dict): 135 | return {'message': f'Hello World mongo partial_update {id}'} 136 | 137 | async def destroy(self, id : int): 138 | return {'message': f'Hello World mongo destroy {id}'} -------------------------------------------------------------------------------- /extviews/viewset.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, List, Sequence, Union 2 | from fastapi import APIRouter, Header 3 | from fastapi.params import Depends 4 | from pydantic import BaseModel 5 | 6 | from .crudset import BaseCrudSet 7 | 8 | __all__ = ['ViewSet', 'CrudViewSet'] 9 | 10 | supported_methods_names: List[str] = [ 11 | 'list', 'retrieve', 'create', 'update', 'partial_update', 'destroy'] 12 | 13 | 14 | class ViewSet: 15 | """ router: APIRouter = None 16 | base_path: str = None 17 | class_tag: str = None 18 | path_key: str = "id" 19 | response_model: BaseModel = None 20 | dependencies: Sequence[Depends] = None 21 | """ 22 | router: APIRouter = None 23 | base_path: str = None 24 | class_tag: str = None 25 | path_key: str = "id" 26 | response_model: BaseModel = None 27 | dependencies: Sequence[Depends] = None 28 | marked_functions: List = [] 29 | 30 | def __init__(self) -> APIRouter: 31 | self.functions: List[Callable] = [] 32 | self.extra_functions: List[List] = [] 33 | 34 | self.execute() 35 | 36 | def get_response_model(self, action: str) -> Union[BaseModel, None]: 37 | """ if override this method, you can return different response model for different action """ 38 | if self.response_model is not None: 39 | return self.response_model 40 | return None 41 | 42 | def get_dependencies(self, action: str) -> Sequence[Depends]: 43 | """ if override this method, you can return different dependencies for different action """ 44 | if self.dependencies is not None: 45 | return self.dependencies 46 | return None 47 | 48 | def execute(self) -> APIRouter: 49 | 50 | if self.router is None: 51 | self.router = APIRouter() 52 | 53 | if self.base_path is None: 54 | self.base_path = '/' + self.__class__.__name__.lower() 55 | 56 | if self.class_tag is None: 57 | self.class_tag = self.__class__.__name__ 58 | 59 | for func in supported_methods_names: 60 | if hasattr(self, func): 61 | self.functions.append(getattr(self, func)) 62 | 63 | for func in self.functions: 64 | self._register_route(func) 65 | 66 | for func, methods, path in self.find_marked_functions(): 67 | self._register_extra_route(func, methods=methods, path=path) 68 | 69 | def _register_route(self, func: Callable, hidden_params: List[str] = ["self"]): 70 | 71 | # hidden_params TODO: add support for hidden params 72 | 73 | extras = {} 74 | extras['response_model'] = self.get_response_model(func.__name__) 75 | extras['dependencies'] = self.get_dependencies(func.__name__) 76 | 77 | if func.__name__ == 'list': 78 | self.router.add_api_route(self.base_path, func, tags=[ 79 | self.class_tag], methods=['GET'], **extras) 80 | elif func.__name__ == 'retrieve': 81 | self.router.add_api_route(f"{self.base_path}/\u007b{self.path_key}\u007d", func, tags=[ 82 | self.class_tag], methods=['GET'], **extras) 83 | elif func.__name__ == 'create': 84 | self.router.add_api_route(self.base_path, func, tags=[ 85 | self.class_tag], methods=['POST'], **extras) 86 | elif func.__name__ == 'update': 87 | self.router.add_api_route(f"{self.base_path}/\u007b{self.path_key}\u007d", func, tags=[ 88 | self.class_tag], methods=['PUT'], **extras) 89 | elif func.__name__ == 'partial_update': 90 | self.router.add_api_route(f"{self.base_path}/\u007b{self.path_key}\u007d", func, tags=[ 91 | self.class_tag], methods=['PATCH'], **extras) 92 | elif func.__name__ == 'destroy': 93 | self.router.add_api_route(f"{self.base_path}/\u007b{self.path_key}\u007d", func, tags=[ 94 | self.class_tag], methods=['DELETE'], **extras) 95 | else: 96 | print(f"Method {func.__name__} is not supported") 97 | 98 | def _register_extra_route(self, func: Callable, methods: List[str] = ["GET"], path: str = None): 99 | extras = {} 100 | extras['response_model'] = self.get_response_model(func.__name__) 101 | extras['dependencies'] = self.get_dependencies(func.__name__) 102 | if path is None: 103 | path = func.__name__ 104 | self.router.add_api_route(f"{self.base_path}{path}", func, tags=[ 105 | self.class_tag], methods=methods, **extras) 106 | 107 | @classmethod 108 | def extra_method(cls, methods: List[str] = ["GET"], path_key: str = None): 109 | """ if you want to add extra method to the viewset, you can use this decorator """ 110 | def decorator(func): 111 | cls.marked_functions.append([func, methods, path_key]) 112 | return func 113 | return decorator 114 | 115 | def find_marked_functions(self): 116 | for func in dir(self): 117 | for marked_func in self.marked_functions: 118 | if func == marked_func[0].__name__: 119 | self.extra_functions.append(marked_func) 120 | self.marked_functions.remove(marked_func) 121 | break 122 | return self.extra_functions 123 | 124 | 125 | class CrudViewSet(ViewSet): 126 | """ 127 | This is the base viewset for CRUD operations. 128 | """ 129 | crud: BaseCrudSet = None 130 | model: BaseModel = None 131 | async_db = False 132 | 133 | def __init__(self): 134 | assert self.crud is not None, "You must define crud model" 135 | assert self.model is not None, "You must define model" 136 | 137 | self._crud = self.crud() 138 | super().__init__() 139 | 140 | async def list(self) -> List[model]: 141 | if self.async_db: 142 | return await self._crud.list() 143 | return self._crud.list() 144 | 145 | async def retrieve(self, id: int) -> model: 146 | if self.async_db: 147 | return await self._crud.retrieve(id) 148 | return self._crud.retrieve(id) 149 | 150 | async def create(self, data: model) -> model: 151 | if self.async_db: 152 | return await self._crud.create(data) 153 | return self._crud.create(data) 154 | 155 | async def update(self, id: int, data: model) -> model: 156 | if self.async_db: 157 | return await self._crud.update(id, data) 158 | return self._crud.update(id, data) 159 | 160 | async def partial_update(self, id: int, data: model) -> model: 161 | if self.async_db: 162 | return await self._crud.partial_update(id, data) 163 | return self._crud.partial_update(id, data) 164 | 165 | async def destroy(self, id: int) -> model: 166 | if self.async_db: 167 | return await self._crud.destroy(id) 168 | return self._crud.destroy(id) 169 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "fastapi-extviews" 3 | version = "0.1.5" 4 | description = "extviews is a fastapi library for creating RESTful APIs with a single class." 5 | readme = "README.md" 6 | homepage = "https://github.com/BilalAlpaslan/fastapi-extviews" 7 | authors = ["Bilal Alpaslan "] 8 | license = "LICENSE" 9 | packages = [{include = "extviews/*"}] 10 | 11 | [tool.poetry.dependencies] 12 | python = ">=3.7" 13 | fastapi = ">=0.73" 14 | pymongo = ">=3.11" 15 | motor = ">=2.0" 16 | 17 | [build-system] 18 | requires = ["poetry>=0.12"] 19 | build-backend = "poetry.masonry.api" -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.74.0 -------------------------------------------------------------------------------- /scripts/publish.sh: -------------------------------------------------------------------------------- 1 | py -m poetry build 2 | py -m poetry publish -------------------------------------------------------------------------------- /test/test_viewset.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fastapi.testclient import TestClient 3 | from pydantic import BaseModel 4 | 5 | from extviews import ViewSet 6 | 7 | 8 | class UserViewSet(ViewSet): 9 | def list(self): 10 | return [{'message': 'Hello World'}] 11 | 12 | def retrieve(self, id): 13 | return {'message': f'Hello World {id}'} 14 | 15 | 16 | class Product(BaseModel): 17 | id: int 18 | name: str 19 | 20 | 21 | class ProductViewSet(ViewSet): 22 | base_path = '/products' 23 | class_tag = 'product' 24 | response_model = Product 25 | 26 | def list(self): 27 | return Product(id=1, name='John Doe') 28 | 29 | def retrieve(self, id): 30 | return Product(id=id, name='John Doe') 31 | 32 | 33 | app = FastAPI() 34 | app.include_router(UserViewSet().router) 35 | app.include_router(ProductViewSet().router) 36 | 37 | client = TestClient(app) 38 | 39 | 40 | def test_base_viewset(): 41 | # assert UserViewSet.base_path == '/userviewset' 42 | # assert UserViewSet.class_tag == 'UserViewSet' 43 | 44 | # TODO this test is not working because not registered in app.include_router in test_base_viewset 45 | 46 | assert UserViewSet.response_model == None 47 | 48 | 49 | def test_base_viewset_2(): 50 | assert ProductViewSet.base_path == '/products' 51 | assert ProductViewSet.class_tag == 'product' 52 | assert ProductViewSet.response_model == Product 53 | 54 | 55 | def test_base_viewset_list(): 56 | response = client.get('/userviewset') 57 | assert response.status_code == 200 58 | assert response.json() == [{'message': 'Hello World'}] 59 | 60 | 61 | def test_base_viewset_get(): 62 | id = 1 63 | response = client.get(f'/userviewset/{id}') 64 | assert response.status_code == 200 65 | assert response.json() == {'message': f'Hello World {id}'} 66 | 67 | 68 | def test_base_viewset_list_2(): 69 | response = client.get('/products') 70 | assert response.status_code == 200 71 | assert response.json() == {'id': 1, 'name': 'John Doe'} 72 | 73 | 74 | def test_base_viewset_get_2(): 75 | id = 1 76 | response = client.get(f'/products/{id}') 77 | assert response.status_code == 200 78 | assert response.json() == {'id': 1, 'name': 'John Doe'} 79 | --------------------------------------------------------------------------------