├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── config.yml └── workflows │ ├── setup.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── README.zh-CN.md ├── django_simple_api ├── __init__.py ├── __version__.py ├── _fields.py ├── apps.py ├── decorators.py ├── exceptions.py ├── extras.py ├── fields.py ├── middleware.py ├── params.py ├── schema.py ├── serialize.py ├── static │ ├── redoc.standalone.js │ ├── swagger-ui-bundle.js │ └── swagger-ui.css ├── templates │ ├── redoc.html │ └── swagger.html ├── types.py ├── urls.py ├── utils.py └── views.py ├── docs ├── declare-parameters.md ├── document-generation.md ├── extensions-function.md ├── index.md ├── parameter-verification.md └── quick-start.md ├── docs_en ├── declare-parameters.md ├── document-generation.md ├── extensions-function.md ├── index.md ├── parameter-verification.md └── quick-start.md ├── example ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py ├── manage.py ├── mkdocs.yml ├── poetry.lock ├── pyproject.toml ├── pytest.ini ├── script ├── test.py └── upload.py └── tests ├── __init__.py ├── testcases ├── Python39.png ├── __init__.py ├── pytest_cases │ ├── __init__.py │ ├── test_parameter_declare.py │ └── test_utils.py ├── unittest_cases │ ├── __init__.py │ ├── test_serialize.py │ └── tests.py └── 洛神赋.md ├── urls.py └── views.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | end_of_line = crlf 8 | insert_final_newline = true 9 | charset = utf-8 10 | 11 | [*.py] 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.{yml,yaml}] 16 | indent_style = space 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Django and Django-Simple-API Version** 11 | - Django: xxx 12 | - Django-Simple-API: xxx 13 | 14 | **Describe the bug** 15 | A clear and concise description of what the bug is. 16 | 17 | **To Reproduce** 18 | Some code. 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser 2 | blank_issues_enabled: true 3 | contact_links: 4 | - name: Question 5 | url: https://github.com/Django-Simple-API/django-simple-api/discussions 6 | about: > 7 | Ask a question 8 | -------------------------------------------------------------------------------- /.github/workflows/setup.yml: -------------------------------------------------------------------------------- 1 | name: Build setup.py 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build-and-deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | 14 | - name: Set up Python 15 | uses: actions/setup-python@v1 16 | with: 17 | python-version: 3.7 18 | 19 | - name: Install poetry2setup 20 | run: | 21 | python -m pip install poetry2setup 22 | - name: Build setup.py 23 | run: | 24 | poetry2setup > setup.py 25 | rm -f pyproject.toml poetry.lock 26 | - name: Push setup.py to branch `setup.py`. 27 | run: | 28 | remote_repo="https://${GITHUB_ACTOR}:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" 29 | git config http.sslVerify false 30 | git config user.name "Automated Publisher" 31 | git config user.email "actions@users.noreply.github.com" 32 | git remote add publisher "${remote_repo}" 33 | git show-ref # useful for debugging 34 | git branch --verbose 35 | # install lfs hooks 36 | git lfs install 37 | # publish any new files 38 | git checkout -b setup.py 39 | git add -A 40 | timestamp=$(date -u) 41 | git commit -m "Automated publish: ${timestamp} ${GITHUB_SHA}" || exit 0 42 | git push --force publisher setup.py 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | 46 | push-to-mirror: 47 | runs-on: ubuntu-latest 48 | needs: [build-and-deploy] 49 | steps: 50 | - name: Clone 51 | run: | 52 | git init 53 | git remote add origin https://github.com/${GITHUB_REPOSITORY}.git 54 | git fetch --all 55 | for branch in `git branch -a | grep remotes | grep -v HEAD`; do 56 | git branch --track ${branch##*/} $branch 57 | done 58 | 59 | - name: Push to Coding 60 | run: | 61 | remote_repo="https://${CODING_USERNAME}:${CODING_PASSWORD}@e.coding.net/${CODING_REPOSITORY}.git" 62 | 63 | git remote add coding "${remote_repo}" 64 | git show-ref # useful for debugging 65 | git branch --verbose 66 | 67 | # publish all 68 | git push --all --force coding 69 | git push --tags --force coding 70 | env: 71 | CODING_REPOSITORY: aber/github/django-simple-api 72 | CODING_USERNAME: ${{ secrets.CODING_USERNAME }} 73 | CODING_PASSWORD: ${{ secrets.CODING_PASSWORD }} 74 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | tests: 13 | name: "Python ${{ matrix.python-version }} ${{ matrix.os }}" 14 | runs-on: "${{ matrix.os }}" 15 | strategy: 16 | matrix: 17 | python-version: [3.7, 3.8, 3.9] 18 | os: [windows-latest, ubuntu-latest, macos-latest] 19 | 20 | steps: 21 | - uses: actions/checkout@v1 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v1 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip poetry 29 | poetry config virtualenvs.create false --local 30 | poetry install 31 | - name: Test with pytest 32 | run: | 33 | python script/test.py 34 | -------------------------------------------------------------------------------- /.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 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs_en/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | .venv/ 85 | 86 | # Spyder project settings 87 | .spyderproject 88 | 89 | # Rope project settings 90 | .ropeproject 91 | *.npy 92 | *.pkl 93 | 94 | # mypy 95 | .mypy_cache/ 96 | 97 | # VSCode 98 | .vscode/ 99 | 100 | # PyCharm 101 | .idea/ 102 | 103 | # mkdocs 104 | site/ 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 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 | **Language: English | [Chinese](README.zh-CN.md)** 2 | 3 | # Django Simple API 4 | 5 | A non-intrusive component that can help you quickly create APIs. 6 | 7 | ## Quick Start 8 | 9 | ### Install 10 | 11 | Download and install from github: 12 | 13 | ``` 14 | pip install django-simple-api 15 | ``` 16 | 17 | ### Configure 18 | 19 | Add django-simple-api to your `INSTALLED_APPS` in settings: 20 | 21 | ```python 22 | INSTALLED_APPS = [ 23 | ..., 24 | "django_simple_api", 25 | ] 26 | ``` 27 | 28 | Register the middleware to your `MIDDLEWARE` in settings: 29 | 30 | ```python 31 | MIDDLEWARE = [ 32 | ..., 33 | "django_simple_api.middleware.SimpleApiMiddleware", 34 | ] 35 | ``` 36 | 37 | Add the url of ***django-simple-api*** to your urls.py: 38 | 39 | ```python 40 | # urls.py 41 | 42 | from django.urls import include, path 43 | from django.conf import settings 44 | 45 | # Your urls 46 | urlpatterns = [ 47 | ... 48 | ] 49 | 50 | # Simple API urls, should only run in a test environment. 51 | if settings.DEBUG: 52 | urlpatterns += [ 53 | # generate documentation 54 | path("docs/", include("django_simple_api.urls")) 55 | ] 56 | ``` 57 | 58 | ### Complete the first example 59 | 60 | Define your url: 61 | 62 | ```python 63 | # your urls.py 64 | 65 | from django.urls import path 66 | from yourviews import JustTest 67 | 68 | urlpatterns = [ 69 | ..., 70 | path("/path//", JustTest.as_view()), 71 | ] 72 | ``` 73 | 74 | Define your view: 75 | 76 | ```python 77 | # your views.py 78 | 79 | from django.views import View 80 | from django.http.response import HttpResponse 81 | 82 | from django_simple_api import Query 83 | 84 | 85 | class JustTest(View): 86 | def get(self, request, id: int = Query()): 87 | return HttpResponse(id) 88 | ``` 89 | 90 | > ⚠️ To generate the document, you must declare the parameters according to the rules of ***Simple API*** (like the example above). 91 | > 92 | > Click [Declare parameters](https://django-simple-api.aber.sh/declare-parameters/) to see how to declare parameters. 93 | 94 | ### Access interface document 95 | 96 | After the above configuration, you can start your server and access the interface documentation now. 97 | 98 | If your service is running locally, you can visit [http://127.0.0.1:8000/docs/](http://127.0.0.1:8000/docs/) to view 99 | your documentation. 100 | 101 | ## More 102 | 103 | For more tutorials, see [Django Simple API](https://django-simple-api.aber.sh/). 104 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | **Language: [English](README.md) | Chinese** 2 | 3 | # Django Simple API 4 | ***Django Simple API*** 是基于 Django 的一个非侵入式组件,可以帮助您快速创建 API。 5 | 6 | ## 快速开始 7 | 8 | ### 安装 9 | 10 | 从 github 下载并安装: 11 | 12 | ```shell 13 | pip install django-simple-api 14 | ``` 15 | 16 | ### 配置 17 | 18 | 第一步:将 `django-simple-api` 添加到 `settings.INSTALLED_APPS` 中: 19 | 20 | ```python 21 | INSTALLED_APPS = [ 22 | ..., 23 | "django_simple_api", 24 | ] 25 | ``` 26 | 27 | 第二步:将中间件注册到 `settings.MIDDLEWARE` 中: 28 | 29 | ```python 30 | MIDDLEWARE = [ 31 | ..., 32 | "django_simple_api.middleware.SimpleApiMiddleware", 33 | ] 34 | ``` 35 | 36 | 第三步:将 `django-simple-api` 的 url 添加到根 urls.py 中: 37 | 38 | ```python 39 | # urls.py 40 | 41 | from django.urls import include, path 42 | from django.conf import settings 43 | 44 | # 根 urls 45 | urlpatterns = [ 46 | ... 47 | ] 48 | 49 | # dsa 的 urls, 应该只在测试环境运行! 50 | if settings.DEBUG: 51 | urlpatterns += [ 52 | # 接口文档 url 53 | path("docs/", include("django_simple_api.urls")) 54 | ] 55 | ``` 56 | 57 | ### 完成第一个示例 58 | 59 | 首先,定义一个路由: 60 | 61 | ```python 62 | # your urls.py 63 | 64 | from django.urls import path 65 | from yourviews import JustTest 66 | 67 | urlpatterns = [ 68 | ..., 69 | path("/path//", JustTest.as_view()), 70 | ] 71 | ``` 72 | 73 | 然后定义一个视图: 74 | 75 | ```python 76 | # your views.py 77 | 78 | from django.views import View 79 | from django.http.response import HttpResponse 80 | 81 | from django_simple_api import Query 82 | 83 | 84 | class JustTest(View): 85 | def get(self, request, id: int = Query()): 86 | return HttpResponse(id) 87 | ``` 88 | > ⚠️ 注意:要生成文档,必须使用 `django-simple-api` 的规则声明参数(如上图所示)! 89 | > 90 | > 点击 [声明参数](declare-parameters.md) 查看如何声明参数。 91 | 92 | ### 访问接口文档 93 | 94 | 完成上述配置和示例后,现在就可以启动服务器并访问接口文档了。 95 | 96 | 如果你的服务在本地运行,可以访问 [http://127.0.0.1:8000/docs/](http://127.0.0.1:8000/docs/) 来查看接口文档。 97 | 98 | ## 详细教程 99 | 100 | 更多详细教程, 请查看 [Django Simple API](https://django-simple-api.aber.sh/)。 101 | -------------------------------------------------------------------------------- /django_simple_api/__init__.py: -------------------------------------------------------------------------------- 1 | from .decorators import ( 2 | allow_request_method, 3 | describe_response, 4 | describe_responses, 5 | mark_tags, 6 | ) 7 | from .extras import describe_extra_docs 8 | from .fields import Body, Cookie, Header, Path, Query 9 | from .types import UploadFile 10 | from .utils import wrapper_include, wrapper_urlpatterns 11 | 12 | __all__ = ["Path", "Query", "Header", "Cookie", "Body"] 13 | __all__ += [ 14 | "allow_request_method", 15 | "describe_response", 16 | "describe_responses", 17 | "mark_tags", 18 | ] 19 | __all__ += ["describe_extra_docs"] 20 | __all__ += ["UploadFile"] 21 | __all__ += ["wrapper_include", "wrapper_urlpatterns"] 22 | 23 | default_app_config = "django_simple_api.apps.DjangoSimpleAPIConfig" 24 | -------------------------------------------------------------------------------- /django_simple_api/__version__.py: -------------------------------------------------------------------------------- 1 | VERSION = (0, 2, 1) 2 | 3 | __version__ = ".".join(map(str, VERSION)) 4 | -------------------------------------------------------------------------------- /django_simple_api/_fields.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from typing import Any 3 | 4 | if sys.version_info[:2] < (3, 8): 5 | from typing_extensions import Literal 6 | else: 7 | from typing import Literal 8 | 9 | from pydantic.fields import FieldInfo as _FieldInfo 10 | from pydantic.fields import Undefined 11 | 12 | from .exceptions import ExclusiveFieldError 13 | 14 | 15 | class FieldInfo(_FieldInfo): 16 | __slots__ = _FieldInfo.__slots__ 17 | 18 | _in: Literal["path", "query", "header", "cookie", "body"] 19 | 20 | def __init__(self, default: Any = Undefined, **kwargs: Any) -> None: 21 | self.exclusive = kwargs.pop("exclusive") 22 | if self.exclusive and any(kwargs.values()): 23 | raise ExclusiveFieldError( 24 | "The `exclusive=True` parameter cannot be used with other parameters at the same time." 25 | ) 26 | super().__init__(default, **kwargs) 27 | 28 | 29 | class PathInfo(FieldInfo): 30 | __slots__ = ("exclusive", *FieldInfo.__slots__) 31 | 32 | _in: Literal["path"] = "path" 33 | 34 | 35 | class QueryInfo(FieldInfo): 36 | __slots__ = ("exclusive", *FieldInfo.__slots__) 37 | 38 | _in: Literal["query"] = "query" 39 | 40 | 41 | class HeaderInfo(FieldInfo): 42 | __slots__ = ("exclusive", *FieldInfo.__slots__) 43 | 44 | _in: Literal["header"] = "header" 45 | 46 | 47 | class CookieInfo(FieldInfo): 48 | __slots__ = ("exclusive", *FieldInfo.__slots__) 49 | 50 | _in: Literal["cookie"] = "cookie" 51 | 52 | 53 | class BodyInfo(FieldInfo): 54 | __slots__ = ("exclusive", *FieldInfo.__slots__) 55 | 56 | _in: Literal["body"] = "body" 57 | -------------------------------------------------------------------------------- /django_simple_api/apps.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.apps import AppConfig 3 | 4 | from .utils import get_all_urls 5 | from .params import parse_and_bound_params 6 | from .serialize import serialize_model, serialize_queryset 7 | 8 | 9 | class DjangoSimpleAPIConfig(AppConfig): 10 | name = "django_simple_api" 11 | 12 | def ready(self): 13 | models.Model.to_json = serialize_model 14 | models.query.QuerySet.to_json = serialize_queryset 15 | models.query.RawQuerySet.to_json = serialize_queryset 16 | 17 | for url_format, http_handler in get_all_urls(): 18 | parse_and_bound_params(http_handler) 19 | -------------------------------------------------------------------------------- /django_simple_api/decorators.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from http import HTTPStatus 3 | from inspect import isclass 4 | from typing import Any, Callable, Dict, List, Type, TypeVar, Union 5 | 6 | from django.views import View 7 | from pydantic import BaseModel, create_model 8 | from pydantic.utils import display_as_type 9 | 10 | from .extras import describe_extra_docs 11 | 12 | if sys.version_info >= (3, 9): 13 | # https://www.python.org/dev/peps/pep-0585/ 14 | 15 | from types import GenericAlias 16 | 17 | GenericType = (GenericAlias, type(List[str])) 18 | else: 19 | GenericType = (type(List[str]),) 20 | 21 | T = TypeVar("T", bound=Callable) 22 | 23 | 24 | def allow_request_method(method: str) -> Callable[[T], T]: 25 | """ 26 | Declare the request method allowed by the view function. 27 | """ 28 | if method not in View.http_method_names: 29 | raise ValueError(f"`method` must in {View.http_method_names}") 30 | 31 | def wrapper(view_func: T) -> T: 32 | if isclass(view_func): 33 | raise RuntimeError( 34 | "`@allow_request_method` Can only be used for functions." 35 | ) 36 | 37 | if hasattr(view_func, "__method__"): 38 | raise RuntimeError( 39 | f"`{view_func.__qualname__}` already has the request method `{getattr(view_func, '__method__')}`, cannot repeat the statement!" 40 | ) 41 | 42 | setattr(view_func, "__method__", method.upper()) 43 | return view_func 44 | 45 | return wrapper 46 | 47 | 48 | def describe_response( 49 | status: Union[int, HTTPStatus], 50 | description: str = "", 51 | *, 52 | content: Union[Type[BaseModel], dict, type] = None, 53 | headers: dict = None, 54 | links: dict = None, 55 | ) -> Callable[[T], T]: 56 | """ 57 | Describe a response in HTTP view function 58 | 59 | https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#responseObject 60 | """ 61 | status = int(status) 62 | if not description: 63 | try: 64 | description = HTTPStatus(status).description 65 | except ValueError: 66 | description = "User-defined status code" 67 | 68 | def decorator(func: T) -> T: 69 | if not hasattr(func, "__responses__"): 70 | responses: Dict[int, Dict[str, Any]] = {} 71 | setattr(func, "__responses__", responses) 72 | else: 73 | responses = getattr(func, "__responses__") 74 | 75 | if ( 76 | content is None 77 | or isinstance(content, dict) 78 | or ( 79 | not isinstance(content, GenericType) 80 | and isclass(content) 81 | and issubclass(content, BaseModel) 82 | ) 83 | ): 84 | real_content = content 85 | else: 86 | real_content = create_model( 87 | f"ParsingModel[{display_as_type(content)}]", __root__=(content, ...) 88 | ) 89 | 90 | response = { 91 | "description": description, 92 | "content": real_content, 93 | "headers": headers, 94 | "links": links, 95 | } 96 | responses[status] = {k: v for k, v in response.items() if v} 97 | 98 | return func 99 | 100 | return decorator 101 | 102 | 103 | def describe_responses(responses: Dict[int, dict]) -> Callable[[T], T]: 104 | """ 105 | Describe responses in HTTP view function 106 | """ 107 | 108 | def decorator(func: T) -> T: 109 | for status, info in responses.items(): 110 | func = describe_response(status, **info)(func) 111 | return func 112 | 113 | return decorator 114 | 115 | 116 | def mark_tags(*tags: str) -> Callable[[T], T]: 117 | """ 118 | mark api tags 119 | """ 120 | 121 | def wrapper(handler: T) -> T: 122 | return describe_extra_docs(handler, {"tags": tags}) 123 | 124 | return wrapper 125 | -------------------------------------------------------------------------------- /django_simple_api/exceptions.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Any, Dict, List, Union 3 | 4 | from pydantic import ValidationError 5 | from pydantic.json import pydantic_encoder 6 | 7 | 8 | class ExclusiveFieldError(Exception): 9 | def __init__(self, message): 10 | self.message = message 11 | 12 | def __str__(self): 13 | return self.message 14 | 15 | 16 | class RequestValidationError(Exception): 17 | def __init__(self, validation_error: ValidationError) -> None: 18 | self.validation_error = validation_error 19 | 20 | def errors(self) -> List[Dict[str, Any]]: 21 | return self.validation_error.errors() 22 | 23 | def json(self, *, indent: Union[None, int, str] = 2) -> str: 24 | return json.dumps(self.errors(), indent=indent, default=pydantic_encoder) 25 | 26 | @staticmethod 27 | def schema() -> dict: 28 | return { 29 | "type": "array", 30 | "items": { 31 | "type": "object", 32 | "properties": { 33 | "loc": { 34 | "title": "Loc", 35 | "description": "error field", 36 | "type": "array", 37 | "items": {"type": "string"}, 38 | }, 39 | "type": { 40 | "title": "Type", 41 | "description": "error type", 42 | "type": "string", 43 | }, 44 | "msg": { 45 | "title": "Msg", 46 | "description": "error message", 47 | "type": "string", 48 | }, 49 | }, 50 | "required": ["loc", "type", "msg"], 51 | }, 52 | } 53 | -------------------------------------------------------------------------------- /django_simple_api/extras.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable, Dict, Sequence, TypeVar 2 | 3 | from .utils import is_class_view 4 | 5 | T = TypeVar("T", bound=Callable) 6 | 7 | 8 | def merge_openapi_info( 9 | operation_info: Dict[str, Any], more_info: Dict[str, Any] 10 | ) -> Dict[str, Any]: 11 | for key, value in more_info.items(): 12 | if key in operation_info: 13 | if isinstance(operation_info[key], Sequence): 14 | operation_info[key] = _ = list(operation_info[key]) 15 | _.extend(value) 16 | continue 17 | elif isinstance(operation_info[key], dict): 18 | operation_info[key] = merge_openapi_info(operation_info[key], value) 19 | continue 20 | operation_info[key] = value 21 | return operation_info 22 | 23 | 24 | def describe_extra_docs(handler: T, info: Dict[str, Any]) -> T: 25 | """ 26 | describe more openapi info in HTTP handler 27 | 28 | https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#operationObject 29 | """ 30 | if is_class_view(handler): 31 | view_class = handler.view_class # type: ignore 32 | for method in filter( 33 | lambda method: hasattr(view_class, method), view_class.http_method_names 34 | ): 35 | handler_method = getattr(view_class, method.lower()) 36 | __extra_docs__ = merge_openapi_info( 37 | getattr(handler_method, "__extra_docs__", {}), info 38 | ) 39 | setattr(handler_method, "__extra_docs__", __extra_docs__) 40 | else: 41 | __extra_docs__ = merge_openapi_info( 42 | getattr(handler, "__extra_docs__", {}), info 43 | ) 44 | setattr(handler, "__extra_docs__", __extra_docs__) 45 | return handler 46 | -------------------------------------------------------------------------------- /django_simple_api/fields.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | 3 | from pydantic.fields import NoArgAnyCallable, Undefined 4 | 5 | from ._fields import BodyInfo, CookieInfo, HeaderInfo, PathInfo, QueryInfo 6 | 7 | __all__ = ["Path", "Query", "Header", "Cookie", "Body"] 8 | 9 | 10 | def Path( 11 | default: Any = Undefined, 12 | *, 13 | default_factory: Optional[NoArgAnyCallable] = None, 14 | alias: str = None, 15 | title: str = None, 16 | description: str = None, 17 | exclusive: bool = False, 18 | **extra: Any, 19 | ) -> Any: 20 | """ 21 | Used to provide extra information about a field. 22 | 23 | :param default: since this is replacing the field’s default, its first argument is used 24 | to set the default, use ellipsis (``...``) to indicate the field is required 25 | :param default_factory: callable that will be called when a default value is needed for this field 26 | If both `default` and `default_factory` are set, an error is raised. 27 | :param alias: the public name of the field 28 | :param title: can be any string, used in the schema 29 | :param description: can be any string, used in the schema 30 | :param exclusive: decide whether this field receives all parameters 31 | :param **extra: any additional keyword arguments will be added as is to the schema 32 | """ 33 | field_info = PathInfo( 34 | default, 35 | default_factory=default_factory, 36 | alias=alias, 37 | title=title, 38 | description=description, 39 | exclusive=exclusive, 40 | **extra, 41 | ) 42 | field_info._validate() 43 | return field_info 44 | 45 | 46 | def Query( 47 | default: Any = Undefined, 48 | *, 49 | default_factory: Optional[NoArgAnyCallable] = None, 50 | alias: str = None, 51 | title: str = None, 52 | description: str = None, 53 | exclusive: bool = False, 54 | **extra: Any, 55 | ) -> Any: 56 | """ 57 | Used to provide extra information about a field. 58 | 59 | :param default: since this is replacing the field’s default, its first argument is used 60 | to set the default, use ellipsis (``...``) to indicate the field is required 61 | :param default_factory: callable that will be called when a default value is needed for this field 62 | If both `default` and `default_factory` are set, an error is raised. 63 | :param alias: the public name of the field 64 | :param title: can be any string, used in the schema 65 | :param description: can be any string, used in the schema 66 | :param exclusive: decide whether this field receives all parameters 67 | :param **extra: any additional keyword arguments will be added as is to the schema 68 | """ 69 | field_info = QueryInfo( 70 | default, 71 | default_factory=default_factory, 72 | alias=alias, 73 | title=title, 74 | description=description, 75 | exclusive=exclusive, 76 | **extra, 77 | ) 78 | field_info._validate() 79 | return field_info 80 | 81 | 82 | def Header( 83 | default: Any = Undefined, 84 | *, 85 | default_factory: Optional[NoArgAnyCallable] = None, 86 | alias: str = None, 87 | title: str = None, 88 | description: str = None, 89 | exclusive: bool = False, 90 | **extra: Any, 91 | ) -> Any: 92 | """ 93 | Used to provide extra information about a field. 94 | 95 | :param default: since this is replacing the field’s default, its first argument is used 96 | to set the default, use ellipsis (``...``) to indicate the field is required 97 | :param default_factory: callable that will be called when a default value is needed for this field 98 | If both `default` and `default_factory` are set, an error is raised. 99 | :param alias: the public name of the field 100 | :param title: can be any string, used in the schema 101 | :param description: can be any string, used in the schema 102 | :param exclusive: decide whether this field receives all parameters 103 | :param **extra: any additional keyword arguments will be added as is to the schema 104 | """ 105 | field_info = HeaderInfo( 106 | default, 107 | default_factory=default_factory, 108 | alias=alias, 109 | title=title, 110 | description=description, 111 | exclusive=exclusive, 112 | **extra, 113 | ) 114 | field_info._validate() 115 | return field_info 116 | 117 | 118 | def Cookie( 119 | default: Any = Undefined, 120 | *, 121 | default_factory: Optional[NoArgAnyCallable] = None, 122 | alias: str = None, 123 | title: str = None, 124 | description: str = None, 125 | exclusive: bool = False, 126 | **extra: Any, 127 | ) -> Any: 128 | """ 129 | Used to provide extra information about a field. 130 | 131 | :param default: since this is replacing the field’s default, its first argument is used 132 | to set the default, use ellipsis (``...``) to indicate the field is required 133 | :param default_factory: callable that will be called when a default value is needed for this field 134 | If both `default` and `default_factory` are set, an error is raised. 135 | :param alias: the public name of the field 136 | :param title: can be any string, used in the schema 137 | :param description: can be any string, used in the schema 138 | :param exclusive: decide whether this field receives all parameters 139 | :param **extra: any additional keyword arguments will be added as is to the schema 140 | """ 141 | field_info = CookieInfo( 142 | default, 143 | default_factory=default_factory, 144 | alias=alias, 145 | title=title, 146 | description=description, 147 | exclusive=exclusive, 148 | **extra, 149 | ) 150 | field_info._validate() 151 | return field_info 152 | 153 | 154 | def Body( 155 | default: Any = Undefined, 156 | *, 157 | default_factory: Optional[NoArgAnyCallable] = None, 158 | alias: str = None, 159 | title: str = None, 160 | description: str = None, 161 | exclusive: bool = False, 162 | **extra: Any, 163 | ) -> Any: 164 | """ 165 | Used to provide extra information about a field. 166 | 167 | :param default: since this is replacing the field’s default, its first argument is used 168 | to set the default, use ellipsis (``...``) to indicate the field is required 169 | :param default_factory: callable that will be called when a default value is needed for this field 170 | If both `default` and `default_factory` are set, an error is raised. 171 | :param alias: the public name of the field 172 | :param title: can be any string, used in the schema 173 | :param description: can be any string, used in the schema 174 | :param exclusive: decide whether this field receives all parameters 175 | :param **extra: any additional keyword arguments will be added as is to the schema 176 | """ 177 | field_info = BodyInfo( 178 | default, 179 | default_factory=default_factory, 180 | alias=alias, 181 | title=title, 182 | description=description, 183 | exclusive=exclusive, 184 | **extra, 185 | ) 186 | field_info._validate() 187 | return field_info 188 | -------------------------------------------------------------------------------- /django_simple_api/middleware.py: -------------------------------------------------------------------------------- 1 | import json 2 | from http import HTTPStatus 3 | from typing import Any, Callable, Dict, List, Optional 4 | 5 | from django.http.request import HttpRequest 6 | from django.http.response import ( 7 | HttpResponse, 8 | HttpResponseBadRequest, 9 | HttpResponseNotAllowed, 10 | ) 11 | from django.utils.deprecation import MiddlewareMixin 12 | 13 | from .exceptions import RequestValidationError 14 | from .params import verify_params 15 | from .utils import merge_query_dict 16 | 17 | 18 | class ParseRequestDataMiddleware(MiddlewareMixin): 19 | def process_request(self, request: HttpRequest) -> HttpResponse: 20 | request.JSON = None 21 | if request.content_type == "application/json": 22 | try: 23 | request.JSON = json.loads(request.body) 24 | except ValueError as ve: 25 | return HttpResponseBadRequest( 26 | "Unable to parse JSON data. Error: {0}".format(ve) 27 | ) 28 | request.DATA = request.JSON 29 | else: 30 | if request.method not in ("GET", "POST"): 31 | # if you want to know why do that, 32 | # read https://aber.sh/articles/Django-Parse-non-POST-Request/ 33 | if hasattr(request, "_post"): 34 | del request._post 35 | del request._files 36 | 37 | _shadow = request.method 38 | request.method = "POST" 39 | request._load_post_and_files() 40 | request.method = _shadow 41 | request.DATA = dict( 42 | **merge_query_dict(request.POST), **merge_query_dict(request.FILES) 43 | ) 44 | 45 | 46 | class ValidateRequestDataMiddleware(ParseRequestDataMiddleware): 47 | def process_view( 48 | self, 49 | request: HttpRequest, 50 | view_func: Callable, 51 | view_args: List[Any], 52 | view_kwargs: Dict[str, Any], 53 | ) -> Optional[HttpResponse]: 54 | 55 | # Put request method check before request parameters check. 56 | if hasattr(view_func, "__method__") and not ( 57 | getattr(view_func, "__method__", None) == request.method.upper() 58 | ): 59 | return HttpResponseNotAllowed([view_func.__method__]) # type: ignore 60 | 61 | try: 62 | view_kwargs.update(verify_params(view_func, request, view_kwargs)) 63 | return None 64 | except RequestValidationError as error: 65 | return self.process_validation_error(error) 66 | 67 | @staticmethod 68 | def process_validation_error( 69 | validation_error: RequestValidationError, 70 | ) -> HttpResponse: 71 | return HttpResponse( 72 | validation_error.json(), 73 | content_type="application/json", 74 | status=HTTPStatus.UNPROCESSABLE_ENTITY, 75 | ) 76 | 77 | 78 | class SimpleApiMiddleware(ValidateRequestDataMiddleware): 79 | """ 80 | Contains all the functional middleware of Django Simple API. More 81 | functions may be added to this middleware at any time. If you only 82 | need certain functions, please use other middleware explicitly. 83 | """ 84 | -------------------------------------------------------------------------------- /django_simple_api/params.py: -------------------------------------------------------------------------------- 1 | from inspect import isclass, signature 2 | from typing import Any, Callable, Dict, List, TypeVar 3 | 4 | from django.http.request import HttpRequest 5 | from pydantic import BaseModel, ValidationError, create_model 6 | 7 | from ._fields import FieldInfo 8 | from .exceptions import RequestValidationError, ExclusiveFieldError 9 | from .utils import is_class_view, merge_query_dict 10 | 11 | HTTPHandler = TypeVar("HTTPHandler", bound=Callable) 12 | 13 | 14 | def verify_params( 15 | handler: Any, request: HttpRequest, may_path_params: Dict[str, Any] 16 | ) -> Dict[str, Any]: 17 | """ 18 | Verify the parameters, and convert the parameters to the corresponding type. 19 | """ 20 | if is_class_view(handler): 21 | return _verify_params( 22 | getattr( 23 | handler.view_class, 24 | request.method.lower(), 25 | handler.view_class.http_method_not_allowed, 26 | ), 27 | request, 28 | may_path_params, 29 | ) 30 | return _verify_params(handler, request, may_path_params) 31 | 32 | 33 | def parse_and_bound_params(handler: Any) -> None: 34 | """ 35 | Get the parameters from the function signature and bind them to the properties of the function 36 | """ 37 | if is_class_view(handler): 38 | view_class = handler.view_class 39 | for method in view_class.http_method_names: 40 | if not hasattr(view_class, method): 41 | continue 42 | setattr( 43 | view_class, method, _parse_and_bound_params(getattr(view_class, method)) 44 | ) 45 | else: 46 | _parse_and_bound_params(handler) 47 | 48 | 49 | def _parse_and_bound_params(handler: HTTPHandler) -> HTTPHandler: 50 | sig = signature(handler) 51 | 52 | __parameters__: Dict[str, Any] = { 53 | "path": {}, 54 | "query": {}, 55 | "header": {}, 56 | "cookie": {}, 57 | "body": {}, 58 | } 59 | __exclusive_models__ = {} 60 | 61 | for name, param in sig.parameters.items(): 62 | default = param.default 63 | annotation = param.annotation 64 | 65 | if default == param.empty or not isinstance(default, FieldInfo): 66 | continue 67 | 68 | if getattr(default, "exclusive", False): 69 | if __parameters__[default._in] != {}: 70 | raise ExclusiveFieldError( 71 | f"You used exclusive parameter: `{default._in.capitalize()}(exclusive=True)`, " 72 | f"Please ensure the `{default._in.capitalize()}` field is unique in `{handler.__qualname__}`." 73 | ) 74 | 75 | if not (isclass(annotation) and issubclass(annotation, BaseModel)): 76 | raise TypeError( 77 | f"The `{name}` parameter of `{handler.__qualname__}` must use type annotations " 78 | f"and the type annotations must be a subclass of BaseModel." 79 | ) 80 | 81 | __parameters__[default._in] = annotation 82 | __exclusive_models__[annotation] = name 83 | continue 84 | 85 | if isclass(__parameters__[default._in]) and issubclass( 86 | __parameters__[default._in], BaseModel 87 | ): 88 | raise ExclusiveFieldError( 89 | f"You used exclusive parameter: `{default._in.capitalize()}(exclusive=True)`, " 90 | f"Please ensure the `{default._in.capitalize()}` field is unique in `{handler.__qualname__}`." 91 | ) 92 | 93 | if annotation != param.empty: 94 | __parameters__[default._in][name] = (annotation, default) 95 | else: 96 | __parameters__[default._in][name] = default 97 | 98 | for key in tuple(__parameters__.keys()): 99 | _params_ = __parameters__.pop(key) 100 | # _params_ is subclass of BaseModel 101 | if isclass(_params_) and issubclass(_params_, BaseModel): 102 | __parameters__[key] = _params_ 103 | # _params_ is have values 104 | elif _params_: 105 | __parameters__[key] = create_model("temporary_model", **_params_) # type: ignore 106 | # _params_ is empty of dict. 107 | else: 108 | continue 109 | 110 | if "body" in __parameters__: 111 | setattr(handler, "__request_body__", __parameters__.pop("body")) 112 | 113 | if __parameters__: 114 | setattr(handler, "__parameters__", __parameters__) 115 | 116 | if __exclusive_models__: 117 | setattr(handler, "__exclusive_models__", __exclusive_models__) 118 | 119 | return handler 120 | 121 | 122 | def _verify_params( 123 | handler: HTTPHandler, request: HttpRequest, may_path_params: Dict[str, Any] 124 | ) -> Dict[str, Any]: 125 | parameters = getattr(handler, "__parameters__", None) 126 | request_body = getattr(handler, "__request_body__", None) 127 | exclusive_models = getattr(handler, "__exclusive_models__", {}) 128 | if not (parameters or request_body or exclusive_models): 129 | return {} 130 | 131 | data: List[Any] = [] 132 | kwargs: Dict[str, Any] = {} 133 | try: 134 | # try to get parameters model and parse 135 | if parameters: 136 | if "path" in parameters: 137 | data.append(parameters["path"].parse_obj(may_path_params)) 138 | 139 | if "query" in parameters: 140 | data.append( 141 | parameters["query"].parse_obj(merge_query_dict(request.GET)) 142 | ) 143 | 144 | if "header" in parameters: 145 | data.append(parameters["header"].parse_obj(request.headers)) 146 | 147 | if "cookie" in parameters: 148 | data.append(parameters["cookie"].parse_obj(request.COOKIES)) 149 | 150 | # try to get body model and parse 151 | if request_body: 152 | data.append(request_body.parse_obj(request.DATA)) 153 | 154 | except ValidationError as e: 155 | raise RequestValidationError(e) 156 | 157 | # Update the verified parameters into the view into the parameters. 158 | for _data in data: 159 | if _data.__class__.__name__ == "temporary_model": 160 | kwargs.update(_data.dict()) 161 | else: 162 | kwargs[exclusive_models[_data.__class__]] = _data 163 | 164 | return kwargs 165 | -------------------------------------------------------------------------------- /django_simple_api/schema.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from copy import deepcopy 3 | from typing import Any, Dict, List, Optional, Tuple, Type, Union 4 | 5 | from pydantic import BaseModel 6 | 7 | from .types import UploadFile 8 | 9 | 10 | def schema_parameter( 11 | m: Optional[Type[BaseModel]], position: str 12 | ) -> List[Dict[str, Any]]: 13 | if m is None: 14 | return [] 15 | 16 | _schemas = deepcopy(m.schema()) 17 | properties: Dict[str, Any] = _schemas["properties"] 18 | required = _schemas.get("required", ()) 19 | 20 | return [ 21 | { 22 | "in": position, 23 | "name": name, 24 | "description": schema.pop("description", ""), 25 | "required": name in required, # type: ignore 26 | "schema": schema, 27 | } 28 | for name, schema in properties.items() 29 | ] 30 | 31 | 32 | def schema_request_body(body: Type[BaseModel] = None) -> Tuple[Optional[Dict], Dict]: 33 | if body is None: 34 | return None, {} 35 | 36 | _schema: Dict = deepcopy(body.schema()) 37 | definitions = _schema.pop("definitions", {}) 38 | content_type = "application/json" 39 | 40 | for field in body.__fields__.values(): 41 | if inspect.isclass(field.type_) and issubclass(field.type_, UploadFile): 42 | content_type = "multipart/form-data" 43 | 44 | return { 45 | "required": True, 46 | "content": {content_type: {"schema": _schema}}, 47 | }, definitions 48 | 49 | 50 | def schema_response(content: Union[Type[BaseModel], Dict]) -> Tuple[Dict, Dict]: 51 | if isinstance(content, dict): 52 | return content, {} 53 | schema = deepcopy(content.schema()) 54 | definitions = schema.pop("definitions", {}) 55 | return {"application/json": {"schema": schema}}, definitions 56 | -------------------------------------------------------------------------------- /django_simple_api/serialize.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from django.db import models 4 | from django.conf import settings 5 | 6 | from django_simple_api.utils import string_convert, do_nothing 7 | 8 | 9 | def serialize_model(self: models.Model, excludes: List[str] = None) -> dict: 10 | """ 11 | 模型序列化,会根据 select_related 和 prefetch_related 关联查询的结果进行序列化,可以在查询时使用 only、defer 来筛选序列化的字段。 12 | 它不会自做主张的去查询数据库,只用你查询出来的结果,成功避免了 N+1 查询问题。 13 | 14 | # See: 15 | https://aber.sh/articles/A-new-idea-of-serializing-Django-model/ 16 | """ 17 | excludes = excludes or [] 18 | serialized = set() 19 | 20 | if getattr(settings, "DSA_SERIALIZE_TO_CAMELCASE", False): 21 | to_camel_case_func = string_convert 22 | else: 23 | to_camel_case_func = do_nothing 24 | 25 | def _serialize_model(model) -> dict: 26 | 27 | # 当 model 存在一对一字段时,会陷入循环,使用闭包的自由变量存储已序列化的 model, 28 | # 在第二次循环到该 model 时直接返回 model.pk,不再循环。 29 | nonlocal serialized 30 | if model in serialized: 31 | return model.pk 32 | else: 33 | serialized.add(model) 34 | 35 | # 当 model 存在一对一或一对多字段,且该字段的值为 None 时,直接返回空{},否则会报错。 36 | if model is None: 37 | return {} 38 | 39 | result = { 40 | to_camel_case_func(name): _serialize_model(foreign_key) 41 | for name, foreign_key in model.__dict__["_state"] 42 | .__dict__.get("fields_cache", {}) 43 | .items() 44 | } 45 | 46 | buried_fields = getattr(model, "buried_fields", []) 47 | 48 | for name, value in model.__dict__.items(): 49 | 50 | # 敏感字段不需要序列化 51 | if name in buried_fields: 52 | continue 53 | 54 | # 私有属性不需要序列化 55 | if name.startswith("_"): 56 | continue 57 | 58 | result[to_camel_case_func(name)] = value 59 | 60 | for name, queryset in model.__dict__.get( 61 | "_prefetched_objects_cache", {} 62 | ).items(): 63 | result[to_camel_case_func(name)] = [_serialize_model(model) for model in queryset] # type: ignore 64 | 65 | return result 66 | 67 | results = _serialize_model(self) 68 | 69 | # 剔除排斥的字段 70 | for field_name in excludes: 71 | del results[to_camel_case_func(field_name)] 72 | 73 | return results 74 | 75 | 76 | def serialize_queryset(self: models.QuerySet, excludes: List[str] = None) -> List[dict]: 77 | return [serialize_model(model, excludes) for model in self] 78 | -------------------------------------------------------------------------------- /django_simple_api/templates/redoc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | OpenAPI power by Django-Simple-Api 6 | 7 | 8 | 9 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /django_simple_api/templates/swagger.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | OpenAPI power by Django-Simple-Api 7 | 8 | 24 | 25 | 26 | 27 |
28 | 29 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /django_simple_api/types.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | 3 | from django.core.files.base import File 4 | 5 | __all__ = ["UploadFile", "UploadImage"] 6 | 7 | 8 | class UploadFile(File): 9 | """ 10 | Wrapping Django File for `pydantic` 11 | """ 12 | 13 | @classmethod 14 | def __get_validators__(cls): 15 | yield cls.validate 16 | 17 | @classmethod 18 | def __modify_schema__(cls, field_schema): 19 | field_schema.update(type="string", format="binary") 20 | 21 | @classmethod 22 | def validate(cls, v): 23 | if not isinstance(v, File): 24 | raise TypeError("file required") 25 | return v 26 | 27 | def __repr__(self): 28 | return f"File({self.name})" 29 | 30 | 31 | class UploadImage(UploadFile): 32 | @classmethod 33 | def validate(cls, v): 34 | v = super().validate(v) 35 | 36 | from PIL import Image 37 | 38 | # We need to get a file object for Pillow. We might have a path or we might 39 | # have to read the data into memory. 40 | if hasattr(v, "temporary_file_path"): 41 | file = v.temporary_file_path() 42 | else: 43 | file = BytesIO(v.read()) 44 | try: 45 | # load() could spot a truncated JPEG, but it loads the entire 46 | # image in memory, which is a DoS vector. See #3848 and #18520. 47 | image = Image.open(file) 48 | # verify() must be called immediately after the constructor. 49 | image.verify() 50 | 51 | # Annotating so subclasses can reuse it for their own validation 52 | v.image = image 53 | # Pillow doesn't detect the MIME type of all formats. In those 54 | # cases, content_type will be None. 55 | v.content_type = Image.MIME.get(image.format) 56 | except Exception: 57 | # Pillow doesn't recognize it as an image. 58 | raise TypeError( 59 | "Upload a valid image. The file you uploaded " 60 | "was either not an image or a corrupted image." 61 | ) 62 | if hasattr(v, "seek") and callable(v.seek): 63 | v.seek(0) 64 | return v 65 | 66 | def __repr__(self): 67 | return f"Image({self.name})" 68 | -------------------------------------------------------------------------------- /django_simple_api/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | app_name = "django_simple_api" 6 | 7 | urlpatterns = [ 8 | path("", views.docs, name="docs"), 9 | path("get-docs/", views.get_docs, name="get_docs"), 10 | path("get-static/", views.get_static, name="get_static"), 11 | ] 12 | -------------------------------------------------------------------------------- /django_simple_api/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | from functools import update_wrapper 3 | from typing import Any, Callable, Generator, List, Sequence, Tuple, TypeVar, Union 4 | 5 | from django.conf import settings 6 | from django.http.request import QueryDict 7 | from django.urls import URLPattern, URLResolver 8 | from django.urls.conf import RegexPattern, RoutePattern 9 | 10 | T = TypeVar("T", bound=Callable) 11 | 12 | RE_PATH_PATTERN = re.compile(r"\(\?P<(?P\w*)>.*?\)") 13 | PATH_PATTERN = re.compile(r"<(.*?:)?(?P\w*)>") 14 | REPLACE_RE_FLAG_PATTERN = re.compile(r"(? str: 18 | path_format = str(pattern) 19 | if isinstance(pattern, RoutePattern): 20 | pattern = PATH_PATTERN 21 | else: # RegexPattern 22 | path_format = re.sub(REPLACE_RE_FLAG_PATTERN, "", path_format) 23 | pattern = RE_PATH_PATTERN 24 | return re.sub(pattern, r"{\g}", path_format) 25 | 26 | 27 | def get_urls( 28 | urlpatterns: List[Union[URLPattern, URLResolver]], 29 | prefix: str = "", 30 | ) -> Generator[Tuple[str, Any], None, None]: 31 | for item in urlpatterns: 32 | if isinstance(item, URLPattern): 33 | yield prefix + _reformat_pattern(item.pattern), item.callback 34 | else: 35 | yield from get_urls( 36 | item.url_patterns, prefix + _reformat_pattern(item.pattern) 37 | ) 38 | 39 | 40 | def get_all_urls() -> Generator[Tuple[str, Any], None, None]: 41 | yield from get_urls( 42 | __import__(settings.ROOT_URLCONF, {}, {}, [""]).urlpatterns, "/" 43 | ) 44 | 45 | 46 | def merge_query_dict(query_dict: QueryDict) -> dict: 47 | return {k: v if len(v) > 1 else v[0] for k, v in query_dict.lists() if len(v) > 0} 48 | 49 | 50 | def is_class_view(handler: Callable) -> bool: 51 | """ 52 | Judge handler is django.views.View subclass 53 | """ 54 | return hasattr(handler, "view_class") 55 | 56 | 57 | def _wrapper_handler(wrappers: Sequence[Callable[[T], T]], handler: T) -> T: 58 | _handler = handler 59 | for wrapper in wrappers: 60 | handler = wrapper(handler) 61 | if _handler is handler: 62 | continue 63 | handler = update_wrapper(handler, _handler) # type: ignore 64 | _handler = handler 65 | return handler 66 | 67 | 68 | def wrapper_urlpatterns( 69 | wrappers: Sequence[Callable[[T], T]], 70 | urlpatterns: List[Union[URLPattern, URLResolver]], 71 | ): 72 | for item in urlpatterns: 73 | if isinstance(item, URLPattern): 74 | _wrapper_handler(wrappers, item.callback) 75 | else: 76 | wrapper_urlpatterns(wrappers, item.url_patterns) 77 | 78 | 79 | def wrapper_include(wrappers: Sequence[Callable[[T], T]], view: Any) -> Any: 80 | if isinstance(view, (list, tuple)): 81 | # For include(...) processing. 82 | urlconf_module = view[0] 83 | urlpatterns = getattr(urlconf_module, "urlpatterns", urlconf_module) 84 | wrapper_urlpatterns(wrappers, urlpatterns) 85 | elif callable(view): 86 | view = _wrapper_handler(wrappers, view) 87 | else: 88 | raise TypeError( 89 | "view must be a callable or a list/tuple in the case of include()." 90 | ) 91 | return view 92 | 93 | 94 | def string_convert(string: str): 95 | """ 96 | 将连字符格式转成小驼峰格式 97 | example: user_name -> userName 98 | """ 99 | 100 | char_list = string.split("_") 101 | 102 | # 首个单词不转换 103 | for index, char in enumerate(char_list[1:], start=1): 104 | char_list[index] = char.capitalize() 105 | 106 | return "".join(char_list) 107 | 108 | 109 | def do_nothing(x): 110 | return x 111 | 112 | 113 | if __name__ == "__main__": 114 | s = string_convert("user") 115 | print(s) 116 | -------------------------------------------------------------------------------- /django_simple_api/views.py: -------------------------------------------------------------------------------- 1 | import operator 2 | import warnings 3 | from pathlib import Path 4 | from copy import deepcopy 5 | from functools import reduce 6 | from typing import Any, Dict, Tuple 7 | 8 | from django.http.response import JsonResponse, HttpResponse 9 | from django.shortcuts import render 10 | 11 | from .exceptions import RequestValidationError 12 | from .extras import merge_openapi_info 13 | from .schema import schema_parameter, schema_request_body, schema_response 14 | from .utils import get_all_urls, is_class_view 15 | 16 | 17 | def docs(request, template_name: str = "swagger.html", **kwargs: Any): 18 | return render(request, template_name, context={}) 19 | 20 | 21 | def _generate_method_docs(function) -> Tuple[Dict[str, Any], Dict[str, Any]]: 22 | result: Dict[str, Any] = {} 23 | definitions: Dict[str, Any] = {} 24 | 25 | doc = function.__doc__ 26 | if isinstance(doc, str): 27 | clean_doc = "\n".join(i.strip() for i in doc.strip().splitlines()) 28 | result.update(zip(("summary", "description"), clean_doc.split("\n\n", 1))) 29 | 30 | # generate params schema 31 | parameters = reduce( 32 | operator.add, 33 | [ 34 | schema_parameter(getattr(function, "__parameters__", {}).get(key), key) 35 | for key in ["path", "query", "header", "cookie"] 36 | ], 37 | ) 38 | result["parameters"] = parameters 39 | 40 | # generate request body schema 41 | request_body, _definitions = schema_request_body( 42 | getattr(function, "__request_body__", None) 43 | ) 44 | result["requestBody"] = request_body 45 | definitions.update(_definitions) 46 | 47 | # generate responses schema 48 | __responses__ = getattr(function, "__responses__", {}) 49 | responses: Dict[int, Any] = {} 50 | if parameters or request_body: 51 | responses[422] = { 52 | "content": { 53 | "application/json": {"schema": RequestValidationError.schema()} 54 | }, 55 | "description": "Failed to verify request parameters", 56 | } 57 | 58 | for status, info in __responses__.items(): 59 | _ = responses[int(status)] = dict(info) 60 | if _.get("content") is not None: 61 | _["content"], _definitions = schema_response(_["content"]) 62 | definitions.update(_definitions) 63 | 64 | result["responses"] = responses 65 | 66 | # merge user custom operation info 67 | return ( 68 | merge_openapi_info( 69 | {k: v for k, v in result.items() if v}, 70 | getattr(function, "__extra_docs__", {}), 71 | ), 72 | definitions, 73 | ) 74 | 75 | 76 | def _generate_path_docs(handler) -> Tuple[Dict[str, Any], Dict[str, Any]]: 77 | result: Dict[str, Any] = {} 78 | definitions: Dict[str, Any] = {} 79 | if is_class_view(handler): 80 | view_class = handler.view_class 81 | for method in filter( 82 | lambda method: hasattr(view_class, method) and method not in ("options",), 83 | view_class.http_method_names, 84 | ): 85 | result[method], _definitions = _generate_method_docs( 86 | getattr(view_class, method) 87 | ) 88 | definitions.update(_definitions) 89 | else: 90 | if hasattr(handler, "__method__"): 91 | result[handler.__method__.lower()], _definitions = _generate_method_docs( 92 | handler 93 | ) 94 | definitions.update(_definitions) 95 | elif ( 96 | hasattr(handler, "__parameters__") 97 | or hasattr(handler, "__request_body__") 98 | or hasattr(handler, "__responses__") 99 | ): 100 | warnings.warn( 101 | "You used the type identifier but did not declare the " 102 | f"request method allowed by the function {handler.__qualname__}. We cannot " 103 | "generate the OpenAPI document of this function for you!" 104 | ) 105 | return {k: v for k, v in result.items() if v}, definitions 106 | 107 | 108 | def get_docs( 109 | request, 110 | title: str = "Django Simple API", 111 | description: str = "This is description of your API document.", 112 | version: str = "0.1.0", 113 | **kwargs: Any, 114 | ): 115 | openapi_docs = { 116 | "openapi": "3.0.0", 117 | "info": {"title": title, "description": description, "version": version}, 118 | "servers": [ 119 | { 120 | "url": request.build_absolute_uri("/"), 121 | "description": "Current API Server Host", 122 | }, 123 | { 124 | "url": "{schema}://{address}/", 125 | "description": "Custom API Server Host", 126 | "variables": { 127 | "schema": { 128 | "default": request.scheme, 129 | "description": "http or https", 130 | }, 131 | "address": { 132 | "default": request.get_host(), 133 | "description": "api server's host[:port]", 134 | }, 135 | }, 136 | }, 137 | ], 138 | } 139 | definitions = {} 140 | paths = {} 141 | for url_pattern, view in get_all_urls(): 142 | paths[url_pattern], _definitions = _generate_path_docs(view) 143 | definitions.update(_definitions) 144 | openapi_docs["paths"] = {k: v for k, v in paths.items() if v} 145 | openapi_docs["definitions"] = deepcopy(definitions) 146 | return JsonResponse(openapi_docs, json_dumps_params={"ensure_ascii": False}) 147 | 148 | 149 | def get_static(request): 150 | file_no = request.GET.get("file_no") 151 | 152 | static_path = Path(__file__).parent.absolute() / "static" 153 | if file_no == "1": 154 | file_path = static_path / "swagger-ui.css" 155 | content_type = "text/css" 156 | elif file_no == "2": 157 | file_path = static_path / "swagger-ui-bundle.js" 158 | content_type = "application/x-javascript" 159 | 160 | elif file_no == "3": 161 | file_path = static_path / "redoc.standalone.js" 162 | content_type = "application/x-javascript" 163 | else: 164 | return HttpResponse() 165 | 166 | with open(file_path, "rb") as file: 167 | file = file.read() 168 | 169 | response = HttpResponse(file) 170 | response["Content-Type"] = content_type 171 | return response 172 | -------------------------------------------------------------------------------- /docs/declare-parameters.md: -------------------------------------------------------------------------------- 1 | 参数声明是 `django-simple-api` 的基础。无论是自动验证请求参数,还是自动生成接口文档, 2 | 都必须先学会如何声明参数。 3 | 4 | 你可以像下方示例一样声明参数: 5 | 6 | ```python 7 | # views.py 8 | 9 | from django.views import View 10 | from django.http.response import HttpResponse 11 | 12 | from django_simple_api import Query 13 | 14 | class JustTest(View): 15 | def get(self, request, 16 | # `id` 是参数名称 17 | # `int` 是参数类型 18 | # `Query` 表示参数所在的位置,并且可以描述有关参数的各种信息 19 | id: int = Query() 20 | ): 21 | return HttpResponse(id) 22 | ``` 23 | 24 | 25 | ## 字段 26 | `django-simple-api` 总共有 5 种字段,分别代表不同位置的参数: 27 | 28 | **全部字段及其描述:** 29 | 30 | | Field | Description | 31 | | --- | --- | 32 | | Query | 表示此参数为 url 查询参数,example: http://host/?param=1| 33 | | Path | 表示此参数为 url 路径参数,example: http://host/{param}/| 34 | | Body | 表示此参数在 body 中,注意:此时该参数只能在非 GET 请求中获取| 35 | | Cookie | 表示此参数在 Cookie 中获取| 36 | | Header | 表示此参数在 Header 中获取| 37 | 38 | 39 | **示例:** 40 | ```python 41 | # urls.py 42 | 43 | urlpatterns = [ 44 | ... 45 | path("/path///", JustTest.as_view(), name="path_name"), 46 | ] 47 | ``` 48 | 49 | ```python 50 | # views.py 51 | 52 | from django_simple_api import Query, Path, Body, Cookie, Header 53 | 54 | 55 | class JustTest(View): 56 | # 以下示例中使用的参数名称仅用于演示。 57 | def post(self, request, 58 | # param1 将会从查询字符串中获取 59 | param1: int = Query(), 60 | 61 | # param2 param3 将从 url 路径中获取 62 | # 可以多次使用来获取多个参数 63 | param2: int = Path(), 64 | param3: str = Path(), 65 | 66 | # param4 将从 body 中获取 67 | param4: str = Body(), 68 | 69 | # userid username 将从 Cookie 中获取 70 | # alias 表示将从 Cookie 获取名为 uid 的参数,然后赋值给 userid 71 | userid: int = Cookie(alias="uid"), 72 | username: int = Cookie(alias="user_name"), 73 | 74 | # token 将从 Header 中获取 75 | csrf_token: str = Header(alias="X-Csrf-Token"), 76 | ): 77 | 78 | return HttpResponse(username) 79 | ``` 80 | 81 | > ⚠️ 注意:当需要从 Header 中获取参数时,可能需要使用 alias 来指明要获取的请求头,因为请求头的名称可能不是有效的 Python 标识符。 82 | 83 | 84 | ## 字段属性 85 | 默认情况下,使用字段声明的参数,都是必填参数。如下个例子中,如果 url 中没有 `id` 参数,则会返回一个客户端错误: 86 | ```python 87 | class JustTest(View): 88 | def get(self, request, id: int = Query()): 89 | return HttpResponse(id) 90 | ``` 91 | 92 | ```shell 93 | [ 94 | { 95 | "loc": [ 96 | "id" 97 | ], 98 | "msg": "field required", 99 | "type": "value_error.missing" 100 | } 101 | ] 102 | ``` 103 | 104 | ### 非必填和默认参数 105 | 如果你想将参数设置为非必填,或者设置一个默认值,则可以使用以下方法: 106 | ```python 107 | # views.py 108 | 109 | class JustTest(View): 110 | def get(self, request, 111 | # 参数 name 为非必填 112 | name: str = Query(None), 113 | 114 | # 参数 id 默认值为 10 115 | id: int = Query(default=10) 116 | ): 117 | return HttpResponse(id) 118 | ``` 119 | 120 | 或者你可以使用 `default_factory` 传入一个函数来动态计算默认值: 121 | 122 | ```python 123 | # views.py 124 | 125 | def func(): 126 | return 1000 127 | 128 | class JustTest(View): 129 | def get(self, request, id: int = Query(default_factory=func)): 130 | print(id) # 1000 131 | return HttpResponse(id) 132 | ``` 133 | 134 | > ⚠️ 注意:不能同时使用 `default` 和 `default_factory`,否则会报错 `ValueError: cannot specify both default and default_factory`。 135 | 136 | ### 其他的字段属性 137 | | Name | description | 138 | | --- | --- | 139 | | default | 参数的默认值 | 140 | | default_factory| 生成参数默认值的可调用对象,`default_factory` 和 `default` 不可同时使用 | 141 | | alias | 别名,从目标位置获取参数的真正名字,接口文档也会展示此名字,默认同参数名 | 142 | | title | 任意字符串 | 143 | | description | 参数描述,将会展示在接口文档中 | 144 | | exclusive | 独占模式,使用该模式时,必须定义一个 `pydantic.BaseModel` 的子类作为表单,一次性接受所有参数;点击 [查看示例](#独占模式) | 145 | 146 | 147 | #### 独占模式 148 | `exclusive` 是一个特殊的属性,当你有一整个表单需要提交时,可以使用独占模式。 149 | 150 | 首先使用 `pydantic.BaseModel` 来定义一个数据模型,然后使用数据模型从指定字段(`Query`、`Body`)中一次性接收所有同字段类型参数: 151 | ```python 152 | # views.py 153 | 154 | from django.views import View 155 | from pydantic import BaseModel, Field 156 | 157 | from django_simple_api import Body 158 | 159 | 160 | # 使用 `pydantic.BaseModel` 来定义一个数据模型 161 | class UserForm(BaseModel): 162 | name: str = Field(max_length=25, description="This is user's name") 163 | age: int = Field(19, description="This is user's age") 164 | 165 | 166 | class UserView(View): 167 | def post(self, request, 168 | # 使用 `exclusive=True` 从 Body 中一次性获取所有参数: 169 | user: UserForm = Body(exclusive=True) 170 | ): 171 | 172 | # 然后可以从数据模型中获取参数 173 | name = user.name 174 | age = user.age 175 | 176 | # 也可以将其转换成字典: 177 | user.dict() # {"name": ..., "age": ...} 178 | 179 | # 更多数据模型的用法,请查看 https://pydantic-docs.helpmanual.io/usage/exporting_models/#modeldict 180 | 181 | return HttpResponse("success") 182 | ``` 183 | > ⚠️ 注意,当使用独占模式时,你不能再在同一个接口中使用相同的字段,但不影响其他字段的使用。 184 | > 例如上述示例中使用了 `user: UserForm = Body(exclusive=True)`,那么就不能再使用 `id:int = Body()` 来声明参数,因为 Body 中所有的参数,都会聚合到 `user` 中。 185 | > 但是你可以使用其他的字段类型,如 `Query` `Path` 等。 186 | 187 | 188 | ## 类型转换 189 | `django-simple-api` 还具有类型转换的功能。如果你传入的参数对于声明的类型是合法的,它会被自动转换为声明的类型,无需手动操作: 190 | 191 | ```python 192 | # views.py 193 | 194 | class JustTest(View): 195 | def get(self, request, 196 | # 接收到的是个日期字符串,会被直接转成 date 类型 197 | last_time: datetime.date = Query() 198 | ): 199 | 200 | # 2008-08-08 201 | print(last_time, type(last_time)) 202 | 203 | return HttpResponse(last_time) 204 | ``` 205 | 206 | 207 | ## 约束请求参数 208 | 209 | > `django-simple-api` 利用 `pydantic` 来对请求参数做出限制,每种类型的参数拥有不同的限制,下方示例中展示了常用的参数类型及其限制条件, 210 | > 更多教程请查阅 [Pydantic](https://pydantic-docs.helpmanual.io/usage/models/)。 211 | 212 | ```python 213 | # views.py 214 | from django.views import View 215 | from pydantic import conint, confloat, constr, conlist 216 | from django_simple_api import Query, Body 217 | 218 | class JustTest(View): 219 | 220 | # 数值类型,可以使用 ge、gt、le、lt、multipleOf 等限制条件。 221 | def get(self, request, 222 | p1: conint(gt=10) = Query(), # 必须 > 10 223 | p2: conint(ge=10) = Query(), # 必须 >= 10 224 | p3: confloat(lt=10.5) = Query(), # 必须 < 10.5 225 | p4: confloat(le=10.5) = Query(), # 必须 <= 10.5 226 | p5: conint(multiple_of=10) = Query(), # 必须是 10 的倍数 227 | ): 228 | return HttpResponse("Success") 229 | 230 | # 字符串类型, 可以使用 strip_whitespace、to_lower、max_length、min_length、curtail_length、regex 等限制条件。 231 | def get(self, request, 232 | p1: constr(strip_whitespace=True) = Query(), # 去掉两边的空白字符 233 | p2: constr(to_lower=True) = Query(), # 将字符串转换为小写 234 | p3: constr(max_length=20) = Query(), # 字符串的最大长度不能超过 20 235 | p4: constr(min_length=8) = Query(), # 字符串的最小长度不能小于 8 236 | p5: constr(curtail_length=10) = Query(), # 截取字符串前 10 个字符 237 | p6: constr(regex='/^(?:(?:\+|00)86)?1[3-9]\d{9}$/') = Query(), # 字符串是否符合正则表达式 238 | ): 239 | return HttpResponse("Success") 240 | 241 | # 列表类型,可以使用 item_type、max_items、min_items 等限制条件。 242 | def post(self, request, 243 | p1: conlist(item_type=int, min_items=1) = Body(), # 列表中至少有 1 个元素,并且应为 int 类型。 244 | p2: conlist(item_type=str, max_items=5) = Body(), # 列表中最多有 5 个元素,并且应为 str 类型。 245 | ): 246 | return HttpResponse("Success") 247 | ``` 248 | 249 | 250 | ## 更多 251 | 完成上述教程后,你应该明白怎么声明参数了。接下来就可以使用 **参数校验** 和 **生成接口文档** 的功能了。 252 | 253 | 点击 [参数校验](parameter-verification.md) 查看如何自动校验请求参数。 254 | 255 | 点击 [生成文档](document-generation.md) 查看如何自动生成接口文档。 256 | -------------------------------------------------------------------------------- /docs/document-generation.md: -------------------------------------------------------------------------------- 1 | **提示:** 2 | 如果要自动生成接口文档,必须将 `django-simple-api` 的 url 添加到你的根 urls.py 中,参考 [快速入门](quick-start.md)。 3 | 4 | ## 修改接口文档描述信息 5 | 你可以在添加 url 的地方修改接口文档中的描述信息,如下: 6 | 7 | ```python 8 | # urls.py 9 | 10 | from django.urls import include, path 11 | from django.conf import settings 12 | 13 | 14 | # 根 urls 15 | urlpatterns = [ 16 | ... 17 | ] 18 | 19 | # dsa 的 urls, 应该只在测试环境运行! 20 | if settings.DEBUG: 21 | urlpatterns += [ 22 | # # 接口文档 url 23 | path( 24 | "docs/", 25 | include("django_simple_api.urls"), 26 | { 27 | "template_name": "swagger.html", 28 | "title": "Django Simple API", 29 | "description": "This is description of your interface document.", 30 | "version": "0.1.0", 31 | }, 32 | ), 33 | ] 34 | ``` 35 | 36 | 在上述示例中,你可以修改 `template_name` 来改变界面文档的 UI 主题,我们目前有两个 UI 主题:swagger.html 和 redoc.html。 37 | 38 | 然后你可以通过 `title`、`description` 和 `version` 来修改接口文档的标题、描述和版本。 39 | 40 | 41 | ## 使用函数视图生成接口文档 42 | 如果你正在使用的是类视图,那你不需要这一步,你可以直接启动服务,查看接口文档了。 43 | 44 | 但是如果使用函数视图,则必须声明函数视图支持的请求方法: 45 | ```python 46 | # views.py 47 | 48 | from django_simple_api import allow_request_method 49 | 50 | @allow_request_method("get") 51 | def just_test(request, id: int = Query()): 52 | return HttpResponse(id) 53 | ``` 54 | 55 | `@allow_request_method` 只能声明一种请求方法,并且必须是 `['get', 'post', 'put', 'patch', 'delete', 'head', 'options', trace']` 中的一种。 56 | 57 | 同一个视图函数不支持多次使用 `@allow_request_method`,每个请求方法应该有一个单独的视图函数来支持,否则我们无法知晓这个视图函数的参数,是用于 `get` 方法还是 `post` 方法。 58 | 59 | 注意,如果使用 `@allow_request_method("get")`,则不能使用除 `get` 以外的请求方法,否则会返回 `405 Method Not Allow` 错误。 60 | 61 | 你也可以不使用 `@allow_request_method`,这不会产生任何负面影响,只是无法生成文档。 62 | 如果你没有在函数视图上使用 `@allow_request_method`,我们会用 `warning.warn()` 来提醒你,这不是个问题,只是为了防止你忘记使用它。 63 | 64 | 现在,函数视图也可以生成接口文档了,你可以访问你的服务器查看效果。 65 | 66 | ## 完善接口信息 67 | `django-simple-api` 是利用 [`OpenAPI`](https://github.com/OAI/OpenAPI-Specification) 的规范来生成接口文档的。 68 | 除了自动生成的参数信息外,你还可以手动为每个接口添加更加详细的信息,例如 `summary`、`description `、`responses` 和 `tags`。 69 | 70 | ### 添加 `summary` 和 `description` 71 | `summary` 用于简要介绍接口的功能,`description` 则用于详细描述接口更多的信息。 72 | 73 | ```python 74 | # views.py 75 | 76 | class JustTest(View): 77 | def get(self, request, id: int = Query()): 78 | """ 79 | This is summary. 80 | 81 | This is description ... 82 | This is description ... 83 | """ 84 | return HttpResponse(id) 85 | ``` 86 | > ⚠️ 注意: `summary` 和 `description` 之间必须有一个空行,如果没有空行,则全部视为 `summary`。 87 | 88 | 89 | ### 添加响应信息 90 | 91 | `responses` 也是接口文档中的重要信息,你可以定义接口在各种情况下应返回的数据格式和类型。 92 | 93 | `django-simple-api` 强烈推荐使用 `pydantic.BaseModel` 来定义响应信息的数据结构,例如: 94 | 95 | ```python 96 | # views.py 97 | 98 | from typing import List 99 | 100 | from pydantic import BaseModel 101 | from django.views import View 102 | 103 | from django_simple_api import describe_response 104 | 105 | 106 | # 为 `response` 定义数据结构和类型 107 | class JustTestResponses(BaseModel): 108 | code: str 109 | message: str 110 | data: List[dict] 111 | 112 | 113 | class JustTest(View): 114 | # 使用 @describe_response 为接口添加响应信息 115 | @describe_response(200, content=JustTestResponses) 116 | def get(self, request, id: int = Query()): 117 | 118 | # 实际响应数据(仅做演示) 119 | resp = { 120 | "code": "0", 121 | "message": "success", 122 | "data": [ 123 | {"id": 0, "name": "Tom"}, 124 | {"id": 1, "name": "John"}, 125 | ] 126 | } 127 | return JsonResponse(resp) 128 | ``` 129 | 130 | 最终,接口文档会展示为: 131 | 132 | ```shell 133 | { 134 | "code": "string", 135 | "message": "string", 136 | "data": [ 137 | {} 138 | ] 139 | } 140 | ``` 141 | 142 | 你也可以直接在接口文档中展示 '示例',只需将 '示例' 添加到 `pydantic.BaseModel` 中即可: 143 | 144 | ```python 145 | # views.py 146 | 147 | class JustTestResponses(BaseModel): 148 | code: str 149 | message: str 150 | data: List[dict] 151 | 152 | class Config: 153 | # 添加 ‘示例’ 154 | schema_extra = { 155 | "example": { 156 | "code": "0", 157 | "message": "success", 158 | "data": [ 159 | {"id": 0, "name": "Tom"}, 160 | {"id": 1, "name": "John"}, 161 | ] 162 | } 163 | } 164 | 165 | 166 | class JustTest(View): 167 | 168 | @describe_response(200, content=JustTestResponses) 169 | def get(self, request, id: int = Query()): 170 | resp = {...} 171 | return JsonResponse(resp) 172 | ``` 173 | 174 | 最终接口文档会展示为: 175 | 176 | ```shell 177 | { 178 | "code": "0", 179 | "message": "success", 180 | "data": [ 181 | { 182 | "id": 0, 183 | "name": "Tom" 184 | }, 185 | { 186 | "id": 1, 187 | "name": "John" 188 | } 189 | ] 190 | } 191 | ``` 192 | 193 | 这样,看上去是不是就舒服多了。 194 | 195 | 另外,`django-simple-api` 默认的响应类型是 `application/json`,如果要设置其他类型,可以这样: 196 | 197 | ```python 198 | # views.py 199 | 200 | class JustTest(View): 201 | 202 | @describe_response(401, content={"text/plain":{"schema": {"type": "string", "example": "No permission"}}}) 203 | def get(self, request, id: int = Query()): 204 | return JsonResponse(id) 205 | ``` 206 | 207 | 虽然我们支持自定义响应类型和数据结构,但我们建议你尽量不要这样做,除非是像示例中那样非常简单的响应, 208 | 否则会在你的代码文件中占用大量空间并且不利于让团队中的其他人阅读代码。 209 | 210 | 如果你需要描述多个响应信息,那么我们会推荐使用 `describe_responses`,它可以一次描述多个响应状态,这相当于同时使用多个 `describe_response`: 211 | 212 | ```python 213 | # views.py 214 | 215 | from django_simple_api import describe_responses 216 | 217 | class JustTestResponses(BaseModel): 218 | code: str 219 | message: str 220 | data: List[dict] 221 | 222 | 223 | class JustTest(View): 224 | 225 | @describe_responses({ 226 | 200: {"content": JustTestResponses}, 227 | 401: {"content": {"text/plain": {"schema": {"type": "string", "example": "No permission"}}}} 228 | }) 229 | def get(self, request, id: int = Query()): 230 | return JsonResponse(id) 231 | ``` 232 | 233 | > 如果你想添加公共的响应信息到多个接口,你可以使用:[wrapper_include](extensions-function.md#wrapper_include) 234 | 235 | 236 | ### 添加标记 237 | 238 | `OpenAPI` 支持通过标记来对接口进行分组管理,你可以通过 `mark_tag` 和 `mark_tags` 来为接口添加标记: 239 | 240 | ```python 241 | from django_simple_api import mark_tags, allow_request_method, Query 242 | 243 | # 使用 @mark_tag 为接口添加标签 244 | @mark_tags("about User") 245 | @allow_request_method("get") 246 | def get_name(request, id: int = Query(...)): 247 | return HttpResponse(get_name_by_id(id)) 248 | 249 | # 使用 @mark_tags 为接口添加多个标签 250 | @mark_tags("tag1", "tag2") 251 | @allow_request_method("get") 252 | def get_name(request, id: int = Query(...)): 253 | return HttpResponse(get_name_by_id(id)) 254 | ``` 255 | 256 | > 如果你想同时为多个接口添加标签,你可以使用:[wrapper_include](extensions-function.md#wrapper_include) 257 | -------------------------------------------------------------------------------- /docs/extensions-function.md: -------------------------------------------------------------------------------- 1 | ## wrapper_include 2 | 3 | `wrapper_include` 可以批量的在视图上使用装饰器,你可以用它来批量应用 `@describe_responses`,`@mark_tags` 等装饰器。 4 | 5 | ```python 6 | from django_simple_api import wrapper_include, mark_tags 7 | 8 | urlpatterns = [ 9 | ..., 10 | # 批量添加标签 11 | path("app/", wrapper_include([mark_tags("demo tag")], include("app.urls"))), 12 | # 批量添加响应信息 13 | path("app/", wrapper_include([describe_response(200, "ok")], include("app.urls"))), 14 | # 或者可以同时使用 15 | path("app/", wrapper_include([mark_tags("demo tag"), describe_response(200, "ok")], include("app.urls"))), 16 | ] 17 | ``` 18 | 19 | 20 | ## 支持 JSON 请求 21 | 默认情况下,Django 只支持 `application/x-www-form-urlencoded` 和 `multipart/form-data` 请求, 22 | `django-simple-api` 扩展支持了 `application/json` 请求。点击 [Django 解析非POST请求](https://aber.sh/articles/Django-Parse-non-POST-Request/) 查看实现思路。 23 | 24 | 25 | ## 序列化方法 26 | `django-simple-api` 还为 Django 的 `Model`、`QuerySet`、`RawQuerySet` 扩展了序列化方法, 27 | 可以让我们很方便的将 `Model`、`QuerySet` 序列化成一个字典或者列表。 28 | 29 | **示例:** 30 | ```python 31 | from django.views import View 32 | from django.http.response import JsonResponse 33 | from django.contrib.auth.models import User 34 | 35 | from django_simple_api import Query 36 | 37 | class SerializeDemo(View): 38 | 39 | def get(self, request, name: str=Query()): 40 | # 序列化 Model 41 | user = User.objects.get(username=name) 42 | # 只要使用 Model.to_json() 方法就可以将 Model 序列化出来 43 | user_dict = user.to_json() 44 | 45 | # 序列化 QuerySet 46 | users = User.objects.filter(username=name) 47 | # 同样只需要使用 QuerySet.to_json() 方法 48 | users_dict = users.to_json() 49 | 50 | return JsonResponse(data=users_dict) 51 | ``` 52 | 53 | ### 隐藏敏感字段 54 | 默认情况下,使用 `to_json()` 序列化方法会将这个 `Model` 实例所有的字段全部序列化出来。 55 | 如果你不想将比较敏感的字段暴露出来,那么你可以在 `Model` 里添加一个 `buried_fields` 属性就可以隐藏敏感字段了。 56 | 57 | **示例:** 58 | ```python 59 | from django.db import models 60 | 61 | class User(models.Model): 62 | name = models.CharField(max_length=20) 63 | mobile = models.CharField(max_length=11) 64 | password = models.CharField(max_length=20) 65 | 66 | # 敏感字段不可暴露 67 | buried_fields = ['password'] 68 | ``` 69 | 70 | 此方式适用于在任何情况下都不会暴露出来的字段,比如用户密码,这样可以防止因某次疏忽导致账户敏感信息泄露。 71 | 72 | > ⚠️ 注意:如果你的查询语句同时查询了关联模型,并且关联模型里也定义了 `buried_fields` 那么关联模型的敏感字段也会被隐藏。 73 | 74 | ### 排除字段 75 | 除此之外,还可以在序列化时排除不需要的字段。在调用 `to_json()` 方法时,传入 `excludes` 参数: 76 | ```python 77 | from django.views import View 78 | from django.http.response import JsonResponse 79 | from django.contrib.auth.models import User 80 | 81 | from django_simple_api import Query 82 | 83 | class SerializeDemo(View): 84 | 85 | def get(self, request, name: str=Query()): 86 | user = User.objects.get(username=name) 87 | # 排除不需要的字段 88 | user_dict = user.to_json(excludes=['email', 'created_time']) 89 | 90 | # QuerySet 也同样支持 91 | users = User.objects.get(username=name) 92 | users_dict = users.to_json(excludes=['email', 'created_time']) 93 | 94 | return JsonResponse(data=user_dict) 95 | ``` 96 | 97 | > ⚠️ 注意:如果你的查询语句同时查询了关联模型,那么 `to_json()` 也会将关联模型序列化出来; 98 | > 但是 `excludes` 只会将主模型的字段排除,而不会排除关联模型的同名字段。 99 | > 100 | > 这是 `excludes` 参数与 `Model.buried_fields` 属性行为不一致的地方,请不要混淆。 101 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Django Simple API 2 | 3 | ## 关于 4 | ***Django Simple API*** 是一个非侵入式组件,可以帮助您快速创建 API。 5 | 6 | 它不是一个类似于 DRF 的框架,它只是一个基于 Django 的轻量级插件,非常易于学习和使用。 7 | 8 | 它有 2 个核心功能: 9 | 10 | * 自动生成接口文件(OpenAPI) 11 | * 自动校验请求参数 12 | 13 | 此外,还实现了自动支持 `application/json` 请求类型,以及为 `Model`、`QuerySet` 拓展了序列化方法。 14 | 15 | ## 学习和使用 16 | 17 | ⚠️ 此库的所有功能默认支持`函数视图`和`类视图`。如果文档中没有特别说明,则表示这两种视图都适用,如果需要特殊支持,我们会在文档中说明如何做。 18 | 19 | [快速开始](quick-start.md) 20 | 21 | [声明参数](declare-parameters.md) 22 | 23 | [参数验证](parameter-verification.md) 24 | 25 | [生成文档](document-generation.md) 26 | 27 | [扩展功能](extensions-function.md) 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /docs/parameter-verification.md: -------------------------------------------------------------------------------- 1 | **提示:** 2 | 不要忘记配置 `INSTALLED_APPS` 和 `MIDDLEWARE `,点击 [快速开始](quick-start.md) 学习如何配置。 3 | 4 | 5 | **自动校验请求参数** 是默认开启的功能,当你使用[字段](declare-parameters.md#字段)声明参数时,`django-simple-api` 会自动检查参数是否合法。 6 | 7 | 如果您的请求参数校验失败,则会返回一个 `422` 客户端错误,如下图所示: 8 | ```shell 9 | [ 10 | { 11 | "loc": [ 12 | "id" 13 | ], 14 | "msg": "value is not a valid integer", 15 | "type": "type_error.integer" 16 | } 17 | ] 18 | ``` 19 | 20 | 在上面的错误信息中,`loc` 指出了哪个参数有错误,`msg` 描述了错误的原因。有了这些信息,便可以快速定位参数问题了。 21 | -------------------------------------------------------------------------------- /docs/quick-start.md: -------------------------------------------------------------------------------- 1 | ## 安装 2 | 3 | 从 github 下载并安装: 4 | 5 | ```shell 6 | pip install django-simple-api 7 | ``` 8 | 9 | ## 配置 10 | 11 | 第一步:将 `django-simple-api` 添加到 `settings.INSTALLED_APPS` 中: 12 | 13 | ```python 14 | INSTALLED_APPS = [ 15 | ..., 16 | "django_simple_api", 17 | ] 18 | ``` 19 | 20 | 第二步:将中间件注册到 `settings.MIDDLEWARE` 中: 21 | 22 | ```python 23 | MIDDLEWARE = [ 24 | ..., 25 | "django_simple_api.middleware.SimpleApiMiddleware", 26 | ] 27 | ``` 28 | 29 | 第三步:将 `django-simple-api` 的 url 添加到根 urls.py 中: 30 | 31 | ```python 32 | # urls.py 33 | 34 | from django.urls import include, path 35 | from django.conf import settings 36 | 37 | # 根 urls 38 | urlpatterns = [ 39 | ... 40 | ] 41 | 42 | # dsa 的 urls, 应该只在测试环境运行! 43 | if settings.DEBUG: 44 | urlpatterns += [ 45 | # 接口文档 url 46 | path("docs/", include("django_simple_api.urls")) 47 | ] 48 | ``` 49 | 50 | ## 完成第一个示例 51 | 52 | 首先,定义一个路由: 53 | 54 | ```python 55 | # your urls.py 56 | 57 | from django.urls import path 58 | from yourviews import JustTest 59 | 60 | urlpatterns = [ 61 | ..., 62 | path("/path//", JustTest.as_view()), 63 | ] 64 | ``` 65 | 66 | 然后定义一个视图: 67 | 68 | ```python 69 | # your views.py 70 | 71 | from django.views import View 72 | from django.http.response import HttpResponse 73 | 74 | from django_simple_api import Query 75 | 76 | 77 | class JustTest(View): 78 | def get(self, request, id: int = Query()): 79 | return HttpResponse(id) 80 | ``` 81 | 82 | > 注意,要生成文档,必须使用 `django-simple-api` 的规则声明参数(如上图所示)! 83 | > 84 | > 点击 [声明参数](declare-parameters.md) 查看如何声明参数。 85 | 86 | ## 访问接口文档 87 | 88 | 完成上述配置和示例后,现在就可以启动服务器并访问接口文档了。 89 | 90 | 如果你的服务在本地运行,可以访问 [http://127.0.0.1:8000/docs/](http://127.0.0.1:8000/docs/) 来查看接口文档。 91 | 92 | -------------------------------------------------------------------------------- /docs_en/declare-parameters.md: -------------------------------------------------------------------------------- 1 | Parameter declaration is the infrastructure of ***Simple API***. Whether you want to automatically verify request parameters or automatically generate interface documents, you must first learn how to declare parameters. 2 | 3 | You can declare request parameters like the following example: 4 | 5 | ```python 6 | # views.py 7 | 8 | from django.views import View 9 | from django.http.response import HttpResponse 10 | 11 | from django_simple_api import Query 12 | 13 | class JustTest(View): 14 | def get(self, request, 15 | # `id` is your parameter name. 16 | # `int` is your parameter type. 17 | # `Query` represents where your parameters are located, 18 | # and can describe various information about your parameters. 19 | id: int = Query() 20 | ): 21 | return HttpResponse(id) 22 | ``` 23 | 24 | 25 | ## Fields 26 | ***Simple API*** has a total of 6 fields, corresponding to the parameters in different positions: 27 | ### All fields and description 28 | | Field | Description | 29 | | --- | --- | 30 | | Query | Indicates that this parameter is in the url query string. example: http://host/?param=1| 31 | | Path | Indicates that this parameter is a url path parameter. example: http://host/{param}/| 32 | | Body | Indicates that this parameter is in the request body, and the value can only be obtained in a non-GET request.| 33 | | Cookie | Indicates that this parameter is in Cookie.| 34 | | Header | Indicates that this parameter is in Header.| 35 | 36 | ### For example: 37 | 38 | ```python 39 | # urls.py 40 | 41 | urlpatterns = [ 42 | ... 43 | path("/path///", JustTest.as_view(), name="path_name"), 44 | ] 45 | ``` 46 | 47 | ```python 48 | # views.py 49 | 50 | from django_simple_api import Query, Path, Body, Cookie, Header 51 | 52 | 53 | class JustTest(View): 54 | # The parameter names used in the above examples are for demonstration only. 55 | def post(self, request, 56 | param1: int = Query(), 57 | # You can use the same field to describe multiple parameters. 58 | param2: int = Path(), 59 | param3: int = Path(), 60 | param4: str = Body(), 61 | userid: int = Cookie(alias="uid"), 62 | username: int = Cookie(alias="username"), 63 | csrf_token: str = Header(alias="X-Csrf-Token"), 64 | ): 65 | 66 | return HttpResponse(username) 67 | ``` 68 | 69 | > ⚠️ In the above example, you have two things to note: 70 | > 71 | > * If you have more than one parameter in a field, you can use the field multiple times to describe different parameters. 72 | > * When you need to get parameters from `Header`, you may need to use `alias` to indicate the request header you want to get, because the name of the request header may not be a valid python identifier. 73 | 74 | 75 | ## Type conversion 76 | ***Simple API*** also has the function of type conversion. If the parameter you pass in is legal for the declared type, it will be converted to the declared type without manual operation: 77 | 78 | ```python 79 | # views.py 80 | 81 | class JustTest(View): 82 | def get(self, request, last_time: datetime.date = Query()): 83 | print(last_time, type(last_time)) 84 | # 2008-08-08 85 | return HttpResponse(last_time) 86 | ``` 87 | 88 | 89 | ## Field properties 90 | Use `Query()` to declare the parameter, which means this parameter is required. If there is no `id` parameter in the query string for url, an error will be returned: 91 | 92 | ```shell 93 | [ 94 | { 95 | "loc": [ 96 | "id" 97 | ], 98 | "msg": "field required", 99 | "type": "value_error.missing" 100 | } 101 | ] 102 | ``` 103 | 104 | In addition to this, you can use default parameters like this: 105 | 106 | ```python 107 | # views.py 108 | 109 | class JustTest(View): 110 | def get(self, request, id: int = Query(default=10)): 111 | return HttpResponse(id) 112 | ``` 113 | 114 | Or you can use the `default_factory` parameter and pass in a function to dynamically calculate the default value: 115 | 116 | ```python 117 | # views.py 118 | 119 | def func(): 120 | return 1000 121 | 122 | class JustTest(View): 123 | def get(self, request, id: int = Query(default_factory=func)): 124 | print(id) # 1000 125 | return HttpResponse(id) 126 | ``` 127 | 128 | But you cannot use `default` and `default_factory` at the same time, otherwise an error will be reported: 129 | 130 | ```shell 131 | ValueError: cannot specify both default and default_factory 132 | ``` 133 | 134 | In addition to the `default`、`default_factory`, you can also use more attributes, such as: 135 | 136 | #### All properties and description 137 | | Name | description | 138 | | --- | --- | 139 | | exclusive | Exclusive mode, when you use this mode, you must take a subclass of `pydantic.BaseModel` as a form and get all the parameters declared in the form at once. [See the sample](#Use exclusive model)| 140 | | default | Since this is replacing the field’s default, its first argument is used to set the default, do not pass `default` or `default_factory` to indicate that this is a required field.| 141 | | default_factory| Callable that will be called when a default value is needed for this field. If both `default` and `default_factory` are set, an error is raised.| 142 | | alias | The public name of the field.| 143 | | title | Can be any string, used in the schema.| 144 | | description | Can be any string, used in the schema.| 145 | | **extra | Any additional keyword arguments will be added as is to the schema.| 146 | 147 | 148 | #### Use exclusive model 149 | `exclusive` is a special property that allows you to retrieve the entire form's parameters at once. 150 | 151 | Example: 152 | 153 | You can use `pydantic.BaseModel` to define a `"Form"`, 154 | Then use the `exclusive` property to get all parameters required for `"Form"` from the `body`: 155 | 156 | 157 | ```python 158 | # views.py 159 | 160 | from django.views import View 161 | from pydantic import BaseModel, Field 162 | 163 | from django_simple_api import Body 164 | 165 | 166 | # Use `pydantic.BaseModel` to define a `"Form"` 167 | class UserForm(BaseModel): 168 | name: str = Field(max_length=25, description="This is user's name") 169 | age: int = Field(19, description="This is user's age") 170 | 171 | 172 | class UserView(View): 173 | def post(self, request, 174 | # Use the `exclusive=True` to get all parameters required for `"Form"` from the `body`: 175 | user: UserForm = Body(exclusive=True) 176 | ): 177 | 178 | # You can get the parameters from the "Form" like this: 179 | name = user.name 180 | age = user.age 181 | 182 | # Also convert the form into a dictionary: 183 | user.dict() # {"name": ..., "age": ...} 184 | 185 | # So you can directly instantiate the UserModel like this: 186 | UserModel(**user.dict()).save() 187 | 188 | return HttpResponse("success") 189 | ``` 190 | > ⚠️ Note, when you use `Body(exclusive=True)`, you can no longer use the `Body()` field in this view, but use of other fields will not be affected. 191 | 192 | >There are other uses of `BaseModel`, see [`pydantic`](https://pydantic-docs.helpmanual.io/usage/exporting_models/#modeldict) for more details. 193 | 194 | 195 | ## Constraint request parameters 196 | ```python 197 | # views.py 198 | from django.views import View 199 | 200 | from pydantic import conint,confloat, constr, conlist 201 | 202 | from django_simple_api import Query, Body 203 | 204 | class JustTest(View): 205 | 206 | # If your parameter is of numeric type , you can use `ge`、`gt`、`le`、`lt`、`multipleOf` and other attributes 207 | def get(self, request, 208 | p1: conint(gt=10) = Query(), # must be > 10 209 | p2: conint(ge=10) = Query(), # must be >= 10 210 | p3: confloat(lt=10.5) = Query(), # must be < 10.5 211 | p4: confloat(le=10.5) = Query(), # must be <= 10.5 212 | p5: conint(multiple_of=10) = Query(), # must be a multiple of 10 213 | ): 214 | return HttpResponse("Success") 215 | 216 | # Or if your parameter is of str type , you can use `strip_whitespace`、`to_lower`、`max_length`、`min_length`、`curtail_length` and other attributes 217 | def get(self, request, 218 | p1: constr(strip_whitespace=True) = Query(), # Remove blank characters on both sides. 219 | p2: constr(to_lower=True) = Query(), # Convert string to lowercase 220 | p3: constr(max_length=20) = Query(), # The maximum length of the string cannot exceed 20. 221 | p4: constr(min_length=8) = Query(), # The minimum length of the string cannot be less than 8. 222 | p5: constr(curtail_length=10) = Query(), # Intercept the first 10 characters. 223 | ): 224 | return HttpResponse("Success") 225 | 226 | # Or if your parameter is of list type , you can use `item_type`、`max_items`、`min_items` attributes 227 | def post(self, request, 228 | p1: conlist(item_type=int, min_items=1) = Body(), # At least one item in the list and should be of type int. 229 | p2: conlist(item_type=str, max_items=5) = Body(), # There are at most five items in the list, and they should be of type str. 230 | ): 231 | return HttpResponse("Success") 232 | ``` 233 | > For more information on attribute constraints, see [Pydantic](https://pydantic-docs.helpmanual.io/usage/models/). 234 | 235 | 236 | ## More 237 | When you finish the above tutorial, you can already declare parameters well. 238 | Next, you can use the functions of "parameter verification" and "document generation". 239 | 240 | Click [Parameter Verification](parameter-verification.md) to see how to verify parameters. 241 | 242 | Click [Document Generation](document-generation.md) to see how to Generating documentation. 243 | 244 | -------------------------------------------------------------------------------- /docs_en/document-generation.md: -------------------------------------------------------------------------------- 1 | **Hints:** 2 | If you want to automatically generate interface documentation, you must add the url of ***Simple API*** to your urls.py, See [Quick Start](quick-start.md). 3 | 4 | ## Modify the interface document description information 5 | You can modify the interface document description information in the url of `Simple API`, like this: 6 | 7 | ```python 8 | # urls.py 9 | 10 | from django.urls import include, path 11 | from django.conf import settings 12 | 13 | 14 | # Your urls 15 | urlpatterns = [ 16 | ... 17 | ] 18 | 19 | # Simple API urls, should only run in a test environment. 20 | if settings.DEBUG: 21 | urlpatterns += [ 22 | # generate documentation 23 | path( 24 | "docs/", 25 | include("django_simple_api.urls"), 26 | { 27 | "template_name": "swagger.html", 28 | "title": "Django Simple API", 29 | "description": "This is description of your interface document.", 30 | "version": "0.1.0", 31 | }, 32 | ), 33 | ] 34 | ``` 35 | 36 | In the above example, you can modify the `template_name` to change the UI theme of the interface document, We currently have two UI themes: `swagger.html` and `redoc.html`. 37 | 38 | And then you can modify `title`、`description` and `version` to describe your interface documentation. 39 | 40 | 41 | ## Generate documentation for `function-view` 42 | If you are using `class-view`, you can now generate documentation. 43 | Start your service, if your service is running locally, you can visit [http://127.0.0.1:8000/docs/](http://127.0.0.1:8000/docs/) to view your documentation. 44 | 45 | But if you are using `view-function`, you must declare the request method supported by the view function: 46 | 47 | ```python 48 | # views.py 49 | 50 | from django_simple_api import allow_request_method 51 | 52 | @allow_request_method("get") 53 | def just_test(request, id: int = Query()): 54 | return HttpResponse(id) 55 | ``` 56 | 57 | `allow_request_method` can only declare one request method, and it must be in `['get', 'post', 'put', 'patch', 'delete', 'head', 'options', trace']`. 58 | We do not support the behavior of using multiple request methods in a `view-function`, which will cause trouble for generating documentation. 59 | 60 | Note that if you use `@allow_request_method("get")` to declare a request method, you will not be able to use request methods other than `get`, otherwise it will return `405 Method Not Allow`. 61 | 62 | You can also not use `allow_request_method`, this will not have any negative effects, but it will not generate documentation. 63 | We will use `warning.warn()` to remind you, this is not a problem, just to prevent you from forgetting to use it. 64 | 65 | Now, the `view-function` can also generate documents, you can continue to visit your server to view the effect. 66 | 67 | 68 | ## Improve documentation information 69 | ***Simple API*** is generated according to the [`OpenAPI`](https://github.com/OAI/OpenAPI-Specification) specification. 70 | In addition to automatically generating function parameters, you can also manually add some additional information to the view yourself, 71 | for example: `summary` `description` `responses` and `tags`. 72 | 73 | ### Add `summary` and `description` to the view 74 | `summary` and `description` can describe the information of your interface in the interface document, `summary` is used to briefly introduce the function of the interface, and `description` is used to describe more information. 75 | 76 | There must be a blank line between `summary` and `description`. If there is no blank line, then all `doc` will be considered as `summary`. 77 | 78 | ```python 79 | # views.py 80 | 81 | class JustTest(View): 82 | def get(self, request, id: int = Query()): 83 | """ 84 | This is summary. 85 | 86 | This is description ... 87 | This is description ... 88 | """ 89 | return HttpResponse(id) 90 | ``` 91 | 92 | ### Add `responses` to the view 93 | `responses` is also important information in the interface documentation. 94 | You can define the response information that the interface should return in various situations. 95 | 96 | ***Simple API*** highly recommends using `pydantic.BaseModel` to define the data structure of the response message, for example: 97 | 98 | ```python 99 | # views.py 100 | 101 | from typing import List 102 | 103 | from pydantic import BaseModel 104 | from django.views import View 105 | 106 | from django_simple_api import describe_response 107 | 108 | 109 | # define the data structure for `response` 110 | class JustTestResponses(BaseModel): 111 | code: str 112 | message: str 113 | data: List[dict] 114 | 115 | 116 | class JustTest(View): 117 | 118 | # describe the response information of the interface 119 | @describe_response(200, content=JustTestResponses) 120 | def get(self, request, id: int = Query()): 121 | 122 | # actual response data(just an example) 123 | resp = { 124 | "code": "0", 125 | "message": "success", 126 | "data": [ 127 | {"id": 0, "name": "Tom"}, 128 | {"id": 1, "name": "John"}, 129 | ] 130 | } 131 | return JsonResponse(resp) 132 | ``` 133 | 134 | Then the interface document will show: 135 | 136 | ```shell 137 | { 138 | "code": "string", 139 | "message": "string", 140 | "data": [ 141 | {} 142 | ] 143 | } 144 | ``` 145 | 146 | You can also show the example in the interface document, you only need to add the example to the `BaseModel` and it will be shown in the interface document: 147 | 148 | ```python 149 | # views.py 150 | 151 | class JustTestResponses(BaseModel): 152 | code: str 153 | message: str 154 | data: List[dict] 155 | 156 | class Config: 157 | schema_extra = { 158 | "example": { 159 | "code": "0", 160 | "message": "success", 161 | "data": [ 162 | {"id": 0, "name": "Tom"}, 163 | {"id": 1, "name": "John"}, 164 | ] 165 | } 166 | } 167 | 168 | class JustTest(View): 169 | 170 | @describe_response(200, content=JustTestResponses) 171 | def get(self, request, id: int = Query()): 172 | resp = {...} 173 | return JsonResponse(resp) 174 | ``` 175 | 176 | Then the interface document will show: 177 | 178 | ```shell 179 | { 180 | "code": "0", 181 | "message": "success", 182 | "data": [ 183 | { 184 | "id": 0, 185 | "name": "Tom" 186 | }, 187 | { 188 | "id": 1, 189 | "name": "John" 190 | } 191 | ] 192 | } 193 | ``` 194 | 195 | The default response type of ***Simple API*** is `application/json`, if you want to set other types, you can use it like this: 196 | 197 | ```python 198 | # views.py 199 | 200 | class JustTest(View): 201 | 202 | @describe_response(401, content={"text/plain":{"schema": {"type": "string", "example": "No permission"}}}) 203 | def get(self, request, id: int = Query()): 204 | return JsonResponse(id) 205 | ``` 206 | 207 | Although we support custom response types and data structures, we recommend that you try not to do this, unless it is a very simple response as in the example, 208 | otherwise it will take up a lot of space in your code files and it will not conducive to other people in the team to read the code. 209 | 210 | If you need to describe multiple responses, then we will recommend you to use `describe_responses`, which can describe multiple response states at once, 211 | this is equivalent to using multiple `describe_response` simultaneously: 212 | 213 | ```python 214 | # views.py 215 | 216 | from django_simple_api import describe_responses 217 | 218 | class JustTestResponses(BaseModel): 219 | code: str 220 | message: str 221 | data: List[dict] 222 | 223 | 224 | class JustTest(View): 225 | 226 | @describe_responses({ 227 | 200: {"content": JustTestResponses}, 228 | 401: {"content": {"text/plain": {"schema": {"type": "string", "example": "No permission"}}}} 229 | }) 230 | def get(self, request, id: int = Query()): 231 | return JsonResponse(id) 232 | ``` 233 | 234 | > Add `responses` to multiple views simultaneously: [wrapper_include](extensions-function.md#wrapper_include) 235 | 236 | 237 | ### Add `tags` to the view 238 | 239 | Tagging interfaces is a good way to manage many interfaces, That's how you tag a view: 240 | 241 | ```python 242 | from django_simple_api import mark_tags, allow_request_method, Query 243 | 244 | @mark_tags("about User") 245 | @allow_request_method("get") 246 | def get_name(request, id: int = Query(...)): 247 | return HttpResponse(get_name_by_id(id)) 248 | ``` 249 | 250 | You can use `@mark_tags("tag1", "tag2")` to tag a view with multiple tags 251 | 252 | > Add `tags` to multiple views simultaneously: [wrapper_include](extensions-function.md#wrapper_include) 253 | 254 | -------------------------------------------------------------------------------- /docs_en/extensions-function.md: -------------------------------------------------------------------------------- 1 | ## wrapper_include 2 | 3 | We also support tagging multiple URLs at the same time 4 | 5 | ```python 6 | from django_simple_api import wrapper_include, mark_tags 7 | 8 | urlpatterns = [ 9 | ..., 10 | path("app/", wrapper_include([mark_tags("demo tag")], include("app.urls"))), 11 | ] 12 | ``` 13 | 14 | The `wrapper_include` in the above code will add `mark_tags` decorators to all views configured in the app URLs 15 | 16 | You can also use the `wrapper_include` based functionality 17 | 18 | ```python 19 | wrapper_include([mark_tags("demo tag"), describe_response(200, "ok")], include("app.urls")) 20 | ``` 21 | 22 | ## Support for JSON requests 23 | 24 | ## To be continue ... 25 | -------------------------------------------------------------------------------- /docs_en/index.md: -------------------------------------------------------------------------------- 1 | # Django Simple API 2 | 3 | ## About Us 4 | ***Django Simple API*** is a non-intrusive component that can help you quickly create APIs. 5 | 6 | It's not an API framework, it's just a lightweight Django-based plugin that's very easy to learn and use. 7 | 8 | It has two core functions: 9 | 10 | * Automatic generation of interface documents 11 | * Automatic validation of request parameters 12 | 13 | To help us develop faster, we also extended support for `application/json` requests and provided some useful utility functions. 14 | 15 | 16 | ## Learn and used 17 | 18 | ⚠️ We support both view-function and class-view at the same time for all functions. If there is no special description in the document, it means that it is applicable to both views. 19 | Where special support is needed, we will indicate how to do it in the document. 20 | 21 | [Quick Start](quick-start.md) 22 | 23 | [Declare Parameters](declare-parameters.md) 24 | 25 | [Parameter Verification](parameter-verification.md) 26 | 27 | [Document Generation](document-generation.md) 28 | -------------------------------------------------------------------------------- /docs_en/parameter-verification.md: -------------------------------------------------------------------------------- 1 | **Hints:** 2 | Don't forget to install the app and register the middleware. See [Quick Start](quick-start.md). 3 | 4 | 5 | "Parameter verification" is enabled by default. 6 | 7 | When you declare a parameter using "[Fields](declare-parameters.md#fields)", 8 | the ***Simple API*** will automatically checks whether the parameter is valid in the request. 9 | 10 | 11 | If your parameter verification fails, an error will be returned, like this: 12 | 13 | ```shell 14 | [ 15 | { 16 | "loc": [ 17 | "id" 18 | ], 19 | "msg": "value is not a valid integer", 20 | "type": "type_error.integer" 21 | } 22 | ] 23 | ``` 24 | 25 | In the error message above, `loc` indicates which parameter has an error, `msg` describes the cause of the error. 26 | With this information, you can quickly locate the problem of parameters. 27 | -------------------------------------------------------------------------------- /docs_en/quick-start.md: -------------------------------------------------------------------------------- 1 | ## Install 2 | 3 | Download and install from github: 4 | 5 | ``` 6 | pip install django-simple-api 7 | ``` 8 | 9 | ## Configure 10 | 11 | Add django-simple-api to your `INSTALLED_APPS` in settings: 12 | 13 | ```python 14 | INSTALLED_APPS = [ 15 | ..., 16 | "django_simple_api", 17 | ] 18 | ``` 19 | 20 | Register the middleware to your `MIDDLEWARE` in settings: 21 | 22 | ```python 23 | MIDDLEWARE = [ 24 | ..., 25 | "django_simple_api.middleware.SimpleApiMiddleware", 26 | ] 27 | ``` 28 | 29 | Add the url of ***django-simple-api*** to your urls.py: 30 | 31 | ```python 32 | # urls.py 33 | 34 | from django.urls import include, path 35 | from django.conf import settings 36 | 37 | # Your urls 38 | urlpatterns = [ 39 | ... 40 | ] 41 | 42 | # Simple API urls, should only run in a test environment. 43 | if settings.DEBUG: 44 | urlpatterns += [ 45 | # generate documentation 46 | path("docs/", include("django_simple_api.urls")) 47 | ] 48 | ``` 49 | 50 | ## Complete the first example 51 | 52 | Define your url: 53 | 54 | ```python 55 | # your urls.py 56 | 57 | from django.urls import path 58 | from yourviews import JustTest 59 | 60 | urlpatterns = [ 61 | ..., 62 | path("/path//", JustTest.as_view()), 63 | ] 64 | ``` 65 | 66 | Define your view: 67 | 68 | ```python 69 | # your views.py 70 | 71 | from django.views import View 72 | from django.http.response import HttpResponse 73 | 74 | from django_simple_api import Query 75 | 76 | 77 | class JustTest(View): 78 | def get(self, request, id: int = Query()): 79 | return HttpResponse(id) 80 | ``` 81 | 82 | > To generate the document, you must declare the parameters according to the rules of ***Simple API*** (like the example above). 83 | > 84 | > Click [Declare parameters](declare-parameters.md) to see how to declare parameters. 85 | 86 | ## Access interface document 87 | 88 | After the above configuration, you can start your server and access the interface documentation now. 89 | 90 | If your service is running locally, you can visit [http://127.0.0.1:8000/docs/](http://127.0.0.1:8000/docs/) to view 91 | your documentation. 92 | -------------------------------------------------------------------------------- /example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Django-Simple-API/django-simple-api/9dd203b0899dbe3f736866702858c6e4a017a7e8/example/__init__.py -------------------------------------------------------------------------------- /example/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for example project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /example/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for example project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.1/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve(strict=True).parent.parent 17 | 18 | # Quick-start development settings - unsuitable for production 19 | # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ 20 | 21 | # SECURITY WARNING: keep the secret key used in production secret! 22 | SECRET_KEY = "67eify(q!gs2g=0h1c2kdhoq)1t=5)!ui^ihwuu@u4ibxpwjl5" 23 | 24 | # SECURITY WARNING: don't run with debug turned on in production! 25 | DEBUG = True 26 | 27 | ALLOWED_HOSTS = [] 28 | 29 | STATICFILES_DIRS = [BASE_DIR / "static/"] # DEBUG=True 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | "django.contrib.admin", 35 | "django.contrib.auth", 36 | "django.contrib.contenttypes", 37 | "django.contrib.sessions", 38 | "django.contrib.messages", 39 | "django.contrib.staticfiles", 40 | "django_simple_api", 41 | ] 42 | 43 | MIDDLEWARE = [ 44 | "django.middleware.security.SecurityMiddleware", 45 | "django.contrib.sessions.middleware.SessionMiddleware", 46 | "django.middleware.common.CommonMiddleware", 47 | # "django.middleware.csrf.CsrfViewMiddleware", 48 | "django.contrib.auth.middleware.AuthenticationMiddleware", 49 | "django.contrib.messages.middleware.MessageMiddleware", 50 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 51 | "django_simple_api.middleware.SimpleApiMiddleware", 52 | ] 53 | 54 | ROOT_URLCONF = "example.urls" 55 | 56 | TEMPLATES = [ 57 | { 58 | "BACKEND": "django.template.backends.django.DjangoTemplates", 59 | "DIRS": [], 60 | "APP_DIRS": True, 61 | "OPTIONS": { 62 | "context_processors": [ 63 | "django.template.context_processors.debug", 64 | "django.template.context_processors.request", 65 | "django.contrib.auth.context_processors.auth", 66 | "django.contrib.messages.context_processors.messages", 67 | ], 68 | }, 69 | }, 70 | ] 71 | 72 | WSGI_APPLICATION = "example.wsgi.application" 73 | 74 | # Database 75 | # https://docs.djangoproject.com/en/3.1/ref/settings/#databases 76 | 77 | DATABASES = { 78 | "default": { 79 | "ENGINE": "django.db.backends.sqlite3", 80 | "NAME": ":memory:", 81 | } 82 | } 83 | 84 | # Password validation 85 | # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators 86 | 87 | AUTH_PASSWORD_VALIDATORS = [ 88 | { 89 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 90 | }, 91 | { 92 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 93 | }, 94 | { 95 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 96 | }, 97 | { 98 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 99 | }, 100 | ] 101 | 102 | # Internationalization 103 | # https://docs.djangoproject.com/en/3.1/topics/i18n/ 104 | 105 | LANGUAGE_CODE = "en-us" 106 | 107 | TIME_ZONE = "UTC" 108 | 109 | USE_I18N = True 110 | 111 | USE_L10N = True 112 | 113 | USE_TZ = True 114 | 115 | # Static files (CSS, JavaScript, Images) 116 | # https://docs.djangoproject.com/en/3.1/howto/static-files/ 117 | 118 | STATIC_URL = "/static/" 119 | -------------------------------------------------------------------------------- /example/urls.py: -------------------------------------------------------------------------------- 1 | """example URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.1/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.urls import include, path 17 | 18 | from django_simple_api import mark_tags, wrapper_include 19 | 20 | urlpatterns = [ 21 | # generate documentation 22 | path("docs/", include("django_simple_api.urls")), 23 | # unit test 24 | path("test/", wrapper_include([mark_tags("unit test")], include("tests.urls"))), 25 | ] 26 | -------------------------------------------------------------------------------- /example/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for example project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /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", "example.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 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Django Simple API 2 | site_description: A non-intrusive component that can help you quickly create APIs. 3 | 4 | repo_name: django-simple-api 5 | repo_url: https://github.com/Django-Simple-API/django-simple-api 6 | edit_uri: https://github.com/Django-Simple-API/django-simple-api/tree/master/docs/ 7 | 8 | use_directory_urls: true 9 | 10 | theme: 11 | name: "material" 12 | language: "en" 13 | palette: 14 | scheme: default 15 | primary: blue 16 | feature: 17 | tabs: true 18 | 19 | nav: 20 | - 首页: "index.md" 21 | - 快速开始: "quick-start.md" 22 | - 参数声明: "declare-parameters.md" 23 | - 参数校验: "parameter-verification.md" 24 | - 文档生成: "document-generation.md" 25 | - 扩展功能: "extensions-function.md" 26 | 27 | markdown_extensions: 28 | - admonition 29 | - extra 30 | - pymdownx.highlight 31 | - pymdownx.superfences 32 | 33 | plugins: 34 | - search 35 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "asgiref" 3 | version = "3.3.1" 4 | description = "ASGI specs, helper code, and adapters" 5 | category = "main" 6 | optional = false 7 | python-versions = ">=3.5" 8 | 9 | [package.extras] 10 | tests = ["pytest", "pytest-asyncio"] 11 | 12 | [[package]] 13 | name = "atomicwrites" 14 | version = "1.4.0" 15 | description = "Atomic file writes." 16 | category = "dev" 17 | optional = false 18 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 19 | 20 | [[package]] 21 | name = "attrs" 22 | version = "20.3.0" 23 | description = "Classes Without Boilerplate" 24 | category = "dev" 25 | optional = false 26 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 27 | 28 | [package.extras] 29 | dev = ["coverage[toml] (>=5.0.2)", "furo", "hypothesis", "pre-commit", "pympler", "pytest (>=4.3.0)", "six", "sphinx", "zope.interface"] 30 | docs = ["furo", "sphinx", "zope.interface"] 31 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] 32 | tests-no-zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] 33 | 34 | [[package]] 35 | name = "black" 36 | version = "22.3.0" 37 | description = "The uncompromising code formatter." 38 | category = "dev" 39 | optional = false 40 | python-versions = ">=3.6.2" 41 | 42 | [package.dependencies] 43 | click = ">=8.0.0" 44 | mypy-extensions = ">=0.4.3" 45 | pathspec = ">=0.9.0" 46 | platformdirs = ">=2" 47 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 48 | typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} 49 | typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} 50 | 51 | [package.extras] 52 | colorama = ["colorama (>=0.4.3)"] 53 | d = ["aiohttp (>=3.7.4)"] 54 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 55 | uvloop = ["uvloop (>=0.15.2)"] 56 | 57 | [[package]] 58 | name = "click" 59 | version = "8.1.3" 60 | description = "Composable command line interface toolkit" 61 | category = "dev" 62 | optional = false 63 | python-versions = ">=3.7" 64 | 65 | [package.dependencies] 66 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 67 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 68 | 69 | [[package]] 70 | name = "colorama" 71 | version = "0.4.4" 72 | description = "Cross-platform colored terminal text." 73 | category = "dev" 74 | optional = false 75 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 76 | 77 | [[package]] 78 | name = "django" 79 | version = "3.1.14" 80 | description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." 81 | category = "main" 82 | optional = false 83 | python-versions = ">=3.6" 84 | 85 | [package.dependencies] 86 | asgiref = ">=3.2.10,<4" 87 | pytz = "*" 88 | sqlparse = ">=0.2.2" 89 | 90 | [package.extras] 91 | argon2 = ["argon2-cffi (>=16.1.0)"] 92 | bcrypt = ["bcrypt"] 93 | 94 | [[package]] 95 | name = "flake8" 96 | version = "3.9.0" 97 | description = "the modular source code checker: pep8 pyflakes and co" 98 | category = "dev" 99 | optional = false 100 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 101 | 102 | [package.dependencies] 103 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 104 | mccabe = ">=0.6.0,<0.7.0" 105 | pycodestyle = ">=2.7.0,<2.8.0" 106 | pyflakes = ">=2.3.0,<2.4.0" 107 | 108 | [[package]] 109 | name = "future" 110 | version = "0.18.2" 111 | description = "Clean single-source support for Python 3 and 2" 112 | category = "dev" 113 | optional = false 114 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 115 | 116 | [[package]] 117 | name = "importlib-metadata" 118 | version = "3.7.3" 119 | description = "Read metadata from Python packages" 120 | category = "dev" 121 | optional = false 122 | python-versions = ">=3.6" 123 | 124 | [package.dependencies] 125 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 126 | zipp = ">=0.5" 127 | 128 | [package.extras] 129 | docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] 130 | testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pep517", "pyfakefs", "pytest (>=3.5,!=3.7.3)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=1.2.3)", "pytest-cov", "pytest-enabler", "pytest-flake8", "pytest-mypy"] 131 | 132 | [[package]] 133 | name = "jinja2" 134 | version = "2.11.3" 135 | description = "A very fast and expressive template engine." 136 | category = "dev" 137 | optional = false 138 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 139 | 140 | [package.dependencies] 141 | MarkupSafe = ">=0.23" 142 | 143 | [package.extras] 144 | i18n = ["Babel (>=0.8)"] 145 | 146 | [[package]] 147 | name = "joblib" 148 | version = "1.2.0" 149 | description = "Lightweight pipelining with Python functions" 150 | category = "dev" 151 | optional = false 152 | python-versions = ">=3.7" 153 | 154 | [[package]] 155 | name = "livereload" 156 | version = "2.6.3" 157 | description = "Python LiveReload is an awesome tool for web developers" 158 | category = "dev" 159 | optional = false 160 | python-versions = "*" 161 | 162 | [package.dependencies] 163 | six = "*" 164 | tornado = {version = "*", markers = "python_version > \"2.7\""} 165 | 166 | [[package]] 167 | name = "lunr" 168 | version = "0.5.8" 169 | description = "A Python implementation of Lunr.js" 170 | category = "dev" 171 | optional = false 172 | python-versions = "*" 173 | 174 | [package.dependencies] 175 | future = ">=0.16.0" 176 | nltk = {version = ">=3.2.5", optional = true, markers = "python_version > \"2.7\" and extra == \"languages\""} 177 | six = ">=1.11.0" 178 | 179 | [package.extras] 180 | languages = ["nltk (>=3.2.5)", "nltk (>=3.2.5,<3.5)"] 181 | 182 | [[package]] 183 | name = "markdown" 184 | version = "3.3.4" 185 | description = "Python implementation of Markdown." 186 | category = "dev" 187 | optional = false 188 | python-versions = ">=3.6" 189 | 190 | [package.dependencies] 191 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 192 | 193 | [package.extras] 194 | testing = ["coverage", "pyyaml"] 195 | 196 | [[package]] 197 | name = "markupsafe" 198 | version = "1.1.1" 199 | description = "Safely add untrusted strings to HTML/XML markup." 200 | category = "dev" 201 | optional = false 202 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" 203 | 204 | [[package]] 205 | name = "mccabe" 206 | version = "0.6.1" 207 | description = "McCabe checker, plugin for flake8" 208 | category = "dev" 209 | optional = false 210 | python-versions = "*" 211 | 212 | [[package]] 213 | name = "mkdocs" 214 | version = "1.1.2" 215 | description = "Project documentation with Markdown." 216 | category = "dev" 217 | optional = false 218 | python-versions = ">=3.5" 219 | 220 | [package.dependencies] 221 | click = ">=3.3" 222 | Jinja2 = ">=2.10.1" 223 | livereload = ">=2.5.1" 224 | lunr = {version = "0.5.8", extras = ["languages"]} 225 | Markdown = ">=3.2.1" 226 | PyYAML = ">=3.10" 227 | tornado = ">=5.0" 228 | 229 | [[package]] 230 | name = "mkdocs-material" 231 | version = "7.0.6" 232 | description = "A Material Design theme for MkDocs" 233 | category = "dev" 234 | optional = false 235 | python-versions = "*" 236 | 237 | [package.dependencies] 238 | markdown = ">=3.2" 239 | mkdocs = ">=1.1" 240 | mkdocs-material-extensions = ">=1.0" 241 | Pygments = ">=2.4" 242 | pymdown-extensions = ">=7.0" 243 | 244 | [[package]] 245 | name = "mkdocs-material-extensions" 246 | version = "1.0.1" 247 | description = "Extension pack for Python Markdown." 248 | category = "dev" 249 | optional = false 250 | python-versions = ">=3.5" 251 | 252 | [package.dependencies] 253 | mkdocs-material = ">=5.0.0" 254 | 255 | [[package]] 256 | name = "more-itertools" 257 | version = "8.7.0" 258 | description = "More routines for operating on iterables, beyond itertools" 259 | category = "dev" 260 | optional = false 261 | python-versions = ">=3.5" 262 | 263 | [[package]] 264 | name = "mypy" 265 | version = "0.812" 266 | description = "Optional static typing for Python" 267 | category = "dev" 268 | optional = false 269 | python-versions = ">=3.5" 270 | 271 | [package.dependencies] 272 | mypy-extensions = ">=0.4.3,<0.5.0" 273 | typed-ast = ">=1.4.0,<1.5.0" 274 | typing-extensions = ">=3.7.4" 275 | 276 | [package.extras] 277 | dmypy = ["psutil (>=4.0)"] 278 | 279 | [[package]] 280 | name = "mypy-extensions" 281 | version = "0.4.3" 282 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 283 | category = "dev" 284 | optional = false 285 | python-versions = "*" 286 | 287 | [[package]] 288 | name = "nltk" 289 | version = "3.6.6" 290 | description = "Natural Language Toolkit" 291 | category = "dev" 292 | optional = false 293 | python-versions = ">=3.6" 294 | 295 | [package.dependencies] 296 | click = "*" 297 | joblib = "*" 298 | regex = ">=2021.8.3" 299 | tqdm = "*" 300 | 301 | [package.extras] 302 | all = ["matplotlib", "numpy", "pyparsing", "python-crfsuite", "requests", "scikit-learn", "scipy", "twython"] 303 | corenlp = ["requests"] 304 | machine-learning = ["numpy", "python-crfsuite", "scikit-learn", "scipy"] 305 | plot = ["matplotlib"] 306 | tgrep = ["pyparsing"] 307 | twitter = ["twython"] 308 | 309 | [[package]] 310 | name = "packaging" 311 | version = "20.9" 312 | description = "Core utilities for Python packages" 313 | category = "dev" 314 | optional = false 315 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 316 | 317 | [package.dependencies] 318 | pyparsing = ">=2.0.2" 319 | 320 | [[package]] 321 | name = "pathspec" 322 | version = "0.9.0" 323 | description = "Utility library for gitignore style pattern matching of file paths." 324 | category = "dev" 325 | optional = false 326 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 327 | 328 | [[package]] 329 | name = "pillow" 330 | version = "9.3.0" 331 | description = "Python Imaging Library (Fork)" 332 | category = "main" 333 | optional = false 334 | python-versions = ">=3.7" 335 | 336 | [package.extras] 337 | docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"] 338 | tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] 339 | 340 | [[package]] 341 | name = "platformdirs" 342 | version = "2.5.2" 343 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 344 | category = "dev" 345 | optional = false 346 | python-versions = ">=3.7" 347 | 348 | [package.extras] 349 | docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"] 350 | test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] 351 | 352 | [[package]] 353 | name = "pluggy" 354 | version = "0.13.1" 355 | description = "plugin and hook calling mechanisms for python" 356 | category = "dev" 357 | optional = false 358 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 359 | 360 | [package.dependencies] 361 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 362 | 363 | [package.extras] 364 | dev = ["pre-commit", "tox"] 365 | 366 | [[package]] 367 | name = "py" 368 | version = "1.10.0" 369 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 370 | category = "dev" 371 | optional = false 372 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 373 | 374 | [[package]] 375 | name = "pycodestyle" 376 | version = "2.7.0" 377 | description = "Python style guide checker" 378 | category = "dev" 379 | optional = false 380 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 381 | 382 | [[package]] 383 | name = "pydantic" 384 | version = "1.8.2" 385 | description = "Data validation and settings management using python 3.6 type hinting" 386 | category = "main" 387 | optional = false 388 | python-versions = ">=3.6.1" 389 | 390 | [package.dependencies] 391 | typing-extensions = ">=3.7.4.3" 392 | 393 | [package.extras] 394 | dotenv = ["python-dotenv (>=0.10.4)"] 395 | email = ["email-validator (>=1.0.3)"] 396 | 397 | [[package]] 398 | name = "pyflakes" 399 | version = "2.3.0" 400 | description = "passive checker of Python programs" 401 | category = "dev" 402 | optional = false 403 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 404 | 405 | [[package]] 406 | name = "pygments" 407 | version = "2.8.1" 408 | description = "Pygments is a syntax highlighting package written in Python." 409 | category = "dev" 410 | optional = false 411 | python-versions = ">=3.5" 412 | 413 | [[package]] 414 | name = "pymdown-extensions" 415 | version = "8.1.1" 416 | description = "Extension pack for Python Markdown." 417 | category = "dev" 418 | optional = false 419 | python-versions = ">=3.6" 420 | 421 | [package.dependencies] 422 | Markdown = ">=3.2" 423 | 424 | [[package]] 425 | name = "pyparsing" 426 | version = "2.4.7" 427 | description = "Python parsing module" 428 | category = "dev" 429 | optional = false 430 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 431 | 432 | [[package]] 433 | name = "pytest" 434 | version = "5.4.3" 435 | description = "pytest: simple powerful testing with Python" 436 | category = "dev" 437 | optional = false 438 | python-versions = ">=3.5" 439 | 440 | [package.dependencies] 441 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 442 | attrs = ">=17.4.0" 443 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 444 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 445 | more-itertools = ">=4.0.0" 446 | packaging = "*" 447 | pluggy = ">=0.12,<1.0" 448 | py = ">=1.5.0" 449 | wcwidth = "*" 450 | 451 | [package.extras] 452 | checkqa-mypy = ["mypy (==v0.761)"] 453 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 454 | 455 | [[package]] 456 | name = "pytz" 457 | version = "2021.1" 458 | description = "World timezone definitions, modern and historical" 459 | category = "main" 460 | optional = false 461 | python-versions = "*" 462 | 463 | [[package]] 464 | name = "pyyaml" 465 | version = "5.4.1" 466 | description = "YAML parser and emitter for Python" 467 | category = "dev" 468 | optional = false 469 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 470 | 471 | [[package]] 472 | name = "regex" 473 | version = "2022.4.24" 474 | description = "Alternative regular expression module, to replace re." 475 | category = "dev" 476 | optional = false 477 | python-versions = ">=3.6" 478 | 479 | [[package]] 480 | name = "six" 481 | version = "1.15.0" 482 | description = "Python 2 and 3 compatibility utilities" 483 | category = "dev" 484 | optional = false 485 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 486 | 487 | [[package]] 488 | name = "sqlparse" 489 | version = "0.4.2" 490 | description = "A non-validating SQL parser." 491 | category = "main" 492 | optional = false 493 | python-versions = ">=3.5" 494 | 495 | [[package]] 496 | name = "tomli" 497 | version = "2.0.1" 498 | description = "A lil' TOML parser" 499 | category = "dev" 500 | optional = false 501 | python-versions = ">=3.7" 502 | 503 | [[package]] 504 | name = "tornado" 505 | version = "6.1" 506 | description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." 507 | category = "dev" 508 | optional = false 509 | python-versions = ">= 3.5" 510 | 511 | [[package]] 512 | name = "tqdm" 513 | version = "4.59.0" 514 | description = "Fast, Extensible Progress Meter" 515 | category = "dev" 516 | optional = false 517 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" 518 | 519 | [package.extras] 520 | dev = ["py-make (>=0.1.0)", "twine", "wheel"] 521 | notebook = ["ipywidgets (>=6)"] 522 | telegram = ["requests"] 523 | 524 | [[package]] 525 | name = "typed-ast" 526 | version = "1.4.2" 527 | description = "a fork of Python 2 and 3 ast modules with type comment support" 528 | category = "dev" 529 | optional = false 530 | python-versions = "*" 531 | 532 | [[package]] 533 | name = "typing-extensions" 534 | version = "4.2.0" 535 | description = "Backported and Experimental Type Hints for Python 3.7+" 536 | category = "main" 537 | optional = false 538 | python-versions = ">=3.7" 539 | 540 | [[package]] 541 | name = "wcwidth" 542 | version = "0.2.5" 543 | description = "Measures the displayed width of unicode strings in a terminal" 544 | category = "dev" 545 | optional = false 546 | python-versions = "*" 547 | 548 | [[package]] 549 | name = "zipp" 550 | version = "3.4.1" 551 | description = "Backport of pathlib-compatible object wrapper for zip files" 552 | category = "dev" 553 | optional = false 554 | python-versions = ">=3.6" 555 | 556 | [package.extras] 557 | docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] 558 | testing = ["func-timeout", "jaraco.itertools", "pytest (>=4.6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=1.2.3)", "pytest-cov", "pytest-enabler", "pytest-flake8", "pytest-mypy"] 559 | 560 | [metadata] 561 | lock-version = "1.1" 562 | python-versions = "^3.7" 563 | content-hash = "7173874bbec0ddc0e9151f222331aec21fc59f5bda7e09791b01b693d864a022" 564 | 565 | [metadata.files] 566 | asgiref = [ 567 | {file = "asgiref-3.3.1-py3-none-any.whl", hash = "sha256:5ee950735509d04eb673bd7f7120f8fa1c9e2df495394992c73234d526907e17"}, 568 | {file = "asgiref-3.3.1.tar.gz", hash = "sha256:7162a3cb30ab0609f1a4c95938fd73e8604f63bdba516a7f7d64b83ff09478f0"}, 569 | ] 570 | atomicwrites = [ 571 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 572 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 573 | ] 574 | attrs = [ 575 | {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"}, 576 | {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, 577 | ] 578 | black = [ 579 | {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"}, 580 | {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"}, 581 | {file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"}, 582 | {file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"}, 583 | {file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"}, 584 | {file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"}, 585 | {file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"}, 586 | {file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"}, 587 | {file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"}, 588 | {file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"}, 589 | {file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"}, 590 | {file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"}, 591 | {file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"}, 592 | {file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"}, 593 | {file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"}, 594 | {file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"}, 595 | {file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"}, 596 | {file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"}, 597 | {file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"}, 598 | {file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"}, 599 | {file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"}, 600 | {file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"}, 601 | {file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"}, 602 | ] 603 | click = [ 604 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, 605 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, 606 | ] 607 | colorama = [ 608 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 609 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 610 | ] 611 | django = [ 612 | {file = "Django-3.1.14-py3-none-any.whl", hash = "sha256:0fabc786489af16ad87a8c170ba9d42bfd23f7b699bd5ef05675864e8d012859"}, 613 | {file = "Django-3.1.14.tar.gz", hash = "sha256:72a4a5a136a214c39cf016ccdd6b69e2aa08c7479c66d93f3a9b5e4bb9d8a347"}, 614 | ] 615 | flake8 = [ 616 | {file = "flake8-3.9.0-py2.py3-none-any.whl", hash = "sha256:12d05ab02614b6aee8df7c36b97d1a3b2372761222b19b58621355e82acddcff"}, 617 | {file = "flake8-3.9.0.tar.gz", hash = "sha256:78873e372b12b093da7b5e5ed302e8ad9e988b38b063b61ad937f26ca58fc5f0"}, 618 | ] 619 | future = [ 620 | {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, 621 | ] 622 | importlib-metadata = [ 623 | {file = "importlib_metadata-3.7.3-py3-none-any.whl", hash = "sha256:b74159469b464a99cb8cc3e21973e4d96e05d3024d337313fedb618a6e86e6f4"}, 624 | {file = "importlib_metadata-3.7.3.tar.gz", hash = "sha256:742add720a20d0467df2f444ae41704000f50e1234f46174b51f9c6031a1bd71"}, 625 | ] 626 | jinja2 = [ 627 | {file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"}, 628 | {file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"}, 629 | ] 630 | joblib = [ 631 | {file = "joblib-1.2.0-py3-none-any.whl", hash = "sha256:091138ed78f800342968c523bdde947e7a305b8594b910a0fea2ab83c3c6d385"}, 632 | {file = "joblib-1.2.0.tar.gz", hash = "sha256:e1cee4a79e4af22881164f218d4311f60074197fb707e082e803b61f6d137018"}, 633 | ] 634 | livereload = [ 635 | {file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"}, 636 | ] 637 | lunr = [ 638 | {file = "lunr-0.5.8-py2.py3-none-any.whl", hash = "sha256:aab3f489c4d4fab4c1294a257a30fec397db56f0a50273218ccc3efdbf01d6ca"}, 639 | {file = "lunr-0.5.8.tar.gz", hash = "sha256:c4fb063b98eff775dd638b3df380008ae85e6cb1d1a24d1cd81a10ef6391c26e"}, 640 | ] 641 | markdown = [ 642 | {file = "Markdown-3.3.4-py3-none-any.whl", hash = "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"}, 643 | {file = "Markdown-3.3.4.tar.gz", hash = "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49"}, 644 | ] 645 | markupsafe = [ 646 | {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, 647 | {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, 648 | {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, 649 | {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, 650 | {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, 651 | {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, 652 | {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, 653 | {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, 654 | {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, 655 | {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, 656 | {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, 657 | {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, 658 | {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, 659 | {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, 660 | {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, 661 | {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, 662 | {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, 663 | {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, 664 | {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5"}, 665 | {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, 666 | {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, 667 | {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f"}, 668 | {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0"}, 669 | {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7"}, 670 | {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, 671 | {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, 672 | {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, 673 | {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193"}, 674 | {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, 675 | {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, 676 | {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1"}, 677 | {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1"}, 678 | {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f"}, 679 | {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, 680 | {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, 681 | {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, 682 | {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, 683 | {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, 684 | {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2"}, 685 | {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032"}, 686 | {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b"}, 687 | {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, 688 | {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, 689 | {file = "MarkupSafe-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c"}, 690 | {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb"}, 691 | {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014"}, 692 | {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850"}, 693 | {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85"}, 694 | {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"}, 695 | {file = "MarkupSafe-1.1.1-cp39-cp39-win32.whl", hash = "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39"}, 696 | {file = "MarkupSafe-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8"}, 697 | {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, 698 | ] 699 | mccabe = [ 700 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 701 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 702 | ] 703 | mkdocs = [ 704 | {file = "mkdocs-1.1.2-py3-none-any.whl", hash = "sha256:096f52ff52c02c7e90332d2e53da862fde5c062086e1b5356a6e392d5d60f5e9"}, 705 | {file = "mkdocs-1.1.2.tar.gz", hash = "sha256:f0b61e5402b99d7789efa032c7a74c90a20220a9c81749da06dbfbcbd52ffb39"}, 706 | ] 707 | mkdocs-material = [ 708 | {file = "mkdocs-material-7.0.6.tar.gz", hash = "sha256:e1423286dcb2ac6b9417e9e04a3f63a97f12f7f64802af09c8257561e9f3a319"}, 709 | {file = "mkdocs_material-7.0.6-py2.py3-none-any.whl", hash = "sha256:a89f8a08a5f0a5ecce2c7a4a61a1ddd2c2cbac86f17978264eb8b8ce2ca5411b"}, 710 | ] 711 | mkdocs-material-extensions = [ 712 | {file = "mkdocs-material-extensions-1.0.1.tar.gz", hash = "sha256:6947fb7f5e4291e3c61405bad3539d81e0b3cd62ae0d66ced018128af509c68f"}, 713 | {file = "mkdocs_material_extensions-1.0.1-py3-none-any.whl", hash = "sha256:d90c807a88348aa6d1805657ec5c0b2d8d609c110e62b9dce4daf7fa981fa338"}, 714 | ] 715 | more-itertools = [ 716 | {file = "more-itertools-8.7.0.tar.gz", hash = "sha256:c5d6da9ca3ff65220c3bfd2a8db06d698f05d4d2b9be57e1deb2be5a45019713"}, 717 | {file = "more_itertools-8.7.0-py3-none-any.whl", hash = "sha256:5652a9ac72209ed7df8d9c15daf4e1aa0e3d2ccd3c87f8265a0673cd9cbc9ced"}, 718 | ] 719 | mypy = [ 720 | {file = "mypy-0.812-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a26f8ec704e5a7423c8824d425086705e381b4f1dfdef6e3a1edab7ba174ec49"}, 721 | {file = "mypy-0.812-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:28fb5479c494b1bab244620685e2eb3c3f988d71fd5d64cc753195e8ed53df7c"}, 722 | {file = "mypy-0.812-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:9743c91088d396c1a5a3c9978354b61b0382b4e3c440ce83cf77994a43e8c521"}, 723 | {file = "mypy-0.812-cp35-cp35m-win_amd64.whl", hash = "sha256:d7da2e1d5f558c37d6e8c1246f1aec1e7349e4913d8fb3cb289a35de573fe2eb"}, 724 | {file = "mypy-0.812-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4eec37370483331d13514c3f55f446fc5248d6373e7029a29ecb7b7494851e7a"}, 725 | {file = "mypy-0.812-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d65cc1df038ef55a99e617431f0553cd77763869eebdf9042403e16089fe746c"}, 726 | {file = "mypy-0.812-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:61a3d5b97955422964be6b3baf05ff2ce7f26f52c85dd88db11d5e03e146a3a6"}, 727 | {file = "mypy-0.812-cp36-cp36m-win_amd64.whl", hash = "sha256:25adde9b862f8f9aac9d2d11971f226bd4c8fbaa89fb76bdadb267ef22d10064"}, 728 | {file = "mypy-0.812-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:552a815579aa1e995f39fd05dde6cd378e191b063f031f2acfe73ce9fb7f9e56"}, 729 | {file = "mypy-0.812-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:499c798053cdebcaa916eef8cd733e5584b5909f789de856b482cd7d069bdad8"}, 730 | {file = "mypy-0.812-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:5873888fff1c7cf5b71efbe80e0e73153fe9212fafdf8e44adfe4c20ec9f82d7"}, 731 | {file = "mypy-0.812-cp37-cp37m-win_amd64.whl", hash = "sha256:9f94aac67a2045ec719ffe6111df543bac7874cee01f41928f6969756e030564"}, 732 | {file = "mypy-0.812-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d23e0ea196702d918b60c8288561e722bf437d82cb7ef2edcd98cfa38905d506"}, 733 | {file = "mypy-0.812-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:674e822aa665b9fd75130c6c5f5ed9564a38c6cea6a6432ce47eafb68ee578c5"}, 734 | {file = "mypy-0.812-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:abf7e0c3cf117c44d9285cc6128856106183938c68fd4944763003decdcfeb66"}, 735 | {file = "mypy-0.812-cp38-cp38-win_amd64.whl", hash = "sha256:0d0a87c0e7e3a9becdfbe936c981d32e5ee0ccda3e0f07e1ef2c3d1a817cf73e"}, 736 | {file = "mypy-0.812-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7ce3175801d0ae5fdfa79b4f0cfed08807af4d075b402b7e294e6aa72af9aa2a"}, 737 | {file = "mypy-0.812-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:b09669bcda124e83708f34a94606e01b614fa71931d356c1f1a5297ba11f110a"}, 738 | {file = "mypy-0.812-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:33f159443db0829d16f0a8d83d94df3109bb6dd801975fe86bacb9bf71628e97"}, 739 | {file = "mypy-0.812-cp39-cp39-win_amd64.whl", hash = "sha256:3f2aca7f68580dc2508289c729bd49ee929a436208d2b2b6aab15745a70a57df"}, 740 | {file = "mypy-0.812-py3-none-any.whl", hash = "sha256:2f9b3407c58347a452fc0736861593e105139b905cca7d097e413453a1d650b4"}, 741 | {file = "mypy-0.812.tar.gz", hash = "sha256:cd07039aa5df222037005b08fbbfd69b3ab0b0bd7a07d7906de75ae52c4e3119"}, 742 | ] 743 | mypy-extensions = [ 744 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 745 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 746 | ] 747 | nltk = [ 748 | {file = "nltk-3.6.6-py3-none-any.whl", hash = "sha256:69470ba480ff4408e8ea82c530c8dc9571bab899f49d4571e8c8833e0916abd0"}, 749 | {file = "nltk-3.6.6.zip", hash = "sha256:0f8ff4e261c78605bca284e8d2025e562304766156af32a1731f56b396ee364b"}, 750 | ] 751 | packaging = [ 752 | {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, 753 | {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, 754 | ] 755 | pathspec = [ 756 | {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, 757 | {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, 758 | ] 759 | pillow = [ 760 | {file = "Pillow-9.3.0-1-cp37-cp37m-win32.whl", hash = "sha256:e6ea6b856a74d560d9326c0f5895ef8050126acfdc7ca08ad703eb0081e82b74"}, 761 | {file = "Pillow-9.3.0-1-cp37-cp37m-win_amd64.whl", hash = "sha256:32a44128c4bdca7f31de5be641187367fe2a450ad83b833ef78910397db491aa"}, 762 | {file = "Pillow-9.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:0b7257127d646ff8676ec8a15520013a698d1fdc48bc2a79ba4e53df792526f2"}, 763 | {file = "Pillow-9.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b90f7616ea170e92820775ed47e136208e04c967271c9ef615b6fbd08d9af0e3"}, 764 | {file = "Pillow-9.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68943d632f1f9e3dce98908e873b3a090f6cba1cbb1b892a9e8d97c938871fbe"}, 765 | {file = "Pillow-9.3.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be55f8457cd1eac957af0c3f5ece7bc3f033f89b114ef30f710882717670b2a8"}, 766 | {file = "Pillow-9.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d77adcd56a42d00cc1be30843d3426aa4e660cab4a61021dc84467123f7a00c"}, 767 | {file = "Pillow-9.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:829f97c8e258593b9daa80638aee3789b7df9da5cf1336035016d76f03b8860c"}, 768 | {file = "Pillow-9.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:801ec82e4188e935c7f5e22e006d01611d6b41661bba9fe45b60e7ac1a8f84de"}, 769 | {file = "Pillow-9.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:871b72c3643e516db4ecf20efe735deb27fe30ca17800e661d769faab45a18d7"}, 770 | {file = "Pillow-9.3.0-cp310-cp310-win32.whl", hash = "sha256:655a83b0058ba47c7c52e4e2df5ecf484c1b0b0349805896dd350cbc416bdd91"}, 771 | {file = "Pillow-9.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:9f47eabcd2ded7698106b05c2c338672d16a6f2a485e74481f524e2a23c2794b"}, 772 | {file = "Pillow-9.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:57751894f6618fd4308ed8e0c36c333e2f5469744c34729a27532b3db106ee20"}, 773 | {file = "Pillow-9.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7db8b751ad307d7cf238f02101e8e36a128a6cb199326e867d1398067381bff4"}, 774 | {file = "Pillow-9.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3033fbe1feb1b59394615a1cafaee85e49d01b51d54de0cbf6aa8e64182518a1"}, 775 | {file = "Pillow-9.3.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22b012ea2d065fd163ca096f4e37e47cd8b59cf4b0fd47bfca6abb93df70b34c"}, 776 | {file = "Pillow-9.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a65733d103311331875c1dca05cb4606997fd33d6acfed695b1232ba1df193"}, 777 | {file = "Pillow-9.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:502526a2cbfa431d9fc2a079bdd9061a2397b842bb6bc4239bb176da00993812"}, 778 | {file = "Pillow-9.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:90fb88843d3902fe7c9586d439d1e8c05258f41da473952aa8b328d8b907498c"}, 779 | {file = "Pillow-9.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:89dca0ce00a2b49024df6325925555d406b14aa3efc2f752dbb5940c52c56b11"}, 780 | {file = "Pillow-9.3.0-cp311-cp311-win32.whl", hash = "sha256:3168434d303babf495d4ba58fc22d6604f6e2afb97adc6a423e917dab828939c"}, 781 | {file = "Pillow-9.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:18498994b29e1cf86d505edcb7edbe814d133d2232d256db8c7a8ceb34d18cef"}, 782 | {file = "Pillow-9.3.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:772a91fc0e03eaf922c63badeca75e91baa80fe2f5f87bdaed4280662aad25c9"}, 783 | {file = "Pillow-9.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa4107d1b306cdf8953edde0534562607fe8811b6c4d9a486298ad31de733b2"}, 784 | {file = "Pillow-9.3.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4012d06c846dc2b80651b120e2cdd787b013deb39c09f407727ba90015c684f"}, 785 | {file = "Pillow-9.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77ec3e7be99629898c9a6d24a09de089fa5356ee408cdffffe62d67bb75fdd72"}, 786 | {file = "Pillow-9.3.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:6c738585d7a9961d8c2821a1eb3dcb978d14e238be3d70f0a706f7fa9316946b"}, 787 | {file = "Pillow-9.3.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:828989c45c245518065a110434246c44a56a8b2b2f6347d1409c787e6e4651ee"}, 788 | {file = "Pillow-9.3.0-cp37-cp37m-win32.whl", hash = "sha256:82409ffe29d70fd733ff3c1025a602abb3e67405d41b9403b00b01debc4c9a29"}, 789 | {file = "Pillow-9.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:41e0051336807468be450d52b8edd12ac60bebaa97fe10c8b660f116e50b30e4"}, 790 | {file = "Pillow-9.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:b03ae6f1a1878233ac620c98f3459f79fd77c7e3c2b20d460284e1fb370557d4"}, 791 | {file = "Pillow-9.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4390e9ce199fc1951fcfa65795f239a8a4944117b5935a9317fb320e7767b40f"}, 792 | {file = "Pillow-9.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40e1ce476a7804b0fb74bcfa80b0a2206ea6a882938eaba917f7a0f004b42502"}, 793 | {file = "Pillow-9.3.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0a06a052c5f37b4ed81c613a455a81f9a3a69429b4fd7bb913c3fa98abefc20"}, 794 | {file = "Pillow-9.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03150abd92771742d4a8cd6f2fa6246d847dcd2e332a18d0c15cc75bf6703040"}, 795 | {file = "Pillow-9.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:15c42fb9dea42465dfd902fb0ecf584b8848ceb28b41ee2b58f866411be33f07"}, 796 | {file = "Pillow-9.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:51e0e543a33ed92db9f5ef69a0356e0b1a7a6b6a71b80df99f1d181ae5875636"}, 797 | {file = "Pillow-9.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3dd6caf940756101205dffc5367babf288a30043d35f80936f9bfb37f8355b32"}, 798 | {file = "Pillow-9.3.0-cp38-cp38-win32.whl", hash = "sha256:f1ff2ee69f10f13a9596480335f406dd1f70c3650349e2be67ca3139280cade0"}, 799 | {file = "Pillow-9.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:276a5ca930c913f714e372b2591a22c4bd3b81a418c0f6635ba832daec1cbcfc"}, 800 | {file = "Pillow-9.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:73bd195e43f3fadecfc50c682f5055ec32ee2c933243cafbfdec69ab1aa87cad"}, 801 | {file = "Pillow-9.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1c7c8ae3864846fc95f4611c78129301e203aaa2af813b703c55d10cc1628535"}, 802 | {file = "Pillow-9.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e0918e03aa0c72ea56edbb00d4d664294815aa11291a11504a377ea018330d3"}, 803 | {file = "Pillow-9.3.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0915e734b33a474d76c28e07292f196cdf2a590a0d25bcc06e64e545f2d146c"}, 804 | {file = "Pillow-9.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af0372acb5d3598f36ec0914deed2a63f6bcdb7b606da04dc19a88d31bf0c05b"}, 805 | {file = "Pillow-9.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:ad58d27a5b0262c0c19b47d54c5802db9b34d38bbf886665b626aff83c74bacd"}, 806 | {file = "Pillow-9.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:97aabc5c50312afa5e0a2b07c17d4ac5e865b250986f8afe2b02d772567a380c"}, 807 | {file = "Pillow-9.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9aaa107275d8527e9d6e7670b64aabaaa36e5b6bd71a1015ddd21da0d4e06448"}, 808 | {file = "Pillow-9.3.0-cp39-cp39-win32.whl", hash = "sha256:bac18ab8d2d1e6b4ce25e3424f709aceef668347db8637c2296bcf41acb7cf48"}, 809 | {file = "Pillow-9.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:b472b5ea442148d1c3e2209f20f1e0bb0eb556538690fa70b5e1f79fa0ba8dc2"}, 810 | {file = "Pillow-9.3.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:ab388aaa3f6ce52ac1cb8e122c4bd46657c15905904b3120a6248b5b8b0bc228"}, 811 | {file = "Pillow-9.3.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbb8e7f2abee51cef77673be97760abff1674ed32847ce04b4af90f610144c7b"}, 812 | {file = "Pillow-9.3.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca31dd6014cb8b0b2db1e46081b0ca7d936f856da3b39744aef499db5d84d02"}, 813 | {file = "Pillow-9.3.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c7025dce65566eb6e89f56c9509d4f628fddcedb131d9465cacd3d8bac337e7e"}, 814 | {file = "Pillow-9.3.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ebf2029c1f464c59b8bdbe5143c79fa2045a581ac53679733d3a91d400ff9efb"}, 815 | {file = "Pillow-9.3.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b59430236b8e58840a0dfb4099a0e8717ffb779c952426a69ae435ca1f57210c"}, 816 | {file = "Pillow-9.3.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12ce4932caf2ddf3e41d17fc9c02d67126935a44b86df6a206cf0d7161548627"}, 817 | {file = "Pillow-9.3.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae5331c23ce118c53b172fa64a4c037eb83c9165aba3a7ba9ddd3ec9fa64a699"}, 818 | {file = "Pillow-9.3.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0b07fffc13f474264c336298d1b4ce01d9c5a011415b79d4ee5527bb69ae6f65"}, 819 | {file = "Pillow-9.3.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:073adb2ae23431d3b9bcbcff3fe698b62ed47211d0716b067385538a1b0f28b8"}, 820 | {file = "Pillow-9.3.0.tar.gz", hash = "sha256:c935a22a557a560108d780f9a0fc426dd7459940dc54faa49d83249c8d3e760f"}, 821 | ] 822 | platformdirs = [ 823 | {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, 824 | {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, 825 | ] 826 | pluggy = [ 827 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 828 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 829 | ] 830 | py = [ 831 | {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, 832 | {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, 833 | ] 834 | pycodestyle = [ 835 | {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, 836 | {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, 837 | ] 838 | pydantic = [ 839 | {file = "pydantic-1.8.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739"}, 840 | {file = "pydantic-1.8.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4"}, 841 | {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e"}, 842 | {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840"}, 843 | {file = "pydantic-1.8.2-cp36-cp36m-win_amd64.whl", hash = "sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b"}, 844 | {file = "pydantic-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20"}, 845 | {file = "pydantic-1.8.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb"}, 846 | {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1"}, 847 | {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23"}, 848 | {file = "pydantic-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287"}, 849 | {file = "pydantic-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd"}, 850 | {file = "pydantic-1.8.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505"}, 851 | {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e"}, 852 | {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820"}, 853 | {file = "pydantic-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3"}, 854 | {file = "pydantic-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316"}, 855 | {file = "pydantic-1.8.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62"}, 856 | {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f"}, 857 | {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b"}, 858 | {file = "pydantic-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3"}, 859 | {file = "pydantic-1.8.2-py3-none-any.whl", hash = "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833"}, 860 | {file = "pydantic-1.8.2.tar.gz", hash = "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b"}, 861 | ] 862 | pyflakes = [ 863 | {file = "pyflakes-2.3.0-py2.py3-none-any.whl", hash = "sha256:910208209dcea632721cb58363d0f72913d9e8cf64dc6f8ae2e02a3609aba40d"}, 864 | {file = "pyflakes-2.3.0.tar.gz", hash = "sha256:e59fd8e750e588358f1b8885e5a4751203a0516e0ee6d34811089ac294c8806f"}, 865 | ] 866 | pygments = [ 867 | {file = "Pygments-2.8.1-py3-none-any.whl", hash = "sha256:534ef71d539ae97d4c3a4cf7d6f110f214b0e687e92f9cb9d2a3b0d3101289c8"}, 868 | {file = "Pygments-2.8.1.tar.gz", hash = "sha256:2656e1a6edcdabf4275f9a3640db59fd5de107d88e8663c5d4e9a0fa62f77f94"}, 869 | ] 870 | pymdown-extensions = [ 871 | {file = "pymdown-extensions-8.1.1.tar.gz", hash = "sha256:632371fa3bf1b21a0e3f4063010da59b41db049f261f4c0b0872069a9b6d1735"}, 872 | {file = "pymdown_extensions-8.1.1-py3-none-any.whl", hash = "sha256:478b2c04513fbb2db61688d5f6e9030a92fb9be14f1f383535c43f7be9dff95b"}, 873 | ] 874 | pyparsing = [ 875 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 876 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 877 | ] 878 | pytest = [ 879 | {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, 880 | {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, 881 | ] 882 | pytz = [ 883 | {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, 884 | {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, 885 | ] 886 | pyyaml = [ 887 | {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, 888 | {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, 889 | {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, 890 | {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, 891 | {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, 892 | {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, 893 | {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"}, 894 | {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"}, 895 | {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, 896 | {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, 897 | {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, 898 | {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, 899 | {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"}, 900 | {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"}, 901 | {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, 902 | {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, 903 | {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, 904 | {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, 905 | {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"}, 906 | {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"}, 907 | {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, 908 | {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, 909 | {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, 910 | {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, 911 | {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"}, 912 | {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"}, 913 | {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, 914 | {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, 915 | {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, 916 | ] 917 | regex = [ 918 | {file = "regex-2022.4.24-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f86aef546add4ff1202e1f31e9bb54f9268f17d996b2428877283146bf9bc013"}, 919 | {file = "regex-2022.4.24-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e944268445b5694f5d41292c9228f0ca46d5a32a67f195d5f8547c1f1d91f4bc"}, 920 | {file = "regex-2022.4.24-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f8da3145f4b72f7ce6181c804eaa44cdcea313c8998cdade3d9e20a8717a9cb"}, 921 | {file = "regex-2022.4.24-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fd464e547dbabf4652ca5fe9d88d75ec30182981e737c07b3410235a44b9939"}, 922 | {file = "regex-2022.4.24-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:071bcb625e890f28b7c4573124a6512ea65107152b1d3ca101ce33a52dad4593"}, 923 | {file = "regex-2022.4.24-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c2de7f32fa87d04d40f54bce3843af430697aba51c3a114aa62837a0772f219"}, 924 | {file = "regex-2022.4.24-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a07e8366115069f26822c47732122ab61598830a69f5629a37ea8881487c107"}, 925 | {file = "regex-2022.4.24-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:036d1c1fbe69eba3ee253c107e71749cdbb4776db93d674bc0d5e28f30300734"}, 926 | {file = "regex-2022.4.24-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:af1e687ffab18a75409e5e5d6215b6ccd41a5a1a0ea6ce9665e01253f737a0d3"}, 927 | {file = "regex-2022.4.24-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:165cc75cfa5aa0f12adb2ac6286330e7229a06dc0e6c004ec35da682b5b89579"}, 928 | {file = "regex-2022.4.24-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:3e35c50b27f36176c792738cb9b858523053bc495044d2c2b44db24376b266f1"}, 929 | {file = "regex-2022.4.24-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:43ee0df35925ae4b0cc6ee3f60b73369e559dd2ac40945044da9394dd9d3a51d"}, 930 | {file = "regex-2022.4.24-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:58521abdab76583bd41ef47e5e2ddd93b32501aee4ee8cee71dee10a45ba46b1"}, 931 | {file = "regex-2022.4.24-cp310-cp310-win32.whl", hash = "sha256:275afc7352982ee947fc88f67a034b52c78395977b5fc7c9be15f7dc95b76f06"}, 932 | {file = "regex-2022.4.24-cp310-cp310-win_amd64.whl", hash = "sha256:253f858a0255cd91a0424a4b15c2eedb12f20274f85731b0d861c8137e843065"}, 933 | {file = "regex-2022.4.24-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:85b7ee4d0c7a46296d884f6b489af8b960c4291d76aea4b22fd4fbe05e6ec08e"}, 934 | {file = "regex-2022.4.24-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e0da7ef160d4f3eb3d4d3e39a02c3c42f7dbcfce62c81f784cc99fc7059765f"}, 935 | {file = "regex-2022.4.24-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f2e2cef324ca9355049ee1e712f68e2e92716eba24275e6767b9bfa15f1f478"}, 936 | {file = "regex-2022.4.24-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6165e737acb3bea3271372e8aa5ebe7226c8a8e8da1b94af2d6547c5a09d689d"}, 937 | {file = "regex-2022.4.24-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f6bd8178cce5bb56336722d5569d19c50bba5915a69a2050c497fb921e7cb0f"}, 938 | {file = "regex-2022.4.24-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:45b761406777a681db0c24686178532134c937d24448d9e085279b69e9eb7da4"}, 939 | {file = "regex-2022.4.24-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dfbadb7b74d95f72f9f9dbf9778f7de92722ab520a109ceaf7927461fa85b10"}, 940 | {file = "regex-2022.4.24-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9913bcf730eb6e9b441fb176832eea9acbebab6035542c7c89d90c803f5cd3be"}, 941 | {file = "regex-2022.4.24-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:68aed3fb0c61296bd6d234f558f78c51671f79ccb069cbcd428c2eea6fee7a5b"}, 942 | {file = "regex-2022.4.24-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:8e7d33f93cdd01868327d834d0f5bb029241cd293b47d51b96814dec27fc9b4b"}, 943 | {file = "regex-2022.4.24-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:82b7fc67e49fdce671bdbec1127189fc979badf062ce6e79dc95ef5e07a8bf92"}, 944 | {file = "regex-2022.4.24-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:c36906a7855ec33a9083608e6cd595e4729dab18aeb9aad0dd0b039240266239"}, 945 | {file = "regex-2022.4.24-cp36-cp36m-win32.whl", hash = "sha256:b2df3ede85d778c949d9bd2a50237072cee3df0a423c91f5514f78f8035bde87"}, 946 | {file = "regex-2022.4.24-cp36-cp36m-win_amd64.whl", hash = "sha256:dffd9114ade73137ab2b79a8faf864683dbd2dbbb6b23a305fbbd4cbaeeb2187"}, 947 | {file = "regex-2022.4.24-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6a0ef57cccd8089b4249eebad95065390e56c04d4a92c51316eab4131bca96a9"}, 948 | {file = "regex-2022.4.24-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12af15b6edb00e425f713160cfd361126e624ec0de86e74f7cad4b97b7f169b3"}, 949 | {file = "regex-2022.4.24-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f271d0831d8ebc56e17b37f9fa1824b0379221d1238ae77c18a6e8c47f1fdce"}, 950 | {file = "regex-2022.4.24-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37903d5ca11fa47577e8952d2e2c6de28553b11c70defee827afb941ab2c6729"}, 951 | {file = "regex-2022.4.24-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b747cef8e5dcdaf394192d43a0c02f5825aeb0ecd3d43e63ae500332ab830b0"}, 952 | {file = "regex-2022.4.24-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:582ea06079a03750b5f71e20a87cd99e646d796638b5894ff85987ebf5e04924"}, 953 | {file = "regex-2022.4.24-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:aa6daa189db9104787ff1fd7a7623ce017077aa59eaac609d0d25ba95ed251a0"}, 954 | {file = "regex-2022.4.24-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7dbc96419ef0fb6ac56626014e6d3a345aeb8b17a3df8830235a88626ffc8d84"}, 955 | {file = "regex-2022.4.24-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:0fb6cb16518ac7eff29d1e0b0cce90275dfae0f17154165491058c31d58bdd1d"}, 956 | {file = "regex-2022.4.24-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bea61de0c688198e3d9479344228c7accaa22a78b58ec408e41750ebafee6c08"}, 957 | {file = "regex-2022.4.24-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:46cbc5b23f85e94161b093dba1b49035697cf44c7db3c930adabfc0e6d861b95"}, 958 | {file = "regex-2022.4.24-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:50b77622016f03989cd06ecf6b602c7a6b4ed2e3ce04133876b041d109c934ee"}, 959 | {file = "regex-2022.4.24-cp37-cp37m-win32.whl", hash = "sha256:2bde99f2cdfd6db1ec7e02d68cadd384ffe7413831373ea7cc68c5415a0cb577"}, 960 | {file = "regex-2022.4.24-cp37-cp37m-win_amd64.whl", hash = "sha256:66fb765b2173d90389384708e3e1d3e4be1148bd8d4d50476b1469da5a2f0229"}, 961 | {file = "regex-2022.4.24-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:709396c0c95b95045fac89b94f997410ff39b81a09863fe21002f390d48cc7d3"}, 962 | {file = "regex-2022.4.24-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7a608022f4593fc67518c6c599ae5abdb03bb8acd75993c82cd7a4c8100eff81"}, 963 | {file = "regex-2022.4.24-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb7107faf0168de087f62a2f2ed00f9e9da12e0b801582b516ddac236b871cda"}, 964 | {file = "regex-2022.4.24-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aabc28f7599f781ddaeac168d0b566d0db82182cc3dcf62129f0a4fc2927b811"}, 965 | {file = "regex-2022.4.24-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:92ad03f928675ca05b79d3b1d3dfc149e2226d57ed9d57808f82105d511d0212"}, 966 | {file = "regex-2022.4.24-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7ba3c304a4a5d8112dbd30df8b3e4ef59b4b07807957d3c410d9713abaee9a8"}, 967 | {file = "regex-2022.4.24-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2acf5c66fbb62b5fe4c40978ddebafa50818f00bf79d60569d9762f6356336e"}, 968 | {file = "regex-2022.4.24-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7c4d9770e579eb11b582b2e2fd19fa204a15cb1589ae73cd4dcbb63b64f3e828"}, 969 | {file = "regex-2022.4.24-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:02543d6d5c32d361b7cc468079ba4cddaaf4a6544f655901ba1ff9d8e3f18755"}, 970 | {file = "regex-2022.4.24-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:73ed1b06abadbf6b61f6033a07c06f36ec0ddca117e41ef2ac37056705e46458"}, 971 | {file = "regex-2022.4.24-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3241db067a7f69da57fba8bca543ac8a7ca415d91e77315690202749b9fdaba1"}, 972 | {file = "regex-2022.4.24-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:d128e278e5e554c5c022c7bed410ca851e00bacebbb4460de546a73bc53f8de4"}, 973 | {file = "regex-2022.4.24-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b1d53835922cd0f9b74b2742453a444865a70abae38d12eb41c59271da66f38d"}, 974 | {file = "regex-2022.4.24-cp38-cp38-win32.whl", hash = "sha256:f2a5d9f612091812dee18375a45d046526452142e7b78c4e21ab192db15453d5"}, 975 | {file = "regex-2022.4.24-cp38-cp38-win_amd64.whl", hash = "sha256:a850f5f369f1e3b6239da7fb43d1d029c1e178263df671819889c47caf7e4ff3"}, 976 | {file = "regex-2022.4.24-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bedb3d01ad35ea1745bdb1d57f3ee0f996f988c98f5bbae9d068c3bb3065d210"}, 977 | {file = "regex-2022.4.24-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8bf867ba71856414a482e4b683500f946c300c4896e472e51d3db8dfa8dc8f32"}, 978 | {file = "regex-2022.4.24-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b415b82e5be7389ec5ee7ee35431e4a549ea327caacf73b697c6b3538cb5c87f"}, 979 | {file = "regex-2022.4.24-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9dae5affbb66178dad6c6fd5b02221ca9917e016c75ee3945e9a9563eb1fbb6f"}, 980 | {file = "regex-2022.4.24-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e65580ae3137bce712f505ec7c2d700aef0014a3878c4767b74aff5895fc454f"}, 981 | {file = "regex-2022.4.24-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e9e983fc8e0d4d5ded7caa5aed39ca2cf6026d7e39801ef6f0af0b1b6cd9276"}, 982 | {file = "regex-2022.4.24-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad3a770839aa456ff9a9aa0e253d98b628d005a3ccb37da1ff9be7c84fee16"}, 983 | {file = "regex-2022.4.24-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ed625205f5f26984382b68e4cbcbc08e6603c9e84c14b38457170b0cc71c823b"}, 984 | {file = "regex-2022.4.24-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c4fdf837666f7793a5c3cfa2f2f39f03eb6c7e92e831bc64486c2f547580c2b3"}, 985 | {file = "regex-2022.4.24-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ed26c3d2d62c6588e0dad175b8d8cc0942a638f32d07b80f92043e5d73b7db67"}, 986 | {file = "regex-2022.4.24-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f89d26e50a4c7453cb8c415acd09e72fbade2610606a9c500a1e48c43210a42d"}, 987 | {file = "regex-2022.4.24-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:97af238389cb029d63d5f2d931a7e8f5954ad96e812de5faaed373b68e74df86"}, 988 | {file = "regex-2022.4.24-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:be392d9cd5309509175a9d7660dc17bf57084501108dbff0c5a8bfc3646048c3"}, 989 | {file = "regex-2022.4.24-cp39-cp39-win32.whl", hash = "sha256:bcc6f7a3a95119c3568c572ca167ada75f8319890706283b9ba59b3489c9bcb3"}, 990 | {file = "regex-2022.4.24-cp39-cp39-win_amd64.whl", hash = "sha256:5b9c7b6895a01204296e9523b3e12b43e013835a9de035a783907c2c1bc447f0"}, 991 | {file = "regex-2022.4.24.tar.gz", hash = "sha256:92183e9180c392371079262879c6532ccf55f808e6900df5d9f03c9ca8807255"}, 992 | ] 993 | six = [ 994 | {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, 995 | {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, 996 | ] 997 | sqlparse = [ 998 | {file = "sqlparse-0.4.2-py3-none-any.whl", hash = "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"}, 999 | {file = "sqlparse-0.4.2.tar.gz", hash = "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae"}, 1000 | ] 1001 | tomli = [ 1002 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 1003 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 1004 | ] 1005 | tornado = [ 1006 | {file = "tornado-6.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32"}, 1007 | {file = "tornado-6.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c"}, 1008 | {file = "tornado-6.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9de9e5188a782be6b1ce866e8a51bc76a0fbaa0e16613823fc38e4fc2556ad05"}, 1009 | {file = "tornado-6.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:61b32d06ae8a036a6607805e6720ef00a3c98207038444ba7fd3d169cd998910"}, 1010 | {file = "tornado-6.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:3e63498f680547ed24d2c71e6497f24bca791aca2fe116dbc2bd0ac7f191691b"}, 1011 | {file = "tornado-6.1-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:6c77c9937962577a6a76917845d06af6ab9197702a42e1346d8ae2e76b5e3675"}, 1012 | {file = "tornado-6.1-cp35-cp35m-win32.whl", hash = "sha256:6286efab1ed6e74b7028327365cf7346b1d777d63ab30e21a0f4d5b275fc17d5"}, 1013 | {file = "tornado-6.1-cp35-cp35m-win_amd64.whl", hash = "sha256:fa2ba70284fa42c2a5ecb35e322e68823288a4251f9ba9cc77be04ae15eada68"}, 1014 | {file = "tornado-6.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb"}, 1015 | {file = "tornado-6.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:748290bf9112b581c525e6e6d3820621ff020ed95af6f17fedef416b27ed564c"}, 1016 | {file = "tornado-6.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e385b637ac3acaae8022e7e47dfa7b83d3620e432e3ecb9a3f7f58f150e50921"}, 1017 | {file = "tornado-6.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:25ad220258349a12ae87ede08a7b04aca51237721f63b1808d39bdb4b2164558"}, 1018 | {file = "tornado-6.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:65d98939f1a2e74b58839f8c4dab3b6b3c1ce84972ae712be02845e65391ac7c"}, 1019 | {file = "tornado-6.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:e519d64089b0876c7b467274468709dadf11e41d65f63bba207e04217f47c085"}, 1020 | {file = "tornado-6.1-cp36-cp36m-win32.whl", hash = "sha256:b87936fd2c317b6ee08a5741ea06b9d11a6074ef4cc42e031bc6403f82a32575"}, 1021 | {file = "tornado-6.1-cp36-cp36m-win_amd64.whl", hash = "sha256:cc0ee35043162abbf717b7df924597ade8e5395e7b66d18270116f8745ceb795"}, 1022 | {file = "tornado-6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7250a3fa399f08ec9cb3f7b1b987955d17e044f1ade821b32e5f435130250d7f"}, 1023 | {file = "tornado-6.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ed3ad863b1b40cd1d4bd21e7498329ccaece75db5a5bf58cd3c9f130843e7102"}, 1024 | {file = "tornado-6.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:dcef026f608f678c118779cd6591c8af6e9b4155c44e0d1bc0c87c036fb8c8c4"}, 1025 | {file = "tornado-6.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:70dec29e8ac485dbf57481baee40781c63e381bebea080991893cd297742b8fd"}, 1026 | {file = "tornado-6.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d3f7594930c423fd9f5d1a76bee85a2c36fd8b4b16921cae7e965f22575e9c01"}, 1027 | {file = "tornado-6.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3447475585bae2e77ecb832fc0300c3695516a47d46cefa0528181a34c5b9d3d"}, 1028 | {file = "tornado-6.1-cp37-cp37m-win32.whl", hash = "sha256:e7229e60ac41a1202444497ddde70a48d33909e484f96eb0da9baf8dc68541df"}, 1029 | {file = "tornado-6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:cb5ec8eead331e3bb4ce8066cf06d2dfef1bfb1b2a73082dfe8a161301b76e37"}, 1030 | {file = "tornado-6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:20241b3cb4f425e971cb0a8e4ffc9b0a861530ae3c52f2b0434e6c1b57e9fd95"}, 1031 | {file = "tornado-6.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c77da1263aa361938476f04c4b6c8916001b90b2c2fdd92d8d535e1af48fba5a"}, 1032 | {file = "tornado-6.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:fba85b6cd9c39be262fcd23865652920832b61583de2a2ca907dbd8e8a8c81e5"}, 1033 | {file = "tornado-6.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:1e8225a1070cd8eec59a996c43229fe8f95689cb16e552d130b9793cb570a288"}, 1034 | {file = "tornado-6.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d14d30e7f46a0476efb0deb5b61343b1526f73ebb5ed84f23dc794bdb88f9d9f"}, 1035 | {file = "tornado-6.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8f959b26f2634a091bb42241c3ed8d3cedb506e7c27b8dd5c7b9f745318ddbb6"}, 1036 | {file = "tornado-6.1-cp38-cp38-win32.whl", hash = "sha256:34ca2dac9e4d7afb0bed4677512e36a52f09caa6fded70b4e3e1c89dbd92c326"}, 1037 | {file = "tornado-6.1-cp38-cp38-win_amd64.whl", hash = "sha256:6196a5c39286cc37c024cd78834fb9345e464525d8991c21e908cc046d1cc02c"}, 1038 | {file = "tornado-6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5"}, 1039 | {file = "tornado-6.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe"}, 1040 | {file = "tornado-6.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea"}, 1041 | {file = "tornado-6.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2"}, 1042 | {file = "tornado-6.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0"}, 1043 | {file = "tornado-6.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd"}, 1044 | {file = "tornado-6.1-cp39-cp39-win32.whl", hash = "sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c"}, 1045 | {file = "tornado-6.1-cp39-cp39-win_amd64.whl", hash = "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4"}, 1046 | {file = "tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791"}, 1047 | ] 1048 | tqdm = [ 1049 | {file = "tqdm-4.59.0-py2.py3-none-any.whl", hash = "sha256:9fdf349068d047d4cfbe24862c425883af1db29bcddf4b0eeb2524f6fbdb23c7"}, 1050 | {file = "tqdm-4.59.0.tar.gz", hash = "sha256:d666ae29164da3e517fcf125e41d4fe96e5bb375cd87ff9763f6b38b5592fe33"}, 1051 | ] 1052 | typed-ast = [ 1053 | {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70"}, 1054 | {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487"}, 1055 | {file = "typed_ast-1.4.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412"}, 1056 | {file = "typed_ast-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400"}, 1057 | {file = "typed_ast-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606"}, 1058 | {file = "typed_ast-1.4.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64"}, 1059 | {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07"}, 1060 | {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc"}, 1061 | {file = "typed_ast-1.4.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a"}, 1062 | {file = "typed_ast-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151"}, 1063 | {file = "typed_ast-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3"}, 1064 | {file = "typed_ast-1.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41"}, 1065 | {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f"}, 1066 | {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581"}, 1067 | {file = "typed_ast-1.4.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37"}, 1068 | {file = "typed_ast-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd"}, 1069 | {file = "typed_ast-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496"}, 1070 | {file = "typed_ast-1.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc"}, 1071 | {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"}, 1072 | {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea"}, 1073 | {file = "typed_ast-1.4.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787"}, 1074 | {file = "typed_ast-1.4.2-cp38-cp38-win32.whl", hash = "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2"}, 1075 | {file = "typed_ast-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937"}, 1076 | {file = "typed_ast-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1"}, 1077 | {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6"}, 1078 | {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166"}, 1079 | {file = "typed_ast-1.4.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d"}, 1080 | {file = "typed_ast-1.4.2-cp39-cp39-win32.whl", hash = "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b"}, 1081 | {file = "typed_ast-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440"}, 1082 | {file = "typed_ast-1.4.2.tar.gz", hash = "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a"}, 1083 | ] 1084 | typing-extensions = [ 1085 | {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"}, 1086 | {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"}, 1087 | ] 1088 | wcwidth = [ 1089 | {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, 1090 | {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, 1091 | ] 1092 | zipp = [ 1093 | {file = "zipp-3.4.1-py3-none-any.whl", hash = "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"}, 1094 | {file = "zipp-3.4.1.tar.gz", hash = "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76"}, 1095 | ] 1096 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | authors = ["abersheeran "] 3 | description = "A non-intrusive component that can help you quickly create APIs." 4 | license = "MIT" 5 | name = "django-simple-api" 6 | readme = "README.md" 7 | version = "0.1.0" 8 | 9 | homepage = "https://github.com/abersheeran/django-simple-api" 10 | repository = "https://github.com/abersheeran/django-simple-api" 11 | 12 | classifiers = [ 13 | "Programming Language :: Python :: 3", 14 | ] 15 | 16 | packages = [ 17 | {include = "django_simple_api"}, 18 | ] 19 | 20 | [tool.poetry.dependencies] 21 | python = "^3.7" 22 | 23 | django = "*" 24 | pydantic = "^1.8.1" 25 | Pillow = ">=8.2,<10.0" 26 | 27 | [tool.poetry.dev-dependencies] 28 | black = {version = "*", allow-prereleases = true} 29 | flake8 = "*" 30 | mypy = "*" 31 | pytest = "^5.4.3" 32 | 33 | mkdocs = "*" 34 | mkdocs-material = "*" 35 | 36 | [tool.isort] 37 | profile = "black" 38 | 39 | [build-system] 40 | build-backend = "poetry.masonry.api" 41 | requires = ["poetry>=0.12"] 42 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = ./tests/testcases/pytest_cases/ 3 | -------------------------------------------------------------------------------- /script/test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import signal 3 | import subprocess 4 | import sys 5 | import time 6 | 7 | 8 | def execute(*commands): 9 | process = subprocess.Popen(" ".join(commands), cwd=os.getcwd(), shell=True) 10 | 11 | def sigterm_handler(sign, frame): 12 | process.terminate() 13 | process.wait() 14 | 15 | signal.signal(signal.SIGTERM, sigterm_handler) 16 | 17 | while process.poll() is None: 18 | time.sleep(1) 19 | return process.poll() 20 | 21 | 22 | def shell(command: str) -> None: 23 | exit_code = execute(command) 24 | if exit_code != 0: 25 | sys.exit(exit_code) 26 | 27 | 28 | if __name__ == "__main__": 29 | shell("black --check django_simple_api example") 30 | shell("mypy -p django_simple_api --ignore-missing-imports") 31 | shell("flake8 django_simple_api --ignore W503,E203,E501,E731") 32 | shell("pytest -o log_cli=true -o log_cli_level=DEBUG") 33 | shell("python manage.py test -v 2") 34 | -------------------------------------------------------------------------------- /script/upload.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | here = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 4 | 5 | package_name = "django_simple_api" 6 | 7 | 8 | def get_version(package: str = package_name) -> str: 9 | """ 10 | Return version. 11 | """ 12 | _globals: dict = {} 13 | with open(os.path.join(here, package, "__version__.py")) as f: 14 | exec(f.read(), _globals) 15 | 16 | return _globals["__version__"] 17 | 18 | 19 | os.chdir(here) 20 | os.system(f"poetry version {get_version()}") 21 | os.system(f"git add {package_name}/__version__.py pyproject.toml") 22 | os.system(f'git commit -m "v{get_version()}"') 23 | os.system("git push") 24 | os.system("git tag v{0}".format(get_version())) 25 | os.system("git push --tags") 26 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") 4 | -------------------------------------------------------------------------------- /tests/testcases/Python39.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Django-Simple-API/django-simple-api/9dd203b0899dbe3f736866702858c6e4a017a7e8/tests/testcases/Python39.png -------------------------------------------------------------------------------- /tests/testcases/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Django-Simple-API/django-simple-api/9dd203b0899dbe3f736866702858c6e4a017a7e8/tests/testcases/__init__.py -------------------------------------------------------------------------------- /tests/testcases/pytest_cases/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Django-Simple-API/django-simple-api/9dd203b0899dbe3f736866702858c6e4a017a7e8/tests/testcases/pytest_cases/__init__.py -------------------------------------------------------------------------------- /tests/testcases/pytest_cases/test_parameter_declare.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from django.http import HttpResponse 4 | 5 | from pydantic import BaseModel, Field 6 | 7 | from django_simple_api import Query 8 | from django_simple_api.exceptions import ExclusiveFieldError 9 | from django_simple_api.params import parse_and_bound_params 10 | 11 | 12 | class QueryPage(BaseModel): 13 | size: int = Field(10, alias="page-size") 14 | num: int = Field(1, alias="page-num") 15 | 16 | 17 | def just_test_view_1(request, p1: int = Query(exclusive=True)): 18 | return HttpResponse() 19 | 20 | 21 | def just_test_view_2(request, p1=Query(exclusive=True)): 22 | return HttpResponse() 23 | 24 | 25 | def just_test_view_3(request, p1: QueryPage = Query(exclusive=True), p2: int = Query()): 26 | return HttpResponse() 27 | 28 | 29 | def just_test_view_4( 30 | request, 31 | p1: int = Query(), 32 | p2: QueryPage = Query(exclusive=True), 33 | ): 34 | return HttpResponse() 35 | 36 | 37 | class TestParameterDeclare: 38 | def test_parameter_declare_1(self): 39 | with pytest.raises( 40 | TypeError, 41 | match="The `p1` parameter of `just_test_view_1` must use type annotations and " 42 | "the type annotations must be a subclass of BaseModel.", 43 | ): 44 | parse_and_bound_params(just_test_view_1) 45 | 46 | def test_parameter_declare_2(self): 47 | with pytest.raises( 48 | TypeError, 49 | match="The `p1` parameter of `just_test_view_2` must use type annotations and " 50 | "the type annotations must be a subclass of BaseModel.", 51 | ): 52 | parse_and_bound_params(just_test_view_2) 53 | 54 | def test_parameter_declare_3(self): 55 | with pytest.raises(ExclusiveFieldError) as e: 56 | parse_and_bound_params(just_test_view_3) 57 | assert ( 58 | str(e.value) == "You used exclusive parameter: `Query(exclusive=True)`, " 59 | "Please ensure the `Query` field is unique in `just_test_view_3`." 60 | ) 61 | 62 | def test_parameter_declare_4(self): 63 | with pytest.raises(ExclusiveFieldError) as e: 64 | parse_and_bound_params(just_test_view_4) 65 | assert ( 66 | str(e.value) == "You used exclusive parameter: `Query(exclusive=True)`, " 67 | "Please ensure the `Query` field is unique in `just_test_view_4`." 68 | ) 69 | 70 | def test_parameter_declare_5(self): 71 | with pytest.raises( 72 | ExclusiveFieldError, 73 | match="The `exclusive=True` parameter cannot be used with other parameters at the same time.", 74 | ): 75 | 76 | def just_test_view( 77 | request, 78 | p1: QueryPage = Query(exclusive=True, title="1"), 79 | ): 80 | return HttpResponse() 81 | -------------------------------------------------------------------------------- /tests/testcases/pytest_cases/test_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.http.request import QueryDict 3 | from django.urls import path, re_path 4 | 5 | from django_simple_api.utils import _reformat_pattern, merge_query_dict 6 | 7 | 8 | @pytest.mark.parametrize("query_dict,result", [(QueryDict(mutable=True), {})]) 9 | def test_merge_query_dict(query_dict, result): 10 | assert merge_query_dict(query_dict) == result 11 | 12 | 13 | @pytest.mark.parametrize( 14 | "pattern,path_format", 15 | [ 16 | (path("", lambda request: None).pattern, ""), 17 | (path("", lambda request: None).pattern, "{user}"), 18 | (path("", lambda request: None).pattern, "{user}"), 19 | (re_path(r"^(?P.*?)$", lambda request: None).pattern, "{user}"), 20 | ], 21 | ) 22 | def test_reformat_pattern(pattern, path_format): 23 | assert _reformat_pattern(pattern) == path_format 24 | -------------------------------------------------------------------------------- /tests/testcases/unittest_cases/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Django-Simple-API/django-simple-api/9dd203b0899dbe3f736866702858c6e4a017a7e8/tests/testcases/unittest_cases/__init__.py -------------------------------------------------------------------------------- /tests/testcases/unittest_cases/test_serialize.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.contrib.auth.models import User 3 | 4 | 5 | class TestSerialize(TestCase): 6 | @classmethod 7 | def setUpTestData(cls): 8 | User.objects.create_user(username="Zhang", email="111@163.com") 9 | 10 | def test_serialize_model(self): 11 | user = User.objects.get(username="Zhang") 12 | assert isinstance(user.to_json(), dict) 13 | 14 | def test_serialize_queryset(self): 15 | users = User.objects.filter(username="Zhang") 16 | assert isinstance(users.to_json(), list) 17 | assert isinstance(users.to_json()[0], dict) 18 | -------------------------------------------------------------------------------- /tests/testcases/unittest_cases/tests.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from django.test import TestCase 4 | 5 | 6 | class TestJustTest(TestCase): 7 | def test_success_get(self): 8 | resp = self.client.get("/test/just-test/1") 9 | self.assertEqual(resp.status_code, 200) 10 | self.assertEqual(resp.content, b"1") 11 | 12 | def test_failed_get(self): 13 | resp = self.client.get("/test/just-test/abc") 14 | self.assertEqual(resp.status_code, 422) 15 | 16 | def test_success_post(self): 17 | resp = self.client.post("/test/just-test/1", data={"name_id": 1}) 18 | self.assertEqual(resp.status_code, 200) 19 | self.assertEqual(resp.content, b"2") 20 | 21 | def test_failed_post(self): 22 | resp = self.client.post("/test/just-test/abc") 23 | self.assertEqual(resp.status_code, 422) 24 | 25 | resp = self.client.post("/test/just-test/1", data={"name_id": "abc"}) 26 | self.assertEqual(resp.status_code, 422) 27 | 28 | 29 | class TestFunctionView(TestCase): 30 | def test_success_get(self): 31 | resp = self.client.get("/test/test-get-func/1", data={"name_id": "2"}) 32 | self.assertEqual(resp.status_code, 200) 33 | self.assertEqual(resp.content, b"12") # '1' + '2' = '12' 34 | 35 | def test_failed_get(self): 36 | resp = self.client.get("/test/test-get-func/1") 37 | self.assertEqual(resp.status_code, 422) 38 | 39 | resp = self.client.get("/test/test-get-func/1", data={"name": "2"}) 40 | self.assertEqual(resp.status_code, 422) 41 | 42 | resp = self.client.post("/test/test-get-func/1") 43 | self.assertEqual(resp.status_code, 405) 44 | 45 | resp = self.client.post("/test/test-get-func/1?name_id=2") 46 | self.assertEqual(resp.status_code, 405) 47 | 48 | def test_success_post(self): 49 | resp = self.client.post("/test/test-post-func/1", HTTP_Authorization="2") 50 | self.assertEqual(resp.status_code, 200) 51 | self.assertEqual(resp.content, b"12") 52 | 53 | def test_failed_post(self): 54 | resp = self.client.post("/test/test-post-func/1") 55 | self.assertEqual(resp.status_code, 422) 56 | 57 | resp = self.client.get("/test/test-post-func/1", HTTP_Authorization="2") 58 | self.assertEqual(resp.status_code, 405) 59 | 60 | def test_success_put(self): 61 | resp = self.client.put("/test/test-put-func/1") 62 | self.assertEqual(resp.status_code, 200) 63 | self.assertEqual(resp.content, b"12") # default = "2" 64 | 65 | resp = self.client.put( 66 | "/test/test-put-func/2", data={"name": "3"}, content_type="application/json" 67 | ) 68 | self.assertEqual(resp.status_code, 200) 69 | self.assertEqual(resp.content, b"23") 70 | 71 | def test_failed_put(self): 72 | resp = self.client.post("/test/test-put-func/1") 73 | self.assertEqual(resp.status_code, 405) 74 | 75 | def test_success_delete(self): 76 | cookies = self.client.cookies 77 | cookies["session_id"] = 2 78 | resp = self.client.delete("/test/test-delete-func/1") 79 | self.assertEqual(resp.status_code, 200) 80 | self.assertEqual(resp.content, b"12") # "1" + "2" = "12" 81 | 82 | resp = self.client.delete("/test/test-delete-func/2") 83 | self.assertEqual(resp.status_code, 200) 84 | self.assertEqual(resp.content, b"22") 85 | 86 | def test_failed_delete(self): 87 | resp = self.client.delete("/test/test-delete-func/1") 88 | self.assertEqual(resp.status_code, 422) 89 | 90 | 91 | class TestExclusive(TestCase): 92 | def test_success_get(self): 93 | resp = self.client.get("/test/test-query-page", data={"page-size": 20}) 94 | self.assertEqual(resp.content, b"0") 95 | 96 | resp = self.client.get( 97 | "/test/test-query-page-by-exclusive", data={"page-size": 20} 98 | ) 99 | self.assertEqual(resp.content, b"0") 100 | 101 | resp = self.client.get( 102 | "/test/test-query-page", data={"page-size": 20, "page-num": 3} 103 | ) 104 | self.assertEqual(resp.content, b"40") 105 | 106 | resp = self.client.get( 107 | "/test/test-query-page-by-exclusive", data={"page-size": 20, "page-num": 3} 108 | ) 109 | self.assertEqual(resp.content, b"40") 110 | 111 | 112 | class TestCommonView(TestCase): 113 | def test_func_view_success(self): 114 | resp = self.client.get("/test/test-common-func-view", data={"id": 1}) 115 | self.assertEqual(resp.status_code, 200) 116 | self.assertEqual(resp.content, b"1") 117 | 118 | resp = self.client.post("/test/test-common-func-view", data={"name": "Rie"}) 119 | self.assertEqual(resp.status_code, 200) 120 | self.assertEqual(resp.content, b"Rie") 121 | 122 | def test_path_func_view_success(self): 123 | resp = self.client.get("/test/test-common-func-view/1", data={"name": "Rie"}) 124 | self.assertEqual(resp.status_code, 200) 125 | self.assertEqual(resp.content, b"1Rie") 126 | 127 | resp = self.client.post("/test/test-common-func-view/2", data={"name": "Rie"}) 128 | self.assertEqual(resp.status_code, 200) 129 | self.assertEqual(resp.content, b"2") 130 | 131 | def test_class_view_success(self): 132 | resp = self.client.get("/test/test-common-class-view", data={"id": 1}) 133 | self.assertEqual(resp.status_code, 200) 134 | self.assertEqual(resp.content, b"1") 135 | 136 | resp = self.client.post("/test/test-common-class-view", data={"name": "Rie"}) 137 | self.assertEqual(resp.status_code, 200) 138 | self.assertEqual(resp.content, b"Rie") 139 | 140 | def test_class_view_failed(self): 141 | resp = self.client.put("/test/test-common-class-view") 142 | self.assertEqual(resp.status_code, 405) 143 | 144 | 145 | class TestUpload(TestCase): 146 | def test_upload_file_failed(self): 147 | resp = self.client.post("/test/test-upload-file-view", data={"file": "file"}) 148 | self.assertEqual(resp.status_code, 422) 149 | 150 | def test_upload_file_success(self): 151 | file_path = Path(__file__).resolve(strict=True).parent.parent / "洛神赋.md" 152 | with open(file_path, "rb") as file: 153 | resp = self.client.post("/test/test-upload-file-view", data={"file": file}) 154 | self.assertEqual(resp.status_code, 200) 155 | 156 | def test_upload_image_failed(self): 157 | file_path = Path(__file__).resolve(strict=True).parent.parent / "洛神赋.md" 158 | with open(file_path, "rb") as file: 159 | resp = self.client.post( 160 | "/test/test-upload-image-view", data={"image": file} 161 | ) 162 | self.assertEqual(resp.status_code, 422) 163 | 164 | def test_upload_image_success(self): 165 | image_path = Path(__file__).resolve(strict=True).parent.parent / "Python39.png" 166 | 167 | with open(image_path, "rb") as image: 168 | resp = self.client.post( 169 | "/test/test-upload-image-view", data={"image": image} 170 | ) 171 | self.assertEqual(resp.status_code, 200) 172 | -------------------------------------------------------------------------------- /tests/testcases/洛神赋.md: -------------------------------------------------------------------------------- 1 | # 洛神赋 2 | 3 | [【作者】曹植 ](https://hanyu.baidu.com/s?wd=曹植)【朝代】魏晋 4 | 5 | 黄初三年,余朝京师,还济洛川。古人有言,斯水之神,名曰宓妃。感宋玉对楚王神女之事,遂作斯赋。其辞曰: 6 | 7 | 余从京域,言归东藩。背伊阙,越轘辕,经通谷,陵景山。日既西倾,车殆马烦。尔乃税驾乎蘅皋,秣驷乎芝田,容与乎阳林,流眄乎洛川。于是精移神骇,忽焉思散。俯则未察,仰以殊观,睹一丽人,于岩之畔。乃援御者而告之曰:“尔有觌于彼者乎?彼何人斯?若此之艳也!”御者对曰:“臣闻河洛之神,名曰宓妃。然则君王之所见也,无乃是乎?其状若何?臣愿闻之。” 8 | 9 | 余告之曰:“其形也,翩若惊鸿,婉若游龙。荣曜秋菊,华茂春松。髣髴兮若轻云之蔽月,飘飖兮若流风之回雪。远而望之,皎若太阳升朝霞;迫而察之,灼若芙蕖出渌波。秾纤得衷,修短合度。肩若削成,腰如约素。延颈秀项,皓质呈露。芳泽无加,铅华弗御。云髻峨峨,修眉联娟。丹唇外朗,皓齿内鲜,明眸善睐,靥辅承权。瑰姿艳逸,仪静体闲。柔情绰态,媚于语言。奇服旷世,骨像应图。披罗衣之璀粲兮,珥瑶碧之华琚。戴金翠之首饰,缀明珠以耀躯。践远游之文履,曳雾绡之轻裾。微幽兰之芳蔼兮,步踟蹰于山隅。 10 | 11 | 于是忽焉纵体,以遨以嬉。左倚采旄,右荫桂旗。攘皓腕于神浒兮,采湍濑之玄芝。余情悦其淑美兮,心振荡而不怡。无良媒以接欢兮,托微波而通辞。愿诚素之先达兮,解玉佩以要之。嗟佳人之信修兮,羌习礼而明诗。抗琼珶以和予兮,指潜渊而为期。执眷眷之款实兮,惧斯灵之我欺。感交甫之弃言兮,怅犹豫而狐疑。收和颜而静志兮,申礼防以自持。 12 | 13 | 于是洛灵感焉,徙倚彷徨,神光离合,乍阴乍阳。竦轻躯以鹤立,若将飞而未翔。践椒涂之郁烈,步蘅薄而流芳。超长吟以永慕兮,声哀厉而弥长。 14 | 15 | 尔乃众灵杂沓,命俦啸侣,或戏清流,或翔神渚,或采明珠,或拾翠羽。从南湘之二妃,携汉滨之游女。叹匏瓜之无匹兮,咏牵牛之独处。扬轻袿之猗靡兮,翳修袖以延伫。体迅飞凫,飘忽若神,凌波微步,罗袜生尘。动无常则,若危若安。进止难期,若往若还。转眄流精,光润玉颜。含辞未吐,气若幽兰。华容婀娜,令我忘餐。 16 | 17 | 于是屏翳收风,川后静波。冯夷鸣鼓,女娲清歌。腾文鱼以警乘,鸣玉鸾以偕逝。六龙俨其齐首,载云车之容裔,鲸鲵踊而夹毂,水禽翔而为卫。 18 | 19 | 于是越北沚。过南冈,纡素领,回清阳,动朱唇以徐言,陈交接之大纲。恨人神之道殊兮,怨盛年之莫当。抗罗袂以掩涕兮,泪流襟之浪浪。悼良会之永绝兮,哀一逝而异乡。无微情以效爱兮,献江南之明珰。虽潜处于太阴,长寄心于君王。忽不悟其所舍,怅神宵而蔽光。 20 | 21 | 于是背下陵高,足往神留,遗情想像,顾望怀愁。冀灵体之复形,御轻舟而上溯。浮长川而忘返,思绵绵而增慕。夜耿耿而不寐,沾繁霜而至曙。命仆夫而就驾,吾将归乎东路。揽騑辔以抗策,怅盘桓而不能去。 -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | urlpatterns = [ 6 | path("just-test/", views.JustTest.as_view()), 7 | # test function based views with method get, post, put, delete 8 | # test view injection parameters check Path, Query, Header, Cookie, Body 9 | path("test-get-func/", views.get_func), 10 | path("test-post-func/", views.post_func), 11 | path("test-put-func/", views.put_func), 12 | path("test-delete-func/", views.test_delete_func), 13 | # test view injection parameters check Exclusive 14 | path("test-query-page", views.query_page), 15 | path("test-query-page-by-exclusive", views.query_page_by_exclusive), 16 | # test common view without @allow_request_method and injection parameters 17 | path("test-common-func-view", views.test_common_func_view), 18 | path("test-common-func-view/", views.test_common_path_func_view), 19 | path("test-common-class-view", views.CommonClassView.as_view()), 20 | path("test-upload-file-view", views.TestUploadFile.as_view()), 21 | path("test-upload-image-view", views.TestUploadImage.as_view()), 22 | ] 23 | -------------------------------------------------------------------------------- /tests/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpRequest 2 | from django.http.response import HttpResponse 3 | from django.views import View 4 | from pydantic import BaseModel, Field 5 | 6 | from django_simple_api import ( 7 | Body, 8 | Cookie, 9 | Header, 10 | Path, 11 | Query, 12 | allow_request_method, 13 | UploadFile, 14 | ) 15 | from django_simple_api.types import UploadImage 16 | 17 | 18 | class JustTest(View): 19 | def get( 20 | self, 21 | request: HttpRequest, 22 | id: int = Path(description="This is description of id."), 23 | ) -> HttpResponse: 24 | """ 25 | This is summary. 26 | 27 | This is description. 28 | """ 29 | return HttpResponse(id) 30 | 31 | def post(self, request, id: int = Path(), name_id: int = Body()): 32 | return HttpResponse(id + name_id) 33 | 34 | 35 | @allow_request_method("get") 36 | def get_func(request, name: str = Path(), name_id: str = Query()): 37 | return HttpResponse(name + name_id) 38 | 39 | 40 | @allow_request_method("post") 41 | def post_func(request, name: str = Path(), token: str = Header(alias="Authorization")): 42 | return HttpResponse(name + token) 43 | 44 | 45 | @allow_request_method("put") 46 | def put_func(request, id: int = Path(default=1), name: str = Body(default="2")): 47 | assert isinstance(id, int), "params type error" 48 | assert isinstance(name, str), "params type error" 49 | return HttpResponse(str(id) + name) 50 | 51 | 52 | @allow_request_method("delete") 53 | def test_delete_func(request, id: int = Path(), session_id: str = Cookie()): 54 | return HttpResponse(str(id) + session_id) 55 | 56 | 57 | @allow_request_method("get") 58 | def query_page( 59 | request, 60 | page_size: int = Query(default=10, alias="page-size"), 61 | page_num: int = Query(default=1, alias="page-num"), 62 | ): 63 | return HttpResponse(page_size * (page_num - 1)) 64 | 65 | 66 | class QueryPage(BaseModel): 67 | size: int = Field(10, alias="page-size") 68 | num: int = Field(1, alias="page-num") 69 | 70 | 71 | @allow_request_method("get") 72 | def query_page_by_exclusive(request, page: QueryPage = Query(exclusive=True)): 73 | return HttpResponse(page.size * (page.num - 1)) 74 | 75 | 76 | def test_common_func_view(request): 77 | id = request.GET.get("id", "") 78 | name = request.POST.get("name", "") 79 | return HttpResponse(id + name) 80 | 81 | 82 | def test_common_path_func_view(request, id): 83 | name = request.GET.get("name", "") 84 | return HttpResponse(id + name) 85 | 86 | 87 | class CommonClassView(View): 88 | def get(self, request): 89 | id = request.GET.get("id", "") 90 | return HttpResponse(id) 91 | 92 | def post(self, request): 93 | name = request.POST.get("name", "") 94 | return HttpResponse(name) 95 | 96 | 97 | class TestUploadFile(View): 98 | def post(self, request, file: UploadFile = Body()): 99 | return HttpResponse(file.name) 100 | 101 | 102 | class TestUploadImage(View): 103 | def post(self, request, image: UploadImage = Body()): 104 | return HttpResponse(image.name) 105 | --------------------------------------------------------------------------------