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/.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/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/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
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 8330357f..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,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/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/env.py b/src/alembic/env.py
deleted file mode 100644
index 00629292..00000000
--- a/src/alembic/env.py
+++ /dev/null
@@ -1,187 +0,0 @@
-import logging
-from asyncio import get_event_loop
-
-from sqlalchemy.ext.asyncio import AsyncEngine
-
-from alembic import context
-from common.bootstrap import application_init
-from common.config import AppConfig
-
-USE_TWOPHASE = False
-
-
-# this is the Alembic Config object, which provides
-# access to the values within the .ini file in use.
-config = context.config
-
-# Interpret the config file for Python logging.
-# This line sets up loggers basically.
-# if config.config_file_name is not None:
-# fileConfig(config.config_file_name)
-
-# gather section names referring to different
-# databases. These are named "engine1", "engine2"
-# in the sample .ini file.
-# db_names = config.get_main_option("databases")
-
-di_container = application_init(AppConfig()).di_container
-logger = logging.getLogger("alembic.env")
-sa_manager = di_container.SQLAlchemyBindManager()
-
-target_metadata = sa_manager.get_bind_mappers_metadata()
-db_names = target_metadata.keys()
-config.set_main_option("databases", ",".join(db_names))
-
-# add your model's MetaData objects here
-# for 'autogenerate' support. These must be set
-# up to hold just those tables targeting a
-# particular database. table.tometadata() may be
-# helpful here in case a "copy" of
-# a MetaData is needed.
-# from myapp import mymodel
-# target_metadata = {
-# 'engine1':mymodel.metadata1,
-# 'engine2':mymodel.metadata2
-# }
-
-
-# other values from the config, defined by the needs of env.py,
-# can be acquired:
-# my_important_option = config.get_main_option("my_important_option")
-# ... etc.
-
-
-def run_migrations_offline() -> None:
- """Run migrations in 'offline' mode.
-
- This configures the context with just a URL
- and not an Engine, though an Engine is acceptable
- here as well. By skipping the Engine creation
- we don't even need a DBAPI to be available.
-
- Calls to context.execute() here emit the given string to the
- script output.
-
- """
- # for the --sql use case, run migrations for each URL into
- # individual files.
-
- engines = {}
- for name in db_names:
- engines[name] = {}
- engines[name]["url"] = sa_manager.get_bind(name).engine.url
-
- for name, rec in engines.items():
- logger.info(f"Migrating database {name}")
- file_ = f"{name}.sql"
- logger.info(f"Writing output to {file_}")
- with open(file_, "w") as buffer:
- context.configure(
- url=rec["url"],
- output_buffer=buffer,
- target_metadata=target_metadata.get(name),
- literal_binds=True,
- dialect_opts={"paramstyle": "named"},
- )
- with context.begin_transaction():
- context.run_migrations(engine_name=name)
-
-
-def do_run_migration(conn, name):
- context.configure(
- connection=conn,
- upgrade_token=f"{name}_upgrades",
- downgrade_token=f"{name}_downgrades",
- target_metadata=target_metadata.get(name),
- )
- context.run_migrations(engine_name=name)
-
-
-async def run_migrations_online() -> None:
- """Run migrations in 'online' mode.
-
- In this scenario we need to create an Engine
- and associate a connection with the context.
- """
-
- # for the direct-to-DB use case, start a transaction on all
- # engines, then run all migrations, then commit all transactions.
-
- engines = {}
- for name in db_names:
- engines[name] = {}
- engines[name]["engine"] = sa_manager.get_bind(name).engine
-
- for name, rec in engines.items():
- engine = rec["engine"]
- if isinstance(engine, AsyncEngine):
- rec["connection"] = conn = await engine.connect()
-
- if USE_TWOPHASE:
- rec["transaction"] = await conn.begin_twophase()
- else:
- rec["transaction"] = await conn.begin()
- else:
- rec["connection"] = conn = engine.connect()
-
- if USE_TWOPHASE:
- rec["transaction"] = conn.begin_twophase()
- else:
- rec["transaction"] = conn.begin()
-
- try:
- 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(name, rec)
-
- if USE_TWOPHASE:
- for rec in engines.values():
- if isinstance(rec["engine"], AsyncEngine):
- await rec["transaction"].prepare()
- else:
- rec["transaction"].prepare()
-
- for rec in engines.values():
- if isinstance(rec["engine"], AsyncEngine):
- await rec["transaction"].commit()
- else:
- rec["transaction"].commit()
- except:
- for rec in engines.values():
- if isinstance(rec["engine"], AsyncEngine):
- await rec["transaction"].rollback()
- else:
- rec["transaction"].rollback()
- raise
- finally:
- for rec in engines.values():
- if isinstance(rec["engine"], AsyncEngine):
- await rec["connection"].close()
- else:
- rec["connection"].close()
-
-
-background_tasks = set()
-
-if context.is_offline_mode():
- run_migrations_offline()
-else:
- loop = get_event_loop()
- if loop.is_running():
- task = loop.create_task(run_migrations_online())
- # Add task to the set. This creates a strong reference.
- background_tasks.add(task)
-
- # To prevent keeping references to finished tasks forever,
- # make each task remove its own reference from the set after
- # completion:
- task.add_done_callback(background_tasks.discard)
- else:
- loop.run_until_complete(run_migrations_online())
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/migrations/env.py b/src/migrations/env.py
new file mode 100644
index 00000000..75abf1ae
--- /dev/null
+++ b/src/migrations/env.py
@@ -0,0 +1,466 @@
+import hashlib
+import importlib
+import logging
+import sys
+from asyncio import get_event_loop
+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 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 common.bootstrap import application_init
+from common.config import AppConfig
+
+USE_TWOPHASE = False
+
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+# if config.config_file_name is not None:
+# fileConfig(config.config_file_name)
+
+# gather section names referring to different
+# databases. These are named "engine1", "engine2"
+# in the sample .ini file.
+# db_names = config.get_main_option("databases")
+
+di_container = application_init(AppConfig()).di_container
+logger = logging.getLogger("alembic.env")
+sa_manager = di_container.SQLAlchemyBindManager()
+
+target_metadata = sa_manager.get_bind_mappers_metadata()
+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"
+
+ 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_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
+ )
+
+ return FixtureMigration
+
+
+fixture_migration_models = {}
+for name in db_names:
+ fixture_migration_models[name] = generate_fixture_migration_model(
+ sa_manager.get_bind(name).declarative_base
+ )
+
+
+# add your model's MetaData objects here
+# for 'autogenerate' support. These must be set
+# up to hold just those tables targeting a
+# particular database. table.tometadata() may be
+# helpful here in case a "copy" of
+# a MetaData is needed.
+# from myapp import mymodel
+# target_metadata = {
+# 'engine1':mymodel.metadata1,
+# 'engine2':mymodel.metadata2
+# }
+
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+
+
+class FixtureHandler:
+ alembic_path = path.dirname(path.realpath(__file__))
+ fixtures_path = alembic_path + "/fixtures"
+ 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.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.module_name}` 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,
+ module_name=f"{fixture_module.__name__}",
+ signature=signature,
+ )
+ )
+
+ @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"
+ )
+ except Exception:
+ cls.logger.error(
+ 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]):
+ """
+ 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"
+ )
+ 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
+ )
+ 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:
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ # for the --sql use case, run migrations for each URL into
+ # individual files.
+
+ engines = {}
+ for name in db_names:
+ engines[name] = {}
+ engines[name]["url"] = sa_manager.get_bind(name).engine.url
+
+ for name, rec in engines.items():
+ logger.info(f"Migrating database {name}")
+ file_ = f"{name}.sql"
+ logger.info(f"Writing output to {file_}")
+ with open(file_, "w") as buffer:
+ context.configure(
+ url=rec["url"],
+ output_buffer=buffer,
+ target_metadata=target_metadata.get(name),
+ literal_binds=True,
+ dialect_opts={"paramstyle": "named"},
+ )
+ with context.begin_transaction():
+ context.run_migrations(engine_name=name)
+
+
+def do_run_migration(conn, name):
+ context.configure(
+ connection=conn,
+ upgrade_token=f"{name}_upgrades",
+ downgrade_token=f"{name}_downgrades",
+ target_metadata=target_metadata.get(name),
+ )
+ context.run_migrations(engine_name=name)
+
+
+async def run_migrations_online() -> None:
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+ """
+
+ # for the direct-to-DB use case, start a transaction on all
+ # engines, then run all migrations, then commit all transactions.
+
+ engines = {}
+ for name in db_names:
+ engines[name] = {}
+ engines[name]["engine"] = sa_manager.get_bind(name).engine
+
+ for name, rec in engines.items():
+ engine = rec["engine"]
+ if isinstance(engine, AsyncEngine):
+ rec["connection"] = conn = await engine.connect()
+
+ if USE_TWOPHASE:
+ rec["transaction"] = await conn.begin_twophase()
+ else:
+ rec["transaction"] = await conn.begin()
+ else:
+ rec["connection"] = conn = engine.connect()
+
+ if USE_TWOPHASE:
+ rec["transaction"] = conn.begin_twophase()
+ else:
+ rec["transaction"] = conn.begin()
+
+ try:
+ 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)
+
+ if USE_TWOPHASE:
+ for rec in engines.values():
+ if isinstance(rec["engine"], AsyncEngine):
+ await rec["transaction"].prepare()
+ else:
+ rec["transaction"].prepare()
+
+ for rec in engines.values():
+ if isinstance(rec["engine"], AsyncEngine):
+ 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):
+ await rec["transaction"].rollback()
+ else:
+ rec["transaction"].rollback()
+ raise
+ finally:
+ for rec in engines.values():
+ if isinstance(rec["engine"], AsyncEngine):
+ await rec["connection"].close()
+ else:
+ rec["connection"].close()
+
+
+background_tasks = set()
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ loop = get_event_loop()
+ if loop.is_running():
+ task = loop.create_task(run_migrations_online())
+ # Add task to the set. This creates a strong reference.
+ background_tasks.add(task)
+
+ # To prevent keeping references to finished tasks forever,
+ # make each task remove its own reference from the set after
+ # completion:
+ task.add_done_callback(background_tasks.discard)
+ else:
+ loop.run_until_complete(run_migrations_online())
diff --git a/src/migrations/fixtures/__init__.py b/src/migrations/fixtures/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/src/migrations/fixtures/books_example.py b/src/migrations/fixtures/books_example.py
new file mode 100644
index 00000000..c58268cc
--- /dev/null
+++ b/src/migrations/fixtures/books_example.py
@@ -0,0 +1,24 @@
+"""
+`fixtures` is a dictionary following the format:
+
+"BIND_NAME": "LIST_OF_FACTORIES"
+"""
+
+from factory import Factory
+
+from domains.books._models import BookModel
+
+
+class BookFactory(Factory):
+ class Meta:
+ model = BookModel
+
+
+fixtures = {
+ "default": [
+ BookFactory(
+ title="The Shining",
+ author_name="Stephen King",
+ ),
+ ],
+}
diff --git a/src/alembic/script.py.mako b/src/migrations/script.py.mako
similarity index 99%
rename from src/alembic/script.py.mako
rename to src/migrations/script.py.mako
index f1c582af..eb6577a4 100644
--- a/src/alembic/script.py.mako
+++ b/src/migrations/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
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
new file mode 100644
index 00000000..1a83c841
--- /dev/null
+++ b/src/migrations/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py
@@ -0,0 +1,40 @@
+"""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("bind", sa.String(), nullable=False),
+ sa.Column("module_name", sa.String(), nullable=False),
+ sa.Column("signature", 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"),
+ )
+
+
+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/migrations/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/migrations/versions/2025-01-26-212826-bd73bd8a2ac4_create_books_table.py
index 803df1fe..d60e6bc6 100644
--- a/src/alembic/versions/2022-11-09-203313-52b1246eda46_create_tables.py
+++ b/src/migrations/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
"""
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
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]]