From 58d64f89e59553d475e3b1b9c4c8aa8de5974edd Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Sun, 26 Jan 2025 21:29:55 +0000 Subject: [PATCH 01/15] Remove extra empty lines --- src/alembic/script.py.mako | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/alembic/script.py.mako b/src/alembic/script.py.mako index f1c582af..eb6577a4 100644 --- a/src/alembic/script.py.mako +++ b/src/alembic/script.py.mako @@ -25,21 +25,17 @@ def upgrade(engine_name: str) -> None: def downgrade(engine_name: str) -> None: globals()[f"downgrade_{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(f"{db_name}_upgrades", "pass")} def downgrade_${db_name}() -> None: ${context.get(f"{db_name}_downgrades", "pass")} - % endfor From 540c4f2c8cedd15b175d7b71fd3e23238463b306 Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Sun, 26 Jan 2025 21:30:51 +0000 Subject: [PATCH 02/15] Inject table for fixtures processing in all binds --- src/alembic/env.py | 16 ++++++++ ...-52b1246eda46_initialize_fixture_tables.py | 38 +++++++++++++++++++ ...212826-bd73bd8a2ac4_create_books_table.py} | 15 ++++---- 3 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 src/alembic/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py rename src/alembic/versions/{2022-11-09-203313-52b1246eda46_create_tables.py => 2025-01-26-212826-bd73bd8a2ac4_create_books_table.py} (80%) diff --git a/src/alembic/env.py b/src/alembic/env.py index 00629292..e6d9ad43 100644 --- a/src/alembic/env.py +++ b/src/alembic/env.py @@ -1,7 +1,10 @@ import logging from asyncio import get_event_loop +from datetime import datetime +from sqlalchemy import Table, Column, String, DateTime from sqlalchemy.ext.asyncio import AsyncEngine +from sqlalchemy.orm import registry from alembic import context from common.bootstrap import application_init @@ -32,6 +35,19 @@ db_names = target_metadata.keys() config.set_main_option("databases", ",".join(db_names)) +def inject_fixture_tables(registry_mapper: registry): + Table( + "alembic_fixtures", + registry_mapper.metadata, + Column("filename", String(), primary_key=True), + Column("signature", String(), nullable=False), + Column("processed_at", DateTime(timezone=True), nullable=False, default=datetime.now), + ) + +for name in db_names: + inject_fixture_tables(sa_manager.get_bind(name).registry_mapper) + + # add your model's MetaData objects here # for 'autogenerate' support. These must be set # up to hold just those tables targeting a diff --git a/src/alembic/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py b/src/alembic/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py new file mode 100644 index 00000000..87e842f7 --- /dev/null +++ b/src/alembic/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py @@ -0,0 +1,38 @@ +"""Initialize fixture tables + +Revision ID: 52b1246eda46 +Revises: +Create Date: 2025-01-26 21:23:26.321986 + +""" + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "52b1246eda46" +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(engine_name: str) -> None: + globals()[f"upgrade_{engine_name}"]() + + +def downgrade(engine_name: str) -> None: + globals()[f"downgrade_{engine_name}"]() + + +def upgrade_default() -> None: + op.create_table('alembic_fixtures', + sa.Column('filename', sa.String(), nullable=False), + sa.Column('signature', sa.String(), nullable=False), + sa.Column('processed_at', sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint('filename') + ) + + +def downgrade_default() -> None: + op.drop_table('alembic_fixtures') diff --git a/src/alembic/versions/2022-11-09-203313-52b1246eda46_create_tables.py b/src/alembic/versions/2025-01-26-212826-bd73bd8a2ac4_create_books_table.py similarity index 80% rename from src/alembic/versions/2022-11-09-203313-52b1246eda46_create_tables.py rename to src/alembic/versions/2025-01-26-212826-bd73bd8a2ac4_create_books_table.py index 803df1fe..65c1a101 100644 --- a/src/alembic/versions/2022-11-09-203313-52b1246eda46_create_tables.py +++ b/src/alembic/versions/2025-01-26-212826-bd73bd8a2ac4_create_books_table.py @@ -1,18 +1,17 @@ -"""create books table +"""Create books table -Revision ID: 52b1246eda46 -Revises: -Create Date: 2022-11-09 20:33:13.035514 +Revision ID: bd73bd8a2ac4 +Revises: 52b1246eda46 +Create Date: 2025-01-26 21:28:26.321986 """ - +from alembic import op import sqlalchemy as sa -from alembic import op # revision identifiers, used by Alembic. -revision = "52b1246eda46" -down_revision = None +revision = 'bd73bd8a2ac4' +down_revision = '52b1246eda46' branch_labels = None depends_on = None From 630988d6f26da4f8e1a41eeb7d3de99b17f8e6c9 Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Sun, 26 Jan 2025 22:05:51 +0000 Subject: [PATCH 03/15] Fix sync migrations --- src/alembic/env.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/alembic/env.py b/src/alembic/env.py index e6d9ad43..f3455eff 100644 --- a/src/alembic/env.py +++ b/src/alembic/env.py @@ -155,7 +155,7 @@ def migration_callable(*args, **kwargs): await rec["connection"].run_sync(migration_callable) else: - do_run_migration(name, rec) + do_run_migration(rec["connection"], name) if USE_TWOPHASE: for rec in engines.values(): From ec5a4589620c12240375c9e92e0ff00fc374e499 Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Mon, 27 Jan 2025 00:53:03 +0000 Subject: [PATCH 04/15] Use model to insert all fixtures in the same transaction. Initial async fixtures implementation. --- .idea/dataSources.xml | 2 +- src/alembic/env.py | 86 ++++++++++++++++--- src/alembic/fixtures/__init__.py | 0 src/alembic/fixtures/books_example.py | 25 ++++++ ...-52b1246eda46_initialize_fixture_tables.py | 3 +- 5 files changed, 100 insertions(+), 16 deletions(-) create mode 100644 src/alembic/fixtures/__init__.py create mode 100644 src/alembic/fixtures/books_example.py diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml index db9e40be..4a94487f 100644 --- a/.idea/dataSources.xml +++ b/.idea/dataSources.xml @@ -5,7 +5,7 @@ sqlite.xerial true org.sqlite.JDBC - jdbc:sqlite:$PROJECT_DIR$/sqlite.db + jdbc:sqlite:$PROJECT_DIR$/src/sqlite.db diff --git a/src/alembic/env.py b/src/alembic/env.py index f3455eff..363aa550 100644 --- a/src/alembic/env.py +++ b/src/alembic/env.py @@ -1,10 +1,15 @@ +import hashlib +import importlib import logging +import sys from asyncio import get_event_loop -from datetime import datetime +from datetime import datetime, timezone +from os import listdir, path +from os.path import isfile, join -from sqlalchemy import Table, Column, String, DateTime -from sqlalchemy.ext.asyncio import AsyncEngine -from sqlalchemy.orm import registry +from sqlalchemy import Table, Column, String, DateTime, UniqueConstraint, text, select +from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker +from sqlalchemy.orm import registry, sessionmaker, Mapped, mapped_column from alembic import context from common.bootstrap import application_init @@ -35,17 +40,21 @@ db_names = target_metadata.keys() config.set_main_option("databases", ",".join(db_names)) -def inject_fixture_tables(registry_mapper: registry): - Table( - "alembic_fixtures", - registry_mapper.metadata, - Column("filename", String(), primary_key=True), - Column("signature", String(), nullable=False), - Column("processed_at", DateTime(timezone=True), nullable=False, default=datetime.now), - ) +def generate_fixture_migration_model(declarative_base: type): + class FixtureMigration(declarative_base): + __tablename__ = "alembic_fixtures" + + bind: Mapped[str] = mapped_column(String(), primary_key=True) + filename: Mapped[str] = mapped_column(String(), primary_key=True) + signature: Mapped[str] = mapped_column(String(), nullable=False) + processed_at: Mapped[datetime] = mapped_column(DateTime(), nullable=False, default=datetime.now) + return FixtureMigration +fixture_migration_models = {} for name in db_names: - inject_fixture_tables(sa_manager.get_bind(name).registry_mapper) + fixture_migration_models[name] = generate_fixture_migration_model( + sa_manager.get_bind(name).declarative_base + ) # add your model's MetaData objects here @@ -66,6 +75,51 @@ def inject_fixture_tables(registry_mapper: registry): # my_important_option = config.get_main_option("my_important_option") # ... etc. +def calculate_md5(file_path): + hasher = hashlib.md5() + with open(file_path, 'rb') as file: + hasher.update(file.read()) + return hasher.hexdigest() + +async def a_migrate_fixtures(bind_name: str, Session: async_sessionmaker[AsyncSession]) -> str: + alembic_path = path.dirname(path.realpath(__file__)) + fixtures_path = alembic_path + "/fixtures" + sys.path.append(alembic_path) + module_names = [ + f[:-3] for f in listdir(fixtures_path) + if isfile(join(fixtures_path, f)) + and f.endswith(".py") + and f != "__init__.py" + ] + async with Session() as session: + for module_name in module_names: + logging.debug(f"Creating {module_name} fixtures for {bind_name}") + m = importlib.import_module(f"fixtures.{module_name}") + fixture_migration = await session.get( + fixture_migration_models[bind_name], + (bind_name, f"{module_name}.py") + ) + signature = calculate_md5(f"{fixtures_path}/{module_name}.py") + if fixture_migration: + if signature != fixture_migration.signature: + logging.warning(f"Signature mismatch for {fixture_migration.filename} fixture. The file has been modified after being processed.") + logging.debug(f"{module_name} fixtures already migrated for {bind_name}") + continue + + session.add_all(m.fixtures().get(bind_name, [])) + session.add(fixture_migration_models[bind_name]( + bind=bind_name, + filename=f"{module_name}.py", + signature=signature, + )) + try: + await session.commit() + logging.info(f"{module_name} fixtures correctly created for {bind_name}") + except: + await session.rollback() + logging.error(f"{module_name} fixtures failed to apply to {bind_name}") + + def run_migrations_offline() -> None: """Run migrations in 'offline' mode. @@ -149,13 +203,17 @@ async def run_migrations_online() -> None: for name, rec in engines.items(): logger.info(f"Migrating database {name}") if isinstance(rec["engine"], AsyncEngine): - def migration_callable(*args, **kwargs): return do_run_migration(*args, name=name, **kwargs) await rec["connection"].run_sync(migration_callable) + Session = async_sessionmaker(bind=rec["connection"]) + await a_migrate_fixtures(bind_name=name, Session=Session) + else: do_run_migration(rec["connection"], name) + # Session = sessionmaker(bind=rec["connection"]) + # session = Session() if USE_TWOPHASE: for rec in engines.values(): diff --git a/src/alembic/fixtures/__init__.py b/src/alembic/fixtures/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/alembic/fixtures/books_example.py b/src/alembic/fixtures/books_example.py new file mode 100644 index 00000000..e6f600bd --- /dev/null +++ b/src/alembic/fixtures/books_example.py @@ -0,0 +1,25 @@ +""" +`fixtures` is a dictionary following the format: + +"BIND_NAME": "LIST_OF_FACTORIES" +""" +from typing import Dict, List + +from factory import Factory + +from domains.books._models import BookModel + + +def fixtures() -> Dict[str, List]: + class BookFactory(Factory): + class Meta: + model = BookModel + + return { + "default": [ + BookFactory( + title="The Shining", + author_name="Stephen King", + ), + ], + } diff --git a/src/alembic/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py b/src/alembic/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py index 87e842f7..1fc75592 100644 --- a/src/alembic/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py +++ b/src/alembic/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py @@ -27,10 +27,11 @@ def downgrade(engine_name: str) -> None: def upgrade_default() -> None: op.create_table('alembic_fixtures', + sa.Column('bind', sa.String(), nullable=False), sa.Column('filename', sa.String(), nullable=False), sa.Column('signature', sa.String(), nullable=False), sa.Column('processed_at', sa.DateTime(timezone=True), nullable=False), - sa.PrimaryKeyConstraint('filename') + sa.PrimaryKeyConstraint('bind', 'filename'), ) From 8819ad208f46f641373f7224b2c2d5758b129a09 Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Mon, 27 Jan 2025 01:06:51 +0000 Subject: [PATCH 05/15] Improve log messages --- src/alembic/env.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/alembic/env.py b/src/alembic/env.py index 363aa550..f7501d8a 100644 --- a/src/alembic/env.py +++ b/src/alembic/env.py @@ -102,8 +102,11 @@ async def a_migrate_fixtures(bind_name: str, Session: async_sessionmaker[AsyncSe signature = calculate_md5(f"{fixtures_path}/{module_name}.py") if fixture_migration: if signature != fixture_migration.signature: - logging.warning(f"Signature mismatch for {fixture_migration.filename} fixture. The file has been modified after being processed.") - logging.debug(f"{module_name} fixtures already migrated for {bind_name}") + logging.warning(f"Signature mismatch for {fixture_migration.filename} fixture." + f" The file has been already processed but has been modified since then." + f" It will not be processed again.") + else: + logging.debug(f"{module_name} fixtures already processed for {bind_name}") continue session.add_all(m.fixtures().get(bind_name, [])) From 0f5da8f67e616deff693939f61670ab1ed85c99c Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Mon, 27 Jan 2025 01:11:45 +0000 Subject: [PATCH 06/15] Make ruff happy --- src/alembic/env.py | 76 +++++++++++-------- src/alembic/fixtures/books_example.py | 1 + ...-52b1246eda46_initialize_fixture_tables.py | 15 ++-- ...-212826-bd73bd8a2ac4_create_books_table.py | 7 +- 4 files changed, 59 insertions(+), 40 deletions(-) diff --git a/src/alembic/env.py b/src/alembic/env.py index f7501d8a..9d53dddb 100644 --- a/src/alembic/env.py +++ b/src/alembic/env.py @@ -3,13 +3,13 @@ import logging import sys from asyncio import get_event_loop -from datetime import datetime, timezone +from datetime import datetime from os import listdir, path from os.path import isfile, join -from sqlalchemy import Table, Column, String, DateTime, UniqueConstraint, text, select +from sqlalchemy import DateTime, String from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker -from sqlalchemy.orm import registry, sessionmaker, Mapped, mapped_column +from sqlalchemy.orm import Mapped, mapped_column from alembic import context from common.bootstrap import application_init @@ -40,6 +40,7 @@ db_names = target_metadata.keys() config.set_main_option("databases", ",".join(db_names)) + def generate_fixture_migration_model(declarative_base: type): class FixtureMigration(declarative_base): __tablename__ = "alembic_fixtures" @@ -47,9 +48,13 @@ class FixtureMigration(declarative_base): bind: Mapped[str] = mapped_column(String(), primary_key=True) filename: Mapped[str] = mapped_column(String(), primary_key=True) signature: Mapped[str] = mapped_column(String(), nullable=False) - processed_at: Mapped[datetime] = mapped_column(DateTime(), nullable=False, default=datetime.now) + processed_at: Mapped[datetime] = mapped_column( + DateTime(), nullable=False, default=datetime.now + ) + return FixtureMigration + fixture_migration_models = {} for name in db_names: fixture_migration_models[name] = generate_fixture_migration_model( @@ -75,55 +80,64 @@ class FixtureMigration(declarative_base): # my_important_option = config.get_main_option("my_important_option") # ... etc. -def calculate_md5(file_path): - hasher = hashlib.md5() - with open(file_path, 'rb') as file: + +def calculate_signature(file_path): + hasher = hashlib.sha256() + with open(file_path, "rb") as file: hasher.update(file.read()) return hasher.hexdigest() -async def a_migrate_fixtures(bind_name: str, Session: async_sessionmaker[AsyncSession]) -> str: + +async def a_migrate_fixtures( + bind_name: str, session: async_sessionmaker[AsyncSession] +) -> str: alembic_path = path.dirname(path.realpath(__file__)) fixtures_path = alembic_path + "/fixtures" sys.path.append(alembic_path) module_names = [ - f[:-3] for f in listdir(fixtures_path) - if isfile(join(fixtures_path, f)) - and f.endswith(".py") - and f != "__init__.py" + f[:-3] + for f in listdir(fixtures_path) + if isfile(join(fixtures_path, f)) and f.endswith(".py") and f != "__init__.py" ] - async with Session() as session: + async with session() as session: for module_name in module_names: logging.debug(f"Creating {module_name} fixtures for {bind_name}") m = importlib.import_module(f"fixtures.{module_name}") fixture_migration = await session.get( - fixture_migration_models[bind_name], - (bind_name, f"{module_name}.py") + fixture_migration_models[bind_name], (bind_name, f"{module_name}.py") ) - signature = calculate_md5(f"{fixtures_path}/{module_name}.py") + signature = calculate_signature(f"{fixtures_path}/{module_name}.py") if fixture_migration: if signature != fixture_migration.signature: - logging.warning(f"Signature mismatch for {fixture_migration.filename} fixture." - f" The file has been already processed but has been modified since then." - f" It will not be processed again.") + logging.warning( + f"Signature mismatch for {fixture_migration.filename} fixture." + f" The file has been already processed but has been modified" + f" since then. It will not be processed again." + ) else: - logging.debug(f"{module_name} fixtures already processed for {bind_name}") + logging.debug( + f"{module_name} fixtures already processed for {bind_name}" + ) continue session.add_all(m.fixtures().get(bind_name, [])) - session.add(fixture_migration_models[bind_name]( - bind=bind_name, - filename=f"{module_name}.py", - signature=signature, - )) + session.add( + fixture_migration_models[bind_name]( + bind=bind_name, + filename=f"{module_name}.py", + signature=signature, + ) + ) try: await session.commit() - logging.info(f"{module_name} fixtures correctly created for {bind_name}") - except: + logging.info( + f"{module_name} fixtures correctly created for {bind_name}" + ) + except Exception: await session.rollback() logging.error(f"{module_name} fixtures failed to apply to {bind_name}") - def run_migrations_offline() -> None: """Run migrations in 'offline' mode. @@ -206,12 +220,14 @@ async def run_migrations_online() -> None: for name, rec in engines.items(): logger.info(f"Migrating database {name}") if isinstance(rec["engine"], AsyncEngine): + def migration_callable(*args, **kwargs): return do_run_migration(*args, name=name, **kwargs) await rec["connection"].run_sync(migration_callable) - Session = async_sessionmaker(bind=rec["connection"]) - await a_migrate_fixtures(bind_name=name, Session=Session) + await a_migrate_fixtures( + bind_name=name, session=async_sessionmaker(bind=rec["connection"]) + ) else: do_run_migration(rec["connection"], name) diff --git a/src/alembic/fixtures/books_example.py b/src/alembic/fixtures/books_example.py index e6f600bd..0c044d1f 100644 --- a/src/alembic/fixtures/books_example.py +++ b/src/alembic/fixtures/books_example.py @@ -3,6 +3,7 @@ "BIND_NAME": "LIST_OF_FACTORIES" """ + from typing import Dict, List from factory import Factory diff --git a/src/alembic/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py b/src/alembic/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py index 1fc75592..0f08f61f 100644 --- a/src/alembic/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py +++ b/src/alembic/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py @@ -26,14 +26,15 @@ def downgrade(engine_name: str) -> None: def upgrade_default() -> None: - op.create_table('alembic_fixtures', - sa.Column('bind', sa.String(), nullable=False), - sa.Column('filename', sa.String(), nullable=False), - sa.Column('signature', sa.String(), nullable=False), - sa.Column('processed_at', sa.DateTime(timezone=True), nullable=False), - sa.PrimaryKeyConstraint('bind', 'filename'), + op.create_table( + "alembic_fixtures", + sa.Column("bind", sa.String(), nullable=False), + sa.Column("filename", sa.String(), nullable=False), + sa.Column("signature", sa.String(), nullable=False), + sa.Column("processed_at", sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint("bind", "filename"), ) def downgrade_default() -> None: - op.drop_table('alembic_fixtures') + op.drop_table("alembic_fixtures") diff --git a/src/alembic/versions/2025-01-26-212826-bd73bd8a2ac4_create_books_table.py b/src/alembic/versions/2025-01-26-212826-bd73bd8a2ac4_create_books_table.py index 65c1a101..b0f60cc4 100644 --- a/src/alembic/versions/2025-01-26-212826-bd73bd8a2ac4_create_books_table.py +++ b/src/alembic/versions/2025-01-26-212826-bd73bd8a2ac4_create_books_table.py @@ -5,13 +5,14 @@ Create Date: 2025-01-26 21:28:26.321986 """ -from alembic import op + import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. -revision = 'bd73bd8a2ac4' -down_revision = '52b1246eda46' +revision = "bd73bd8a2ac4" +down_revision = "52b1246eda46" branch_labels = None depends_on = None From 249dd16462f201a6153e5e2000c711abc117dc1a Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Mon, 27 Jan 2025 20:28:58 +0000 Subject: [PATCH 07/15] Refactor fixture migration logic into `FixtureHandler` class This refactor centralizes and organizes the fixture migration logic within the new `FixtureHandler` class. It improves modularity, adds clarity with docstrings, and ensures code reuse for both synchronous and asynchronous contexts. Additionally, it updates the linting configuration to allow long lines in `env.py`. --- pyproject.toml | 1 + src/alembic/env.py | 280 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 229 insertions(+), 52 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8330357f..d584d7f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -151,3 +151,4 @@ ignore = [ [tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401"] # Ignore unused imports on init files "tests/**/*.py" = ["S101"] # Allow assert usage on tests +"src/alembic/env.py" = ["E501"] # Allow long lines diff --git a/src/alembic/env.py b/src/alembic/env.py index 9d53dddb..e5eea2d6 100644 --- a/src/alembic/env.py +++ b/src/alembic/env.py @@ -6,10 +6,12 @@ from datetime import datetime from os import listdir, path from os.path import isfile, join +from types import ModuleType +from typing import List, Union from sqlalchemy import DateTime, String from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker -from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy.orm import Mapped, Session, mapped_column, sessionmaker from alembic import context from common.bootstrap import application_init @@ -81,61 +83,234 @@ class FixtureMigration(declarative_base): # ... etc. -def calculate_signature(file_path): - hasher = hashlib.sha256() - with open(file_path, "rb") as file: - hasher.update(file.read()) - return hasher.hexdigest() - - -async def a_migrate_fixtures( - bind_name: str, session: async_sessionmaker[AsyncSession] -) -> str: +class FixtureHandler: alembic_path = path.dirname(path.realpath(__file__)) fixtures_path = alembic_path + "/fixtures" - sys.path.append(alembic_path) - module_names = [ - f[:-3] - for f in listdir(fixtures_path) - if isfile(join(fixtures_path, f)) and f.endswith(".py") and f != "__init__.py" - ] - async with session() as session: - for module_name in module_names: - logging.debug(f"Creating {module_name} fixtures for {bind_name}") - m = importlib.import_module(f"fixtures.{module_name}") - fixture_migration = await session.get( - fixture_migration_models[bind_name], (bind_name, f"{module_name}.py") + logger = logging.getLogger("alembic.runtime.fixtures") + + @classmethod + def _calculate_signature(cls, fixture_module: ModuleType) -> str: + """ + Calculate the SHA-256 signature for a fixture module's corresponding file. + + This method computes a unique hash for the content of a specific Python source + file associated with a given fixture module. The hash is calculated using the + SHA-256 algorithm, ensuring a consistent and secure checksum. + + Args: + fixture_module (ModuleType): The module whose associated file's signature + needs to be calculated. + + Returns: + str: The hexadecimal SHA-256 hash of the file content. + """ + file_path = f"{cls.fixtures_path}/{fixture_module.__name__[9:]}.py" + hasher = hashlib.sha256() + with open(file_path, "rb") as file: + hasher.update(file.read()) + return hasher.hexdigest() + + @classmethod + def _get_fixture_modules(cls) -> List[ModuleType]: + """ + This private class method is responsible for retrieving modules from the fixtures + directory defined by the class attributes. It dynamically imports Python modules + located in the specified fixtures directory and filters out non-Python files + or the __init__.py file. It adds the Alembic path to the system path to ensure + successful imports. + + Parameters + ---------- + None + + Returns + ------- + List[ModuleType] + A list of imported module objects dynamically loaded from the fixtures + directory. + """ + sys.path.append(cls.alembic_path) + return [ + importlib.import_module(f"fixtures.{f[:-3]}") + for f in listdir(cls.fixtures_path) + if isfile(join(cls.fixtures_path, f)) + and f.endswith(".py") + and f != "__init__.py" + ] + + @classmethod + def _fixture_already_migrated(cls, fixture_migration, signature) -> bool: + """ + Determines if a fixture has already been migrated based on the given fixture + migration and its signature. + + The method examines the provided fixture migration data and its signature to + decide whether the fixture has already been processed. If the signatures do not + match, a warning is logged to indicate potential modifications. Otherwise, a debug + message is logged to confirm prior processing. The return value indicates whether + the fixture should be skipped. + + Args: + fixture_migration (FixtureMigration | None): An object representing the migration + details of a fixture. Can be None. + signature (str): A unique string indicating the signature of the current fixture. + + Returns: + bool: True if the fixture has already been migrated and should not be processed + again; False otherwise. + """ + if fixture_migration: + if signature != fixture_migration.signature: + cls.logger.warning( + f"Signature mismatch for `{fixture_migration.filename}` fixture." + f" The file has been already processed but has been modified" + f" since then. It will not be processed again." + ) + else: + cls.logger.debug( + f"`{fixture_migration.filename}` fixtures already processed for `{fixture_migration.bind}` bind" + ) + return True + return False + + @classmethod + def _add_fixture_data_to_session( + cls, + bind_name: str, + fixture_module: ModuleType, + session: Union[Session, AsyncSession], + signature: str, + ): + """ + Adds fixture data and migration model to the given session. + + This method interacts with the database session to add predefined fixture data + and creates a corresponding migration model for tracking purposes. The fixture + data is retrieved from the specified fixture module, based on the provided bind + name. The migration model contains metadata about the fixture module and its + signature. + + Args: + bind_name (str): The binding name used to fetch fixture data from the + fixture module. + fixture_module (ModuleType): The module containing fixture data and fixture + metadata definitions. + session (Union[Session, AsyncSession]): A database session where fixture + data and migration models are added. + signature (str): A unique signature representing the state of the fixture + module. + + Returns: + None + """ + session.add_all(fixture_module.fixtures().get(bind_name, [])) + session.add( + fixture_migration_models[bind_name]( + bind=bind_name, + filename=f"{fixture_module.__name__}", + signature=signature, ) - signature = calculate_signature(f"{fixtures_path}/{module_name}.py") - if fixture_migration: - if signature != fixture_migration.signature: - logging.warning( - f"Signature mismatch for {fixture_migration.filename} fixture." - f" The file has been already processed but has been modified" - f" since then. It will not be processed again." + ) + + @classmethod + async def a_migrate_fixtures( + cls, bind_name: str, session: async_sessionmaker[AsyncSession] + ): + """ + Perform asynchronous migration of fixture data modules for a specific database bind. + + This method iterates over fixture data modules, calculates their signatures, and determines + whether fixtures have already been migrated for a specific database bind. If not, it migrates + them by adding the data to the session and commits the changes. If an error occurs during + the commit, it rolls back the session. Logs are produced at each significant step. + + Args: + bind_name: The name of the database bind for which the fixtures are being migrated. + session: An instance of `async_sessionmaker[AsyncSession]` used for interacting with + the database. + + Raises: + Exception: If a commit to the database fails. + + Returns: + None + """ + modules = cls._get_fixture_modules() + async with session() as session: + for fixture_module in modules: + cls.logger.debug( + f"Creating `{fixture_module.__name__}` fixtures for `{bind_name}` bind" + ) + fixture_migration = await session.get( + fixture_migration_models[bind_name], + (bind_name, f"{fixture_module.__name__}"), + ) + + signature = cls._calculate_signature(fixture_module) + if cls._fixture_already_migrated(fixture_migration, signature): + continue + + cls._add_fixture_data_to_session( + bind_name, fixture_module, session, signature + ) + try: + await session.commit() + cls.logger.info( + f"`{fixture_module.__name__}` fixtures correctly created for `{bind_name}` bind" ) - else: - logging.debug( - f"{module_name} fixtures already processed for {bind_name}" + except Exception: + await session.rollback() + cls.logger.error( + f"`{fixture_module.__name__}` fixtures failed to apply to `{bind_name}` bind" ) - continue - - session.add_all(m.fixtures().get(bind_name, [])) - session.add( - fixture_migration_models[bind_name]( - bind=bind_name, - filename=f"{module_name}.py", - signature=signature, + + @classmethod + def migrate_fixtures(cls, bind_name: str, session: sessionmaker[Session]): + """ + Migrate fixture data for a specified bind to the database session. This process involves identifying + fixture modules, calculating their signatures, checking if a module's data is already migrated, and + applying the fixture data if necessary. The migration process is committed to the session or rolled back + in case of failure. + + Parameters: + cls: Type[CurrentClassType] + The class on which the method is being called. + bind_name: str + The name of the database bind to which the fixtures are being migrated. + session: sessionmaker[Session] + The SQLAlchemy session maker instance used for initiating the session. + + Raises: + None explicitly raised but may propagate exceptions during database operations. + """ + modules = cls._get_fixture_modules() + with session() as session: + for fixture_module in modules: + cls.logger.debug( + f"Creating `{fixture_module.__name__}` fixtures for `{bind_name}` bind" ) - ) - try: - await session.commit() - logging.info( - f"{module_name} fixtures correctly created for {bind_name}" + fixture_migration = session.get( + fixture_migration_models[bind_name], + (bind_name, f"{fixture_module.__name__}"), + ) + + signature = cls._calculate_signature(fixture_module) + if cls._fixture_already_migrated(fixture_migration, signature): + continue + + cls._add_fixture_data_to_session( + bind_name, fixture_module, session, signature ) - except Exception: - await session.rollback() - logging.error(f"{module_name} fixtures failed to apply to {bind_name}") + try: + session.commit() + cls.logger.info( + f"`{fixture_module.__name__}` fixtures correctly created for `{bind_name}` bind" + ) + except Exception: + session.rollback() + cls.logger.error( + f"`{fixture_module.__name__}` fixtures failed to apply to `{bind_name}` bind" + ) def run_migrations_offline() -> None: @@ -225,14 +400,15 @@ def migration_callable(*args, **kwargs): return do_run_migration(*args, name=name, **kwargs) await rec["connection"].run_sync(migration_callable) - await a_migrate_fixtures( + await FixtureHandler.a_migrate_fixtures( bind_name=name, session=async_sessionmaker(bind=rec["connection"]) ) else: do_run_migration(rec["connection"], name) - # Session = sessionmaker(bind=rec["connection"]) - # session = Session() + FixtureHandler.migrate_fixtures( + bind_name=name, session=sessionmaker(bind=rec["connection"]) + ) if USE_TWOPHASE: for rec in engines.values(): From 3443dce54bd6870a4fa42786ad5aa165cbe1dc0c Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Mon, 27 Jan 2025 20:34:31 +0000 Subject: [PATCH 08/15] Refactor fixture loading to use dictionary instead of function. Replaced the `fixtures()` function with a dictionary object for improved readability and simplicity. Updated the related call to access the dictionary directly, eliminating unnecessary function wrapping. --- src/alembic/env.py | 2 +- src/alembic/fixtures/books_example.py | 27 ++++++++++++--------------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/alembic/env.py b/src/alembic/env.py index e5eea2d6..5dbbee7d 100644 --- a/src/alembic/env.py +++ b/src/alembic/env.py @@ -203,7 +203,7 @@ def _add_fixture_data_to_session( Returns: None """ - session.add_all(fixture_module.fixtures().get(bind_name, [])) + session.add_all(fixture_module.fixtures.get(bind_name, [])) session.add( fixture_migration_models[bind_name]( bind=bind_name, diff --git a/src/alembic/fixtures/books_example.py b/src/alembic/fixtures/books_example.py index 0c044d1f..cdbfa8bc 100644 --- a/src/alembic/fixtures/books_example.py +++ b/src/alembic/fixtures/books_example.py @@ -3,24 +3,21 @@ "BIND_NAME": "LIST_OF_FACTORIES" """ - -from typing import Dict, List - from factory import Factory from domains.books._models import BookModel -def fixtures() -> Dict[str, List]: - class BookFactory(Factory): - class Meta: - model = BookModel +class BookFactory(Factory): + class Meta: + model = BookModel + - return { - "default": [ - BookFactory( - title="The Shining", - author_name="Stephen King", - ), - ], - } +fixtures = { + "default": [ + BookFactory( + title="The Shining", + author_name="Stephen King", + ), + ], +} From 0f9332cd08da4a6f811fac208695f68ef0b9490a Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Mon, 27 Jan 2025 20:37:51 +0000 Subject: [PATCH 09/15] Update dependencies to their latest versions This commit updates multiple dependencies in `uv.lock`, including `cloudevents-pydantic`, `deprecated`, `faker`, `fastapi`, `graphql-core`, and `grpcio`. These updates ensure improved functionality, compatibility, and potential bug fixes. --- src/alembic/fixtures/books_example.py | 1 + uv.lock | 182 +++++++++++++------------- 2 files changed, 92 insertions(+), 91 deletions(-) diff --git a/src/alembic/fixtures/books_example.py b/src/alembic/fixtures/books_example.py index cdbfa8bc..c58268cc 100644 --- a/src/alembic/fixtures/books_example.py +++ b/src/alembic/fixtures/books_example.py @@ -3,6 +3,7 @@ "BIND_NAME": "LIST_OF_FACTORIES" """ + from factory import Factory from domains.books._models import BookModel diff --git a/uv.lock b/uv.lock index fa405046..3c40b0b5 100644 --- a/uv.lock +++ b/uv.lock @@ -400,16 +400,16 @@ wheels = [ [[package]] name = "cloudevents-pydantic" -version = "0.0.3" +version = "0.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cloudevents" }, { name = "pydantic" }, { name = "python-ulid" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cf/e4/cc005b07e1b2f01be0f6058a5ef747dcc5a91f77d217c0f2a25442eceba4/cloudevents_pydantic-0.0.3.tar.gz", hash = "sha256:5f20d479ae18094b2b0b7f2072cc1b615931af788f0c9ae35bcdfd1001c2aee1", size = 11151 } +sdist = { url = "https://files.pythonhosted.org/packages/89/18/5418acc834018c88d220c03e414f7c15d7b826798475d05a79ef5e6ee2ff/cloudevents_pydantic-0.1.0.tar.gz", hash = "sha256:5fb6c8324d8ab02ca833abca975cf72d741aa28e3d6779cfa840a4fae494b3d3", size = 12358 } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/69/5ed8a9857d4b3a3db2e30b94aa60c95220af63b631c4b10462759f39d5c8/cloudevents_pydantic-0.0.3-py3-none-any.whl", hash = "sha256:7d75b9e9f0942174c0843e6322d18974c57532274769ccb80677bf07a9e5d5c9", size = 19997 }, + { url = "https://files.pythonhosted.org/packages/22/b5/5134876d71c3717eacb148f05260dd1f3c25306a230f34666ee53901c690/cloudevents_pydantic-0.1.0-py3-none-any.whl", hash = "sha256:2ccde1e0cd525686b0ef468ac7f407d7b36c851c5be71cf64f47735fbef500e6", size = 22683 }, ] [[package]] @@ -602,14 +602,14 @@ pydantic = [ [[package]] name = "deprecated" -version = "1.2.15" +version = "1.2.18" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/a3/53e7d78a6850ffdd394d7048a31a6f14e44900adedf190f9a165f6b69439/deprecated-1.2.15.tar.gz", hash = "sha256:683e561a90de76239796e6b6feac66b99030d2dd3fcf61ef996330f14bbb9b0d", size = 2977612 } +sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/8f/c7f227eb42cfeaddce3eb0c96c60cbca37797fa7b34f8e1aeadf6c5c0983/Deprecated-1.2.15-py2.py3-none-any.whl", hash = "sha256:353bc4a8ac4bfc96800ddab349d89c25dec1079f65fd53acdcc1e0b975b21320", size = 9941 }, + { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998 }, ] [[package]] @@ -686,29 +686,29 @@ wheels = [ [[package]] name = "faker" -version = "34.0.1" +version = "35.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "python-dateutil" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cb/c4/dfe2fca4eef9e8d18b506e95488cb0c6841f9ef925aa198f8b6e66c04df8/faker-34.0.1.tar.gz", hash = "sha256:4152be8a9e0fe9e4638b1170e30772b33d51eed733b68cdf6bcdb90dcc0c9ca0", size = 1855788 } +sdist = { url = "https://files.pythonhosted.org/packages/d5/18/86fe668976308d09e0178041c3756e646a1f5ddc676aa7fb0cf3cd52f5b9/faker-35.0.0.tar.gz", hash = "sha256:42f2da8cf561e38c72b25e9891168b1e25fec42b6b0b5b0b6cd6041da54af885", size = 1855098 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/c1/4c38d399f7ce09f3bc4586d79b4126a2879f3e9d635555fce40d82384b2c/Faker-34.0.1-py3-none-any.whl", hash = "sha256:1bef5a4037f327185833a6104c6d1f7dda64f720e68a46968768e7cfddb71297", size = 1895553 }, + { url = "https://files.pythonhosted.org/packages/b8/fe/40452fb1730b10afa34dfe016097b28baa070ad74a1c1a3512ebed438c08/Faker-35.0.0-py3-none-any.whl", hash = "sha256:926d2301787220e0554c2e39afc4dc535ce4b0a8d0a089657137999f66334ef4", size = 1894841 }, ] [[package]] name = "fastapi" -version = "0.115.6" +version = "0.115.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/72/d83b98cd106541e8f5e5bfab8ef2974ab45a62e8a6c5b5e6940f26d2ed4b/fastapi-0.115.6.tar.gz", hash = "sha256:9ec46f7addc14ea472958a96aae5b5de65f39721a46aaf5705c480d9a8b76654", size = 301336 } +sdist = { url = "https://files.pythonhosted.org/packages/a2/f5/3f921e59f189e513adb9aef826e2841672d50a399fead4e69afdeb808ff4/fastapi-0.115.7.tar.gz", hash = "sha256:0f106da6c01d88a6786b3248fb4d7a940d071f6f488488898ad5d354b25ed015", size = 293177 } wheels = [ - { url = "https://files.pythonhosted.org/packages/52/b3/7e4df40e585df024fac2f80d1a2d579c854ac37109675db2b0cc22c0bb9e/fastapi-0.115.6-py3-none-any.whl", hash = "sha256:e9240b29e36fa8f4bb7290316988e90c381e5092e0cbe84e7818cc3713bcf305", size = 94843 }, + { url = "https://files.pythonhosted.org/packages/e6/7f/bbd4dcf0faf61bc68a01939256e2ed02d681e9334c1a3cef24d5f77aba9f/fastapi-0.115.7-py3-none-any.whl", hash = "sha256:eb6a8c8bf7f26009e8147111ff15b5177a0e19bb4a45bc3486ab14804539d21e", size = 94777 }, ] [[package]] @@ -788,14 +788,14 @@ wheels = [ [[package]] name = "graphql-core" -version = "3.2.5" +version = "3.2.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/b5/ebc6fe3852e2d2fdaf682dddfc366934f3d2c9ef9b6d1b0e6ca348d936ba/graphql_core-3.2.5.tar.gz", hash = "sha256:e671b90ed653c808715645e3998b7ab67d382d55467b7e2978549111bbabf8d5", size = 504664 } +sdist = { url = "https://files.pythonhosted.org/packages/c4/16/7574029da84834349b60ed71614d66ca3afe46e9bf9c7b9562102acb7d4f/graphql_core-3.2.6.tar.gz", hash = "sha256:c08eec22f9e40f0bd61d805907e3b3b1b9a320bc606e23dc145eebca07c8fbab", size = 505353 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/dc/078bd6b304de790618ebb95e2aedaadb78f4527ac43a9ad8815f006636b6/graphql_core-3.2.5-py3-none-any.whl", hash = "sha256:2f150d5096448aa4f8ab26268567bbfeef823769893b39c1a2e1409590939c8a", size = 203189 }, + { url = "https://files.pythonhosted.org/packages/ae/4f/7297663840621022bc73c22d7d9d80dbc78b4db6297f764b545cd5dd462d/graphql_core-3.2.6-py3-none-any.whl", hash = "sha256:78b016718c161a6fb20a7d97bbf107f331cd1afe53e45566c59f776ed7f0b45f", size = 203416 }, ] [[package]] @@ -861,55 +861,55 @@ wheels = [ [[package]] name = "grpcio" -version = "1.69.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/87/06a145284cbe86c91ca517fe6b57be5efbb733c0d6374b407f0992054d18/grpcio-1.69.0.tar.gz", hash = "sha256:936fa44241b5379c5afc344e1260d467bee495747eaf478de825bab2791da6f5", size = 12738244 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/6e/2f8ee5fb65aef962d0bd7e46b815e7b52820687e29c138eaee207a688abc/grpcio-1.69.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:2060ca95a8db295ae828d0fc1c7f38fb26ccd5edf9aa51a0f44251f5da332e97", size = 5190753 }, - { url = "https://files.pythonhosted.org/packages/89/07/028dcda44d40f9488f0a0de79c5ffc80e2c1bc5ed89da9483932e3ea67cf/grpcio-1.69.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:2e52e107261fd8fa8fa457fe44bfadb904ae869d87c1280bf60f93ecd3e79278", size = 11096752 }, - { url = "https://files.pythonhosted.org/packages/99/a0/c727041b1410605ba38b585b6b52c1a289d7fcd70a41bccbc2c58fc643b2/grpcio-1.69.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:316463c0832d5fcdb5e35ff2826d9aa3f26758d29cdfb59a368c1d6c39615a11", size = 5705442 }, - { url = "https://files.pythonhosted.org/packages/7a/2f/1c53f5d127ff882443b19c757d087da1908f41c58c4b098e8eaf6b2bb70a/grpcio-1.69.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26c9a9c4ac917efab4704b18eed9082ed3b6ad19595f047e8173b5182fec0d5e", size = 6333796 }, - { url = "https://files.pythonhosted.org/packages/cc/f6/2017da2a1b64e896af710253e5bfbb4188605cdc18bce3930dae5cdbf502/grpcio-1.69.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90b3646ced2eae3a0599658eeccc5ba7f303bf51b82514c50715bdd2b109e5ec", size = 5954245 }, - { url = "https://files.pythonhosted.org/packages/c1/65/1395bec928e99ba600464fb01b541e7e4cdd462e6db25259d755ef9f8d02/grpcio-1.69.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3b75aea7c6cb91b341c85e7c1d9db1e09e1dd630b0717f836be94971e015031e", size = 6664854 }, - { url = "https://files.pythonhosted.org/packages/40/57/8b3389cfeb92056c8b44288c9c4ed1d331bcad0215c4eea9ae4629e156d9/grpcio-1.69.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5cfd14175f9db33d4b74d63de87c64bb0ee29ce475ce3c00c01ad2a3dc2a9e51", size = 6226854 }, - { url = "https://files.pythonhosted.org/packages/cc/61/1f2bbeb7c15544dffc98b3f65c093e746019995e6f1e21dc3655eec3dc23/grpcio-1.69.0-cp310-cp310-win32.whl", hash = "sha256:9031069d36cb949205293cf0e243abd5e64d6c93e01b078c37921493a41b72dc", size = 3662734 }, - { url = "https://files.pythonhosted.org/packages/ef/ba/bf1a6d9f5c17d2da849793d72039776c56c98c889c9527f6721b6ee57e6e/grpcio-1.69.0-cp310-cp310-win_amd64.whl", hash = "sha256:cc89b6c29f3dccbe12d7a3b3f1b3999db4882ae076c1c1f6df231d55dbd767a5", size = 4410306 }, - { url = "https://files.pythonhosted.org/packages/8d/cd/ca256aeef64047881586331347cd5a68a4574ba1a236e293cd8eba34e355/grpcio-1.69.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:8de1b192c29b8ce45ee26a700044717bcbbd21c697fa1124d440548964328561", size = 5198734 }, - { url = "https://files.pythonhosted.org/packages/37/3f/10c1e5e0150bf59aa08ea6aebf38f87622f95f7f33f98954b43d1b2a3200/grpcio-1.69.0-cp311-cp311-macosx_10_14_universal2.whl", hash = "sha256:7e76accf38808f5c5c752b0ab3fd919eb14ff8fafb8db520ad1cc12afff74de6", size = 11135285 }, - { url = "https://files.pythonhosted.org/packages/08/61/61cd116a572203a740684fcba3fef37a3524f1cf032b6568e1e639e59db0/grpcio-1.69.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:d5658c3c2660417d82db51e168b277e0ff036d0b0f859fa7576c0ffd2aec1442", size = 5699468 }, - { url = "https://files.pythonhosted.org/packages/01/f1/a841662e8e2465ba171c973b77d18fa7438ced535519b3c53617b7e6e25c/grpcio-1.69.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5494d0e52bf77a2f7eb17c6da662886ca0a731e56c1c85b93505bece8dc6cf4c", size = 6332337 }, - { url = "https://files.pythonhosted.org/packages/62/b1/c30e932e02c2e0bfdb8df46fe3b0c47f518fb04158ebdc0eb96cc97d642f/grpcio-1.69.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ed866f9edb574fd9be71bf64c954ce1b88fc93b2a4cbf94af221e9426eb14d6", size = 5949844 }, - { url = "https://files.pythonhosted.org/packages/5e/cb/55327d43b6286100ffae7d1791be6178d13c917382f3e9f43f82e8b393cf/grpcio-1.69.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c5ba38aeac7a2fe353615c6b4213d1fbb3a3c34f86b4aaa8be08baaaee8cc56d", size = 6661828 }, - { url = "https://files.pythonhosted.org/packages/6f/e4/120d72ae982d51cb9cabcd9672f8a1c6d62011b493a4d049d2abdf564db0/grpcio-1.69.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f79e05f5bbf551c4057c227d1b041ace0e78462ac8128e2ad39ec58a382536d2", size = 6226026 }, - { url = "https://files.pythonhosted.org/packages/96/e8/2cc15f11db506d7b1778f0587fa7bdd781602b05b3c4d75b7ca13de33d62/grpcio-1.69.0-cp311-cp311-win32.whl", hash = "sha256:bf1f8be0da3fcdb2c1e9f374f3c2d043d606d69f425cd685110dd6d0d2d61258", size = 3662653 }, - { url = "https://files.pythonhosted.org/packages/42/78/3c5216829a48237fcb71a077f891328a435e980d9757a9ebc49114d88768/grpcio-1.69.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb9302afc3a0e4ba0b225cd651ef8e478bf0070cf11a529175caecd5ea2474e7", size = 4412824 }, - { url = "https://files.pythonhosted.org/packages/61/1d/8f28f147d7f3f5d6b6082f14e1e0f40d58e50bc2bd30d2377c730c57a286/grpcio-1.69.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:fc18a4de8c33491ad6f70022af5c460b39611e39578a4d84de0fe92f12d5d47b", size = 5161414 }, - { url = "https://files.pythonhosted.org/packages/35/4b/9ab8ea65e515e1844feced1ef9e7a5d8359c48d986c93f3d2a2006fbdb63/grpcio-1.69.0-cp312-cp312-macosx_10_14_universal2.whl", hash = "sha256:0f0270bd9ffbff6961fe1da487bdcd594407ad390cc7960e738725d4807b18c4", size = 11108909 }, - { url = "https://files.pythonhosted.org/packages/99/68/1856fde2b3c3162bdfb9845978608deef3606e6907fdc2c87443fce6ecd0/grpcio-1.69.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:dc48f99cc05e0698e689b51a05933253c69a8c8559a47f605cff83801b03af0e", size = 5658302 }, - { url = "https://files.pythonhosted.org/packages/3e/21/3fa78d38dc5080d0d677103fad3a8cd55091635cc2069a7c06c7a54e6c4d/grpcio-1.69.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e925954b18d41aeb5ae250262116d0970893b38232689c4240024e4333ac084", size = 6306201 }, - { url = "https://files.pythonhosted.org/packages/f3/cb/5c47b82fd1baf43dba973ae399095d51aaf0085ab0439838b4cbb1e87e3c/grpcio-1.69.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87d222569273720366f68a99cb62e6194681eb763ee1d3b1005840678d4884f9", size = 5919649 }, - { url = "https://files.pythonhosted.org/packages/c6/67/59d1a56a0f9508a29ea03e1ce800bdfacc1f32b4f6b15274b2e057bf8758/grpcio-1.69.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b62b0f41e6e01a3e5082000b612064c87c93a49b05f7602fe1b7aa9fd5171a1d", size = 6648974 }, - { url = "https://files.pythonhosted.org/packages/f8/fe/ca70c14d98c6400095f19a0f4df8273d09c2106189751b564b26019f1dbe/grpcio-1.69.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:db6f9fd2578dbe37db4b2994c94a1d9c93552ed77dca80e1657bb8a05b898b55", size = 6215144 }, - { url = "https://files.pythonhosted.org/packages/b3/94/b2b0a9fd487fc8262e20e6dd0ec90d9fa462c82a43b4855285620f6e9d01/grpcio-1.69.0-cp312-cp312-win32.whl", hash = "sha256:b192b81076073ed46f4b4dd612b8897d9a1e39d4eabd822e5da7b38497ed77e1", size = 3644552 }, - { url = "https://files.pythonhosted.org/packages/93/99/81aec9f85412e3255a591ae2ccb799238e074be774e5f741abae08a23418/grpcio-1.69.0-cp312-cp312-win_amd64.whl", hash = "sha256:1227ff7836f7b3a4ab04e5754f1d001fa52a730685d3dc894ed8bc262cc96c01", size = 4399532 }, - { url = "https://files.pythonhosted.org/packages/54/47/3ff4501365f56b7cc16617695dbd4fd838c5e362bc7fa9fee09d592f7d78/grpcio-1.69.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:a78a06911d4081a24a1761d16215a08e9b6d4d29cdbb7e427e6c7e17b06bcc5d", size = 5162928 }, - { url = "https://files.pythonhosted.org/packages/c0/63/437174c5fa951052c9ecc5f373f62af6f3baf25f3f5ef35cbf561806b371/grpcio-1.69.0-cp313-cp313-macosx_10_14_universal2.whl", hash = "sha256:dc5a351927d605b2721cbb46158e431dd49ce66ffbacb03e709dc07a491dde35", size = 11103027 }, - { url = "https://files.pythonhosted.org/packages/53/df/53566a6fdc26b6d1f0585896e1cc4825961039bca5a6a314ff29d79b5d5b/grpcio-1.69.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:3629d8a8185f5139869a6a17865d03113a260e311e78fbe313f1a71603617589", size = 5659277 }, - { url = "https://files.pythonhosted.org/packages/e6/4c/b8a0c4f71498b6f9be5ca6d290d576cf2af9d95fd9827c47364f023969ad/grpcio-1.69.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9a281878feeb9ae26db0622a19add03922a028d4db684658f16d546601a4870", size = 6305255 }, - { url = "https://files.pythonhosted.org/packages/ef/55/d9aa05eb3dfcf6aa946aaf986740ec07fc5189f20e2cbeb8c5d278ffd00f/grpcio-1.69.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cc614e895177ab7e4b70f154d1a7c97e152577ea101d76026d132b7aaba003b", size = 5920240 }, - { url = "https://files.pythonhosted.org/packages/ea/eb/774b27c51e3e386dfe6c491a710f6f87ffdb20d88ec6c3581e047d9354a2/grpcio-1.69.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:1ee76cd7e2e49cf9264f6812d8c9ac1b85dda0eaea063af07292400f9191750e", size = 6652974 }, - { url = "https://files.pythonhosted.org/packages/59/98/96de14e6e7d89123813d58c246d9b0f1fbd24f9277f5295264e60861d9d6/grpcio-1.69.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:0470fa911c503af59ec8bc4c82b371ee4303ececbbdc055f55ce48e38b20fd67", size = 6215757 }, - { url = "https://files.pythonhosted.org/packages/7d/5b/ce922e0785910b10756fabc51fd294260384a44bea41651dadc4e47ddc82/grpcio-1.69.0-cp313-cp313-win32.whl", hash = "sha256:b650f34aceac8b2d08a4c8d7dc3e8a593f4d9e26d86751ebf74ebf5107d927de", size = 3642488 }, - { url = "https://files.pythonhosted.org/packages/5d/04/11329e6ca1ceeb276df2d9c316b5e170835a687a4d0f778dba8294657e36/grpcio-1.69.0-cp313-cp313-win_amd64.whl", hash = "sha256:028337786f11fecb5d7b7fa660475a06aabf7e5e52b5ac2df47414878c0ce7ea", size = 4399968 }, - { url = "https://files.pythonhosted.org/packages/c6/e6/9c6448a9f2b192b4dab8ecba6a99d34aebfb3398da9f407eb8f5a14181d4/grpcio-1.69.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:dd034d68a2905464c49479b0c209c773737a4245d616234c79c975c7c90eca03", size = 5190897 }, - { url = "https://files.pythonhosted.org/packages/4d/ce/fb54596867c813756c70266cb433e37619324c0f18ad917c2bbeeb6b5b21/grpcio-1.69.0-cp39-cp39-macosx_10_14_universal2.whl", hash = "sha256:01f834732c22a130bdf3dc154d1053bdbc887eb3ccb7f3e6285cfbfc33d9d5cc", size = 11124006 }, - { url = "https://files.pythonhosted.org/packages/af/c1/c314372f3b6605b3ef8c03bcecd3deef92a3a5817b26ca4c5a6d519bdf04/grpcio-1.69.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:a7f4ed0dcf202a70fe661329f8874bc3775c14bb3911d020d07c82c766ce0eb1", size = 5703399 }, - { url = "https://files.pythonhosted.org/packages/c6/e4/d4a051b2e3752590e5a8fdfd3270045d8c0e49f0566fd9dacf30e3de1bc3/grpcio-1.69.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd7ea241b10bc5f0bb0f82c0d7896822b7ed122b3ab35c9851b440c1ccf81588", size = 6333585 }, - { url = "https://files.pythonhosted.org/packages/9b/dd/3b0057863f27325ad9371e966684d2e287cdb4ee5861b4cd4fbbb1c7bf91/grpcio-1.69.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f03dc9b4da4c0dc8a1db7a5420f575251d7319b7a839004d8916257ddbe4816", size = 5953919 }, - { url = "https://files.pythonhosted.org/packages/98/8a/5f782d5493e4c67c64389996d800a666987dc27ab5fbe093864e9fd66982/grpcio-1.69.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ca71d73a270dff052fe4edf74fef142d6ddd1f84175d9ac4a14b7280572ac519", size = 6666357 }, - { url = "https://files.pythonhosted.org/packages/de/a4/d1a03913df292ba7322086c68301c66e14b3f8f9532e4c3854846442f0a0/grpcio-1.69.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ccbed100dc43704e94ccff9e07680b540d64e4cc89213ab2832b51b4f68a520", size = 6226574 }, - { url = "https://files.pythonhosted.org/packages/8d/fb/e104bc4296ee4991d803dd39b6c72ed247ba0e18a4e56fd895947aae1249/grpcio-1.69.0-cp39-cp39-win32.whl", hash = "sha256:1514341def9c6ec4b7f0b9628be95f620f9d4b99331b7ef0a1845fd33d9b579c", size = 3663452 }, - { url = "https://files.pythonhosted.org/packages/ad/39/12d48bccd429699a3c909173b395900eb64e4c6bc5eed34d7088e438bc4d/grpcio-1.69.0-cp39-cp39-win_amd64.whl", hash = "sha256:c1fea55d26d647346acb0069b08dca70984101f2dc95066e003019207212e303", size = 4411151 }, +version = "1.70.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/e1/4b21b5017c33f3600dcc32b802bb48fe44a4d36d6c066f52650c7c2690fa/grpcio-1.70.0.tar.gz", hash = "sha256:8d1584a68d5922330025881e63a6c1b54cc8117291d382e4fa69339b6d914c56", size = 12788932 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/e9/f72408bac1f7b05b25e4df569b02d6b200c8e7857193aa9f1df7a3744add/grpcio-1.70.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:95469d1977429f45fe7df441f586521361e235982a0b39e33841549143ae2851", size = 5229736 }, + { url = "https://files.pythonhosted.org/packages/b3/17/e65139ea76dac7bcd8a3f17cbd37e3d1a070c44db3098d0be5e14c5bd6a1/grpcio-1.70.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:ed9718f17fbdb472e33b869c77a16d0b55e166b100ec57b016dc7de9c8d236bf", size = 11432751 }, + { url = "https://files.pythonhosted.org/packages/a0/12/42de6082b4ab14a59d30b2fc7786882fdaa75813a4a4f3d4a8c4acd6ed59/grpcio-1.70.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:374d014f29f9dfdb40510b041792e0e2828a1389281eb590df066e1cc2b404e5", size = 5711439 }, + { url = "https://files.pythonhosted.org/packages/34/f8/b5a19524d273cbd119274a387bb72d6fbb74578e13927a473bc34369f079/grpcio-1.70.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2af68a6f5c8f78d56c145161544ad0febbd7479524a59c16b3e25053f39c87f", size = 6330777 }, + { url = "https://files.pythonhosted.org/packages/1a/67/3d6c0ad786238aac7fa93b79246fc452978fbfe9e5f86f70da8e8a2797d0/grpcio-1.70.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce7df14b2dcd1102a2ec32f621cc9fab6695effef516efbc6b063ad749867295", size = 5944639 }, + { url = "https://files.pythonhosted.org/packages/76/0d/d9f7cbc41c2743cf18236a29b6a582f41bd65572a7144d92b80bc1e68479/grpcio-1.70.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c78b339869f4dbf89881e0b6fbf376313e4f845a42840a7bdf42ee6caed4b11f", size = 6643543 }, + { url = "https://files.pythonhosted.org/packages/fc/24/bdd7e606b3400c14330e33a4698fa3a49e38a28c9e0a831441adbd3380d2/grpcio-1.70.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:58ad9ba575b39edef71f4798fdb5c7b6d02ad36d47949cd381d4392a5c9cbcd3", size = 6199897 }, + { url = "https://files.pythonhosted.org/packages/d1/33/8132eb370087960c82d01b89faeb28f3e58f5619ffe19889f57c58a19c18/grpcio-1.70.0-cp310-cp310-win32.whl", hash = "sha256:2b0d02e4b25a5c1f9b6c7745d4fa06efc9fd6a611af0fb38d3ba956786b95199", size = 3617513 }, + { url = "https://files.pythonhosted.org/packages/99/bc/0fce5cfc0ca969df66f5dca6cf8d2258abb88146bf9ab89d8cf48e970137/grpcio-1.70.0-cp310-cp310-win_amd64.whl", hash = "sha256:0de706c0a5bb9d841e353f6343a9defc9fc35ec61d6eb6111802f3aa9fef29e1", size = 4303342 }, + { url = "https://files.pythonhosted.org/packages/65/c4/1f67d23d6bcadd2fd61fb460e5969c52b3390b4a4e254b5e04a6d1009e5e/grpcio-1.70.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:17325b0be0c068f35770f944124e8839ea3185d6d54862800fc28cc2ffad205a", size = 5229017 }, + { url = "https://files.pythonhosted.org/packages/e4/bd/cc36811c582d663a740fb45edf9f99ddbd99a10b6ba38267dc925e1e193a/grpcio-1.70.0-cp311-cp311-macosx_10_14_universal2.whl", hash = "sha256:dbe41ad140df911e796d4463168e33ef80a24f5d21ef4d1e310553fcd2c4a386", size = 11472027 }, + { url = "https://files.pythonhosted.org/packages/7e/32/8538bb2ace5cd72da7126d1c9804bf80b4fe3be70e53e2d55675c24961a8/grpcio-1.70.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:5ea67c72101d687d44d9c56068328da39c9ccba634cabb336075fae2eab0d04b", size = 5707785 }, + { url = "https://files.pythonhosted.org/packages/ce/5c/a45f85f2a0dfe4a6429dee98717e0e8bd7bd3f604315493c39d9679ca065/grpcio-1.70.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb5277db254ab7586769e490b7b22f4ddab3876c490da0a1a9d7c695ccf0bf77", size = 6331599 }, + { url = "https://files.pythonhosted.org/packages/9f/e5/5316b239380b8b2ad30373eb5bb25d9fd36c0375e94a98a0a60ea357d254/grpcio-1.70.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7831a0fc1beeeb7759f737f5acd9fdcda520e955049512d68fda03d91186eea", size = 5940834 }, + { url = "https://files.pythonhosted.org/packages/05/33/dbf035bc6d167068b4a9f2929dfe0b03fb763f0f861ecb3bb1709a14cb65/grpcio-1.70.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:27cc75e22c5dba1fbaf5a66c778e36ca9b8ce850bf58a9db887754593080d839", size = 6641191 }, + { url = "https://files.pythonhosted.org/packages/4c/c4/684d877517e5bfd6232d79107e5a1151b835e9f99051faef51fed3359ec4/grpcio-1.70.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d63764963412e22f0491d0d32833d71087288f4e24cbcddbae82476bfa1d81fd", size = 6198744 }, + { url = "https://files.pythonhosted.org/packages/e9/43/92fe5eeaf340650a7020cfb037402c7b9209e7a0f3011ea1626402219034/grpcio-1.70.0-cp311-cp311-win32.whl", hash = "sha256:bb491125103c800ec209d84c9b51f1c60ea456038e4734688004f377cfacc113", size = 3617111 }, + { url = "https://files.pythonhosted.org/packages/55/15/b6cf2c9515c028aff9da6984761a3ab484a472b0dc6435fcd07ced42127d/grpcio-1.70.0-cp311-cp311-win_amd64.whl", hash = "sha256:d24035d49e026353eb042bf7b058fb831db3e06d52bee75c5f2f3ab453e71aca", size = 4304604 }, + { url = "https://files.pythonhosted.org/packages/4c/a4/ddbda79dd176211b518f0f3795af78b38727a31ad32bc149d6a7b910a731/grpcio-1.70.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:ef4c14508299b1406c32bdbb9fb7b47612ab979b04cf2b27686ea31882387cff", size = 5198135 }, + { url = "https://files.pythonhosted.org/packages/30/5c/60eb8a063ea4cb8d7670af8fac3f2033230fc4b75f62669d67c66ac4e4b0/grpcio-1.70.0-cp312-cp312-macosx_10_14_universal2.whl", hash = "sha256:aa47688a65643afd8b166928a1da6247d3f46a2784d301e48ca1cc394d2ffb40", size = 11447529 }, + { url = "https://files.pythonhosted.org/packages/fb/b9/1bf8ab66729f13b44e8f42c9de56417d3ee6ab2929591cfee78dce749b57/grpcio-1.70.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:880bfb43b1bb8905701b926274eafce5c70a105bc6b99e25f62e98ad59cb278e", size = 5664484 }, + { url = "https://files.pythonhosted.org/packages/d1/06/2f377d6906289bee066d96e9bdb91e5e96d605d173df9bb9856095cccb57/grpcio-1.70.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e654c4b17d07eab259d392e12b149c3a134ec52b11ecdc6a515b39aceeec898", size = 6303739 }, + { url = "https://files.pythonhosted.org/packages/ae/50/64c94cfc4db8d9ed07da71427a936b5a2bd2b27c66269b42fbda82c7c7a4/grpcio-1.70.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2394e3381071045a706ee2eeb6e08962dd87e8999b90ac15c55f56fa5a8c9597", size = 5910417 }, + { url = "https://files.pythonhosted.org/packages/53/89/8795dfc3db4389c15554eb1765e14cba8b4c88cc80ff828d02f5572965af/grpcio-1.70.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b3c76701428d2df01964bc6479422f20e62fcbc0a37d82ebd58050b86926ef8c", size = 6626797 }, + { url = "https://files.pythonhosted.org/packages/9c/b2/6a97ac91042a2c59d18244c479ee3894e7fb6f8c3a90619bb5a7757fa30c/grpcio-1.70.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ac073fe1c4cd856ebcf49e9ed6240f4f84d7a4e6ee95baa5d66ea05d3dd0df7f", size = 6190055 }, + { url = "https://files.pythonhosted.org/packages/86/2b/28db55c8c4d156053a8c6f4683e559cd0a6636f55a860f87afba1ac49a51/grpcio-1.70.0-cp312-cp312-win32.whl", hash = "sha256:cd24d2d9d380fbbee7a5ac86afe9787813f285e684b0271599f95a51bce33528", size = 3600214 }, + { url = "https://files.pythonhosted.org/packages/17/c3/a7a225645a965029ed432e5b5e9ed959a574e62100afab553eef58be0e37/grpcio-1.70.0-cp312-cp312-win_amd64.whl", hash = "sha256:0495c86a55a04a874c7627fd33e5beaee771917d92c0e6d9d797628ac40e7655", size = 4292538 }, + { url = "https://files.pythonhosted.org/packages/68/38/66d0f32f88feaf7d83f8559cd87d899c970f91b1b8a8819b58226de0a496/grpcio-1.70.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:aa573896aeb7d7ce10b1fa425ba263e8dddd83d71530d1322fd3a16f31257b4a", size = 5199218 }, + { url = "https://files.pythonhosted.org/packages/c1/96/947df763a0b18efb5cc6c2ae348e56d97ca520dc5300c01617b234410173/grpcio-1.70.0-cp313-cp313-macosx_10_14_universal2.whl", hash = "sha256:d405b005018fd516c9ac529f4b4122342f60ec1cee181788249372524e6db429", size = 11445983 }, + { url = "https://files.pythonhosted.org/packages/fd/5b/f3d4b063e51b2454bedb828e41f3485800889a3609c49e60f2296cc8b8e5/grpcio-1.70.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:f32090238b720eb585248654db8e3afc87b48d26ac423c8dde8334a232ff53c9", size = 5663954 }, + { url = "https://files.pythonhosted.org/packages/bd/0b/dab54365fcedf63e9f358c1431885478e77d6f190d65668936b12dd38057/grpcio-1.70.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfa089a734f24ee5f6880c83d043e4f46bf812fcea5181dcb3a572db1e79e01c", size = 6304323 }, + { url = "https://files.pythonhosted.org/packages/76/a8/8f965a7171ddd336ce32946e22954aa1bbc6f23f095e15dadaa70604ba20/grpcio-1.70.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f19375f0300b96c0117aca118d400e76fede6db6e91f3c34b7b035822e06c35f", size = 5910939 }, + { url = "https://files.pythonhosted.org/packages/1b/05/0bbf68be8b17d1ed6f178435a3c0c12e665a1e6054470a64ce3cb7896596/grpcio-1.70.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:7c73c42102e4a5ec76608d9b60227d917cea46dff4d11d372f64cbeb56d259d0", size = 6631405 }, + { url = "https://files.pythonhosted.org/packages/79/6a/5df64b6df405a1ed1482cb6c10044b06ec47fd28e87c2232dbcf435ecb33/grpcio-1.70.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:0a5c78d5198a1f0aa60006cd6eb1c912b4a1520b6a3968e677dbcba215fabb40", size = 6190982 }, + { url = "https://files.pythonhosted.org/packages/42/aa/aeaac87737e6d25d1048c53b8ec408c056d3ed0c922e7c5efad65384250c/grpcio-1.70.0-cp313-cp313-win32.whl", hash = "sha256:fe9dbd916df3b60e865258a8c72ac98f3ac9e2a9542dcb72b7a34d236242a5ce", size = 3598359 }, + { url = "https://files.pythonhosted.org/packages/1f/79/8edd2442d2de1431b4a3de84ef91c37002f12de0f9b577fb07b452989dbc/grpcio-1.70.0-cp313-cp313-win_amd64.whl", hash = "sha256:4119fed8abb7ff6c32e3d2255301e59c316c22d31ab812b3fbcbaf3d0d87cc68", size = 4293938 }, + { url = "https://files.pythonhosted.org/packages/9d/0e/64061c9746a2dd6e07cb0a0f3829f0a431344add77ec36397cc452541ff6/grpcio-1.70.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:4f1937f47c77392ccd555728f564a49128b6a197a05a5cd527b796d36f3387d0", size = 5231123 }, + { url = "https://files.pythonhosted.org/packages/72/9f/c93501d5f361aecee0146ab19300d5acb1c2747b00217c641f06fffbcd62/grpcio-1.70.0-cp39-cp39-macosx_10_14_universal2.whl", hash = "sha256:0cd430b9215a15c10b0e7d78f51e8a39d6cf2ea819fd635a7214fae600b1da27", size = 11467217 }, + { url = "https://files.pythonhosted.org/packages/0a/1a/980d115b701023450a304881bf3f6309f6fb15787f9b78d2728074f3bf86/grpcio-1.70.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:e27585831aa6b57b9250abaf147003e126cd3a6c6ca0c531a01996f31709bed1", size = 5710913 }, + { url = "https://files.pythonhosted.org/packages/a0/84/af420067029808f9790e98143b3dd0f943bebba434a4706755051a520c91/grpcio-1.70.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1af8e15b0f0fe0eac75195992a63df17579553b0c4af9f8362cc7cc99ccddf4", size = 6330947 }, + { url = "https://files.pythonhosted.org/packages/24/1c/e1f06a7d29a1fa5053dcaf5352a50f8e1f04855fd194a65422a9d685d375/grpcio-1.70.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbce24409beaee911c574a3d75d12ffb8c3e3dd1b813321b1d7a96bbcac46bf4", size = 5943913 }, + { url = "https://files.pythonhosted.org/packages/41/8f/de13838e4467519a50cd0693e98b0b2bcc81d656013c38a1dd7dcb801526/grpcio-1.70.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ff4a8112a79464919bb21c18e956c54add43ec9a4850e3949da54f61c241a4a6", size = 6643236 }, + { url = "https://files.pythonhosted.org/packages/ac/73/d68c745d34e43a80440da4f3d79fa02c56cb118c2a26ba949f3cfd8316d7/grpcio-1.70.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5413549fdf0b14046c545e19cfc4eb1e37e9e1ebba0ca390a8d4e9963cab44d2", size = 6199038 }, + { url = "https://files.pythonhosted.org/packages/7e/dd/991f100b8c31636b4bb2a941dbbf54dbcc55d69c722cfa038c3d017eaa0c/grpcio-1.70.0-cp39-cp39-win32.whl", hash = "sha256:b745d2c41b27650095e81dea7091668c040457483c9bdb5d0d9de8f8eb25e59f", size = 3617512 }, + { url = "https://files.pythonhosted.org/packages/4d/80/1aa2ba791207a13e314067209b48e1a0893ed8d1f43ef012e194aaa6c2de/grpcio-1.70.0-cp39-cp39-win_amd64.whl", hash = "sha256:a31d7e3b529c94e930a117b2175b2efd179d96eb3c7a21ccb0289a8ab05b645c", size = 4303506 }, ] [[package]] @@ -1922,16 +1922,16 @@ wheels = [ [[package]] name = "pydantic" -version = "2.10.5" +version = "2.10.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, { name = "pydantic-core" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6a/c7/ca334c2ef6f2e046b1144fe4bb2a5da8a4c574e7f2ebf7e16b34a6a2fa92/pydantic-2.10.5.tar.gz", hash = "sha256:278b38dbbaec562011d659ee05f63346951b3a248a6f3642e1bc68894ea2b4ff", size = 761287 } +sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/26/82663c79010b28eddf29dcdd0ea723439535fa917fce5905885c0e9ba562/pydantic-2.10.5-py3-none-any.whl", hash = "sha256:4dd4e322dbe55472cb7ca7e73f4b63574eecccf2835ffa2af9021ce113c83c53", size = 431426 }, + { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, ] [[package]] @@ -2366,27 +2366,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.9.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/80/63/77ecca9d21177600f551d1c58ab0e5a0b260940ea7312195bd2a4798f8a8/ruff-0.9.2.tar.gz", hash = "sha256:b5eceb334d55fae5f316f783437392642ae18e16dcf4f1858d55d3c2a0f8f5d0", size = 3553799 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/af/b9/0e168e4e7fb3af851f739e8f07889b91d1a33a30fca8c29fa3149d6b03ec/ruff-0.9.2-py3-none-linux_armv6l.whl", hash = "sha256:80605a039ba1454d002b32139e4970becf84b5fee3a3c3bf1c2af6f61a784347", size = 11652408 }, - { url = "https://files.pythonhosted.org/packages/2c/22/08ede5db17cf701372a461d1cb8fdde037da1d4fa622b69ac21960e6237e/ruff-0.9.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b9aab82bb20afd5f596527045c01e6ae25a718ff1784cb92947bff1f83068b00", size = 11587553 }, - { url = "https://files.pythonhosted.org/packages/42/05/dedfc70f0bf010230229e33dec6e7b2235b2a1b8cbb2a991c710743e343f/ruff-0.9.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fbd337bac1cfa96be615f6efcd4bc4d077edbc127ef30e2b8ba2a27e18c054d4", size = 11020755 }, - { url = "https://files.pythonhosted.org/packages/df/9b/65d87ad9b2e3def67342830bd1af98803af731243da1255537ddb8f22209/ruff-0.9.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b35259b0cbf8daa22a498018e300b9bb0174c2bbb7bcba593935158a78054d", size = 11826502 }, - { url = "https://files.pythonhosted.org/packages/93/02/f2239f56786479e1a89c3da9bc9391120057fc6f4a8266a5b091314e72ce/ruff-0.9.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b6a9701d1e371bf41dca22015c3f89769da7576884d2add7317ec1ec8cb9c3c", size = 11390562 }, - { url = "https://files.pythonhosted.org/packages/c9/37/d3a854dba9931f8cb1b2a19509bfe59e00875f48ade632e95aefcb7a0aee/ruff-0.9.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9cc53e68b3c5ae41e8faf83a3b89f4a5d7b2cb666dff4b366bb86ed2a85b481f", size = 12548968 }, - { url = "https://files.pythonhosted.org/packages/fa/c3/c7b812bb256c7a1d5553433e95980934ffa85396d332401f6b391d3c4569/ruff-0.9.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8efd9da7a1ee314b910da155ca7e8953094a7c10d0c0a39bfde3fcfd2a015684", size = 13187155 }, - { url = "https://files.pythonhosted.org/packages/bd/5a/3c7f9696a7875522b66aa9bba9e326e4e5894b4366bd1dc32aa6791cb1ff/ruff-0.9.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3292c5a22ea9a5f9a185e2d131dc7f98f8534a32fb6d2ee7b9944569239c648d", size = 12704674 }, - { url = "https://files.pythonhosted.org/packages/be/d6/d908762257a96ce5912187ae9ae86792e677ca4f3dc973b71e7508ff6282/ruff-0.9.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a605fdcf6e8b2d39f9436d343d1f0ff70c365a1e681546de0104bef81ce88df", size = 14529328 }, - { url = "https://files.pythonhosted.org/packages/2d/c2/049f1e6755d12d9cd8823242fa105968f34ee4c669d04cac8cea51a50407/ruff-0.9.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c547f7f256aa366834829a08375c297fa63386cbe5f1459efaf174086b564247", size = 12385955 }, - { url = "https://files.pythonhosted.org/packages/91/5a/a9bdb50e39810bd9627074e42743b00e6dc4009d42ae9f9351bc3dbc28e7/ruff-0.9.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d18bba3d3353ed916e882521bc3e0af403949dbada344c20c16ea78f47af965e", size = 11810149 }, - { url = "https://files.pythonhosted.org/packages/e5/fd/57df1a0543182f79a1236e82a79c68ce210efb00e97c30657d5bdb12b478/ruff-0.9.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b338edc4610142355ccf6b87bd356729b62bf1bc152a2fad5b0c7dc04af77bfe", size = 11479141 }, - { url = "https://files.pythonhosted.org/packages/dc/16/bc3fd1d38974f6775fc152a0554f8c210ff80f2764b43777163c3c45d61b/ruff-0.9.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:492a5e44ad9b22a0ea98cf72e40305cbdaf27fac0d927f8bc9e1df316dcc96eb", size = 12014073 }, - { url = "https://files.pythonhosted.org/packages/47/6b/e4ca048a8f2047eb652e1e8c755f384d1b7944f69ed69066a37acd4118b0/ruff-0.9.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:af1e9e9fe7b1f767264d26b1075ac4ad831c7db976911fa362d09b2d0356426a", size = 12435758 }, - { url = "https://files.pythonhosted.org/packages/c2/40/4d3d6c979c67ba24cf183d29f706051a53c36d78358036a9cd21421582ab/ruff-0.9.2-py3-none-win32.whl", hash = "sha256:71cbe22e178c5da20e1514e1e01029c73dc09288a8028a5d3446e6bba87a5145", size = 9796916 }, - { url = "https://files.pythonhosted.org/packages/c3/ef/7f548752bdb6867e6939489c87fe4da489ab36191525fadc5cede2a6e8e2/ruff-0.9.2-py3-none-win_amd64.whl", hash = "sha256:c5e1d6abc798419cf46eed03f54f2e0c3adb1ad4b801119dedf23fcaf69b55b5", size = 10773080 }, - { url = "https://files.pythonhosted.org/packages/0e/4e/33df635528292bd2d18404e4daabcd74ca8a9853b2e1df85ed3d32d24362/ruff-0.9.2-py3-none-win_arm64.whl", hash = "sha256:a1b63fa24149918f8b37cef2ee6fff81f24f0d74b6f0bdc37bc3e1f2143e41c6", size = 10001738 }, +version = "0.9.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/7f/60fda2eec81f23f8aa7cbbfdf6ec2ca11eb11c273827933fb2541c2ce9d8/ruff-0.9.3.tar.gz", hash = "sha256:8293f89985a090ebc3ed1064df31f3b4b56320cdfcec8b60d3295bddb955c22a", size = 3586740 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/77/4fb790596d5d52c87fd55b7160c557c400e90f6116a56d82d76e95d9374a/ruff-0.9.3-py3-none-linux_armv6l.whl", hash = "sha256:7f39b879064c7d9670197d91124a75d118d00b0990586549949aae80cdc16624", size = 11656815 }, + { url = "https://files.pythonhosted.org/packages/a2/a8/3338ecb97573eafe74505f28431df3842c1933c5f8eae615427c1de32858/ruff-0.9.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a187171e7c09efa4b4cc30ee5d0d55a8d6c5311b3e1b74ac5cb96cc89bafc43c", size = 11594821 }, + { url = "https://files.pythonhosted.org/packages/8e/89/320223c3421962762531a6b2dd58579b858ca9916fb2674874df5e97d628/ruff-0.9.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c59ab92f8e92d6725b7ded9d4a31be3ef42688a115c6d3da9457a5bda140e2b4", size = 11040475 }, + { url = "https://files.pythonhosted.org/packages/b2/bd/1d775eac5e51409535804a3a888a9623e87a8f4b53e2491580858a083692/ruff-0.9.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dc153c25e715be41bb228bc651c1e9b1a88d5c6e5ed0194fa0dfea02b026439", size = 11856207 }, + { url = "https://files.pythonhosted.org/packages/7f/c6/3e14e09be29587393d188454064a4aa85174910d16644051a80444e4fd88/ruff-0.9.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:646909a1e25e0dc28fbc529eab8eb7bb583079628e8cbe738192853dbbe43af5", size = 11420460 }, + { url = "https://files.pythonhosted.org/packages/ef/42/b7ca38ffd568ae9b128a2fa76353e9a9a3c80ef19746408d4ce99217ecc1/ruff-0.9.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a5a46e09355695fbdbb30ed9889d6cf1c61b77b700a9fafc21b41f097bfbba4", size = 12605472 }, + { url = "https://files.pythonhosted.org/packages/a6/a1/3167023f23e3530fde899497ccfe239e4523854cb874458ac082992d206c/ruff-0.9.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c4bb09d2bbb394e3730d0918c00276e79b2de70ec2a5231cd4ebb51a57df9ba1", size = 13243123 }, + { url = "https://files.pythonhosted.org/packages/d0/b4/3c600758e320f5bf7de16858502e849f4216cb0151f819fa0d1154874802/ruff-0.9.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:96a87ec31dc1044d8c2da2ebbed1c456d9b561e7d087734336518181b26b3aa5", size = 12744650 }, + { url = "https://files.pythonhosted.org/packages/be/38/266fbcbb3d0088862c9bafa8b1b99486691d2945a90b9a7316336a0d9a1b/ruff-0.9.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb7554aca6f842645022fe2d301c264e6925baa708b392867b7a62645304df4", size = 14458585 }, + { url = "https://files.pythonhosted.org/packages/63/a6/47fd0e96990ee9b7a4abda62de26d291bd3f7647218d05b7d6d38af47c30/ruff-0.9.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cabc332b7075a914ecea912cd1f3d4370489c8018f2c945a30bcc934e3bc06a6", size = 12419624 }, + { url = "https://files.pythonhosted.org/packages/84/5d/de0b7652e09f7dda49e1a3825a164a65f4998175b6486603c7601279baad/ruff-0.9.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:33866c3cc2a575cbd546f2cd02bdd466fed65118e4365ee538a3deffd6fcb730", size = 11843238 }, + { url = "https://files.pythonhosted.org/packages/9e/be/3f341ceb1c62b565ec1fb6fd2139cc40b60ae6eff4b6fb8f94b1bb37c7a9/ruff-0.9.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:006e5de2621304c8810bcd2ee101587712fa93b4f955ed0985907a36c427e0c2", size = 11484012 }, + { url = "https://files.pythonhosted.org/packages/a3/c8/ff8acbd33addc7e797e702cf00bfde352ab469723720c5607b964491d5cf/ruff-0.9.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ba6eea4459dbd6b1be4e6bfc766079fb9b8dd2e5a35aff6baee4d9b1514ea519", size = 12038494 }, + { url = "https://files.pythonhosted.org/packages/73/b1/8d9a2c0efbbabe848b55f877bc10c5001a37ab10aca13c711431673414e5/ruff-0.9.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:90230a6b8055ad47d3325e9ee8f8a9ae7e273078a66401ac66df68943ced029b", size = 12473639 }, + { url = "https://files.pythonhosted.org/packages/cb/44/a673647105b1ba6da9824a928634fe23186ab19f9d526d7bdf278cd27bc3/ruff-0.9.3-py3-none-win32.whl", hash = "sha256:eabe5eb2c19a42f4808c03b82bd313fc84d4e395133fb3fc1b1516170a31213c", size = 9834353 }, + { url = "https://files.pythonhosted.org/packages/c3/01/65cadb59bf8d4fbe33d1a750103e6883d9ef302f60c28b73b773092fbde5/ruff-0.9.3-py3-none-win_amd64.whl", hash = "sha256:040ceb7f20791dfa0e78b4230ee9dce23da3b64dd5848e40e3bf3ab76468dcf4", size = 10821444 }, + { url = "https://files.pythonhosted.org/packages/69/cb/b3fe58a136a27d981911cba2f18e4b29f15010623b79f0f2510fd0d31fd3/ruff-0.9.3-py3-none-win_arm64.whl", hash = "sha256:800d773f6d4d33b0a3c60e2c6ae8f4c202ea2de056365acfa519aa48acf28e0b", size = 10038168 }, ] [[package]] @@ -2510,15 +2510,15 @@ wheels = [ [[package]] name = "starlette" -version = "0.41.3" +version = "0.45.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1a/4c/9b5764bd22eec91c4039ef4c55334e9187085da2d8a2df7bd570869aae18/starlette-0.41.3.tar.gz", hash = "sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835", size = 2574159 } +sdist = { url = "https://files.pythonhosted.org/packages/ff/fb/2984a686808b89a6781526129a4b51266f678b2d2b97ab2d325e56116df8/starlette-0.45.3.tar.gz", hash = "sha256:2cbcba2a75806f8a41c722141486f37c28e30a0921c5f6fe4346cb0dcee1302f", size = 2574076 } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/00/2b325970b3060c7cecebab6d295afe763365822b1306a12eeab198f74323/starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7", size = 73225 }, + { url = "https://files.pythonhosted.org/packages/d9/61/f2b52e107b1fc8944b33ef56bf6ac4ebbe16d91b94d2b87ce013bf63fb84/starlette-0.45.3-py3-none-any.whl", hash = "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d", size = 71507 }, ] [[package]] From 83091777bfe6b583b616f3597e0e9437145eed60 Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Mon, 27 Jan 2025 20:40:37 +0000 Subject: [PATCH 10/15] Improve README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5dadb4e1..e3c29f5c 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,10 @@ This template provides out of the box some commonly used functionalities: * Async tasks execution using [Dramatiq](https://dramatiq.io/index.html) * Repository pattern for databases using [SQLAlchemy](https://www.sqlalchemy.org/) and [SQLAlchemy bind manager](https://febus982.github.io/sqlalchemy-bind-manager/stable/) * Database migrations using [Alembic](https://alembic.sqlalchemy.org/en/latest/) (configured supporting both sync and async SQLAlchemy engines) +* Database fixtures support using customized [Alembic](https://alembic.sqlalchemy.org/en/latest/) configuration * Authentication and Identity Provider using [ORY Zero Trust architecture](https://www.ory.sh/docs/kratos/guides/zero-trust-iap-proxy-identity-access-proxy) * Example CI/CD deployment pipeline for GitLab (The focus for this repository is still GitHub but, in case you want to use GitLab 🤷) -* [TODO] Producer and consumer to emit and consume events using [CloudEvents](https://cloudevents.io/) format on [Confluent Kafka](https://docs.confluent.io/kafka-clients/python/current/overview.html) +* [TODO] Producer and consumer to emit and consume events using [CloudEvents](https://cloudevents.io/) format using HTTP, to be used with [Knative Eventing](https://knative.dev/docs/eventing/) ## Documentation From 45b6af11c001197e4def0d5486fdfc2317e66f34 Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Mon, 27 Jan 2025 20:47:15 +0000 Subject: [PATCH 11/15] Rename `alembic` package to `migrations` to avoid naming collisions with `alembic` package --- .codeclimate.yml | 2 +- Dockerfile | 2 +- alembic.ini | 2 +- docs/architecture.md | 6 +++--- docs/packages/alembic.md | 1 + pyproject.toml | 6 +++--- src/alembic.ini | 2 +- src/{alembic => migrations}/__init__.py | 0 src/{alembic => migrations}/env.py | 0 src/{alembic => migrations}/fixtures/__init__.py | 0 src/{alembic => migrations}/fixtures/books_example.py | 0 src/{alembic => migrations}/script.py.mako | 0 ...5-01-26-212326-52b1246eda46_initialize_fixture_tables.py | 0 .../2025-01-26-212826-bd73bd8a2ac4_create_books_table.py | 0 14 files changed, 11 insertions(+), 10 deletions(-) rename src/{alembic => migrations}/__init__.py (100%) rename src/{alembic => migrations}/env.py (100%) rename src/{alembic => migrations}/fixtures/__init__.py (100%) rename src/{alembic => migrations}/fixtures/books_example.py (100%) rename src/{alembic => migrations}/script.py.mako (100%) rename src/{alembic => migrations}/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py (100%) rename src/{alembic => migrations}/versions/2025-01-26-212826-bd73bd8a2ac4_create_books_table.py (100%) diff --git a/.codeclimate.yml b/.codeclimate.yml index 00a00436..7b4c91ae 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -6,7 +6,7 @@ exclude_patterns: - "spec/" - "!spec/support/helpers" - "config/" - - "src/alembic/" + - "src/migrations/" - "db/" - "dist/" - "features/" diff --git a/Dockerfile b/Dockerfile index 0499e81e..07bb43f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -64,7 +64,7 @@ RUN --mount=type=cache,target=~/.cache/uv \ # Create the base app with the common python packages FROM base AS base_app USER nonroot -COPY --chown=nonroot:nonroot src/alembic ./alembic +COPY --chown=nonroot:nonroot src/migrations ./migrations COPY --chown=nonroot:nonroot src/domains ./domains COPY --chown=nonroot:nonroot src/gateways ./gateways COPY --chown=nonroot:nonroot src/common ./common diff --git a/alembic.ini b/alembic.ini index af119d94..b85e09a4 100644 --- a/alembic.ini +++ b/alembic.ini @@ -2,7 +2,7 @@ [alembic] # path to migration scripts -script_location = src/alembic +script_location = src/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 diff --git a/docs/architecture.md b/docs/architecture.md index d66bc9ac..3427b56b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -10,7 +10,7 @@ and the persistence layer. This is a high level list of the packages in this application template: -* `alembic` (database migration manager) +* `migrations` (database migration manager) * `dramatiq_worker` (async tasks runner) * `common` (some common boilerplate initialisation shared by all applications ) * `http_app` (http presentation layer) @@ -30,7 +30,7 @@ This is a high level representation of the nested layers in the application: ```mermaid flowchart TD subgraph "Framework & Drivers + Interface Adapters" - alembic + migrations dramatiq_worker http_app gateways @@ -51,7 +51,7 @@ flowchart TD end end - alembic ~~~ domains.books + migrations ~~~ domains.books dramatiq_worker ~~~ domains.books http_app ~~~ domains.books gateways ~~~ domains.books diff --git a/docs/packages/alembic.md b/docs/packages/alembic.md index 0d901e17..643733ea 100644 --- a/docs/packages/alembic.md +++ b/docs/packages/alembic.md @@ -4,6 +4,7 @@ we implement some extra features on top of the default configuration: * Support for both sync and async SQLAlchemy engines at the same time +* Support for fixtures management * Grabs the database information from the `SQLAlchemyBindManager` configuration in the application, so we won't have duplicate configuration. * `alembic.ini` (not technically part of the python package) is setup to diff --git a/pyproject.toml b/pyproject.toml index d584d7f2..9b40455b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,7 @@ includes = ["src/**/*.py"] branch = true source = ["src"] omit = [ - "src/alembic/*", + "src/migrations/*", "src/common/config.py", "src/common/logs/*", "src/dramatiq_worker/__init__.py", @@ -94,7 +94,7 @@ exclude_also = [ [tool.mypy] files = ["src", "tests"] -exclude = ["alembic"] +exclude = ["migrations"] # Pydantic plugin causes some issues: https://github.com/pydantic/pydantic-settings/issues/403 #plugins = "pydantic.mypy,strawberry.ext.mypy_plugin" plugins = "strawberry.ext.mypy_plugin" @@ -151,4 +151,4 @@ ignore = [ [tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401"] # Ignore unused imports on init files "tests/**/*.py" = ["S101"] # Allow assert usage on tests -"src/alembic/env.py" = ["E501"] # Allow long lines +"src/migrations/env.py" = ["E501"] # Allow long lines diff --git a/src/alembic.ini b/src/alembic.ini index fe8241fc..223f247d 100644 --- a/src/alembic.ini +++ b/src/alembic.ini @@ -1,7 +1,7 @@ # Copy of alembic.ini used to run migrations inside the container [alembic] -script_location = alembic +script_location = migrations prepend_sys_path = . file_template = %%(year)d-%%(month).2d-%%(day).2d-%%(hour).2d%%(minute).2d%%(second).2d-%%(rev)s_%%(slug)s version_path_separator = os # Use os.pathsep. Default configuration used for new projects. diff --git a/src/alembic/__init__.py b/src/migrations/__init__.py similarity index 100% rename from src/alembic/__init__.py rename to src/migrations/__init__.py diff --git a/src/alembic/env.py b/src/migrations/env.py similarity index 100% rename from src/alembic/env.py rename to src/migrations/env.py diff --git a/src/alembic/fixtures/__init__.py b/src/migrations/fixtures/__init__.py similarity index 100% rename from src/alembic/fixtures/__init__.py rename to src/migrations/fixtures/__init__.py diff --git a/src/alembic/fixtures/books_example.py b/src/migrations/fixtures/books_example.py similarity index 100% rename from src/alembic/fixtures/books_example.py rename to src/migrations/fixtures/books_example.py diff --git a/src/alembic/script.py.mako b/src/migrations/script.py.mako similarity index 100% rename from src/alembic/script.py.mako rename to src/migrations/script.py.mako diff --git a/src/alembic/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py b/src/migrations/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py similarity index 100% rename from src/alembic/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py rename to src/migrations/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py diff --git a/src/alembic/versions/2025-01-26-212826-bd73bd8a2ac4_create_books_table.py b/src/migrations/versions/2025-01-26-212826-bd73bd8a2ac4_create_books_table.py similarity index 100% rename from src/alembic/versions/2025-01-26-212826-bd73bd8a2ac4_create_books_table.py rename to src/migrations/versions/2025-01-26-212826-bd73bd8a2ac4_create_books_table.py From 88216a4ac3bf2986c2e8995994401d021e669488 Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Mon, 27 Jan 2025 20:49:34 +0000 Subject: [PATCH 12/15] Code style --- src/migrations/env.py | 2 +- .../2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py | 1 - .../2025-01-26-212826-bd73bd8a2ac4_create_books_table.py | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/migrations/env.py b/src/migrations/env.py index 5dbbee7d..3ea15dcc 100644 --- a/src/migrations/env.py +++ b/src/migrations/env.py @@ -9,11 +9,11 @@ from types import ModuleType from typing import List, Union +from alembic import context from sqlalchemy import DateTime, String from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker from sqlalchemy.orm import Mapped, Session, mapped_column, sessionmaker -from alembic import context from common.bootstrap import application_init from common.config import AppConfig diff --git a/src/migrations/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py b/src/migrations/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py index 0f08f61f..3af24087 100644 --- a/src/migrations/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py +++ b/src/migrations/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py @@ -7,7 +7,6 @@ """ import sqlalchemy as sa - from alembic import op # revision identifiers, used by Alembic. diff --git a/src/migrations/versions/2025-01-26-212826-bd73bd8a2ac4_create_books_table.py b/src/migrations/versions/2025-01-26-212826-bd73bd8a2ac4_create_books_table.py index b0f60cc4..d60e6bc6 100644 --- a/src/migrations/versions/2025-01-26-212826-bd73bd8a2ac4_create_books_table.py +++ b/src/migrations/versions/2025-01-26-212826-bd73bd8a2ac4_create_books_table.py @@ -7,7 +7,6 @@ """ import sqlalchemy as sa - from alembic import op # revision identifiers, used by Alembic. From b310c55566e472788bc4de5da0eeedce5fa46f73 Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Mon, 27 Jan 2025 22:15:05 +0000 Subject: [PATCH 13/15] Rename filename to module_name --- src/migrations/env.py | 29 ++++++++++--------- ...-52b1246eda46_initialize_fixture_tables.py | 4 +-- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/migrations/env.py b/src/migrations/env.py index 3ea15dcc..a2e56588 100644 --- a/src/migrations/env.py +++ b/src/migrations/env.py @@ -48,8 +48,9 @@ class FixtureMigration(declarative_base): __tablename__ = "alembic_fixtures" bind: Mapped[str] = mapped_column(String(), primary_key=True) - filename: Mapped[str] = mapped_column(String(), primary_key=True) + module_name: Mapped[str] = mapped_column(String(), primary_key=True) signature: Mapped[str] = mapped_column(String(), nullable=False) + processed_at: Mapped[datetime] = mapped_column( DateTime(), nullable=False, default=datetime.now ) @@ -162,13 +163,13 @@ def _fixture_already_migrated(cls, fixture_migration, signature) -> bool: if fixture_migration: if signature != fixture_migration.signature: cls.logger.warning( - f"Signature mismatch for `{fixture_migration.filename}` fixture." + f"Signature mismatch for `{fixture_migration.module_name}` fixture." f" The file has been already processed but has been modified" f" since then. It will not be processed again." ) else: cls.logger.debug( - f"`{fixture_migration.filename}` fixtures already processed for `{fixture_migration.bind}` bind" + f"`{fixture_migration.module_name}` fixtures already processed for `{fixture_migration.bind}` bind" ) return True return False @@ -207,7 +208,7 @@ def _add_fixture_data_to_session( session.add( fixture_migration_models[bind_name]( bind=bind_name, - filename=f"{fixture_module.__name__}", + module_name=f"{fixture_module.__name__}", signature=signature, ) ) @@ -395,20 +396,11 @@ async def run_migrations_online() -> None: for name, rec in engines.items(): logger.info(f"Migrating database {name}") if isinstance(rec["engine"], AsyncEngine): - def migration_callable(*args, **kwargs): return do_run_migration(*args, name=name, **kwargs) - await rec["connection"].run_sync(migration_callable) - await FixtureHandler.a_migrate_fixtures( - bind_name=name, session=async_sessionmaker(bind=rec["connection"]) - ) - else: do_run_migration(rec["connection"], name) - FixtureHandler.migrate_fixtures( - bind_name=name, session=sessionmaker(bind=rec["connection"]) - ) if USE_TWOPHASE: for rec in engines.values(): @@ -422,6 +414,17 @@ def migration_callable(*args, **kwargs): await rec["transaction"].commit() else: rec["transaction"].commit() + + if context.config.cmd_opts.cmd[0].__name__ == "upgrade": + for name, rec in engines.items(): + if isinstance(rec["engine"], AsyncEngine): + await FixtureHandler.a_migrate_fixtures( + bind_name=name, session=async_sessionmaker(bind=rec["connection"]) + ) + else: + FixtureHandler.migrate_fixtures( + bind_name=name, session=sessionmaker(bind=rec["connection"]) + ) except: for rec in engines.values(): if isinstance(rec["engine"], AsyncEngine): diff --git a/src/migrations/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py b/src/migrations/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py index 3af24087..de41d633 100644 --- a/src/migrations/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py +++ b/src/migrations/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py @@ -28,10 +28,10 @@ def upgrade_default() -> None: op.create_table( "alembic_fixtures", sa.Column("bind", sa.String(), nullable=False), - sa.Column("filename", sa.String(), nullable=False), + sa.Column("module_name", sa.String(), nullable=False), sa.Column("signature", sa.String(), nullable=False), sa.Column("processed_at", sa.DateTime(timezone=True), nullable=False), - sa.PrimaryKeyConstraint("bind", "filename"), + sa.PrimaryKeyConstraint("bind", "module_name"), ) From ff5700738e8483432fe0ff6e28c858d727fff40d Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Mon, 27 Jan 2025 22:28:23 +0000 Subject: [PATCH 14/15] Add `alembic_revision` column to fixture tables This update introduces a new `alembic_revision` column to fixture tables for tracking the Alembic migration state. Additionally, error logging in fixture creation now includes exception details, and rollback is moved to ensure consistency in failure scenarios. --- src/migrations/env.py | 7 ++++--- ...-01-26-212326-52b1246eda46_initialize_fixture_tables.py | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/migrations/env.py b/src/migrations/env.py index a2e56588..983f34c6 100644 --- a/src/migrations/env.py +++ b/src/migrations/env.py @@ -50,6 +50,7 @@ class FixtureMigration(declarative_base): bind: Mapped[str] = mapped_column(String(), primary_key=True) module_name: Mapped[str] = mapped_column(String(), primary_key=True) signature: Mapped[str] = mapped_column(String(), nullable=False) + alembic_revision: Mapped[str] = mapped_column(String(), nullable=True, default=str(context.get_head_revision())) processed_at: Mapped[datetime] = mapped_column( DateTime(), nullable=False, default=datetime.now @@ -259,11 +260,11 @@ async def a_migrate_fixtures( cls.logger.info( f"`{fixture_module.__name__}` fixtures correctly created for `{bind_name}` bind" ) - except Exception: - await session.rollback() + except Exception as e: cls.logger.error( - f"`{fixture_module.__name__}` fixtures failed to apply to `{bind_name}` bind" + f"`{fixture_module.__name__}` fixtures failed to apply to `{bind_name}` bind", exc_info=True ) + await session.rollback() @classmethod def migrate_fixtures(cls, bind_name: str, session: sessionmaker[Session]): diff --git a/src/migrations/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py b/src/migrations/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py index de41d633..28734492 100644 --- a/src/migrations/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py +++ b/src/migrations/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py @@ -30,6 +30,7 @@ def upgrade_default() -> None: sa.Column("bind", sa.String(), nullable=False), sa.Column("module_name", sa.String(), nullable=False), sa.Column("signature", sa.String(), nullable=False), + sa.Column("alembic_revision", sa.String(), nullable=False), sa.Column("processed_at", sa.DateTime(timezone=True), nullable=False), sa.PrimaryKeyConstraint("bind", "module_name"), ) From 9a4335f69f67cb3b3b9676882a0d9e1e72d3e963 Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Mon, 27 Jan 2025 22:31:25 +0000 Subject: [PATCH 15/15] Refactor column name to "alembic_head_revisions". Renamed the "alembic_revision" column to "alembic_head_revisions" in both the migration environment and initialization script. This ensures consistency and better reflects the purpose of the column. --- src/migrations/env.py | 14 ++++++++++---- ...12326-52b1246eda46_initialize_fixture_tables.py | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/migrations/env.py b/src/migrations/env.py index 983f34c6..75abf1ae 100644 --- a/src/migrations/env.py +++ b/src/migrations/env.py @@ -50,7 +50,9 @@ class FixtureMigration(declarative_base): bind: Mapped[str] = mapped_column(String(), primary_key=True) module_name: Mapped[str] = mapped_column(String(), primary_key=True) signature: Mapped[str] = mapped_column(String(), nullable=False) - alembic_revision: Mapped[str] = mapped_column(String(), nullable=True, default=str(context.get_head_revision())) + alembic_head_revisions: Mapped[str] = mapped_column( + String(), nullable=True, default=str(context.get_head_revision()) + ) processed_at: Mapped[datetime] = mapped_column( DateTime(), nullable=False, default=datetime.now @@ -260,9 +262,10 @@ async def a_migrate_fixtures( cls.logger.info( f"`{fixture_module.__name__}` fixtures correctly created for `{bind_name}` bind" ) - except Exception as e: + except Exception: cls.logger.error( - f"`{fixture_module.__name__}` fixtures failed to apply to `{bind_name}` bind", exc_info=True + f"`{fixture_module.__name__}` fixtures failed to apply to `{bind_name}` bind", + exc_info=True, ) await session.rollback() @@ -397,8 +400,10 @@ async def run_migrations_online() -> None: for name, rec in engines.items(): logger.info(f"Migrating database {name}") if isinstance(rec["engine"], AsyncEngine): + def migration_callable(*args, **kwargs): return do_run_migration(*args, name=name, **kwargs) + await rec["connection"].run_sync(migration_callable) else: do_run_migration(rec["connection"], name) @@ -420,7 +425,8 @@ def migration_callable(*args, **kwargs): for name, rec in engines.items(): if isinstance(rec["engine"], AsyncEngine): await FixtureHandler.a_migrate_fixtures( - bind_name=name, session=async_sessionmaker(bind=rec["connection"]) + bind_name=name, + session=async_sessionmaker(bind=rec["connection"]), ) else: FixtureHandler.migrate_fixtures( diff --git a/src/migrations/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py b/src/migrations/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py index 28734492..1a83c841 100644 --- a/src/migrations/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py +++ b/src/migrations/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py @@ -30,7 +30,7 @@ def upgrade_default() -> None: sa.Column("bind", sa.String(), nullable=False), sa.Column("module_name", sa.String(), nullable=False), sa.Column("signature", sa.String(), nullable=False), - sa.Column("alembic_revision", sa.String(), nullable=False), + sa.Column("alembic_head_revisions", sa.String(), nullable=False), sa.Column("processed_at", sa.DateTime(timezone=True), nullable=False), sa.PrimaryKeyConstraint("bind", "module_name"), )