diff --git a/README.md b/README.md index ba42b66..666caf9 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,8 @@ Or pro version online demo [here](https://fastapi-admin-pro.long2ice.cn/admin/lo ![](https://raw.githubusercontent.com/fastapi-admin/fastapi-admin/dev/images/dashboard.png) +## Run examples in local + ## Documentation See documentation at [https://fastapi-admin.github.io/fastapi-admin](https://fastapi-admin.github.io/fastapi-admin). diff --git a/examples/main.py b/examples/main.py index 2ca0b32..c6ac4c5 100644 --- a/examples/main.py +++ b/examples/main.py @@ -11,10 +11,10 @@ from tortoise.contrib.fastapi import register_tortoise from examples import settings from examples.constants import BASE_DIR from examples.models import Admin +from examples.providers import LoginProvider from fastapi_admin.app import app as admin_app -from fastapi_admin.providers.login import UsernamePasswordProvider -login_provider = UsernamePasswordProvider(admin_model=Admin) +login_provider = LoginProvider(admin_model=Admin) def create_app(): diff --git a/examples/providers.py b/examples/providers.py new file mode 100644 index 0000000..4da4837 --- /dev/null +++ b/examples/providers.py @@ -0,0 +1,7 @@ +from fastapi_admin.models import AbstractAdmin +from fastapi_admin.providers.login import UsernamePasswordProvider + + +class LoginProvider(UsernamePasswordProvider): + async def update_password(self, admin: AbstractAdmin, password: str): + pass diff --git a/examples/resources.py b/examples/resources.py index 40c27ae..96be721 100644 --- a/examples/resources.py +++ b/examples/resources.py @@ -1,11 +1,12 @@ import os +from typing import List from examples import enums from examples.constants import BASE_DIR from examples.models import Admin, Category, Config, Product from fastapi_admin.app import app from fastapi_admin.providers.file_upload import FileUploadProvider -from fastapi_admin.resources import Dropdown, Field, Link, Model +from fastapi_admin.resources import Action, Dropdown, Field, Link, Model from fastapi_admin.widgets import displays, filters, inputs upload_provider = FileUploadProvider(uploads_dir=os.path.join(BASE_DIR, "static", "uploads")) @@ -14,7 +15,7 @@ upload_provider = FileUploadProvider(uploads_dir=os.path.join(BASE_DIR, "static" @app.register class Home(Link): label = "Home" - icon = "ti ti-home" + icon = "fas fa-home" url = "/admin" @@ -22,7 +23,7 @@ class Home(Link): class AdminResource(Model): label = "Admin" model = Admin - icon = "ti ti-user" + icon = "fas fa-user" page_pre_title = "admin list" page_title = "admin model" filters = [ @@ -49,6 +50,13 @@ class AdminResource(Model): ), "created_at", ] + can_create = False + + def get_actions(self) -> List[Action]: + return [] + + def get_bulk_actions(self) -> List[Action]: + return [] @app.register @@ -78,7 +86,7 @@ class Content(Dropdown): ] label = "Content" - icon = "ti ti-package" + icon = "fas fa-bars" resources = [ProductResource, CategoryResource] @@ -86,7 +94,7 @@ class Content(Dropdown): class ConfigResource(Model): label = "Config" model = Config - icon = "ti ti-settings" + icon = "fas fa-cogs" filters = [ filters.Enum(enum=enums.Status, name="status", label="Status"), filters.Search(name="key", label="Key", search_mode="equal"), @@ -107,8 +115,8 @@ class ConfigResource(Model): @app.register class GithubLink(Link): label = "Github" - url = "https://github.com/long2ice" - icon = "ti ti-brand-github" + url = "https://github.com/fastapi-admin/fastapi-admin" + icon = "fab fa-github" target = "_blank" @@ -116,5 +124,5 @@ class GithubLink(Link): class DocumentationLink(Link): label = "Documentation" url = "https://long2ice.github.io/fastadmin" - icon = "ti ti-file-text" + icon = "fas fa-file-code" target = "_blank" diff --git a/examples/templates/password.html b/examples/templates/password.html new file mode 100644 index 0000000..5dc5c14 --- /dev/null +++ b/examples/templates/password.html @@ -0,0 +1,37 @@ +{% extends "layout.html" %} +{% block page_body %} + {% include "components/alert_error.html" %} +
+
+

{{ _('update_password') }} (This will not take effect because is overwrite in examples)

+
+
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+{% endblock %} diff --git a/fastapi_admin/app.py b/fastapi_admin/app.py index 819adc3..4af6ff6 100644 --- a/fastapi_admin/app.py +++ b/fastapi_admin/app.py @@ -21,7 +21,7 @@ class FastAdmin(FastAPI): admin_path: str resources: List[Type[Resource]] = [] model_resources: Dict[Type[Model], Type[Resource]] = {} - login_provider: Optional[Type[LoginProvider]] = LoginProvider + login_provider: Optional[LoginProvider] redis: Redis def configure( @@ -77,7 +77,7 @@ class FastAdmin(FastAPI): self.resources.append(resource) def get_model_resource(self, model: Type[Model]): - return self.model_resources[model] + return self.model_resources[model]() app = FastAdmin( diff --git a/fastapi_admin/depends.py b/fastapi_admin/depends.py index 49770bf..2541b61 100644 --- a/fastapi_admin/depends.py +++ b/fastapi_admin/depends.py @@ -19,7 +19,12 @@ def get_model(resource: Optional[str] = Path(...)): def get_model_resource(request: Request, model=Depends(get_model)): - return request.app.get_model_resource(model) + model_resource = request.app.get_model_resource(model) # type:Model + actions = model_resource.get_actions() + bulk_actions = model_resource.get_bulk_actions() + model_resource.actions = actions + model_resource.bulk_actions = bulk_actions + return model_resource def _get_resources(resources: List[Type[Resource]]): diff --git a/fastapi_admin/middlewares.py b/fastapi_admin/middlewares.py index 4f58f95..1672b6c 100644 --- a/fastapi_admin/middlewares.py +++ b/fastapi_admin/middlewares.py @@ -2,7 +2,7 @@ from typing import Callable from starlette.requests import Request -from fastapi_admin import i18n, template +from fastapi_admin import i18n async def language_processor(request: Request, call_next: Callable): diff --git a/fastapi_admin/resources.py b/fastapi_admin/resources.py index ac814de..94563e9 100644 --- a/fastapi_admin/resources.py +++ b/fastapi_admin/resources.py @@ -1,14 +1,14 @@ from typing import List, Optional, Type, Union from pydantic import BaseModel -from tortoise import ForeignKeyFieldInstance +from starlette.datastructures import FormData +from tortoise import ForeignKeyFieldInstance, ManyToManyFieldInstance from tortoise import Model as TortoiseModel from tortoise.fields import BooleanField, DateField, DatetimeField, JSONField from tortoise.fields.data import CharEnumFieldInstance, IntEnumFieldInstance, IntField, TextField from fastapi_admin.exceptions import NoSuchFieldFound from fastapi_admin.i18n import _ -from fastapi_admin.utils import ClassProperty from fastapi_admin.widgets import Widget, displays, inputs from fastapi_admin.widgets.filters import Filter @@ -34,11 +34,11 @@ class Field: input: inputs.Input def __init__( - self, - name: str, - label: str, - display: Optional[displays.Display] = None, - input_: Optional[Widget] = None, + self, + name: str, + label: str, + display: Optional[displays.Display] = None, + input_: Optional[Widget] = None, ): self.name = name self.label = label @@ -67,20 +67,16 @@ class Model(Resource): page_pre_title: Optional[str] = None page_title: Optional[str] = None filters: Optional[List[Union[str, Filter]]] = [] - can_edit: bool = True - can_delete: bool = True can_create: bool = True enctype = "application/x-www-form-urlencoded" - @ClassProperty - def actions(self) -> List[Action]: + def get_actions(self) -> List[Action]: return [ Action(label=_("update"), icon="ti ti-edit", name="update", ajax=False), Action(label=_("delete"), icon="ti ti-trash", name="delete", method="DELETE"), ] - @ClassProperty - def bulk_actions(self) -> List[Action]: + def get_bulk_actions(self) -> List[Action]: return [ Action(label=_("delete_selected"), icon="ti ti-trash", name="delete", method="DELETE"), ] @@ -109,16 +105,23 @@ class Model(Resource): return ret @classmethod - async def resolve_data(cls, data: dict): + async def resolve_data(cls, data: FormData): ret = {} + m2m_ret = {} for field in cls.get_fields(is_display=False): input_ = field.input if input_.context.get("disabled") or isinstance(input_, inputs.DisplayOnly): continue name = input_.context.get("name") - v = data.get(name) - ret[name] = await input_.parse_value(v) - return ret + if isinstance(input_, inputs.ManyToMany): + v = data.getlist(name) + value = await input_.parse_value(v) + m2m_ret[name] = await input_.model.filter(pk__in=value) + else: + v = data.get(name) + value = await input_.parse_value(v) + ret[name] = value + return ret, m2m_ret @classmethod async def get_filters(cls, values: Optional[dict] = None): @@ -191,7 +194,8 @@ class Model(Resource): field.related_model, null=null, default=field.default ) field_name = field.source_field - + elif isinstance(field, ManyToManyFieldInstance): + display, input_ = displays.InputOnly(), inputs.ManyToMany(field.related_model) return Field(name=field_name, label=label.title(), display=display, input_=input_) @classmethod @@ -203,7 +207,7 @@ class Model(Resource): ret.append(field) else: if (is_display and isinstance(field.display, displays.InputOnly)) or ( - not is_display and isinstance(field.input, inputs.DisplayOnly) + not is_display and isinstance(field.input, inputs.DisplayOnly) ): continue ret.append(field) @@ -213,6 +217,14 @@ class Model(Resource): def get_fields_label(cls, display: bool = True): return cls._get_fields_attr("label", display) + @classmethod + def get_m2m_field(cls): + ret = [] + for field in cls.fields or cls.model._meta.fields: + if field in cls.model._meta.m2m_fields: + ret.append(field) + return ret + class Dropdown(Resource): resources: List[Type[Resource]] diff --git a/fastapi_admin/routes/password.py b/fastapi_admin/routes/password.py index a44bf35..093c15e 100644 --- a/fastapi_admin/routes/password.py +++ b/fastapi_admin/routes/password.py @@ -1,5 +1,4 @@ from fastapi import APIRouter, Depends, Form -from pydantic import BaseModel from starlette.requests import Request from fastapi_admin.depends import get_current_admin, get_resources diff --git a/fastapi_admin/routes/resources.py b/fastapi_admin/routes/resources.py index 9c4326d..1939e21 100644 --- a/fastapi_admin/routes/resources.py +++ b/fastapi_admin/routes/resources.py @@ -1,10 +1,11 @@ -from typing import Optional - from fastapi import APIRouter, Depends, Path +from jinja2 import TemplateNotFound from starlette.requests import Request from starlette.responses import RedirectResponse from starlette.status import HTTP_303_SEE_OTHER from tortoise import Model +from tortoise.fields import ManyToManyRelation +from tortoise.transactions import in_transaction from fastapi_admin.depends import get_model, get_model_resource, get_resources from fastapi_admin.resources import Model as ModelResource @@ -38,27 +39,34 @@ async def list_view( qs = qs.offset((page_num - 1) * page_size) values = await qs.values_list(*fields_name) values = await render_values(fields, values) - return templates.TemplateResponse( - "list.html", - context={ - "request": request, - "resources": resources, - "fields_label": fields_label, - "fields": fields, - "values": values, - "filters": filters, - "resource": resource, - "model_resource": model_resource, - "resource_label": model_resource.label, - "page_size": page_size, - "page_num": page_num, - "total": total, - "from": page_size * (page_num - 1) + 1, - "to": page_size * page_num, - "page_title": model_resource.page_title, - "page_pre_title": model_resource.page_pre_title, - }, - ) + context = { + "request": request, + "resources": resources, + "fields_label": fields_label, + "fields": fields, + "values": values, + "filters": filters, + "resource": resource, + "model_resource": model_resource, + "resource_label": model_resource.label, + "page_size": page_size, + "page_num": page_num, + "total": total, + "from": page_size * (page_num - 1) + 1, + "to": page_size * page_num, + "page_title": model_resource.page_title, + "page_pre_title": model_resource.page_pre_title, + } + try: + return templates.TemplateResponse( + f"{resource}/list.html", + context=context, + ) + except TemplateNotFound: + return templates.TemplateResponse( + "list.html", + context=context, + ) @router.post("/{resource}/update/{pk}") @@ -71,25 +79,50 @@ async def update( model=Depends(get_model), ): form = await request.form() - data = await model_resource.resolve_data(dict(form)) - obj = await model.get(pk=pk) - await obj.update_from_dict(data).save() + data, m2m_data = await model_resource.resolve_data(form) + async with in_transaction() as conn: + obj = ( + await model.filter(pk=pk) + .using_db(conn) + .select_for_update() + .get() + .prefetch_related(*model_resource.get_m2m_field()) + ) + await obj.update_from_dict(data).save(using_db=conn) + for k, items in m2m_data.items(): + m2m_obj = getattr(obj, k) + await m2m_obj.clear() + if items: + await m2m_obj.add(*items) + obj = ( + await model.filter(pk=pk) + .using_db(conn) + .get() + .prefetch_related(*model_resource.get_m2m_field()) + ) inputs = await model_resource.get_inputs(obj) if "save" in form.keys(): - return templates.TemplateResponse( - "update.html", - context={ - "request": request, - "resources": resources, - "resource_label": model_resource.label, - "resource": resource, - "model_resource": model_resource, - "inputs": inputs, - "pk": pk, - "page_title": model_resource.page_title, - "page_pre_title": model_resource.page_pre_title, - }, - ) + context = { + "request": request, + "resources": resources, + "resource_label": model_resource.label, + "resource": resource, + "model_resource": model_resource, + "inputs": inputs, + "pk": pk, + "page_title": model_resource.page_title, + "page_pre_title": model_resource.page_pre_title, + } + try: + return templates.TemplateResponse( + f"{resource}/update.html", + context=context, + ) + except TemplateNotFound: + return templates.TemplateResponse( + "update.html", + context=context, + ) return redirect(request, "list_view", resource=resource) @@ -104,20 +137,27 @@ async def update_view( ): obj = await model.get(pk=pk) inputs = await model_resource.get_inputs(obj) - return templates.TemplateResponse( - "update.html", - context={ - "request": request, - "resources": resources, - "resource_label": model_resource.label, - "resource": resource, - "inputs": inputs, - "pk": pk, - "model_resource": model_resource, - "page_title": model_resource.page_title, - "page_pre_title": model_resource.page_pre_title, - }, - ) + context = { + "request": request, + "resources": resources, + "resource_label": model_resource.label, + "resource": resource, + "inputs": inputs, + "pk": pk, + "model_resource": model_resource, + "page_title": model_resource.page_title, + "page_pre_title": model_resource.page_pre_title, + } + try: + return templates.TemplateResponse( + f"{resource}/update.html", + context=context, + ) + except TemplateNotFound: + return templates.TemplateResponse( + "update.html", + context=context, + ) @router.get("/{resource}/create") @@ -128,19 +168,26 @@ async def create_view( model_resource: ModelResource = Depends(get_model_resource), ): inputs = await model_resource.get_inputs() - return templates.TemplateResponse( - "create.html", - context={ - "request": request, - "resources": resources, - "resource_label": model_resource.label, - "resource": resource, - "inputs": inputs, - "model_resource": model_resource, - "page_title": model_resource.page_title, - "page_pre_title": model_resource.page_pre_title, - }, - ) + context = { + "request": request, + "resources": resources, + "resource_label": model_resource.label, + "resource": resource, + "inputs": inputs, + "model_resource": model_resource, + "page_title": model_resource.page_title, + "page_pre_title": model_resource.page_pre_title, + } + try: + return templates.TemplateResponse( + f"{resource}/create.html", + context=context, + ) + except TemplateNotFound: + return templates.TemplateResponse( + "create.html", + context=context, + ) @router.post("/{resource}/create") @@ -153,23 +200,34 @@ async def create( ): inputs = await model_resource.get_inputs() form = await request.form() - data = await model_resource.resolve_data(dict(form)) - await model.create(**data) + data, m2m_data = await model_resource.resolve_data(form) + async with in_transaction() as conn: + obj = await model.create(**data, using_db=conn) + for k, items in m2m_data.items(): + m2m_obj = getattr(obj, k) # type:ManyToManyRelation + await m2m_obj.add(*items, using_db=conn) if "save" in form.keys(): return redirect(request, "list_view", resource=resource) - return templates.TemplateResponse( - "create.html", - context={ - "request": request, - "resources": resources, - "resource_label": model_resource.label, - "resource": resource, - "inputs": inputs, - "model_resource": model_resource, - "page_title": model_resource.page_title, - "page_pre_title": model_resource.page_pre_title, - }, - ) + context = { + "request": request, + "resources": resources, + "resource_label": model_resource.label, + "resource": resource, + "inputs": inputs, + "model_resource": model_resource, + "page_title": model_resource.page_title, + "page_pre_title": model_resource.page_pre_title, + } + try: + return templates.TemplateResponse( + f"{resource}/create.html", + context=context, + ) + except TemplateNotFound: + return templates.TemplateResponse( + "create.html", + context=context, + ) @router.delete("/{resource}/delete/{pk}") diff --git a/fastapi_admin/template.py b/fastapi_admin/template.py index 509fed3..7fac426 100644 --- a/fastapi_admin/template.py +++ b/fastapi_admin/template.py @@ -42,7 +42,7 @@ def set_global_env(name: str, value: Any): def add_template_folder(*folders: str): for folder in folders: - templates.env.loader.searchpath.append(folder) + templates.env.loader.searchpath.insert(0, folder) async def render_values( diff --git a/fastapi_admin/templates/base.html b/fastapi_admin/templates/base.html index 281479c..7243bfd 100644 --- a/fastapi_admin/templates/base.html +++ b/fastapi_admin/templates/base.html @@ -4,10 +4,10 @@ - + diff --git a/fastapi_admin/templates/layout.html b/fastapi_admin/templates/layout.html index 5f129c2..72b907c 100644 --- a/fastapi_admin/templates/layout.html +++ b/fastapi_admin/templates/layout.html @@ -116,7 +116,8 @@ >
  • - License + License
  • Copyright © {{ 2021 }} - {{ NOW_YEAR }} - FastAPI-Adminlong2ice. All rights reserved.
  • diff --git a/fastapi_admin/templates/list.html b/fastapi_admin/templates/list.html index 8ec849c..2808567 100644 --- a/fastapi_admin/templates/list.html +++ b/fastapi_admin/templates/list.html @@ -16,7 +16,7 @@ type="text" class="form-control" value="{{ page_size }}" - size="1" + size="2" name="page_size" aria-label="entries count" /> @@ -89,29 +89,35 @@ - + {% if model_resource.bulk_actions %} + + {% endif %} {% for label in fields_label %} {% endfor %} - + {% if model_resource.actions %} + + {% endif %} {% for value in values %} - + {% if model_resource.bulk_actions %} + + {% endif %} {% for v in value %} {% endfor %} @@ -169,8 +175,8 @@ {% with total_page = (total/page_size)|round(0,'ceil')|int,start_page = - (1 if page_num <=3 else page_num - 2 ) %} {% for i in - range(start_page,[start_page + 5,total_page + 1]|min) %} + (1 if page_num <=3 else page_num - 2 ) %} {% for i in + range(start_page,[start_page + 5,total_page + 1]|min) %}
  • +
    {{ label }}
    + + + +{% endwith %} diff --git a/fastapi_admin/utils.py b/fastapi_admin/utils.py deleted file mode 100644 index 1a5cbbb..0000000 --- a/fastapi_admin/utils.py +++ /dev/null @@ -1,9 +0,0 @@ -class ClassProperty(property): - def __get__(self, obj, obj_type=None): - return super(ClassProperty, self).__get__(obj_type) - - def __set__(self, obj, value): - super(ClassProperty, self).__set__(type(obj), value) - - def __delete__(self, obj): - super(ClassProperty, self).__delete__(type(obj)) diff --git a/fastapi_admin/widgets/displays.py b/fastapi_admin/widgets/displays.py index 3a8b902..5c760a0 100644 --- a/fastapi_admin/widgets/displays.py +++ b/fastapi_admin/widgets/displays.py @@ -50,5 +50,5 @@ class Json(Display): async def render(self, value: dict): return self.templates.get_template(self.template).render( - value=json.dumps(value, indent=4, sort_keys=True), + value=json.dumps(value), ) diff --git a/fastapi_admin/widgets/inputs.py b/fastapi_admin/widgets/inputs.py index 78304a7..548b7a7 100644 --- a/fastapi_admin/widgets/inputs.py +++ b/fastapi_admin/widgets/inputs.py @@ -96,6 +96,35 @@ class ForeignKey(Select): return await self.model.all() +class ManyToMany(Select): + template = "widgets/inputs/many_to_many.html" + + def __init__( + self, + model: Type[Model], + disabled: bool = False, + ): + super().__init__(disabled=disabled) + self.model = model + + async def get_options(self): + ret = await self.get_queryset() + options = [dict(label=str(x), value=x.pk) for x in ret] + return options + + async def get_queryset(self): + return await self.model.all() + + async def render(self, value: Any): + options = await self.get_options() + selected = list(map(lambda x: x.pk, value.related_objects if value else [])) + for option in options: + if option.get("value") in selected: + option["selected"] = True + self.context.update(options=json.dumps(options)) + return await super(Input, self).render(value) + + class Enum(Select): def __init__( self, diff --git a/poetry.lock b/poetry.lock index 1fd1c1e..14baea3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -428,7 +428,7 @@ tornado = ">=5.0" [[package]] name = "mkdocs-git-revision-date-localized-plugin" -version = "0.9" +version = "0.9.2" description = "Mkdocs plugin that enables displaying the localized date of the last git modification of a markdown file." category = "dev" optional = false @@ -593,7 +593,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pygments" -version = "2.8.1" +version = "2.9.0" description = "Pygments is a syntax highlighting package written in Python." category = "dev" optional = false @@ -722,7 +722,7 @@ testing = ["filelock"] [[package]] name = "python-dotenv" -version = "0.17.0" +version = "0.17.1" description = "Read key-value pairs from a .env file and set them as environment variables" category = "main" optional = false @@ -853,7 +853,7 @@ python-versions = "*" [[package]] name = "typing-extensions" -version = "3.7.4.3" +version = "3.10.0.0" description = "Backported and Experimental Type Hints for Python 3.5+" category = "main" optional = false @@ -1249,8 +1249,8 @@ mkdocs = [ {file = "mkdocs-1.1.2.tar.gz", hash = "sha256:f0b61e5402b99d7789efa032c7a74c90a20220a9c81749da06dbfbcbd52ffb39"}, ] mkdocs-git-revision-date-localized-plugin = [ - {file = "mkdocs-git-revision-date-localized-plugin-0.9.tar.gz", hash = "sha256:49e59396f1e83264b8f54fcb339a9137925a1af19e639f03dc59dae7f22e914f"}, - {file = "mkdocs_git_revision_date_localized_plugin-0.9-py3-none-any.whl", hash = "sha256:5d319398e9ce325d02df1cba232a92b215f8d0c0ffd3810b6a61d6c5eb6306e6"}, + {file = "mkdocs-git-revision-date-localized-plugin-0.9.2.tar.gz", hash = "sha256:c15c76d5baa1f8f37e3a4146b9a6f1b00c5a361ea959ada0703fd9ec462afbe3"}, + {file = "mkdocs_git_revision_date_localized_plugin-0.9.2-py3-none-any.whl", hash = "sha256:d4b21eeb9f212efd314a05e771cf69f2bab1f51bdaa5c9955d09410a396a069b"}, ] mkdocs-material = [ {file = "mkdocs-material-7.1.3.tar.gz", hash = "sha256:e34bba93ad1a0e6f9afc371f4ef55bedabbf13b9a786b013b0ce26ac55ec2932"}, @@ -1345,8 +1345,8 @@ pyflakes = [ {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, ] pygments = [ - {file = "Pygments-2.8.1-py3-none-any.whl", hash = "sha256:534ef71d539ae97d4c3a4cf7d6f110f214b0e687e92f9cb9d2a3b0d3101289c8"}, - {file = "Pygments-2.8.1.tar.gz", hash = "sha256:2656e1a6edcdabf4275f9a3640db59fd5de107d88e8663c5d4e9a0fa62f77f94"}, + {file = "Pygments-2.9.0-py3-none-any.whl", hash = "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"}, + {file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"}, ] pylint = [ {file = "pylint-2.8.2-py3-none-any.whl", hash = "sha256:f7e2072654a6b6afdf5e2fb38147d3e2d2d43c89f648637baab63e026481279b"}, @@ -1385,8 +1385,8 @@ pytest-xdist = [ {file = "pytest_xdist-2.2.1-py3-none-any.whl", hash = "sha256:2447a1592ab41745955fb870ac7023026f20a5f0bfccf1b52a879bd193d46450"}, ] python-dotenv = [ - {file = "python-dotenv-0.17.0.tar.gz", hash = "sha256:471b782da0af10da1a80341e8438fca5fadeba2881c54360d5fd8d03d03a4f4a"}, - {file = "python_dotenv-0.17.0-py2.py3-none-any.whl", hash = "sha256:49782a97c9d641e8a09ae1d9af0856cc587c8d2474919342d5104d85be9890b2"}, + {file = "python-dotenv-0.17.1.tar.gz", hash = "sha256:b1ae5e9643d5ed987fc57cc2583021e38db531946518130777734f9589b3141f"}, + {file = "python_dotenv-0.17.1-py2.py3-none-any.whl", hash = "sha256:00aa34e92d992e9f8383730816359647f358f4a3be1ba45e5a5cefd27ee91544"}, ] python-multipart = [ {file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"}, @@ -1569,9 +1569,9 @@ typed-ast = [ {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, ] typing-extensions = [ - {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, - {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, - {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, + {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, + {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, + {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, ] uvicorn = [ {file = "uvicorn-0.13.4-py3-none-any.whl", hash = "sha256:7587f7b08bd1efd2b9bad809a3d333e972f1d11af8a5e52a9371ee3a5de71524"},
  • - - + + {{ label }}
    - - + + {{ v|safe }}