├── .github └── workflows │ └── python-publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── VERSION ├── example.py ├── fastapiwee ├── __init__.py ├── crud │ ├── __init__.py │ ├── base.py │ ├── exceptions.py │ ├── views.py │ └── viewsets.py ├── pwpd.py └── tests │ ├── __init__.py │ └── test_pwpd.py ├── mkdocs ├── docs │ ├── index.md │ ├── media │ │ ├── css │ │ │ └── termynal.css │ │ ├── js │ │ │ ├── custom.js │ │ │ └── termynal.js │ │ └── logo.png │ └── modules │ │ ├── CRUD │ │ ├── advanced │ │ │ ├── base.md │ │ │ ├── exceptions.md │ │ │ └── views.md │ │ └── viewsets.md │ │ └── PwPd.md └── mkdocs.yml ├── requirements.txt └── setup.py /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | deploy: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Python 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: '3.x' 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install setuptools wheel twine 26 | - name: Build and publish 27 | env: 28 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 29 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 30 | run: | 31 | python setup.py sdist bdist_wheel 32 | twine upload dist/* 33 | -------------------------------------------------------------------------------- /.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 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 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 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Tools 132 | .vscode 133 | .idea 134 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Herman Hensetskyi 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 | # FastAPIwee 2 | FastAPI + PeeWee = <3 3 | 4 | ## Using 5 | 6 | Python >= 3.6 :snake: 7 | 8 | ## Installation 9 | 10 | ```python 11 | pip install FastAPIwee 12 | ``` 13 | 14 | :tada: 15 | 16 | ## Documentation 17 | 18 | Documentation can be found here: https://fastapiwee.qqmber.wtf 19 | 20 | ## Development 21 | 22 | ### Documentation 23 | 24 | Documentation is made with [MkDocs](https://www.mkdocs.org) and [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/). 25 | 26 | To run MkDocs server: 27 | 28 | - Go to mkdocs directory 29 | 30 | ```bash 31 | cd mkdocs 32 | ``` 33 | 34 | - Run server 35 | 36 | ```bash 37 | mkdocs serve 38 | ``` 39 | 40 | To build use: 41 | 42 | ```bash 43 | mkdocs build 44 | ``` 45 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.0.8 2 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import string 4 | 5 | import peewee as pw 6 | from fastapi import FastAPI 7 | 8 | from fastapiwee import AutoFastAPIViewSet 9 | 10 | DB = pw.SqliteDatabase('/tmp/fastapiwee_example.db') 11 | 12 | 13 | class ParentTestModel(pw.Model): 14 | id = pw.AutoField() 15 | text = pw.TextField() 16 | 17 | class Meta: 18 | database = DB 19 | 20 | 21 | class TestModel(pw.Model): 22 | id = pw.AutoField() 23 | text = pw.TextField() 24 | number = pw.IntegerField(null=True) 25 | is_test = pw.BooleanField(default=True) 26 | related = pw.ForeignKeyField(ParentTestModel, backref='test_models') 27 | 28 | class Meta: 29 | database = DB 30 | 31 | 32 | class ChildTestModel(pw.Model): 33 | id = pw.AutoField() 34 | test = pw.ForeignKeyField(TestModel, backref='childs') 35 | 36 | class Meta: 37 | database = DB 38 | 39 | 40 | def create_dummy_data(amount=10, childs=5): 41 | DB.create_tables((ParentTestModel, TestModel, ChildTestModel)) 42 | 43 | rel_tm = ParentTestModel.create( 44 | text='Parent Test', 45 | ) 46 | 47 | for _ in range(amount): 48 | tm = TestModel.create( 49 | text=f'Test {"".join(random.choices(string.ascii_letters, k=8))}', 50 | number=random.randint(0, 1e10), 51 | is_test=bool(random.getrandbits(1)), 52 | related=rel_tm, 53 | ) 54 | 55 | for _ in range(childs): 56 | ChildTestModel.create( 57 | test=tm, 58 | ) 59 | 60 | 61 | def drop_dummy_data(): 62 | DB.drop_tables((ParentTestModel, TestModel, ChildTestModel)) 63 | DB.close() 64 | os.remove(DB.database) 65 | 66 | 67 | app = FastAPI(on_startup=(create_dummy_data,), on_shutdown=(drop_dummy_data,)) 68 | 69 | AutoFastAPIViewSet(TestModel, app) 70 | AutoFastAPIViewSet(ParentTestModel, app) 71 | AutoFastAPIViewSet(ChildTestModel, app, actions={'create', 'retrieve'}) 72 | -------------------------------------------------------------------------------- /fastapiwee/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapiwee.crud.viewsets import AutoFastAPIViewSet 2 | -------------------------------------------------------------------------------- /fastapiwee/crud/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ignisor/FastAPIwee/585a7c51575cdad84950d9b636d6f4887ff84afd/fastapiwee/crud/__init__.py -------------------------------------------------------------------------------- /fastapiwee/crud/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, ABCMeta 2 | from fastapiwee.crud.exceptions import NotFoundExceptionHandler 3 | import re 4 | 5 | from fastapi.params import Depends 6 | from starlette.responses import Response 7 | from fastapiwee.pwpd import PwPdModelFactory 8 | from typing import Any, Optional, Type, Union 9 | 10 | import peewee as pw 11 | import pydantic as pd 12 | from fastapi import FastAPI, APIRouter 13 | 14 | 15 | class FastAPIView(ABC): 16 | MODEL: Type[pw.Model] 17 | _RESPONSE_MODEL: Optional[pd.BaseModel] = None 18 | URL: str 19 | METHOD: str 20 | STATUS_CODE: int = 200 21 | 22 | def __init__(self): 23 | self._response_model = self._RESPONSE_MODEL 24 | 25 | def __call__(self, *args, **kwargs) -> Any: 26 | raise NotImplementedError 27 | 28 | def _get_query(self) -> pw.ModelSelect: 29 | return self.MODEL.select() 30 | 31 | @property 32 | def response_model(self) -> pd.BaseModel: 33 | if self._response_model is None: 34 | raise ValueError('Response model is not defined') 35 | 36 | return self._response_model 37 | 38 | def _get_api_route_params(self) -> dict: 39 | return { 40 | 'path': self.URL, 41 | 'endpoint': self, 42 | 'methods': [self.METHOD], 43 | 'response_model': self.response_model, 44 | 'status_code': self.STATUS_CODE, 45 | 'name': re.sub(r'(? Type['FastAPIView']: 57 | return type(model.__name__ + cls.__name__, (cls, ), {'MODEL': model}) 58 | 59 | 60 | class BaseReadFastAPIView(FastAPIView, metaclass=ABCMeta): 61 | @property 62 | def response_model(self): 63 | if self._response_model is None: 64 | self._response_model = PwPdModelFactory(self.MODEL).read_pd 65 | 66 | return self._response_model 67 | 68 | def _get_instance(self, pk: Any) -> Type[pw.Model]: 69 | return self._get_query().where(self.MODEL._meta.primary_key == pk).get() 70 | 71 | 72 | class BaseWriteFastAPIView(BaseReadFastAPIView, metaclass=ABCMeta): 73 | _SERIALIZER: Optional[pd.BaseModel] = None 74 | 75 | def __init__(self): 76 | super().__init__() 77 | self._serializer = self._SERIALIZER 78 | self._obj_data = None 79 | 80 | @property 81 | def serializer(self): 82 | if self._serializer is None: 83 | self._serializer = PwPdModelFactory(self.MODEL).write_pd 84 | 85 | return self._serializer 86 | 87 | def create(self) -> pw.Model: 88 | return self.MODEL.create(**self._obj_data.dict()) 89 | 90 | def update(self, pk: Any, partial: bool = False) -> pw.Model: 91 | instance = self._get_instance(pk) 92 | for name, value in self._obj_data.dict(exclude_unset=partial).items(): 93 | setattr(instance, name, value) 94 | 95 | instance.save() 96 | 97 | return instance 98 | 99 | def _get_api_route_params(self) -> dict: 100 | def data_dependency(data: self.serializer): 101 | self._obj_data = data 102 | 103 | params = super()._get_api_route_params() 104 | params.update({ 105 | 'dependencies': [Depends(data_dependency)], 106 | }) 107 | 108 | return params 109 | 110 | 111 | class BaseDeleteFastAPIView(BaseReadFastAPIView): 112 | STATUS_CODE = 204 113 | 114 | @property 115 | def response_model(self): 116 | return None 117 | 118 | def __call__(self, pk: Any): 119 | self.delete(pk) 120 | 121 | def delete(self, pk: Any): 122 | instance = self._get_instance(pk) 123 | instance.delete_instance() 124 | 125 | def _get_api_route_params(self) -> dict: 126 | params = super()._get_api_route_params() 127 | del params['response_model'] 128 | params.update({ 129 | 'response_class': Response 130 | }) 131 | 132 | return params 133 | -------------------------------------------------------------------------------- /fastapiwee/crud/exceptions.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from typing import Type 3 | 4 | import peewee as pw 5 | from fastapi import FastAPI, Request, Response 6 | from fastapi.responses import JSONResponse 7 | 8 | 9 | class ExceptionHandler(ABC): 10 | EXCEPTION: Type[Exception] 11 | 12 | def __call__(self, request: Request, exc: Exception) -> Response: 13 | raise NotImplementedError 14 | 15 | @classmethod 16 | def add_to_app(cls, app: FastAPI): 17 | app.add_exception_handler(cls.EXCEPTION, cls()) 18 | 19 | 20 | class NotFoundExceptionHandler(ExceptionHandler): 21 | EXCEPTION = pw.DoesNotExist 22 | 23 | def __call__(self, request: Request, exc: Exception) -> Response: 24 | return JSONResponse( 25 | status_code=404, 26 | content={ 27 | 'msg': 'Instance not found', 28 | 'type': 'not_found', 29 | }, 30 | ) 31 | -------------------------------------------------------------------------------- /fastapiwee/crud/views.py: -------------------------------------------------------------------------------- 1 | from fastapiwee.pwpd import PwPdPartUpdateModel 2 | from typing import Any, List 3 | 4 | from fastapiwee.crud.base import (BaseDeleteFastAPIView, BaseReadFastAPIView, BaseWriteFastAPIView) 5 | 6 | 7 | # Read 8 | class RetrieveFastAPIView(BaseReadFastAPIView): 9 | METHOD = 'GET' 10 | URL = '/{pk}/' 11 | 12 | def __call__(self, pk: Any): 13 | return self._get_instance(pk) 14 | 15 | 16 | # List 17 | class ListFastAPIView(BaseReadFastAPIView): 18 | METHOD = 'GET' 19 | URL = '/' 20 | 21 | def __call__(self): 22 | return list(self._get_query()) 23 | 24 | @property 25 | def response_model(self): 26 | return List[super().response_model] 27 | 28 | 29 | # Create 30 | class CreateFastAPIView(BaseWriteFastAPIView): 31 | METHOD = 'POST' 32 | URL = '/' 33 | STATUS_CODE = 201 34 | 35 | def __call__(self): 36 | return self.create() 37 | 38 | 39 | # Update 40 | class UpdateFastAPIView(BaseWriteFastAPIView): 41 | METHOD = 'PUT' 42 | URL = '/{pk}/' 43 | 44 | def __call__(self, pk: Any): 45 | return self.update(pk) 46 | 47 | 48 | # Partial update 49 | class PartialUpdateFastAPIView(BaseWriteFastAPIView): 50 | METHOD = 'PATCH' 51 | URL = '/{pk}/' 52 | 53 | def __call__(self, pk: Any): 54 | return self.update(pk, partial=True) 55 | 56 | @property 57 | def serializer(self): 58 | if self._serializer is None: 59 | self._serializer = PwPdPartUpdateModel.make_serializer(self.MODEL) 60 | 61 | return self._serializer 62 | 63 | 64 | # Delete 65 | class DeleteFastAPIView(BaseDeleteFastAPIView): 66 | METHOD = 'DELETE' 67 | URL = '/{pk}/' 68 | -------------------------------------------------------------------------------- /fastapiwee/crud/viewsets.py: -------------------------------------------------------------------------------- 1 | from fastapiwee.crud.exceptions import NotFoundExceptionHandler 2 | import logging 3 | import re 4 | from typing import Type, List, Optional 5 | 6 | import peewee as pw 7 | from fastapi import APIRouter, FastAPI 8 | 9 | from fastapiwee.crud.base import FastAPIView 10 | from fastapiwee.crud.views import ( 11 | CreateFastAPIView, 12 | DeleteFastAPIView, 13 | ListFastAPIView, 14 | PartialUpdateFastAPIView, 15 | RetrieveFastAPIView, 16 | UpdateFastAPIView 17 | ) 18 | 19 | 20 | class BaseFastAPIViewSet: 21 | VIEWS: Optional[List[FastAPIView]] = None 22 | 23 | def __init__(self, views: Optional[List[FastAPIView]] = None): 24 | if self.VIEWS is not None and views is not None: 25 | logging.warning('`VIEWS` class-level constant variable will be ignored, ' 26 | 'since `views` argument is set on initialization.') 27 | self._views = views or self.VIEWS 28 | assert self._views, 'Views must be not null nor empty. ' \ 29 | 'Either define `VIEWS` class-level constant variable or `views` argument on initialization.' 30 | 31 | self._views = [view() for view in views] # initialize views 32 | 33 | self._router = None 34 | 35 | @property 36 | def router(self): 37 | if self._router is None: 38 | self._router = APIRouter(**self._get_api_router_params()) 39 | for view in self._views: 40 | view.add_to_app(self._router) 41 | 42 | return self._router 43 | 44 | def _get_api_router_params(self): 45 | return dict() 46 | 47 | def add_to_app(self, app: FastAPI): 48 | app.include_router(self.router) 49 | NotFoundExceptionHandler.add_to_app(app) 50 | 51 | 52 | class AutoFastAPIViewSet(BaseFastAPIViewSet): 53 | _ACTIONS_MAP = { 54 | 'retrieve': RetrieveFastAPIView, 55 | 'list': ListFastAPIView, 56 | 'create': CreateFastAPIView, 57 | 'update': UpdateFastAPIView, 58 | 'part_update': PartialUpdateFastAPIView, 59 | 'delete': DeleteFastAPIView, 60 | } 61 | 62 | def __init__( 63 | self, 64 | model: Type[pw.Model], 65 | app: FastAPI, 66 | actions: set = ('retrieve', 'list', 'create', 'update', 'part_update', 'delete'), 67 | ): 68 | actions = set(actions) 69 | self.model = model 70 | super().__init__(list(self._make_views(actions))) 71 | self.add_to_app(app) 72 | 73 | def _get_api_router_params(self): 74 | params = super()._get_api_router_params() 75 | params.update({ 76 | 'prefix': '/' + re.sub(r'(? dict: 17 | for k, v in u.items(): 18 | if isinstance(v, Mapping): 19 | d[k] = deep_update(d.get(k, {}), v) 20 | else: 21 | d[k] = v 22 | return d 23 | 24 | 25 | def letter_hash(obj: Any): 26 | numbers = str(hash(obj)) 27 | negative = numbers.startswith('-') 28 | numbers = numbers[negative:] 29 | 30 | letters = '' 31 | 32 | for i in range(0, len(numbers), 2): 33 | num = int(numbers[i:i+2]) 34 | if num > (len(string.ascii_letters) - 1): 35 | letters += string.ascii_letters[int(numbers[i])] 36 | letters += string.ascii_letters[int(numbers[i+1])] 37 | else: 38 | letters += string.ascii_letters[num] 39 | 40 | return ''.join(letters) 41 | 42 | 43 | class _FieldTranslator: 44 | FIELDS_MAPPING = { 45 | pw.IntegerField: int, 46 | pw.FloatField: float, 47 | pw.BooleanField: bool, 48 | pw.UUIDField: UUID, 49 | } 50 | 51 | def __init__(self, field: pw.Field, nest_fk: bool = False, all_optional: bool = False): 52 | self.field = field 53 | self.nest_fk = nest_fk 54 | self.all_optional = all_optional 55 | 56 | @property 57 | def pd_type(self) -> str: 58 | field = self.field 59 | 60 | if isinstance(self.field, pw.ForeignKeyField): 61 | if self.nest_fk: 62 | return PwPdModel.make_serializer(self.field.rel_model) 63 | 64 | field = self.field.rel_field 65 | 66 | for field_ancestor in field.__class__.__mro__: 67 | pd_type = self.FIELDS_MAPPING.get(field_ancestor) 68 | if pd_type is not None: 69 | break 70 | else: 71 | pd_type = str 72 | 73 | if not self.is_required or self.all_optional: 74 | pd_type = Optional[pd_type] 75 | 76 | return pd_type 77 | 78 | @property 79 | def is_required(self) -> bool: 80 | return self.field.primary_key or (not self.field.null) 81 | 82 | 83 | class PwPdMeta(ModelMetaclass): 84 | def __new__(mcs, cls_name, bases, namespace, **kwargs): 85 | config = namespace.get('Config', BaseConfig) 86 | for base in reversed(bases): 87 | if issubclass(base, PdBaseModel) and base != PdBaseModel: 88 | config = inherit_config(base.__config__, config) 89 | 90 | # retrieve config values 91 | pw_model = getattr(config, 'pw_model', None) 92 | 93 | if pw_model is None: 94 | return super().__new__(mcs, cls_name, bases, namespace, **kwargs) 95 | # return cls 96 | 97 | pw_fields = set(getattr( 98 | config, 99 | 'pw_fields', 100 | set(pw_model._meta.fields.keys()) | set(f.backref for f in pw_model._meta.backrefs.keys()) 101 | )) 102 | pw_exclude = set(getattr(config, 'pw_exclude', set())) 103 | exclude_pk = getattr(config, 'pw_exclude_pk', False) 104 | nest_fk = getattr(config, 'pw_nest_fk', False) 105 | nest_backrefs = getattr(config, 'pw_nest_backrefs', False) 106 | all_optional = getattr(config, 'pw_all_optional', False) 107 | 108 | allowed_fields = pw_fields - pw_exclude 109 | 110 | # collect peewee model fields 111 | fields = dict() 112 | for name, field in pw_model._meta.fields.items(): 113 | if any(( 114 | name not in allowed_fields, 115 | name in namespace or name in namespace.get('__annotations__', {}), 116 | exclude_pk and field.primary_key, 117 | )): 118 | continue 119 | 120 | if isinstance(field, pw.ForeignKeyField) and not nest_fk: 121 | name += '_id' 122 | 123 | fields[name] = _FieldTranslator(field, nest_fk, all_optional) 124 | 125 | # collect backrefs 126 | if nest_backrefs: 127 | for field, model in pw_model._meta.backrefs.items(): 128 | if field.backref not in allowed_fields: 129 | continue 130 | 131 | fields[field.backref] = List[PwPdModel.make_serializer(model)] 132 | 133 | namespace_new = {'__annotations__': {}} # create new namespace to keep peewee fields first in order 134 | for name, type_ in fields.items(): 135 | if isinstance(type_, _FieldTranslator): 136 | value = type_.field.default 137 | annotation = type_.pd_type 138 | else: 139 | value = None 140 | annotation = type_ 141 | 142 | if value is not None: 143 | namespace_new[name] = value 144 | 145 | namespace_new['__annotations__'][name] = annotation 146 | 147 | namespace_new = deep_update(namespace_new, namespace) 148 | 149 | return super().__new__(mcs, cls_name, bases, namespace_new, **kwargs) 150 | 151 | 152 | class PwPdGetterDict(GetterDict): 153 | def get(self, key: Any, default: Any) -> Any: 154 | res = getattr(self._obj, key, default) 155 | if isinstance(res, pw.ModelSelect): # handle backrefs 156 | return list(res) 157 | return res 158 | 159 | 160 | class PwPdModel(PdBaseModel, metaclass=PwPdMeta): 161 | __CACHE = dict() 162 | 163 | class Config: 164 | extra = 'forbid' 165 | orm_mode = True 166 | getter_dict = PwPdGetterDict 167 | 168 | @classmethod 169 | def make_serializer(cls, model: Type[pw.Model], **config_values) -> Type['PwPdModel']: 170 | name = model.__name__ + cls.__name__ + (letter_hash(repr(config_values)) if config_values else '') 171 | 172 | if name not in cls.__CACHE: 173 | class Config: 174 | pw_model = model 175 | 176 | for key, value in config_values.items(): 177 | if key == 'pw_model': 178 | raise ValueError('`pw_model` can not be overriden with config values, use `model` argument') 179 | 180 | setattr(Config, key, value) 181 | 182 | cls.__CACHE[name] = type(name, (cls, ), {'Config': Config}) 183 | 184 | return cls.__CACHE[name] 185 | 186 | 187 | class PwPdWriteModel(PwPdModel): 188 | """Shortcut for write model config""" 189 | class Config: 190 | pw_exclude_pk = True 191 | pw_nest_fk = False 192 | pw_nest_backrefs = False 193 | 194 | 195 | class PwPdPartUpdateModel(PwPdWriteModel): 196 | """Shortcut for update model config (nothing is required)""" 197 | class Config: 198 | pw_all_optional = True 199 | 200 | 201 | class PwPdModelFactory: 202 | def __init__(self, model: Type[pw.Model]): 203 | self._model = model 204 | self._read_pd = None 205 | self._write_pd = None 206 | 207 | @property 208 | def model(self): 209 | return self._model 210 | 211 | @property 212 | def read_pd(self): 213 | if self._read_pd is None: 214 | self._read_pd = PwPdModel.make_serializer( 215 | self._model 216 | ) 217 | 218 | return self._read_pd 219 | 220 | @property 221 | def write_pd(self): 222 | if self._write_pd is None: 223 | self._write_pd = PwPdWriteModel.make_serializer( 224 | self._model, 225 | ) 226 | 227 | return self._write_pd 228 | -------------------------------------------------------------------------------- /fastapiwee/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ignisor/FastAPIwee/585a7c51575cdad84950d9b636d6f4887ff84afd/fastapiwee/tests/__init__.py -------------------------------------------------------------------------------- /fastapiwee/tests/test_pwpd.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | from unittest import TestCase 4 | 5 | import peewee as pw 6 | import pydantic as pd 7 | from playhouse.shortcuts import model_to_dict 8 | 9 | from fastapiwee.pwpd import PwPdMeta, PwPdModel, PwPdModelFactory 10 | 11 | DB = pw.SqliteDatabase(':memory:') 12 | 13 | 14 | class ParentTestModel(pw.Model): 15 | id = pw.AutoField() 16 | text = pw.TextField() 17 | 18 | class Meta: 19 | database = DB 20 | 21 | 22 | class TestModel(pw.Model): 23 | id = pw.AutoField() 24 | text = pw.TextField() 25 | number = pw.IntegerField(null=True) 26 | is_test = pw.BooleanField(default=True) 27 | related = pw.ForeignKeyField(ParentTestModel, backref='test_models') 28 | 29 | class Meta: 30 | database = DB 31 | 32 | 33 | class ChildTestModel(pw.Model): 34 | id = pw.AutoField() 35 | test = pw.ForeignKeyField(TestModel, backref='childs') 36 | 37 | class Meta: 38 | database = DB 39 | 40 | 41 | def create_dummy_data(amount=10, childs=5): 42 | DB.create_tables((ParentTestModel, TestModel, ChildTestModel)) 43 | 44 | rel_tm = ParentTestModel.create( 45 | text='Parent Test', 46 | ) 47 | 48 | for _ in range(amount): 49 | tm = TestModel.create( 50 | text=f'Test {"".join(random.choices(string.ascii_letters, k=8))}', 51 | number=random.randint(0, 1e10), 52 | is_test=bool(random.getrandbits(1)), 53 | related=rel_tm, 54 | ) 55 | 56 | for _ in range(childs): 57 | ChildTestModel.create( 58 | test=tm, 59 | ) 60 | 61 | 62 | class PwPdMetaTestCase(TestCase): 63 | def test_creation(self): 64 | class TestModelSerializer(pd.BaseModel, metaclass=PwPdMeta): 65 | class Config: 66 | pw_model = TestModel 67 | pw_fields = {'id', 'text', 'number', 'related', 'childs'} 68 | pw_exclude = {'text'} 69 | pw_nest_fk = True 70 | pw_nest_backrefs = True 71 | 72 | for field_in_check in {'id', 'number', 'related', 'childs'}: 73 | self.assertIn(field_in_check, TestModelSerializer.__fields__) 74 | 75 | for field_not_in_check in {'is_test', 'text'}: 76 | self.assertNotIn(field_not_in_check, TestModelSerializer.__fields__) 77 | 78 | # FK field nested 79 | self.assertTrue(issubclass(TestModelSerializer.__fields__['related'].type_, PwPdModel)) 80 | 81 | # Backref type is List[Serializer] 82 | self.assertIs(TestModelSerializer.__fields__['childs'].shape, pd.fields.SHAPE_LIST) 83 | self.assertTrue(issubclass(TestModelSerializer.__fields__['childs'].type_, PwPdModel)) 84 | 85 | class TestModelSerializer(pd.BaseModel, metaclass=PwPdMeta): 86 | class Config: 87 | pw_model = TestModel 88 | pw_exclude_pk = True 89 | pw_nest_fk = False 90 | pw_nest_backrefs = False 91 | 92 | # All fields are in serializer by default 93 | model_fields = set(TestModel._meta.fields.keys()) 94 | model_fields.remove('id') # pw_exclude_pk = True 95 | model_fields.remove('related') # pw_nest_fk = False 96 | model_fields.add('related_id') 97 | 98 | for field_in_check in model_fields: 99 | self.assertIn(field_in_check, TestModelSerializer.__fields__) 100 | 101 | # FK field not nested, just an ID 102 | self.assertIs(TestModelSerializer.__fields__['related_id'].type_, int) 103 | 104 | # Backref not included 105 | self.assertNotIn('childs', TestModelSerializer.__fields__) 106 | 107 | 108 | class PwPdModelTestCase(TestCase): 109 | def setUp(self): 110 | self.tm_amount = 3 111 | self.child_tm_amount = 4 112 | create_dummy_data() 113 | 114 | def tearDown(self): 115 | DB.drop_tables((ParentTestModel, TestModel, ChildTestModel)) 116 | 117 | def test_serialization(self): 118 | class TestModelSerializer(PwPdModel): 119 | class Config: 120 | pw_model = TestModel 121 | pw_nest_fk = True 122 | pw_nest_backrefs = True 123 | 124 | test_model = TestModel.select().order_by('?').first() 125 | test_model_dict = model_to_dict(test_model) 126 | serialized = TestModelSerializer.from_orm(test_model) 127 | serialized_dict = serialized.dict() 128 | 129 | # check values equal 130 | for field_check in TestModel._meta.fields.keys(): 131 | self.assertEqual(test_model_dict[field_check], serialized_dict[field_check]) 132 | 133 | # check backref 134 | self.assertEqual(test_model.childs.count(), len(serialized_dict['childs'])) 135 | 136 | def test_make_serializer(self): 137 | serializer = PwPdModel.make_serializer(TestModel) 138 | test_model = TestModel.select().order_by('?').first() 139 | test_model_dict = model_to_dict(test_model) 140 | test_model_dict['related_id'] = test_model_dict.pop('related')['id'] # FK no nested, just ID 141 | serialized = serializer.from_orm(test_model) 142 | serialized_dict = serialized.dict() 143 | 144 | # check values equal 145 | for field_check in test_model_dict.keys(): 146 | self.assertEqual(test_model_dict[field_check], serialized_dict[field_check]) 147 | 148 | serializer = PwPdModel.make_serializer(TestModel, pw_fields={'id'}) 149 | serialized = serializer.from_orm(test_model) 150 | 151 | self.assertListEqual(['id'], list(serialized.dict().keys())) 152 | 153 | with self.assertRaises(ValueError): 154 | PwPdModel.make_serializer(TestModel, pw_model=ParentTestModel) 155 | 156 | 157 | class PwPdModelFactoryTestCase(TestCase): 158 | def setUp(self): 159 | create_dummy_data() 160 | self.test_model_pwpd = PwPdModelFactory(TestModel) 161 | 162 | def test_read_pd(self): 163 | test_model = TestModel.select().order_by('?').first() 164 | test_model_dict = model_to_dict(test_model) 165 | test_model_dict['related_id'] = test_model_dict.pop('related')['id'] # FK no nested, just ID 166 | serialized = self.test_model_pwpd.read_pd.from_orm(test_model) 167 | serialized_dict = serialized.dict() 168 | 169 | # check values equal 170 | for field_check in test_model_dict.keys(): 171 | self.assertEqual(test_model_dict[field_check], serialized_dict[field_check]) 172 | 173 | def test_write_pd(self): 174 | valid_data = { 175 | 'text': 'Cucumber', 176 | 'number': 300, 177 | 'is_test': False, 178 | 'related_id': ParentTestModel.select().first().id, 179 | } 180 | 181 | self.test_model_pwpd.write_pd(**valid_data) 182 | 183 | minimal_valid_data = { 184 | 'text': 'Cucumber', 185 | 'related_id': ParentTestModel.select().first().id, 186 | } 187 | expected_data = { 188 | 'text': 'Cucumber', 189 | 'number': None, 190 | 'is_test': True, 191 | 'related_id': ParentTestModel.select().first().id, 192 | } 193 | 194 | data = self.test_model_pwpd.write_pd(**minimal_valid_data) 195 | 196 | self.assertDictEqual(data.dict(), expected_data) 197 | 198 | invalid_data = { 199 | 'id': 1, 200 | 'text': 23, 201 | 'is_test': None, 202 | } 203 | expected_errors = { 204 | 'id': 'value_error.extra', 205 | 'is_test': 'type_error.none.not_allowed', 206 | 'related_id': 'value_error.missing', 207 | } 208 | 209 | with self.assertRaises(pd.ValidationError) as raised: 210 | self.test_model_pwpd.write_pd(**invalid_data) 211 | 212 | raised_errors = dict() 213 | for error in raised.exception.errors(): 214 | raised_errors[error['loc'][0]] = error['type'] 215 | 216 | self.assertDictEqual(raised_errors, expected_errors) 217 | -------------------------------------------------------------------------------- /mkdocs/docs/index.md: -------------------------------------------------------------------------------- 1 | # FastAPIwee 2 | 3 | ![Logo](media/logo.png) 4 | 5 |

6 | FastAPI + PeeWee = <3 7 |

8 | 9 | Made for [FastAPI web framework](https://fastapi.tiangolo.com) and [PeeWee ORM](http://docs.peewee-orm.com/en/latest/). 10 | 11 | A fast and simple (I hope) way to create REST API based on PeeWee models. 12 | 13 | ## Requirements 14 | 15 | Python 3.6+ 16 | 17 | - [FastAPI](https://fastapi.tiangolo.com) 18 | - [Peewee](http://docs.peewee-orm.com/en/latest/) 19 | 20 | ## Installation 21 | 22 |
 23 | 
 24 | ```bash
 25 | $ pip install FastAPIwee
 26 | 
 27 | ---> 100%
 28 | ```
 29 | 
 30 | 
31 | 32 | ## Example 33 | 34 | ### Prepare models 35 | 36 | - Define Peewee models in example.py: 37 | 38 | ```python 39 | import peewee as pw 40 | 41 | DB = pw.SqliteDatabase('/tmp/fastapiwee_example.db') 42 | 43 | 44 | class TestModel(pw.Model): 45 | id = pw.AutoField() 46 | text = pw.TextField() 47 | number = pw.IntegerField(null=True) 48 | is_test = pw.BooleanField(default=True) 49 | 50 | class Meta: 51 | database = DB 52 | 53 | 54 | class AnotherModel(pw.Model): 55 | id = pw.AutoField() 56 | text = pw.TextField(default='Cucumber') 57 | 58 | class Meta: 59 | database = DB 60 | ``` 61 | 62 | ### Add a dash of FastAPI 63 | 64 | - Create FastAPI app and add CRUD APIs for models: 65 | 66 | ```python hl_lines="2-4 27-30" 67 | import peewee as pw 68 | from fastapi import FastAPI 69 | 70 | from fastapiwee import AutoFastAPIViewSet 71 | 72 | DB = pw.SqliteDatabase('/tmp/fastapiwee_example.db') 73 | 74 | 75 | class TestModel(pw.Model): 76 | id = pw.AutoField() 77 | text = pw.TextField() 78 | number = pw.IntegerField(null=True) 79 | is_test = pw.BooleanField(default=True) 80 | 81 | class Meta: 82 | database = DB 83 | 84 | 85 | class AnotherModel(pw.Model): 86 | id = pw.AutoField() 87 | text = pw.TextField(default='Cucumber') 88 | 89 | class Meta: 90 | database = DB 91 | 92 | 93 | app = FastAPI() 94 | 95 | AutoFastAPIViewSet(TestModel, app) 96 | AutoFastAPIViewSet(AnotherModel, app, actions={'create', 'list'}) 97 | ``` 98 | 99 | ### Shake well and serve 100 | 101 | - Start a server: 102 | 103 | ```bash 104 | uvicorn example:app --reload 105 | ``` 106 | 107 | ### Try it 108 | 109 | - In your terminal, use curl: 110 | 111 | Create a new shiny TestModel 112 | 113 |
114 | 
115 | ```bash
116 | $ curl -i -X 'POST' \
117 | >    'http://127.0.0.1:8000/test_model/' \
118 | >    -d '{
119 | >        "text": "Cucumber",
120 | >        "number": 33,
121 | >        "is_test": true
122 | >    }'
123 | 
124 | HTTP/1.1 200 OK
125 | date: Wed, 21 Apr 2021 07:20:23 GMT
126 | server: uvicorn
127 | content-length: 54
128 | content-type: application/json
129 | 
130 | {"id":1,"text":"Cucumber","number":33,"is_test":true}
131 | ```
132 | 
133 | 
134 | 135 | Let's have one more 136 | 137 |
138 | 
139 | ```bash
140 | $ curl -i -X 'POST' \
141 | >    'http://127.0.0.1:8000/test_model/' \
142 | >    -d '{"text": "Magic"}'
143 | 
144 | HTTP/1.1 200 OK
145 | date: Wed, 21 Apr 2021 07:51:31 GMT
146 | server: uvicorn
147 | content-length: 53
148 | content-type: application/json
149 | 
150 | {"id":2,"text":"Magic","number":null,"is_test":true}
151 | ```
152 | 
153 | 
154 | 155 | Let's see how they are doing. _(`python -m json.tool` is used to prettify JSON output)_ 156 | 157 |
158 | 
159 | ```bash
160 | $ curl -s -X 'GET' \
161 | >    'http://127.0.0.1:8000/test_model/' \
162 | >    | python -m json.tool
163 | 
164 | [
165 |     {
166 |         "id": 1,
167 |         "is_test": true,
168 |         "number": 33,
169 |         "text": "Cucumber"
170 |     },
171 |     {
172 |         "id": 2,
173 |         "is_test": true,
174 |         "number": null,
175 |         "text": "Magic"
176 |     }
177 | ]
178 | ```
179 | 
180 | 
181 | 182 | ### Interactive API docs 183 | 184 | Thanks to power of the FastAPI, you can go to [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs). 185 | 186 | You will see the automatic interactive API documentation (provided by [Swagger UI](https://github.com/swagger-api/swagger-ui)). 187 | 188 | Or you can go to [http://127.0.0.1:8000/redoc](http://127.0.0.1:8000/redoc). 189 | 190 | And see the alternative automatic documentation (provided by [ReDoc](https://github.com/Redocly/redoc)). 191 | 192 | ## License 193 | 194 | This project is licensed under the terms of the MIT license. 195 | -------------------------------------------------------------------------------- /mkdocs/docs/media/css/termynal.css: -------------------------------------------------------------------------------- 1 | /** 2 | * termynal.js 3 | * 4 | * @author Ines Montani 5 | * @version 0.0.1 6 | * @license MIT 7 | */ 8 | 9 | :root { 10 | --color-bg: #252a33; 11 | --color-text: #eee; 12 | --color-text-subtle: #a2a2a2; 13 | } 14 | 15 | [data-termynal] { 16 | width: 750px; 17 | max-width: 100%; 18 | background: var(--color-bg); 19 | color: var(--color-text); 20 | font-size: 18px; 21 | font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; 22 | border-radius: 4px; 23 | padding: 75px 45px 35px; 24 | position: relative; 25 | -webkit-box-sizing: border-box; 26 | box-sizing: border-box; 27 | } 28 | 29 | [data-termynal]:before { 30 | content: ''; 31 | position: absolute; 32 | top: 15px; 33 | left: 15px; 34 | display: inline-block; 35 | width: 15px; 36 | height: 15px; 37 | border-radius: 50%; 38 | /* A little hack to display the window buttons in one pseudo element. */ 39 | background: #d9515d; 40 | -webkit-box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930; 41 | box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930; 42 | } 43 | 44 | [data-termynal]:after { 45 | content: 'bash'; 46 | position: absolute; 47 | color: var(--color-text-subtle); 48 | top: 5px; 49 | left: 0; 50 | width: 100%; 51 | text-align: center; 52 | } 53 | 54 | [data-ty] { 55 | display: block; 56 | line-height: 2; 57 | } 58 | 59 | [data-ty]:before { 60 | /* Set up defaults and ensure empty lines are displayed. */ 61 | content: ''; 62 | display: inline-block; 63 | vertical-align: middle; 64 | } 65 | 66 | [data-ty="input"]:before, 67 | [data-ty-prompt]:before { 68 | margin-right: 0.75em; 69 | color: var(--color-text-subtle); 70 | } 71 | 72 | [data-ty="input"]:before { 73 | content: '$'; 74 | } 75 | 76 | [data-ty][data-ty-prompt]:before { 77 | content: attr(data-ty-prompt); 78 | } 79 | 80 | [data-ty-cursor]:after { 81 | content: attr(data-ty-cursor); 82 | font-family: monospace; 83 | margin-left: 0.5em; 84 | -webkit-animation: blink 1s infinite; 85 | animation: blink 1s infinite; 86 | } 87 | 88 | 89 | /* Cursor animation */ 90 | 91 | @-webkit-keyframes blink { 92 | 50% { 93 | opacity: 0; 94 | } 95 | } 96 | 97 | @keyframes blink { 98 | 50% { 99 | opacity: 0; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /mkdocs/docs/media/js/custom.js: -------------------------------------------------------------------------------- 1 | function setupTermynal() { 2 | document.querySelectorAll(".use-termynal").forEach(node => { 3 | node.style.display = "block"; 4 | new Termynal(node, { 5 | lineDelay: 500 6 | }); 7 | }); 8 | const progressLiteralStart = "---> 100%"; 9 | const promptLiteralStart = "$ "; 10 | const continuationLiteralStart = "> "; 11 | const customPromptLiteralStart = "# "; 12 | const termynalActivateClass = "termy"; 13 | let termynals = []; 14 | 15 | function createTermynals() { 16 | document 17 | .querySelectorAll(`.${termynalActivateClass} .highlight`) 18 | .forEach(node => { 19 | const text = node.textContent; 20 | const lines = text.split("\n"); 21 | const useLines = []; 22 | let buffer = []; 23 | function saveBuffer() { 24 | if (buffer.length) { 25 | let isBlankSpace = true; 26 | buffer.forEach(line => { 27 | if (line) { 28 | isBlankSpace = false; 29 | } 30 | }); 31 | dataValue = {}; 32 | if (isBlankSpace) { 33 | dataValue["delay"] = 0; 34 | } 35 | if (buffer[buffer.length - 1] === "") { 36 | // A last single
won't have effect 37 | // so put an additional one 38 | buffer.push(""); 39 | } 40 | const bufferValue = buffer.join("
"); 41 | dataValue["value"] = bufferValue; 42 | useLines.push(dataValue); 43 | buffer = []; 44 | } 45 | } 46 | for (let line of lines) { 47 | if (line === progressLiteralStart) { 48 | saveBuffer(); 49 | useLines.push({ 50 | type: "progress" 51 | }); 52 | } else if (line.startsWith(promptLiteralStart)) { 53 | saveBuffer(); 54 | const value = line.replace(promptLiteralStart, "").trimEnd(); 55 | useLines.push({ 56 | type: "input", 57 | value: value 58 | }); 59 | } else if (line.startsWith(continuationLiteralStart)) { 60 | saveBuffer(); 61 | const value = line.replace(continuationLiteralStart, "").trimEnd(); 62 | useLines.push({ 63 | type: "input", 64 | prompt: continuationLiteralStart, 65 | value: value 66 | }); 67 | } else if (line.startsWith("// ")) { 68 | saveBuffer(); 69 | const value = "💬 " + line.replace("// ", "").trimEnd(); 70 | useLines.push({ 71 | value: value, 72 | class: "termynal-comment", 73 | delay: 0 74 | }); 75 | } else if (line.startsWith(customPromptLiteralStart)) { 76 | saveBuffer(); 77 | const promptStart = line.indexOf(promptLiteralStart); 78 | if (promptStart === -1) { 79 | console.error("Custom prompt found but no end delimiter", line) 80 | } 81 | const prompt = line.slice(0, promptStart).replace(customPromptLiteralStart, "") 82 | let value = line.slice(promptStart + promptLiteralStart.length); 83 | useLines.push({ 84 | type: "input", 85 | value: value, 86 | prompt: prompt 87 | }); 88 | } else { 89 | buffer.push(line); 90 | } 91 | } 92 | saveBuffer(); 93 | const div = document.createElement("div"); 94 | node.replaceWith(div); 95 | const termynal = new Termynal(div, { 96 | lineData: useLines, 97 | noInit: true, 98 | lineDelay: 300, 99 | typeDelay: 30, 100 | }); 101 | termynals.push(termynal); 102 | }); 103 | } 104 | 105 | function loadVisibleTermynals() { 106 | termynals = termynals.filter(termynal => { 107 | if (termynal.container.getBoundingClientRect().top - innerHeight <= 0) { 108 | termynal.init(); 109 | return false; 110 | } 111 | return true; 112 | }); 113 | } 114 | window.addEventListener("scroll", loadVisibleTermynals); 115 | createTermynals(); 116 | loadVisibleTermynals(); 117 | } 118 | 119 | setupTermynal(); 120 | -------------------------------------------------------------------------------- /mkdocs/docs/media/js/termynal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * termynal.js 3 | * A lightweight, modern and extensible animated terminal window, using 4 | * async/await. 5 | * 6 | * @author Ines Montani 7 | * @version 0.0.1 8 | * @license MIT 9 | */ 10 | 11 | 'use strict'; 12 | 13 | /** Generate a terminal widget. */ 14 | class Termynal { 15 | /** 16 | * Construct the widget's settings. 17 | * @param {(string|Node)=} container - Query selector or container element. 18 | * @param {Object=} options - Custom settings. 19 | * @param {string} options.prefix - Prefix to use for data attributes. 20 | * @param {number} options.startDelay - Delay before animation, in ms. 21 | * @param {number} options.typeDelay - Delay between each typed character, in ms. 22 | * @param {number} options.lineDelay - Delay between each line, in ms. 23 | * @param {number} options.progressLength - Number of characters displayed as progress bar. 24 | * @param {string} options.progressChar – Character to use for progress bar, defaults to █. 25 | * @param {number} options.progressPercent - Max percent of progress. 26 | * @param {string} options.cursor – Character to use for cursor, defaults to ▋. 27 | * @param {Object[]} lineData - Dynamically loaded line data objects. 28 | * @param {boolean} options.noInit - Don't initialise the animation. 29 | */ 30 | constructor(container = '#termynal', options = {}) { 31 | this.container = (typeof container === 'string') ? document.querySelector(container) : container; 32 | this.pfx = `data-${options.prefix || 'ty'}`; 33 | this.originalStartDelay = this.startDelay = options.startDelay 34 | || parseFloat(this.container.getAttribute(`${this.pfx}-startDelay`)) || 600; 35 | this.originalTypeDelay = this.typeDelay = options.typeDelay 36 | || parseFloat(this.container.getAttribute(`${this.pfx}-typeDelay`)) || 90; 37 | this.originalLineDelay = this.lineDelay = options.lineDelay 38 | || parseFloat(this.container.getAttribute(`${this.pfx}-lineDelay`)) || 1500; 39 | this.progressLength = options.progressLength 40 | || parseFloat(this.container.getAttribute(`${this.pfx}-progressLength`)) || 40; 41 | this.progressChar = options.progressChar 42 | || this.container.getAttribute(`${this.pfx}-progressChar`) || '█'; 43 | this.progressPercent = options.progressPercent 44 | || parseFloat(this.container.getAttribute(`${this.pfx}-progressPercent`)) || 100; 45 | this.cursor = options.cursor 46 | || this.container.getAttribute(`${this.pfx}-cursor`) || '▋'; 47 | this.lineData = this.lineDataToElements(options.lineData || []); 48 | this.loadLines() 49 | if (!options.noInit) this.init() 50 | } 51 | 52 | loadLines() { 53 | // Load all the lines and create the container so that the size is fixed 54 | // Otherwise it would be changing and the user viewport would be constantly 55 | // moving as she/he scrolls 56 | const finish = this.generateFinish() 57 | finish.style.visibility = 'hidden' 58 | this.container.appendChild(finish) 59 | // Appends dynamically loaded lines to existing line elements. 60 | this.lines = [...this.container.querySelectorAll(`[${this.pfx}]`)].concat(this.lineData); 61 | for (let line of this.lines) { 62 | line.style.visibility = 'hidden' 63 | this.container.appendChild(line) 64 | } 65 | const restart = this.generateRestart() 66 | restart.style.visibility = 'hidden' 67 | this.container.appendChild(restart) 68 | this.container.setAttribute('data-termynal', ''); 69 | } 70 | 71 | /** 72 | * Initialise the widget, get lines, clear container and start animation. 73 | */ 74 | init() { 75 | /** 76 | * Calculates width and height of Termynal container. 77 | * If container is empty and lines are dynamically loaded, defaults to browser `auto` or CSS. 78 | */ 79 | const containerStyle = getComputedStyle(this.container); 80 | this.container.style.width = containerStyle.width !== '0px' ? 81 | containerStyle.width : undefined; 82 | this.container.style.minHeight = containerStyle.height !== '0px' ? 83 | containerStyle.height : undefined; 84 | 85 | this.container.setAttribute('data-termynal', ''); 86 | this.container.innerHTML = ''; 87 | for (let line of this.lines) { 88 | line.style.visibility = 'visible' 89 | } 90 | this.start(); 91 | } 92 | 93 | /** 94 | * Start the animation and rener the lines depending on their data attributes. 95 | */ 96 | async start() { 97 | this.addFinish() 98 | await this._wait(this.startDelay); 99 | 100 | for (let line of this.lines) { 101 | const type = line.getAttribute(this.pfx); 102 | const delay = line.getAttribute(`${this.pfx}-delay`) || this.lineDelay; 103 | 104 | if (type == 'input') { 105 | line.setAttribute(`${this.pfx}-cursor`, this.cursor); 106 | await this.type(line); 107 | await this._wait(delay); 108 | } 109 | 110 | else if (type == 'progress') { 111 | await this.progress(line); 112 | await this._wait(delay); 113 | } 114 | 115 | else { 116 | this.container.appendChild(line); 117 | await this._wait(delay); 118 | } 119 | 120 | line.removeAttribute(`${this.pfx}-cursor`); 121 | } 122 | this.addRestart() 123 | this.finishElement.style.visibility = 'hidden' 124 | this.lineDelay = this.originalLineDelay 125 | this.typeDelay = this.originalTypeDelay 126 | this.startDelay = this.originalStartDelay 127 | } 128 | 129 | generateRestart() { 130 | const restart = document.createElement('a') 131 | restart.onclick = (e) => { 132 | e.preventDefault() 133 | this.container.innerHTML = '' 134 | this.init() 135 | } 136 | restart.href = '#' 137 | restart.setAttribute('data-terminal-control', '') 138 | restart.innerHTML = "restart ↻" 139 | return restart 140 | } 141 | 142 | generateFinish() { 143 | const finish = document.createElement('a') 144 | finish.onclick = (e) => { 145 | e.preventDefault() 146 | this.lineDelay = 0 147 | this.typeDelay = 0 148 | this.startDelay = 0 149 | } 150 | finish.href = '#' 151 | finish.setAttribute('data-terminal-control', '') 152 | finish.innerHTML = "fast →" 153 | this.finishElement = finish 154 | return finish 155 | } 156 | 157 | addRestart() { 158 | const restart = this.generateRestart() 159 | this.container.appendChild(restart) 160 | } 161 | 162 | addFinish() { 163 | const finish = this.generateFinish() 164 | this.container.appendChild(finish) 165 | } 166 | 167 | /** 168 | * Animate a typed line. 169 | * @param {Node} line - The line element to render. 170 | */ 171 | async type(line) { 172 | const chars = [...line.textContent]; 173 | line.textContent = ''; 174 | this.container.appendChild(line); 175 | 176 | for (let char of chars) { 177 | const delay = line.getAttribute(`${this.pfx}-typeDelay`) || this.typeDelay; 178 | await this._wait(delay); 179 | line.textContent += char; 180 | } 181 | } 182 | 183 | /** 184 | * Animate a progress bar. 185 | * @param {Node} line - The line element to render. 186 | */ 187 | async progress(line) { 188 | const progressLength = line.getAttribute(`${this.pfx}-progressLength`) 189 | || this.progressLength; 190 | const progressChar = line.getAttribute(`${this.pfx}-progressChar`) 191 | || this.progressChar; 192 | const chars = progressChar.repeat(progressLength); 193 | const progressPercent = line.getAttribute(`${this.pfx}-progressPercent`) 194 | || this.progressPercent; 195 | line.textContent = ''; 196 | this.container.appendChild(line); 197 | 198 | for (let i = 1; i < chars.length + 1; i++) { 199 | await this._wait(this.typeDelay); 200 | const percent = Math.round(i / chars.length * 100); 201 | line.textContent = `${chars.slice(0, i)} ${percent}%`; 202 | if (percent>progressPercent) { 203 | break; 204 | } 205 | } 206 | } 207 | 208 | /** 209 | * Helper function for animation delays, called with `await`. 210 | * @param {number} time - Timeout, in ms. 211 | */ 212 | _wait(time) { 213 | return new Promise(resolve => setTimeout(resolve, time)); 214 | } 215 | 216 | /** 217 | * Converts line data objects into line elements. 218 | * 219 | * @param {Object[]} lineData - Dynamically loaded lines. 220 | * @param {Object} line - Line data object. 221 | * @returns {Element[]} - Array of line elements. 222 | */ 223 | lineDataToElements(lineData) { 224 | return lineData.map(line => { 225 | let div = document.createElement('div'); 226 | div.innerHTML = `${line.value || ''}`; 227 | 228 | return div.firstElementChild; 229 | }); 230 | } 231 | 232 | /** 233 | * Helper function for generating attributes string. 234 | * 235 | * @param {Object} line - Line data object. 236 | * @returns {string} - String of attributes. 237 | */ 238 | _attributes(line) { 239 | let attrs = ''; 240 | for (let prop in line) { 241 | // Custom add class 242 | if (prop === 'class') { 243 | attrs += ` class=${line[prop]} ` 244 | continue 245 | } 246 | if (prop === 'type') { 247 | attrs += `${this.pfx}="${line[prop]}" ` 248 | } else if (prop !== 'value') { 249 | attrs += `${this.pfx}-${prop}="${line[prop]}" ` 250 | } 251 | } 252 | 253 | return attrs; 254 | } 255 | } 256 | 257 | /** 258 | * HTML API: If current script has container(s) specified, initialise Termynal. 259 | */ 260 | if (document.currentScript.hasAttribute('data-termynal-container')) { 261 | const containers = document.currentScript.getAttribute('data-termynal-container'); 262 | containers.split('|') 263 | .forEach(container => new Termynal(container)) 264 | } 265 | -------------------------------------------------------------------------------- /mkdocs/docs/media/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ignisor/FastAPIwee/585a7c51575cdad84950d9b636d6f4887ff84afd/mkdocs/docs/media/logo.png -------------------------------------------------------------------------------- /mkdocs/docs/modules/CRUD/advanced/base.md: -------------------------------------------------------------------------------- 1 | # CRUD base 2 | 3 | `base` is a module with generic base classes for views. 4 | 5 | ## `FastAPIView` 6 | 7 | Abstract base of all views in that library. 8 | 9 | Static attributes: 10 | 11 | - `MODEL: pw.Model` - Peewee model for a view 12 | - `_RESPONSE_MODEL: Optional[pd.BaseModel]` - Pydantic model for a response 13 | - `URL: str` - Endpoint URL 14 | - `METHOD: str` - Endpoint method 15 | - `STATUS_CODE: int` - Default 200. Status code for a successful response 16 | 17 | Methods: 18 | 19 | - `__call__` - abstract method, must be implemented. Main place to put execution logic for a view 20 | - `_get_query` - Default: all model instances (`self.MODEL.select()`). Method to retrieve query. Useful to filter available objects. 21 | - `@property response_model` - Property to retrieve Pydantic model for a response. 22 | - `_get_api_route_params` - Method to retrieve FastAPI route params. 23 | - `add_to_app(app: Union[FastAPI, APIRouter])` - Method to add endpoint to application. 24 | - `make_model_view(model: pw.Model)` - Class method to create new view for a `model` without explicitly defining a class. 25 | 26 | ## `BaseReadFastAPIView` 27 | 28 | Abstract base for "read" action views. Inherits `FastAPIView`. 29 | 30 | Methods: 31 | 32 | - `@property response_model` - If `_RESPONSE_MODEL` is not specified will make a `PwPdModel` for a specified `MODEL`. 33 | - `_get_instance(pk: Any)` - Method to retrieve instance from query by it's primary key. 34 | 35 | ## `BaseWriteFastAPIView` 36 | 37 | Abstract base for "write" action views. Inherits `BaseReadFastAPIView`. 38 | 39 | Static attributes: 40 | 41 | - `_SERIALIZER: Optional[pd.BaseModel]` - Pydantic model for a request body serialization. 42 | 43 | Attributes: 44 | 45 | - `_obj_data` - Instance of pydantic model (from `serializer` property) with data from request body. 46 | 47 | Methods: 48 | 49 | - `@property serializer` - If `_SERIALIZER` is not specified will make a `PwPdWriteModel` for a specified `MODEL`. 50 | - `create` - Method for create action. Will create an instance of `MODEL` in database with data from `_obj_data` attribute. 51 | - `update(pk: Any, partial: bool = False)` - Method for update action. Will create an update values of `MODEL` in database by it's primary key with data from `_obj_data` attribute. If `partial` is `True` will only use fields that were specified in the request. 52 | - `_get_api_route_params` - Extends default parameters with dependency to set `_obj_data` attribute. 53 | 54 | ## `BaseDeleteFastAPIView` 55 | 56 | Abstract base for "delete" action views. Inherits `BaseReadFastAPIView`. 57 | 58 | Static attributes: 59 | 60 | - `STATUS_CODE = 204` - `STATUS_CODE` set to [204 No Content](https://developer.mozilla.org/ru/docs/Web/HTTP/Status/204). Since by default no content will be returned in response. 61 | 62 | Methods: 63 | 64 | - `@property response_model` - Returns None, since no content in response will be returned. 65 | - `__calll__(pk: Any)` - Calls `delete` mothod. 66 | - `delete(pk: Any)` - Deletes a `MODEL` instance by it's primary key. Utilizes `_get_instance` from `BaseReadFastAPIView` to retrieve an instance. 67 | - `_get_api_route_params` - Removes `response_model` from default parameters. And sets `response_class` to `starlette.responses.Response` for empty response. 68 | -------------------------------------------------------------------------------- /mkdocs/docs/modules/CRUD/advanced/exceptions.md: -------------------------------------------------------------------------------- 1 | # Exceptions 2 | 3 | This module contains generic exception handlers. 4 | 5 | ## ExceptionHandler 6 | 7 | Abstract base for other exception handlers. 8 | 9 | Static attributes: 10 | 11 | - `EXCEPTION: Type[Exception]` - Exception to handle. 12 | 13 | Methods: 14 | 15 | - `__call__(request: Request, exc: Exception)` - Handler implementation goes here. Must return response object. 16 | - `@classmethod add_to_app(app: FastAPI)` - Method to add handler to the application. 17 | 18 | ### Example: 19 | 20 | ```python 21 | from fastapi import Request, Response 22 | from fastapi.responses import JSONResponse 23 | 24 | 25 | class CucumberTooSmall(Exception): 26 | pass 27 | 28 | 29 | class CucumberExceptionHandler(ExceptionHandler): 30 | EXCEPTION = CucumberTooSmall 31 | 32 | def __call__(self, request: Request, exc: Exception) -> Response: 33 | return JSONResponse( 34 | status_code=400, 35 | content={ 36 | 'msg': 'Cucumber is too small for that action!', 37 | 'type': 'cucumber_small', 38 | }, 39 | ) 40 | ``` 41 | 42 | ## NotFoundExceptionHandler 43 | 44 | Handles exception `peewee.DoesNotExist`, returns `JSONResponse` with status code `404` ([HTTP Not Found](https://developer.mozilla.org/docs/Web/HTTP/Status/404)). 45 | 46 | Implements `__call__` method. 47 | 48 | Added to the app automatically when `add_to_app` method on view or viewset used. **Will not be added when `add_to_app` view method used with `fastapi.Router`.** 49 | -------------------------------------------------------------------------------- /mkdocs/docs/modules/CRUD/advanced/views.md: -------------------------------------------------------------------------------- 1 | # Views 2 | 3 | Module with generic base views for all CRUD actions: create, retrieve, list, update, partial update and delete. 4 | 5 | Any of those views can be created simply using `make_model_view`: 6 | ```python 7 | view = RetrieveFastAPIView.make_model_view(peewee.Model) 8 | view.add_to_app(FastAPI) 9 | ``` 10 | 11 | ## RetrieveFastAPIView 12 | 13 | View that will return object data by it's primary key. Inherits `BaseReadFastAPIView`. 14 | 15 | Static attributes: 16 | 17 | - `METHOD` - `'GET'` 18 | - `URL` - `'/{pk}/'` model primary key (`pk`) as URL parameter. 19 | 20 | Implements: 21 | 22 | - `__call__(pk: Any)` - accepts primary key from URL parameter. Utilizes `_get_instance` from `BaseReadFastAPIView` to retrieve instance. 23 | 24 | ### Example 25 | 26 | ```python 27 | import peewee as pw 28 | import pydantic as pd 29 | from fastapi import FastAPI 30 | from fastapiwee.crud.views import RetrieveFastAPIView 31 | from fastapiwee.pwpd import PwPdModel 32 | 33 | DB = pw.SqliteDatabase(':memory:') 34 | 35 | 36 | class Cucumber(pw.Model): 37 | id = pw.AutoField() 38 | size = pw.IntegerField() 39 | taste = pw.CharField(null=True) 40 | 41 | class Meta: 42 | database = DB 43 | 44 | 45 | class CucumberData(PwPdModel): 46 | class Config: 47 | pw_model = Cucumber 48 | 49 | @pd.validator('size') 50 | def validate_size(cls, v, **kwargs): 51 | """Make sure size is big enough""" 52 | return v ** 2 53 | 54 | 55 | class CucumberRetrieveView(RetrieveFastAPIView): 56 | MODEL = Cucumber 57 | _RESPONSE_MODEL = CucumberData 58 | 59 | def _get_instance(self, pk: int) -> pw.Model: 60 | """Mock data to not use a database""" 61 | return Cucumber( 62 | id=pk, 63 | size=10, 64 | taste='tasty', 65 | ) 66 | 67 | 68 | app = FastAPI() 69 | 70 | CucumberRetrieveView().add_to_app(app) 71 | ``` 72 | 73 | ## ListFastAPIView 74 | 75 | View that will return list of all objects from `_get_query` method. Inherits `BaseReadFastAPIView`. 76 | 77 | Static attributes: 78 | 79 | - `METHOD` - `'GET'` 80 | - `URL` - `'/'` 81 | 82 | Implements: 83 | 84 | - `__call__` - Utilizes `_get_query` from `FastAPIView` to retrieve query and converts it to list. 85 | - `@property response_model` - wraps base `response_model` to List. 86 | 87 | ### Example 88 | 89 | ```python 90 | import peewee as pw 91 | import pydantic as pd 92 | from fastapi import FastAPI 93 | from fastapiwee.crud.views import ListFastAPIView 94 | from fastapiwee.pwpd import PwPdModel 95 | 96 | DB = pw.SqliteDatabase(':memory:') 97 | 98 | 99 | class Cucumber(pw.Model): 100 | id = pw.AutoField() 101 | size = pw.IntegerField() 102 | taste = pw.CharField(null=True) 103 | 104 | class Meta: 105 | database = DB 106 | 107 | 108 | class CucumberData(PwPdModel): 109 | class Config: 110 | pw_model = Cucumber 111 | 112 | @pd.validator('size') 113 | def validate_size(cls, v, **kwargs): 114 | """Make sure size is big enough""" 115 | return v ** 2 116 | 117 | 118 | class CucumberListView(ListFastAPIView): 119 | MODEL = Cucumber 120 | _RESPONSE_MODEL = CucumberData 121 | 122 | def _get_query(self): 123 | """Mock data to not use a database""" 124 | return (Cucumber( 125 | id=i, 126 | size=10 + i, 127 | taste='tasty', 128 | ) for i in range(10)) 129 | 130 | 131 | app = FastAPI() 132 | 133 | CucumberListView().add_to_app(app) 134 | ``` 135 | 136 | ## CreateFastAPIView 137 | 138 | View that will create new object. Inherits `BaseWriteFastAPIView`. 139 | 140 | Static attributes: 141 | 142 | - `METHOD` - `'POST'` 143 | - `URL` - `'/'` 144 | - `STATUS_CODE` - [`201` Created](https://developer.mozilla.org/ru/docs/Web/HTTP/Status/201) 145 | 146 | Implements: 147 | 148 | - `__call__` - Calls `create` method. Default implementation in `BaseWriteFastAPIView`. 149 | 150 | ### Example 151 | 152 | ```python 153 | import peewee as pw 154 | import pydantic as pd 155 | from fastapi import FastAPI 156 | from fastapiwee.crud.views import CreateFastAPIView 157 | from fastapiwee.pwpd import PwPdWriteModel 158 | 159 | DB = pw.SqliteDatabase(':memory:') 160 | 161 | 162 | class Cucumber(pw.Model): 163 | id = pw.AutoField() 164 | size = pw.IntegerField() 165 | taste = pw.CharField(null=True) 166 | 167 | class Meta: 168 | database = DB 169 | 170 | 171 | class CucumberData(PwPdWriteModel): 172 | class Config: 173 | pw_model = Cucumber 174 | 175 | @pd.validator('size') 176 | def validate_size(cls, v, **kwargs): 177 | """Make sure size is big enough""" 178 | if v < 10: 179 | raise ValueError(f'Size {v} is too small. Should be at least 10') 180 | return v 181 | 182 | 183 | class CucumberCreateView(CreateFastAPIView): 184 | MODEL = Cucumber 185 | _SERIALIZER = CucumberData 186 | 187 | def create(self) -> pw.Model: 188 | """Create DB tables before execution""" 189 | DB.create_tables([Cucumber]) 190 | return super().create() 191 | 192 | 193 | app = FastAPI() 194 | 195 | CucumberCreateView().add_to_app(app) 196 | ``` 197 | 198 | ## UpdateFastAPIView 199 | 200 | View that will update all fields of the object. Inherits `BaseWriteFastAPIView`. 201 | 202 | Static attributes: 203 | 204 | - `METHOD` - `'PUT'` 205 | - `URL` - `'/{pk}/'` model primary key (`pk`) as URL parameter. 206 | 207 | Implements: 208 | 209 | - `__call__` - Calls `update` method. Default implementation in `BaseWriteFastAPIView`. 210 | 211 | ### Example 212 | 213 | ```python 214 | import peewee as pw 215 | import pydantic as pd 216 | from fastapi import FastAPI 217 | from fastapiwee.crud.views import UpdateFastAPIView 218 | from fastapiwee.pwpd import PwPdWriteModel 219 | 220 | DB = pw.SqliteDatabase(':memory:') 221 | 222 | 223 | class Cucumber(pw.Model): 224 | id = pw.AutoField() 225 | size = pw.IntegerField() 226 | taste = pw.CharField(null=True) 227 | 228 | class Meta: 229 | database = DB 230 | 231 | 232 | class CucumberData(PwPdWriteModel): 233 | class Config: 234 | pw_model = Cucumber 235 | 236 | @pd.validator('size') 237 | def validate_size(cls, v, **kwargs): 238 | """Make sure size is big enough""" 239 | if v < 10: 240 | raise ValueError(f'Size {v} is too small. Should be at least 10') 241 | return v 242 | 243 | 244 | class CucumberUpdateView(UpdateFastAPIView): 245 | MODEL = Cucumber 246 | _SERIALIZER = CucumberData 247 | 248 | def __call__(self, pk): 249 | """Create DB tables and dummy data before execution""" 250 | DB.create_tables([Cucumber]) 251 | 252 | for i in range(10): 253 | Cucumber.create( 254 | size=i, 255 | taste='tasty', 256 | ) 257 | 258 | return super().__call__(pk) 259 | 260 | 261 | app = FastAPI() 262 | 263 | CucumberUpdateView().add_to_app(app) 264 | ``` 265 | 266 | ## PartialUpdateFastAPIView 267 | 268 | View that will update specified fields of the object (all fields are not required). Inherits `BaseWriteFastAPIView`. 269 | 270 | Static attributes: 271 | 272 | - `METHOD` - `'PATCH'` 273 | - `URL` - `'/{pk}/'` model primary key (`pk`) as URL parameter. 274 | 275 | Implements: 276 | 277 | - `__call__` - Calls `update` method with argument `partial = True`. Default implementation in `BaseWriteFastAPIView`. 278 | - `@property serializer` - If `_SERIALIZER` is not specified will make a `PwPdPartUpdateModel` for a specified `MODEL`. 279 | 280 | ### Example 281 | 282 | ```python 283 | import peewee as pw 284 | import pydantic as pd 285 | from fastapi import FastAPI 286 | from fastapiwee.crud.views import PartialUpdateFastAPIView 287 | from fastapiwee.pwpd import PwPdPartUpdateModel 288 | 289 | DB = pw.SqliteDatabase(':memory:') 290 | 291 | 292 | class Cucumber(pw.Model): 293 | id = pw.AutoField() 294 | size = pw.IntegerField() 295 | taste = pw.CharField(null=True) 296 | 297 | class Meta: 298 | database = DB 299 | 300 | 301 | class CucumberData(PwPdPartUpdateModel): 302 | class Config: 303 | pw_model = Cucumber 304 | 305 | @pd.validator('size') 306 | def validate_size(cls, v, **kwargs): 307 | """Make sure size is big enough""" 308 | if v < 10: 309 | raise ValueError(f'Size {v} is too small. Should be at least 10') 310 | return v 311 | 312 | 313 | class CucumberPartUpdateView(PartialUpdateFastAPIView): 314 | MODEL = Cucumber 315 | _SERIALIZER = CucumberData 316 | 317 | def __call__(self, pk): 318 | """Create DB tables and dummy data before execution""" 319 | DB.create_tables([Cucumber]) 320 | 321 | for i in range(10): 322 | Cucumber.create( 323 | size=i, 324 | taste='tasty', 325 | ) 326 | 327 | return super().__call__(pk) 328 | 329 | 330 | app = FastAPI() 331 | 332 | CucumberPartUpdateView().add_to_app(app) 333 | ``` 334 | 335 | ## DeleteFastAPIView 336 | 337 | View that will delete object by it's primary key. Inherits `BaseDeleteFastAPIView`. 338 | 339 | Static attributes: 340 | 341 | - `METHOD` - `'DELETE'` 342 | - `URL` - `'/{pk}/'` model primary key (`pk`) as URL parameter. 343 | 344 | ### Example 345 | 346 | ```python 347 | import peewee as pw 348 | from fastapi import FastAPI, HTTPException 349 | from fastapiwee.crud.views import DeleteFastAPIView 350 | 351 | DB = pw.SqliteDatabase(':memory:') 352 | 353 | 354 | class Cucumber(pw.Model): 355 | id = pw.AutoField() 356 | size = pw.IntegerField() 357 | taste = pw.CharField(null=True) 358 | 359 | class Meta: 360 | database = DB 361 | 362 | 363 | class CucumberDeleteView(DeleteFastAPIView): 364 | MODEL = Cucumber 365 | 366 | def __call__(self, pk): 367 | """Create DB tables and dummy data before execution""" 368 | DB.create_tables([Cucumber]) 369 | 370 | for i in range(20): 371 | Cucumber.create( 372 | size=i, 373 | taste='tasty', 374 | ) 375 | 376 | return super().__call__(pk) 377 | 378 | def delete(self, pk): 379 | instance = self._get_instance(pk) 380 | if instance.size > 10: 381 | raise HTTPException(400, 'That Cucumber is too big for you to delete it') 382 | instance.delete_instance() 383 | 384 | 385 | app = FastAPI() 386 | 387 | CucumberDeleteView().add_to_app(app) 388 | ``` 389 | -------------------------------------------------------------------------------- /mkdocs/docs/modules/CRUD/viewsets.md: -------------------------------------------------------------------------------- 1 | # Viewsets 2 | 3 | Viewset is a collection of views. Simplest usecase is to use `AutoFastAPIViewSet` to create a CRUD endpoints for a peewee model. 4 | 5 | ## AutoFastAPIViewSet 6 | 7 | Will automatically make default create, retrieve, list, update, partial update and delete API endpoints for the specified Peewee model. 8 | 9 | ### Usage 10 | 11 | ```python 12 | AutoFastAPIViewSet(model: Type[peewee.Model], app: FastAPI, actions: Optional[Set[str]]) 13 | ``` 14 | 15 | - `model: Type[peewee.Model]` - Peewee model for which endpoints will be created. 16 | - `app: FastAPI` - FastAPI application 17 | - `actions: Set[str]` - Optional. Set of actions, possible values: 'retrieve', 'list', 'create', 'update', 'part_update', 'delete'. 18 | 19 | ### Actions 20 | 21 | - `retrieve` 22 | 23 | **HTTP method:** `GET`
24 | **URL:** `/{model_name}/{pk}/`
25 | Retrieve an instance data by it's primary key. 26 | 27 | - `list` 28 | 29 | **HTTP method:** `GET`
30 | **URL:** `/{model_name}/`
31 | Retrieve a list of all instances data. 32 | 33 | - `create` 34 | 35 | **HTTP method:** `POST`
36 | **URL:** `/{model_name}/`
37 | **Request body:** JSON data
38 | Create a new model instance. 39 | 40 | - `update` 41 | 42 | **HTTP method:** `PUT`
43 | **URL:** `/{model_name}/{pk}/`
44 | **Request body:** JSON data
45 | Full update of a model instance by it's primary key. 46 | 47 | - `part_update` 48 | 49 | **HTTP method:** `PATCH`
50 | **URL:** `/{model_name}/{pk}/`
51 | **Request body:** JSON data
52 | Partial update of a model instance by it's primary key. (Only specified fields will be updated). 53 | 54 | - `delete` 55 | 56 | **HTTP method:** `DELETE`
57 | **URL:** `/{model_name}/{pk}/`
58 | Delete a model instance by it's primary key. 59 | 60 | 61 | ### Example 62 | 63 | ```python 64 | import peewee as pw 65 | from fastapi import FastAPI 66 | from fastapiwee import AutoFastAPIViewSet 67 | 68 | DB = pw.SqliteDatabase(':memory:') 69 | 70 | 71 | class TestModel(pw.Model): 72 | id = pw.AutoField() 73 | text = pw.TextField() 74 | number = pw.IntegerField(null=True) 75 | is_test = pw.BooleanField(default=True) 76 | 77 | class Meta: 78 | database = DB 79 | 80 | 81 | app = FastAPI() 82 | 83 | AutoFastAPIViewSet(TestModel, app) 84 | ``` 85 | 86 | That will create next endpoints: 87 | 88 | - `GET` `/test_model/{pk}/`
89 | **Response:**
90 | `200`
91 | ```JSON 92 | { 93 | "id": 0, 94 | "text": "string", 95 | "number": 0, 96 | "is_test": true 97 | } 98 | ``` 99 | 100 | - `GET` `/test_model/`
101 | **Response:**
102 | `200`
103 | ```JSON 104 | [ 105 | { 106 | "id": 0, 107 | "text": "string", 108 | "number": 0, 109 | "is_test": true 110 | } 111 | ] 112 | ``` 113 | 114 | - `POST` `/test_model/`
115 | **Body:**
116 | ```JSON 117 | { 118 | "text": "string", 119 | "number": 0, 120 | "is_test": true 121 | } 122 | ``` 123 | **Response:**
124 | `201`
125 | ```JSON 126 | [ 127 | { 128 | "id": 0, 129 | "text": "string", 130 | "number": 0, 131 | "is_test": true 132 | } 133 | ] 134 | ``` 135 | 136 | - `PUT` `/test_model/{pk}`
137 | **Body:**
138 | ```JSON 139 | { 140 | "text": "string", 141 | "number": 0, 142 | "is_test": true 143 | } 144 | ``` 145 | **Response:**
146 | `200`
147 | ```JSON 148 | { 149 | "id": 0, 150 | "text": "string", 151 | "number": 0, 152 | "is_test": true 153 | } 154 | ``` 155 | 156 | - `PATCH` `/test_model/{pk}`
157 | **Body:**
158 | ```JSON 159 | { 160 | "text": "string", 161 | "number": 0, 162 | "is_test": true 163 | } 164 | ``` 165 | **Response:**
166 | `200`
167 | ```JSON 168 | { 169 | "id": 0, 170 | "text": "string", 171 | "number": 0, 172 | "is_test": true 173 | } 174 | ``` 175 | 176 | - `DELETE` `/test_model/{pk}`
177 | **Response:**
178 | `204` 179 | 180 | For a more detailed example refer to [example.py](https://github.com/Ignisor/FastAPIwee/blob/main/example.py) 181 | 182 | ## BaseFastAPIViewSet 183 | 184 | For a more specific usecases you can extend or use `BaseFastAPIViewSet` class. It takes views as input and will create a router and add it to the app. 185 | 186 | ### Usage 187 | 188 | ```python 189 | BaseFastAPIViewSet(views: List[Type[FastAPIView]]).add_to_app(app: FastAPI) 190 | ``` 191 | 192 | - `views: List[Type[FastAPIView]]` - List of views to add to the app. 193 | - `app: FastAPI` - FastAPI app to which views will be added. 194 | 195 | ### Example 196 | 197 | `BaseFastAPIViewSet` can be used to define views with custom serializers and logic. User creation for example. 198 | 199 | ```python 200 | import peewee as pw 201 | import pydantic as pd 202 | from fastapi import FastAPI 203 | from fastapiwee.crud.views import CreateFastAPIView, RetrieveFastAPIView 204 | from fastapiwee.crud.viewsets import BaseFastAPIViewSet 205 | from fastapiwee.pwpd import PwPdWriteModel 206 | 207 | DB = pw.SqliteDatabase(':memory:') 208 | 209 | 210 | class User(pw.Model): 211 | id = pw.AutoField() 212 | email = pw.TextField() 213 | password = pw.TextField() # Never store passwords as an unencrypted plain text data 214 | bio = pw.TextField(null=True) 215 | 216 | class Meta: 217 | database = DB 218 | 219 | 220 | class SignUpData(PwPdWriteModel): 221 | password2: str 222 | 223 | class Config: 224 | pw_model = User 225 | pw_exclude = {'bio'} 226 | 227 | @pd.validator('email') 228 | def check_email(cls, v, **kwargs): 229 | """Simple and dumb email validator for example""" 230 | if '@' not in v: 231 | raise ValueError('invalid email') 232 | 233 | return v 234 | 235 | @pd.validator('password2') 236 | def passwords_match(cls, v, values, **kwargs): 237 | if v != values['password']: 238 | raise ValueError('passwords do not match') 239 | return v 240 | 241 | 242 | class SignUpView(CreateFastAPIView): 243 | MODEL = User 244 | _SERIALIZER = SignUpData 245 | 246 | def create(self) -> pw.Model: 247 | DB.create_tables([User]) 248 | return self.MODEL.create(**self._obj_data.dict(exclude={'password2'})) 249 | 250 | 251 | app = FastAPI() 252 | 253 | viewset = BaseFastAPIViewSet(views=[SignUpView, RetrieveFastAPIView.make_model_view(User)]) 254 | viewset.add_to_app(app) 255 | ``` 256 | -------------------------------------------------------------------------------- /mkdocs/docs/modules/PwPd.md: -------------------------------------------------------------------------------- 1 | # PwPd module 2 | 3 | PwPd - PeeweePydantic. 4 | Module with adapters for transfering Peewee models to Pydantic models (a.k.a. serializer). 5 | 6 | ## PwPdModel 7 | 8 | Base class for PwPd models adapters. 9 | 10 | Inherits Pydantic `BaseModel` and thus supports Pydantic features. 11 | 12 | ### Config 13 | 14 | Same as Pydantic models, behaviour of `PwPdModel` can be controlled via the Config class on a model. 15 | For available Pydantic options refer to [Pydantic documentation](https://pydantic-docs.helpmanual.io/usage/model_config/). 16 | 17 | Next Pydantic config options are set on `PwPdModel`: 18 | 19 | - `orm_mode = True` - Required to work with ORM model such as Peewee. 20 | - `extra = 'forbid'` - Will cause validation to fail if extra attributes are included. 21 | - `getter_dict = PwPdGetterDict` - Custom getter dict to handle backrefs. 22 | 23 | #### Additional PwPd options: 24 | 25 | - `pw_model: Type[peewee.Model]` 26 | 27 | Required. Used to specify Peewee model for adapter. 28 | 29 | - `pw_fields: set` 30 | 31 | Set of PeeWee model fields to add to the Pydantic model. If not specified - all fields are used. 32 | 33 | - `pw_exclude: set` 34 | 35 | Set of PeeWee model fields to exclude from the the Pydantic model. If not specified - no fields are excluded. 36 | 37 | - `pw_exclude_pk: bool` 38 | 39 | Default - `False`. Whether to exclude primary key field from Pydantic model or not. Used for "write" models (create/update). 40 | 41 | - `pw_nest_fk: bool` 42 | 43 | Default - `False`. Whether to nest model data by foreign key or only return it's primary key value. 44 | 45 | - `pw_nest_backrefs: bool` 46 | 47 | Default - `False`. Whether to nest backrefs data or ignore backrefs. 48 | 49 | - `pw_all_optional: bool` 50 | 51 | Default - `False`. Wheter to set all fields optional or not. Used for partial update models, where any number of fields can be specified. 52 | 53 | ### Creating PwPd model 54 | 55 | #### Defining model class 56 | 57 | You can define a model class using PwPdModel as a base class. Defining a PwPdModel is similar to [defining Pydantic model](https://pydantic-docs.helpmanual.io/usage/models/). The only difference is `Config` options and automated definition of fields. You only need to define fields which behaviour you want to change. 58 | 59 | It is required to set `pw_model` option in `Config` class. 60 | 61 | Minimal usage example: 62 | 63 | ```python 64 | class CucumberPwPdModel(PwPdModel): 65 | class Config: 66 | pw_model: peewee.Model = Cucumber 67 | ``` 68 | 69 | More complex example: 70 | 71 | ```python 72 | import peewee as pw 73 | from fastapiwee.pwpd import PwPdModel 74 | 75 | DB = pw.SqliteDatabase(':memory:') 76 | 77 | 78 | class TestModel(pw.Model): 79 | id = pw.AutoField() 80 | text = pw.TextField() 81 | number = pw.IntegerField(null=True) 82 | is_test = pw.BooleanField(default=True) 83 | 84 | class Meta: 85 | database = DB 86 | 87 | 88 | # Define a PwPdModel (a.k.a. serializer) 89 | class TestPwPdModel(PwPdModel): 90 | text: str = 'Something default' 91 | 92 | class Config: 93 | pw_model = TestModel 94 | pw_exclude_pk = True 95 | 96 | 97 | # `text` field now has a default value and not required to specify 98 | validated_data = TestPwPdModel(number=23) 99 | new_instance = TestModel(**validated_data.dict()) 100 | 101 | assert new_instance.text == 'Something default' 102 | assert new_instance.number == 23 103 | ``` 104 | 105 | #### Using make_serializer class method 106 | 107 | `make_serializer` method can be used to create a generic Pydantic model from Peewee model. Method arguments can be used to override any of the `Config` options. 108 | 109 | Usage: `PwPdModel.make_serializer(peewee.Model, **config_options)` 110 | 111 | _Example:_ 112 | 113 | - Define a model: 114 | 115 | ```python 116 | import peewee as pw 117 | 118 | DB = pw.SqliteDatabase('/tmp/fastapiwee_example.db') 119 | 120 | 121 | class TestModel(pw.Model): 122 | id = pw.AutoField() 123 | text = pw.TextField() 124 | number = pw.IntegerField(null=True) 125 | is_test = pw.BooleanField(default=True) 126 | 127 | class Meta: 128 | database = DB 129 | ``` 130 | 131 | - Make a simple serializer and try it: 132 | 133 | ```python hl_lines="2 17-33" 134 | import peewee as pw 135 | from fastapiwee.pwpd import PwPdModel 136 | 137 | DB = pw.SqliteDatabase(':memory:') 138 | 139 | 140 | class TestModel(pw.Model): 141 | id = pw.AutoField() 142 | text = pw.TextField() 143 | number = pw.IntegerField(null=True) 144 | is_test = pw.BooleanField(default=True) 145 | 146 | class Meta: 147 | database = DB 148 | 149 | 150 | test_instance = TestModel( 151 | id=1, 152 | text='Cucumber', 153 | number=123, 154 | ) 155 | 156 | # Make a simple serializer 157 | serializer = PwPdModel.make_serializer(TestModel) 158 | 159 | # Try it 160 | data = serializer.from_orm(test_instance) 161 | assert data.dict() == { 162 | 'id': 1, 163 | 'text': 'Cucumber', 164 | 'number': 123, 165 | 'is_test': True, 166 | } 167 | ``` 168 | 169 | - Make a customized serializer and try it: 170 | 171 | ```python hl_lines="35-45" 172 | import peewee as pw 173 | from fastapiwee.pwpd import PwPdModel 174 | 175 | DB = pw.SqliteDatabase(':memory:') 176 | 177 | 178 | class TestModel(pw.Model): 179 | id = pw.AutoField() 180 | text = pw.TextField() 181 | number = pw.IntegerField(null=True) 182 | is_test = pw.BooleanField(default=True) 183 | 184 | class Meta: 185 | database = DB 186 | 187 | 188 | test_instance = TestModel( 189 | id=1, 190 | text='Cucumber', 191 | number=123, 192 | ) 193 | 194 | # Make a simple serializer 195 | serializer = PwPdModel.make_serializer(TestModel) 196 | 197 | # Try it 198 | data = serializer.from_orm(test_instance) 199 | assert data.dict() == { 200 | 'id': 1, 201 | 'text': 'Cucumber', 202 | 'number': 123, 203 | 'is_test': True, 204 | } 205 | 206 | # Make customized serializer 207 | serializer = PwPdModel.make_serializer( 208 | TestModel, 209 | pw_exclude_pk=True, 210 | pw_exclude={'is_test'}, 211 | anystr_lower=True 212 | ) 213 | 214 | # Try it 215 | data = serializer.from_orm(test_instance) 216 | assert data.dict() == {'text': 'cucumber', 'number': 123} 217 | ``` 218 | 219 | ## Other base PwPd models 220 | 221 | Module also contains another base classes that are inherit default PwPdModel (thus `make_serializer` can be used on them as well). The only difference is a default `Config` values. 222 | 223 | ### PwPdWriteModel 224 | 225 | Base to create models for write actions (create/update). Excludes primary key field, will not nest foreign keys and backrefs. 226 | 227 | Has next config: 228 | 229 | ```python 230 | class Config: 231 | pw_exclude_pk = True 232 | pw_nest_fk = False 233 | pw_nest_backrefs = False 234 | ``` 235 | 236 | ### PwPdPartialUpdateModel 237 | 238 | Base to create models for partial update actions (e.g. `PATCH`). Inherits config from PwPdWriteModel, but all fields will be optional. 239 | 240 | Has next config: 241 | 242 | ```python 243 | class Config: 244 | pw_all_optional = True 245 | ``` 246 | 247 | ## Factory 248 | 249 | `PwPdModelFactory` class can be used to create "read" and "write" PwPd models for a single Peewee model. 250 | 251 | Usage: 252 | ```python 253 | from fastapiwee.pwpd import PwPdModelFactory 254 | 255 | pwpd_factory = PwPdModelFactory(peewee.Model) 256 | 257 | # equals to PwPdModel.make_serializer(peewee.Model) 258 | read_serializer = pwpd_factory.read_pd() 259 | 260 | # equals to PwPdWriteModel.make_serializer(peewee.Model) 261 | write_serializer = pwpd_factory.write_pd() 262 | ``` 263 | -------------------------------------------------------------------------------- /mkdocs/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: FastAPIwee 2 | site_description: FastAPI + PeeWee = <3 3 | 4 | theme: 5 | name: material 6 | palette: 7 | primary: purple 8 | 9 | repo_url: https://github.com/Ignisor/FastAPIwee 10 | 11 | markdown_extensions: 12 | - pymdownx.highlight 13 | - pymdownx.superfences 14 | 15 | 16 | extra_css: 17 | - /media/css/termynal.css 18 | extra_javascript: 19 | - /media/js/termynal.js 20 | - /media/js/custom.js 21 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.66.0 2 | pydantic==1.8.2 3 | peewee==3.14.4 4 | mkdocs==1.1.2 5 | mkdocs-material==7.1.2 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | 4 | with open("README.md", "r") as readme: 5 | long_description = readme.read() 6 | 7 | with open("VERSION", "r") as version_f: 8 | version = version_f.read() 9 | 10 | setuptools.setup( 11 | name="FastAPIwee", 12 | version=version, 13 | author="German Gensetskyi", 14 | author_email="Ignis2497@gmail.com", 15 | description="FastAPIwee - FastAPI + PeeWee = <3", 16 | long_description=long_description, 17 | long_description_content_type="text/markdown", 18 | url="https://github.com/Ignisor/FastAPIwee", 19 | project_urls={ 20 | 'Documentation': 'https://fastapiwee.qqmber.wtf', 21 | }, 22 | packages=setuptools.find_packages(exclude=('tests', )), 23 | classifiers=[ 24 | "Programming Language :: Python :: 3", 25 | "Operating System :: OS Independent", 26 | ], 27 | python_requires='>=3.6', 28 | install_requires=[ 29 | 'fastapi==0.66.0', 30 | 'pydantic==1.8.2', 31 | 'peewee==3.14.4', 32 | ], 33 | ) 34 | --------------------------------------------------------------------------------