This commit is contained in:
long2ice 2020-05-13 23:16:27 +08:00
parent a5a5de529b
commit d385647fba
9 changed files with 185 additions and 122 deletions

3
.gitignore vendored
View File

@ -140,4 +140,5 @@ dmypy.json
# Cython debug symbols # Cython debug symbols
cython_debug/ cython_debug/
.idea .idea
migrations

View File

@ -3,8 +3,13 @@ import os
import sys import sys
from enum import Enum from enum import Enum
import click import asyncclick as click
from click import BadParameter, ClickException from asyncclick import BadParameter, ClickException
from tortoise import Tortoise, generate_schema_for_client
from alice.backends.mysql import MysqlDDL
from alice.migrate import Migrate
from alice.utils import get_app_connection
sys.path.append(os.getcwd()) sys.path.append(os.getcwd())
@ -15,15 +20,15 @@ class Color(str, Enum):
@click.group(context_settings={'help_option_names': ['-h', '--help']}) @click.group(context_settings={'help_option_names': ['-h', '--help']})
@click.option('-c', '--config', default='settings', @click.option('-c', '--config', default='settings', show_default=True,
help='Tortoise-ORM config module, will read config variable from it, default is `settings`.') help='Tortoise-ORM config module, will read config variable from it, default is `settings`.')
@click.option('-t', '--tortoise-orm', default='TORTOISE_ORM', @click.option('-t', '--tortoise-orm', default='TORTOISE_ORM', show_default=True,
help='Tortoise-ORM config dict variable, default is `TORTOISE_ORM`.') help='Tortoise-ORM config dict variable, default is `TORTOISE_ORM`.')
@click.option('-l', '--location', default='./migrations', @click.option('-l', '--location', default='./migrations', show_default=True,
help='Migrate store location, default is `./migrations`.') help='Migrate store location, default is `./migrations`.')
@click.option('--connection', default='default', help='Tortoise-ORM connection name, default is `default`.') @click.option('-a', '--app', default='models', show_default=True, help='Tortoise-ORM app name, default is `models`.')
@click.pass_context @click.pass_context
def cli(ctx, config, tortoise_orm, location, connection): async def cli(ctx, config, tortoise_orm, location, app):
ctx.ensure_object(dict) ctx.ensure_object(dict)
try: try:
config_module = importlib.import_module(config) config_module = importlib.import_module(config)
@ -31,10 +36,16 @@ def cli(ctx, config, tortoise_orm, location, connection):
if not config: if not config:
raise BadParameter(param_hint=['--config'], raise BadParameter(param_hint=['--config'],
message=f'Can\'t get "{tortoise_orm}" from module "{config_module}"') message=f'Can\'t get "{tortoise_orm}" from module "{config_module}"')
await Tortoise.init(config=config)
ctx.obj['config'] = config ctx.obj['config'] = config
ctx.obj['location'] = location ctx.obj['location'] = location
if connection not in config.get('connections').keys(): ctx.obj['app'] = app
raise BadParameter(param_hint=['--connection'], message=f'No connection found in "{config}"')
if app not in config.get('apps').keys():
raise BadParameter(param_hint=['--app'], message=f'No app found in "{config}"')
except ModuleNotFoundError: except ModuleNotFoundError:
raise BadParameter(param_hint=['--tortoise-orm'], message=f'No module named "{config}"') raise BadParameter(param_hint=['--tortoise-orm'], message=f'No module named "{config}"')
@ -43,6 +54,17 @@ def cli(ctx, config, tortoise_orm, location, connection):
@click.pass_context @click.pass_context
def migrate(ctx): def migrate(ctx):
config = ctx.obj['config'] config = ctx.obj['config']
location = ctx.obj['location']
app = ctx.obj['app']
old_models = Migrate.read_old_models(app, location)
print(old_models)
new_models = Tortoise.apps.get(app)
print(new_models)
ret = Migrate(MysqlDDL(get_app_connection(config, app))).diff_models(old_models, new_models)
print(ret)
@cli.command() @cli.command()
@ -58,26 +80,38 @@ def downgrade():
@cli.command() @cli.command()
@click.option('--safe', is_flag=True, default=True,
help='When set to true, creates the table only when it does not already exist..', show_default=True)
@click.pass_context @click.pass_context
def initdb(): async def initdb(ctx, safe):
pass location = ctx.obj['location']
config = ctx.obj['config']
app = ctx.obj['app']
await generate_schema_for_client(get_app_connection(config, app), safe)
Migrate.write_old_models(app, location)
click.secho(f'Success initdb for app `{app}`', fg=Color.green)
@cli.command() @cli.command()
@click.option('--overwrite', type=bool, default=False, help='Overwrite old_models.py.') @click.option('--overwrite', is_flag=True, default=False, help=f'Overwrite {Migrate.old_models}.', show_default=True)
@click.pass_context @click.pass_context
def init(ctx, overwrite): def init(ctx, overwrite):
location = ctx.obj['location'] location = ctx.obj['location']
config = ctx.obj['config'] app = ctx.obj['app']
if not os.path.isdir(location) or overwrite: if not os.path.isdir(location):
os.mkdir(location) os.mkdir(location)
connections = config.get('connections').keys() dirname = os.path.join(location, app)
for connection in connections: if not os.path.isdir(dirname):
dirname = os.path.join(location, connection) os.mkdir(dirname)
if not os.path.isdir(dirname): click.secho(f'Success create migrate location {dirname}', fg=Color.green)
os.mkdir(dirname) if overwrite:
click.secho(f'Success create migrate location {dirname}', fg=Color.green) Migrate.write_old_models(app, location)
if overwrite:
pass
else: else:
raise ClickException('Already inited') raise ClickException('Already inited')
if __name__ == '__main__':
cli(_anyio_backend='asyncio')

View File

@ -1,8 +1,12 @@
import importlib import importlib
import inspect import inspect
import os
from copy import deepcopy
import dill
from typing import List, Type, Dict from typing import List, Type, Dict
from tortoise import Model, ForeignKeyFieldInstance from tortoise import Model, ForeignKeyFieldInstance, Tortoise
from tortoise.fields import Field from tortoise.fields import Field
from alice.backends import DDL from alice.backends import DDL
@ -11,11 +15,30 @@ from alice.backends import DDL
class Migrate: class Migrate:
operators: List operators: List
ddl: DDL ddl: DDL
old_models = 'old_models.pickle'
def __init__(self, ddl: DDL): def __init__(self, ddl: DDL):
self.operators = [] self.operators = []
self.ddl = ddl self.ddl = ddl
@staticmethod
def write_old_models(app, location):
ret = Tortoise.apps.get(app)
old_models = {}
for k, v in ret.items():
old_models[k] = deepcopy(v)
dirname = os.path.join(location, app)
with open(os.path.join(dirname, Migrate.old_models), 'wb') as f:
dill.dump(old_models, f, )
@staticmethod
def read_old_models(app, location):
dirname = os.path.join(location, app)
with open(os.path.join(dirname, Migrate.old_models), 'rb') as f:
return dill.load(f, )
def diff_models_module(self, old_models_module, new_models_module): def diff_models_module(self, old_models_module, new_models_module):
old_module = importlib.import_module(old_models_module) old_module = importlib.import_module(old_models_module)
old_models = {} old_models = {}

View File

@ -1,17 +1,11 @@
import re from tortoise import Tortoise
def cp_models(old_model_file, new_model_file, new_app): def get_app_connection(config: dict, app: str):
""" """
cp models file to old_models.py and rename model app get tortoise connection by app
:param old_app: :param config:
:param new_app: :param app:
:param old_model_file: :return:
:param new_model_file:
:return:r
""" """
pattern = r'(ManyToManyField|ForeignKeyField|OneToOneField)\((model_name)?(\"|\')(?P<app>\w+).+\)' return Tortoise.get_connection(config.get('apps').get(app).get('default_connection')),
with open(old_model_file, 'r') as f:
content = f.read()
ret = re.sub(pattern, rf'{new_app} \g<app>', content)
print(ret)

92
poetry.lock generated
View File

@ -20,6 +20,47 @@ optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
version = "0.13.0" version = "0.13.0"
[[package]]
category = "main"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
name = "anyio"
optional = false
python-versions = ">=3.5.3"
version = "1.3.0"
[package.dependencies]
async-generator = "*"
sniffio = ">=1.1"
[package.extras]
curio = ["curio (>=0.9)"]
doc = ["sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"]
test = ["coverage (>=4.5)", "hypothesis (>=4.0)", "pytest (>=3.7.2)", "uvloop"]
trio = ["trio (>=0.12)"]
[[package]]
category = "main"
description = "Async generators and context managers for Python 3.5+"
name = "async-generator"
optional = false
python-versions = ">=3.5"
version = "1.10"
[[package]]
category = "main"
description = "A simple anyio-compatible fork of Click, for powerful command line utilities."
name = "asyncclick"
optional = false
python-versions = ">=3.6"
version = "7.0.9"
[package.dependencies]
anyio = "*"
[package.extras]
dev = ["coverage", "pytest-runner", "pytest-trio", "pytest (>=3)", "sphinx", "tox"]
docs = ["sphinx"]
[[package]] [[package]]
category = "dev" category = "dev"
description = "Enhance the standard unittest package with features for testing asyncio libraries" description = "Enhance the standard unittest package with features for testing asyncio libraries"
@ -48,14 +89,6 @@ optional = false
python-versions = "*" python-versions = "*"
version = "2.1.3" version = "2.1.3"
[[package]]
category = "main"
description = "Composable command line interface toolkit"
name = "click"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "7.1.2"
[[package]] [[package]]
category = "main" category = "main"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
@ -75,6 +108,17 @@ 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 = "main"
description = "serialize all of python"
name = "dill"
optional = false
python-versions = ">=2.6, !=3.0.*"
version = "0.3.1.1"
[package.extras]
graph = ["objgraph (>=1.7.2)"]
[[package]] [[package]]
category = "dev" category = "dev"
description = "the modular source code checker: pep8 pyflakes and co" description = "the modular source code checker: pep8 pyflakes and co"
@ -156,6 +200,14 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
version = "1.14.0" version = "1.14.0"
[[package]]
category = "main"
description = "Sniff out which async library your code is running under"
name = "sniffio"
optional = false
python-versions = ">=3.5"
version = "1.1.0"
[[package]] [[package]]
category = "dev" category = "dev"
description = "tasks runner for python projects" description = "tasks runner for python projects"
@ -203,7 +255,7 @@ python-versions = "*"
version = "3.7.4.2" version = "3.7.4.2"
[metadata] [metadata]
content-hash = "708876857d4653fd45cb251e9a1689c4158966f42da2efca1e8167becef89837" content-hash = "4809b238c12841eb28a6517843828716f207e9ed41b273bb681ae7a831e34af4"
python-versions = "^3.8" python-versions = "^3.8"
[metadata.files] [metadata.files]
@ -215,6 +267,17 @@ aiosqlite = [
{file = "aiosqlite-0.13.0-py3-none-any.whl", hash = "sha256:50688c40632ae249f986ab3ae2c66a45c0535b84a5d4aae0e0be572b5fed6909"}, {file = "aiosqlite-0.13.0-py3-none-any.whl", hash = "sha256:50688c40632ae249f986ab3ae2c66a45c0535b84a5d4aae0e0be572b5fed6909"},
{file = "aiosqlite-0.13.0.tar.gz", hash = "sha256:6e92961ae9e606b43b05e29b129e346b29e400fcbd63e3c0c564d89230257645"}, {file = "aiosqlite-0.13.0.tar.gz", hash = "sha256:6e92961ae9e606b43b05e29b129e346b29e400fcbd63e3c0c564d89230257645"},
] ]
anyio = [
{file = "anyio-1.3.0-py3-none-any.whl", hash = "sha256:db2c3d21576870b95d4fd0b8f4a0f9c64057f777c578f3a8127179a17c8c067e"},
{file = "anyio-1.3.0.tar.gz", hash = "sha256:7deae0315dd10aa41c21528b83352e4b52f44e6153a21081a3d1cd8c03728e46"},
]
async-generator = [
{file = "async_generator-1.10-py3-none-any.whl", hash = "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b"},
{file = "async_generator-1.10.tar.gz", hash = "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"},
]
asyncclick = [
{file = "asyncclick-7.0.9.tar.gz", hash = "sha256:62cebf3eca36d973802e2dd521ca1db11c5bf4544e9795e093d1a53cb688a8c2"},
]
asynctest = [ asynctest = [
{file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"}, {file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"},
{file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"}, {file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"},
@ -252,10 +315,6 @@ cffi = [
ciso8601 = [ ciso8601 = [
{file = "ciso8601-2.1.3.tar.gz", hash = "sha256:bdbb5b366058b1c87735603b23060962c439ac9be66f1ae91e8c7dbd7d59e262"}, {file = "ciso8601-2.1.3.tar.gz", hash = "sha256:bdbb5b366058b1c87735603b23060962c439ac9be66f1ae91e8c7dbd7d59e262"},
] ]
click = [
{file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
{file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},
]
cryptography = [ cryptography = [
{file = "cryptography-2.9.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:daf54a4b07d67ad437ff239c8a4080cfd1cc7213df57d33c97de7b4738048d5e"}, {file = "cryptography-2.9.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:daf54a4b07d67ad437ff239c8a4080cfd1cc7213df57d33c97de7b4738048d5e"},
{file = "cryptography-2.9.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:3b3eba865ea2754738616f87292b7f29448aec342a7c720956f8083d252bf28b"}, {file = "cryptography-2.9.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:3b3eba865ea2754738616f87292b7f29448aec342a7c720956f8083d252bf28b"},
@ -277,6 +336,9 @@ 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"},
] ]
dill = [
{file = "dill-0.3.1.1.tar.gz", hash = "sha256:42d8ef819367516592a825746a18073ced42ca169ab1f5f4044134703e7a049c"},
]
flake8 = [ flake8 = [
{file = "flake8-3.8.1-py2.py3-none-any.whl", hash = "sha256:6c1193b0c3f853ef763969238f6c81e9e63ace9d024518edc020d5f1d6d93195"}, {file = "flake8-3.8.1-py2.py3-none-any.whl", hash = "sha256:6c1193b0c3f853ef763969238f6c81e9e63ace9d024518edc020d5f1d6d93195"},
{file = "flake8-3.8.1.tar.gz", hash = "sha256:ea6623797bf9a52f4c9577d780da0bb17d65f870213f7b5bcc9fca82540c31d5"}, {file = "flake8-3.8.1.tar.gz", hash = "sha256:ea6623797bf9a52f4c9577d780da0bb17d65f870213f7b5bcc9fca82540c31d5"},
@ -313,6 +375,10 @@ six = [
{file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"}, {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.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"},
] ]
sniffio = [
{file = "sniffio-1.1.0-py3-none-any.whl", hash = "sha256:20ed6d5b46f8ae136d00b9dcb807615d83ed82ceea6b2058cecb696765246da5"},
{file = "sniffio-1.1.0.tar.gz", hash = "sha256:8e3810100f69fe0edd463d02ad407112542a11ffdc29f67db2bf3771afb87a21"},
]
taskipy = [ taskipy = [
{file = "taskipy-1.2.1-py3-none-any.whl", hash = "sha256:99bdaf5b19791c2345806847147e0fc2d28e1ac9446058def5a8b6b3fc9f23e2"}, {file = "taskipy-1.2.1-py3-none-any.whl", hash = "sha256:99bdaf5b19791c2345806847147e0fc2d28e1ac9446058def5a8b6b3fc9f23e2"},
{file = "taskipy-1.2.1.tar.gz", hash = "sha256:5eb2c3b1606c896c7fa799848e71e8883b880759224958d07ba760e5db263175"}, {file = "taskipy-1.2.1.tar.gz", hash = "sha256:5eb2c3b1606c896c7fa799848e71e8883b880759224958d07ba760e5db263175"},

View File

@ -8,7 +8,8 @@ authors = ["long2ice <long2ice@gmail.com>"]
python = "^3.8" python = "^3.8"
tortoise-orm = {git = "https://github.com/tortoise/tortoise-orm.git", branch = "develop"} tortoise-orm = {git = "https://github.com/tortoise/tortoise-orm.git", branch = "develop"}
aiomysql = "*" aiomysql = "*"
click = "*" asyncclick = "*"
dill = "*"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
taskipy = "*" taskipy = "*"

View File

@ -1,13 +1,17 @@
aiomysql==0.0.20 aiomysql==0.0.20
aiosqlite==0.13.0 aiosqlite==0.13.0
anyio==1.3.0
async-generator==1.10
asyncclick==7.0.9
cffi==1.14.0 cffi==1.14.0
ciso8601==2.1.3; sys_platform != "win32" and implementation_name == "cpython" ciso8601==2.1.3; sys_platform != "win32" and implementation_name == "cpython"
click==7.1.2
cryptography==2.9.2 cryptography==2.9.2
dill==0.3.1.1
iso8601==0.1.12; sys_platform == "win32" or implementation_name != "cpython" iso8601==0.1.12; sys_platform == "win32" or implementation_name != "cpython"
pycparser==2.20 pycparser==2.20
pymysql==0.9.2 pymysql==0.9.2
pypika==0.37.6 pypika==0.37.6
six==1.14.0 six==1.14.0
tortoise-orm==0.16.10 sniffio==1.1.0
-e git+https://github.com/tortoise/tortoise-orm.git@72f84f0848dc68041157f03e60cd1c92b0ee5137#egg=tortoise-orm
typing-extensions==3.7.4.2 typing-extensions==3.7.4.2

View File

@ -1,69 +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='')
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('new_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('new_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}'

9
tests/test_utils.py Normal file
View File

@ -0,0 +1,9 @@
from unittest import TestCase
from alice.utils import cp_models
class TestUtils(TestCase):
def test_cp_models(self):
ret = cp_models('models.py', 'new_models.py', 'new_models')
print(ret)