├── .github └── workflows │ └── django.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── TEST_PROJECT ├── __init__.py ├── settings.py ├── urls.py └── wsgi.py ├── __init__.py ├── manage.py ├── publish.sh ├── setup.cfg ├── setup.py └── src ├── fast_api ├── __init__.py ├── decorators.py └── error_handling.py └── test_api ├── __init__.py ├── apps.py ├── migrations └── __init__.py ├── models.py ├── serializers.py ├── tests.py ├── urls.py └── views.py /.github/workflows/django.yml: -------------------------------------------------------------------------------- 1 | name: Django CI 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | max-parallel: 4 15 | matrix: 16 | python-version: [3.7, 3.8, 3.9, 4.1] 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v3 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install Dependencies 25 | run: | 26 | python setup.py install 27 | - name: Run Tests 28 | run: | 29 | python manage.py test 30 | -------------------------------------------------------------------------------- /.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 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .nox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # IPython 77 | profile_default/ 78 | ipython_config.py 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # SageMath parsed files 87 | *.sage.py 88 | 89 | # Environments 90 | .env 91 | .venv 92 | env/ 93 | venv*/ 94 | ENV/ 95 | env.bak/ 96 | venv.bak/ 97 | 98 | # Spyder project settings 99 | .spyderproject 100 | .spyproject 101 | 102 | # Rope project settings 103 | .ropeproject 104 | 105 | # mkdocs documentation 106 | /site 107 | 108 | # mypy 109 | .mypy_cache/ 110 | .dmypy.json 111 | dmypy.json 112 | 113 | 114 | # idea 115 | .idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Assem Chelli 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Django Fast API 2 | ================= 3 | 4 | Few hacks to speed up defining APIs based on django rest framwork, inspired from fastapi 5 | 6 | **First version tested on: python 3.9 and django 4.0** 7 | 8 | Features: 9 | --------- 10 | - [x] Function based view 11 | - [x] Easy to use decorator 12 | - [x] Accepts validation of input and output using DRF serializers 13 | - [x] Accept CamelCased input and group all rest input methods in same dict :`req` 14 | - [x] Jsonify and camelcase any type of output: `str`, `dict`, `QuerySet`, `Model` 15 | - [x] AutoSchema docs using drf-spectacular 16 | - [x] Error handler that works with DRF 17 | - [ ] Better way to pass the `request` object inside views 18 | - [ ] Convert DRF serializers into a type annotation classes for better processing by IDEs 19 | 20 | Quick start 21 | ----------- 22 | 23 | 1. Install the lib:: 24 | 25 | `$ pip install django-fast-api` 26 | 27 | 1. Add "drf_spectacular" to your ``INSTALLED_APPS`` setting like this:: 28 | ```python 29 | INSTALLED_APPS = [ 30 | ... 31 | "drf_spectacular", 32 | ... 33 | ] 34 | ``` 35 | 2. Include the swagger documentation in your project ``urls.py`` like this:: 36 | ```python 37 | from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView 38 | 39 | path('api-schema/', SpectacularAPIView.as_view(), name='schema'), 40 | path('api-doc/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), 41 | ``` 42 | 3. Add open api schema class and exception handler to "REST_FRAMEWORK" settings:: 43 | ```python 44 | 45 | REST_FRAMEWORK = { 46 | 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', 47 | 'EXCEPTION_HANDLER': 'fast_api.error_handling.exception_handler' 48 | } 49 | 50 | ``` 51 | 4. Examples of usage in views:: 52 | ```python 53 | 54 | from fast_api.decorators import APIRouter 55 | 56 | router = APIRouter() 57 | from . import serializers, models 58 | 59 | @router.api('public/health_check') 60 | def health_check(req): 61 | return "ok" 62 | 63 | @router.api('sample') 64 | def sample_view(): 65 | return { 66 | 'key' : 'value' 67 | } 68 | 69 | @router.api('sample/error') 70 | def error_view(): 71 | assert False, "This is the error message user will get" 72 | 73 | 74 | # with input & output validation 75 | 76 | from rest_framework import serializers 77 | from test_api.models import Country 78 | 79 | 80 | class CreateCountryRequest(serializers.ModelSerializer): 81 | class Meta: 82 | model = Country 83 | fields = ['name'] 84 | 85 | 86 | class GetCountryRequest(serializers.Serializer): 87 | id = serializers.IntegerField() 88 | 89 | class CountryResponse(serializers.ModelSerializer): 90 | class Meta: 91 | model = Country 92 | fields = ['name'] 93 | 94 | @router.api('country/create') 95 | def create_company(req: serializers.CreateCountryRequest) -> serializers.CountryResponse: 96 | return models.Country.objects.create(**req.args) 97 | 98 | 99 | @router.api('country/get') 100 | def create_company(req: GetCountryRequest) -> CountryResponse: 101 | return models.Country.objects.get(id=req.args.id) 102 | ``` 103 | 104 | * req is a django request 105 | * you will find all endpoint args in req.args 106 | 107 | Issues and Feedback 108 | ==================== 109 | 110 | If you found an issue or you have a feedback , don't hesitate to point to it as a github issue. 111 | -------------------------------------------------------------------------------- /TEST_PROJECT/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/assem-ch/django-fast-api/d23157a54f431288a5d75d37fbce7013ef764624/TEST_PROJECT/__init__.py -------------------------------------------------------------------------------- /TEST_PROJECT/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 5 | from sys import path 6 | 7 | BASE_DIR = Path(__file__).resolve().parent.parent 8 | 9 | path.insert(0, os.path.join(BASE_DIR, 'src')) 10 | 11 | SECRET_KEY = '&-$r1&aiui8j*3e_iw71o0*vvp!u6c$girojhdzi*(!iew9wpm' 12 | 13 | DEBUG = True 14 | 15 | ALLOWED_HOSTS = ['*'] 16 | 17 | FOO_SETTING = "test" 18 | 19 | INSTALLED_APPS = [ 20 | 'django.contrib.admin', 21 | 'django.contrib.auth', 22 | 'django.contrib.contenttypes', 23 | 'django.contrib.sessions', 24 | 'django.contrib.messages', 25 | 'django.contrib.staticfiles', 26 | 'drf_spectacular', 27 | 'test_api' 28 | ] 29 | 30 | MIDDLEWARE = [ 31 | 'django.middleware.security.SecurityMiddleware', 32 | 'django.contrib.sessions.middleware.SessionMiddleware', 33 | 'django.middleware.common.CommonMiddleware', 34 | 'django.middleware.csrf.CsrfViewMiddleware', 35 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 36 | 'django.contrib.messages.middleware.MessageMiddleware', 37 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 38 | ] 39 | 40 | ROOT_URLCONF = 'TEST_PROJECT.urls' 41 | 42 | TEMPLATES = [ 43 | { 44 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 45 | 'APP_DIRS': True, 46 | 'OPTIONS': { 47 | 'context_processors': [ 48 | "django.template.context_processors.debug", 49 | "django.template.context_processors.request", 50 | "django.contrib.auth.context_processors.auth", 51 | "django.contrib.messages.context_processors.messages", 52 | ], 53 | }, 54 | }, 55 | ] 56 | 57 | WSGI_APPLICATION = 'TEST_PROJECT.wsgi.application' 58 | 59 | 60 | # Database 61 | # https://docs.djangoproject.com/en/3.1/ref/settings/#databases 62 | 63 | DATABASES = { 64 | 'default': { 65 | 'ENGINE': 'django.db.backends.sqlite3', 66 | 'NAME': BASE_DIR / 'db.sqlite3', 67 | } 68 | } 69 | 70 | 71 | # Password validation 72 | # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators 73 | 74 | AUTH_PASSWORD_VALIDATORS = [ 75 | { 76 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 77 | }, 78 | { 79 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 80 | }, 81 | { 82 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 83 | }, 84 | { 85 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 86 | }, 87 | ] 88 | 89 | 90 | # Internationalization 91 | # https://docs.djangoproject.com/en/3.1/topics/i18n/ 92 | 93 | LANGUAGE_CODE = 'en-us' 94 | 95 | TIME_ZONE = 'UTC' 96 | 97 | USE_I18N = True 98 | 99 | USE_L10N = True 100 | 101 | USE_TZ = True 102 | 103 | REST_FRAMEWORK = { 104 | 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', 105 | 'EXCEPTION_HANDLER': 'fast_api.error_handling.exception_handler' 106 | } 107 | 108 | # Static files (CSS, JavaScript, Images) 109 | # https://docs.djangoproject.com/en/3.1/howto/static-files/ 110 | 111 | STATIC_URL = '/static/' 112 | -------------------------------------------------------------------------------- /TEST_PROJECT/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path, include 3 | 4 | urlpatterns = [ 5 | path('admin/', admin.site.urls), 6 | path('', include('test_api.urls')), 7 | ] 8 | -------------------------------------------------------------------------------- /TEST_PROJECT/wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.wsgi import get_wsgi_application 4 | 5 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'TEST_PROJECT.settings') 6 | 7 | application = get_wsgi_application() 8 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/assem-ch/django-fast-api/d23157a54f431288a5d75d37fbce7013ef764624/__init__.py -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'TEST_PROJECT.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Remove dist folder as it will be generated again in the next step with a new build. 3 | if [ -d "dist" ] 4 | then 5 | echo "Deleting dist directory" 6 | rm -r dist 7 | fi 8 | # Install/update needed tools 9 | python -m pip install --upgrade twine setuptools wheel 10 | # Create new build 11 | python setup.py sdist 12 | # Upload new build to pypi using twine 13 | python -m twine upload --verbose dist/* -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name=django-fast-api 3 | version=0.1.17 4 | long_description= check the github repo! 5 | author= Assem Chelli 6 | url = https://github.com/assem-ch/django-fast-api 7 | license=BSD License 8 | classifiers= 9 | Environment :: Web Environment 10 | Framework :: Django 11 | Intended Audience :: Developers 12 | License :: OSI Approved :: BSD License 13 | Programming Language :: Python 14 | 15 | [options] 16 | include_package_data = true 17 | 18 | 19 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import find_packages, setup 3 | 4 | with open(os.path.join(os.path.dirname(__file__), 'README.md')) as readme: 5 | README = readme.read() 6 | 7 | # allow setup.py to be run from any path 8 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 9 | 10 | setup( 11 | install_requires=["django>=4.0.0", 12 | "djangorestframework", 13 | "djangorestframework-camel-case==1.3.0", 14 | "drf-spectacular", 15 | "prodict"], 16 | package_dir={'': 'src'}, 17 | packages=['fast_api'], 18 | ) 19 | -------------------------------------------------------------------------------- /src/fast_api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/assem-ch/django-fast-api/d23157a54f431288a5d75d37fbce7013ef764624/src/fast_api/__init__.py -------------------------------------------------------------------------------- /src/fast_api/decorators.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from typing import Iterable 3 | 4 | from django.db.models import Model, QuerySet 5 | from django.db.models.query import RawQuerySet 6 | from django.template.response import TemplateResponse 7 | from djangorestframework_camel_case.parser import ( 8 | CamelCaseFormParser, 9 | CamelCaseJSONParser, 10 | CamelCaseMultiPartParser, 11 | ) 12 | from djangorestframework_camel_case.render import CamelCaseJSONRenderer 13 | from drf_spectacular.types import OpenApiTypes 14 | from drf_spectacular.utils import OpenApiParameter, extend_schema 15 | from prodict import Prodict 16 | from rest_framework import serializers 17 | from rest_framework.decorators import ( 18 | api_view, 19 | parser_classes, 20 | permission_classes, 21 | renderer_classes, 22 | ) 23 | from rest_framework.response import Response 24 | 25 | 26 | def update_dict(d, u): 27 | import collections.abc 28 | 29 | for k, v in u.items(): 30 | if isinstance(v, collections.abc.Mapping): 31 | d[k] = update_dict(d.get(k, {}), v) 32 | else: 33 | if isinstance(v, list) and len(v) == 1: 34 | d[k] = v[0] 35 | else: 36 | d[k] = v 37 | return d 38 | 39 | 40 | 41 | def parse_url_objects(param, value): 42 | import re 43 | 44 | reg = re.compile( 45 | r"(?P[^\[^\]]+)(?:(?:\[(?P[^\[\]]+)\])(?:\[(?P[^\[\]]+)\])?)?" 46 | ) 47 | match = reg.match(param) 48 | d = {} 49 | if match: 50 | level1, level2, level3 = match.groups() 51 | if not level2: 52 | d[level1] = value 53 | elif level2 and not level3: 54 | d[level1] = {level2: value} 55 | else: 56 | d[level1] = {level2: {level3: value}} 57 | print(d) 58 | return d 59 | 60 | 61 | def omittable_parentheses(maybe_decorator=None, /, allow_partial=False): 62 | """A decorator for decorators that allows them to be used without parentheses""" 63 | 64 | def decorator(func): 65 | @wraps(decorator) 66 | def wrapper(*args, **kwargs): 67 | if len(args) == 1 and callable(args[0]): 68 | if allow_partial: 69 | return func(**kwargs)(args[0]) 70 | elif not kwargs: 71 | return func()(args[0]) 72 | return func(*args, **kwargs) 73 | 74 | return wrapper 75 | 76 | if maybe_decorator is None: 77 | return decorator 78 | else: 79 | return decorator(maybe_decorator) 80 | 81 | 82 | def get_serializer(model, fields="__all__"): 83 | class GenericSerializer(serializers.ModelSerializer): 84 | class Meta: 85 | pass 86 | 87 | Meta.model = model 88 | Meta.fields = fields 89 | 90 | return GenericSerializer 91 | 92 | 93 | 94 | def build_params(request_serializer, response_serializer, query_params): 95 | if not query_params: 96 | query_params = [] 97 | params = {} 98 | manual_params = [] 99 | for query_param in query_params: 100 | manual_params.append( 101 | OpenApiParameter( 102 | name=query_param, 103 | type=OpenApiTypes.STR, 104 | location=OpenApiParameter.QUERY, 105 | default="", 106 | ), 107 | ) 108 | 109 | params["request"] = request_serializer or dict 110 | 111 | params["parameters"] = manual_params 112 | params["responses"] = {"200": response_serializer or dict} 113 | return params 114 | 115 | 116 | class APIRouter: 117 | def __init__(self, prefix=None, tags=tuple()): 118 | self.prefix = prefix or "" 119 | self.urls = [] 120 | self.tags = tags or (prefix and [prefix.strip("/")]) 121 | 122 | @omittable_parentheses(allow_partial=True) 123 | def api( 124 | self, 125 | path=None, 126 | query_params_for_docs_only=None, 127 | permissions=tuple(), 128 | methods=("post",), 129 | renderers=(CamelCaseJSONRenderer,), 130 | template=None, 131 | parsers=(CamelCaseJSONParser, CamelCaseFormParser, CamelCaseMultiPartParser), 132 | tags=None, 133 | ): 134 | def decorator(func): 135 | req_serializer = getattr( 136 | func, "__annotations__", {} 137 | ).get("req") 138 | res_serializer = getattr( 139 | func, "__annotations__", {} 140 | ).get("return") 141 | 142 | @wraps(func) 143 | @extend_schema( 144 | methods=methods, 145 | tags=tags or self.tags, 146 | **build_params( 147 | req_serializer, 148 | res_serializer, 149 | query_params=query_params_for_docs_only, 150 | ), 151 | ) 152 | @api_view(methods) 153 | @renderer_classes(renderers) 154 | @permission_classes(permissions) 155 | @parser_classes(parsers) 156 | def decorated(request, **kwargs): 157 | 158 | args = {} 159 | request_data = request.data 160 | request_get = {} 161 | for param in request.GET: 162 | update_dict( 163 | request_get, 164 | parse_url_objects(param, request.GET.getlist(param)), 165 | ) 166 | request_files = request.FILES 167 | data = {**request_get, **request_data, **request_files, **kwargs} 168 | print(data) 169 | decorated_args = set( 170 | func.__code__.co_varnames[: func.__code__.co_argcount] 171 | ) 172 | 173 | if func.__code__.co_argcount: 174 | if "req" in decorated_args: 175 | if req_serializer and "file" not in req_serializer().fields: 176 | serializer_data = req_serializer(data=data) 177 | serializer_data.is_valid(raise_exception=True) 178 | data = serializer_data.data 179 | 180 | 181 | 182 | request.args = Prodict(data) 183 | args['req'] = request 184 | 185 | raw_data = func(**args) 186 | 187 | if res_serializer: 188 | response_data = res_serializer(raw_data).data 189 | elif isinstance(raw_data, QuerySet): 190 | response_data = get_serializer(raw_data.model)( 191 | raw_data, many=True 192 | ).data 193 | elif isinstance(raw_data, RawQuerySet): 194 | response_data = get_serializer( 195 | raw_data.model, fields=raw_data.columns 196 | )(raw_data, many=True).data 197 | elif isinstance(raw_data, Model): 198 | response_data = get_serializer(type(raw_data))(raw_data).data 199 | else: 200 | response_data = raw_data 201 | 202 | if template: 203 | return TemplateResponse(request, template, response_data) 204 | return Response(response_data) 205 | 206 | if path: 207 | from django import urls 208 | 209 | if isinstance(path, str): 210 | paths = [path] 211 | elif isinstance(path, Iterable): 212 | paths = path 213 | else: 214 | paths = [] 215 | for p in paths: 216 | self.urls.append(urls.path(f"{self.prefix}{p}", decorated)) 217 | 218 | return decorated 219 | 220 | return decorator 221 | -------------------------------------------------------------------------------- /src/fast_api/error_handling.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import traceback 4 | 5 | from django.core.exceptions import ObjectDoesNotExist 6 | from django.db import IntegrityError 7 | from django.http import JsonResponse 8 | 9 | 10 | DEBUG = os.getenv('DEBUG', 0) 11 | 12 | def report_exception(exc, level="warning"): 13 | # TODO add sentry settings 14 | try: 15 | import sentry_sdk 16 | with sentry_sdk.configure_scope() as scope: 17 | scope.level = level 18 | sentry_sdk.capture_exception(exc) 19 | 20 | sentry_sdk.capture_exception(exc) 21 | except: 22 | pass 23 | 24 | 25 | def exception_handler(exc, context): 26 | from rest_framework.views import exception_handler 27 | 28 | response = exception_handler(exc, context) 29 | 30 | if response is not None: 31 | response.data["status_code"] = response.status_code 32 | elif isinstance(exc, AssertionError): 33 | err_data = {"error": str(exc)} 34 | logging.warning(exc) 35 | report_exception(exc) 36 | response = JsonResponse(err_data, safe=True, status=400) 37 | elif isinstance(exc, IntegrityError): 38 | err_data = {"error": str(exc)} 39 | logging.warning(exc) 40 | report_exception(exc) 41 | response = JsonResponse(err_data, safe=True, status=400) 42 | 43 | elif isinstance(exc, ObjectDoesNotExist): 44 | tb = "\n".join(traceback.format_exception(type(exc), exc, exc.__traceback__)) 45 | err_data = {"error": "Doesn't exist"} 46 | logging.warning(exc) 47 | report_exception(exc) 48 | response = JsonResponse(err_data, safe=True, status=400) 49 | 50 | elif isinstance(exc, Exception): 51 | tb = "\n".join(traceback.format_exception(type(exc), exc, exc.__traceback__)) 52 | err_data = { 53 | "error": "Unknown error" 54 | } 55 | if DEBUG: 56 | err_data['tb'] = tb 57 | logging.error(exc) 58 | logging.error(tb) 59 | report_exception(exc, "error") 60 | response = JsonResponse(err_data, safe=True, status=500) 61 | 62 | return response 63 | -------------------------------------------------------------------------------- /src/test_api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/assem-ch/django-fast-api/d23157a54f431288a5d75d37fbce7013ef764624/src/test_api/__init__.py -------------------------------------------------------------------------------- /src/test_api/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TestAPIConfig(AppConfig): 5 | name = 'test_api' 6 | -------------------------------------------------------------------------------- /src/test_api/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/assem-ch/django-fast-api/d23157a54f431288a5d75d37fbce7013ef764624/src/test_api/migrations/__init__.py -------------------------------------------------------------------------------- /src/test_api/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Country(models.Model): 5 | name = models.CharField(max_length=50) 6 | 7 | -------------------------------------------------------------------------------- /src/test_api/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from test_api.models import Country 4 | 5 | 6 | class CreateCountryRequest(serializers.ModelSerializer): 7 | class Meta: 8 | model = Country 9 | fields = ['name'] 10 | 11 | 12 | class GetCountryRequest(serializers.Serializer): 13 | id = serializers.IntegerField() 14 | 15 | class CountryResponse(serializers.ModelSerializer): 16 | class Meta: 17 | model = Country 18 | fields = ['name'] 19 | 20 | -------------------------------------------------------------------------------- /src/test_api/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /src/test_api/urls.py: -------------------------------------------------------------------------------- 1 | from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView 2 | 3 | 4 | from django.urls import path 5 | 6 | from test_api.views import router 7 | 8 | urlpatterns = [ 9 | *router.urls, 10 | path('api-schema/', SpectacularAPIView.as_view(), name='schema'), 11 | path('api-doc/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), 12 | ] 13 | -------------------------------------------------------------------------------- /src/test_api/views.py: -------------------------------------------------------------------------------- 1 | from fast_api.decorators import APIRouter 2 | 3 | router = APIRouter() 4 | from . import serializers, models 5 | 6 | 7 | @router.api('public/health_check') 8 | def sample_view(req): 9 | return "ok" 10 | 11 | 12 | @router.api('sample/success') 13 | def sample_view(req): 14 | return { 15 | 'key_name': 'value' 16 | } 17 | 18 | 19 | @router.api('sample/error') 20 | def error_view(): 21 | assert False, "This is the error message user will get" 22 | 23 | 24 | 25 | # with input & output validation 26 | 27 | 28 | @router.api('country/create') 29 | def create_company(req: serializers.CreateCountryRequest) -> serializers.CountryResponse: 30 | assert req.user, "not authentified" 31 | return models.Country.objects.create(**req.args) 32 | 33 | 34 | @router.api('country/get') 35 | def create_company(req: serializers.GetCountryRequest) -> serializers.CountryResponse: 36 | return models.Country.objects.get(id=req.args.id) --------------------------------------------------------------------------------