diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c495da..fbcf5bf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,20 +25,20 @@ jobs: - tortoise021 - tortoise022 - tortoise023 - - tortoisedev + - tortoise024 steps: - name: Start MySQL run: sudo systemctl start mysql.service + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} - uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/poetry.lock') }} restore-keys: | ${{ runner.os }}-pip- - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - name: Install and configure Poetry run: | pip install -U pip poetry @@ -54,11 +54,23 @@ jobs: - name: Install TortoiseORM v0.23 if: matrix.tortoise-orm == 'tortoise023' run: poetry run pip install --upgrade "tortoise-orm>=0.23,<0.24" + - name: Install TortoiseORM v0.24 + if: matrix.tortoise-orm == 'tortoise024' + run: | + if [[ "${{ matrix.python-version }}" == "3.8" ]]; then + echo "Skip test for tortoise v0.24 as it does not support Python3.8" + else + poetry run pip install --upgrade "tortoise-orm>=0.24,<0.25" + fi - name: Install TortoiseORM develop branch if: matrix.tortoise-orm == 'tortoisedev' run: | - poetry run pip uninstall -y tortoise-orm - poetry run pip install --upgrade "git+https://github.com/tortoise/tortoise-orm" + if [[ "${{ matrix.python-version }}" == "3.8" ]]; then + echo "Skip test for tortoise develop branch as it does not support Python3.8" + else + poetry run pip uninstall -y tortoise-orm + poetry run pip install --upgrade "git+https://github.com/tortoise/tortoise-orm" + fi - name: CI env: MYSQL_PASS: root diff --git a/CHANGELOG.md b/CHANGELOG.md index 504e1e9..c4837b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,13 @@ ### [0.8.2]**(Unreleased)** +#### Added +- feat: add --fake to upgrade/downgrade. ([#398]) + #### Fixed - fix: inspectdb raise KeyError 'int2' for smallint. ([#401]) +[#398]: https://github.com/tortoise/aerich/pull/398 [#401]: https://github.com/tortoise/aerich/pull/401 ### [0.8.1](../../releases/tag/v0.8.1) - 2024-12-27 diff --git a/README.md b/README.md index 2234b77..bd786df 100644 --- a/README.md +++ b/README.md @@ -278,6 +278,23 @@ await command.init() await command.migrate('test') ``` +## Upgrade/Downgrade with `--fake` option + +Marks the migrations up to the latest one(or back to the target one) as applied, but without actually running the SQL to change your database schema. + +- Upgrade + +```bash +aerich upgrade --fake +aerich --app models upgrade --fake +``` +- Downgrade + +```bash +aerich downgrade --fake -v 2 +aerich --app models downgrade --fake -v 2 +``` + ## License This project is licensed under the diff --git a/aerich/__init__.py b/aerich/__init__.py index d552bf8..7106f01 100644 --- a/aerich/__init__.py +++ b/aerich/__init__.py @@ -39,18 +39,19 @@ class Command: async def init(self) -> None: await Migrate.init(self.tortoise_config, self.app, self.location) - async def _upgrade(self, conn, version_file) -> None: + async def _upgrade(self, conn, version_file, fake=False) -> None: file_path = Path(Migrate.migrate_location, version_file) m = import_py_file(file_path) upgrade = m.upgrade - await conn.execute_script(await upgrade(conn)) + if not fake: + await conn.execute_script(await upgrade(conn)) await Aerich.create( version=version_file, app=self.app, content=get_models_describe(self.app), ) - async def upgrade(self, run_in_transaction: bool = True) -> List[str]: + async def upgrade(self, run_in_transaction: bool = True, fake=False) -> List[str]: migrated = [] for version_file in Migrate.get_all_version_files(): try: @@ -61,14 +62,14 @@ class Command: app_conn_name = get_app_connection_name(self.tortoise_config, self.app) if run_in_transaction: async with in_transaction(app_conn_name) as conn: - await self._upgrade(conn, version_file) + await self._upgrade(conn, version_file, fake=fake) else: app_conn = get_app_connection(self.tortoise_config, self.app) - await self._upgrade(app_conn, version_file) + await self._upgrade(app_conn, version_file, fake=fake) migrated.append(version_file) return migrated - async def downgrade(self, version: int, delete: bool) -> List[str]: + async def downgrade(self, version: int, delete: bool, fake=False) -> List[str]: ret: List[str] = [] if version == -1: specified_version = await Migrate.get_last_version() @@ -93,7 +94,8 @@ class Command: downgrade_sql = await downgrade(conn) if not downgrade_sql.strip(): raise DowngradeError("No downgrade items found") - await conn.execute_script(downgrade_sql) + if not fake: + await conn.execute_script(downgrade_sql) await version_obj.delete() if delete: os.unlink(file_path) diff --git a/aerich/cli.py b/aerich/cli.py index ddf88c4..5de026c 100644 --- a/aerich/cli.py +++ b/aerich/cli.py @@ -93,15 +93,26 @@ async def migrate(ctx: Context, name, empty) -> None: type=bool, help="Make migrations in a single transaction or not. Can be helpful for large migrations or creating concurrent indexes.", ) +@click.option( + "--fake", + default=False, + is_flag=True, + help="Mark migrations as run without actually running them.", +) @click.pass_context -async def upgrade(ctx: Context, in_transaction: bool) -> None: +async def upgrade(ctx: Context, in_transaction: bool, fake: bool) -> None: command = ctx.obj["command"] - migrated = await command.upgrade(run_in_transaction=in_transaction) + migrated = await command.upgrade(run_in_transaction=in_transaction, fake=fake) if not migrated: click.secho("No upgrade items found", fg=Color.yellow) else: for version_file in migrated: - click.secho(f"Success upgrading to {version_file}", fg=Color.green) + if fake: + click.echo( + f"Upgrading to {version_file}... " + click.style("FAKED", fg=Color.green) + ) + else: + click.secho(f"Success upgrading to {version_file}", fg=Color.green) @cli.command(help="Downgrade to specified version.") @@ -121,18 +132,27 @@ async def upgrade(ctx: Context, in_transaction: bool) -> None: show_default=True, help="Also delete the migration files.", ) +@click.option( + "--fake", + default=False, + is_flag=True, + help="Mark migrations as run without actually running them.", +) @click.pass_context @click.confirmation_option( prompt="Downgrade is dangerous: you might lose your data! Are you sure?", ) -async def downgrade(ctx: Context, version: int, delete: bool) -> None: +async def downgrade(ctx: Context, version: int, delete: bool, fake: bool) -> None: command = ctx.obj["command"] try: - files = await command.downgrade(version, delete) + files = await command.downgrade(version, delete, fake=fake) except DowngradeError as e: return click.secho(str(e), fg=Color.yellow) for file in files: - click.secho(f"Success downgrading to {file}", fg=Color.green) + if fake: + click.echo(f"Downgrading to {file}... " + click.style("FAKED", fg=Color.green)) + else: + click.secho(f"Success downgrading to {file}", fg=Color.green) @cli.command(help="Show currently available heads (unapplied migrations).") diff --git a/aerich/migrate.py b/aerich/migrate.py index 720e51f..87abaee 100644 --- a/aerich/migrate.py +++ b/aerich/migrate.py @@ -4,7 +4,7 @@ import importlib import os from datetime import datetime from pathlib import Path -from typing import Dict, Iterable, List, Optional, Set, Tuple, Type, Union, cast +from typing import Iterable, Optional, Union, cast import asyncclick as click import tortoise @@ -37,17 +37,17 @@ async def downgrade(db: BaseDBAsyncClient) -> str: class Migrate: - upgrade_operators: List[str] = [] - downgrade_operators: List[str] = [] - _upgrade_fk_m2m_index_operators: List[str] = [] - _downgrade_fk_m2m_index_operators: List[str] = [] - _upgrade_m2m: List[str] = [] - _downgrade_m2m: List[str] = [] + upgrade_operators: list[str] = [] + downgrade_operators: list[str] = [] + _upgrade_fk_m2m_index_operators: list[str] = [] + _downgrade_fk_m2m_index_operators: list[str] = [] + _upgrade_m2m: list[str] = [] + _downgrade_m2m: list[str] = [] _aerich = Aerich.__name__ - _rename_fields: Dict[str, Dict[str, str]] = {} # {'model': {'old_field': 'new_field'}} + _rename_fields: dict[str, dict[str, str]] = {} # {'model': {'old_field': 'new_field'}} ddl: BaseDDL - ddl_class: Type[BaseDDL] + ddl_class: type[BaseDDL] _last_version_content: Optional[dict] = None app: str migrate_location: Path @@ -55,11 +55,11 @@ class Migrate: _db_version: Optional[str] = None @staticmethod - def get_field_by_name(name: str, fields: List[dict]) -> dict: + def get_field_by_name(name: str, fields: list[dict]) -> dict: return next(filter(lambda x: x.get("name") == name, fields)) @classmethod - def get_all_version_files(cls) -> List[str]: + def get_all_version_files(cls) -> list[str]: def get_file_version(file_name: str) -> str: return file_name.split("_")[0] @@ -74,7 +74,7 @@ class Migrate: return sorted(files, key=lambda x: int(get_file_version(x))) @classmethod - def _get_model(cls, model: str) -> Type[Model]: + def _get_model(cls, model: str) -> type[Model]: return Tortoise.apps[cls.app].get(model) # type: ignore @classmethod @@ -92,7 +92,7 @@ class Migrate: cls._db_version = ret[1][0].get("version") @classmethod - async def load_ddl_class(cls) -> Type[BaseDDL]: + async def load_ddl_class(cls) -> type[BaseDDL]: ddl_dialect_module = importlib.import_module(f"aerich.ddl.{cls.dialect}") return getattr(ddl_dialect_module, f"{cls.dialect.capitalize()}DDL") @@ -142,6 +142,22 @@ class Migrate: Path(cls.migrate_location, version).write_text(content, encoding="utf-8") return version + @classmethod + def _exclude_extra_field_types(cls, diffs) -> list[tuple]: + # Exclude changes of db_field_types that is not about the current dialect, e.g.: + # {"db_field_types": { + # "oracle": "VARCHAR(255)" --> "oracle": "NVARCHAR2(255)" + # }} + return [ + c + for c in diffs + if not ( + len(c) == 3 + and c[1] == "db_field_types" + and not ({i[0] for i in c[2]} & {cls.dialect, ""}) + ) + ] + @classmethod async def migrate(cls, name: str, empty: bool) -> str: """ @@ -170,7 +186,7 @@ class Migrate: builds content for diff file from template """ - def join_lines(lines: List[str]) -> str: + def join_lines(lines: list[str]) -> str: if not lines: return "" return ";\n ".join(lines) + ";" @@ -202,7 +218,7 @@ class Migrate: cls.downgrade_operators.append(operator) @classmethod - def _handle_indexes(cls, model: Type[Model], indexes: List[Union[Tuple[str], Index]]) -> list: + def _handle_indexes(cls, model: type[Model], indexes: list[Union[tuple[str], Index]]) -> list: if tortoise.__version__ > "0.22.2": # The min version of tortoise is '0.11.0', so we can compare it by a `>`, # tortoise>0.22.2 have __eq__/__hash__ with Index class since 313ee76. @@ -224,13 +240,13 @@ class Migrate: return indexes @classmethod - def _get_indexes(cls, model, model_describe: dict) -> Set[Union[Index, Tuple[str, ...]]]: - indexes: Set[Union[Index, Tuple[str, ...]]] = set() + def _get_indexes(cls, model, model_describe: dict) -> set[Union[Index, tuple[str, ...]]]: + indexes: set[Union[Index, tuple[str, ...]]] = set() for x in cls._handle_indexes(model, model_describe.get("indexes", [])): if isinstance(x, Index): indexes.add(x) else: - indexes.add(cast(Tuple[str, ...], tuple(x))) + indexes.add(cast("tuple[str, ...]", tuple(x))) return indexes @staticmethod @@ -240,11 +256,11 @@ class Migrate: @classmethod def _handle_m2m_fields( - cls, old_model_describe: Dict, new_model_describe: Dict, model, new_models, upgrade=True + cls, old_model_describe: dict, new_model_describe: dict, model, new_models, upgrade=True ) -> None: - old_m2m_fields = cast(List[dict], old_model_describe.get("m2m_fields", [])) - new_m2m_fields = cast(List[dict], new_model_describe.get("m2m_fields", [])) - new_tables: Dict[str, dict] = {field["table"]: field for field in new_models.values()} + old_m2m_fields = cast("list[dict]", old_model_describe.get("m2m_fields", [])) + new_m2m_fields = cast("list[dict]", new_model_describe.get("m2m_fields", [])) + new_tables: dict[str, dict] = {field["table"]: field for field in new_models.values()} for action, option, change in get_dict_diff_by_key(old_m2m_fields, new_m2m_fields): if (option and option[-1] == "nullable") or change[0][0] == "db_constraint": continue @@ -290,18 +306,18 @@ class Migrate: def _handle_relational( cls, key: str, - old_model_describe: Dict, - new_model_describe: Dict, - model: Type[Model], - old_models: Dict, - new_models: Dict, + old_model_describe: dict, + new_model_describe: dict, + model: type[Model], + old_models: dict, + new_models: dict, upgrade=True, ) -> None: - old_fk_fields = cast(List[dict], old_model_describe.get(key)) - new_fk_fields = cast(List[dict], new_model_describe.get(key)) + old_fk_fields = cast("list[dict]", old_model_describe.get(key)) + new_fk_fields = cast("list[dict]", new_model_describe.get(key)) - old_fk_fields_name: List[str] = [i.get("name", "") for i in old_fk_fields] - new_fk_fields_name: List[str] = [i.get("name", "") for i in new_fk_fields] + old_fk_fields_name: list[str] = [i.get("name", "") for i in old_fk_fields] + new_fk_fields_name: list[str] = [i.get("name", "") for i in new_fk_fields] # add for new_fk_field_name in set(new_fk_fields_name).difference(set(old_fk_fields_name)): @@ -312,7 +328,9 @@ class Migrate: cls._add_operator(sql, upgrade, fk_m2m_index=True) # drop for old_fk_field_name in set(old_fk_fields_name).difference(set(new_fk_fields_name)): - old_fk_field = cls.get_field_by_name(old_fk_field_name, cast(List[dict], old_fk_fields)) + old_fk_field = cls.get_field_by_name( + old_fk_field_name, cast("list[dict]", old_fk_fields) + ) if old_fk_field.get("db_constraint"): ref_describe = cast(dict, old_models[old_fk_field["python_type"]]) sql = cls._drop_fk(model, old_fk_field, ref_describe) @@ -321,11 +339,11 @@ class Migrate: @classmethod def _handle_fk_fields( cls, - old_model_describe: Dict, - new_model_describe: Dict, - model: Type[Model], - old_models: Dict, - new_models: Dict, + old_model_describe: dict, + new_model_describe: dict, + model: type[Model], + old_models: dict, + new_models: dict, upgrade=True, ) -> None: key = "fk_fields" @@ -336,11 +354,11 @@ class Migrate: @classmethod def _handle_o2o_fields( cls, - old_model_describe: Dict, - new_model_describe: Dict, - model: Type[Model], - old_models: Dict, - new_models: Dict, + old_model_describe: dict, + new_model_describe: dict, + model: type[Model], + old_models: dict, + new_models: dict, upgrade=True, ) -> None: key = "o2o_fields" @@ -350,7 +368,7 @@ class Migrate: @classmethod def diff_models( - cls, old_models: Dict[str, dict], new_models: Dict[str, dict], upgrade=True + cls, old_models: dict[str, dict], new_models: dict[str, dict], upgrade=True ) -> None: """ diff models and add operators @@ -362,7 +380,7 @@ class Migrate: _aerich = f"{cls.app}.{cls._aerich}" old_models.pop(_aerich, None) new_models.pop(_aerich, None) - models_with_rename_field: Set[str] = set() # models that trigger the click.prompt + models_with_rename_field: set[str] = set() # models that trigger the click.prompt for new_model_str, new_model_describe in new_models.items(): model = cls._get_model(new_model_describe["name"].split(".")[1]) @@ -383,13 +401,13 @@ class Migrate: old_unique_together = set( map( lambda x: tuple(x), - cast(List[Iterable[str]], old_model_describe.get("unique_together")), + cast("list[Iterable[str]]", old_model_describe.get("unique_together")), ) ) new_unique_together = set( map( lambda x: tuple(x), - cast(List[Iterable[str]], new_model_describe.get("unique_together")), + cast("list[Iterable[str]]", new_model_describe.get("unique_together")), ) ) old_indexes = cls._get_indexes(model, old_model_describe) @@ -428,18 +446,18 @@ class Migrate: old_data_fields = list( filter( lambda x: x.get("db_field_types") is not None, - cast(List[dict], old_model_describe.get("data_fields")), + cast("list[dict]", old_model_describe.get("data_fields")), ) ) new_data_fields = list( filter( lambda x: x.get("db_field_types") is not None, - cast(List[dict], new_model_describe.get("data_fields")), + cast("list[dict]", new_model_describe.get("data_fields")), ) ) - old_data_fields_name = cast(List[str], [i.get("name") for i in old_data_fields]) - new_data_fields_name = cast(List[str], [i.get("name") for i in new_data_fields]) + old_data_fields_name = cast("list[str]", [i.get("name") for i in old_data_fields]) + new_data_fields_name = cast("list[str]", [i.get("name") for i in new_data_fields]) # add fields or rename fields for new_data_field_name in set(new_data_fields_name).difference( @@ -459,7 +477,9 @@ class Migrate: len(new_name.symmetric_difference(set(f.get("name", "")))), ), ): - changes = list(diff(old_data_field, new_data_field)) + changes = cls._exclude_extra_field_types( + diff(old_data_field, new_data_field) + ) old_data_field_name = cast(str, old_data_field.get("name")) if len(changes) == 2: # rename field @@ -566,7 +586,7 @@ class Migrate: for field_name in set(new_data_fields_name).intersection(set(old_data_fields_name)): old_data_field = cls.get_field_by_name(field_name, old_data_fields) new_data_field = cls.get_field_by_name(field_name, new_data_fields) - changes = diff(old_data_field, new_data_field) + changes = cls._exclude_extra_field_types(diff(old_data_field, new_data_field)) modified = False for change in changes: _, option, old_new = change @@ -622,11 +642,11 @@ class Migrate: cls._add_operator(cls.drop_model(old_models[old_model]["table"]), upgrade) @classmethod - def rename_table(cls, model: Type[Model], old_table_name: str, new_table_name: str) -> str: + def rename_table(cls, model: type[Model], old_table_name: str, new_table_name: str) -> str: return cls.ddl.rename_table(model, old_table_name, new_table_name) @classmethod - def add_model(cls, model: Type[Model]) -> str: + def add_model(cls, model: type[Model]) -> str: return cls.ddl.create_table(model) @classmethod @@ -635,7 +655,7 @@ class Migrate: @classmethod def create_m2m( - cls, model: Type[Model], field_describe: dict, reference_table_describe: dict + cls, model: type[Model], field_describe: dict, reference_table_describe: dict ) -> str: return cls.ddl.create_m2m(model, field_describe, reference_table_describe) @@ -644,7 +664,7 @@ class Migrate: return cls.ddl.drop_m2m(table_name) @classmethod - def _resolve_fk_fields_name(cls, model: Type[Model], fields_name: Iterable[str]) -> List[str]: + def _resolve_fk_fields_name(cls, model: type[Model], fields_name: Iterable[str]) -> list[str]: ret = [] for field_name in fields_name: try: @@ -662,7 +682,7 @@ class Migrate: @classmethod def _drop_index( - cls, model: Type[Model], fields_name: Union[Iterable[str], Index], unique=False + cls, model: type[Model], fields_name: Union[Iterable[str], Index], unique=False ) -> str: if isinstance(fields_name, Index): return cls.ddl.drop_index_by_name( @@ -673,7 +693,7 @@ class Migrate: @classmethod def _add_index( - cls, model: Type[Model], fields_name: Union[Iterable[str], Index], unique=False + cls, model: type[Model], fields_name: Union[Iterable[str], Index], unique=False ) -> str: if isinstance(fields_name, Index): return fields_name.get_sql(cls.ddl.schema_generator, model, False) @@ -681,42 +701,42 @@ class Migrate: return cls.ddl.add_index(model, field_names, unique) @classmethod - def _add_field(cls, model: Type[Model], field_describe: dict, is_pk: bool = False) -> str: + def _add_field(cls, model: type[Model], field_describe: dict, is_pk: bool = False) -> str: return cls.ddl.add_column(model, field_describe, is_pk) @classmethod - def _alter_default(cls, model: Type[Model], field_describe: dict) -> str: + def _alter_default(cls, model: type[Model], field_describe: dict) -> str: return cls.ddl.alter_column_default(model, field_describe) @classmethod - def _alter_null(cls, model: Type[Model], field_describe: dict) -> str: + def _alter_null(cls, model: type[Model], field_describe: dict) -> str: return cls.ddl.alter_column_null(model, field_describe) @classmethod - def _set_comment(cls, model: Type[Model], field_describe: dict) -> str: + def _set_comment(cls, model: type[Model], field_describe: dict) -> str: return cls.ddl.set_comment(model, field_describe) @classmethod - def _modify_field(cls, model: Type[Model], field_describe: dict) -> str: + def _modify_field(cls, model: type[Model], field_describe: dict) -> str: return cls.ddl.modify_column(model, field_describe) @classmethod def _drop_fk( - cls, model: Type[Model], field_describe: dict, reference_table_describe: dict + cls, model: type[Model], field_describe: dict, reference_table_describe: dict ) -> str: return cls.ddl.drop_fk(model, field_describe, reference_table_describe) @classmethod - def _remove_field(cls, model: Type[Model], column_name: str) -> str: + def _remove_field(cls, model: type[Model], column_name: str) -> str: return cls.ddl.drop_column(model, column_name) @classmethod - def _rename_field(cls, model: Type[Model], old_field_name: str, new_field_name: str) -> str: + def _rename_field(cls, model: type[Model], old_field_name: str, new_field_name: str) -> str: return cls.ddl.rename_column(model, old_field_name, new_field_name) @classmethod def _change_field( - cls, model: Type[Model], old_field_describe: dict, new_field_describe: dict + cls, model: type[Model], old_field_describe: dict, new_field_describe: dict ) -> str: db_field_types = cast(dict, new_field_describe.get("db_field_types")) return cls.ddl.change_column( @@ -728,7 +748,7 @@ class Migrate: @classmethod def _add_fk( - cls, model: Type[Model], field_describe: dict, reference_table_describe: dict + cls, model: type[Model], field_describe: dict, reference_table_describe: dict ) -> str: """ add fk diff --git a/aerich/utils.py b/aerich/utils.py index 0fbbb04..0629990 100644 --- a/aerich/utils.py +++ b/aerich/utils.py @@ -6,7 +6,7 @@ import re import sys from pathlib import Path from types import ModuleType -from typing import Dict, Generator, Optional, Union +from typing import Generator, Optional, Union from asyncclick import BadOptionUsage, ClickException, Context from dictdiffer import diff @@ -37,20 +37,20 @@ def get_app_connection_name(config, app_name: str) -> str: :return: """ app = config.get("apps").get(app_name) - if app: - return app.get("default_connection", "default") - raise BadOptionUsage( - option_name="--app", - message=f'Can\'t get app named "{app_name}"', - ) + if not app: + raise BadOptionUsage( + option_name="--app", + message=f'Can\'t get app named "{app_name}"', + ) + return app.get("default_connection", "default") def get_app_connection(config, app) -> BaseDBAsyncClient: """ - get connection name + get connection client :param config: :param app: - :return: + :return: client instance """ return Tortoise.get_connection(get_app_connection_name(config, app)) @@ -81,7 +81,7 @@ def get_tortoise_config(ctx: Context, tortoise_orm: str) -> dict: return config -def get_models_describe(app: str) -> Dict: +def get_models_describe(app: str) -> dict: """ get app models describe :param app: diff --git a/conftest.py b/conftest.py index f4843df..b3f35e0 100644 --- a/conftest.py +++ b/conftest.py @@ -1,27 +1,26 @@ import asyncio -import contextlib import os from typing import Generator import pytest -from tortoise import Tortoise, expand_db_url, generate_schema_for_client +from tortoise import Tortoise, expand_db_url from tortoise.backends.asyncpg.schema_generator import AsyncpgSchemaGenerator from tortoise.backends.mysql.schema_generator import MySQLSchemaGenerator from tortoise.backends.sqlite.schema_generator import SqliteSchemaGenerator from tortoise.contrib.test import MEMORY_SQLITE -from tortoise.exceptions import DBConnectionError, OperationalError from aerich.ddl.mysql import MysqlDDL from aerich.ddl.postgres import PostgresDDL from aerich.ddl.sqlite import SqliteDDL from aerich.migrate import Migrate +from tests._utils import init_db db_url = os.getenv("TEST_DB", MEMORY_SQLITE) db_url_second = os.getenv("TEST_DB_SECOND", MEMORY_SQLITE) tortoise_orm = { "connections": { - "default": expand_db_url(db_url, True), - "second": expand_db_url(db_url_second, True), + "default": expand_db_url(db_url, testing=True), + "second": expand_db_url(db_url_second, testing=True), }, "apps": { "models": {"models": ["tests.models", "aerich.models"], "default_connection": "default"}, @@ -55,24 +54,7 @@ def event_loop() -> Generator: @pytest.fixture(scope="session", autouse=True) async def initialize_tests(event_loop, request) -> None: - # Placing init outside the try block since it doesn't - # establish connections to the DB eagerly. - await Tortoise.init(config=tortoise_orm) - with contextlib.suppress(DBConnectionError, OperationalError): - await Tortoise._drop_databases() - await Tortoise.init(config=tortoise_orm, _create_db=True) - try: - await generate_schema_for_client(Tortoise.get_connection("default"), safe=True) - except OperationalError as e: - if (s := "IF NOT EXISTS") not in str(e): - raise e - # MySQL does not support `CREATE INDEX IF NOT EXISTS` syntax - client = Tortoise.get_connection("default") - generator = client.schema_generator(client) - schema = generator.get_create_schema_sql(safe=True) - schema = schema.replace(f" INDEX {s}", " INDEX") - await generator.generate_from_string(schema) - + await init_db(tortoise_orm) client = Tortoise.get_connection("default") if client.schema_generator is MySQLSchemaGenerator: Migrate.ddl = MysqlDDL(client) diff --git a/poetry.lock b/poetry.lock index 17c8969..cd42a5d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. [[package]] name = "aiosqlite" @@ -6,6 +6,7 @@ version = "0.20.0" description = "asyncio bridge to the standard sqlite3 module" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "aiosqlite-0.20.0-py3-none-any.whl", hash = "sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6"}, {file = "aiosqlite-0.20.0.tar.gz", hash = "sha256:6d35c8c256637f4672f843c31021464090805bf925385ac39473fb16eaaca3d7"}, @@ -24,6 +25,7 @@ version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, @@ -38,6 +40,7 @@ version = "4.5.2" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f"}, {file = "anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b"}, @@ -60,6 +63,8 @@ version = "5.0.1" description = "Timeout context manager for asyncio programs" optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"asyncpg\" and python_version < \"3.11.0\"" files = [ {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, @@ -71,6 +76,7 @@ version = "8.1.7.2" description = "Composable command line interface toolkit, async version" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "asyncclick-8.1.7.2-py3-none-any.whl", hash = "sha256:1ab940b04b22cb89b5b400725132b069d01b0c3472a9702c7a2c9d5d007ded02"}, {file = "asyncclick-8.1.7.2.tar.gz", hash = "sha256:219ea0f29ccdc1bb4ff43bcab7ce0769ac6d48a04f997b43ec6bee99a222daa0"}, @@ -86,6 +92,8 @@ version = "0.2.10" description = "A fast asyncio MySQL driver" optional = true python-versions = ">=3.8,<4.0" +groups = ["main"] +markers = "extra == \"asyncmy\"" files = [ {file = "asyncmy-0.2.10-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:c2237c8756b8f374099bd320c53b16f7ec0cee8258f00d72eed5a2cd3d251066"}, {file = "asyncmy-0.2.10-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:6e98d4fbf7ea0d99dfecb24968c9c350b019397ba1af9f181d51bb0f6f81919b"}, @@ -151,6 +159,8 @@ version = "0.30.0" description = "An asyncio PostgreSQL driver" optional = true python-versions = ">=3.8.0" +groups = ["main"] +markers = "extra == \"asyncpg\"" files = [ {file = "asyncpg-0.30.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfb4dd5ae0699bad2b233672c8fc5ccbd9ad24b89afded02341786887e37927e"}, {file = "asyncpg-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc1f62c792752a49f88b7e6f774c26077091b44caceb1983509edc18a2222ec0"}, @@ -217,6 +227,7 @@ version = "1.7.10" description = "Security oriented static analyser for python code." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "bandit-1.7.10-py3-none-any.whl", hash = "sha256:665721d7bebbb4485a339c55161ac0eedde27d51e638000d91c8c2d68343ad02"}, {file = "bandit-1.7.10.tar.gz", hash = "sha256:59ed5caf5d92b6ada4bf65bc6437feea4a9da1093384445fed4d472acc6cff7b"}, @@ -241,6 +252,7 @@ version = "24.8.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, @@ -287,6 +299,8 @@ version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "platform_python_implementation != \"PyPy\"" files = [ {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, @@ -366,6 +380,7 @@ version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, @@ -380,10 +395,12 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +markers = {main = "platform_system == \"Windows\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} [[package]] name = "cryptography" @@ -391,6 +408,7 @@ version = "43.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, @@ -440,6 +458,7 @@ version = "0.9.0" description = "Dictdiffer is a library that helps you to diff and patch dictionaries." optional = false python-versions = "*" +groups = ["main"] files = [ {file = "dictdiffer-0.9.0-py2.py3-none-any.whl", hash = "sha256:442bfc693cfcadaf46674575d2eba1c53b42f5e404218ca2c2ff549f2df56595"}, {file = "dictdiffer-0.9.0.tar.gz", hash = "sha256:17bacf5fbfe613ccf1b6d512bd766e6b21fb798822a133aa86098b8ac9997578"}, @@ -457,6 +476,8 @@ version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] +markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -471,6 +492,7 @@ version = "2.1.1" description = "execnet: rapid multi-Python deployment" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, @@ -485,6 +507,7 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -499,6 +522,7 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -510,6 +534,7 @@ version = "2.1.0" description = "Simple module to parse ISO 8601 dates" optional = false python-versions = ">=3.7,<4.0" +groups = ["main"] files = [ {file = "iso8601-2.1.0-py3-none-any.whl", hash = "sha256:aac4145c4dcb66ad8b648a02830f5e2ff6c24af20f4f482689be402db2429242"}, {file = "iso8601-2.1.0.tar.gz", hash = "sha256:6b1d3829ee8921c4301998c909f7829fa9ed3cbdac0d3b16af2d743aed1ba8df"}, @@ -521,6 +546,7 @@ version = "5.13.2" description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.8.0" +groups = ["dev"] files = [ {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, @@ -535,6 +561,7 @@ version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, @@ -559,6 +586,7 @@ version = "0.1.2" description = "Markdown URL utilities" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, @@ -566,43 +594,50 @@ files = [ [[package]] name = "mypy" -version = "1.14.0" +version = "1.14.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "mypy-1.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e971c1c667007f9f2b397ffa80fa8e1e0adccff336e5e77e74cb5f22868bee87"}, - {file = "mypy-1.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e86aaeaa3221a278c66d3d673b297232947d873773d61ca3ee0e28b2ff027179"}, - {file = "mypy-1.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1628c5c3ce823d296e41e2984ff88c5861499041cb416a8809615d0c1f41740e"}, - {file = "mypy-1.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7fadb29b77fc14a0dd81304ed73c828c3e5cde0016c7e668a86a3e0dfc9f3af3"}, - {file = "mypy-1.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:3fa76988dc760da377c1e5069200a50d9eaaccf34f4ea18428a3337034ab5a44"}, - {file = "mypy-1.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6e73c8a154eed31db3445fe28f63ad2d97b674b911c00191416cf7f6459fd49a"}, - {file = "mypy-1.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:273e70fcb2e38c5405a188425aa60b984ffdcef65d6c746ea5813024b68c73dc"}, - {file = "mypy-1.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1daca283d732943731a6a9f20fdbcaa927f160bc51602b1d4ef880a6fb252015"}, - {file = "mypy-1.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7e68047bedb04c1c25bba9901ea46ff60d5eaac2d71b1f2161f33107e2b368eb"}, - {file = "mypy-1.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:7a52f26b9c9b1664a60d87675f3bae00b5c7f2806e0c2800545a32c325920bcc"}, - {file = "mypy-1.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d5326ab70a6db8e856d59ad4cb72741124950cbbf32e7b70e30166ba7bbf61dd"}, - {file = "mypy-1.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bf4ec4980bec1e0e24e5075f449d014011527ae0055884c7e3abc6a99cd2c7f1"}, - {file = "mypy-1.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:390dfb898239c25289495500f12fa73aa7f24a4c6d90ccdc165762462b998d63"}, - {file = "mypy-1.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7e026d55ddcd76e29e87865c08cbe2d0104e2b3153a523c529de584759379d3d"}, - {file = "mypy-1.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:585ed36031d0b3ee362e5107ef449a8b5dfd4e9c90ccbe36414ee405ee6b32ba"}, - {file = "mypy-1.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9f6f4c0b27401d14c483c622bc5105eff3911634d576bbdf6695b9a7c1ba741"}, - {file = "mypy-1.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b2280cedcb312c7a79f5001ae5325582d0d339bce684e4a529069d0e7ca1e7"}, - {file = "mypy-1.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:342de51c48bab326bfc77ce056ba08c076d82ce4f5a86621f972ed39970f94d8"}, - {file = "mypy-1.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:00df23b42e533e02a6f0055e54de9a6ed491cd8b7ea738647364fd3a39ea7efc"}, - {file = "mypy-1.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:e8c8387e5d9dff80e7daf961df357c80e694e942d9755f3ad77d69b0957b8e3f"}, - {file = "mypy-1.14.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b16738b1d80ec4334654e89e798eb705ac0c36c8a5c4798496cd3623aa02286"}, - {file = "mypy-1.14.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:10065fcebb7c66df04b05fc799a854b1ae24d9963c8bb27e9064a9bdb43aa8ad"}, - {file = "mypy-1.14.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fbb7d683fa6bdecaa106e8368aa973ecc0ddb79a9eaeb4b821591ecd07e9e03c"}, - {file = "mypy-1.14.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3498cb55448dc5533e438cd13d6ddd28654559c8c4d1fd4b5ca57a31b81bac01"}, - {file = "mypy-1.14.0-cp38-cp38-win_amd64.whl", hash = "sha256:c7b243408ea43755f3a21a0a08e5c5ae30eddb4c58a80f415ca6b118816e60aa"}, - {file = "mypy-1.14.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:14117b9da3305b39860d0aa34b8f1ff74d209a368829a584eb77524389a9c13e"}, - {file = "mypy-1.14.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af98c5a958f9c37404bd4eef2f920b94874507e146ed6ee559f185b8809c44cc"}, - {file = "mypy-1.14.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0b343a1d3989547024377c2ba0dca9c74a2428ad6ed24283c213af8dbb0710b"}, - {file = "mypy-1.14.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cdb5563c1726c85fb201be383168f8c866032db95e1095600806625b3a648cb7"}, - {file = "mypy-1.14.0-cp39-cp39-win_amd64.whl", hash = "sha256:74e925649c1ee0a79aa7448baf2668d81cc287dc5782cff6a04ee93f40fb8d3f"}, - {file = "mypy-1.14.0-py3-none-any.whl", hash = "sha256:2238d7f93fc4027ed1efc944507683df3ba406445a2b6c96e79666a045aadfab"}, - {file = "mypy-1.14.0.tar.gz", hash = "sha256:822dbd184d4a9804df5a7d5335a68cf7662930e70b8c1bc976645d1509f9a9d6"}, + {file = "mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb"}, + {file = "mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0"}, + {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d"}, + {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b"}, + {file = "mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427"}, + {file = "mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f"}, + {file = "mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c"}, + {file = "mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1"}, + {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8"}, + {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f"}, + {file = "mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1"}, + {file = "mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae"}, + {file = "mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14"}, + {file = "mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9"}, + {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11"}, + {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e"}, + {file = "mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89"}, + {file = "mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b"}, + {file = "mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255"}, + {file = "mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34"}, + {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a"}, + {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9"}, + {file = "mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd"}, + {file = "mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107"}, + {file = "mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31"}, + {file = "mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6"}, + {file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319"}, + {file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac"}, + {file = "mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b"}, + {file = "mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837"}, + {file = "mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35"}, + {file = "mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc"}, + {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9"}, + {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb"}, + {file = "mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60"}, + {file = "mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c"}, + {file = "mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1"}, + {file = "mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6"}, ] [package.dependencies] @@ -623,6 +658,7 @@ version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, @@ -634,6 +670,7 @@ version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, @@ -645,6 +682,7 @@ version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -656,6 +694,7 @@ version = "6.1.0" description = "Python Build Reasonableness" optional = false python-versions = ">=2.6" +groups = ["dev"] files = [ {file = "pbr-6.1.0-py2.py3-none-any.whl", hash = "sha256:a776ae228892d8013649c0aeccbb3d5f99ee15e005a4cbb7e61d55a067b28a2a"}, {file = "pbr-6.1.0.tar.gz", hash = "sha256:788183e382e3d1d7707db08978239965e8b9e4e5ed42669bf4758186734d5f24"}, @@ -667,6 +706,7 @@ version = "4.3.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, @@ -683,6 +723,7 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -698,6 +739,8 @@ version = "2.22" description = "C parser in Python" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "platform_python_implementation != \"PyPy\"" files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, @@ -705,13 +748,14 @@ files = [ [[package]] name = "pydantic" -version = "2.10.4" +version = "2.10.6" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ - {file = "pydantic-2.10.4-py3-none-any.whl", hash = "sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d"}, - {file = "pydantic-2.10.4.tar.gz", hash = "sha256:82f12e9723da6de4fe2ba888b5971157b3be7ad914267dea8f05f82b28254f06"}, + {file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"}, + {file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"}, ] [package.dependencies] @@ -729,6 +773,7 @@ version = "2.27.2" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, @@ -837,13 +882,14 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pygments" -version = "2.18.0" +version = "2.19.1" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, - {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, + {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, + {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, ] [package.extras] @@ -855,6 +901,7 @@ version = "0.3.2" description = "Forked from pypika and streamline just for tortoise-orm" optional = false python-versions = ">=3.8,<4.0" +groups = ["main"] files = [ {file = "pypika_tortoise-0.3.2-py3-none-any.whl", hash = "sha256:c5c52bc4473fe6f3db36cf659340750246ec5dd0f980d04ae7811430e299c3a2"}, {file = "pypika_tortoise-0.3.2.tar.gz", hash = "sha256:f5d508e2ef00255e52ec6ac79ef889e10dbab328f218c55cd134c4d02ff9f6f4"}, @@ -866,6 +913,7 @@ version = "8.3.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, @@ -888,6 +936,7 @@ version = "0.21.2" description = "Pytest support for asyncio" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pytest_asyncio-0.21.2-py3-none-any.whl", hash = "sha256:ab664c88bb7998f711d8039cacd4884da6430886ae8bbd4eded552ed2004f16b"}, {file = "pytest_asyncio-0.21.2.tar.gz", hash = "sha256:d67738fc232b94b326b9d060750beb16e0074210b98dd8b58a5239fa2a154f45"}, @@ -906,6 +955,7 @@ version = "3.14.0" description = "Thin-wrapper around the mock package for easier use with pytest" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, @@ -923,6 +973,7 @@ version = "3.6.1" description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"}, {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"}, @@ -939,13 +990,14 @@ testing = ["filelock"] [[package]] name = "pytz" -version = "2024.2" +version = "2025.1" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" +groups = ["main"] files = [ - {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, - {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, + {file = "pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57"}, + {file = "pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e"}, ] [[package]] @@ -954,6 +1006,7 @@ version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -1016,6 +1069,7 @@ version = "13.9.4" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.8.0" +groups = ["dev"] files = [ {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, @@ -1031,29 +1085,30 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.8.4" +version = "0.9.4" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ - {file = "ruff-0.8.4-py3-none-linux_armv6l.whl", hash = "sha256:58072f0c06080276804c6a4e21a9045a706584a958e644353603d36ca1eb8a60"}, - {file = "ruff-0.8.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ffb60904651c00a1e0b8df594591770018a0f04587f7deeb3838344fe3adabac"}, - {file = "ruff-0.8.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ddf5d654ac0d44389f6bf05cee4caeefc3132a64b58ea46738111d687352296"}, - {file = "ruff-0.8.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e248b1f0fa2749edd3350a2a342b67b43a2627434c059a063418e3d375cfe643"}, - {file = "ruff-0.8.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf197b98ed86e417412ee3b6c893f44c8864f816451441483253d5ff22c0e81e"}, - {file = "ruff-0.8.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c41319b85faa3aadd4d30cb1cffdd9ac6b89704ff79f7664b853785b48eccdf3"}, - {file = "ruff-0.8.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:9f8402b7c4f96463f135e936d9ab77b65711fcd5d72e5d67597b543bbb43cf3f"}, - {file = "ruff-0.8.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4e56b3baa9c23d324ead112a4fdf20db9a3f8f29eeabff1355114dd96014604"}, - {file = "ruff-0.8.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:736272574e97157f7edbbb43b1d046125fce9e7d8d583d5d65d0c9bf2c15addf"}, - {file = "ruff-0.8.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5fe710ab6061592521f902fca7ebcb9fabd27bc7c57c764298b1c1f15fff720"}, - {file = "ruff-0.8.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:13e9ec6d6b55f6da412d59953d65d66e760d583dd3c1c72bf1f26435b5bfdbae"}, - {file = "ruff-0.8.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:97d9aefef725348ad77d6db98b726cfdb075a40b936c7984088804dfd38268a7"}, - {file = "ruff-0.8.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ab78e33325a6f5374e04c2ab924a3367d69a0da36f8c9cb6b894a62017506111"}, - {file = "ruff-0.8.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8ef06f66f4a05c3ddbc9121a8b0cecccd92c5bf3dd43b5472ffe40b8ca10f0f8"}, - {file = "ruff-0.8.4-py3-none-win32.whl", hash = "sha256:552fb6d861320958ca5e15f28b20a3d071aa83b93caee33a87b471f99a6c0835"}, - {file = "ruff-0.8.4-py3-none-win_amd64.whl", hash = "sha256:f21a1143776f8656d7f364bd264a9d60f01b7f52243fbe90e7670c0dfe0cf65d"}, - {file = "ruff-0.8.4-py3-none-win_arm64.whl", hash = "sha256:9183dd615d8df50defa8b1d9a074053891ba39025cf5ae88e8bcb52edcc4bf08"}, - {file = "ruff-0.8.4.tar.gz", hash = "sha256:0d5f89f254836799af1615798caa5f80b7f935d7a670fad66c5007928e57ace8"}, + {file = "ruff-0.9.4-py3-none-linux_armv6l.whl", hash = "sha256:64e73d25b954f71ff100bb70f39f1ee09e880728efb4250c632ceed4e4cdf706"}, + {file = "ruff-0.9.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6ce6743ed64d9afab4fafeaea70d3631b4d4b28b592db21a5c2d1f0ef52934bf"}, + {file = "ruff-0.9.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:54499fb08408e32b57360f6f9de7157a5fec24ad79cb3f42ef2c3f3f728dfe2b"}, + {file = "ruff-0.9.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37c892540108314a6f01f105040b5106aeb829fa5fb0561d2dcaf71485021137"}, + {file = "ruff-0.9.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de9edf2ce4b9ddf43fd93e20ef635a900e25f622f87ed6e3047a664d0e8f810e"}, + {file = "ruff-0.9.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87c90c32357c74f11deb7fbb065126d91771b207bf9bfaaee01277ca59b574ec"}, + {file = "ruff-0.9.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56acd6c694da3695a7461cc55775f3a409c3815ac467279dfa126061d84b314b"}, + {file = "ruff-0.9.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0c93e7d47ed951b9394cf352d6695b31498e68fd5782d6cbc282425655f687a"}, + {file = "ruff-0.9.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d4c8772670aecf037d1bf7a07c39106574d143b26cfe5ed1787d2f31e800214"}, + {file = "ruff-0.9.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfc5f1d7afeda8d5d37660eeca6d389b142d7f2b5a1ab659d9214ebd0e025231"}, + {file = "ruff-0.9.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:faa935fc00ae854d8b638c16a5f1ce881bc3f67446957dd6f2af440a5fc8526b"}, + {file = "ruff-0.9.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a6c634fc6f5a0ceae1ab3e13c58183978185d131a29c425e4eaa9f40afe1e6d6"}, + {file = "ruff-0.9.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:433dedf6ddfdec7f1ac7575ec1eb9844fa60c4c8c2f8887a070672b8d353d34c"}, + {file = "ruff-0.9.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d612dbd0f3a919a8cc1d12037168bfa536862066808960e0cc901404b77968f0"}, + {file = "ruff-0.9.4-py3-none-win32.whl", hash = "sha256:db1192ddda2200671f9ef61d9597fcef89d934f5d1705e571a93a67fb13a4402"}, + {file = "ruff-0.9.4-py3-none-win_amd64.whl", hash = "sha256:05bebf4cdbe3ef75430d26c375773978950bbf4ee3c95ccb5448940dc092408e"}, + {file = "ruff-0.9.4-py3-none-win_arm64.whl", hash = "sha256:585792f1e81509e38ac5123492f8875fbc36f3ede8185af0a26df348e5154f41"}, + {file = "ruff-0.9.4.tar.gz", hash = "sha256:6907ee3529244bb0ed066683e075f09285b38dd5b4039370df6ff06041ca19e7"}, ] [[package]] @@ -1062,6 +1117,7 @@ version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -1073,6 +1129,7 @@ version = "5.3.0" description = "Manage dynamic plugins for Python applications" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "stevedore-5.3.0-py3-none-any.whl", hash = "sha256:1efd34ca08f474dad08d9b19e934a22c68bb6fe416926479ba29e5013bcc8f78"}, {file = "stevedore-5.3.0.tar.gz", hash = "sha256:9a64265f4060312828151c204efbe9b7a9852a0d9228756344dbc7e4023e375a"}, @@ -1087,6 +1144,8 @@ version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version < \"3.11\"" files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -1124,13 +1183,15 @@ files = [ [[package]] name = "tomli-w" -version = "1.1.0" +version = "1.2.0" description = "A lil' TOML writer" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.11\" and extra == \"toml\"" files = [ - {file = "tomli_w-1.1.0-py3-none-any.whl", hash = "sha256:1403179c78193e3184bfaade390ddbd071cba48a32a2e62ba11aae47490c63f7"}, - {file = "tomli_w-1.1.0.tar.gz", hash = "sha256:49e847a3a304d516a169a601184932ef0f6b61623fe680f836a2aa7128ed0d33"}, + {file = "tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90"}, + {file = "tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021"}, ] [[package]] @@ -1139,6 +1200,8 @@ version = "0.13.2" description = "Style preserving TOML library" optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "python_version < \"3.11\" and extra == \"toml\"" files = [ {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, @@ -1150,6 +1213,7 @@ version = "0.23.0" description = "Easy async ORM for python, built with relations in mind" optional = false python-versions = ">=3.8,<4.0" +groups = ["main"] files = [ {file = "tortoise_orm-0.23.0-py3-none-any.whl", hash = "sha256:deaabed1619ea8aab6213508dff025571a701b7f34ee534473d7bb7661aa9f4f"}, {file = "tortoise_orm-0.23.0.tar.gz", hash = "sha256:f25d431ef4fb521a84edad582f4b9c53dccc5abf6cfbc6f228cbece5a13952fa"}, @@ -1175,6 +1239,7 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, @@ -1186,6 +1251,6 @@ asyncpg = ["asyncpg"] toml = ["tomli-w", "tomlkit"] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "^3.8" content-hash = "5a17cf1dd79829b76fc2c71cbd83032d70ada4f129cf56973c417eac91a975f6" diff --git a/pyproject.toml b/pyproject.toml index 2185b48..e95b2cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ aerich = "aerich.cli:main" [tool.black] line-length = 100 -target-version = ['py38', 'py39', 'py310', 'py311', 'py312'] +target-version = ['py38', 'py39', 'py310', 'py311', 'py312', 'py313'] [tool.pytest.ini_options] asyncio_mode = 'auto' diff --git a/tests/_utils.py b/tests/_utils.py new file mode 100644 index 0000000..1585000 --- /dev/null +++ b/tests/_utils.py @@ -0,0 +1,46 @@ +import contextlib +import os +import shutil +import sys +from pathlib import Path + +from tortoise import Tortoise, generate_schema_for_client +from tortoise.exceptions import DBConnectionError, OperationalError + +if sys.version_info >= (3, 11): + from contextlib import chdir +else: + + class chdir(contextlib.AbstractContextManager): # Copied from source code of Python3.13 + """Non thread-safe context manager to change the current working directory.""" + + def __init__(self, path): + self.path = path + self._old_cwd = [] + + def __enter__(self): + self._old_cwd.append(os.getcwd()) + os.chdir(self.path) + + def __exit__(self, *excinfo): + os.chdir(self._old_cwd.pop()) + + +async def drop_db(tortoise_orm) -> None: + # Placing init outside the try-block(suppress) since it doesn't + # establish connections to the DB eagerly. + await Tortoise.init(config=tortoise_orm) + with contextlib.suppress(DBConnectionError, OperationalError): + await Tortoise._drop_databases() + + +async def init_db(tortoise_orm, generate_schemas=True) -> None: + await drop_db(tortoise_orm) + await Tortoise.init(config=tortoise_orm, _create_db=True) + if generate_schemas: + await generate_schema_for_client(Tortoise.get_connection("default"), safe=True) + + +def copy_files(*src_files: Path, target_dir: Path) -> None: + for src in src_files: + shutil.copy(src, target_dir) diff --git a/tests/assets/fake/_tests.py b/tests/assets/fake/_tests.py new file mode 100644 index 0000000..1909557 --- /dev/null +++ b/tests/assets/fake/_tests.py @@ -0,0 +1,72 @@ +import pytest +from models import NewModel +from models_second import Config +from settings import TORTOISE_ORM +from tortoise import Tortoise +from tortoise.exceptions import OperationalError + + +@pytest.fixture(scope="session") +def anyio_backend() -> str: + return "asyncio" + + +@pytest.fixture(autouse=True) +async def init_connections(): + await Tortoise.init(TORTOISE_ORM) + try: + yield + finally: + await Tortoise.close_connections() + + +@pytest.mark.anyio +async def test_init_db(): + m1 = await NewModel.filter(name="") + assert isinstance(m1, list) + m2 = await Config.filter(key="") + assert isinstance(m2, list) + await NewModel.create(name="") + await Config.create(key="", label="", value={}) + + +@pytest.mark.anyio +async def test_fake_field_1(): + assert "field_1" in NewModel._meta.fields_map + assert "field_1" in Config._meta.fields_map + with pytest.raises(OperationalError): + await NewModel.create(name="", field_1=1) + with pytest.raises(OperationalError): + await Config.create(key="", label="", value={}, field_1=1) + + obj1 = NewModel(name="", field_1=1) + with pytest.raises(OperationalError): + await obj1.save() + obj1 = NewModel(name="") + with pytest.raises(OperationalError): + await obj1.save() + with pytest.raises(OperationalError): + obj1 = await NewModel.first() + obj1 = await NewModel.all().first().values("id", "name") + assert obj1 and obj1["id"] + + obj2 = Config(key="", label="", value={}, field_1=1) + with pytest.raises(OperationalError): + await obj2.save() + obj2 = Config(key="", label="", value={}) + with pytest.raises(OperationalError): + await obj2.save() + with pytest.raises(OperationalError): + obj2 = await Config.first() + obj2 = await Config.all().first().values("id", "key") + assert obj2 and obj2["id"] + + +@pytest.mark.anyio +async def test_fake_field_2(): + assert "field_2" in NewModel._meta.fields_map + assert "field_2" in Config._meta.fields_map + with pytest.raises(OperationalError): + await NewModel.create(name="") + with pytest.raises(OperationalError): + await Config.create(key="", label="", value={}) diff --git a/tests/assets/fake/db.py b/tests/assets/fake/db.py new file mode 100644 index 0000000..9f29395 --- /dev/null +++ b/tests/assets/fake/db.py @@ -0,0 +1,28 @@ +import asyncclick as click +from settings import TORTOISE_ORM + +from tests._utils import drop_db, init_db + + +@click.group() +def cli(): ... + + +@cli.command() +async def create(): + await init_db(TORTOISE_ORM, False) + click.echo(f"Success to create databases for {TORTOISE_ORM['connections']}") + + +@cli.command() +async def drop(): + await drop_db(TORTOISE_ORM) + click.echo(f"Dropped databases for {TORTOISE_ORM['connections']}") + + +def main(): + cli() + + +if __name__ == "__main__": + main() diff --git a/tests/assets/fake/settings.py b/tests/assets/fake/settings.py new file mode 100644 index 0000000..9407767 --- /dev/null +++ b/tests/assets/fake/settings.py @@ -0,0 +1,22 @@ +import os +from datetime import date + +from tortoise.contrib.test import MEMORY_SQLITE + +DB_URL = ( + _u.replace("\\{\\}", f"aerich_fake_{date.today():%Y%m%d}") + if (_u := os.getenv("TEST_DB")) + else MEMORY_SQLITE +) +DB_URL_SECOND = (DB_URL + "_second") if DB_URL != MEMORY_SQLITE else MEMORY_SQLITE + +TORTOISE_ORM = { + "connections": { + "default": DB_URL.replace(MEMORY_SQLITE, "sqlite://db.sqlite3"), + "second": DB_URL_SECOND.replace(MEMORY_SQLITE, "sqlite://db_second.sqlite3"), + }, + "apps": { + "models": {"models": ["models", "aerich.models"], "default_connection": "default"}, + "models_second": {"models": ["models_second"], "default_connection": "second"}, + }, +} diff --git a/tests/assets/sqlite_migrate/_tests.py b/tests/assets/sqlite_migrate/_tests.py new file mode 100644 index 0000000..6fe8d46 --- /dev/null +++ b/tests/assets/sqlite_migrate/_tests.py @@ -0,0 +1,75 @@ +import uuid + +import pytest +from models import Foo +from tortoise.exceptions import IntegrityError + + +@pytest.mark.asyncio +async def test_allow_duplicate() -> None: + await Foo.all().delete() + await Foo.create(name="foo") + obj = await Foo.create(name="foo") + assert (await Foo.all().count()) == 2 + await obj.delete() + + +@pytest.mark.asyncio +async def test_unique_is_true() -> None: + with pytest.raises(IntegrityError): + await Foo.create(name="foo") + + +@pytest.mark.asyncio +async def test_add_unique_field() -> None: + if not await Foo.filter(age=0).exists(): + await Foo.create(name="0_" + uuid.uuid4().hex, age=0) + with pytest.raises(IntegrityError): + await Foo.create(name=uuid.uuid4().hex, age=0) + + +@pytest.mark.asyncio +async def test_drop_unique_field() -> None: + name = "1_" + uuid.uuid4().hex + await Foo.create(name=name, age=0) + assert await Foo.filter(name=name).exists() + + +@pytest.mark.asyncio +async def test_with_age_field() -> None: + name = "2_" + uuid.uuid4().hex + await Foo.create(name=name, age=0) + obj = await Foo.get(name=name) + assert obj.age == 0 + + +@pytest.mark.asyncio +async def test_without_age_field() -> None: + name = "3_" + uuid.uuid4().hex + await Foo.create(name=name, age=0) + obj = await Foo.get(name=name) + assert getattr(obj, "age", None) is None + + +@pytest.mark.asyncio +async def test_m2m_with_custom_through() -> None: + from models import FooGroup, Group + + name = "4_" + uuid.uuid4().hex + foo = await Foo.create(name=name) + group = await Group.create(name=name + "1") + await FooGroup.all().delete() + await foo.groups.add(group) + foo_group = await FooGroup.get(foo=foo, group=group) + assert not foo_group.is_active + + +@pytest.mark.asyncio +async def test_add_m2m_field_after_init_db() -> None: + from models import Group + + name = "5_" + uuid.uuid4().hex + foo = await Foo.create(name=name) + group = await Group.create(name=name + "1") + await foo.groups.add(group) + assert (await group.users.all().first()) == foo diff --git a/tests/assets/sqlite_migrate/conftest_.py b/tests/assets/sqlite_migrate/conftest_.py new file mode 100644 index 0000000..5cc98be --- /dev/null +++ b/tests/assets/sqlite_migrate/conftest_.py @@ -0,0 +1,26 @@ +import asyncio +from typing import Generator + +import pytest +import pytest_asyncio +import settings +from tortoise import Tortoise, connections + + +@pytest.fixture(scope="session") +def event_loop() -> Generator: + policy = asyncio.get_event_loop_policy() + res = policy.new_event_loop() + asyncio.set_event_loop(res) + res._close = res.close # type:ignore[attr-defined] + res.close = lambda: None # type:ignore[method-assign] + + yield res + + res._close() # type:ignore[attr-defined] + + +@pytest_asyncio.fixture(scope="session", autouse=True) +async def api(event_loop, request): + await Tortoise.init(config=settings.TORTOISE_ORM) + request.addfinalizer(lambda: event_loop.run_until_complete(connections.close_all(discard=True))) diff --git a/tests/assets/sqlite_migrate/models.py b/tests/assets/sqlite_migrate/models.py new file mode 100644 index 0000000..08c2b22 --- /dev/null +++ b/tests/assets/sqlite_migrate/models.py @@ -0,0 +1,5 @@ +from tortoise import Model, fields + + +class Foo(Model): + name = fields.CharField(max_length=60, db_index=False) diff --git a/tests/assets/sqlite_migrate/settings.py b/tests/assets/sqlite_migrate/settings.py new file mode 100644 index 0000000..bfefc9f --- /dev/null +++ b/tests/assets/sqlite_migrate/settings.py @@ -0,0 +1,4 @@ +TORTOISE_ORM = { + "connections": {"default": "sqlite://db.sqlite3"}, + "apps": {"models": {"models": ["models", "aerich.models"]}}, +} diff --git a/tests/old_models.py b/tests/old_models.py index 92eb7f8..99f123f 100644 --- a/tests/old_models.py +++ b/tests/old_models.py @@ -52,6 +52,7 @@ class Category(Model): user: fields.ForeignKeyRelation[User] = fields.ForeignKeyField( "models.User", description="User" ) + title = fields.CharField(max_length=20, unique=True) created_at = fields.DatetimeField(auto_now_add=True) diff --git a/tests/test_fake.py b/tests/test_fake.py new file mode 100644 index 0000000..53d3175 --- /dev/null +++ b/tests/test_fake.py @@ -0,0 +1,149 @@ +from __future__ import annotations + +import os +import re +import shlex +import subprocess +import sys +from pathlib import Path + +import pytest + +from aerich.ddl.sqlite import SqliteDDL +from aerich.migrate import Migrate +from tests._utils import chdir, copy_files + + +def run_shell(command: str, capture_output=True, **kw) -> str: + r = subprocess.run(shlex.split(command), capture_output=capture_output) + if r.returncode != 0 and r.stderr: + return r.stderr.decode() + if not r.stdout: + return "" + return r.stdout.decode() + + +@pytest.fixture +def new_aerich_project(tmp_path: Path): + test_dir = Path(__file__).parent + asset_dir = test_dir / "assets" / "fake" + settings_py = asset_dir / "settings.py" + _tests_py = asset_dir / "_tests.py" + db_py = asset_dir / "db.py" + models_py = test_dir / "models.py" + models_second_py = test_dir / "models_second.py" + copy_files(settings_py, _tests_py, models_py, models_second_py, db_py, target_dir=tmp_path) + dst_dir = tmp_path / "tests" + dst_dir.mkdir() + dst_dir.joinpath("__init__.py").touch() + copy_files(test_dir / "_utils.py", test_dir / "indexes.py", target_dir=dst_dir) + if should_remove := str(tmp_path) not in sys.path: + sys.path.append(str(tmp_path)) + with chdir(tmp_path): + run_shell("python db.py create", capture_output=False) + try: + yield + finally: + if not os.getenv("AERICH_DONT_DROP_FAKE_DB"): + run_shell("python db.py drop", capture_output=False) + if should_remove: + sys.path.remove(str(tmp_path)) + + +def _append_field(*files: str, name="field_1") -> None: + for file in files: + p = Path(file) + field = f" {name} = fields.IntField(default=0)" + with p.open("a") as f: + f.write(os.linesep + field) + + +def test_fake(new_aerich_project): + if (ddl := getattr(Migrate, "ddl", None)) and isinstance(ddl, SqliteDDL): + # TODO: go ahead if sqlite alter-column supported + return + output = run_shell("aerich init -t settings.TORTOISE_ORM") + assert "Success" in output + output = run_shell("aerich init-db") + assert "Success" in output + output = run_shell("aerich --app models_second init-db") + assert "Success" in output + output = run_shell("pytest _tests.py::test_init_db") + assert "error" not in output.lower() + _append_field("models.py", "models_second.py") + output = run_shell("aerich migrate") + assert "Success" in output + output = run_shell("aerich --app models_second migrate") + assert "Success" in output + output = run_shell("aerich upgrade --fake") + assert "FAKED" in output + output = run_shell("aerich --app models_second upgrade --fake") + assert "FAKED" in output + output = run_shell("pytest _tests.py::test_fake_field_1") + assert "error" not in output.lower() + _append_field("models.py", "models_second.py", name="field_2") + output = run_shell("aerich migrate") + assert "Success" in output + output = run_shell("aerich --app models_second migrate") + assert "Success" in output + output = run_shell("aerich heads") + assert "_update.py" in output + output = run_shell("aerich upgrade --fake") + assert "FAKED" in output + output = run_shell("aerich --app models_second upgrade --fake") + assert "FAKED" in output + output = run_shell("pytest _tests.py::test_fake_field_2") + assert "error" not in output.lower() + output = run_shell("aerich heads") + assert "No available heads." in output + output = run_shell("aerich --app models_second heads") + assert "No available heads." in output + _append_field("models.py", "models_second.py", name="field_3") + run_shell("aerich migrate", capture_output=False) + run_shell("aerich --app models_second migrate", capture_output=False) + run_shell("aerich upgrade --fake", capture_output=False) + run_shell("aerich --app models_second upgrade --fake", capture_output=False) + output = run_shell("aerich downgrade --fake -v 2 --yes", input="y\n") + assert "FAKED" in output + output = run_shell("aerich --app models_second downgrade --fake -v 2 --yes", input="y\n") + assert "FAKED" in output + output = run_shell("aerich heads") + assert "No available heads." not in output + assert not re.search(r"1_\d+_update\.py", output) + assert re.search(r"2_\d+_update\.py", output) + output = run_shell("aerich --app models_second heads") + assert "No available heads." not in output + assert not re.search(r"1_\d+_update\.py", output) + assert re.search(r"2_\d+_update\.py", output) + output = run_shell("aerich downgrade --fake -v 1 --yes", input="y\n") + assert "FAKED" in output + output = run_shell("aerich --app models_second downgrade --fake -v 1 --yes", input="y\n") + assert "FAKED" in output + output = run_shell("aerich heads") + assert "No available heads." not in output + assert re.search(r"1_\d+_update\.py", output) + assert re.search(r"2_\d+_update\.py", output) + output = run_shell("aerich --app models_second heads") + assert "No available heads." not in output + assert re.search(r"1_\d+_update\.py", output) + assert re.search(r"2_\d+_update\.py", output) + output = run_shell("aerich upgrade --fake") + assert "FAKED" in output + output = run_shell("aerich --app models_second upgrade --fake") + assert "FAKED" in output + output = run_shell("aerich heads") + assert "No available heads." in output + output = run_shell("aerich --app models_second heads") + assert "No available heads." in output + output = run_shell("aerich downgrade --fake -v 1 --yes", input="y\n") + assert "FAKED" in output + output = run_shell("aerich --app models_second downgrade --fake -v 1 --yes", input="y\n") + assert "FAKED" in output + output = run_shell("aerich heads") + assert "No available heads." not in output + assert re.search(r"1_\d+_update\.py", output) + assert re.search(r"2_\d+_update\.py", output) + output = run_shell("aerich --app models_second heads") + assert "No available heads." not in output + assert re.search(r"1_\d+_update\.py", output) + assert re.search(r"2_\d+_update\.py", output) diff --git a/tests/test_migrate.py b/tests/test_migrate.py index 4838b39..1f97f4a 100644 --- a/tests/test_migrate.py +++ b/tests/test_migrate.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pathlib import Path import pytest @@ -13,6 +15,14 @@ from aerich.migrate import MIGRATE_TEMPLATE, Migrate from aerich.utils import get_models_describe from tests.indexes import CustomIndex + +def describe_index(idx: Index) -> Index | dict: + # tortoise-orm>=0.24 changes Index desribe to be dict + if tortoise.__version__ < "0.24": + return idx + return idx.describe() # type:ignore + + # tortoise-orm>=0.21 changes IntField constraints # from {"ge": 1, "le": 2147483647} to {"ge": -2147483648, "le": 2147483647} MIN_INT = 1 if tortoise.__version__ < "0.21" else -2147483648 @@ -640,7 +650,10 @@ old_models_describe = { "description": None, "docstring": None, "unique_together": [], - "indexes": [Index(fields=("username", "is_active")), CustomIndex(fields=("is_superuser",))], + "indexes": [ + describe_index(Index(fields=("username", "is_active"))), + describe_index(CustomIndex(fields=("is_superuser",))), + ], "pk_field": { "name": "id", "field_type": "IntField", @@ -958,7 +971,6 @@ def test_migrate(mocker: MockerFixture): "ALTER TABLE `config` ADD `user_id` INT NOT NULL COMMENT 'User'", "ALTER TABLE `config` ADD CONSTRAINT `fk_config_user_17daa970` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE", "ALTER TABLE `config` ALTER COLUMN `status` DROP DEFAULT", - "ALTER TABLE `config` MODIFY COLUMN `value` JSON NOT NULL", "ALTER TABLE `email` ADD `address` VARCHAR(200) NOT NULL", "ALTER TABLE `email` ADD CONSTRAINT `fk_email_config_76a9dc71` FOREIGN KEY (`config_id`) REFERENCES `config` (`id`) ON DELETE CASCADE", "ALTER TABLE `email` ADD `config_id` INT NOT NULL UNIQUE", @@ -971,23 +983,24 @@ def test_migrate(mocker: MockerFixture): "ALTER TABLE `email` ADD INDEX `idx_email_email_4a1a33` (`email`)", "ALTER TABLE `product` ADD UNIQUE INDEX `uid_product_name_869427` (`name`, `type_db_alias`)", "ALTER TABLE `product` ALTER COLUMN `view_num` SET DEFAULT 0", - "ALTER TABLE `product` MODIFY COLUMN `created_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)", "ALTER TABLE `product` RENAME COLUMN `is_delete` TO `is_deleted`", "ALTER TABLE `product` RENAME COLUMN `is_review` TO `is_reviewed`", "ALTER TABLE `user` DROP COLUMN `avatar`", "ALTER TABLE `user` MODIFY COLUMN `password` VARCHAR(100) NOT NULL", - "ALTER TABLE `user` MODIFY COLUMN `intro` LONGTEXT NOT NULL", - "ALTER TABLE `user` MODIFY COLUMN `last_login` DATETIME(6) NOT NULL COMMENT 'Last Login'", "ALTER TABLE `user` MODIFY COLUMN `longitude` DECIMAL(10,8) NOT NULL", "ALTER TABLE `user` ADD UNIQUE INDEX `username` (`username`)", "CREATE TABLE `email_user` (\n `email_id` INT NOT NULL REFERENCES `email` (`email_id`) ON DELETE CASCADE,\n `user_id` INT NOT NULL REFERENCES `user` (`id`) ON DELETE CASCADE\n) CHARACTER SET utf8mb4", "CREATE TABLE IF NOT EXISTS `newmodel` (\n `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT,\n `name` VARCHAR(50) NOT NULL\n) CHARACTER SET utf8mb4", - "ALTER TABLE `category` MODIFY COLUMN `created_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)", - "ALTER TABLE `product` MODIFY COLUMN `body` LONGTEXT NOT NULL", "CREATE TABLE `product_user` (\n `product_id` INT NOT NULL REFERENCES `product` (`id`) ON DELETE CASCADE,\n `user_id` INT NOT NULL REFERENCES `user` (`id`) ON DELETE CASCADE\n) CHARACTER SET utf8mb4", "CREATE TABLE `config_category_map` (\n `category_id` INT NOT NULL REFERENCES `category` (`id`) ON DELETE CASCADE,\n `config_id` INT NOT NULL REFERENCES `config` (`id`) ON DELETE CASCADE\n) CHARACTER SET utf8mb4", "DROP TABLE IF EXISTS `config_category`", } + upgrade_operators = set(Migrate.upgrade_operators) + upgrade_more_than_expected = upgrade_operators - expected_upgrade_operators + assert not upgrade_more_than_expected + upgrade_less_than_expected = expected_upgrade_operators - upgrade_operators + assert not upgrade_less_than_expected + expected_downgrade_operators = { "ALTER TABLE `category` MODIFY COLUMN `name` VARCHAR(200) NOT NULL", "ALTER TABLE `category` MODIFY COLUMN `slug` VARCHAR(200) NOT NULL", @@ -1020,28 +1033,21 @@ def test_migrate(mocker: MockerFixture): "DROP TABLE IF EXISTS `email_user`", "DROP TABLE IF EXISTS `newmodel`", "DROP TABLE IF EXISTS `product_user`", - "ALTER TABLE `user` MODIFY COLUMN `intro` LONGTEXT NOT NULL", - "ALTER TABLE `config` MODIFY COLUMN `value` TEXT NOT NULL", - "ALTER TABLE `category` MODIFY COLUMN `created_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)", - "ALTER TABLE `product` MODIFY COLUMN `created_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)", - "ALTER TABLE `user` MODIFY COLUMN `last_login` DATETIME(6) NOT NULL COMMENT 'Last Login'", "ALTER TABLE `user` MODIFY COLUMN `longitude` DECIMAL(12,9) NOT NULL", - "ALTER TABLE `product` MODIFY COLUMN `body` LONGTEXT NOT NULL", "CREATE TABLE `config_category` (\n `config_id` INT NOT NULL REFERENCES `config` (`id`) ON DELETE CASCADE,\n `category_id` INT NOT NULL REFERENCES `category` (`id`) ON DELETE CASCADE\n) CHARACTER SET utf8mb4", "DROP TABLE IF EXISTS `config_category_map`", } - assert not set(Migrate.upgrade_operators).symmetric_difference(expected_upgrade_operators) - - assert not set(Migrate.downgrade_operators).symmetric_difference( - expected_downgrade_operators - ) + downgrade_operators = set(Migrate.downgrade_operators) + downgrade_more_than_expected = downgrade_operators - expected_downgrade_operators + assert not downgrade_more_than_expected + downgrade_less_than_expected = expected_downgrade_operators - downgrade_operators + assert not downgrade_less_than_expected elif isinstance(Migrate.ddl, PostgresDDL): expected_upgrade_operators = { 'DROP INDEX IF EXISTS "uid_category_title_f7fc03"', 'ALTER TABLE "category" ALTER COLUMN "name" DROP NOT NULL', 'ALTER TABLE "category" ALTER COLUMN "slug" TYPE VARCHAR(100) USING "slug"::VARCHAR(100)', - 'ALTER TABLE "category" ALTER COLUMN "created_at" TYPE TIMESTAMPTZ USING "created_at"::TIMESTAMPTZ', 'ALTER TABLE "category" RENAME COLUMN "user_id" TO "owner_id"', 'ALTER TABLE "category" ADD CONSTRAINT "fk_category_user_110d4c63" FOREIGN KEY ("owner_id") REFERENCES "user" ("id") ON DELETE CASCADE', 'ALTER TABLE "config" DROP COLUMN "name"', @@ -1049,7 +1055,6 @@ def test_migrate(mocker: MockerFixture): 'ALTER TABLE "config" ADD "user_id" INT NOT NULL', 'ALTER TABLE "config" ADD CONSTRAINT "fk_config_user_17daa970" FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE', 'ALTER TABLE "config" ALTER COLUMN "status" DROP DEFAULT', - 'ALTER TABLE "config" ALTER COLUMN "value" TYPE JSONB USING "value"::JSONB', 'ALTER TABLE "configs" RENAME TO "config"', 'ALTER TABLE "email" ADD "address" VARCHAR(200) NOT NULL', 'ALTER TABLE "email" RENAME COLUMN "id" TO "email_id"', @@ -1060,14 +1065,10 @@ def test_migrate(mocker: MockerFixture): 'ALTER TABLE "product" DROP COLUMN "uuid"', 'ALTER TABLE "product" ALTER COLUMN "view_num" SET DEFAULT 0', 'ALTER TABLE "product" RENAME COLUMN "image" TO "pic"', - 'ALTER TABLE "product" ALTER COLUMN "body" TYPE TEXT USING "body"::TEXT', - 'ALTER TABLE "product" ALTER COLUMN "created_at" TYPE TIMESTAMPTZ USING "created_at"::TIMESTAMPTZ', 'ALTER TABLE "product" RENAME COLUMN "is_review" TO "is_reviewed"', 'ALTER TABLE "product" RENAME COLUMN "is_delete" TO "is_deleted"', 'ALTER TABLE "user" ALTER COLUMN "password" TYPE VARCHAR(100) USING "password"::VARCHAR(100)', 'ALTER TABLE "user" DROP COLUMN "avatar"', - 'ALTER TABLE "user" ALTER COLUMN "last_login" TYPE TIMESTAMPTZ USING "last_login"::TIMESTAMPTZ', - 'ALTER TABLE "user" ALTER COLUMN "intro" TYPE TEXT USING "intro"::TEXT', 'ALTER TABLE "user" ALTER COLUMN "longitude" TYPE DECIMAL(10,8) USING "longitude"::DECIMAL(10,8)', 'CREATE INDEX "idx_product_name_869427" ON "product" ("name", "type_db_alias")', 'CREATE INDEX "idx_email_email_4a1a33" ON "email" ("email")', @@ -1079,11 +1080,16 @@ def test_migrate(mocker: MockerFixture): 'CREATE TABLE "config_category_map" (\n "category_id" INT NOT NULL REFERENCES "category" ("id") ON DELETE CASCADE,\n "config_id" INT NOT NULL REFERENCES "config" ("id") ON DELETE CASCADE\n)', 'DROP TABLE IF EXISTS "config_category"', } + upgrade_operators = set(Migrate.upgrade_operators) + upgrade_more_than_expected = upgrade_operators - expected_upgrade_operators + assert not upgrade_more_than_expected + upgrade_less_than_expected = expected_upgrade_operators - upgrade_operators + assert not upgrade_less_than_expected + expected_downgrade_operators = { 'CREATE UNIQUE INDEX "uid_category_title_f7fc03" ON "category" ("title")', 'ALTER TABLE "category" ALTER COLUMN "name" SET NOT NULL', 'ALTER TABLE "category" ALTER COLUMN "slug" TYPE VARCHAR(200) USING "slug"::VARCHAR(200)', - 'ALTER TABLE "category" ALTER COLUMN "created_at" TYPE TIMESTAMPTZ USING "created_at"::TIMESTAMPTZ', 'ALTER TABLE "category" RENAME COLUMN "owner_id" TO "user_id"', 'ALTER TABLE "category" DROP CONSTRAINT IF EXISTS "fk_category_user_110d4c63"', 'ALTER TABLE "config" ADD "name" VARCHAR(100) NOT NULL UNIQUE', @@ -1091,7 +1097,6 @@ def test_migrate(mocker: MockerFixture): 'ALTER TABLE "config" ALTER COLUMN "status" SET DEFAULT 1', 'ALTER TABLE "config" DROP CONSTRAINT IF EXISTS "fk_config_user_17daa970"', 'ALTER TABLE "config" RENAME TO "configs"', - 'ALTER TABLE "config" ALTER COLUMN "value" TYPE JSONB USING "value"::JSONB', 'ALTER TABLE "config" DROP COLUMN "user_id"', 'ALTER TABLE "email" ADD "user_id" INT NOT NULL', 'ALTER TABLE "email" DROP COLUMN "address"', @@ -1106,11 +1111,7 @@ def test_migrate(mocker: MockerFixture): 'ALTER TABLE "product" RENAME COLUMN "is_reviewed" TO "is_review"', 'ALTER TABLE "user" ADD "avatar" VARCHAR(200) NOT NULL DEFAULT \'\'', 'ALTER TABLE "user" ALTER COLUMN "password" TYPE VARCHAR(200) USING "password"::VARCHAR(200)', - 'ALTER TABLE "user" ALTER COLUMN "last_login" TYPE TIMESTAMPTZ USING "last_login"::TIMESTAMPTZ', - 'ALTER TABLE "user" ALTER COLUMN "intro" TYPE TEXT USING "intro"::TEXT', 'ALTER TABLE "user" ALTER COLUMN "longitude" TYPE DECIMAL(12,9) USING "longitude"::DECIMAL(12,9)', - 'ALTER TABLE "product" ALTER COLUMN "created_at" TYPE TIMESTAMPTZ USING "created_at"::TIMESTAMPTZ', - 'ALTER TABLE "product" ALTER COLUMN "body" TYPE TEXT USING "body"::TEXT', 'DROP TABLE IF EXISTS "product_user"', 'DROP INDEX IF EXISTS "idx_product_name_869427"', 'DROP INDEX IF EXISTS "idx_email_email_4a1a33"', @@ -1121,10 +1122,11 @@ def test_migrate(mocker: MockerFixture): 'CREATE TABLE "config_category" (\n "config_id" INT NOT NULL REFERENCES "config" ("id") ON DELETE CASCADE,\n "category_id" INT NOT NULL REFERENCES "category" ("id") ON DELETE CASCADE\n)', 'DROP TABLE IF EXISTS "config_category_map"', } - assert not set(Migrate.upgrade_operators).symmetric_difference(expected_upgrade_operators) - assert not set(Migrate.downgrade_operators).symmetric_difference( - expected_downgrade_operators - ) + downgrade_operators = set(Migrate.downgrade_operators) + downgrade_more_than_expected = downgrade_operators - expected_downgrade_operators + assert not downgrade_more_than_expected + downgrade_less_than_expected = expected_downgrade_operators - downgrade_operators + assert not downgrade_less_than_expected elif isinstance(Migrate.ddl, SqliteDDL): assert Migrate.upgrade_operators == [] diff --git a/tests/test_sqlite_migrate.py b/tests/test_sqlite_migrate.py index b629635..735defb 100644 --- a/tests/test_sqlite_migrate.py +++ b/tests/test_sqlite_migrate.py @@ -3,161 +3,16 @@ import os import shlex import shutil import subprocess -import sys from pathlib import Path from aerich.ddl.sqlite import SqliteDDL from aerich.migrate import Migrate - -if sys.version_info >= (3, 11): - from contextlib import chdir -else: - - class chdir(contextlib.AbstractContextManager): # Copied from source code of Python3.13 - """Non thread-safe context manager to change the current working directory.""" - - def __init__(self, path): - self.path = path - self._old_cwd = [] - - def __enter__(self): - self._old_cwd.append(os.getcwd()) - os.chdir(self.path) - - def __exit__(self, *excinfo): - os.chdir(self._old_cwd.pop()) - - -MODELS = """from __future__ import annotations - -from tortoise import Model, fields - - -class Foo(Model): - name = fields.CharField(max_length=60, db_index=False) -""" - -SETTINGS = """from __future__ import annotations - -TORTOISE_ORM = { - "connections": {"default": "sqlite://db.sqlite3"}, - "apps": {"models": {"models": ["models", "aerich.models"]}}, -} -""" - -CONFTEST = """from __future__ import annotations - -import asyncio -from typing import Generator - -import pytest -import pytest_asyncio -from tortoise import Tortoise, connections - -import settings - - -@pytest.fixture(scope="session") -def event_loop() -> Generator: - policy = asyncio.get_event_loop_policy() - res = policy.new_event_loop() - asyncio.set_event_loop(res) - res._close = res.close # type:ignore[attr-defined] - res.close = lambda: None # type:ignore[method-assign] - - yield res - - res._close() # type:ignore[attr-defined] - - -@pytest_asyncio.fixture(scope="session", autouse=True) -async def api(event_loop, request): - await Tortoise.init(config=settings.TORTOISE_ORM) - request.addfinalizer(lambda: event_loop.run_until_complete(connections.close_all(discard=True))) -""" - -TESTS = """from __future__ import annotations - -import uuid - -import pytest -from tortoise.exceptions import IntegrityError - -from models import Foo - - -@pytest.mark.asyncio -async def test_allow_duplicate() -> None: - await Foo.all().delete() - await Foo.create(name="foo") - obj = await Foo.create(name="foo") - assert (await Foo.all().count()) == 2 - await obj.delete() - - -@pytest.mark.asyncio -async def test_unique_is_true() -> None: - with pytest.raises(IntegrityError): - await Foo.create(name="foo") - - -@pytest.mark.asyncio -async def test_add_unique_field() -> None: - if not await Foo.filter(age=0).exists(): - await Foo.create(name="0_"+uuid.uuid4().hex, age=0) - with pytest.raises(IntegrityError): - await Foo.create(name=uuid.uuid4().hex, age=0) - - -@pytest.mark.asyncio -async def test_drop_unique_field() -> None: - name = "1_" + uuid.uuid4().hex - await Foo.create(name=name, age=0) - assert (await Foo.filter(name=name).exists()) - - -@pytest.mark.asyncio -async def test_with_age_field() -> None: - name = "2_" + uuid.uuid4().hex - await Foo.create(name=name, age=0) - obj = await Foo.get(name=name) - assert obj.age == 0 - - -@pytest.mark.asyncio -async def test_without_age_field() -> None: - name = "3_" + uuid.uuid4().hex - await Foo.create(name=name, age=0) - obj = await Foo.get(name=name) - assert getattr(obj, "age", None) is None - - -@pytest.mark.asyncio -async def test_m2m_with_custom_through() -> None: - from models import Group, FooGroup - name = "4_" + uuid.uuid4().hex - foo = await Foo.create(name=name) - group = await Group.create(name=name+"1") - await FooGroup.all().delete() - await foo.groups.add(group) - foo_group = await FooGroup.get(foo=foo, group=group) - assert not foo_group.is_active - - -@pytest.mark.asyncio -async def test_add_m2m_field_after_init_db() -> None: - from models import Group - name = "5_" + uuid.uuid4().hex - foo = await Foo.create(name=name) - group = await Group.create(name=name+"1") - await foo.groups.add(group) - assert (await group.users.all().first()) == foo -""" +from tests._utils import chdir, copy_files def run_aerich(cmd: str) -> None: with contextlib.suppress(subprocess.TimeoutExpired): - if not cmd.startswith("aerich"): + if not cmd.startswith("aerich") and not cmd.startswith("poetry"): cmd = "aerich " + cmd subprocess.run(shlex.split(cmd), timeout=2) @@ -170,60 +25,60 @@ def run_shell(cmd: str) -> subprocess.CompletedProcess: def test_sqlite_migrate(tmp_path: Path) -> None: if (ddl := getattr(Migrate, "ddl", None)) and not isinstance(ddl, SqliteDDL): return + test_dir = Path(__file__).parent + asset_dir = test_dir / "assets" / "sqlite_migrate" with chdir(tmp_path): - models_py = Path("models.py") - settings_py = Path("settings.py") - test_py = Path("_test.py") - models_py.write_text(MODELS) - settings_py.write_text(SETTINGS) - test_py.write_text(TESTS) - Path("conftest.py").write_text(CONFTEST) + files = ("models.py", "settings.py", "_tests.py") + copy_files(*(asset_dir / f for f in files), target_dir=Path()) + models_py, settings_py, test_py = (Path(f) for f in files) + copy_files(asset_dir / "conftest_.py", target_dir=Path("conftest.py")) if (db_file := Path("db.sqlite3")).exists(): db_file.unlink() + MODELS = models_py.read_text("utf-8") run_aerich("aerich init -t settings.TORTOISE_ORM") config_file = Path("pyproject.toml") modify_time = config_file.stat().st_mtime run_aerich("aerich init-db") run_aerich("aerich init -t settings.TORTOISE_ORM") assert modify_time == config_file.stat().st_mtime - r = run_shell("pytest _test.py::test_allow_duplicate") + r = run_shell("pytest _tests.py::test_allow_duplicate") assert r.returncode == 0 # Add index models_py.write_text(MODELS.replace("index=False", "index=True")) run_aerich("aerich migrate") # migrations/models/1_ run_aerich("aerich upgrade") - r = run_shell("pytest -s _test.py::test_allow_duplicate") + r = run_shell("pytest -s _tests.py::test_allow_duplicate") assert r.returncode == 0 # Drop index models_py.write_text(MODELS) run_aerich("aerich migrate") # migrations/models/2_ run_aerich("aerich upgrade") - r = run_shell("pytest -s _test.py::test_allow_duplicate") + r = run_shell("pytest -s _tests.py::test_allow_duplicate") assert r.returncode == 0 # Add unique index models_py.write_text(MODELS.replace("index=False", "index=True, unique=True")) run_aerich("aerich migrate") # migrations/models/3_ run_aerich("aerich upgrade") - r = run_shell("pytest _test.py::test_unique_is_true") + r = run_shell("pytest _tests.py::test_unique_is_true") assert r.returncode == 0 # Drop unique index models_py.write_text(MODELS) run_aerich("aerich migrate") # migrations/models/4_ run_aerich("aerich upgrade") - r = run_shell("pytest _test.py::test_allow_duplicate") + r = run_shell("pytest _tests.py::test_allow_duplicate") assert r.returncode == 0 # Add field with unique=True with models_py.open("a") as f: f.write(" age = fields.IntField(unique=True, default=0)") run_aerich("aerich migrate") # migrations/models/5_ run_aerich("aerich upgrade") - r = run_shell("pytest _test.py::test_add_unique_field") + r = run_shell("pytest _tests.py::test_add_unique_field") assert r.returncode == 0 # Drop unique field models_py.write_text(MODELS) run_aerich("aerich migrate") # migrations/models/6_ run_aerich("aerich upgrade") - r = run_shell("pytest -s _test.py::test_drop_unique_field") + r = run_shell("pytest -s _tests.py::test_drop_unique_field") assert r.returncode == 0 # Initial with indexed field and then drop it @@ -235,14 +90,14 @@ def test_sqlite_migrate(tmp_path: Path) -> None: run_aerich("aerich init-db") migration_file = list(migrations_dir.glob("0_*.py"))[0] assert "CREATE INDEX" in migration_file.read_text() - r = run_shell("pytest _test.py::test_with_age_field") + r = run_shell("pytest _tests.py::test_with_age_field") assert r.returncode == 0 models_py.write_text(MODELS) run_aerich("aerich migrate") run_aerich("aerich upgrade") migration_file_1 = list(migrations_dir.glob("1_*.py"))[0] assert "DROP INDEX" in migration_file_1.read_text() - r = run_shell("pytest _test.py::test_without_age_field") + r = run_shell("pytest _tests.py::test_without_age_field") assert r.returncode == 0 # Generate migration file in emptry directory @@ -283,7 +138,7 @@ class FooGroup(Model): run_aerich("aerich upgrade") migration_file_1 = list(migrations_dir.glob("1_*.py"))[0] assert "foo_group" in migration_file_1.read_text() - r = run_shell("pytest _test.py::test_m2m_with_custom_through") + r = run_shell("pytest _tests.py::test_m2m_with_custom_through") assert r.returncode == 0 # add m2m field after init-db @@ -304,5 +159,5 @@ class Group(Model): run_aerich("aerich upgrade") migration_file_1 = list(migrations_dir.glob("1_*.py"))[0] assert "foo_group" in migration_file_1.read_text() - r = run_shell("pytest _test.py::test_add_m2m_field_after_init_db") + r = run_shell("pytest _tests.py::test_add_m2m_field_after_init_db") assert r.returncode == 0