add builtin permissions

remove orjson
fix tortoise error
This commit is contained in:
long2ice
2020-04-15 18:59:41 +08:00
parent 4ebd2f80df
commit 4e70174c85
22 changed files with 362 additions and 121 deletions

View File

@@ -1,6 +1,6 @@
from enum import IntEnum
from fastapi_admin.enum import EnumMixin
from fastapi_admin.enums import EnumMixin
class ProductType(EnumMixin, IntEnum):

View File

@@ -38,12 +38,14 @@ def create_app():
fast_app = FastAPI(debug=True)
register_tortoise(fast_app, config=TORTOISE_ORM, generate_schemas=True)
fast_app.mount('/admin', admin_app)
fast_app.mount('/admin', admin_app)
admin_app.debug = True
admin_app.init(
user_model='User',
admin_secret='test',
models='examples.models',
permission=True,
site=Site(
name='FastAPI-admin Demo',
logo='https://github.com/long2ice/fastapi-admin/raw/master/front/static/img/logo.png',
@@ -101,12 +103,19 @@ def create_app():
name='User',
url='/rest/User',
icon='fa fa-user',
fields_type={
'avatar': 'image'
},
exclude=('password',),
search_fields=('username',)
),
Menu(
name='Role',
url='/rest/Role',
icon='fa fa-group'
),
Menu(
name='Permission',
url='/rest/Permission',
icon='fa fa-copyright'
),
Menu(
name='Logout',
url='/logout',

View File

@@ -1,14 +1,17 @@
import datetime
from tortoise import fields, Model
from fastapi_admin.models import User as AdminUser
from fastapi_admin.models import User as AdminUser, Permission, Role
from .enums import ProductType
class User(AdminUser, Model):
last_login = fields.DatetimeField(description='Last Login')
last_login = fields.DatetimeField(description='Last Login', default=datetime.datetime.now)
is_active = fields.BooleanField(default=True, description='Is Active')
avatar = fields.CharField(max_length=200)
intro = fields.TextField()
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):

105
fastapi_admin/cli.py Normal file
View File

@@ -0,0 +1,105 @@
import argparse
import importlib
from colorama import Fore, init
from prompt_toolkit import PromptSession
from tortoise import Tortoise, run_async
from fastapi_admin import enums
from fastapi_admin.common import pwd_context
from fastapi_admin.models import Permission
init(autoreset=True)
class Logger:
@classmethod
def success(cls, text):
print(Fore.GREEN + text)
@classmethod
def waring(cls, text):
print(Fore.YELLOW + text)
@classmethod
def error(cls, text):
print(Fore.RED + text)
def import_obj(path):
module_name, class_name = path.rsplit('.', 1)
return getattr(importlib.import_module(module_name), class_name)
async def init_tortoise(args):
await Tortoise.init(config=import_obj(args.config))
async def register_permissions(args):
await init_tortoise(args)
if args.clean:
await Permission.all().delete()
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,
)
if created:
Logger.success(f'Create permission {label} success.')
async def createsuperuser(args):
await init_tortoise(args)
user_model = import_obj(args.user_model)
prompt = PromptSession()
while True:
try:
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
)
Logger.success(f'Create superuser {username} success.')
return
except Exception as e:
Logger.error(f'Create superuser {username} error,{e}')
except (EOFError, KeyboardInterrupt):
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.')
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.set_defaults(func=createsuperuser)
parse_args = parser.parse_args()
run_async(parse_args.func(parse_args))
if __name__ == '__main__':
cli()

View File

@@ -1,5 +1,9 @@
from copy import deepcopy
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto')
async def handle_m2m_fields_create_or_update(body, m2m_fields, model, create=True, pk=None):
"""
@@ -25,7 +29,7 @@ async def handle_m2m_fields_create_or_update(body, m2m_fields, model, create=Tru
m2m_related = getattr(obj, k)
if not create:
await m2m_related.clear()
m2m_model = m2m_related.model
m2m_model = m2m_related.remote_model
m2m_objs = await m2m_model.filter(pk__in=v)
await m2m_related.add(*m2m_objs)
return obj

View File

@@ -1,10 +1,14 @@
import json
import jwt
import orjson
from fastapi import Query, Path, Depends, HTTPException
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from fastapi.security.utils import get_authorization_scheme_param
from pydantic import BaseModel
from starlette.requests import Request
from starlette.status import HTTP_401_UNAUTHORIZED
from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND
from . import enums
from .factory import app
auth_schema = HTTPBearer()
@@ -38,7 +42,7 @@ class QueryItem(BaseModel):
def get_query(query=Query(...)):
query = orjson.loads(query)
query = json.loads(query)
return QueryItem.parse_obj(query)
@@ -57,3 +61,35 @@ async def parse_body(request: Request, resource: str = Path(...)):
if v is not None:
ret[key] = v
return ret, resource_fields
async def get_current_user(user_id=Depends(jwt_required)):
user = await app.user_model.get_or_none(pk=user_id)
if not user:
raise HTTPException(HTTP_404_NOT_FOUND)
return user
class PermissionsChecker:
def __init__(self, action: enums.PermissionAction):
self.action = action
async def __call__(self, resource: str = Path(...), user=Depends(get_current_user)):
if not app.permission or user.is_superuser:
return
if not user.is_active:
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail='User is not Active!')
has_permission = False
await user.fetch_related('roles')
for role in user.roles:
if await role.permissions.filter(model=resource, action=self.action):
has_permission = True
break
if not has_permission:
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail='No Permission!')
read_checker = PermissionsChecker(action=enums.PermissionAction.read)
create_checker = PermissionsChecker(action=enums.PermissionAction.create)
update_checker = PermissionsChecker(action=enums.PermissionAction.update)
delete_checker = PermissionsChecker(action=enums.PermissionAction.delete)

View File

@@ -1,8 +0,0 @@
import abc
class EnumMixin:
@classmethod
@abc.abstractmethod
def choices(cls):
pass

25
fastapi_admin/enums.py Normal file
View File

@@ -0,0 +1,25 @@
import abc
from enum import IntEnum
class EnumMixin:
@classmethod
@abc.abstractmethod
def choices(cls):
pass
class PermissionAction(EnumMixin, IntEnum):
create = 1
delete = 2
update = 3
read = 4
@classmethod
def choices(cls):
return {
cls.create: 'Create',
cls.delete: 'Delete',
cls.update: 'Update',
cls.read: 'Read',
}

View File

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

View File

@@ -1,30 +1,35 @@
import importlib
from typing import Type, List, Dict
from fastapi import FastAPI
from tortoise import Model, Tortoise
from fastapi import FastAPI, HTTPException
from tortoise import Model
from tortoise.fields import IntField, BooleanField, DatetimeField, DateField
from tortoise.fields.data import IntEnumFieldInstance, CharEnumFieldInstance, DecimalField, FloatField, TextField, \
SmallIntField, JSONField
from .exceptions import exception_handler
from .site import Site, Resource, Field, Menu
class AdminApp(FastAPI):
models: str
admin_secret: str
user_model: str
user_model: Model
site: Site
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] = {}
@@ -35,16 +40,18 @@ class AdminApp(FastAPI):
else:
self.model_menu_mapping[menu.url.split('?')[0].split('/')[-1]] = menu
def init(self, site: Site, user_model: str, admin_secret: str, models: str, ):
def init(self, site: Site, user_model: str, admin_secret: str, models: str, permission: bool = False):
"""
init admin site
:param permission: active builtin permission
:param models:
:param site:
:param user_model: admin user model path,like admin.models.user
:param admin_secret: admin jwt secret.
:param models: tortoise models
:return:
"""
self.site = site
self.permission = permission
self.admin_secret = admin_secret
self.models = importlib.import_module(models)
self.user_model = getattr(self.models, user_model)
@@ -160,7 +167,7 @@ class AdminApp(FastAPI):
name = m2m_field.get('name')
if not self._exclude_field(resource, name):
label = m2m_field.get('description') or name.title()
m2m_model_class = model._meta.fields_map[name].model
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))
fields[name] = Field(
@@ -190,3 +197,4 @@ class AdminApp(FastAPI):
app = AdminApp(
openapi_prefix='/admin',
)
app.add_exception_handler(HTTPException, exception_handler)

View File

@@ -1,5 +1,7 @@
from tortoise import Model, fields
from fastapi_admin import enums
class User(Model):
username = fields.CharField(max_length=20, unique=True)
@@ -7,3 +9,22 @@ class User(Model):
class Meta:
abstract = True
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)
def __str__(self):
return self.label
class Role(Model):
label = fields.CharField(max_length=50)
users = fields.ManyToManyField('models.User')
permissions: fields.ManyToManyRelation[Permission] = fields.ManyToManyField('models.Permission')
def __str__(self):
return self.label

View File

@@ -1,5 +1,5 @@
from fastapi import Depends, APIRouter
from fastapi.responses import ORJSONResponse
from fastapi.responses import UJSONResponse
from starlette.status import HTTP_409_CONFLICT
from tortoise import Model
from tortoise.contrib.pydantic import pydantic_model_creator
@@ -7,7 +7,8 @@ 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
from ..depends import QueryItem, get_query, parse_body, get_model, read_checker, delete_checker, update_checker, \
create_checker
from ..factory import app
from ..responses import GetManyOut
from ..schemas import BulkIn
@@ -17,7 +18,8 @@ router = APIRouter()
@router.get(
'/{resource}'
'/{resource}',
dependencies=[Depends(read_checker)]
)
async def get_resource(
resource: str,
@@ -45,7 +47,8 @@ async def get_resource(
@router.get(
'/{resource}/form'
'/{resource}/form',
dependencies=[Depends(read_checker)]
)
async def form(
resource: str,
@@ -55,7 +58,8 @@ async def form(
@router.get(
'/{resource}/grid'
'/{resource}/grid',
dependencies=[Depends(read_checker)]
)
async def grid(
resource: str,
@@ -65,7 +69,8 @@ async def grid(
@router.get(
'/{resource}/view'
'/{resource}/view',
dependencies=[Depends(read_checker)]
)
async def view(
resource: str,
@@ -75,7 +80,8 @@ async def view(
@router.post(
'/{resource}/bulk/delete'
'/{resource}/bulk/delete',
dependencies=[Depends(delete_checker)]
)
async def bulk_delete(
bulk_in: BulkIn,
@@ -86,7 +92,8 @@ async def bulk_delete(
@router.delete(
'/{resource}/{id}'
'/{resource}/{id}',
dependencies=[Depends(delete_checker)]
)
async def delete_one(
id: int,
@@ -97,7 +104,8 @@ async def delete_one(
@router.put(
'/{resource}/{id}'
'/{resource}/{id}',
dependencies=[Depends(update_checker)]
)
async def update_one(
id: int,
@@ -109,7 +117,7 @@ async def update_one(
try:
obj = await handle_m2m_fields_create_or_update(body, m2m_fields, model, False, id)
except IntegrityError as e:
return ORJSONResponse(status_code=HTTP_409_CONFLICT, content=dict(
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)
@@ -117,7 +125,8 @@ async def update_one(
@router.post(
'/{resource}'
'/{resource}',
dependencies=[Depends(create_checker)]
)
async def create_one(
parsed=Depends(parse_body),
@@ -129,14 +138,15 @@ async def create_one(
try:
obj = await handle_m2m_fields_create_or_update(body, m2m_fields, model)
except IntegrityError as e:
return ORJSONResponse(status_code=HTTP_409_CONFLICT, content=dict(
return UJSONResponse(status_code=HTTP_409_CONFLICT, content=dict(
message=f'Create Error,{e}'
))
return creator.from_orm(obj).dict()
@router.get(
'/{resource}/{id}'
'/{resource}/{id}',
dependencies=[Depends(read_checker)]
)
async def get_one(
id: int,

View File

@@ -1,15 +1,12 @@
import jwt
from fastapi import APIRouter
from fastapi.responses import ORJSONResponse
from passlib.context import CryptContext
from fastapi import APIRouter, HTTPException
from starlette.status import HTTP_403_FORBIDDEN
from ..common import pwd_context
from ..factory import app
from ..schemas import LoginIn
from ..shortcuts import get_object_or_404
pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto')
router = APIRouter()
@@ -21,13 +18,16 @@ async def login(
):
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!')
if not pwd_context.verify(login_in.password, user.password):
return ORJSONResponse(status_code=HTTP_403_FORBIDDEN, content={
'message': 'Incorrect Password'
})
return {
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail='Incorrect Password!')
ret = {
'user': {
'username': user.username
'username': user.username,
'is_superuser': user.is_superuser,
},
'token': jwt.encode({'user_id': user.pk}, app.admin_secret, algorithm='HS256')
}
return ret

View File

@@ -1,5 +1,8 @@
from fastapi import APIRouter
from copy import deepcopy
from fastapi import APIRouter, Depends
from ..depends import get_current_user
from ..factory import app
router = APIRouter()
@@ -17,5 +20,19 @@ async def home():
@router.get(
'/site',
)
async def site():
return app.site.dict(by_alias=True, exclude_unset=True)
async def site(
user=Depends(get_current_user)
):
site_ = app.site
if app.permission and not user.is_superuser:
site_ = deepcopy(site_)
await user.fetch_related('roles')
for menu in site_.menus:
has_permission = False
if not has_permission:
for role in user.roles:
if await role.permissions.filter(model=menu.name):
has_permission = True
if not has_permission:
site_.menus.remove(menu)
return site_.dict(by_alias=True, exclude_unset=True)

View File

@@ -328,9 +328,7 @@ export default {
return this.onSubmit(this.model);
}
const methodName = String(this.method).toLowerCase();
// console.log(this.$refs.form);
let formData = this.model;
console.log(formData,)
if (this.useFormData) {
formData = new FormData();
_.mapValues(this.model, (v, k) => formData.append(k, v));

View File

@@ -5,8 +5,7 @@
<b-img class="site-logo" :src="site.logo" fluid/>
</a>
<!-- <b-img class="site-logo" :src="require('../assets/img/gengyi-logo.svg')" fluid style="border-radius: 5px;" /> -->
<!-- <b-img rounded="circle" :src="auth.user.avatar" height="70" blank-color="#777" alt="avatar" class="m-2" /> -->
<b-img rounded="circle" :src="auth.user.avatar" height="70" blank-color="#777" alt="avatar" class="m-2"/>
<div class="my-3" v-if="site.sidebar_userinfo !== false" v-show="!collapsed">
<h5 style="letter-spacing:2px">{{site.name}}</h5>
<template v-if="auth.user">

View File

@@ -1,12 +1,12 @@
export default {
get(key, defaultValue) {
try {
return JSON.parse(localStorage.getItem(`rest_admin_${key}`))
return JSON.parse(localStorage.getItem(`fastapi_admin_${key}`))
} catch (e) {
return defaultValue
}
},
set(key, value) {
localStorage.setItem(`rest_admin_${key}`, JSON.stringify(value))
localStorage.setItem(`fastapi_admin_${key}`, JSON.stringify(value))
},
}
}

View File

@@ -348,6 +348,8 @@
if (this.$refs.table) {
this.$refs.table.refresh();
}
}).catch(error => {
this.table = {}
});
}
},

90
poetry.lock generated
View File

@@ -119,6 +119,14 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "7.1.1"
[[package]]
category = "main"
description = "Cross-platform colored terminal text."
name = "colorama"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "0.4.3"
[[package]]
category = "main"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
@@ -162,18 +170,6 @@ version = "1.0.5"
dnspython = ">=1.15.0"
idna = ">=2.0.0"
[[package]]
category = "dev"
description = "Faker is a Python package that generates fake data for you."
name = "faker"
optional = false
python-versions = ">=3.4"
version = "4.0.2"
[package.dependencies]
python-dateutil = ">=2.4"
text-unidecode = "1.3"
[[package]]
category = "main"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
@@ -344,7 +340,7 @@ description = "A very fast and expressive template engine."
name = "jinja2"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "2.11.1"
version = "2.11.2"
[package.dependencies]
MarkupSafe = ">=0.23"
@@ -396,6 +392,17 @@ six = "*"
[package.extras]
test = ["pytest (>=2.7.3)", "pytest-cov", "coveralls", "futures", "pytest-benchmark", "mock"]
[[package]]
category = "main"
description = "Library for building powerful interactive command lines in Python"
name = "prompt-toolkit"
optional = false
python-versions = ">=3.6.1"
version = "3.0.5"
[package.dependencies]
wcwidth = "*"
[[package]]
category = "main"
description = "C parser in Python"
@@ -449,17 +456,6 @@ optional = false
python-versions = "*"
version = "0.36.3"
[[package]]
category = "dev"
description = "Extensions to the standard Python datetime module"
name = "python-dateutil"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
version = "2.8.1"
[package.dependencies]
six = ">=1.5"
[[package]]
category = "main"
description = "Add .env support to your django/flask apps in development and deployments"
@@ -554,14 +550,6 @@ version = "1.2.1"
[package.dependencies]
toml = ">=0.10.0,<0.11.0"
[[package]]
category = "dev"
description = "The most basic Text::Unidecode port"
name = "text-unidecode"
optional = false
python-versions = "*"
version = "1.3"
[[package]]
category = "dev"
description = "Python Library for Tom's Obvious, Minimal Language"
@@ -641,6 +629,14 @@ optional = false
python-versions = "*"
version = "0.14.0"
[[package]]
category = "main"
description = "Measures number of Terminal column cells of wide-character codes"
name = "wcwidth"
optional = false
python-versions = "*"
version = "0.1.9"
[[package]]
category = "main"
description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
@@ -658,7 +654,7 @@ python-versions = "*"
version = "1.2.8"
[metadata]
content-hash = "826fcab0d9d69efbbecc67bb195b8dcf7da1b2b2ddd2bae7a4d4468d95270fde"
content-hash = "9c9adb8acd429a938c06fd7ad2da8b3416af5edfd391c38f3e7a8ffb3bd0db74"
python-versions = "^3.8"
[metadata.files]
@@ -754,6 +750,10 @@ click = [
{file = "click-7.1.1-py2.py3-none-any.whl", hash = "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"},
{file = "click-7.1.1.tar.gz", hash = "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc"},
]
colorama = [
{file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"},
{file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"},
]
cryptography = [
{file = "cryptography-2.9-cp27-cp27m-macosx_10_9_intel.whl", hash = "sha256:ef9a55013676907df6c9d7dd943eb1770d014f68beaa7e73250fb43c759f4585"},
{file = "cryptography-2.9-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:2a2ad24d43398d89f92209289f15265107928f22a8d10385f70def7a698d6a02"},
@@ -782,10 +782,6 @@ dnspython = [
email-validator = [
{file = "email_validator-1.0.5-py2.py3-none-any.whl", hash = "sha256:e3e6ede1765d7c1e580d2050d834b2689361f7da2d50ce74df6a5968fca7cb13"},
]
faker = [
{file = "Faker-4.0.2-py3-none-any.whl", hash = "sha256:b89aa33837498498e15c709eb40c31386408a901a53c7a5e12a425737a767976"},
{file = "Faker-4.0.2.tar.gz", hash = "sha256:2d3f866ef25e1a5af80e7b0ceeacc3c92dec5d0fdbad3e2cb6adf6e60b22188f"},
]
fastapi = [
{file = "fastapi-0.54.1-py3-none-any.whl", hash = "sha256:1ee9a49f28d510b62b3b51a9452b274853bfc9c5d4b947ed054366e2d49f9efa"},
{file = "fastapi-0.54.1.tar.gz", hash = "sha256:72f40f47e5235cb5cbbad1d4f97932ede6059290c07e12e9784028dcd1063d28"},
@@ -834,8 +830,8 @@ itsdangerous = [
{file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"},
]
jinja2 = [
{file = "Jinja2-2.11.1-py2.py3-none-any.whl", hash = "sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49"},
{file = "Jinja2-2.11.1.tar.gz", hash = "sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250"},
{file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"},
{file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"},
]
markupsafe = [
{file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"},
@@ -896,6 +892,10 @@ passlib = [
promise = [
{file = "promise-2.3.tar.gz", hash = "sha256:dfd18337c523ba4b6a58801c164c1904a9d4d1b1747c7d5dbf45b693a49d93d0"},
]
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"},
]
pycparser = [
{file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"},
{file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"},
@@ -927,10 +927,6 @@ pymysql = [
pypika = [
{file = "PyPika-0.36.3.tar.gz", hash = "sha256:4fd6455955faa9d9016c2e39281a4808f82bbcf6065726f2c73e923a9e32b1bc"},
]
python-dateutil = [
{file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"},
{file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"},
]
python-dotenv = [
{file = "python-dotenv-0.12.0.tar.gz", hash = "sha256:92b3123fb2d58a284f76cc92bfe4ee6c502c32ded73e8b051c4f6afc8b6751ed"},
{file = "python_dotenv-0.12.0-py2.py3-none-any.whl", hash = "sha256:81822227f771e0cab235a2939f0f265954ac4763cafd806d845801c863bf372f"},
@@ -1002,10 +998,6 @@ taskipy = [
{file = "taskipy-1.2.1-py3-none-any.whl", hash = "sha256:99bdaf5b19791c2345806847147e0fc2d28e1ac9446058def5a8b6b3fc9f23e2"},
{file = "taskipy-1.2.1.tar.gz", hash = "sha256:5eb2c3b1606c896c7fa799848e71e8883b880759224958d07ba760e5db263175"},
]
text-unidecode = [
{file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"},
{file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"},
]
toml = [
{file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"},
{file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"},
@@ -1045,6 +1037,10 @@ uvloop = [
{file = "uvloop-0.14.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09"},
{file = "uvloop-0.14.0.tar.gz", hash = "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e"},
]
wcwidth = [
{file = "wcwidth-0.1.9-py2.py3-none-any.whl", hash = "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1"},
{file = "wcwidth-0.1.9.tar.gz", hash = "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1"},
]
websockets = [
{file = "websockets-8.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c"},
{file = "websockets-8.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170"},

View File

@@ -17,11 +17,12 @@ passlib = "*"
bcrypt = "*"
pyjwt = "*"
xlsxwriter = "*"
colorama = "*"
prompt_toolkit = "*"
[tool.poetry.dev-dependencies]
taskipy = "*"
asynctest = "*"
faker = "*"
[build-system]
requires = ["poetry>=0.12"]

View File

@@ -10,6 +10,7 @@ cffi==1.14.0
chardet==3.0.4
ciso8601==2.1.3; sys_platform != "win32" and implementation_name == "cpython"
click==7.1.1
colorama==0.4.3
cryptography==2.9
dnspython==1.16.0
email-validator==1.0.5
@@ -22,11 +23,11 @@ httptools==0.1.1; sys_platform != "win32" and sys_platform != "cygwin" and platf
idna==2.9
iso8601==0.1.12; sys_platform == "win32" or implementation_name != "cpython"
itsdangerous==1.1.0
jinja2==2.11.1
jinja2==2.11.2
markupsafe==1.1.1
orjson==2.6.5
passlib==1.7.2
promise==2.3
prompt-toolkit==3.0.5
pycparser==2.20
pydantic==1.4
pyjwt==1.7.1
@@ -45,5 +46,6 @@ ujson==2.0.3
urllib3==1.25.8
uvicorn==0.11.3
uvloop==0.14.0
wcwidth==0.1.9
websockets==8.1
xlsxwriter==1.2.8

View File

@@ -33,6 +33,9 @@ setup(
packages=find_packages(include=['fastapi_admin*']),
include_package_data=True,
zip_safe=True,
entry_points={
'console_scripts': ['fastapi-admin = fastapi_admin.cli:cli'],
},
platforms='any',
keywords=(
'fastapi admin dashboard'