diff --git a/.gitignore b/.gitignore index f3d46ad..e941050 100644 --- a/.gitignore +++ b/.gitignore @@ -140,4 +140,5 @@ dmypy.json # Cython debug symbols cython_debug/ -.idea \ No newline at end of file +.idea +migrations \ No newline at end of file diff --git a/alice/cli.py b/alice/cli.py index 080cfc4..358ab6a 100644 --- a/alice/cli.py +++ b/alice/cli.py @@ -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') diff --git a/alice/migrate.py b/alice/migrate.py index c9b1330..d3c6df3 100644 --- a/alice/migrate.py +++ b/alice/migrate.py @@ -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 = {} diff --git a/alice/utils.py b/alice/utils.py index e5b9908..9d5445a 100644 --- a/alice/utils.py +++ b/alice/utils.py @@ -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\w+).+\)' - with open(old_model_file, 'r') as f: - content = f.read() - ret = re.sub(pattern, rf'{new_app} \g', content) - print(ret) + return Tortoise.get_connection(config.get('apps').get(app).get('default_connection')), diff --git a/poetry.lock b/poetry.lock index 8bce2a0..e8625aa 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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"}, diff --git a/pyproject.toml b/pyproject.toml index de85af0..f4c81f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,8 @@ authors = ["long2ice "] python = "^3.8" tortoise-orm = {git = "https://github.com/tortoise/tortoise-orm.git", branch = "develop"} aiomysql = "*" -click = "*" +asyncclick = "*" +dill = "*" [tool.poetry.dev-dependencies] taskipy = "*" diff --git a/requirements.txt b/requirements.txt index 9f16636..5fecaeb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/tests/new_models.py b/tests/new_models.py deleted file mode 100644 index afcb683..0000000 --- a/tests/new_models.py +++ /dev/null @@ -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}' diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..e7b95cd --- /dev/null +++ b/tests/test_utils.py @@ -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)