add Makefile

update cli
This commit is contained in:
long2ice
2020-05-22 21:28:32 +08:00
parent ae4eb02145
commit afabc6cd27
26 changed files with 782 additions and 384 deletions

View File

@ -7,6 +7,7 @@ ChangeLog
0.2.6
-----
- Fix createsuperuser error.
- Update cli.
0.2.5
-----

42
Makefile Normal file
View File

@ -0,0 +1,42 @@
checkfiles = fastapi_admin/ examples/ tests/
black_opts = -l 100 -t py38
py_warn = PYTHONDEVMODE=1
help:
@echo "FastAPI-Admin development makefile"
@echo
@echo "usage: make <target>"
@echo "Targets:"
@echo " deps Ensure dev/test dependencies are installed"
@echo " check Checks that build is sane"
@echo " lint Reports all linter violations"
@echo " test Runs all tests"
@echo " style Auto-formats the code"
deps:
@pip install -r requirements-dev.txt
style: deps
isort -rc $(checkfiles)
black $(black_opts) $(checkfiles)
check: deps
ifneq ($(shell which black),)
black --check $(black_opts) $(checkfiles) || (echo "Please run 'make style' to auto-fix style issues" && false)
endif
flake8 $(checkfiles)
mypy $(checkfiles)
pylint -d C,W,R $(checkfiles)
bandit -r $(checkfiles)
python setup.py check -mrs
test: deps
$(py_warn) py.test
publish: deps
rm -fR dist/
python setup.py sdist
twine upload dist/*
ci:
@act -P ubuntu-latest=nektos/act-environments-ubuntu:18.04 -b

View File

@ -1,3 +0,0 @@

View File

@ -9,10 +9,7 @@ class ProductType(EnumMixin, IntEnum):
@classmethod
def choices(cls):
return {
cls.article: 'Article',
cls.page: 'Page'
}
return {cls.article: "Article", cls.page: "Page"}
class Status(EnumMixin, IntEnum):
@ -21,7 +18,4 @@ class Status(EnumMixin, IntEnum):
@classmethod
def choices(cls):
return {
cls.on: 'On',
cls.off: 'Off'
}
return {cls.on: "On", cls.off: "Off"}

View File

@ -1,7 +1,7 @@
import os
import uvicorn
from fastapi import FastAPI, Depends
from fastapi import Depends, FastAPI
from starlette.middleware.cors import CORSMiddleware
from starlette.templating import Jinja2Templates
from tortoise.contrib.fastapi import register_tortoise
@ -13,53 +13,42 @@ from fastapi_admin.schemas import BulkIn
from fastapi_admin.site import Site
TORTOISE_ORM = {
'connections': {
'default': os.getenv('DATABASE_URL')
},
'apps': {
'models': {
'models': ['examples.models', 'fastapi_admin.models'],
'default_connection': 'default',
"connections": {"default": os.getenv("DATABASE_URL")},
"apps": {
"models": {
"models": ["examples.models", "fastapi_admin.models"],
"default_connection": "default",
}
}
},
}
templates = Jinja2Templates(directory='examples/templates')
templates = Jinja2Templates(directory="examples/templates")
@admin_app.post(
'/rest/{resource}/bulk/test_bulk'
)
async def test_bulk(
bulk_in: BulkIn,
model=Depends(get_model)
):
@admin_app.post("/rest/{resource}/bulk/test_bulk")
async def test_bulk(bulk_in: BulkIn, model=Depends(get_model)):
qs = model.filter(pk__in=bulk_in.pk_list)
pydantic = pydantic_queryset_creator(model)
ret = await pydantic.from_queryset(qs)
return ret.dict()
@admin_app.get(
'/home',
)
@admin_app.get("/home",)
async def home():
return {
'html': templates.get_template('home.html').render()
}
return {"html": templates.get_template("home.html").render()}
def create_app():
fast_app = FastAPI(debug=False)
register_tortoise(fast_app, config=TORTOISE_ORM, generate_schemas=True)
fast_app.mount('/admin', admin_app)
fast_app.mount("/admin", admin_app)
fast_app.add_middleware(
CORSMiddleware,
allow_origins=['*'],
allow_origins=["*"],
allow_credentials=True,
allow_methods=['*'],
allow_headers=['*'],
allow_methods=["*"],
allow_headers=["*"],
)
return fast_app
@ -68,25 +57,25 @@ def create_app():
app = create_app()
@app.on_event('startup')
@app.on_event("startup")
async def start_up():
admin_app.debug = False
admin_app.init(
user_model='User',
tortoise_app='models',
admin_secret='test',
user_model="User",
tortoise_app="models",
admin_secret="test",
permission=True,
site=Site(
name='FastAPI-Admin DEMO',
logo='https://github.com/long2ice/fastapi-admin/raw/master/front/static/img/logo.png',
login_footer='FASTAPI ADMIN - FastAPI Admin Dashboard',
login_description='FastAPI Admin Dashboard',
locale='en-US',
name="FastAPI-Admin DEMO",
logo="https://github.com/long2ice/fastapi-admin/raw/master/front/static/img/logo.png",
login_footer="FASTAPI ADMIN - FastAPI Admin Dashboard",
login_description="FastAPI Admin Dashboard",
locale="en-US",
locale_switcher=True,
theme_switcher=True,
)
),
)
if __name__ == '__main__':
uvicorn.run('main:app', port=8000, debug=False, reload=False, lifespan='on')
if __name__ == "__main__":
uvicorn.run("main:app", port=8000, debug=False, reload=False, lifespan="on")

View File

@ -1,21 +1,22 @@
import datetime
from tortoise import fields, Model
from tortoise import Model, fields
from fastapi_admin.models import User as AdminUser
from .enums import ProductType, Status
class User(AdminUser):
last_login = fields.DatetimeField(description='Last Login', default=datetime.datetime.now)
is_active = fields.BooleanField(default=True, description='Is Active')
is_superuser = fields.BooleanField(default=False, description='Is SuperUser')
avatar = fields.CharField(max_length=200, default='')
intro = fields.TextField(default='')
last_login = fields.DatetimeField(description="Last Login", default=datetime.datetime.now)
is_active = fields.BooleanField(default=True, description="Is Active")
is_superuser = fields.BooleanField(default=False, description="Is SuperUser")
avatar = fields.CharField(max_length=200, default="")
intro = fields.TextField(default="")
created_at = fields.DatetimeField(auto_now_add=True)
def __str__(self):
return f'{self.pk}#{self.username}'
return f"{self.pk}#{self.username}"
class Category(Model):
@ -24,22 +25,22 @@ class Category(Model):
created_at = fields.DatetimeField(auto_now_add=True)
def __str__(self):
return f'{self.pk}#{self.name}'
return f"{self.pk}#{self.name}"
class Product(Model):
categories = fields.ManyToManyField('models.Category')
categories = fields.ManyToManyField("models.Category")
name = fields.CharField(max_length=50)
view_num = fields.IntField(description='View Num')
view_num = fields.IntField(description="View Num")
sort = fields.IntField()
is_reviewed = fields.BooleanField(description='Is Reviewed')
type = fields.IntEnumField(ProductType, description='Product Type')
is_reviewed = fields.BooleanField(description="Is Reviewed")
type = fields.IntEnumField(ProductType, description="Product Type")
image = fields.CharField(max_length=200)
body = fields.TextField()
created_at = fields.DatetimeField(auto_now_add=True)
def __str__(self):
return f'{self.pk}#{self.name}'
return f"{self.pk}#{self.name}"
class Config(Model):
@ -49,4 +50,4 @@ class Config(Model):
status: Status = fields.IntEnumField(Status, default=Status.on)
def __str__(self):
return f'{self.pk}#{self.label}'
return f"{self.pk}#{self.label}"

View File

@ -1,3 +1,3 @@
from . import routes
__version__ = '0.2.5'
__version__ = "0.2.5"

View File

@ -44,62 +44,60 @@ async def register_permissions(args):
if args.clean:
await Permission.all().delete()
Logger.waring('Cleaned all permissions success.')
models = Tortoise.apps.get('models').keys()
Logger.waring("Cleaned all permissions success.")
models = Tortoise.apps.get("models").keys()
models = list(models)
for model in models:
for action in enums.PermissionAction:
label = f'{enums.PermissionAction.choices().get(action)} {model}'
defaults = dict(
label=label,
model=model,
action=action,
)
_, created = await Permission.get_or_create(
**defaults,
)
label = f"{enums.PermissionAction.choices().get(action)} {model}"
defaults = dict(label=label, model=model, action=action,)
_, created = await Permission.get_or_create(**defaults,)
if created:
Logger.success(f'Create permission {label} success.')
Logger.success(f"Create permission {label} success.")
async def createsuperuser(args):
await init_tortoise(args)
user_model = Tortoise.apps.get('models').get(args.user_model)
user_model = Tortoise.apps.get("models").get(args.user_model)
prompt = PromptSession()
while True:
try:
username = await prompt.prompt_async('Username: ')
password = await prompt.prompt_async('Password: ', is_password=True)
username = await prompt.prompt_async("Username: ")
password = await prompt.prompt_async("Password: ", is_password=True)
try:
await user_model.create(
username=username,
password=pwd_context.hash(password),
is_superuser=True
username=username, password=pwd_context.hash(password), is_superuser=True
)
Logger.success(f'Create superuser {username} success.')
Logger.success(f"Create superuser {username} success.")
return
except Exception as e:
Logger.error(f'Create superuser {username} error,{e}')
Logger.error(f"Create superuser {username} error,{e}")
except (EOFError, KeyboardInterrupt):
Logger.success(f'Exit success!')
Logger.success(f"Exit success!")
return
def cli():
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(title='subcommands')
parser.add_argument('-c', '--config', required=True,
help='Tortoise-orm config dict import path,like settings.TORTOISE_ORM.')
subparsers = parser.add_subparsers(title="subcommands")
parser.add_argument(
"-c",
"--config",
required=True,
help="Tortoise-orm config dict import path,like settings.TORTOISE_ORM.",
)
parser_register_permissions = subparsers.add_parser('register_permissions')
parser_register_permissions.add_argument('--clean', required=False, action='store_true',
help='Clean up old permissions then renew.')
parser_register_permissions = subparsers.add_parser("register_permissions")
parser_register_permissions.add_argument(
"--clean", required=False, action="store_true", help="Clean up old permissions then renew."
)
parser_register_permissions.set_defaults(func=register_permissions)
parser_createsuperuser = subparsers.add_parser('createsuperuser')
parser_createsuperuser.add_argument('--user-model', required=True,
help='User model import path,like examples.models.User.')
parser_createsuperuser = subparsers.add_parser("createsuperuser")
parser_createsuperuser.add_argument(
"--user", required=True, help="User model name, like User or Admin."
)
parser_createsuperuser.set_defaults(func=createsuperuser)
parse_args = parser.parse_args()

View File

@ -2,7 +2,7 @@ from copy import deepcopy
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto')
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
async def handle_m2m_fields_create_or_update(body, m2m_fields, model, create=True, pk=None):

View File

@ -1,7 +1,7 @@
import json
import jwt
from fastapi import Query, Path, Depends, HTTPException
from fastapi import Depends, HTTPException, Path, Query
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from fastapi.security.utils import get_authorization_scheme_param
from pydantic import BaseModel
@ -14,16 +14,18 @@ from .factory import app
auth_schema = HTTPBearer()
async def jwt_required(request: Request, token: HTTPAuthorizationCredentials = Depends(auth_schema)):
async def jwt_required(
request: Request, token: HTTPAuthorizationCredentials = Depends(auth_schema)
):
credentials_exception = HTTPException(HTTP_401_UNAUTHORIZED)
try:
payload = jwt.decode(token.credentials, app.admin_secret)
user_id = payload.get('user_id')
user_id = payload.get("user_id")
if user_id is None:
raise credentials_exception
except jwt.PyJWTError:
raise credentials_exception
request.scope['user_id'] = user_id
request.scope["user_id"] = user_id
return user_id
@ -33,8 +35,8 @@ async def jwt_optional(request: Request):
if credentials:
try:
payload = jwt.decode(credentials, app.admin_secret)
user_id = payload.get('user_id')
request.scope['user_id'] = user_id
user_id = payload.get("user_id")
request.scope["user_id"] = user_id
return user_id
except jwt.PyJWTError:
pass
@ -50,9 +52,7 @@ class QueryItem(BaseModel):
sort: dict = {}
class Config:
fields = {
'with_': 'with'
}
fields = {"with_": "with"}
def get_query(query=Query(...)):
@ -94,7 +94,7 @@ class PermissionsChecker:
if not user.is_active:
raise HTTPException(status_code=HTTP_403_FORBIDDEN)
has_permission = False
await user.fetch_related('roles')
await user.fetch_related("roles")
for role in user.roles:
if await role.permissions.filter(model=resource, action=self.action):
has_permission = True

View File

@ -18,8 +18,8 @@ class PermissionAction(EnumMixin, IntEnum):
@classmethod
def choices(cls):
return {
cls.create: 'Create',
cls.delete: 'Delete',
cls.update: 'Update',
cls.read: 'Read',
cls.create: "Create",
cls.delete: "Delete",
cls.update: "Update",
cls.read: "Read",
}

View File

@ -1,10 +1,7 @@
from starlette.requests import Request
from fastapi.exceptions import HTTPException
from starlette.requests import Request
from starlette.responses import UJSONResponse
async def exception_handler(request: Request, exc: HTTPException):
return UJSONResponse(
status_code=exc.status_code,
content={'msg': exc.detail},
)
return UJSONResponse(status_code=exc.status_code, content={"msg": exc.detail},)

View File

@ -1,11 +1,11 @@
from copy import deepcopy
from typing import Type, List, Dict, Any, Optional
from typing import Any, Dict, List, Optional, Type
from fastapi import FastAPI, HTTPException
from tortoise import Model, Tortoise
from .exceptions import exception_handler
from .site import Site, Resource, Field, Menu
from .site import Field, Menu, Resource, Site
class AdminApp(FastAPI):
@ -16,57 +16,54 @@ class AdminApp(FastAPI):
permission: bool
_inited: bool = False
_field_type_mapping = {
'IntField': 'number',
'BooleanField': 'checkbox',
'DatetimeField': 'datetime',
'DateField': 'date',
'IntEnumFieldInstance': 'select',
'CharEnumFieldInstance': 'select',
'DecimalField': 'number',
'FloatField': 'number',
'TextField': 'textarea',
'SmallIntField': 'number',
'JSONField': 'json',
"IntField": "number",
"BooleanField": "checkbox",
"DatetimeField": "datetime",
"DateField": "date",
"IntEnumFieldInstance": "select",
"CharEnumFieldInstance": "select",
"DecimalField": "number",
"FloatField": "number",
"TextField": "textarea",
"SmallIntField": "number",
"JSONField": "json",
}
model_menu_mapping: Dict[str, Menu] = {}
def _get_model_menu_mapping(self, menus: List[Menu]):
for menu in filter(lambda x: (x.url and 'rest' in x.url) or x.children, menus):
for menu in filter(lambda x: (x.url and "rest" in x.url) or x.children, menus):
if menu.children:
self._get_model_menu_mapping(menu.children)
else:
self.model_menu_mapping[menu.url.split('?')[0].split('/')[-1]] = menu
self.model_menu_mapping[menu.url.split("?")[0].split("/")[-1]] = menu
def _get_model_fields_type(self, model: Type[Model]) -> Dict:
model_describe = model.describe()
ret = {}
data_fields = model_describe.get('data_fields')
pk_field = model_describe.get('pk_field')
fk_fields = model_describe.get('fk_fields')
m2m_fields = model_describe.get('m2m_fields')
data_fields = model_describe.get("data_fields")
pk_field = model_describe.get("pk_field")
fk_fields = model_describe.get("fk_fields")
m2m_fields = model_describe.get("m2m_fields")
fields = [pk_field] + data_fields + fk_fields + m2m_fields
for field in fields:
ret[field.get('name')] = self._get_field_type(field.get('name'), field.get('field_type'))
ret[field.get("name")] = self._get_field_type(
field.get("name"), field.get("field_type")
)
return ret
def _build_content_menus(self) -> List[Menu]:
models = deepcopy(self.models) # type:Dict[str,Type[Model]]
models.pop('Role', None)
models.pop('User', None)
models.pop('Permission', None)
models.pop("Role", None)
models.pop("User", None)
models.pop("Permission", None)
menus = []
for k, v in models.items():
menu = Menu(
name=v._meta.table_description or k,
url=f'/rest/{k}',
url=f"/rest/{k}",
fields_type=self._get_model_fields_type(v),
icon='icon-list',
bulk_actions=[
{
'value': 'delete',
'text': 'delete_all',
},
]
icon="icon-list",
bulk_actions=[{"value": "delete", "text": "delete_all",},],
)
menus.append(menu)
return menus
@ -78,66 +75,47 @@ class AdminApp(FastAPI):
"""
menus = [
Menu(
name='Home',
url='/',
icon='fa fa-home'
),
Menu(
name='Content',
title=True
),
Menu(name="Home", url="/", icon="fa fa-home"),
Menu(name="Content", title=True),
*self._build_content_menus(),
Menu(name="External", title=True),
Menu(
name='External',
title=True
),
Menu(
name='Github',
url='https://github.com/long2ice/fastapi-admin',
icon='fa fa-github',
external=True
name="Github",
url="https://github.com/long2ice/fastapi-admin",
icon="fa fa-github",
external=True,
),
]
if permission:
permission_menus = [
Menu(name="Auth", title=True),
Menu(
name='Auth',
title=True
name="User",
url="/rest/User",
icon="fa fa-user",
exclude=("password",),
search_fields=("username",),
),
Menu(name="Role", url="/rest/Role", icon="fa fa-group", actions={"delete": False}),
Menu(
name='User',
url='/rest/User',
icon='fa fa-user',
exclude=('password',),
search_fields=('username',),
name="Permission",
url="/rest/Permission",
icon="fa fa-user-plus",
actions={"delete": False},
),
Menu(
name='Role',
url='/rest/Role',
icon='fa fa-group',
actions={
'delete': False
}
),
Menu(
name='Permission',
url='/rest/Permission',
icon='fa fa-user-plus',
actions={
'delete': False
}
),
Menu(
name='Logout',
url='/logout',
icon='fa fa-lock',
)
Menu(name="Logout", url="/logout", icon="fa fa-lock",),
]
menus += permission_menus
return menus
def init(self, site: Site, user_model: str, tortoise_app: str, admin_secret: str, permission: bool = False):
def init(
self,
site: Site,
user_model: str,
tortoise_app: str,
admin_secret: str,
permission: bool = False,
):
"""
init admin site
:param tortoise_app:
@ -180,13 +158,20 @@ class AdminApp(FastAPI):
:param field_type:
:return:
"""
field_type = self._field_type_mapping.get(field_type) or 'text'
field_type = self._field_type_mapping.get(field_type) or "text"
if menu:
field_type = menu.fields_type.get(name) or field_type
return field_type
async def _build_resource_from_model_describe(self, resource: str, model: Type[Model], model_describe: dict,
exclude_pk: bool, exclude_m2m_field=True, exclude_actions=False):
async def _build_resource_from_model_describe(
self,
resource: str,
model: Type[Model],
model_describe: dict,
exclude_pk: bool,
exclude_m2m_field=True,
exclude_actions=False,
):
"""
build resource
:param resource:
@ -196,113 +181,112 @@ class AdminApp(FastAPI):
:param exclude_m2m_field:
:return:
"""
data_fields = model_describe.get('data_fields')
pk_field = model_describe.get('pk_field')
fk_fields = model_describe.get('fk_fields')
m2m_fields = model_describe.get('m2m_fields')
data_fields = model_describe.get("data_fields")
pk_field = model_describe.get("pk_field")
fk_fields = model_describe.get("fk_fields")
m2m_fields = model_describe.get("m2m_fields")
menu = self.model_menu_mapping[resource]
search_fields_ret = {}
search_fields = menu.search_fields
sort_fields = menu.sort_fields
fields = {}
pk = name = pk_field.get('name')
pk = name = pk_field.get("name")
if not exclude_pk and not self._exclude_field(resource, name):
fields = {
name: Field(
label=pk_field.get('name').title(),
label=pk_field.get("name").title(),
required=True,
type=self._get_field_type(name, pk_field.get('field_type').__name__, menu),
type=self._get_field_type(name, pk_field.get("field_type").__name__, menu),
sortable=name in sort_fields,
**menu.attrs.get(name) or {}
**menu.attrs.get(name) or {},
)
}
if not exclude_actions and menu.actions:
fields['_actions'] = menu.actions
fields["_actions"] = menu.actions
for data_field in data_fields:
readonly = data_field.get('constraints').get('readOnly')
field_type = data_field.get('field_type').__name__
name = data_field.get('name')
readonly = data_field.get("constraints").get("readOnly")
field_type = data_field.get("field_type").__name__
name = data_field.get("name")
if self._exclude_field(resource, name):
continue
type_ = self._get_field_type(name, field_type, menu)
options = []
if type_ == 'select' or type_ == 'radiolist':
if type_ == "select" or type_ == "radiolist":
for k, v in model._meta.fields_map[name].enum_type.choices().items():
options.append({'text': v, 'value': k})
options.append({"text": v, "value": k})
label = data_field.get('description') or data_field.get('name').title()
label = data_field.get("description") or data_field.get("name").title()
field = Field(
label=label,
required=not data_field.get('nullable'),
required=not data_field.get("nullable"),
type=type_,
options=options,
sortable=name in sort_fields,
disabled=readonly,
**menu.attrs.get(name) or {}
**menu.attrs.get(name) or {},
)
fields[name] = field
if name in search_fields:
search_fields_ret[name] = field
for fk_field in fk_fields:
name = fk_field.get('name')
name = fk_field.get("name")
if not self._exclude_field(resource, name):
if name not in menu.raw_id_fields:
fk_model_class = fk_field.get('python_type')
fk_model_class = fk_field.get("python_type")
objs = await fk_model_class.all()
raw_field = fk_field.get('raw_field')
label = fk_field.get('description') or name.title()
options = list(map(lambda x: {'text': str(x), 'value': x.pk}, objs))
raw_field = fk_field.get("raw_field")
label = fk_field.get("description") or name.title()
options = list(map(lambda x: {"text": str(x), "value": x.pk}, objs))
field = Field(
label=label,
required=True,
type='select',
type="select",
options=options,
sortable=name in sort_fields,
**menu.attrs.get(name) or {}
**menu.attrs.get(name) or {},
)
fields[raw_field] = field
if name in search_fields:
search_fields_ret[raw_field] = field
if not exclude_m2m_field:
for m2m_field in m2m_fields:
name = m2m_field.get('name')
name = m2m_field.get("name")
if not self._exclude_field(resource, name):
label = m2m_field.get('description') or name.title()
m2m_model_class = m2m_field.get('python_type')
label = m2m_field.get("description") or name.title()
m2m_model_class = m2m_field.get("python_type")
objs = await m2m_model_class.all()
options = list(map(lambda x: {'text': str(x), 'value': x.pk}, objs))
options = list(map(lambda x: {"text": str(x), "value": x.pk}, objs))
fields[name] = Field(
label=label,
type='tree',
type="tree",
options=options,
multiple=True,
**menu.attrs.get(name) or {}
**menu.attrs.get(name) or {},
)
return pk, fields, search_fields_ret
async def get_resource(self, resource: str, exclude_pk=False, exclude_m2m_field=True,
exclude_actions=False) -> Resource:
assert self._inited, 'must call init() first!'
async def get_resource(
self, resource: str, exclude_pk=False, exclude_m2m_field=True, exclude_actions=False
) -> Resource:
assert self._inited, "must call init() first!"
model = self.models.get(resource) # type:Type[Model]
model_describe = model.describe(serializable=False)
pk, fields, search_fields = await self._build_resource_from_model_describe(resource, model, model_describe,
exclude_pk, exclude_m2m_field,
exclude_actions)
pk, fields, search_fields = await self._build_resource_from_model_describe(
resource, model, model_describe, exclude_pk, exclude_m2m_field, exclude_actions
)
menu = self.model_menu_mapping[resource]
return Resource(
title=model_describe.get('description') or resource.title(),
title=model_describe.get("description") or resource.title(),
fields=fields,
searchFields=search_fields,
pk=pk,
bulk_actions=menu.bulk_actions,
export=menu.export
export=menu.export,
)
app = AdminApp(
openapi_prefix='/admin',
)
app = AdminApp(openapi_prefix="/admin",)
app.add_exception_handler(HTTPException, exception_handler)

View File

@ -14,8 +14,9 @@ class User(Model):
class Permission(Model):
label = fields.CharField(max_length=50)
model = fields.CharField(max_length=50)
action: enums.PermissionAction = fields.IntEnumField(enums.PermissionAction, default=enums.PermissionAction.read,
description='Permission Action')
action: enums.PermissionAction = fields.IntEnumField(
enums.PermissionAction, default=enums.PermissionAction.read, description="Permission Action"
)
def __str__(self):
return self.label
@ -23,9 +24,9 @@ class Permission(Model):
class Role(Model):
label = fields.CharField(max_length=50)
users = fields.ManyToManyField('models.User')
users = fields.ManyToManyField("models.User")
permissions: fields.ManyToManyRelation[Permission] = fields.ManyToManyField('models.Permission')
permissions: fields.ManyToManyRelation[Permission] = fields.ManyToManyField("models.Permission")
def __str__(self):
return self.label

View File

@ -1,4 +1,5 @@
from typing import Sequence, Dict
from typing import Dict, Sequence
from pydantic import BaseModel

View File

@ -1,9 +1,9 @@
from fastapi import Depends
from . import login, site, rest
from ..depends import jwt_required
from ..factory import app
from . import login, rest, site
app.include_router(login.router)
app.include_router(site.router)
app.include_router(rest.router, dependencies=[Depends(jwt_required)], prefix='/rest')
app.include_router(rest.router, dependencies=[Depends(jwt_required)], prefix="/rest")

View File

@ -10,25 +10,20 @@ from ..shortcuts import get_object_or_404
router = APIRouter()
@router.post(
'/login',
)
async def login(
login_in: LoginIn
):
@router.post("/login",)
async def login(login_in: LoginIn):
user_model = app.user_model
user = await get_object_or_404(user_model, username=login_in.username)
if not user.is_active:
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail='User is not Active!')
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="User is not Active!")
if not pwd_context.verify(login_in.password, user.password):
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail='Incorrect Password!')
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Incorrect Password!")
ret = {
'user': {
'username': user.username,
'is_superuser': user.is_superuser,
'avatar': user.avatar if hasattr(user, 'avatar') else None
"user": {
"username": user.username,
"is_superuser": user.is_superuser,
"avatar": user.avatar if hasattr(user, "avatar") else None,
},
'token': jwt.encode({'user_id': user.pk}, app.admin_secret, algorithm='HS256')
"token": jwt.encode({"user_id": user.pk}, app.admin_secret, algorithm="HS256"),
}
return ret

View File

@ -1,7 +1,7 @@
import io
import xlsxwriter
from fastapi import Depends, APIRouter
from fastapi import APIRouter, Depends
from fastapi.responses import UJSONResponse
from starlette.responses import StreamingResponse
from starlette.status import HTTP_409_CONFLICT
@ -11,8 +11,16 @@ from tortoise.exceptions import IntegrityError
from tortoise.fields import ManyToManyRelation
from ..common import handle_m2m_fields_create_or_update
from ..depends import QueryItem, get_query, parse_body, get_model, read_checker, delete_checker, update_checker, \
create_checker
from ..depends import (
QueryItem,
create_checker,
delete_checker,
get_model,
get_query,
parse_body,
read_checker,
update_checker,
)
from ..factory import app
from ..responses import GetManyOut
from ..schemas import BulkIn
@ -21,20 +29,16 @@ from ..shortcuts import get_object_or_404
router = APIRouter()
@router.get(
'/{resource}/export'
)
async def export(
resource: str,
query: QueryItem = Depends(get_query),
model=Depends(get_model)
):
@router.get("/{resource}/export")
async def export(resource: str, query: QueryItem = Depends(get_query), model=Depends(get_model)):
qs = model.all()
if query.where:
qs = qs.filter(**query.where)
resource = await app.get_resource(resource)
result = await qs
creator = pydantic_model_creator(model, include=resource.resource_fields.keys(), exclude=model._meta.m2m_fields)
creator = pydantic_model_creator(
model, include=resource.resource_fields.keys(), exclude=model._meta.m2m_fields
)
data = map(lambda x: creator.from_orm(x).dict(), result)
output = io.BytesIO()
@ -54,14 +58,9 @@ async def export(
return StreamingResponse(output)
@router.get(
'/{resource}',
dependencies=[Depends(read_checker)]
)
@router.get("/{resource}", dependencies=[Depends(read_checker)])
async def get_resource(
resource: str,
query: QueryItem = Depends(get_query),
model=Depends(get_model)
resource: str, query: QueryItem = Depends(get_query), model=Depends(get_model)
):
menu = app.model_menu_mapping[resource]
qs = model.all()
@ -71,125 +70,81 @@ async def get_resource(
for k, v in sort.items():
if k in menu.sort_fields:
if v == -1:
qs = qs.order_by(f'-{k}')
qs = qs.order_by(f"-{k}")
elif v == 1:
qs = qs.order_by(k)
resource = await app.get_resource(resource)
result = await qs.limit(query.size).offset((query.page - 1) * query.size)
creator = pydantic_model_creator(model, include=resource.resource_fields.keys(), exclude=model._meta.m2m_fields)
creator = pydantic_model_creator(
model, include=resource.resource_fields.keys(), exclude=model._meta.m2m_fields
)
return GetManyOut(
total=await qs.count(),
data=list(map(lambda x: creator.from_orm(x).dict(), result))
total=await qs.count(), data=list(map(lambda x: creator.from_orm(x).dict(), result))
)
@router.get(
'/{resource}/form',
dependencies=[Depends(read_checker)]
)
async def form(
resource: str,
):
resource = await app.get_resource(resource, exclude_pk=True, exclude_m2m_field=False, exclude_actions=True)
@router.get("/{resource}/form", dependencies=[Depends(read_checker)])
async def form(resource: str,):
resource = await app.get_resource(
resource, exclude_pk=True, exclude_m2m_field=False, exclude_actions=True
)
return resource.dict(by_alias=True, exclude_unset=True)
@router.get(
'/{resource}/grid',
dependencies=[Depends(read_checker)]
)
async def grid(
resource: str,
):
@router.get("/{resource}/grid", dependencies=[Depends(read_checker)])
async def grid(resource: str,):
resource = await app.get_resource(resource)
return resource.dict(by_alias=True, exclude_unset=True)
@router.get(
'/{resource}/view',
dependencies=[Depends(read_checker)]
)
async def view(
resource: str,
):
@router.get("/{resource}/view", dependencies=[Depends(read_checker)])
async def view(resource: str,):
resource = await app.get_resource(resource)
return resource.dict(by_alias=True, exclude_unset=True)
@router.post(
'/{resource}/bulk/delete',
dependencies=[Depends(delete_checker)]
)
async def bulk_delete(
bulk_in: BulkIn,
model=Depends(get_model)
):
@router.post("/{resource}/bulk/delete", dependencies=[Depends(delete_checker)])
async def bulk_delete(bulk_in: BulkIn, model=Depends(get_model)):
await model.filter(pk__in=bulk_in.pk_list).delete()
return {'success': True}
return {"success": True}
@router.delete(
'/{resource}/{id}',
dependencies=[Depends(delete_checker)]
)
async def delete_one(
id: int,
model=Depends(get_model)
):
@router.delete("/{resource}/{id}", dependencies=[Depends(delete_checker)])
async def delete_one(id: int, model=Depends(get_model)):
await model.filter(pk=id).delete()
return {'success': True}
return {"success": True}
@router.put(
'/{resource}/{id}',
dependencies=[Depends(update_checker)]
)
async def update_one(
id: int,
parsed=Depends(parse_body),
model=Depends(get_model)
):
@router.put("/{resource}/{id}", dependencies=[Depends(update_checker)])
async def update_one(id: int, parsed=Depends(parse_body), model=Depends(get_model)):
body, resource_fields = parsed
m2m_fields = model._meta.m2m_fields
try:
obj = await handle_m2m_fields_create_or_update(body, m2m_fields, model, False, id)
except IntegrityError as e:
return UJSONResponse(status_code=HTTP_409_CONFLICT, content=dict(
message=f'Update Error,{e}'
))
return UJSONResponse(
status_code=HTTP_409_CONFLICT, content=dict(message=f"Update Error,{e}")
)
creator = pydantic_model_creator(model, include=resource_fields, exclude=m2m_fields)
return creator.from_orm(obj).dict()
@router.post(
'/{resource}',
dependencies=[Depends(create_checker)]
)
async def create_one(
parsed=Depends(parse_body),
model=Depends(get_model)
):
@router.post("/{resource}", dependencies=[Depends(create_checker)])
async def create_one(parsed=Depends(parse_body), model=Depends(get_model)):
body, resource_fields = parsed
m2m_fields = model._meta.m2m_fields
creator = pydantic_model_creator(model, include=resource_fields, exclude=m2m_fields)
try:
obj = await handle_m2m_fields_create_or_update(body, m2m_fields, model)
except IntegrityError as e:
return UJSONResponse(status_code=HTTP_409_CONFLICT, content=dict(
message=f'Create Error,{e}'
))
return UJSONResponse(
status_code=HTTP_409_CONFLICT, content=dict(message=f"Create Error,{e}")
)
return creator.from_orm(obj).dict()
@router.get(
'/{resource}/{id}',
dependencies=[Depends(read_checker)]
)
async def get_one(
id: int,
resource: str,
model=Depends(get_model)
):
@router.get("/{resource}/{id}", dependencies=[Depends(read_checker)])
async def get_one(id: int, resource: str, model=Depends(get_model)):
obj = await get_object_or_404(model, pk=id) # type:Model
m2m_fields = model._meta.m2m_fields
resource = await app.get_resource(resource, exclude_m2m_field=False)
@ -201,5 +156,5 @@ async def get_one(
relate_model = getattr(obj, m2m_field) # type:ManyToManyRelation
ids = await relate_model.all().values_list(relate_model.remote_model._meta.pk_attr)
ret[m2m_field] = list(map(lambda x: x[0], ids))
ret['__str__'] = str(obj)
ret["__str__"] = str(obj)
return ret

View File

@ -9,26 +9,22 @@ from ..shortcuts import get_object_or_404
router = APIRouter()
@router.get(
'/site',
)
async def site(
user_id=Depends(jwt_optional)
):
@router.get("/site",)
async def site(user_id=Depends(jwt_optional)):
site_ = app.site
user = None
if user_id:
user = await get_object_or_404(app.user_model, pk=user_id)
if user and app.permission and not user.is_superuser:
site_ = deepcopy(site_)
await user.fetch_related('roles')
filter_menus = filter(lambda x: (x.url and 'rest' in x.url) or x.children, site_.menus)
await user.fetch_related("roles")
filter_menus = filter(lambda x: (x.url and "rest" in x.url) or x.children, site_.menus)
hide_menus = []
for menu in filter_menus:
has_permission = False
for role in user.roles:
if not has_permission:
model = menu.url.split('/')[-1]
model = menu.url.split("/")[-1]
permission = await role.permissions.filter(model=model)
if permission:
has_permission = True

View File

@ -5,8 +5,8 @@ from pydantic import BaseModel
class LoginIn(BaseModel):
username: str = Body(..., example='long2ice')
password: str = Body(..., example='123456')
username: str = Body(..., example="long2ice")
password: str = Body(..., example="123456")
class BulkIn(BaseModel):

View File

@ -14,5 +14,5 @@ async def get_object_or_404(model: Generic[MODEL], **kwargs):
"""
obj = await model.filter(**kwargs).first() # type:model
if not obj:
raise HTTPException(HTTP_404_NOT_FOUND, 'Not Found')
raise HTTPException(HTTP_404_NOT_FOUND, "Not Found")
return obj

View File

@ -1,4 +1,4 @@
from typing import Optional, Tuple, Dict, Union, List, Set
from typing import Dict, List, Optional, Set, Tuple, Union
from pydantic import BaseModel, HttpUrl
@ -11,7 +11,7 @@ class Menu(BaseModel):
url: Optional[str]
icon: Optional[str]
# children menu
children: Optional[List['Menu']] = []
children: Optional[List["Menu"]] = []
# include fields
include: Optional[Tuple[str]]
# exclude fields
@ -31,10 +31,7 @@ class Menu(BaseModel):
# active table export
export: bool = True
actions: Optional[Dict]
bulk_actions: List[Dict] = [{
'value': 'delete',
'text': 'delete_all',
}]
bulk_actions: List[Dict] = [{"value": "delete", "text": "delete_all",}]
Menu.update_forward_refs()
@ -86,5 +83,5 @@ class Resource(BaseModel):
class Config:
fields = {
'resource_fields': 'fields',
"resource_fields": "fields",
}

384
poetry.lock generated
View File

@ -36,6 +36,22 @@ optional = false
python-versions = "*"
version = "7.0.0"
[[package]]
category = "dev"
description = "apipkg: namespace control and lazy-import mechanism"
name = "apipkg"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.5"
[[package]]
category = "dev"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
name = "appdirs"
optional = false
python-versions = "*"
version = "1.4.4"
[[package]]
category = "main"
description = "AsyncExitStack backport for Python 3.5+"
@ -60,6 +76,29 @@ optional = false
python-versions = ">=3.5"
version = "0.13.0"
[[package]]
category = "dev"
description = "Atomic file writes."
marker = "sys_platform == \"win32\""
name = "atomicwrites"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.4.0"
[[package]]
category = "dev"
description = "Classes Without Boilerplate"
name = "attrs"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "19.3.0"
[package.extras]
azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"]
dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"]
docs = ["sphinx", "zope.interface"]
tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
[[package]]
category = "main"
description = "Modern password hashing for your software and your servers"
@ -75,6 +114,26 @@ six = ">=1.4.1"
[package.extras]
tests = ["pytest (>=3.2.1,<3.3.0 || >3.3.0)"]
[[package]]
category = "dev"
description = "The uncompromising code formatter."
name = "black"
optional = false
python-versions = ">=3.6"
version = "19.10b0"
[package.dependencies]
appdirs = "*"
attrs = ">=18.1.0"
click = ">=6.5"
pathspec = ">=0.6,<1"
regex = "*"
toml = ">=0.9.4"
typed-ast = ">=1.4.0"
[package.extras]
d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
[[package]]
category = "main"
description = "Python package for providing Mozilla's CA Bundle."
@ -170,6 +229,20 @@ version = "1.1.1"
dnspython = ">=1.15.0"
idna = ">=2.0.0"
[[package]]
category = "dev"
description = "execnet: rapid multi-Python deployment"
name = "execnet"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.7.1"
[package.dependencies]
apipkg = ">=1.4"
[package.extras]
testing = ["pre-commit"]
[[package]]
category = "main"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
@ -240,6 +313,19 @@ dev = ["pyjwt", "passlib", "autoflake", "flake8", "uvicorn", "graphene"]
doc = ["mkdocs", "mkdocs-material", "markdown-include", "typer", "typer-cli", "pyyaml"]
test = ["pytest (>=4.0.0)", "pytest-cov", "mypy", "black", "isort", "requests", "email-validator", "sqlalchemy", "peewee", "databases", "orjson", "async-exit-stack", "async-generator", "python-multipart", "aiofiles", "flask"]
[[package]]
category = "dev"
description = "the modular source code checker: pep8 pyflakes and co"
name = "flake8"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
version = "3.8.1"
[package.dependencies]
mccabe = ">=0.6.0,<0.7.0"
pycodestyle = ">=2.6.0a1,<2.7.0"
pyflakes = ">=2.2.0,<2.3.0"
[[package]]
category = "main"
description = "GraphQL Framework for Python"
@ -326,6 +412,20 @@ optional = false
python-versions = "*"
version = "0.1.12"
[[package]]
category = "dev"
description = "A Python utility / library to sort Python imports."
name = "isort"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "4.3.21"
[package.extras]
pipfile = ["pipreqs", "requirementslib"]
pyproject = ["toml"]
requirements = ["pipreqs", "pip-api"]
xdg_home = ["appdirs (>=1.4.0)"]
[[package]]
category = "main"
description = "Various helpers to pass data to untrusted environments and back."
@ -356,6 +456,22 @@ optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
version = "1.1.1"
[[package]]
category = "dev"
description = "McCabe checker, plugin for flake8"
name = "mccabe"
optional = false
python-versions = "*"
version = "0.6.1"
[[package]]
category = "dev"
description = "More routines for operating on iterables, beyond itertools"
name = "more-itertools"
optional = false
python-versions = ">=3.5"
version = "8.3.0"
[[package]]
category = "main"
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
@ -364,6 +480,18 @@ optional = false
python-versions = ">=3.6"
version = "3.0.1"
[[package]]
category = "dev"
description = "Core utilities for Python packages"
name = "packaging"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "20.4"
[package.dependencies]
pyparsing = ">=2.0.2"
six = "*"
[[package]]
category = "main"
description = "comprehensive password hashing framework supporting over 30 schemes"
@ -378,6 +506,25 @@ bcrypt = ["bcrypt (>=3.1.0)"]
build_docs = ["sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)", "cloud-sptheme (>=1.10.0)"]
totp = ["cryptography"]
[[package]]
category = "dev"
description = "Utility library for gitignore style pattern matching of file paths."
name = "pathspec"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "0.8.0"
[[package]]
category = "dev"
description = "plugin and hook calling mechanisms for python"
name = "pluggy"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "0.13.1"
[package.extras]
dev = ["pre-commit", "tox"]
[[package]]
category = "main"
description = "Promises/A+ implementation for Python"
@ -403,6 +550,22 @@ version = "3.0.5"
[package.dependencies]
wcwidth = "*"
[[package]]
category = "dev"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
name = "py"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.8.1"
[[package]]
category = "dev"
description = "Python style guide checker"
name = "pycodestyle"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "2.6.0"
[[package]]
category = "main"
description = "C parser in Python"
@ -424,6 +587,14 @@ dotenv = ["python-dotenv (>=0.10.4)"]
email = ["email-validator (>=1.0.3)"]
typing_extensions = ["typing-extensions (>=3.7.2)"]
[[package]]
category = "dev"
description = "passive checker of Python programs"
name = "pyflakes"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "2.2.0"
[[package]]
category = "main"
description = "JSON Web Token implementation in Python"
@ -448,6 +619,14 @@ version = "0.9.2"
[package.dependencies]
cryptography = "*"
[[package]]
category = "dev"
description = "Python parsing module"
name = "pyparsing"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
version = "2.4.7"
[[package]]
category = "main"
description = "A SQL query builder API for Python"
@ -456,6 +635,56 @@ optional = false
python-versions = "*"
version = "0.37.6"
[[package]]
category = "dev"
description = "pytest: simple powerful testing with Python"
name = "pytest"
optional = false
python-versions = ">=3.5"
version = "5.4.2"
[package.dependencies]
atomicwrites = ">=1.0"
attrs = ">=17.4.0"
colorama = "*"
more-itertools = ">=4.0.0"
packaging = "*"
pluggy = ">=0.12,<1.0"
py = ">=1.5.0"
wcwidth = "*"
[package.extras]
checkqa-mypy = ["mypy (v0.761)"]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
[[package]]
category = "dev"
description = "run tests in isolated forked subprocesses"
name = "pytest-forked"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.1.3"
[package.dependencies]
pytest = ">=3.1.0"
[[package]]
category = "dev"
description = "pytest xdist plugin for distributed testing and loop-on-failing modes"
name = "pytest-xdist"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "1.32.0"
[package.dependencies]
execnet = ">=1.1"
pytest = ">=4.4.0"
pytest-forked = "*"
six = "*"
[package.extras]
testing = ["filelock"]
[[package]]
category = "main"
description = "Add .env support to your django/flask apps in development and deployments"
@ -494,6 +723,14 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "5.3.1"
[[package]]
category = "dev"
description = "Alternative regular expression module, to replace re."
name = "regex"
optional = false
python-versions = "*"
version = "2020.5.14"
[[package]]
category = "main"
description = "Python HTTP for Humans."
@ -526,7 +763,7 @@ description = "Python 2 and 3 compatibility utilities"
name = "six"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
version = "1.14.0"
version = "1.15.0"
[[package]]
category = "main"
@ -573,6 +810,14 @@ iso8601 = ">=0.1.12"
pypika = ">=0.36.5"
typing-extensions = ">=3.7"
[[package]]
category = "dev"
description = "a fork of Python 2 and 3 ast modules with type comment support"
name = "typed-ast"
optional = false
python-versions = "*"
version = "1.4.1"
[[package]]
category = "main"
description = "Backported and Experimental Type Hints for Python 3.5+"
@ -653,7 +898,7 @@ python-versions = "*"
version = "1.2.8"
[metadata]
content-hash = "02e9b18be076e5c7b8f95c5176d95ea55274ad0bb37157e3bf8bc7fd6a68a912"
content-hash = "136bc2fdede1a36872a4d17b125eb604420fe935f2291156bf593fbf52ae6bc4"
python-versions = "^3.8"
[metadata.files]
@ -673,6 +918,14 @@ aniso8601 = [
{file = "aniso8601-7.0.0-py2.py3-none-any.whl", hash = "sha256:d10a4bf949f619f719b227ef5386e31f49a2b6d453004b21f02661ccc8670c7b"},
{file = "aniso8601-7.0.0.tar.gz", hash = "sha256:513d2b6637b7853806ae79ffaca6f3e8754bdd547048f5ccc1420aec4b714f1e"},
]
apipkg = [
{file = "apipkg-1.5-py2.py3-none-any.whl", hash = "sha256:58587dd4dc3daefad0487f6d9ae32b4542b185e1c36db6993290e7c41ca2b47c"},
{file = "apipkg-1.5.tar.gz", hash = "sha256:37228cda29411948b422fae072f57e31d3396d2ee1c9783775980ee9c9990af6"},
]
appdirs = [
{file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
{file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
]
async-exit-stack = [
{file = "async_exit_stack-1.0.1-py3-none-any.whl", hash = "sha256:9b43b17683b3438f428ef3bbec20689f5abbb052aa4b564c643397330adfaa99"},
{file = "async_exit_stack-1.0.1.tar.gz", hash = "sha256:24de1ad6d0ff27be97c89d6709fa49bf20db179eaf1f4d2e6e9b4409b80e747d"},
@ -685,6 +938,14 @@ asynctest = [
{file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"},
{file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"},
]
atomicwrites = [
{file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
]
attrs = [
{file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"},
{file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"},
]
bcrypt = [
{file = "bcrypt-3.1.7-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:d7bdc26475679dd073ba0ed2766445bb5b20ca4793ca0db32b399dccc6bc84b7"},
{file = "bcrypt-3.1.7-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:69361315039878c0680be456640f8705d76cb4a3a3fe1e057e0f261b74be4b31"},
@ -705,6 +966,10 @@ bcrypt = [
{file = "bcrypt-3.1.7-cp38-cp38-win_amd64.whl", hash = "sha256:6305557019906466fc42dbc53b46da004e72fd7a551c044a827e572c82191752"},
{file = "bcrypt-3.1.7.tar.gz", hash = "sha256:0b0069c752ec14172c5f78208f1863d7ad6755a6fae6fe76ec2c80d13be41e42"},
]
black = [
{file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"},
{file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"},
]
certifi = [
{file = "certifi-2020.4.5.1-py2.py3-none-any.whl", hash = "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304"},
{file = "certifi-2020.4.5.1.tar.gz", hash = "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519"},
@ -783,10 +1048,18 @@ email-validator = [
{file = "email_validator-1.1.1-py2.py3-none-any.whl", hash = "sha256:5f246ae8d81ce3000eade06595b7bb55a4cf350d559e890182a1466a21f25067"},
{file = "email_validator-1.1.1.tar.gz", hash = "sha256:63094045c3e802c3d3d575b18b004a531c36243ca8d1cec785ff6bfcb04185bb"},
]
execnet = [
{file = "execnet-1.7.1-py2.py3-none-any.whl", hash = "sha256:d4efd397930c46415f62f8a31388d6be4f27a91d7550eb79bc64a756e0056547"},
{file = "execnet-1.7.1.tar.gz", hash = "sha256:cacb9df31c9680ec5f95553976c4da484d407e85e41c83cb812aa014f0eddc50"},
]
fastapi = [
{file = "fastapi-0.54.2-py3-none-any.whl", hash = "sha256:c8651f8316956240c2ffe5bc05c334c8359a3887e642720a9b23319c51e82907"},
{file = "fastapi-0.54.2.tar.gz", hash = "sha256:fff1b4a7fdf4812abb4507fb7aa30ef4206a0435839626ebe3b2871ec9aa367f"},
]
flake8 = [
{file = "flake8-3.8.1-py2.py3-none-any.whl", hash = "sha256:6c1193b0c3f853ef763969238f6c81e9e63ace9d024518edc020d5f1d6d93195"},
{file = "flake8-3.8.1.tar.gz", hash = "sha256:ea6623797bf9a52f4c9577d780da0bb17d65f870213f7b5bcc9fca82540c31d5"},
]
graphene = [
{file = "graphene-2.1.8-py2.py3-none-any.whl", hash = "sha256:09165f03e1591b76bf57b133482db9be6dac72c74b0a628d3c93182af9c5a896"},
{file = "graphene-2.1.8.tar.gz", hash = "sha256:2cbe6d4ef15cfc7b7805e0760a0e5b80747161ce1b0f990dfdc0d2cf497c12f9"},
@ -826,6 +1099,10 @@ iso8601 = [
{file = "iso8601-0.1.12-py3-none-any.whl", hash = "sha256:bbbae5fb4a7abfe71d4688fd64bff70b91bbd74ef6a99d964bab18f7fdf286dd"},
{file = "iso8601-0.1.12.tar.gz", hash = "sha256:49c4b20e1f38aa5cf109ddcd39647ac419f928512c869dc01d5c7098eddede82"},
]
isort = [
{file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"},
{file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"},
]
itsdangerous = [
{file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"},
{file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"},
@ -862,13 +1139,16 @@ markupsafe = [
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"},
{file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"},
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"},
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"},
{file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"},
{file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"},
{file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"},
]
mccabe = [
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
]
more-itertools = [
{file = "more-itertools-8.3.0.tar.gz", hash = "sha256:558bb897a2232f5e4f8e2399089e35aecb746e1f9191b6584a151647e89267be"},
{file = "more_itertools-8.3.0-py3-none-any.whl", hash = "sha256:7818f596b1e87be009031c7653d01acc46ed422e6656b394b0f765ce66ed4982"},
]
orjson = [
{file = "orjson-3.0.1-cp36-cp36m-macosx_10_7_x86_64.whl", hash = "sha256:5bf352dac1a9433a55b3558cea484f3548e58e137f883eabbf7a8eb489389fca"},
{file = "orjson-3.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6ecdb764a81260bb65fc37b1733bbb02666c5aa97835a87febbad130c6b4b9ac"},
@ -886,10 +1166,22 @@ orjson = [
{file = "orjson-3.0.1-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:b9c0f7d14cac42a41084d784b93288ebdf61537fa2d160610c1f1dd9a6f1d62f"},
{file = "orjson-3.0.1.tar.gz", hash = "sha256:a75f72c8a7cb0602c2fc7e9806554912b70bf79bbe559e55e1951b1f279c0ad2"},
]
packaging = [
{file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"},
{file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"},
]
passlib = [
{file = "passlib-1.7.2-py2.py3-none-any.whl", hash = "sha256:68c35c98a7968850e17f1b6892720764cc7eed0ef2b7cb3116a89a28e43fe177"},
{file = "passlib-1.7.2.tar.gz", hash = "sha256:8d666cef936198bc2ab47ee9b0410c94adf2ba798e5a84bf220be079ae7ab6a8"},
]
pathspec = [
{file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"},
{file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"},
]
pluggy = [
{file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
]
promise = [
{file = "promise-2.3.tar.gz", hash = "sha256:dfd18337c523ba4b6a58801c164c1904a9d4d1b1747c7d5dbf45b693a49d93d0"},
]
@ -897,6 +1189,14 @@ prompt-toolkit = [
{file = "prompt_toolkit-3.0.5-py3-none-any.whl", hash = "sha256:df7e9e63aea609b1da3a65641ceaf5bc7d05e0a04de5bd45d05dbeffbabf9e04"},
{file = "prompt_toolkit-3.0.5.tar.gz", hash = "sha256:563d1a4140b63ff9dd587bda9557cffb2fe73650205ab6f4383092fb882e7dc8"},
]
py = [
{file = "py-1.8.1-py2.py3-none-any.whl", hash = "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"},
{file = "py-1.8.1.tar.gz", hash = "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa"},
]
pycodestyle = [
{file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"},
{file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"},
]
pycparser = [
{file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"},
{file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"},
@ -920,6 +1220,10 @@ pydantic = [
{file = "pydantic-1.5.1-py36.py37.py38-none-any.whl", hash = "sha256:70f27d2f0268f490fe3de0a9b6fca7b7492b8fd6623f9fecd25b221ebee385e3"},
{file = "pydantic-1.5.1.tar.gz", hash = "sha256:f0018613c7a0d19df3240c2a913849786f21b6539b9f23d85ce4067489dfacfa"},
]
pyflakes = [
{file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"},
{file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"},
]
pyjwt = [
{file = "PyJWT-1.7.1-py2.py3-none-any.whl", hash = "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e"},
{file = "PyJWT-1.7.1.tar.gz", hash = "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96"},
@ -928,9 +1232,25 @@ pymysql = [
{file = "PyMySQL-0.9.2-py2.py3-none-any.whl", hash = "sha256:95f057328357e0e13a30e67857a8c694878b0175797a9a203ee7adbfb9b1ec5f"},
{file = "PyMySQL-0.9.2.tar.gz", hash = "sha256:9ec760cbb251c158c19d6c88c17ca00a8632bac713890e465b2be01fdc30713f"},
]
pyparsing = [
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
]
pypika = [
{file = "PyPika-0.37.6.tar.gz", hash = "sha256:64510fa36667e8bb654bdc1be5a3a77bac1dbc2f03d4848efac08e39d9cac6f5"},
]
pytest = [
{file = "pytest-5.4.2-py3-none-any.whl", hash = "sha256:95c710d0a72d91c13fae35dce195633c929c3792f54125919847fdcdf7caa0d3"},
{file = "pytest-5.4.2.tar.gz", hash = "sha256:eb2b5e935f6a019317e455b6da83dd8650ac9ffd2ee73a7b657a30873d67a698"},
]
pytest-forked = [
{file = "pytest-forked-1.1.3.tar.gz", hash = "sha256:1805699ed9c9e60cb7a8179b8d4fa2b8898098e82d229b0825d8095f0f261100"},
{file = "pytest_forked-1.1.3-py2.py3-none-any.whl", hash = "sha256:1ae25dba8ee2e56fb47311c9638f9e58552691da87e82d25b0ce0e4bf52b7d87"},
]
pytest-xdist = [
{file = "pytest-xdist-1.32.0.tar.gz", hash = "sha256:1d4166dcac69adb38eeaedb88c8fada8588348258a3492ab49ba9161f2971129"},
{file = "pytest_xdist-1.32.0-py2.py3-none-any.whl", hash = "sha256:ba5ec9fde3410bd9a116ff7e4f26c92e02fa3d27975ef3ad03f330b3d4b54e91"},
]
python-dotenv = [
{file = "python-dotenv-0.13.0.tar.gz", hash = "sha256:3b9909bc96b0edc6b01586e1eed05e71174ef4e04c71da5786370cebea53ad74"},
{file = "python_dotenv-0.13.0-py2.py3-none-any.whl", hash = "sha256:25c0ff1a3e12f4bde8d592cc254ab075cfe734fc5dd989036716fd17ee7e5ec7"},
@ -982,6 +1302,29 @@ pyyaml = [
{file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"},
{file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"},
]
regex = [
{file = "regex-2020.5.14-cp27-cp27m-win32.whl", hash = "sha256:e565569fc28e3ba3e475ec344d87ed3cd8ba2d575335359749298a0899fe122e"},
{file = "regex-2020.5.14-cp27-cp27m-win_amd64.whl", hash = "sha256:d466967ac8e45244b9dfe302bbe5e3337f8dc4dec8d7d10f5e950d83b140d33a"},
{file = "regex-2020.5.14-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:27ff7325b297fb6e5ebb70d10437592433601c423f5acf86e5bc1ee2919b9561"},
{file = "regex-2020.5.14-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ea55b80eb0d1c3f1d8d784264a6764f931e172480a2f1868f2536444c5f01e01"},
{file = "regex-2020.5.14-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:c9bce6e006fbe771a02bda468ec40ffccbf954803b470a0345ad39c603402577"},
{file = "regex-2020.5.14-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:d881c2e657c51d89f02ae4c21d9adbef76b8325fe4d5cf0e9ad62f850f3a98fd"},
{file = "regex-2020.5.14-cp36-cp36m-win32.whl", hash = "sha256:99568f00f7bf820c620f01721485cad230f3fb28f57d8fbf4a7967ec2e446994"},
{file = "regex-2020.5.14-cp36-cp36m-win_amd64.whl", hash = "sha256:70c14743320a68c5dac7fc5a0f685be63bc2024b062fe2aaccc4acc3d01b14a1"},
{file = "regex-2020.5.14-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:a7c37f048ec3920783abab99f8f4036561a174f1314302ccfa4e9ad31cb00eb4"},
{file = "regex-2020.5.14-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:89d76ce33d3266173f5be80bd4efcbd5196cafc34100fdab814f9b228dee0fa4"},
{file = "regex-2020.5.14-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:51f17abbe973c7673a61863516bdc9c0ef467407a940f39501e786a07406699c"},
{file = "regex-2020.5.14-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:ce5cc53aa9fbbf6712e92c7cf268274eaff30f6bd12a0754e8133d85a8fb0f5f"},
{file = "regex-2020.5.14-cp37-cp37m-win32.whl", hash = "sha256:8044d1c085d49673aadb3d7dc20ef5cb5b030c7a4fa253a593dda2eab3059929"},
{file = "regex-2020.5.14-cp37-cp37m-win_amd64.whl", hash = "sha256:c2062c7d470751b648f1cacc3f54460aebfc261285f14bc6da49c6943bd48bdd"},
{file = "regex-2020.5.14-cp38-cp38-manylinux1_i686.whl", hash = "sha256:329ba35d711e3428db6b45a53b1b13a0a8ba07cbbcf10bbed291a7da45f106c3"},
{file = "regex-2020.5.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:579ea215c81d18da550b62ff97ee187b99f1b135fd894a13451e00986a080cad"},
{file = "regex-2020.5.14-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:3a9394197664e35566242686d84dfd264c07b20f93514e2e09d3c2b3ffdf78fe"},
{file = "regex-2020.5.14-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ce367d21f33e23a84fb83a641b3834dd7dd8e9318ad8ff677fbfae5915a239f7"},
{file = "regex-2020.5.14-cp38-cp38-win32.whl", hash = "sha256:1386e75c9d1574f6aa2e4eb5355374c8e55f9aac97e224a8a5a6abded0f9c927"},
{file = "regex-2020.5.14-cp38-cp38-win_amd64.whl", hash = "sha256:7e61be8a2900897803c293247ef87366d5df86bf701083b6c43119c7c6c99108"},
{file = "regex-2020.5.14.tar.gz", hash = "sha256:ce450ffbfec93821ab1fea94779a8440e10cf63819be6e176eb1973a6017aff5"},
]
requests = [
{file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"},
{file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"},
@ -991,8 +1334,8 @@ rx = [
{file = "Rx-1.6.1.tar.gz", hash = "sha256:13a1d8d9e252625c173dc795471e614eadfe1cf40ffc684e08b8fff0d9748c23"},
]
six = [
{file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"},
{file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"},
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
{file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
]
starlette = [
{file = "starlette-0.13.2-py3-none-any.whl", hash = "sha256:6169ee78ded501095d1dda7b141a1dc9f9934d37ad23196e180150ace2c6449b"},
@ -1009,6 +1352,29 @@ toml = [
tortoise-orm = [
{file = "tortoise-orm-0.16.11.tar.gz", hash = "sha256:08a25c59a171bdabe9469d1606f8c4ae41516504cc501f5d60c1b953f3e186bb"},
]
typed-ast = [
{file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"},
{file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"},
{file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"},
{file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"},
{file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"},
{file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"},
{file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"},
{file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"},
{file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"},
{file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"},
{file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"},
{file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"},
{file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"},
{file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"},
{file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"},
{file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"},
{file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"},
{file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"},
{file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"},
{file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"},
{file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"},
]
typing-extensions = [
{file = "typing_extensions-3.7.4.2-py2-none-any.whl", hash = "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392"},
{file = "typing_extensions-3.7.4.2-py3-none-any.whl", hash = "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5"},

View File

@ -2,7 +2,7 @@
name = "fastapi-admin"
version = "0.2.6"
description = "Fast Admin Dashboard based on fastapi and tortoise-orm and rest-admin."
authors = ["long2ice <long2ice@prismslight.com>"]
authors = ["long2ice <long2ice@gmail.com>"]
[tool.poetry.dependencies]
python = "^3.8"
@ -23,10 +23,16 @@ prompt_toolkit = "*"
[tool.poetry.dev-dependencies]
taskipy = "*"
asynctest = "*"
flake8 = "*"
isort = "*"
black = "^19.10b0"
pytest = "*"
pytest-xdist = "*"
[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"
[tool.taskipy.tasks]
export = "poetry export -f requirements.txt --without-hashes > requirements.txt"
export = "poetry export -f requirements.txt --without-hashes > requirements.txt"
export-dev = "poetry export -f requirements.txt --dev --without-hashes > requirements-dev.txt"

78
requirements-dev.txt Normal file
View File

@ -0,0 +1,78 @@
aiofiles==0.5.0
aiomysql==0.0.20
aiosqlite==0.13.0
aniso8601==7.0.0
apipkg==1.5
appdirs==1.4.4
async-exit-stack==1.0.1
async-generator==1.10
asynctest==0.13.0
atomicwrites==1.4.0; sys_platform == "win32"
attrs==19.3.0
bcrypt==3.1.7
black==19.10b0
certifi==2020.4.5.1
cffi==1.14.0
chardet==3.0.4
ciso8601==2.1.3; sys_platform != "win32" and implementation_name == "cpython"
click==7.1.2
colorama==0.4.3
cryptography==2.9.2
dnspython==1.16.0
email-validator==1.1.1
execnet==1.7.1
fastapi==0.54.2
flake8==3.8.1
graphene==2.1.8
graphql-core==2.3.2
graphql-relay==2.0.1
h11==0.9.0
httptools==0.1.1; sys_platform != "win32" and sys_platform != "cygwin" and platform_python_implementation != "PyPy"
idna==2.9
iso8601==0.1.12; sys_platform == "win32" or implementation_name != "cpython"
isort==4.3.21
itsdangerous==1.1.0
jinja2==2.11.2
markupsafe==1.1.1
mccabe==0.6.1
more-itertools==8.3.0
orjson==3.0.1
packaging==20.4
passlib==1.7.2
pathspec==0.8.0
pluggy==0.13.1
promise==2.3
prompt-toolkit==3.0.5
py==1.8.1
pycodestyle==2.6.0
pycparser==2.20
pydantic==1.5.1
pyflakes==2.2.0
pyjwt==1.7.1
pymysql==0.9.2
pyparsing==2.4.7
pypika==0.37.6
pytest==5.4.2
pytest-forked==1.1.3
pytest-xdist==1.32.0
python-dotenv==0.13.0
python-multipart==0.0.5
python-rapidjson==0.9.1
pyyaml==5.3.1
regex==2020.5.14
requests==2.23.0
rx==1.6.1
six==1.15.0
starlette==0.13.2
taskipy==1.2.1
toml==0.10.1
tortoise-orm==0.16.11
typed-ast==1.4.1
typing-extensions==3.7.4.2
ujson==2.0.3
urllib3==1.25.9
uvicorn==0.11.5
uvloop==0.14.0
wcwidth==0.1.9
websockets==8.1
xlsxwriter==1.2.8

View File

@ -40,7 +40,7 @@ python-rapidjson==0.9.1
pyyaml==5.3.1
requests==2.23.0
rx==1.6.1
six==1.14.0
six==1.15.0
starlette==0.13.2
tortoise-orm==0.16.11
typing-extensions==3.7.4.2