├── .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 | 
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 |
--------------------------------------------------------------------------------