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

+## 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" %}
+
+{% 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 %}
{{ label }} |
{% endfor %}
- |
+ {% if model_resource.actions %}
+ |
+ {% endif %}
{% for value in values %}
-
-
- |
+ {% if model_resource.bulk_actions %}
+
+
+ |
+ {% endif %}
{% for v in value %}
{{ v|safe }} |
{% 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"},