This commit is contained in:
long2ice 2020-05-12 19:33:21 +08:00
parent 75e7a46e85
commit 4e7e1626aa
11 changed files with 318 additions and 121 deletions

19
Makefile Normal file
View File

@ -0,0 +1,19 @@
checkfiles = alice/ examples/ tests/ conftest.py
black_opts = -l 100 -t py38
py_warn = PYTHONDEVMODE=1
help:
@echo "Alice development makefile"
@echo
@echo "usage: make <target>"
@echo "Targets:"
@echo " test Runs all tests"
@echo " style Auto-formats the code"
deps:
@which pip-sync > /dev/null || pip install -q pip-tools
@pip-sync tests/requirements.txt
style: deps
isort -rc $(checkfiles)
black $(black_opts) $(checkfiles)

View File

@ -1,9 +1,11 @@
__version__ = '0.1.0' __version__ = '0.1.0'
from alice.cmd import CommandLine
def main():
pass def main(argv):
command = CommandLine(argv)
if __name__ == '__main__': if __name__ == '__main__':
main() main(None)

View File

@ -1,36 +1,125 @@
from typing import Type from typing import Type, List
from tortoise import Model, BaseDBAsyncClient from tortoise import Model, BaseDBAsyncClient, ForeignKeyFieldInstance
from tortoise.backends.base.schema_generator import BaseSchemaGenerator from tortoise.backends.base.schema_generator import BaseSchemaGenerator
from tortoise.fields import Field, UUIDField, TextField, JSONField
class DDL: class DDL:
schema_generator_cls: Type[BaseSchemaGenerator] = BaseSchemaGenerator schema_generator_cls: Type[BaseSchemaGenerator] = BaseSchemaGenerator
DIALECT = "sql"
_DROP_TABLE_TEMPLATE = 'DROP TABLE {table_name} IF EXISTS'
_ADD_COLUMN_TEMPLATE = 'ALTER TABLE {table_name} ADD {column}'
_DROP_COLUMN_TEMPLATE = 'ALTER TABLE {table_name} DROP COLUMN {column_name}'
_ADD_INDEX_TEMPLATE = 'ALTER TABLE {table_name} ADD {unique} INDEX {index_name} ({column_names})'
_DROP_INDEX_TEMPLATE = 'ALTER TABLE {table_name} DROP INDEX {index_name}'
_ADD_FK_TEMPLATE = 'ALTER TABLE {table_name} ADD CONSTRAINT `{fk_name}` FOREIGN KEY (`{db_column}`) REFERENCES `{table}` (`{field}`) ON DELETE {on_delete}'
_DROP_FK_TEMPLATE = 'ALTER TABLE {table_name} DROP FOREIGN KEY {fk_name}'
def __init__(self, client: "BaseDBAsyncClient", model: "Type[Model]"): def __init__(self, client: "BaseDBAsyncClient"):
self.model = model self.client = client
self.schema_generator = self.schema_generator_cls(client) self.schema_generator = self.schema_generator_cls(client)
def create_table(self): def create_table(self, model: "Type[Model]"):
return self.schema_generator._get_table_sql(self.model, True)['table_creation_string'] return self.schema_generator._get_table_sql(model, True)['table_creation_string']
def drop_table(self): def drop_table(self, model: "Type[Model]"):
return f'drop table {self.model._meta.db_table}' return self._DROP_TABLE_TEMPLATE.format(
table_name=model._meta.db_table
)
def add_column(self): def add_column(self, model: "Type[Model]", field_object: Field):
raise NotImplementedError() db_table = model._meta.db_table
default = field_object.default
db_column = field_object.model_field_name
auto_now_add = getattr(field_object, "auto_now_add", False)
auto_now = getattr(field_object, "auto_now", False)
if default is not None or auto_now or auto_now_add:
if callable(default) or isinstance(field_object, (UUIDField, TextField, JSONField)):
default = ""
else:
default = field_object.to_db_value(default, model)
try:
default = self.schema_generator._column_default_generator(
db_table,
db_column,
self.schema_generator._escape_default_value(default),
auto_now_add,
auto_now,
)
except NotImplementedError:
default = ""
else:
default = ""
return self._ADD_COLUMN_TEMPLATE.format(
table_name=db_table,
column=self.schema_generator._create_string(
db_column=field_object.model_field_name,
field_type=field_object.get_for_dialect(self.DIALECT, "SQL_TYPE"),
nullable="NOT NULL" if not field_object.null else "",
unique="UNIQUE" if field_object.unique else "",
comment=self.schema_generator._column_comment_generator(
table=db_table, column=field_object.model_field_name, comment=field_object.description
)
if field_object.description else "",
is_primary_key=field_object.pk,
default=default,
)
)
def drop_column(self): def drop_column(self, model: "Type[Model]", column_name: str):
raise NotImplementedError() return self._DROP_COLUMN_TEMPLATE.format(
table_name=model._meta.db_table,
column_name=column_name
)
def add_index(self): def add_index(self, model: "Type[Model]", field_names: List[str], unique=False):
raise NotImplementedError() return self._ADD_INDEX_TEMPLATE.format(
unique='UNIQUE' if unique else '',
index_name=self.schema_generator._generate_index_name("idx" if not unique else "uid", model,
field_names),
table_name=model._meta.db_table,
column_names=", ".join([self.schema_generator.quote(f) for f in field_names]),
)
def drop_index(self): def drop_index(self, model: "Type[Model]", field_names: List[str], unique=False):
raise NotImplementedError() return self._DROP_INDEX_TEMPLATE.format(
index_name=self.schema_generator._generate_index_name("idx" if not unique else "uid", model,
field_names),
table_name=model._meta.db_table,
)
def add_fk(self): def add_fk(self, model: "Type[Model]", field: ForeignKeyFieldInstance):
raise NotImplementedError() db_table = model._meta.db_table
to_field_name = field.to_field_instance.source_field
if not to_field_name:
to_field_name = field.to_field_instance.model_field_name
def drop_fk(self): fk_name = self.schema_generator._generate_fk_name(
raise NotImplementedError() from_table=db_table,
from_field=field.model_field_name,
to_table=field.related_model._meta.db_table,
to_field=to_field_name
)
return self._ADD_FK_TEMPLATE.format(
table_name=db_table,
fk_name=fk_name,
db_column=field.model_field_name,
table=field.related_model._meta.db_table,
field=to_field_name,
on_delete=field.on_delete,
)
def drop_fk(self, model: "Type[Model]", field: ForeignKeyFieldInstance):
to_field_name = field.to_field_instance.source_field
if not to_field_name:
to_field_name = field.to_field_instance.model_field_name
return self._DROP_FK_TEMPLATE.format(
table_name=model._meta.db_table,
fk_name=self.schema_generator._generate_fk_name(
from_table=model._meta.db_table,
from_field=field.model_field_name,
to_table=field.related_model._meta.db_table,
to_field=to_field_name
)
)

View File

@ -5,24 +5,4 @@ from alice.backends import DDL
class MysqlDDL(DDL): class MysqlDDL(DDL):
schema_generator_cls = MySQLSchemaGenerator schema_generator_cls = MySQLSchemaGenerator
DIALECT = "mysql"
def drop_table(self):
pass
def add_column(self):
pass
def drop_column(self):
pass
def add_index(self):
pass
def drop_index(self):
pass
def add_fk(self):
pass
def drop_fk(self):
pass

55
poetry.lock generated
View File

@ -67,6 +67,19 @@ idna = ["idna (>=2.1)"]
pep8test = ["flake8", "flake8-import-order", "pep8-naming"] pep8test = ["flake8", "flake8-import-order", "pep8-naming"]
test = ["pytest (>=3.6.0,<3.9.0 || >3.9.0,<3.9.1 || >3.9.1,<3.9.2 || >3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,<3.79.2 || >3.79.2)"] test = ["pytest (>=3.6.0,<3.9.0 || >3.9.0,<3.9.1 || >3.9.1,<3.9.2 || >3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,<3.79.2 || >3.79.2)"]
[[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]] [[package]]
category = "main" category = "main"
description = "Simple module to parse ISO 8601 dates" description = "Simple module to parse ISO 8601 dates"
@ -76,6 +89,22 @@ optional = false
python-versions = "*" python-versions = "*"
version = "0.1.12" version = "0.1.12"
[[package]]
category = "dev"
description = "McCabe checker, plugin for flake8"
name = "mccabe"
optional = false
python-versions = "*"
version = "0.6.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]] [[package]]
category = "main" category = "main"
description = "C parser in Python" description = "C parser in Python"
@ -84,6 +113,14 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "2.20" version = "2.20"
[[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]] [[package]]
category = "main" category = "main"
description = "Pure Python MySQL Driver" description = "Pure Python MySQL Driver"
@ -154,7 +191,7 @@ python-versions = "*"
version = "3.7.4.2" version = "3.7.4.2"
[metadata] [metadata]
content-hash = "e8e49dcc243fcd3a31fd60ee5a637af7b265c4fea2078a48d2bd7f10794fe323" content-hash = "0b4c3c6eb6ed2e84b03745542ebc42dc53e6e5466bea0c4eee991bcd063a6ef3"
python-versions = "^3.8" python-versions = "^3.8"
[metadata.files] [metadata.files]
@ -224,15 +261,31 @@ cryptography = [
{file = "cryptography-2.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:1dfa985f62b137909496e7fc182dac687206d8d089dd03eaeb28ae16eec8e7d5"}, {file = "cryptography-2.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:1dfa985f62b137909496e7fc182dac687206d8d089dd03eaeb28ae16eec8e7d5"},
{file = "cryptography-2.9.2.tar.gz", hash = "sha256:a0c30272fb4ddda5f5ffc1089d7405b7a71b0b0f51993cb4e5dbb4590b2fc229"}, {file = "cryptography-2.9.2.tar.gz", hash = "sha256:a0c30272fb4ddda5f5ffc1089d7405b7a71b0b0f51993cb4e5dbb4590b2fc229"},
] ]
flake8 = [
{file = "flake8-3.8.1-py2.py3-none-any.whl", hash = "sha256:6c1193b0c3f853ef763969238f6c81e9e63ace9d024518edc020d5f1d6d93195"},
{file = "flake8-3.8.1.tar.gz", hash = "sha256:ea6623797bf9a52f4c9577d780da0bb17d65f870213f7b5bcc9fca82540c31d5"},
]
iso8601 = [ iso8601 = [
{file = "iso8601-0.1.12-py2.py3-none-any.whl", hash = "sha256:210e0134677cc0d02f6028087fee1df1e1d76d372ee1db0bf30bf66c5c1c89a3"}, {file = "iso8601-0.1.12-py2.py3-none-any.whl", hash = "sha256:210e0134677cc0d02f6028087fee1df1e1d76d372ee1db0bf30bf66c5c1c89a3"},
{file = "iso8601-0.1.12-py3-none-any.whl", hash = "sha256:bbbae5fb4a7abfe71d4688fd64bff70b91bbd74ef6a99d964bab18f7fdf286dd"}, {file = "iso8601-0.1.12-py3-none-any.whl", hash = "sha256:bbbae5fb4a7abfe71d4688fd64bff70b91bbd74ef6a99d964bab18f7fdf286dd"},
{file = "iso8601-0.1.12.tar.gz", hash = "sha256:49c4b20e1f38aa5cf109ddcd39647ac419f928512c869dc01d5c7098eddede82"}, {file = "iso8601-0.1.12.tar.gz", hash = "sha256:49c4b20e1f38aa5cf109ddcd39647ac419f928512c869dc01d5c7098eddede82"},
] ]
mccabe = [
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
]
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 = [ pycparser = [
{file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"},
{file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"},
] ]
pyflakes = [
{file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"},
{file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"},
]
pymysql = [ pymysql = [
{file = "PyMySQL-0.9.2-py2.py3-none-any.whl", hash = "sha256:95f057328357e0e13a30e67857a8c694878b0175797a9a203ee7adbfb9b1ec5f"}, {file = "PyMySQL-0.9.2-py2.py3-none-any.whl", hash = "sha256:95f057328357e0e13a30e67857a8c694878b0175797a9a203ee7adbfb9b1ec5f"},
{file = "PyMySQL-0.9.2.tar.gz", hash = "sha256:9ec760cbb251c158c19d6c88c17ca00a8632bac713890e465b2be01fdc30713f"}, {file = "PyMySQL-0.9.2.tar.gz", hash = "sha256:9ec760cbb251c158c19d6c88c17ca00a8632bac713890e465b2be01fdc30713f"},

View File

@ -12,6 +12,7 @@ aiomysql = "*"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
taskipy = "*" taskipy = "*"
asynctest = "*" asynctest = "*"
flake8 = "*"
[build-system] [build-system]
requires = ["poetry>=0.12"] requires = ["poetry>=0.12"]

View File

@ -1,68 +0,0 @@
import datetime
from enum import IntEnum
from tortoise import fields, Model
class ProductType(IntEnum):
article = 1
page = 2
class PermissionAction(IntEnum):
create = 1
delete = 2
update = 3
read = 4
class Status(IntEnum):
on = 1
off = 0
class User(Model):
username = fields.CharField(max_length=20, unique=True)
password = fields.CharField(max_length=200)
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}'
class Category(Model):
slug = fields.CharField(max_length=200)
name = fields.CharField(max_length=200)
created_at = fields.DatetimeField(auto_now_add=True)
def __str__(self):
return f'{self.pk}#{self.name}'
class Product(Model):
categories = fields.ManyToManyField('models.Category')
name = fields.CharField(max_length=50)
view_num = fields.IntField(description='View Num')
sort = fields.IntField()
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}'
class Config(Model):
label = fields.CharField(max_length=200)
key = fields.CharField(max_length=20)
value = fields.JSONField()
status: Status = fields.IntEnumField(Status, default=Status.on)
def __str__(self):
return f'{self.pk}#{self.label}'

View File

View File

@ -2,28 +2,26 @@ from asynctest import TestCase
from tortoise import Tortoise from tortoise import Tortoise
from alice.backends.mysql import MysqlDDL from alice.backends.mysql import MysqlDDL
from tests import User from tests.models import Category
TORTOISE_ORM = { TORTOISE_ORM = {
'connections': { 'connections': {
'default': 'mysql://root:123456@127.0.0.1:3306/fastapi-admin' 'default': 'mysql://root:123456@127.0.0.1:3306/test'
}, },
'apps': { 'apps': {
'models': { 'models': {
'models': ['tests'], 'models': ['tests.models'],
'default_connection': 'default', 'default_connection': 'default',
} }
} }
} }
class TestMysql(TestCase): class DBTestCase(TestCase):
async def setUp(self) -> None: async def setUp(self) -> None:
await Tortoise.init(config=TORTOISE_ORM) await Tortoise.init(config=TORTOISE_ORM)
self.client = Tortoise.get_connection('default')
async def test_create_table(self): self.ddl = MysqlDDL(self.client)
ddl = MysqlDDL(Tortoise.get_connection('default'), User)
print(ddl.create_table())
async def tearDown(self) -> None: async def tearDown(self) -> None:
await Tortoise.close_connections() await Tortoise.close_connections()

View File

@ -0,0 +1,53 @@
from tests.backends.mysql import DBTestCase
from tests.models import Category, User
class TestDDL(DBTestCase):
def test_create_table(self):
ret = self.ddl.create_table(Category)
self.assertEqual(
ret, """CREATE TABLE IF NOT EXISTS `category` (
`id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
`slug` VARCHAR(200) NOT NULL,
`name` VARCHAR(200) NOT NULL,
`created_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`user_id` INT NOT NULL COMMENT 'User',
CONSTRAINT `fk_category_user_e2e3874c` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE
) CHARACTER SET utf8mb4;""")
def test_drop_table(self):
ret = self.ddl.drop_table(Category)
self.assertEqual(ret, "DROP TABLE category IF EXISTS")
def test_add_column(self):
ret = self.ddl.add_column(Category, Category._meta.fields_map.get('name'))
self.assertEqual(ret, "ALTER TABLE category ADD `name` VARCHAR(200) NOT NULL")
def test_drop_column(self):
ret = self.ddl.drop_column(Category, 'name')
self.assertEqual(ret, "ALTER TABLE category DROP COLUMN name")
def test_add_index(self):
ret = self.ddl.add_index(Category, ['name'])
self.assertEqual(ret, "ALTER TABLE category ADD INDEX idx_category_name_8b0cb9 (`name`)")
ret = self.ddl.add_index(Category, ['name'], True)
self.assertEqual(ret, "ALTER TABLE category ADD UNIQUE INDEX uid_category_name_8b0cb9 (`name`)")
def test_drop_index(self):
ret = self.ddl.drop_index(Category, ['name'])
self.assertEqual(ret, "ALTER TABLE category DROP INDEX idx_category_name_8b0cb9")
ret = self.ddl.drop_index(Category, ['name'], True)
self.assertEqual(ret, "ALTER TABLE category DROP INDEX uid_category_name_8b0cb9")
def test_add_fk(self):
ret = self.ddl.add_fk(Category, Category._meta.fields_map.get('user'))
self.assertEqual(ret,
"ALTER TABLE category ADD CONSTRAINT `fk_category_user_366ffa6f` FOREIGN KEY (`user`) REFERENCES `user` (`id`) ON DELETE CASCADE")
def test_drop_fk(self):
ret = self.ddl.drop_fk(Category, Category._meta.fields_map.get('user'))
self.assertEqual(ret, "ALTER TABLE category DROP FOREIGN KEY fk_category_user_366ffa6f")
async def test_aa(self):
user = await User.get(username='test')
await user.save()

70
tests/models.py Normal file
View File

@ -0,0 +1,70 @@
import datetime
from enum import IntEnum
from tortoise import fields, Model
class ProductType(IntEnum):
article = 1
page = 2
class PermissionAction(IntEnum):
create = 1
delete = 2
update = 3
read = 4
class Status(IntEnum):
on = 1
off = 0
class User(Model):
username = fields.CharField(max_length=20, unique=True)
password = fields.CharField(max_length=200)
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)
updated_at = fields.DatetimeField(auto_now=True)
def __str__(self):
return f'{self.pk}#{self.username}'
class Category(Model):
slug = fields.CharField(max_length=200)
name = fields.CharField(max_length=200)
user = fields.ForeignKeyField('models.User', description='User')
created_at = fields.DatetimeField(auto_now_add=True)
def __str__(self):
return f'{self.pk}#{self.name}'
class Product(Model):
categories = fields.ManyToManyField('models.Category')
name = fields.CharField(max_length=50)
view_num = fields.IntField(description='View Num')
sort = fields.IntField()
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}'
class Config(Model):
label = fields.CharField(max_length=200)
key = fields.CharField(max_length=20)
value = fields.JSONField()
status: Status = fields.IntEnumField(Status, default=Status.on)
def __str__(self):
return f'{self.pk}#{self.label}'