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

1
.gitignore vendored
View File

@ -141,3 +141,4 @@ dmypy.json
cython_debug/
.idea
migrations

View File

@ -3,8 +3,13 @@ import os
import sys
from enum import Enum
import click
from click import BadParameter, ClickException
import asyncclick as click
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())
@ -15,15 +20,15 @@ class Color(str, Enum):
@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`.')
@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`.')
@click.option('-l', '--location', default='./migrations',
@click.option('-l', '--location', default='./migrations', show_default=True,
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
def cli(ctx, config, tortoise_orm, location, connection):
async def cli(ctx, config, tortoise_orm, location, app):
ctx.ensure_object(dict)
try:
config_module = importlib.import_module(config)
@ -31,10 +36,16 @@ def cli(ctx, config, tortoise_orm, location, connection):
if not config:
raise BadParameter(param_hint=['--config'],
message=f'Can\'t get "{tortoise_orm}" from module "{config_module}"')
await Tortoise.init(config=config)
ctx.obj['config'] = config
ctx.obj['location'] = location
if connection not in config.get('connections').keys():
raise BadParameter(param_hint=['--connection'], message=f'No connection found in "{config}"')
ctx.obj['app'] = app
if app not in config.get('apps').keys():
raise BadParameter(param_hint=['--app'], message=f'No app found in "{config}"')
except ModuleNotFoundError:
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
def migrate(ctx):
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()
@ -58,26 +80,38 @@ def downgrade():
@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
def initdb():
pass
async def initdb(ctx, safe):
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()
@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
def init(ctx, overwrite):
location = ctx.obj['location']
config = ctx.obj['config']
if not os.path.isdir(location) or overwrite:
app = ctx.obj['app']
if not os.path.isdir(location):
os.mkdir(location)
connections = config.get('connections').keys()
for connection in connections:
dirname = os.path.join(location, connection)
if not os.path.isdir(dirname):
os.mkdir(dirname)
click.secho(f'Success create migrate location {dirname}', fg=Color.green)
if overwrite:
pass
dirname = os.path.join(location, app)
if not os.path.isdir(dirname):
os.mkdir(dirname)
click.secho(f'Success create migrate location {dirname}', fg=Color.green)
if overwrite:
Migrate.write_old_models(app, location)
else:
raise ClickException('Already inited')
if __name__ == '__main__':
cli(_anyio_backend='asyncio')

View File

@ -1,8 +1,12 @@
import importlib
import inspect
import os
from copy import deepcopy
import dill
from typing import List, Type, Dict
from tortoise import Model, ForeignKeyFieldInstance
from tortoise import Model, ForeignKeyFieldInstance, Tortoise
from tortoise.fields import Field
from alice.backends import DDL
@ -11,11 +15,30 @@ from alice.backends import DDL
class Migrate:
operators: List
ddl: DDL
old_models = 'old_models.pickle'
def __init__(self, ddl: DDL):
self.operators = []
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):
old_module = importlib.import_module(old_models_module)
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
:param old_app:
:param new_app:
:param old_model_file:
:param new_model_file:
:return:r
get tortoise connection by app
:param config:
:param app:
:return:
"""
pattern = r'(ManyToManyField|ForeignKeyField|OneToOneField)\((model_name)?(\"|\')(?P<app>\w+).+\)'
with open(old_model_file, 'r') as f:
content = f.read()
ret = re.sub(pattern, rf'{new_app} \g<app>', content)
print(ret)
return Tortoise.get_connection(config.get('apps').get(app).get('default_connection')),

92
poetry.lock generated
View File

@ -20,6 +20,47 @@ optional = false
python-versions = ">=3.6"
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]]
category = "dev"
description = "Enhance the standard unittest package with features for testing asyncio libraries"
@ -48,14 +89,6 @@ optional = false
python-versions = "*"
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]]
category = "main"
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"]
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]]
category = "dev"
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.*"
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]]
category = "dev"
description = "tasks runner for python projects"
@ -203,7 +255,7 @@ python-versions = "*"
version = "3.7.4.2"
[metadata]
content-hash = "708876857d4653fd45cb251e9a1689c4158966f42da2efca1e8167becef89837"
content-hash = "4809b238c12841eb28a6517843828716f207e9ed41b273bb681ae7a831e34af4"
python-versions = "^3.8"
[metadata.files]
@ -215,6 +267,17 @@ aiosqlite = [
{file = "aiosqlite-0.13.0-py3-none-any.whl", hash = "sha256:50688c40632ae249f986ab3ae2c66a45c0535b84a5d4aae0e0be572b5fed6909"},
{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 = [
{file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"},
{file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"},
@ -252,10 +315,6 @@ cffi = [
ciso8601 = [
{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 = [
{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"},
@ -277,6 +336,9 @@ cryptography = [
{file = "cryptography-2.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:1dfa985f62b137909496e7fc182dac687206d8d089dd03eaeb28ae16eec8e7d5"},
{file = "cryptography-2.9.2.tar.gz", hash = "sha256:a0c30272fb4ddda5f5ffc1089d7405b7a71b0b0f51993cb4e5dbb4590b2fc229"},
]
dill = [
{file = "dill-0.3.1.1.tar.gz", hash = "sha256:42d8ef819367516592a825746a18073ced42ca169ab1f5f4044134703e7a049c"},
]
flake8 = [
{file = "flake8-3.8.1-py2.py3-none-any.whl", hash = "sha256:6c1193b0c3f853ef763969238f6c81e9e63ace9d024518edc020d5f1d6d93195"},
{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.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 = [
{file = "taskipy-1.2.1-py3-none-any.whl", hash = "sha256:99bdaf5b19791c2345806847147e0fc2d28e1ac9446058def5a8b6b3fc9f23e2"},
{file = "taskipy-1.2.1.tar.gz", hash = "sha256:5eb2c3b1606c896c7fa799848e71e8883b880759224958d07ba760e5db263175"},

View File

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

View File

@ -1,13 +1,17 @@
aiomysql==0.0.20
aiosqlite==0.13.0
anyio==1.3.0
async-generator==1.10
asyncclick==7.0.9
cffi==1.14.0
ciso8601==2.1.3; sys_platform != "win32" and implementation_name == "cpython"
click==7.1.2
cryptography==2.9.2
dill==0.3.1.1
iso8601==0.1.12; sys_platform == "win32" or implementation_name != "cpython"
pycparser==2.20
pymysql==0.9.2
pypika==0.37.6
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

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)