From a15f1c9a2961bd2602ac2a569fa98801a00d2c9c Mon Sep 17 00:00:00 2001 From: Jufik Date: Tue, 31 Oct 2023 19:56:41 +0100 Subject: [PATCH 01/12] first WIP --- .../simple_hero_migration/__init__.py | 0 .../simple_hero_migration/alembic.ini | 116 +++++++++++++++++ .../simple_hero_migration/alembic001.ini | 116 +++++++++++++++++ .../simple_hero_migration/migrations/001.py | 76 +++++++++++ .../simple_hero_migration/migrations/README | 2 + .../migrations/__init__.py | 0 .../simple_hero_migration/migrations/env.py | 76 +++++++++++ .../migrations/script.py.mako | 27 ++++ ...46-1e606859995a_migrating_me_iam_famous.py | 37 ++++++ .../migrations/versions/__init__.py | 0 .../simple_hero_migration/models.py | 10 ++ pyproject.toml | 25 ++-- sqlmodel/__init__.py | 10 +- sqlmodel/cli/__init__.py | 17 +++ sqlmodel/cli/migrations/__init__.py | 2 + sqlmodel/cli/migrations/cli.py | 49 +++++++ sqlmodel/cli/migrations/templates/LICENSE.md | 1 + .../cli/migrations/templates/async/README | 1 + .../templates/async/alembic.ini.mako | 114 +++++++++++++++++ .../migrations/templates/async/script.py.mako | 26 ++++ .../cli/migrations/templates/generic/README | 2 + .../templates/generic/alembic.ini.mako | 116 +++++++++++++++++ .../templates/generic/script.py.mako | 26 ++++ .../cli/migrations/templates/multidb/README | 12 ++ .../templates/multidb/alembic.ini.mako | 121 ++++++++++++++++++ .../templates/multidb/script.py.mako | 47 +++++++ sqlmodel/main.py | 42 ++---- sqlmodel/orm/session.py | 3 +- sqlmodel/sql/expression.py | 14 +- 29 files changed, 1026 insertions(+), 62 deletions(-) create mode 100644 docs_src/tutorial/migrations/simple_hero_migration/__init__.py create mode 100644 docs_src/tutorial/migrations/simple_hero_migration/alembic.ini create mode 100644 docs_src/tutorial/migrations/simple_hero_migration/alembic001.ini create mode 100644 docs_src/tutorial/migrations/simple_hero_migration/migrations/001.py create mode 100644 docs_src/tutorial/migrations/simple_hero_migration/migrations/README create mode 100644 docs_src/tutorial/migrations/simple_hero_migration/migrations/__init__.py create mode 100644 docs_src/tutorial/migrations/simple_hero_migration/migrations/env.py create mode 100644 docs_src/tutorial/migrations/simple_hero_migration/migrations/script.py.mako create mode 100644 docs_src/tutorial/migrations/simple_hero_migration/migrations/versions/2023_10_31_1946-1e606859995a_migrating_me_iam_famous.py create mode 100644 docs_src/tutorial/migrations/simple_hero_migration/migrations/versions/__init__.py create mode 100644 docs_src/tutorial/migrations/simple_hero_migration/models.py create mode 100644 sqlmodel/cli/__init__.py create mode 100644 sqlmodel/cli/migrations/__init__.py create mode 100644 sqlmodel/cli/migrations/cli.py create mode 100644 sqlmodel/cli/migrations/templates/LICENSE.md create mode 100644 sqlmodel/cli/migrations/templates/async/README create mode 100644 sqlmodel/cli/migrations/templates/async/alembic.ini.mako create mode 100644 sqlmodel/cli/migrations/templates/async/script.py.mako create mode 100644 sqlmodel/cli/migrations/templates/generic/README create mode 100644 sqlmodel/cli/migrations/templates/generic/alembic.ini.mako create mode 100644 sqlmodel/cli/migrations/templates/generic/script.py.mako create mode 100644 sqlmodel/cli/migrations/templates/multidb/README create mode 100644 sqlmodel/cli/migrations/templates/multidb/alembic.ini.mako create mode 100644 sqlmodel/cli/migrations/templates/multidb/script.py.mako diff --git a/docs_src/tutorial/migrations/simple_hero_migration/__init__.py b/docs_src/tutorial/migrations/simple_hero_migration/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/tutorial/migrations/simple_hero_migration/alembic.ini b/docs_src/tutorial/migrations/simple_hero_migration/alembic.ini new file mode 100644 index 0000000000..f9363281d5 --- /dev/null +++ b/docs_src/tutorial/migrations/simple_hero_migration/alembic.ini @@ -0,0 +1,116 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = migrations + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python-dateutil library that can be +# installed by adding `alembic[tz]` to the pip requirements +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to migrations/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = sqlite:///database.db + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the exec runner, execute a binary +# hooks = ruff +# ruff.type = exec +# ruff.executable = %(here)s/.venv/bin/ruff +# ruff.options = --fix REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/docs_src/tutorial/migrations/simple_hero_migration/alembic001.ini b/docs_src/tutorial/migrations/simple_hero_migration/alembic001.ini new file mode 100644 index 0000000000..07489da977 --- /dev/null +++ b/docs_src/tutorial/migrations/simple_hero_migration/alembic001.ini @@ -0,0 +1,116 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = migrations + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python-dateutil library that can be +# installed by adding `alembic[tz]` to the pip requirements +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to migrations/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = driver://user:pass@localhost/dbname + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the exec runner, execute a binary +# hooks = ruff +# ruff.type = exec +# ruff.executable = %(here)s/.venv/bin/ruff +# ruff.options = --fix REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/docs_src/tutorial/migrations/simple_hero_migration/migrations/001.py b/docs_src/tutorial/migrations/simple_hero_migration/migrations/001.py new file mode 100644 index 0000000000..182fc6d541 --- /dev/null +++ b/docs_src/tutorial/migrations/simple_hero_migration/migrations/001.py @@ -0,0 +1,76 @@ +from logging.config import fileConfig + +from alembic import context +from sqlalchemy import engine_from_config, pool +from models import Hero +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = Hero.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/docs_src/tutorial/migrations/simple_hero_migration/migrations/README b/docs_src/tutorial/migrations/simple_hero_migration/migrations/README new file mode 100644 index 0000000000..da94f1daf6 --- /dev/null +++ b/docs_src/tutorial/migrations/simple_hero_migration/migrations/README @@ -0,0 +1,2 @@ +Generic single-database configuration. +I'm the best! \ No newline at end of file diff --git a/docs_src/tutorial/migrations/simple_hero_migration/migrations/__init__.py b/docs_src/tutorial/migrations/simple_hero_migration/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/tutorial/migrations/simple_hero_migration/migrations/env.py b/docs_src/tutorial/migrations/simple_hero_migration/migrations/env.py new file mode 100644 index 0000000000..182fc6d541 --- /dev/null +++ b/docs_src/tutorial/migrations/simple_hero_migration/migrations/env.py @@ -0,0 +1,76 @@ +from logging.config import fileConfig + +from alembic import context +from sqlalchemy import engine_from_config, pool +from models import Hero +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = Hero.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/docs_src/tutorial/migrations/simple_hero_migration/migrations/script.py.mako b/docs_src/tutorial/migrations/simple_hero_migration/migrations/script.py.mako new file mode 100644 index 0000000000..6ce3351093 --- /dev/null +++ b/docs_src/tutorial/migrations/simple_hero_migration/migrations/script.py.mako @@ -0,0 +1,27 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import sqlmodel +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/docs_src/tutorial/migrations/simple_hero_migration/migrations/versions/2023_10_31_1946-1e606859995a_migrating_me_iam_famous.py b/docs_src/tutorial/migrations/simple_hero_migration/migrations/versions/2023_10_31_1946-1e606859995a_migrating_me_iam_famous.py new file mode 100644 index 0000000000..a7c739a277 --- /dev/null +++ b/docs_src/tutorial/migrations/simple_hero_migration/migrations/versions/2023_10_31_1946-1e606859995a_migrating_me_iam_famous.py @@ -0,0 +1,37 @@ +"""migrating me iam famous + +Revision ID: 1e606859995a +Revises: +Create Date: 2023-10-31 19:46:27.802669 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import sqlmodel + + +# revision identifiers, used by Alembic. +revision: str = '1e606859995a' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('hero', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('secret_name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('age', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('hero') + # ### end Alembic commands ### diff --git a/docs_src/tutorial/migrations/simple_hero_migration/migrations/versions/__init__.py b/docs_src/tutorial/migrations/simple_hero_migration/migrations/versions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/tutorial/migrations/simple_hero_migration/models.py b/docs_src/tutorial/migrations/simple_hero_migration/models.py new file mode 100644 index 0000000000..509d282b1e --- /dev/null +++ b/docs_src/tutorial/migrations/simple_hero_migration/models.py @@ -0,0 +1,10 @@ +from typing import Optional + +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str + secret_name: str + age: Optional[int] = None diff --git a/pyproject.toml b/pyproject.toml index 23fa79bf31..e84a84eb43 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,16 @@ classifiers = [ python = "^3.7" SQLAlchemy = ">=1.4.36,<2.0.0" pydantic = "^1.9.0" -sqlalchemy2-stubs = {version = "*", allow-prereleases = true} +sqlalchemy2-stubs = { version = "*", allow-prereleases = true } +typer = "^0.9.0" +alembic = "^1.12.0" + +[tool.poetry.scripts] +sqlmodel = "sqlmodel.cli:cli" + +[tool.poetry.plugins."sqlmodel"] +migrations = "sqlmodel.cli.migrations:migrations" + [tool.poetry.group.dev.dependencies] pytest = "^7.0.1" @@ -44,7 +53,7 @@ mkdocs-material = "9.2.7" pillow = "^9.3.0" cairosvg = "^2.5.2" mdx-include = "^1.4.1" -coverage = {extras = ["toml"], version = ">=6.2,<8.0"} +coverage = { extras = ["toml"], version = ">=6.2,<8.0" } fastapi = "^0.68.1" requests = "^2.26.0" ruff = "^0.1.2" @@ -58,11 +67,7 @@ source = "init" [tool.coverage.run] parallel = true -source = [ - "docs_src", - "tests", - "sqlmodel" -] +source = ["docs_src", "tests", "sqlmodel"] context = '${CONTEXT}' [tool.coverage.report] @@ -91,9 +96,9 @@ select = [ "UP", # pyupgrade ] ignore = [ - "E501", # line too long, handled by black - "B008", # do not perform function calls in argument defaults - "C901", # too complex + "E501", # line too long, handled by black + "B008", # do not perform function calls in argument defaults + "C901", # too complex "W191", # indentation contains tabs ] diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index 495ac9c8a8..4894b8c911 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -24,13 +24,11 @@ from sqlalchemy.schema import ThreadLocalMetaData as ThreadLocalMetaData from sqlalchemy.schema import UniqueConstraint as UniqueConstraint from sqlalchemy.sql import LABEL_STYLE_DEFAULT as LABEL_STYLE_DEFAULT -from sqlalchemy.sql import ( - LABEL_STYLE_DISAMBIGUATE_ONLY as LABEL_STYLE_DISAMBIGUATE_ONLY, -) +from sqlalchemy.sql import \ + LABEL_STYLE_DISAMBIGUATE_ONLY as LABEL_STYLE_DISAMBIGUATE_ONLY from sqlalchemy.sql import LABEL_STYLE_NONE as LABEL_STYLE_NONE -from sqlalchemy.sql import ( - LABEL_STYLE_TABLENAME_PLUS_COL as LABEL_STYLE_TABLENAME_PLUS_COL, -) +from sqlalchemy.sql import \ + LABEL_STYLE_TABLENAME_PLUS_COL as LABEL_STYLE_TABLENAME_PLUS_COL from sqlalchemy.sql import alias as alias from sqlalchemy.sql import all_ as all_ from sqlalchemy.sql import and_ as and_ diff --git a/sqlmodel/cli/__init__.py b/sqlmodel/cli/__init__.py new file mode 100644 index 0000000000..a78013ad0a --- /dev/null +++ b/sqlmodel/cli/__init__.py @@ -0,0 +1,17 @@ +""" +An extendable and simple CLI. +Load plugins from "sqlmodel" entry points. +""" +import pkg_resources +from typer import Typer + +cli = Typer() + + +def get_entry_points(plugin_name: str = "sqlmodel", app: Typer = cli): + for entry_point in pkg_resources.iter_entry_points(plugin_name): + plugin = entry_point.load() + cli.add_typer(plugin, name=plugin.info.name) + + +get_entry_points() diff --git a/sqlmodel/cli/migrations/__init__.py b/sqlmodel/cli/migrations/__init__.py new file mode 100644 index 0000000000..46d55f5201 --- /dev/null +++ b/sqlmodel/cli/migrations/__init__.py @@ -0,0 +1,2 @@ +from sqlmodel.cli.migrations.cli import migrations + diff --git a/sqlmodel/cli/migrations/cli.py b/sqlmodel/cli/migrations/cli.py new file mode 100644 index 0000000000..8a3da1132d --- /dev/null +++ b/sqlmodel/cli/migrations/cli.py @@ -0,0 +1,49 @@ +import configparser +import logging +from pathlib import Path +from typing import Literal, Optional, Union + +import typer +from alembic import command +from alembic.config import Config +from typer import Typer +from typing_extensions import Annotated + +logger = logging.getLogger(__name__) + +migrations = Typer( + name="migrations", help="Commands to interact with Alembic migrations" +) + + +@migrations.command() +def init( + module: Path = ".", + config_file: Path = "alembic.ini", + script_location: Path = "migrations", + template: str = "generic", # Should be Literal["generic", "multidb", "async"] + target_metadata: Annotated[Optional[str], typer.Argument()] = None, +): + """Initialize Alembic""" + _config_file = module / config_file + _script_location = module / script_location + + config = Config(_config_file) + + template_path = Path(__file__).parent.absolute() / "templates" / template + + command.init( + config, + _script_location, + template=template_path, + package=True, + ) + logger.debug(f"Inited alembic") + # Add target_metadata to alembic.ini + + updated_config = configparser.ConfigParser() + updated_config.read(_config_file) + updated_config["alembic"].update({"target_metadata": target_metadata}) + with open(_config_file, "w") as configfile: + updated_config.write(configfile) + logger.debug(f"Alembic defaults overrwritten") diff --git a/sqlmodel/cli/migrations/templates/LICENSE.md b/sqlmodel/cli/migrations/templates/LICENSE.md new file mode 100644 index 0000000000..9fc0da344a --- /dev/null +++ b/sqlmodel/cli/migrations/templates/LICENSE.md @@ -0,0 +1 @@ +This folder is adapted from [Alembic](https://github.com/sqlalchemy/alembic) diff --git a/sqlmodel/cli/migrations/templates/async/README b/sqlmodel/cli/migrations/templates/async/README new file mode 100644 index 0000000000..e0d0858f26 --- /dev/null +++ b/sqlmodel/cli/migrations/templates/async/README @@ -0,0 +1 @@ +Generic single-database configuration with an async dbapi. \ No newline at end of file diff --git a/sqlmodel/cli/migrations/templates/async/alembic.ini.mako b/sqlmodel/cli/migrations/templates/async/alembic.ini.mako new file mode 100644 index 0000000000..bc9f2d50ff --- /dev/null +++ b/sqlmodel/cli/migrations/templates/async/alembic.ini.mako @@ -0,0 +1,114 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = ${script_location} + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python-dateutil library that can be +# installed by adding `alembic[tz]` to the pip requirements +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to ${script_location}/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:${script_location}/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = driver://user:pass@localhost/dbname + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the exec runner, execute a binary +# hooks = ruff +# ruff.type = exec +# ruff.executable = %(here)s/.venv/bin/ruff +# ruff.options = --fix REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/sqlmodel/cli/migrations/templates/async/script.py.mako b/sqlmodel/cli/migrations/templates/async/script.py.mako new file mode 100644 index 0000000000..fbc4b07dce --- /dev/null +++ b/sqlmodel/cli/migrations/templates/async/script.py.mako @@ -0,0 +1,26 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/sqlmodel/cli/migrations/templates/generic/README b/sqlmodel/cli/migrations/templates/generic/README new file mode 100644 index 0000000000..da94f1daf6 --- /dev/null +++ b/sqlmodel/cli/migrations/templates/generic/README @@ -0,0 +1,2 @@ +Generic single-database configuration. +I'm the best! \ No newline at end of file diff --git a/sqlmodel/cli/migrations/templates/generic/alembic.ini.mako b/sqlmodel/cli/migrations/templates/generic/alembic.ini.mako new file mode 100644 index 0000000000..c18ddb4e04 --- /dev/null +++ b/sqlmodel/cli/migrations/templates/generic/alembic.ini.mako @@ -0,0 +1,116 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = ${script_location} + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python-dateutil library that can be +# installed by adding `alembic[tz]` to the pip requirements +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to ${script_location}/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:${script_location}/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = driver://user:pass@localhost/dbname + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the exec runner, execute a binary +# hooks = ruff +# ruff.type = exec +# ruff.executable = %(here)s/.venv/bin/ruff +# ruff.options = --fix REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/sqlmodel/cli/migrations/templates/generic/script.py.mako b/sqlmodel/cli/migrations/templates/generic/script.py.mako new file mode 100644 index 0000000000..fbc4b07dce --- /dev/null +++ b/sqlmodel/cli/migrations/templates/generic/script.py.mako @@ -0,0 +1,26 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/sqlmodel/cli/migrations/templates/multidb/README b/sqlmodel/cli/migrations/templates/multidb/README new file mode 100644 index 0000000000..f046ec9142 --- /dev/null +++ b/sqlmodel/cli/migrations/templates/multidb/README @@ -0,0 +1,12 @@ +Rudimentary multi-database configuration. + +Multi-DB isn't vastly different from generic. The primary difference is that it +will run the migrations N times (depending on how many databases you have +configured), providing one engine name and associated context for each run. + +That engine name will then allow the migration to restrict what runs within it to +just the appropriate migrations for that engine. You can see this behavior within +the mako template. + +In the provided configuration, you'll need to have `databases` provided in +alembic's config, and an `sqlalchemy.url` provided for each engine name. diff --git a/sqlmodel/cli/migrations/templates/multidb/alembic.ini.mako b/sqlmodel/cli/migrations/templates/multidb/alembic.ini.mako new file mode 100644 index 0000000000..a9ea075516 --- /dev/null +++ b/sqlmodel/cli/migrations/templates/multidb/alembic.ini.mako @@ -0,0 +1,121 @@ +# a multi-database configuration. + +[alembic] +# path to migration scripts +script_location = ${script_location} + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python-dateutil library that can be +# installed by adding `alembic[tz]` to the pip requirements +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to ${script_location}/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:${script_location}/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +databases = engine1, engine2 + +[engine1] +sqlalchemy.url = driver://user:pass@localhost/dbname + +[engine2] +sqlalchemy.url = driver://user:pass@localhost/dbname2 + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the exec runner, execute a binary +# hooks = ruff +# ruff.type = exec +# ruff.executable = %(here)s/.venv/bin/ruff +# ruff.options = --fix REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/sqlmodel/cli/migrations/templates/multidb/script.py.mako b/sqlmodel/cli/migrations/templates/multidb/script.py.mako new file mode 100644 index 0000000000..6108b8a0dc --- /dev/null +++ b/sqlmodel/cli/migrations/templates/multidb/script.py.mako @@ -0,0 +1,47 @@ +<%! +import re + +%>"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade(engine_name: str) -> None: + globals()["upgrade_%s" % engine_name]() + + +def downgrade(engine_name: str) -> None: + globals()["downgrade_%s" % engine_name]() + +<% + db_names = config.get_main_option("databases") +%> + +## generate an "upgrade_() / downgrade_()" function +## for each database name in the ini file. + +% for db_name in re.split(r',\s*', db_names): + +def upgrade_${db_name}() -> None: + ${context.get("%s_upgrades" % db_name, "pass")} + + +def downgrade_${db_name}() -> None: + ${context.get("%s_downgrades" % db_name, "pass")} + +% endfor diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 2b69dd2a75..88ed1b6e10 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -5,47 +5,23 @@ from decimal import Decimal from enum import Enum from pathlib import Path -from typing import ( - AbstractSet, - Any, - Callable, - ClassVar, - Dict, - ForwardRef, - List, - Mapping, - Optional, - Sequence, - Set, - Tuple, - Type, - TypeVar, - Union, - cast, - overload, -) +from typing import (AbstractSet, Any, Callable, ClassVar, Dict, ForwardRef, + List, Mapping, Optional, Sequence, Set, Tuple, Type, + TypeVar, Union, cast, overload) from pydantic import BaseConfig, BaseModel from pydantic.errors import ConfigError, DictError -from pydantic.fields import SHAPE_SINGLETON, ModelField, Undefined, UndefinedType +from pydantic.fields import SHAPE_SINGLETON from pydantic.fields import FieldInfo as PydanticFieldInfo +from pydantic.fields import ModelField, Undefined, UndefinedType from pydantic.main import ModelMetaclass, validate_model from pydantic.typing import NoArgAnyCallable, resolve_annotations from pydantic.utils import ROOT_KEY, Representation -from sqlalchemy import ( - Boolean, - Column, - Date, - DateTime, - Float, - ForeignKey, - Integer, - Interval, - Numeric, - inspect, -) +from sqlalchemy import Boolean, Column, Date, DateTime from sqlalchemy import Enum as sa_Enum -from sqlalchemy.orm import RelationshipProperty, declared_attr, registry, relationship +from sqlalchemy import Float, ForeignKey, Integer, Interval, Numeric, inspect +from sqlalchemy.orm import (RelationshipProperty, declared_attr, registry, + relationship) from sqlalchemy.orm.attributes import set_attribute from sqlalchemy.orm.decl_api import DeclarativeMeta from sqlalchemy.orm.instrumentation import is_instrumented diff --git a/sqlmodel/orm/session.py b/sqlmodel/orm/session.py index 0c70c290ae..587c11b5b0 100644 --- a/sqlmodel/orm/session.py +++ b/sqlmodel/orm/session.py @@ -1,4 +1,5 @@ -from typing import Any, Mapping, Optional, Sequence, Type, TypeVar, Union, overload +from typing import (Any, Mapping, Optional, Sequence, Type, TypeVar, Union, + overload) from sqlalchemy import util from sqlalchemy.orm import Query as _Query diff --git a/sqlmodel/sql/expression.py b/sqlmodel/sql/expression.py index 264e39cba7..204b3f1dc3 100644 --- a/sqlmodel/sql/expression.py +++ b/sqlmodel/sql/expression.py @@ -1,18 +1,8 @@ # WARNING: do not modify this code, it is generated by expression.py.jinja2 from datetime import datetime -from typing import ( - TYPE_CHECKING, - Any, - Generic, - Mapping, - Sequence, - Tuple, - Type, - TypeVar, - Union, - overload, -) +from typing import (TYPE_CHECKING, Any, Generic, Mapping, Sequence, Tuple, + Type, TypeVar, Union, overload) from uuid import UUID from sqlalchemy import Column From ab2f8c292fe6f4c542ec9adbbf6e1f63bfdbc7cc Mon Sep 17 00:00:00 2001 From: Jufik Date: Tue, 31 Oct 2023 19:58:48 +0100 Subject: [PATCH 02/12] Format code --- .gitignore | 1 + docs/advanced/index.md | 1 - docs/advanced/migrations.md | 211 ++++++++++++++++++ .../simple_hero_migration/migrations/001.py | 7 +- .../simple_hero_migration/migrations/env.py | 7 +- ...46-1e606859995a_migrating_me_iam_famous.py | 22 +- .../simple_hero_migration/models.py | 2 +- mkdocs.yml | 1 + sqlmodel/__init__.py | 10 +- sqlmodel/cli/migrations/__init__.py | 1 - sqlmodel/cli/migrations/cli.py | 110 +++++++-- .../migrations/templates/async/script.py.mako | 1 + .../templates/generic/script.py.mako | 1 + .../cli/migrations/templates/multidb/README | 12 - .../templates/multidb/alembic.ini.mako | 121 ---------- .../templates/multidb/script.py.mako | 47 ---- sqlmodel/main.py | 42 +++- sqlmodel/orm/session.py | 3 +- sqlmodel/sql/expression.py | 14 +- 19 files changed, 381 insertions(+), 233 deletions(-) create mode 100644 docs/advanced/migrations.md delete mode 100644 sqlmodel/cli/migrations/templates/multidb/README delete mode 100644 sqlmodel/cli/migrations/templates/multidb/alembic.ini.mako delete mode 100644 sqlmodel/cli/migrations/templates/multidb/script.py.mako diff --git a/.gitignore b/.gitignore index 4006069389..9231514804 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ coverage.xml site *.db .cache +!docs_src/**/env.py \ No newline at end of file diff --git a/docs/advanced/index.md b/docs/advanced/index.md index f6178249ce..baa50cc37b 100644 --- a/docs/advanced/index.md +++ b/docs/advanced/index.md @@ -5,6 +5,5 @@ The **Advanced User Guide** is gradually growing, you can already read about som At some point it will include: * How to use `async` and `await` with the async session. -* How to run migrations. * How to combine **SQLModel** models with SQLAlchemy. * ...and more. πŸ€“ diff --git a/docs/advanced/migrations.md b/docs/advanced/migrations.md new file mode 100644 index 0000000000..e43a6f86d4 --- /dev/null +++ b/docs/advanced/migrations.md @@ -0,0 +1,211 @@ +# Manage migrations + +SQLModel integrates [Alembic](https://alembic.sqlalchemy.org/en/latest/) to manage migrations and DB Schema. + + +## **SQLModel** Code - Models and Migrations + +Now let's start with the SQLModel code. + +We will start with the **simplest version**, with just heroes (no teams yet). + +This is almost the same code as you start to know by heart: + +```Python +{!./docs_src/tutorial/migrations/simple_hero_migration/models.py!} +``` + +Let's jump in your shell and init migrations: + +
+ +```console +$ sqlmodel migrations init +Creating directory '/path/to/your/project/migrations' ... done +Creating directory '/path/to/your/project/migrations/versions' ... done +Generating /path/to/your/project/migrations/script.py.mako ... done +Generating /path/to/your/project/migrations/env.py ... done +Generating /path/to/your/project/migrations/README ... done +Generating /path/to/your/project/alembic.ini ... done +Adding '/path/to/your/project/migrations/__init__.py' ... done +Adding '/path/to/your/project/migrations/versions/__init__.py' ... done +Please edit configuration/connection/logging settings in '/path/to/your/project/alembic.ini' before proceeding. +``` +
+ +Few things happended under the hood. + +Let's review what happened: Below files just got created! + +```hl_lines="5-12" +. +β”œβ”€β”€ project + β”œβ”€β”€ __init__.py + β”œβ”€β”€ models.py + β”œβ”€β”€ alembic.ini + └── migrations + β”œβ”€β”€ __init__.py + β”œβ”€β”€ env.py + β”œβ”€β”€ README + β”œβ”€β”€ script.py.mako + └── versions + └── __init__.py +``` + +Let's review them step by step. + +## Alembic configuration + +**`alembic.ini`** gives all the details of Alembic's configuration. You shouldn't \*have to\* touch that a lot, but for our setup, we'll need to change few things. + +We need to tell alembic how to connect to the database: + +```ini hl_lines="10" +{!./docs_src/tutorial/migrations/simple_hero_migration/alembic001.ini[ln:1-5]!} + +#.... Lot's of configuration! + + +{!./docs_src/tutorial/migrations/simple_hero_migration/alembic001.ini[ln:63]!} # πŸ‘ˆ Let's Change that! +``` +Adapting our file, you will have: + +```ini hl_lines="10" +{!./docs_src/tutorial/migrations/simple_hero_migration/alembic.ini[ln:1-5]!} + +#.... Lot's of configuration! + + +{!./docs_src/tutorial/migrations/simple_hero_migration/alembic.ini[ln:63]!} # πŸ‘ˆ To that +``` + +For the full document, refer to [Alembic's official documentation](https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file) + +**`./migrations/env.py`** is another file we'll need to configure: +It gives which Tables you want to migrate, let's open it and: + +1. Import our models +2. Change `target_metadata` value + +```Python hl_lines="5 7" +{!./docs_src/tutorial/migrations/simple_hero_migration/migrations/001.py[ln:1-5]!} πŸ‘ˆ Import your model +# ..... +{!./docs_src/tutorial/migrations/simple_hero_migration/migrations/001.py[ln:19]!} πŸ‘ˆ Set you Metadata value +``` + +## Create an apply your first migration +!!! success + πŸ‘πŸŽ‰At this point, you are ready to track your DB Schema ! + +Let's create you first migration + +
+```console +$ sqlmodel migrations init +INFO [alembic.runtime.migration] Context impl SQLiteImpl. +INFO [alembic.runtime.migration] Will assume non-transactional DDL. +INFO [alembic.autogenerate.compare] Detected added table 'hero' + Generating /path/to/your/project/migrations/versions/0610946706a0_.py ... done + +``` + +
+ +Alembic did its magic and started to track your `Hero` model! +It created a new file `0610946706a0_.py` + + +```hl_lines="13" +. +β”œβ”€β”€ project + β”œβ”€β”€ __init__.py + β”œβ”€β”€ models.py + β”œβ”€β”€ alembic.ini + └── migrations + β”œβ”€β”€ __init__.py + β”œβ”€β”€ env.py + β”œβ”€β”€ README + β”œβ”€β”€ script.py.mako + └── versions + β”œβ”€β”€ __init__.py + └── 0610946706a0_.py +``` + +Let's prepare for our migration, and see what will happen. + +
+``` +$ sqlmodel migrations show +Rev: 50624637e300 (head) +Parent: +Path: /path/to/your/project/migrations/versions/0610946706a0_.py #πŸ‘ˆ That's our file + + empty message + + Revision ID: 50624637e300 + Revises: + Create Date: 2023-10-31 19:40:22.084162 +``` +
+ +We are pretty sure about what will happen during migration, let's do it: + +
+``` +$ sqlmodel migrations upgrade +INFO [alembic.runtime.migration] Context impl SQLiteImpl. +INFO [alembic.runtime.migration] Will assume non-transactional DDL. +INFO [alembic.runtime.migration] Running upgrade -> 1e606859995a, migrating me iam famous +``` +
+ + +Let's open our DB browser and check it out: + + + + + +## Change you versions file name + +Why the heck `0610946706a0_.py`?!!!! + +The goal is to have a unique revision name to avoid collision. +In order to have a cleaner file name, we can edit `alembic.ini` and uncomment + +```ini +{!./docs_src/tutorial/migrations/simple_hero_migration/alembic.ini[ln:11]!} #πŸ‘ˆ Uncoment this line +``` + +Let's remove `0610946706a0_.py` and start it over. + +
+```console +$ sqlmodel migrations revision +INFO [alembic.runtime.migration] Context impl SQLiteImpl. +INFO [alembic.runtime.migration] Will assume non-transactional DDL. +INFO [alembic.autogenerate.compare] Detected added table 'hero' + Generating /path/to/your/project/migrations/versions//2023_10_31_1940-50624637e300_.py ... done +``` +
+ +Much better, not perfect but better. + +To get more details just by looking at you file name, you can also run + + +
+```console +$ sqlmodel migrations revision "migrate me iam famous" +INFO [alembic.runtime.migration] Context impl SQLiteImpl. +INFO [alembic.runtime.migration] Will assume non-transactional DDL. +INFO [alembic.autogenerate.compare] Detected added table 'hero' + Generating /path/to/your/project/migrations/versions/2023_10_31_1946-1e606859995a_migrate_me_iam_famous.py + ... done +``` +
+ + +You can think of "migrate me iam famous" as a message you add to you migration. + +It helps you keep track of what they do, pretty much like in `git` \ No newline at end of file diff --git a/docs_src/tutorial/migrations/simple_hero_migration/migrations/001.py b/docs_src/tutorial/migrations/simple_hero_migration/migrations/001.py index 182fc6d541..c865fe6961 100644 --- a/docs_src/tutorial/migrations/simple_hero_migration/migrations/001.py +++ b/docs_src/tutorial/migrations/simple_hero_migration/migrations/001.py @@ -1,8 +1,9 @@ from logging.config import fileConfig from alembic import context -from sqlalchemy import engine_from_config, pool from models import Hero +from sqlalchemy import engine_from_config, pool + # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config @@ -62,9 +63,7 @@ def run_migrations_online() -> None: ) with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata - ) + context.configure(connection=connection, target_metadata=target_metadata) with context.begin_transaction(): context.run_migrations() diff --git a/docs_src/tutorial/migrations/simple_hero_migration/migrations/env.py b/docs_src/tutorial/migrations/simple_hero_migration/migrations/env.py index 182fc6d541..c865fe6961 100644 --- a/docs_src/tutorial/migrations/simple_hero_migration/migrations/env.py +++ b/docs_src/tutorial/migrations/simple_hero_migration/migrations/env.py @@ -1,8 +1,9 @@ from logging.config import fileConfig from alembic import context -from sqlalchemy import engine_from_config, pool from models import Hero +from sqlalchemy import engine_from_config, pool + # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config @@ -62,9 +63,7 @@ def run_migrations_online() -> None: ) with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata - ) + context.configure(connection=connection, target_metadata=target_metadata) with context.begin_transaction(): context.run_migrations() diff --git a/docs_src/tutorial/migrations/simple_hero_migration/migrations/versions/2023_10_31_1946-1e606859995a_migrating_me_iam_famous.py b/docs_src/tutorial/migrations/simple_hero_migration/migrations/versions/2023_10_31_1946-1e606859995a_migrating_me_iam_famous.py index a7c739a277..af3f7bad85 100644 --- a/docs_src/tutorial/migrations/simple_hero_migration/migrations/versions/2023_10_31_1946-1e606859995a_migrating_me_iam_famous.py +++ b/docs_src/tutorial/migrations/simple_hero_migration/migrations/versions/2023_10_31_1946-1e606859995a_migrating_me_iam_famous.py @@ -1,19 +1,18 @@ """migrating me iam famous Revision ID: 1e606859995a -Revises: +Revises: Create Date: 2023-10-31 19:46:27.802669 """ from typing import Sequence, Union -from alembic import op import sqlalchemy as sa import sqlmodel - +from alembic import op # revision identifiers, used by Alembic. -revision: str = '1e606859995a' +revision: str = "1e606859995a" down_revision: Union[str, None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -21,17 +20,18 @@ def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.create_table('hero', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('secret_name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('age', sa.Integer(), nullable=True), - sa.PrimaryKeyConstraint('id') + op.create_table( + "hero", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("secret_name", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("age", sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint("id"), ) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('hero') + op.drop_table("hero") # ### end Alembic commands ### diff --git a/docs_src/tutorial/migrations/simple_hero_migration/models.py b/docs_src/tutorial/migrations/simple_hero_migration/models.py index 509d282b1e..12132bb7b9 100644 --- a/docs_src/tutorial/migrations/simple_hero_migration/models.py +++ b/docs_src/tutorial/migrations/simple_hero_migration/models.py @@ -1,6 +1,6 @@ from typing import Optional -from sqlmodel import Field, Session, SQLModel, create_engine, select +from sqlmodel import Field, SQLModel class Hero(SQLModel, table=True): diff --git a/mkdocs.yml b/mkdocs.yml index 646af7c39e..3c82dabdd4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -85,6 +85,7 @@ nav: - Advanced User Guide: - advanced/index.md - advanced/decimal.md + - advanced/migrations.md - alternatives.md - help.md - contributing.md diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index 4894b8c911..495ac9c8a8 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -24,11 +24,13 @@ from sqlalchemy.schema import ThreadLocalMetaData as ThreadLocalMetaData from sqlalchemy.schema import UniqueConstraint as UniqueConstraint from sqlalchemy.sql import LABEL_STYLE_DEFAULT as LABEL_STYLE_DEFAULT -from sqlalchemy.sql import \ - LABEL_STYLE_DISAMBIGUATE_ONLY as LABEL_STYLE_DISAMBIGUATE_ONLY +from sqlalchemy.sql import ( + LABEL_STYLE_DISAMBIGUATE_ONLY as LABEL_STYLE_DISAMBIGUATE_ONLY, +) from sqlalchemy.sql import LABEL_STYLE_NONE as LABEL_STYLE_NONE -from sqlalchemy.sql import \ - LABEL_STYLE_TABLENAME_PLUS_COL as LABEL_STYLE_TABLENAME_PLUS_COL +from sqlalchemy.sql import ( + LABEL_STYLE_TABLENAME_PLUS_COL as LABEL_STYLE_TABLENAME_PLUS_COL, +) from sqlalchemy.sql import alias as alias from sqlalchemy.sql import all_ as all_ from sqlalchemy.sql import and_ as and_ diff --git a/sqlmodel/cli/migrations/__init__.py b/sqlmodel/cli/migrations/__init__.py index 46d55f5201..8b13789179 100644 --- a/sqlmodel/cli/migrations/__init__.py +++ b/sqlmodel/cli/migrations/__init__.py @@ -1,2 +1 @@ -from sqlmodel.cli.migrations.cli import migrations diff --git a/sqlmodel/cli/migrations/cli.py b/sqlmodel/cli/migrations/cli.py index 8a3da1132d..de51aae003 100644 --- a/sqlmodel/cli/migrations/cli.py +++ b/sqlmodel/cli/migrations/cli.py @@ -1,7 +1,7 @@ import configparser import logging from pathlib import Path -from typing import Literal, Optional, Union +from typing import List, Optional import typer from alembic import command @@ -10,7 +10,6 @@ from typing_extensions import Annotated logger = logging.getLogger(__name__) - migrations = Typer( name="migrations", help="Commands to interact with Alembic migrations" ) @@ -20,13 +19,13 @@ def init( module: Path = ".", config_file: Path = "alembic.ini", - script_location: Path = "migrations", + directory: Path = "migrations", template: str = "generic", # Should be Literal["generic", "multidb", "async"] target_metadata: Annotated[Optional[str], typer.Argument()] = None, ): """Initialize Alembic""" _config_file = module / config_file - _script_location = module / script_location + _directory = module / directory config = Config(_config_file) @@ -34,16 +33,99 @@ def init( command.init( config, - _script_location, + _directory, template=template_path, package=True, ) - logger.debug(f"Inited alembic") - # Add target_metadata to alembic.ini - - updated_config = configparser.ConfigParser() - updated_config.read(_config_file) - updated_config["alembic"].update({"target_metadata": target_metadata}) - with open(_config_file, "w") as configfile: - updated_config.write(configfile) - logger.debug(f"Alembic defaults overrwritten") + logger.debug("Inited alembic") + if target_metadata: + # Add target_metadata to alembic.ini + + updated_config = configparser.ConfigParser() + updated_config.read(_config_file) + updated_config["alembic"].update({"target_metadata": target_metadata}) + with open(_config_file, "w") as configfile: + updated_config.write(configfile) + logger.debug("Alembic defaults overrwritten") + + +@migrations.command() +def revision( + module: Path = ".", + config_file: Path = "alembic.ini", + message: Annotated[Optional[str], typer.Argument()] = None, + autogenerate: bool = True, + sql: bool = False, + head: str = "head", + splice: bool = False, + # branch_label: Annotated[Optional[_RevIdType], typer.Argument()] = None, + version_path: Annotated[Optional[str], typer.Argument()] = None, + rev_id: Annotated[Optional[str], typer.Argument()] = None, + depends_on: Annotated[Optional[str], typer.Argument()] = None, +): + """Create a new Alembic revision""" + config = Config(module / config_file) + command.revision( + config, + message=message, + autogenerate=autogenerate, + sql=sql, + head=head, + splice=splice, + # branch_label=branch_label, + version_path=version_path, + rev_id=rev_id, + depends_on=depends_on, + ) + + +@migrations.command() +def show(module: Path = ".", config_file: Path = "alembic.ini", rev: str = "head"): + """Show the revision""" + config = Config(module / config_file) + command.show(config, rev) + + +def merge( + revisions: List[str], + module: Path = ".", + config_file: Path = "alembic.ini", + message: Annotated[Optional[str], typer.Argument()] = None, + branch_label: Annotated[Optional[List[str]], typer.Argument()] = None, + rev_id: Annotated[Optional[str], typer.Argument()] = None, +): + """Merge two revisions together, creating a new migration file""" + config = Config(module / config_file) + command.merge( + config, + revisions, + message=message, + branch_label=branch_label, + rev_id=rev_id, + ) + + +@migrations.command() +def upgrade( + revision: str = "head", + module: Path = ".", + config_file: Path = "alembic.ini", + sql: bool = False, + tag: Annotated[Optional[str], typer.Argument()] = None, +): + """Upgrade to the given revision""" + config = Config(module / config_file) + command.upgrade(config, revision) + + +@migrations.command() +def downgrade( + revision: str = "head", + module: Path = ".", + config_file: Path = "alembic.ini", + sql: bool = False, + tag: Annotated[Optional[str], typer.Argument()] = None, +): + """Downgrade to the given revision""" + config = Config(module / config_file) + command.downgrade(config, revision) diff --git a/sqlmodel/cli/migrations/templates/async/script.py.mako b/sqlmodel/cli/migrations/templates/async/script.py.mako index fbc4b07dce..6ce3351093 100644 --- a/sqlmodel/cli/migrations/templates/async/script.py.mako +++ b/sqlmodel/cli/migrations/templates/async/script.py.mako @@ -9,6 +9,7 @@ from typing import Sequence, Union from alembic import op import sqlalchemy as sa +import sqlmodel ${imports if imports else ""} # revision identifiers, used by Alembic. diff --git a/sqlmodel/cli/migrations/templates/generic/script.py.mako b/sqlmodel/cli/migrations/templates/generic/script.py.mako index fbc4b07dce..6ce3351093 100644 --- a/sqlmodel/cli/migrations/templates/generic/script.py.mako +++ b/sqlmodel/cli/migrations/templates/generic/script.py.mako @@ -9,6 +9,7 @@ from typing import Sequence, Union from alembic import op import sqlalchemy as sa +import sqlmodel ${imports if imports else ""} # revision identifiers, used by Alembic. diff --git a/sqlmodel/cli/migrations/templates/multidb/README b/sqlmodel/cli/migrations/templates/multidb/README deleted file mode 100644 index f046ec9142..0000000000 --- a/sqlmodel/cli/migrations/templates/multidb/README +++ /dev/null @@ -1,12 +0,0 @@ -Rudimentary multi-database configuration. - -Multi-DB isn't vastly different from generic. The primary difference is that it -will run the migrations N times (depending on how many databases you have -configured), providing one engine name and associated context for each run. - -That engine name will then allow the migration to restrict what runs within it to -just the appropriate migrations for that engine. You can see this behavior within -the mako template. - -In the provided configuration, you'll need to have `databases` provided in -alembic's config, and an `sqlalchemy.url` provided for each engine name. diff --git a/sqlmodel/cli/migrations/templates/multidb/alembic.ini.mako b/sqlmodel/cli/migrations/templates/multidb/alembic.ini.mako deleted file mode 100644 index a9ea075516..0000000000 --- a/sqlmodel/cli/migrations/templates/multidb/alembic.ini.mako +++ /dev/null @@ -1,121 +0,0 @@ -# a multi-database configuration. - -[alembic] -# path to migration scripts -script_location = ${script_location} - -# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s -# Uncomment the line below if you want the files to be prepended with date and time -# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file -# for all available tokens -# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s - -# sys.path path, will be prepended to sys.path if present. -# defaults to the current working directory. -prepend_sys_path = . - -# timezone to use when rendering the date within the migration file -# as well as the filename. -# If specified, requires the python-dateutil library that can be -# installed by adding `alembic[tz]` to the pip requirements -# string value is passed to dateutil.tz.gettz() -# leave blank for localtime -# timezone = - -# max length of characters to apply to the -# "slug" field -# truncate_slug_length = 40 - -# set to 'true' to run the environment during -# the 'revision' command, regardless of autogenerate -# revision_environment = false - -# set to 'true' to allow .pyc and .pyo files without -# a source .py file to be detected as revisions in the -# versions/ directory -# sourceless = false - -# version location specification; This defaults -# to ${script_location}/versions. When using multiple version -# directories, initial revisions must be specified with --version-path. -# The path separator used here should be the separator specified by "version_path_separator" below. -# version_locations = %(here)s/bar:%(here)s/bat:${script_location}/versions - -# version path separator; As mentioned above, this is the character used to split -# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. -# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. -# Valid values for version_path_separator are: -# -# version_path_separator = : -# version_path_separator = ; -# version_path_separator = space -version_path_separator = os # Use os.pathsep. Default configuration used for new projects. - -# set to 'true' to search source files recursively -# in each "version_locations" directory -# new in Alembic version 1.10 -# recursive_version_locations = false - -# the output encoding used when revision files -# are written from script.py.mako -# output_encoding = utf-8 - -databases = engine1, engine2 - -[engine1] -sqlalchemy.url = driver://user:pass@localhost/dbname - -[engine2] -sqlalchemy.url = driver://user:pass@localhost/dbname2 - -[post_write_hooks] -# post_write_hooks defines scripts or Python functions that are run -# on newly generated revision scripts. See the documentation for further -# detail and examples - -# format using "black" - use the console_scripts runner, against the "black" entrypoint -# hooks = black -# black.type = console_scripts -# black.entrypoint = black -# black.options = -l 79 REVISION_SCRIPT_FILENAME - -# lint with attempts to fix using "ruff" - use the exec runner, execute a binary -# hooks = ruff -# ruff.type = exec -# ruff.executable = %(here)s/.venv/bin/ruff -# ruff.options = --fix REVISION_SCRIPT_FILENAME - -# Logging configuration -[loggers] -keys = root,sqlalchemy,alembic - -[handlers] -keys = console - -[formatters] -keys = generic - -[logger_root] -level = WARN -handlers = console -qualname = - -[logger_sqlalchemy] -level = WARN -handlers = -qualname = sqlalchemy.engine - -[logger_alembic] -level = INFO -handlers = -qualname = alembic - -[handler_console] -class = StreamHandler -args = (sys.stderr,) -level = NOTSET -formatter = generic - -[formatter_generic] -format = %(levelname)-5.5s [%(name)s] %(message)s -datefmt = %H:%M:%S diff --git a/sqlmodel/cli/migrations/templates/multidb/script.py.mako b/sqlmodel/cli/migrations/templates/multidb/script.py.mako deleted file mode 100644 index 6108b8a0dc..0000000000 --- a/sqlmodel/cli/migrations/templates/multidb/script.py.mako +++ /dev/null @@ -1,47 +0,0 @@ -<%! -import re - -%>"""${message} - -Revision ID: ${up_revision} -Revises: ${down_revision | comma,n} -Create Date: ${create_date} - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -${imports if imports else ""} - -# revision identifiers, used by Alembic. -revision: str = ${repr(up_revision)} -down_revision: Union[str, None] = ${repr(down_revision)} -branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} -depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} - - -def upgrade(engine_name: str) -> None: - globals()["upgrade_%s" % engine_name]() - - -def downgrade(engine_name: str) -> None: - globals()["downgrade_%s" % engine_name]() - -<% - db_names = config.get_main_option("databases") -%> - -## generate an "upgrade_() / downgrade_()" function -## for each database name in the ini file. - -% for db_name in re.split(r',\s*', db_names): - -def upgrade_${db_name}() -> None: - ${context.get("%s_upgrades" % db_name, "pass")} - - -def downgrade_${db_name}() -> None: - ${context.get("%s_downgrades" % db_name, "pass")} - -% endfor diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 88ed1b6e10..2b69dd2a75 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -5,23 +5,47 @@ from decimal import Decimal from enum import Enum from pathlib import Path -from typing import (AbstractSet, Any, Callable, ClassVar, Dict, ForwardRef, - List, Mapping, Optional, Sequence, Set, Tuple, Type, - TypeVar, Union, cast, overload) +from typing import ( + AbstractSet, + Any, + Callable, + ClassVar, + Dict, + ForwardRef, + List, + Mapping, + Optional, + Sequence, + Set, + Tuple, + Type, + TypeVar, + Union, + cast, + overload, +) from pydantic import BaseConfig, BaseModel from pydantic.errors import ConfigError, DictError -from pydantic.fields import SHAPE_SINGLETON +from pydantic.fields import SHAPE_SINGLETON, ModelField, Undefined, UndefinedType from pydantic.fields import FieldInfo as PydanticFieldInfo -from pydantic.fields import ModelField, Undefined, UndefinedType from pydantic.main import ModelMetaclass, validate_model from pydantic.typing import NoArgAnyCallable, resolve_annotations from pydantic.utils import ROOT_KEY, Representation -from sqlalchemy import Boolean, Column, Date, DateTime +from sqlalchemy import ( + Boolean, + Column, + Date, + DateTime, + Float, + ForeignKey, + Integer, + Interval, + Numeric, + inspect, +) from sqlalchemy import Enum as sa_Enum -from sqlalchemy import Float, ForeignKey, Integer, Interval, Numeric, inspect -from sqlalchemy.orm import (RelationshipProperty, declared_attr, registry, - relationship) +from sqlalchemy.orm import RelationshipProperty, declared_attr, registry, relationship from sqlalchemy.orm.attributes import set_attribute from sqlalchemy.orm.decl_api import DeclarativeMeta from sqlalchemy.orm.instrumentation import is_instrumented diff --git a/sqlmodel/orm/session.py b/sqlmodel/orm/session.py index 587c11b5b0..0c70c290ae 100644 --- a/sqlmodel/orm/session.py +++ b/sqlmodel/orm/session.py @@ -1,5 +1,4 @@ -from typing import (Any, Mapping, Optional, Sequence, Type, TypeVar, Union, - overload) +from typing import Any, Mapping, Optional, Sequence, Type, TypeVar, Union, overload from sqlalchemy import util from sqlalchemy.orm import Query as _Query diff --git a/sqlmodel/sql/expression.py b/sqlmodel/sql/expression.py index 204b3f1dc3..264e39cba7 100644 --- a/sqlmodel/sql/expression.py +++ b/sqlmodel/sql/expression.py @@ -1,8 +1,18 @@ # WARNING: do not modify this code, it is generated by expression.py.jinja2 from datetime import datetime -from typing import (TYPE_CHECKING, Any, Generic, Mapping, Sequence, Tuple, - Type, TypeVar, Union, overload) +from typing import ( + TYPE_CHECKING, + Any, + Generic, + Mapping, + Sequence, + Tuple, + Type, + TypeVar, + Union, + overload, +) from uuid import UUID from sqlalchemy import Column From 32afa192caba141896eb5c1d8384264043a175fb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 31 Oct 2023 19:07:58 +0000 Subject: [PATCH 03/12] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- docs/advanced/migrations.md | 24 +++++++++---------- .../simple_hero_migration/migrations/README | 2 +- sqlmodel/cli/migrations/__init__.py | 1 - .../cli/migrations/templates/async/README | 2 +- .../cli/migrations/templates/generic/README | 2 +- 6 files changed, 16 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index 9231514804..5fc9253dc1 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,4 @@ coverage.xml site *.db .cache -!docs_src/**/env.py \ No newline at end of file +!docs_src/**/env.py diff --git a/docs/advanced/migrations.md b/docs/advanced/migrations.md index e43a6f86d4..6c0d78e529 100644 --- a/docs/advanced/migrations.md +++ b/docs/advanced/migrations.md @@ -11,8 +11,8 @@ We will start with the **simplest version**, with just heroes (no teams yet). This is almost the same code as you start to know by heart: -```Python -{!./docs_src/tutorial/migrations/simple_hero_migration/models.py!} +```Python +{!./docs_src/tutorial/migrations/simple_hero_migration/models.py!} ``` Let's jump in your shell and init migrations: @@ -47,7 +47,7 @@ Let's review what happened: Below files just got created! β”œβ”€β”€ __init__.py β”œβ”€β”€ env.py β”œβ”€β”€ README - β”œβ”€β”€ script.py.mako + β”œβ”€β”€ script.py.mako └── versions └── __init__.py ``` @@ -61,17 +61,17 @@ Let's review them step by step. We need to tell alembic how to connect to the database: ```ini hl_lines="10" -{!./docs_src/tutorial/migrations/simple_hero_migration/alembic001.ini[ln:1-5]!} +{!./docs_src/tutorial/migrations/simple_hero_migration/alembic001.ini[ln:1-5]!} #.... Lot's of configuration! {!./docs_src/tutorial/migrations/simple_hero_migration/alembic001.ini[ln:63]!} # πŸ‘ˆ Let's Change that! ``` -Adapting our file, you will have: +Adapting our file, you will have: ```ini hl_lines="10" -{!./docs_src/tutorial/migrations/simple_hero_migration/alembic.ini[ln:1-5]!} +{!./docs_src/tutorial/migrations/simple_hero_migration/alembic.ini[ln:1-5]!} #.... Lot's of configuration! @@ -85,7 +85,7 @@ For the full document, refer to [Alembic's official documentation](https://alemb It gives which Tables you want to migrate, let's open it and: 1. Import our models -2. Change `target_metadata` value +2. Change `target_metadata` value ```Python hl_lines="5 7" {!./docs_src/tutorial/migrations/simple_hero_migration/migrations/001.py[ln:1-5]!} πŸ‘ˆ Import your model @@ -125,7 +125,7 @@ It created a new file `0610946706a0_.py` β”œβ”€β”€ __init__.py β”œβ”€β”€ env.py β”œβ”€β”€ README - β”œβ”€β”€ script.py.mako + β”œβ”€β”€ script.py.mako └── versions β”œβ”€β”€ __init__.py └── 0610946706a0_.py @@ -141,9 +141,9 @@ Parent: Path: /path/to/your/project/migrations/versions/0610946706a0_.py #πŸ‘ˆ That's our file empty message - + Revision ID: 50624637e300 - Revises: + Revises: Create Date: 2023-10-31 19:40:22.084162 ``` @@ -173,7 +173,7 @@ Why the heck `0610946706a0_.py`?!!!! The goal is to have a unique revision name to avoid collision. In order to have a cleaner file name, we can edit `alembic.ini` and uncomment -```ini +```ini {!./docs_src/tutorial/migrations/simple_hero_migration/alembic.ini[ln:11]!} #πŸ‘ˆ Uncoment this line ``` @@ -208,4 +208,4 @@ INFO [alembic.autogenerate.compare] Detected added table 'hero' You can think of "migrate me iam famous" as a message you add to you migration. -It helps you keep track of what they do, pretty much like in `git` \ No newline at end of file +It helps you keep track of what they do, pretty much like in `git` diff --git a/docs_src/tutorial/migrations/simple_hero_migration/migrations/README b/docs_src/tutorial/migrations/simple_hero_migration/migrations/README index da94f1daf6..45acd9d199 100644 --- a/docs_src/tutorial/migrations/simple_hero_migration/migrations/README +++ b/docs_src/tutorial/migrations/simple_hero_migration/migrations/README @@ -1,2 +1,2 @@ Generic single-database configuration. -I'm the best! \ No newline at end of file +I'm the best! diff --git a/sqlmodel/cli/migrations/__init__.py b/sqlmodel/cli/migrations/__init__.py index 8b13789179..e69de29bb2 100644 --- a/sqlmodel/cli/migrations/__init__.py +++ b/sqlmodel/cli/migrations/__init__.py @@ -1 +0,0 @@ - diff --git a/sqlmodel/cli/migrations/templates/async/README b/sqlmodel/cli/migrations/templates/async/README index e0d0858f26..a23d4fb519 100644 --- a/sqlmodel/cli/migrations/templates/async/README +++ b/sqlmodel/cli/migrations/templates/async/README @@ -1 +1 @@ -Generic single-database configuration with an async dbapi. \ No newline at end of file +Generic single-database configuration with an async dbapi. diff --git a/sqlmodel/cli/migrations/templates/generic/README b/sqlmodel/cli/migrations/templates/generic/README index da94f1daf6..45acd9d199 100644 --- a/sqlmodel/cli/migrations/templates/generic/README +++ b/sqlmodel/cli/migrations/templates/generic/README @@ -1,2 +1,2 @@ Generic single-database configuration. -I'm the best! \ No newline at end of file +I'm the best! From 114f9feb59a1dd4684399c6a8ed518e6c90101d7 Mon Sep 17 00:00:00 2001 From: Jufik Date: Tue, 31 Oct 2023 20:11:24 +0100 Subject: [PATCH 04/12] Installed pre-commits --- .gitignore | 2 +- docs/advanced/migrations.md | 26 +++++++++---------- .../simple_hero_migration/migrations/README | 2 +- sqlmodel/cli/migrations/__init__.py | 1 - sqlmodel/cli/migrations/cli.py | 26 ++++++++++--------- .../cli/migrations/templates/async/README | 2 +- .../cli/migrations/templates/generic/README | 2 +- 7 files changed, 31 insertions(+), 30 deletions(-) diff --git a/.gitignore b/.gitignore index 9231514804..5fc9253dc1 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,4 @@ coverage.xml site *.db .cache -!docs_src/**/env.py \ No newline at end of file +!docs_src/**/env.py diff --git a/docs/advanced/migrations.md b/docs/advanced/migrations.md index e43a6f86d4..afd83ce876 100644 --- a/docs/advanced/migrations.md +++ b/docs/advanced/migrations.md @@ -11,8 +11,8 @@ We will start with the **simplest version**, with just heroes (no teams yet). This is almost the same code as you start to know by heart: -```Python -{!./docs_src/tutorial/migrations/simple_hero_migration/models.py!} +```Python +{!./docs_src/tutorial/migrations/simple_hero_migration/models.py!} ``` Let's jump in your shell and init migrations: @@ -47,7 +47,7 @@ Let's review what happened: Below files just got created! β”œβ”€β”€ __init__.py β”œβ”€β”€ env.py β”œβ”€β”€ README - β”œβ”€β”€ script.py.mako + β”œβ”€β”€ script.py.mako └── versions └── __init__.py ``` @@ -61,17 +61,17 @@ Let's review them step by step. We need to tell alembic how to connect to the database: ```ini hl_lines="10" -{!./docs_src/tutorial/migrations/simple_hero_migration/alembic001.ini[ln:1-5]!} +{!./docs_src/tutorial/migrations/simple_hero_migration/alembic001.ini[ln:1-5]!} #.... Lot's of configuration! {!./docs_src/tutorial/migrations/simple_hero_migration/alembic001.ini[ln:63]!} # πŸ‘ˆ Let's Change that! ``` -Adapting our file, you will have: +Adapting our file, you will have: ```ini hl_lines="10" -{!./docs_src/tutorial/migrations/simple_hero_migration/alembic.ini[ln:1-5]!} +{!./docs_src/tutorial/migrations/simple_hero_migration/alembic.ini[ln:1-5]!} #.... Lot's of configuration! @@ -85,7 +85,7 @@ For the full document, refer to [Alembic's official documentation](https://alemb It gives which Tables you want to migrate, let's open it and: 1. Import our models -2. Change `target_metadata` value +2. Change `target_metadata` value ```Python hl_lines="5 7" {!./docs_src/tutorial/migrations/simple_hero_migration/migrations/001.py[ln:1-5]!} πŸ‘ˆ Import your model @@ -117,7 +117,7 @@ It created a new file `0610946706a0_.py` ```hl_lines="13" . -β”œβ”€β”€ project +└── project β”œβ”€β”€ __init__.py β”œβ”€β”€ models.py β”œβ”€β”€ alembic.ini @@ -125,7 +125,7 @@ It created a new file `0610946706a0_.py` β”œβ”€β”€ __init__.py β”œβ”€β”€ env.py β”œβ”€β”€ README - β”œβ”€β”€ script.py.mako + β”œβ”€β”€ script.py.mako └── versions β”œβ”€β”€ __init__.py └── 0610946706a0_.py @@ -141,9 +141,9 @@ Parent: Path: /path/to/your/project/migrations/versions/0610946706a0_.py #πŸ‘ˆ That's our file empty message - + Revision ID: 50624637e300 - Revises: + Revises: Create Date: 2023-10-31 19:40:22.084162 ``` @@ -173,7 +173,7 @@ Why the heck `0610946706a0_.py`?!!!! The goal is to have a unique revision name to avoid collision. In order to have a cleaner file name, we can edit `alembic.ini` and uncomment -```ini +```ini {!./docs_src/tutorial/migrations/simple_hero_migration/alembic.ini[ln:11]!} #πŸ‘ˆ Uncoment this line ``` @@ -208,4 +208,4 @@ INFO [alembic.autogenerate.compare] Detected added table 'hero' You can think of "migrate me iam famous" as a message you add to you migration. -It helps you keep track of what they do, pretty much like in `git` \ No newline at end of file +It helps you keep track of what they do, pretty much like in `git` diff --git a/docs_src/tutorial/migrations/simple_hero_migration/migrations/README b/docs_src/tutorial/migrations/simple_hero_migration/migrations/README index da94f1daf6..45acd9d199 100644 --- a/docs_src/tutorial/migrations/simple_hero_migration/migrations/README +++ b/docs_src/tutorial/migrations/simple_hero_migration/migrations/README @@ -1,2 +1,2 @@ Generic single-database configuration. -I'm the best! \ No newline at end of file +I'm the best! diff --git a/sqlmodel/cli/migrations/__init__.py b/sqlmodel/cli/migrations/__init__.py index 8b13789179..e69de29bb2 100644 --- a/sqlmodel/cli/migrations/__init__.py +++ b/sqlmodel/cli/migrations/__init__.py @@ -1 +0,0 @@ - diff --git a/sqlmodel/cli/migrations/cli.py b/sqlmodel/cli/migrations/cli.py index de51aae003..9156c2bd43 100644 --- a/sqlmodel/cli/migrations/cli.py +++ b/sqlmodel/cli/migrations/cli.py @@ -17,9 +17,9 @@ @migrations.command() def init( - module: Path = ".", - config_file: Path = "alembic.ini", - directory: Path = "migrations", + module: Path = Path("."), + config_file: Path = Path("alembic.ini"), + directory: Path = Path("migrations"), template: str = "generic", # Should be Literal["generic", "multidb", "async"] target_metadata: Annotated[Optional[str], typer.Argument()] = None, ): @@ -51,8 +51,8 @@ def init( @migrations.command() def revision( - module: Path = ".", - config_file: Path = "alembic.ini", + module: Path = Path("."), + config_file: Path = Path("alembic.ini"), message: Annotated[Optional[str], typer.Argument()] = None, autogenerate: bool = True, sql: bool = False, @@ -80,7 +80,9 @@ def revision( @migrations.command() -def show(module: Path = ".", config_file: Path = "alembic.ini", rev: str = "head"): +def show( + module: Path = Path("."), config_file: Path = Path("alembic.ini"), rev: str = "head" +): """Show the revision""" config = Config(module / config_file) command.show(config, rev) @@ -88,8 +90,8 @@ def show(module: Path = ".", config_file: Path = "alembic.ini", rev: str = "head def merge( revisions: List[str], - module: Path = ".", - config_file: Path = "alembic.ini", + module: Path = Path("."), + config_file: Path = Path("alembic.ini"), message: Annotated[Optional[str], typer.Argument()] = None, branch_label: Annotated[Optional[List[str]], typer.Argument()] = None, rev_id: Annotated[Optional[str], typer.Argument()] = None, @@ -108,8 +110,8 @@ def merge( @migrations.command() def upgrade( revision: str = "head", - module: Path = ".", - config_file: Path = "alembic.ini", + module: Path = Path("."), + config_file: Path = Path("alembic.ini"), sql: bool = False, tag: Annotated[Optional[str], typer.Argument()] = None, ): @@ -121,8 +123,8 @@ def upgrade( @migrations.command() def downgrade( revision: str = "head", - module: Path = ".", - config_file: Path = "alembic.ini", + module: Path = Path("."), + config_file: Path = Path("alembic.ini"), sql: bool = False, tag: Annotated[Optional[str], typer.Argument()] = None, ): diff --git a/sqlmodel/cli/migrations/templates/async/README b/sqlmodel/cli/migrations/templates/async/README index e0d0858f26..a23d4fb519 100644 --- a/sqlmodel/cli/migrations/templates/async/README +++ b/sqlmodel/cli/migrations/templates/async/README @@ -1 +1 @@ -Generic single-database configuration with an async dbapi. \ No newline at end of file +Generic single-database configuration with an async dbapi. diff --git a/sqlmodel/cli/migrations/templates/generic/README b/sqlmodel/cli/migrations/templates/generic/README index da94f1daf6..45acd9d199 100644 --- a/sqlmodel/cli/migrations/templates/generic/README +++ b/sqlmodel/cli/migrations/templates/generic/README @@ -1,2 +1,2 @@ Generic single-database configuration. -I'm the best! \ No newline at end of file +I'm the best! From 3a2ae72a5c268eb30b772074868af39fe46ae64b Mon Sep 17 00:00:00 2001 From: Jufik Date: Tue, 31 Oct 2023 20:54:35 +0100 Subject: [PATCH 05/12] Returns None --- sqlmodel/cli/migrations/cli.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sqlmodel/cli/migrations/cli.py b/sqlmodel/cli/migrations/cli.py index 9156c2bd43..1564717d73 100644 --- a/sqlmodel/cli/migrations/cli.py +++ b/sqlmodel/cli/migrations/cli.py @@ -22,7 +22,7 @@ def init( directory: Path = Path("migrations"), template: str = "generic", # Should be Literal["generic", "multidb", "async"] target_metadata: Annotated[Optional[str], typer.Argument()] = None, -): +) -> None: """Initialize Alembic""" _config_file = module / config_file _directory = module / directory @@ -62,7 +62,7 @@ def revision( version_path: Annotated[Optional[str], typer.Argument()] = None, rev_id: Annotated[Optional[str], typer.Argument()] = None, depends_on: Annotated[Optional[str], typer.Argument()] = None, -): +) -> None: """Create a new Alembic revision""" config = Config(module / config_file) command.revision( @@ -82,7 +82,7 @@ def revision( @migrations.command() def show( module: Path = Path("."), config_file: Path = Path("alembic.ini"), rev: str = "head" -): +) -> None: """Show the revision""" config = Config(module / config_file) command.show(config, rev) @@ -95,7 +95,7 @@ def merge( message: Annotated[Optional[str], typer.Argument()] = None, branch_label: Annotated[Optional[List[str]], typer.Argument()] = None, rev_id: Annotated[Optional[str], typer.Argument()] = None, -): +) -> None: """Merge two revisions together, creating a new migration file""" config = Config(module / config_file) command.merge( @@ -114,7 +114,7 @@ def upgrade( config_file: Path = Path("alembic.ini"), sql: bool = False, tag: Annotated[Optional[str], typer.Argument()] = None, -): +) -> None: """Upgrade to the given revision""" config = Config(module / config_file) command.upgrade(config, revision) @@ -127,7 +127,7 @@ def downgrade( config_file: Path = Path("alembic.ini"), sql: bool = False, tag: Annotated[Optional[str], typer.Argument()] = None, -): +) -> None: """Downgrade to the given revision""" config = Config(module / config_file) command.downgrade(config, revision) From 380b6b6993c744e4afe5eef68c82710c1a7b74aa Mon Sep 17 00:00:00 2001 From: Jufik Date: Tue, 31 Oct 2023 21:52:43 +0100 Subject: [PATCH 06/12] Add tests --- pyproject.toml | 2 +- .../cli/migrations/templates/generic/README | 1 - tests/test_cli/__init__.py | 8 ++++ tests/test_cli/conftest.py | 25 ++++++++++ tests/test_cli/test_loader.py | 18 ++++++++ tests/test_cli/test_migrations.py | 46 +++++++++++++++++++ 6 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 tests/test_cli/__init__.py create mode 100644 tests/test_cli/conftest.py create mode 100644 tests/test_cli/test_loader.py create mode 100644 tests/test_cli/test_migrations.py diff --git a/pyproject.toml b/pyproject.toml index e84a84eb43..fb8270ad95 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ alembic = "^1.12.0" sqlmodel = "sqlmodel.cli:cli" [tool.poetry.plugins."sqlmodel"] -migrations = "sqlmodel.cli.migrations:migrations" +migrations = "sqlmodel.cli.migrations.cli:migrations" [tool.poetry.group.dev.dependencies] diff --git a/sqlmodel/cli/migrations/templates/generic/README b/sqlmodel/cli/migrations/templates/generic/README index 45acd9d199..2500aa1bcf 100644 --- a/sqlmodel/cli/migrations/templates/generic/README +++ b/sqlmodel/cli/migrations/templates/generic/README @@ -1,2 +1 @@ Generic single-database configuration. -I'm the best! diff --git a/tests/test_cli/__init__.py b/tests/test_cli/__init__.py new file mode 100644 index 0000000000..9ad6f32f25 --- /dev/null +++ b/tests/test_cli/__init__.py @@ -0,0 +1,8 @@ +from typer.testing import CliRunner + + +def test_import_module(): + from sqlmodel.cli import cli + + assert cli is not None + assert cli.__name__ == "ok" \ No newline at end of file diff --git a/tests/test_cli/conftest.py b/tests/test_cli/conftest.py new file mode 100644 index 0000000000..cff1b7cbe1 --- /dev/null +++ b/tests/test_cli/conftest.py @@ -0,0 +1,25 @@ +import pytest +import typer +import pkg_resources +from typer.testing import CliRunner + +app = typer.Typer(name="dummy", help="Dummy app") + + +@pytest.fixture +def runner() -> CliRunner: + yield CliRunner() + + +@pytest.fixture +def fake_cli() -> typer.Typer: + entry_point = pkg_resources.EntryPoint( + name="dummy", module_name="tests.test_cli.conftest", attrs=["app"] + ) + entry_point.extras = [] + distribution = pkg_resources.Distribution() + entry_point.dist = distribution + distribution._ep_map = {"sqlmodel": {"dummy": entry_point}} + pkg_resources.working_set.add(distribution, "dummy") + + return app diff --git a/tests/test_cli/test_loader.py b/tests/test_cli/test_loader.py new file mode 100644 index 0000000000..47d2f850c2 --- /dev/null +++ b/tests/test_cli/test_loader.py @@ -0,0 +1,18 @@ +from typer.testing import CliRunner + + +def test_import_module() -> None: + from sqlmodel.cli import cli + from sqlmodel.cli.migrations.cli import migrations + + assert len(cli.registered_groups) == 1 + assert cli.registered_groups[0].name == "migrations" + + +def test_import_module_with_fake_cli(fake_cli) -> None: + from sqlmodel.cli import get_entry_points, cli + + cli.registered_groups = [] + get_entry_points() + assert len(cli.registered_groups) == 2 + assert cli.registered_groups[0].name == "dummy" diff --git a/tests/test_cli/test_migrations.py b/tests/test_cli/test_migrations.py new file mode 100644 index 0000000000..ec89a67238 --- /dev/null +++ b/tests/test_cli/test_migrations.py @@ -0,0 +1,46 @@ +from sqlmodel.cli import cli +from pathlib import Path +import configparser + + +def test_base_init(tmpdir, runner): + runner.invoke(cli, ["migrations", "init", "--module", str(tmpdir)]) + assert (Path(tmpdir) / "alembic.ini").exists() + + +def test_base_init_with_metadata(tmpdir, runner): + runner.invoke(cli, ["migrations", "init", "--module", str(tmpdir), "path.to.Model"]) + config = configparser.ConfigParser() + config.read(Path(tmpdir) / "alembic.ini") + + assert config["alembic"]["target_metadata"] == "path.to.Model" + + +def test_base_init_with_metadata_and_configfile(tmpdir, runner): + runner.invoke( + cli, + [ + "migrations", + "init", + "--module", + str(tmpdir), + "--config-file", + "foo.ini", + "path.to.Model", + ], + ) + config = configparser.ConfigParser() + config.read(Path(tmpdir) / "foo.ini") + + assert config["alembic"]["target_metadata"] == "path.to.Model" + + +def test_base_init_with_async_template(tmpdir, runner): + runner.invoke( + cli, + ["migrations", "init", "--module", str(tmpdir), "--template", "async"], + ) + with open(Path(tmpdir) / "migrations" / "README") as f: + assert ( + f.read() == "Generic single-database configuration with an async dbapi.\n" + ) From 864116faa4c265fd662e496eeb7ff048e5d5091a Mon Sep 17 00:00:00 2001 From: Jufik Date: Tue, 31 Oct 2023 21:58:54 +0100 Subject: [PATCH 07/12] remove unused imports --- .DS_Store | Bin 0 -> 8196 bytes tests/test_cli/__init__.py | 8 -------- tests/test_cli/test_loader.py | 5 +---- 3 files changed, 1 insertion(+), 12 deletions(-) create mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..30c082949f147d3b62cb8e45930152501efdd7a5 GIT binary patch literal 8196 zcmeHMTWl3Y7@lw2(%I$GZF^WQ_P~jii?u)xU;&Hf^ad1!657&I(BrwRbYahKJ-g== zB*Yr!4UCDmXd*FQ9(<5Q4QO~UlK4Q>_+W?;6L~T55;d9_A52X2pPfB~w(#HshJ>AD z=AVE5`R4M^_wQuRVT_@xlut6YkTJ$73#reh;yOv#MV^tAu%((L2+vrKxvZR>@W!+D zjNVa0`Dda4UF#?rf3x4eotujmpRakq2%|4~XwWg0hgQP)>Y7=#yq3@A z4-JgD69az6FL_;=;(nnGx~`qA)HB(=mN}8uW8JRfXDr7o1^_E>QkMh6mXmiUy4;eJ z_X6yIG;o!xX?^jrW69*EWNRXM@5bcu*2J+*Yuj2A>({j(Kd$om&1*XLqz~H#$2}>x zg79;|=1$w%shRt-y>fTf7W-2EtnBOS%Dz->wlNqR($acEnHJwa?pvh7n`zU$K5d3Czhpv8vEmS<&cb7;cK z`(yhuUe0v#mUD2FJm~n=qn24z>udVeK_^o%1C+7ASj6>aK2pkBkCiO{s2r=wSfc7@ z`GGvmzthbNWM1}fGVbJjzi96+npDbWZJDt`)iZq1qb?C`Yoo4T_bL=Db z3HyRwX5X-H*%kI3`-T0=eq+D0zo4KBHJF7)G$D>9Xu&$HM>{rPGkUQF+t7zy7{&-j zu^$;6!eJEPpo}LliKp;1PT@togqQIO-o|@)A7^n67w{=Q!{@kyAMhi7!XNlssZ;8e z2BlGnDRE`FT(nXNiQ%cdQdW>YEz~I_=%>0MBo19KB;s2dx852dae0mig8GKW*!(3T zGTJ**SNS6uS0UN8bh2;TXgZP+EG-JWSMT_sy;zLAe^T_lNtAw6jE2zsaiQY>S zT3oLo%1iaGWu(p`x=Z!$mUvuOD9WU|dQDu@t0;!l0K2Y*RD{Zgjhd!cQ#482=KD!y zB9Nut*+nCX*!XW8eV3hQAG3?>5^?l<_A@c_A8^b=j2O8bD{&86v5Hu^5%-}ZVCGih z<#z1AAodbB_YpgDFmVuLuuz1LQoztB1BRZ!Gk6xy;d#7((|8T9;|;utw{Qj@OygnS zR375PnLI2H6dl`j4w9w9-l?ESy9pksl4C9u@BiK#|NegyW{!r8JP>){R`CF;dj@*C zXf-$b_9|pcP=186NTT1QlmUba(M7Ze&vBAh|1hL_BDAGKIVmMsDE;q01km*!-T%@3 KACT`hF8&1)z6r_z literal 0 HcmV?d00001 diff --git a/tests/test_cli/__init__.py b/tests/test_cli/__init__.py index 9ad6f32f25..e69de29bb2 100644 --- a/tests/test_cli/__init__.py +++ b/tests/test_cli/__init__.py @@ -1,8 +0,0 @@ -from typer.testing import CliRunner - - -def test_import_module(): - from sqlmodel.cli import cli - - assert cli is not None - assert cli.__name__ == "ok" \ No newline at end of file diff --git a/tests/test_cli/test_loader.py b/tests/test_cli/test_loader.py index 47d2f850c2..ef13a8a060 100644 --- a/tests/test_cli/test_loader.py +++ b/tests/test_cli/test_loader.py @@ -1,10 +1,7 @@ -from typer.testing import CliRunner +from sqlmodel.cli import cli def test_import_module() -> None: - from sqlmodel.cli import cli - from sqlmodel.cli.migrations.cli import migrations - assert len(cli.registered_groups) == 1 assert cli.registered_groups[0].name == "migrations" From 0083a168ba6267e5f7577f925e67f072b36b51ef Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 31 Oct 2023 20:59:09 +0000 Subject: [PATCH 08/12] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_cli/conftest.py | 2 +- tests/test_cli/test_loader.py | 2 +- tests/test_cli/test_migrations.py | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_cli/conftest.py b/tests/test_cli/conftest.py index cff1b7cbe1..55d0fb21aa 100644 --- a/tests/test_cli/conftest.py +++ b/tests/test_cli/conftest.py @@ -1,6 +1,6 @@ +import pkg_resources import pytest import typer -import pkg_resources from typer.testing import CliRunner app = typer.Typer(name="dummy", help="Dummy app") diff --git a/tests/test_cli/test_loader.py b/tests/test_cli/test_loader.py index ef13a8a060..1958695669 100644 --- a/tests/test_cli/test_loader.py +++ b/tests/test_cli/test_loader.py @@ -7,7 +7,7 @@ def test_import_module() -> None: def test_import_module_with_fake_cli(fake_cli) -> None: - from sqlmodel.cli import get_entry_points, cli + from sqlmodel.cli import cli, get_entry_points cli.registered_groups = [] get_entry_points() diff --git a/tests/test_cli/test_migrations.py b/tests/test_cli/test_migrations.py index ec89a67238..440c1bcebb 100644 --- a/tests/test_cli/test_migrations.py +++ b/tests/test_cli/test_migrations.py @@ -1,6 +1,7 @@ -from sqlmodel.cli import cli -from pathlib import Path import configparser +from pathlib import Path + +from sqlmodel.cli import cli def test_base_init(tmpdir, runner): From e8c45fdc5492f02411c7ab663d0f1727531f3671 Mon Sep 17 00:00:00 2001 From: Jufik Date: Tue, 31 Oct 2023 22:07:42 +0100 Subject: [PATCH 09/12] Typing and linting --- sqlmodel/cli/__init__.py | 2 +- sqlmodel/cli/migrations/cli.py | 7 ++++--- tests/test_cli/conftest.py | 2 +- tests/test_cli/test_loader.py | 2 +- tests/test_cli/test_migrations.py | 5 +++-- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/sqlmodel/cli/__init__.py b/sqlmodel/cli/__init__.py index a78013ad0a..764b7cda9f 100644 --- a/sqlmodel/cli/__init__.py +++ b/sqlmodel/cli/__init__.py @@ -8,7 +8,7 @@ cli = Typer() -def get_entry_points(plugin_name: str = "sqlmodel", app: Typer = cli): +def get_entry_points(plugin_name: str = "sqlmodel", app: Typer = cli) -> None: for entry_point in pkg_resources.iter_entry_points(plugin_name): plugin = entry_point.load() cli.add_typer(plugin, name=plugin.info.name) diff --git a/sqlmodel/cli/migrations/cli.py b/sqlmodel/cli/migrations/cli.py index 1564717d73..b38ab08f75 100644 --- a/sqlmodel/cli/migrations/cli.py +++ b/sqlmodel/cli/migrations/cli.py @@ -33,8 +33,8 @@ def init( command.init( config, - _directory, - template=template_path, + str(_directory), + template=str(template_path), package=True, ) logger.debug("Inited alembic") @@ -85,7 +85,8 @@ def show( ) -> None: """Show the revision""" config = Config(module / config_file) - command.show(config, rev) + # Untyped function in Alembic + command.show(config, rev) # type: ignore def merge( diff --git a/tests/test_cli/conftest.py b/tests/test_cli/conftest.py index cff1b7cbe1..55d0fb21aa 100644 --- a/tests/test_cli/conftest.py +++ b/tests/test_cli/conftest.py @@ -1,6 +1,6 @@ +import pkg_resources import pytest import typer -import pkg_resources from typer.testing import CliRunner app = typer.Typer(name="dummy", help="Dummy app") diff --git a/tests/test_cli/test_loader.py b/tests/test_cli/test_loader.py index ef13a8a060..1958695669 100644 --- a/tests/test_cli/test_loader.py +++ b/tests/test_cli/test_loader.py @@ -7,7 +7,7 @@ def test_import_module() -> None: def test_import_module_with_fake_cli(fake_cli) -> None: - from sqlmodel.cli import get_entry_points, cli + from sqlmodel.cli import cli, get_entry_points cli.registered_groups = [] get_entry_points() diff --git a/tests/test_cli/test_migrations.py b/tests/test_cli/test_migrations.py index ec89a67238..440c1bcebb 100644 --- a/tests/test_cli/test_migrations.py +++ b/tests/test_cli/test_migrations.py @@ -1,6 +1,7 @@ -from sqlmodel.cli import cli -from pathlib import Path import configparser +from pathlib import Path + +from sqlmodel.cli import cli def test_base_init(tmpdir, runner): From cc42afb3b35a51cbc58cf5de277965b208f84e72 Mon Sep 17 00:00:00 2001 From: Jufik Date: Tue, 31 Oct 2023 22:12:06 +0100 Subject: [PATCH 10/12] Install missing stubs in pipeline --- .github/workflows/test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 201abc7c22..b8bbc9f068 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -56,7 +56,9 @@ jobs: if: steps.cache.outputs.cache-hit != 'true' run: python -m poetry install - name: Lint - run: python -m poetry run bash scripts/lint.sh + run: | + mypy --install-types + python -m poetry run bash scripts/lint.sh - run: mkdir coverage - name: Test run: python -m poetry run bash scripts/test.sh From 64aedd76316ee7aca9966d93628b7f003620fa66 Mon Sep 17 00:00:00 2001 From: Jufik Date: Tue, 31 Oct 2023 22:14:45 +0100 Subject: [PATCH 11/12] Install missing stubs --- .github/workflows/test.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b8bbc9f068..46ef5978d8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,9 +11,9 @@ on: workflow_dispatch: inputs: debug_enabled: - description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' + description: "Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)" required: false - default: 'false' + default: "false" jobs: test: @@ -48,17 +48,17 @@ jobs: if: steps.cache.outputs.cache-hit != 'true' run: | python -m pip install --upgrade pip + ython3 -m pip install types-setuptools python -m pip install "poetry" python -m poetry self add poetry-version-plugin + - name: Configure poetry run: python -m poetry config virtualenvs.create false - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: python -m poetry install - name: Lint - run: | - mypy --install-types - python -m poetry run bash scripts/lint.sh + run: python -m poetry run bash scripts/lint.sh - run: mkdir coverage - name: Test run: python -m poetry run bash scripts/test.sh @@ -80,7 +80,7 @@ jobs: - uses: actions/setup-python@v4 with: - python-version: '3.8' + python-version: "3.8" - name: Get coverage files uses: actions/download-artifact@v3 @@ -102,7 +102,7 @@ jobs: path: htmlcov # https://github.com/marketplace/actions/alls-green#why - alls-green: # This job does nothing and is only used for the branch protection + alls-green: # This job does nothing and is only used for the branch protection if: always() needs: - coverage-combine From 1eb82af4eb003dd7f5352aff8dab16d14aa345c9 Mon Sep 17 00:00:00 2001 From: Jufik Date: Tue, 31 Oct 2023 22:15:36 +0100 Subject: [PATCH 12/12] fix typo --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 46ef5978d8..9ed9ea433e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,7 +48,7 @@ jobs: if: steps.cache.outputs.cache-hit != 'true' run: | python -m pip install --upgrade pip - ython3 -m pip install types-setuptools + python -m pip install types-setuptools python -m pip install "poetry" python -m poetry self add poetry-version-plugin