From 23407f031e48b94ab249069881b0d74ce1534d49 Mon Sep 17 00:00:00 2001 From: Kumi Date: Tue, 29 Aug 2023 10:30:45 +0200 Subject: [PATCH] Bump version to 0.2.0 and add alembic as a dependency. The commit also includes changes to add Alembic configuration file, add migration scripts for initial migration, and modify the database class to include functions for running migrations and creating new migrations. --- pyproject.toml | 3 +- src/trackbert/alembic.ini | 2 + src/trackbert/classes/database.py | 23 +++++- src/trackbert/migrations/env.py | 72 +++++++++++++++++++ src/trackbert/migrations/script.py.mako | 26 +++++++ .../770fdbef1f4e_initial_migration.py | 60 ++++++++++++++++ 6 files changed, 183 insertions(+), 3 deletions(-) create mode 100644 src/trackbert/alembic.ini create mode 100644 src/trackbert/migrations/env.py create mode 100644 src/trackbert/migrations/script.py.mako create mode 100644 src/trackbert/migrations/versions/770fdbef1f4e_initial_migration.py diff --git a/pyproject.toml b/pyproject.toml index c2b9d08..e5880af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "trackbert" -version = "0.1.3" +version = "0.2.0" authors = [ { name="Kumi Mitterer", email="trackbert@kumi.email" }, ] @@ -23,6 +23,7 @@ dependencies = [ "glsapi", "fedextrack", "sqlalchemy", + "alembic", "python-dateutil", ] diff --git a/src/trackbert/alembic.ini b/src/trackbert/alembic.ini new file mode 100644 index 0000000..0bc695b --- /dev/null +++ b/src/trackbert/alembic.ini @@ -0,0 +1,2 @@ +[alembic] +version_path_separator = os \ No newline at end of file diff --git a/src/trackbert/classes/database.py b/src/trackbert/classes/database.py index 2833844..d9ca303 100644 --- a/src/trackbert/classes/database.py +++ b/src/trackbert/classes/database.py @@ -3,7 +3,13 @@ from sqlalchemy import create_engine, ForeignKey from sqlalchemy.orm import sessionmaker, relationship from sqlalchemy.ext.declarative import declarative_base +from alembic.config import Config +from alembic import command + import json +import time + +from pathlib import Path Base = declarative_base() @@ -35,6 +41,8 @@ class Database: Session = sessionmaker(bind=self.engine) self.session = Session() + self.run_migrations() + def create_shipment(self, tracking_number, carrier, description=""): new_shipment = Shipment( tracking_number=tracking_number, carrier=carrier, description=description @@ -102,5 +110,16 @@ class Database: ) return event - def initialize_db(self): - Base.metadata.create_all(self.engine) + def make_migration(self, message): + alembic_cfg = Config(Path(__file__).parent.parent / "alembic.ini") + alembic_cfg.set_main_option("sqlalchemy.url", self.engine.url.__to_string__(hide_password=False)) + migrations_dir = Path(__file__).parent.parent / 'migrations' + alembic_cfg.set_main_option("script_location", str(migrations_dir)) + command.revision(alembic_cfg, message=message, autogenerate=True) + + def run_migrations(self): + alembic_cfg = Config(Path(__file__).parent.parent / "alembic.ini") + alembic_cfg.set_main_option("sqlalchemy.url", self.engine.url.__to_string__(hide_password=False)) + migrations_dir = Path(__file__).parent.parent / 'migrations' + alembic_cfg.set_main_option("script_location", str(migrations_dir)) + command.upgrade(alembic_cfg, "head") \ No newline at end of file diff --git a/src/trackbert/migrations/env.py b/src/trackbert/migrations/env.py new file mode 100644 index 0000000..22c5f8a --- /dev/null +++ b/src/trackbert/migrations/env.py @@ -0,0 +1,72 @@ +from sqlalchemy import engine_from_config +from sqlalchemy import pool +from alembic import context + +from trackbert.classes.database import Base + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = Base.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/src/trackbert/migrations/script.py.mako b/src/trackbert/migrations/script.py.mako new file mode 100644 index 0000000..fbc4b07 --- /dev/null +++ b/src/trackbert/migrations/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/src/trackbert/migrations/versions/770fdbef1f4e_initial_migration.py b/src/trackbert/migrations/versions/770fdbef1f4e_initial_migration.py new file mode 100644 index 0000000..302cb1c --- /dev/null +++ b/src/trackbert/migrations/versions/770fdbef1f4e_initial_migration.py @@ -0,0 +1,60 @@ +"""Initial migration + +Revision ID: 770fdbef1f4e +Revises: +Create Date: 2023-08-29 10:01:10.100731 + +""" +from typing import Sequence, Union + +from alembic import op +from sqlalchemy.exc import ProgrammingError, OperationalError + +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "770fdbef1f4e" +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: + if not table_exists("shipments"): + op.create_table( + "shipments", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("tracking_number", sa.String(), nullable=True), + sa.Column("carrier", sa.String(), nullable=True), + sa.Column("description", sa.String(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + + if not table_exists("events"): + op.create_table( + "events", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("event_time", sa.String(), nullable=True), + sa.Column("event_description", sa.String(), nullable=True), + sa.Column("raw_event", sa.String(), nullable=True), + sa.Column("shipment_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["shipment_id"], + ["shipments.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + + +def downgrade() -> None: + op.drop_table("events") + op.drop_table("shipments") + + +def table_exists(table_name): + try: + op.execute(f'SELECT 1 FROM "{table_name}" LIMIT 1;') + return True + except (OperationalError, ProgrammingError): + return False