From bd79886286ec499d7cf18ee55c334cf0f8749479 Mon Sep 17 00:00:00 2001 From: Pauline Ribeyre <4224001+paulineribeyre@users.noreply.github.com> Date: Thu, 21 Nov 2024 10:50:49 -0600 Subject: [PATCH] MIDRC-849 Add DB migrations setup and tests (#17) --- .github/workflows/ci.yml | 2 +- .secrets.baseline | 40 ++- alembic.ini | 117 +++++++ bin/_common_setup.sh | 50 +++ bin/test.sh | 9 + docs/local_installation.md | 6 + gen3workflow/aws_utils.py | 39 ++- gen3workflow/config-default.yaml | 20 +- gen3workflow/config.py | 23 ++ gen3workflow/models.py | 13 + gen3workflow/routes/storage.py | 2 +- migrations/env.py | 81 +++++ migrations/script.py.mako | 26 ++ .../e1886270d9d2_create_system_key_table.py | 33 ++ poetry.lock | 324 +++++++++++++++++- pyproject.toml | 5 +- tests/conftest.py | 49 ++- tests/migrations/migration_utils.py | 46 +++ .../migrations/test_migration_e1886270d9d2.py | 29 ++ tests/migrations/test_migrations.py | 39 +++ tests/test.sh | 3 - tests/test_misc.py | 20 +- tests/test_storage.py | 15 +- 23 files changed, 929 insertions(+), 62 deletions(-) create mode 100644 alembic.ini create mode 100644 bin/_common_setup.sh create mode 100755 bin/test.sh create mode 100644 gen3workflow/models.py create mode 100644 migrations/env.py create mode 100644 migrations/script.py.mako create mode 100644 migrations/versions/e1886270d9d2_create_system_key_table.py create mode 100644 tests/migrations/migration_utils.py create mode 100644 tests/migrations/test_migration_e1886270d9d2.py create mode 100644 tests/migrations/test_migrations.py delete mode 100755 tests/test.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f8ee601..1311398 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: name: Python Unit Test with Postgres uses: uc-cdis/.github/.github/workflows/python_unit_test.yaml@master with: - test-script: 'tests/test.sh' + test-script: 'bin/test.sh' python-version: '3.9' use-cache: true # run-coveralls: true # TODO enable once the repo is public diff --git a/.secrets.baseline b/.secrets.baseline index 58a7f33..9b3abca 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -136,15 +136,51 @@ "line_number": 15 } ], + "alembic.ini": [ + { + "type": "Basic Auth Credentials", + "filename": "alembic.ini", + "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", + "is_verified": false, + "line_number": 64 + } + ], + "gen3workflow/config-default.yaml": [ + { + "type": "Secret Keyword", + "filename": "gen3workflow/config-default.yaml", + "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", + "is_verified": false, + "line_number": 27 + } + ], + "migrations/versions/e1886270d9d2_create_system_key_table.py": [ + { + "type": "Hex High Entropy String", + "filename": "migrations/versions/e1886270d9d2_create_system_key_table.py", + "hashed_secret": "1df47988c41b70d5541f29636c48c6127cf593b8", + "is_verified": false, + "line_number": 16 + } + ], "tests/conftest.py": [ { "type": "Base64 High Entropy String", "filename": "tests/conftest.py", "hashed_secret": "0dd78d9147bb410f0cb0199c5037da36594f77d8", "is_verified": false, - "line_number": 141 + "line_number": 188 + } + ], + "tests/migrations/test_migration_e1886270d9d2.py": [ + { + "type": "Hex High Entropy String", + "filename": "tests/migrations/test_migration_e1886270d9d2.py", + "hashed_secret": "1df47988c41b70d5541f29636c48c6127cf593b8", + "is_verified": false, + "line_number": 24 } ] }, - "generated_at": "2024-10-23T16:21:37Z" + "generated_at": "2024-11-19T19:43:31Z" } diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..421e3fe --- /dev/null +++ b/alembic.ini @@ -0,0 +1,117 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +# Use forward slashes (/) also on windows to provide an os agnostic path +script_location = migrations + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python>=3.9 or backports.zoneinfo library. +# Any required deps can installed by adding `alembic[tz]` to the pip requirements +# string value is passed to ZoneInfo() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to migrations/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +# version_path_separator = newline +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = driver://user:pass@localhost/dbname + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the exec runner, execute a binary +# hooks = ruff +# ruff.type = exec +# ruff.executable = %(here)s/.venv/bin/ruff +# ruff.options = --fix REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARNING +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARNING +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/bin/_common_setup.sh b/bin/_common_setup.sh new file mode 100644 index 0000000..b981b3a --- /dev/null +++ b/bin/_common_setup.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +set -e + +# Common setup for both tests and running the service +# Used in run.sh and test.sh + +CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source the environment variables from the metrics setup script +# source "${CURRENT_DIR}/setup_prometheus" + +echo "installing dependencies with 'poetry install -vv'..." +poetry install -vv +poetry env info +echo "ensuring db exists" + +# Get the username, password, host, port, and database name +db_settings=$(poetry run python $CURRENT_DIR/../gen3workflow/config.py | tail -1) +if [ -z "${db_settings}" ]; then + echo "'gen3workflow/config.py' did not return DB settings" + exit 1 +fi +db_settings_array=($db_settings) +HOST=${db_settings_array[0]} +PORT=${db_settings_array[1]} +USER=${db_settings_array[2]} +PASSWORD=${db_settings_array[3]} +DB_NAME=${db_settings_array[4]} + +if [ -z "${HOST}" ] || [ -z "${PORT}" ] || [ -z "${USER}" ] || [ -z "${PASSWORD}" ] || [ -z "${DB_NAME}" ]; then + echo "Failed to extract one or more components from DB settings" + exit 1 +fi + +echo "Extracted database name: ${DB_NAME}" +echo "Extracted username: ${USER}" + +# Check if the database exists +# Use the full connection string to connect directly +if [ "$( PGPASSWORD="${PASSWORD}" psql -h "${HOST}" -p "${PORT}" -U "${USER}" -d postgres -XtAc "SELECT 1 FROM pg_database WHERE datname='${DB_NAME}'" )" = '1' ] +then + echo "Database ${DB_NAME} already exists." +else + echo "Database ${DB_NAME} does not exist. Creating it..." + # Connect to the default postgres database to create the new database + PGPASSWORD="${PASSWORD}" psql -h "${HOST}" -p "${PORT}" -U "${USER}" -d postgres -c "CREATE DATABASE \"${DB_NAME}\";" +fi + +echo "running db migration with 'poetry run alembic upgrade head'..." +poetry run alembic upgrade head diff --git a/bin/test.sh b/bin/test.sh new file mode 100755 index 0000000..ac49b59 --- /dev/null +++ b/bin/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -e + +CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +source "${CURRENT_DIR}/_common_setup.sh" + +echo "running tests with 'pytest'..." +poetry run pytest -vv --cov=gen3workflow --cov=migrations --cov-report term-missing:skip-covered --cov-report xml diff --git a/docs/local_installation.md b/docs/local_installation.md index 68be775..cbf69b7 100644 --- a/docs/local_installation.md +++ b/docs/local_installation.md @@ -59,6 +59,12 @@ Update your configuration file: - set `TES_SERVER_URL` to the TES server URL - set `MOCK_AUTH` to `true`, so that no attempts to interact with Arborist are made. +Run database schema migration: + +```bash +alembic upgrade head +``` + Start the Gen3Workflow app: ```bash diff --git a/gen3workflow/aws_utils.py b/gen3workflow/aws_utils.py index 6b70408..e98e8ae 100644 --- a/gen3workflow/aws_utils.py +++ b/gen3workflow/aws_utils.py @@ -13,30 +13,32 @@ iam_resp_err = "Unexpected response from AWS IAM" -def get_iam_user_name(user_id): +def get_safe_name_from_user_id(user_id): """ - Generate a valid IAM user name for the specified user. - IAM user names can contain up to 64 characters. They can only contain alphanumeric characters + Generate a valid IAM user name or S3 bucket name for the specified user. + - IAM user names can contain up to 64 characters. They can only contain alphanumeric characters and/or the following: +=,.@_- (not enforced here since user IDs and hostname should not contain special characters). + - S3 bucket names can contain up to 63 characters. Args: user_id (str): The user's unique Gen3 ID Returns: - str: IAM user name + str: safe name """ escaped_hostname = config["HOSTNAME"].replace(".", "-") - iam_user_name = f"gen3wf-{escaped_hostname}" - max = 64 - len(f"-{user_id}") - if len(iam_user_name) > max: - iam_user_name = iam_user_name[:max] - iam_user_name = f"{iam_user_name}-{user_id}" - return iam_user_name + safe_name = f"gen3wf-{escaped_hostname}" + max = 63 - len(f"-{user_id}") + if len(safe_name) > max: + safe_name = safe_name[:max] + safe_name = f"{safe_name}-{user_id}" + return safe_name -def get_user_bucket_info(user_id): - """TODO +def create_user_bucket(user_id): + """ + Create an S3 bucket for the specified user and return information about the bucket. Args: user_id (str): The user's unique Gen3 ID @@ -44,7 +46,10 @@ def get_user_bucket_info(user_id): Returns: tuple: (bucket name, prefix where the user stores objects in the bucket, bucket region) """ - return "TODO", "ga4gh-tes", "us-east-1" + user_bucket_name = get_safe_name_from_user_id(user_id) + s3_client = boto3.client("s3") + s3_client.create_bucket(Bucket=user_bucket_name) + return user_bucket_name, "ga4gh-tes", config["USER_BUCKETS_REGION"] def create_or_update_policy(policy_name, policy_document, path_prefix, tags): @@ -94,7 +99,7 @@ def create_or_update_policy(policy_name, policy_document, path_prefix, tags): def create_iam_user_and_key(user_id): - iam_user_name = get_iam_user_name(user_id) + iam_user_name = get_safe_name_from_user_id(user_id) escaped_hostname = config["HOSTNAME"].replace(".", "-") iam_tags = [ { @@ -111,7 +116,7 @@ def create_iam_user_and_key(user_id): raise # grant the IAM user access to the user's s3 bucket - bucket_name, bucket_prefix, _ = get_user_bucket_info(user_id) + bucket_name, bucket_prefix, _ = create_user_bucket(user_id) policy_document = { "Version": "2012-10-17", "Statement": [ @@ -145,7 +150,7 @@ def create_iam_user_and_key(user_id): def list_iam_user_keys(user_id): - iam_user_name = get_iam_user_name(user_id) + iam_user_name = get_safe_name_from_user_id(user_id) try: response = iam_client.list_access_keys(UserName=iam_user_name) except ClientError as e: @@ -164,7 +169,7 @@ def list_iam_user_keys(user_id): def delete_iam_user_key(user_id, key_id): try: iam_client.delete_access_key( - UserName=get_iam_user_name(user_id), + UserName=get_safe_name_from_user_id(user_id), AccessKeyId=key_id, ) except ClientError as e: diff --git a/gen3workflow/config-default.yaml b/gen3workflow/config-default.yaml index b8d5fc5..270384d 100644 --- a/gen3workflow/config-default.yaml +++ b/gen3workflow/config-default.yaml @@ -6,14 +6,26 @@ HOSTNAME: localhost DEBUG: false DOCS_URL_PREFIX: /gen3workflow -MAX_IAM_KEYS_PER_USER: 2 # the default AWS AccessKeysPerUser quota is 2 -IAM_KEYS_LIFETIME_DAYS: 30 - # override the default Arborist URL; ignored if already set as an environment variable ARBORIST_URL: # /!\ only use for development! Allows running gen3workflow locally without Arborist interaction -MOCK_AUTH: false +MOCK_AUTH: false # TODO add to config validation. Also add "no unexpected props" to validation. + +MAX_IAM_KEYS_PER_USER: 2 # the default AWS AccessKeysPerUser quota is 2 +IAM_KEYS_LIFETIME_DAYS: 30 +USER_BUCKETS_REGION: us-east-1 + +############# +# DATABASE # +############# + +DB_DRIVER: postgresql+asyncpg +DB_HOST: localhost +DB_PORT: 5432 +DB_USER: postgres +DB_PASSWORD: postgres +DB_DATABASE: gen3workflow_test diff --git a/gen3workflow/config.py b/gen3workflow/config.py index 1520f08..fc747da 100644 --- a/gen3workflow/config.py +++ b/gen3workflow/config.py @@ -17,6 +17,18 @@ class Gen3WorkflowConfig(Config): def __init__(self, *args, **kwargs): super(Gen3WorkflowConfig, self).__init__(*args, **kwargs) + def post_process(self) -> None: + # generate DB_CONNECTION_STRING from DB configs or env vars + drivername = os.environ.get("DB_DRIVER", self["DB_DRIVER"]) + host = os.environ.get("DB_HOST", self["DB_HOST"]) + port = os.environ.get("DB_PORT", self["DB_PORT"]) + username = os.environ.get("DB_USER", self["DB_USER"]) + password = os.environ.get("DB_PASSWORD", self["DB_PASSWORD"]) + database = os.environ.get("DB_DATABASE", self["DB_DATABASE"]) + self["DB_CONNECTION_STRING"] = ( + f"{drivername}://{username}:{password}@{host}:{port}/{database}" + ) + def validate(self) -> None: """ Perform a series of sanity checks on a loaded config. @@ -35,6 +47,7 @@ def validate_top_level_configs(self): # aws_utils.list_iam_user_keys should be updated to fetch paginated results if >100 "MAX_IAM_KEYS_PER_USER": {"type": "integer", "maximum": 100}, "IAM_KEYS_LIFETIME_DAYS": {"type": "integer"}, + "USER_BUCKETS_REGION": {"type": "string"}, "ARBORIST_URL": {"type": ["string", "null"]}, "TASK_IMAGE_WHITELIST": {"type": "array", "items": {"type": "string"}}, "TES_SERVER_URL": {"type": "string"}, @@ -56,3 +69,13 @@ def validate_top_level_configs(self): except Exception: logger.warning("Unable to load config, using default config...", exc_info=True) config.load(config_path=DEFAULT_CFG_PATH) + + +if __name__ == "__main__": + # used by `bin._common_setup.sh` to create the database as configured + host = os.environ.get("DB_HOST", config["DB_HOST"]) + port = os.environ.get("DB_PORT", config["DB_PORT"]) + username = os.environ.get("DB_USER", config["DB_USER"]) + password = os.environ.get("DB_PASSWORD", config["DB_PASSWORD"]) + database = os.environ.get("DB_DATABASE", config["DB_DATABASE"]) + print("\n", host, port, username, password, database) diff --git a/gen3workflow/models.py b/gen3workflow/models.py new file mode 100644 index 0000000..c4d86de --- /dev/null +++ b/gen3workflow/models.py @@ -0,0 +1,13 @@ +from sqlalchemy import Column, String +from sqlalchemy.orm import declarative_base + + +Base = declarative_base() + + +class SystemKey(Base): + __tablename__ = "system_key" + + key_id = Column(String, primary_key=True) + key_secret = Column(String) + user_id = Column(String) diff --git a/gen3workflow/routes/storage.py b/gen3workflow/routes/storage.py index ba2c337..c1cb3d9 100644 --- a/gen3workflow/routes/storage.py +++ b/gen3workflow/routes/storage.py @@ -20,7 +20,7 @@ async def get_storage_info(request: Request, auth=Depends(Auth)): token_claims = await auth.get_token_claims() user_id = token_claims.get("sub") - bucket_name, bucket_prefix, bucket_region = aws_utils.get_user_bucket_info(user_id) + bucket_name, bucket_prefix, bucket_region = aws_utils.create_user_bucket(user_id) return { "bucket": bucket_name, "workdir": f"s3://{bucket_name}/{bucket_prefix}", diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..9e7770c --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,81 @@ +from logging.config import fileConfig + +from alembic import context +import asyncio +from sqlalchemy import pool +from sqlalchemy.engine import Connection +from sqlalchemy.ext.asyncio import async_engine_from_config + +from gen3workflow.config import config as gen3workflow_config +from gen3workflow.models import Base + + +config = context.config +if config.config_file_name is not None: + fileConfig(config.config_file_name) +config.set_main_option("sqlalchemy.url", gen3workflow_config["DB_CONNECTION_STRING"]) + +target_metadata = Base.metadata + + +def run_migrations_offline() -> None: + """ + Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def do_run_migrations(connection: Connection) -> None: + context.configure(connection=connection, target_metadata=target_metadata) + + with context.begin_transaction(): + context.run_migrations() + + +async def run_async_migrations() -> None: + """ + In this scenario we need to create an Engine + and associate a connection with the context. + """ + connectable = async_engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + async with connectable.connect() as connection: + await connection.run_sync(do_run_migrations) + + await connectable.dispose() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode.""" + connectable = context.config.attributes.get("connection", None) + if connectable is None: + asyncio.run(run_async_migrations()) + else: + do_run_migrations(connectable) # to support running migrations in unit tests + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..fbc4b07 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,26 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/e1886270d9d2_create_system_key_table.py b/migrations/versions/e1886270d9d2_create_system_key_table.py new file mode 100644 index 0000000..d44a9c5 --- /dev/null +++ b/migrations/versions/e1886270d9d2_create_system_key_table.py @@ -0,0 +1,33 @@ +"""Create system_key table + +Revision ID: e1886270d9d2 +Revises: +Create Date: 2024-11-18 11:12:41.727481 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "e1886270d9d2" +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "system_key", + sa.Column("key_id", sa.VARCHAR(), nullable=False), + sa.Column("key_secret", sa.VARCHAR(), nullable=False), + sa.Column("user_id", sa.VARCHAR(), nullable=False), + sa.PrimaryKeyConstraint("key_id", name="system_key_pkey"), + ) + + +def downgrade() -> None: + op.drop_table("system_key") diff --git a/poetry.lock b/poetry.lock index 5cc3bc7..c4b36bf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,23 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "alembic" +version = "1.14.0" +description = "A database migration tool for SQLAlchemy." +optional = false +python-versions = ">=3.8" +files = [ + {file = "alembic-1.14.0-py3-none-any.whl", hash = "sha256:99bd884ca390466db5e27ffccff1d179ec5c05c965cfefc0607e69f9e411cb25"}, + {file = "alembic-1.14.0.tar.gz", hash = "sha256:b00892b53b3642d0b8dbedba234dbf1924b69be83a9a769d5a624b01094e304b"}, +] + +[package.dependencies] +Mako = "*" +SQLAlchemy = ">=1.3.0" +typing-extensions = ">=4" + +[package.extras] +tz = ["backports.zoneinfo"] [[package]] name = "annotated-types" @@ -33,6 +52,83 @@ doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] trio = ["trio (>=0.26.1)"] +[[package]] +name = "async-timeout" +version = "5.0.1" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, + {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, +] + +[[package]] +name = "asyncpg" +version = "0.30.0" +description = "An asyncio PostgreSQL driver" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "asyncpg-0.30.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfb4dd5ae0699bad2b233672c8fc5ccbd9ad24b89afded02341786887e37927e"}, + {file = "asyncpg-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc1f62c792752a49f88b7e6f774c26077091b44caceb1983509edc18a2222ec0"}, + {file = "asyncpg-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3152fef2e265c9c24eec4ee3d22b4f4d2703d30614b0b6753e9ed4115c8a146f"}, + {file = "asyncpg-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7255812ac85099a0e1ffb81b10dc477b9973345793776b128a23e60148dd1af"}, + {file = "asyncpg-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:578445f09f45d1ad7abddbff2a3c7f7c291738fdae0abffbeb737d3fc3ab8b75"}, + {file = "asyncpg-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c42f6bb65a277ce4d93f3fba46b91a265631c8df7250592dd4f11f8b0152150f"}, + {file = "asyncpg-0.30.0-cp310-cp310-win32.whl", hash = "sha256:aa403147d3e07a267ada2ae34dfc9324e67ccc4cdca35261c8c22792ba2b10cf"}, + {file = "asyncpg-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb622c94db4e13137c4c7f98834185049cc50ee01d8f657ef898b6407c7b9c50"}, + {file = "asyncpg-0.30.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5e0511ad3dec5f6b4f7a9e063591d407eee66b88c14e2ea636f187da1dcfff6a"}, + {file = "asyncpg-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:915aeb9f79316b43c3207363af12d0e6fd10776641a7de8a01212afd95bdf0ed"}, + {file = "asyncpg-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c198a00cce9506fcd0bf219a799f38ac7a237745e1d27f0e1f66d3707c84a5a"}, + {file = "asyncpg-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3326e6d7381799e9735ca2ec9fd7be4d5fef5dcbc3cb555d8a463d8460607956"}, + {file = "asyncpg-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:51da377487e249e35bd0859661f6ee2b81db11ad1f4fc036194bc9cb2ead5056"}, + {file = "asyncpg-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc6d84136f9c4d24d358f3b02be4b6ba358abd09f80737d1ac7c444f36108454"}, + {file = "asyncpg-0.30.0-cp311-cp311-win32.whl", hash = "sha256:574156480df14f64c2d76450a3f3aaaf26105869cad3865041156b38459e935d"}, + {file = "asyncpg-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:3356637f0bd830407b5597317b3cb3571387ae52ddc3bca6233682be88bbbc1f"}, + {file = "asyncpg-0.30.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c902a60b52e506d38d7e80e0dd5399f657220f24635fee368117b8b5fce1142e"}, + {file = "asyncpg-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aca1548e43bbb9f0f627a04666fedaca23db0a31a84136ad1f868cb15deb6e3a"}, + {file = "asyncpg-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c2a2ef565400234a633da0eafdce27e843836256d40705d83ab7ec42074efb3"}, + {file = "asyncpg-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1292b84ee06ac8a2ad8e51c7475aa309245874b61333d97411aab835c4a2f737"}, + {file = "asyncpg-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5712350388d0cd0615caec629ad53c81e506b1abaaf8d14c93f54b35e3595a"}, + {file = "asyncpg-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db9891e2d76e6f425746c5d2da01921e9a16b5a71a1c905b13f30e12a257c4af"}, + {file = "asyncpg-0.30.0-cp312-cp312-win32.whl", hash = "sha256:68d71a1be3d83d0570049cd1654a9bdfe506e794ecc98ad0873304a9f35e411e"}, + {file = "asyncpg-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:9a0292c6af5c500523949155ec17b7fe01a00ace33b68a476d6b5059f9630305"}, + {file = "asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70"}, + {file = "asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3"}, + {file = "asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33"}, + {file = "asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4"}, + {file = "asyncpg-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4"}, + {file = "asyncpg-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba"}, + {file = "asyncpg-0.30.0-cp313-cp313-win32.whl", hash = "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590"}, + {file = "asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e"}, + {file = "asyncpg-0.30.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:29ff1fc8b5bf724273782ff8b4f57b0f8220a1b2324184846b39d1ab4122031d"}, + {file = "asyncpg-0.30.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64e899bce0600871b55368b8483e5e3e7f1860c9482e7f12e0a771e747988168"}, + {file = "asyncpg-0.30.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b290f4726a887f75dcd1b3006f484252db37602313f806e9ffc4e5996cfe5cb"}, + {file = "asyncpg-0.30.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f86b0e2cd3f1249d6fe6fd6cfe0cd4538ba994e2d8249c0491925629b9104d0f"}, + {file = "asyncpg-0.30.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:393af4e3214c8fa4c7b86da6364384c0d1b3298d45803375572f415b6f673f38"}, + {file = "asyncpg-0.30.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:fd4406d09208d5b4a14db9a9dbb311b6d7aeeab57bded7ed2f8ea41aeef39b34"}, + {file = "asyncpg-0.30.0-cp38-cp38-win32.whl", hash = "sha256:0b448f0150e1c3b96cb0438a0d0aa4871f1472e58de14a3ec320dbb2798fb0d4"}, + {file = "asyncpg-0.30.0-cp38-cp38-win_amd64.whl", hash = "sha256:f23b836dd90bea21104f69547923a02b167d999ce053f3d502081acea2fba15b"}, + {file = "asyncpg-0.30.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6f4e83f067b35ab5e6371f8a4c93296e0439857b4569850b178a01385e82e9ad"}, + {file = "asyncpg-0.30.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5df69d55add4efcd25ea2a3b02025b669a285b767bfbf06e356d68dbce4234ff"}, + {file = "asyncpg-0.30.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3479a0d9a852c7c84e822c073622baca862d1217b10a02dd57ee4a7a081f708"}, + {file = "asyncpg-0.30.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26683d3b9a62836fad771a18ecf4659a30f348a561279d6227dab96182f46144"}, + {file = "asyncpg-0.30.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1b982daf2441a0ed314bd10817f1606f1c28b1136abd9e4f11335358c2c631cb"}, + {file = "asyncpg-0.30.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1c06a3a50d014b303e5f6fc1e5f95eb28d2cee89cf58384b700da621e5d5e547"}, + {file = "asyncpg-0.30.0-cp39-cp39-win32.whl", hash = "sha256:1b11a555a198b08f5c4baa8f8231c74a366d190755aa4f99aacec5970afe929a"}, + {file = "asyncpg-0.30.0-cp39-cp39-win_amd64.whl", hash = "sha256:8b684a3c858a83cd876f05958823b68e8d14ec01bb0c0d14a6704c5bf9711773"}, + {file = "asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851"}, +] + +[package.dependencies] +async-timeout = {version = ">=4.0.3", markers = "python_version < \"3.11.0\""} + +[package.extras] +docs = ["Sphinx (>=8.1.3,<8.2.0)", "sphinx-rtd-theme (>=1.2.2)"] +gssauth = ["gssapi", "sspilib"] +test = ["distro (>=1.9.0,<1.10.0)", "flake8 (>=6.1,<7.0)", "flake8-pyi (>=24.1.0,<24.2.0)", "gssapi", "k5test", "mypy (>=1.8.0,<1.9.0)", "sspilib", "uvloop (>=0.15.3)"] + [[package]] name = "attrs" version = "24.2.0" @@ -103,17 +199,17 @@ files = [ [[package]] name = "boto3" -version = "1.35.64" +version = "1.35.65" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.35.64-py3-none-any.whl", hash = "sha256:cdacf03fc750caa3aa0dbf6158166def9922c9d67b4160999ff8fc350662facc"}, - {file = "boto3-1.35.64.tar.gz", hash = "sha256:bc3fc12b41fa2c91e51ab140f74fb1544408a2b1e00f88a4c2369a66d18ddf20"}, + {file = "boto3-1.35.65-py3-none-any.whl", hash = "sha256:acbca38322b66516450f959c7874826267d431becdc2b080e331e56c2ebbe507"}, + {file = "boto3-1.35.65.tar.gz", hash = "sha256:f6c266b4124b92b1603727bf1ed1917e0b74a899bd0e326f151d80c3eaed27a1"}, ] [package.dependencies] -botocore = ">=1.35.64,<1.36.0" +botocore = ">=1.35.65,<1.36.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -122,13 +218,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.35.64" +version = "1.35.65" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.35.64-py3-none-any.whl", hash = "sha256:bbd96bf7f442b1d5e35b36f501076e4a588c83d8d84a1952e9ee1d767e5efb3e"}, - {file = "botocore-1.35.64.tar.gz", hash = "sha256:2f95c83f31c9e38a66995c88810fc638c829790e125032ba00ab081a2cf48cb9"}, + {file = "botocore-1.35.65-py3-none-any.whl", hash = "sha256:8fcaa82ab2338f412e1494449c4c57f9ca785623fb0303f6be5b279c4d27522c"}, + {file = "botocore-1.35.65.tar.gz", hash = "sha256:46652f732f2b2fb395fffcc33cacb288d05ea283047c9a996fb59d6849464919"}, ] [package.dependencies] @@ -618,6 +714,92 @@ jinja2 = "*" pyyaml = "*" six = "*" +[[package]] +name = "greenlet" +version = "3.1.1" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.7" +files = [ + {file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617"}, + {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7"}, + {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6"}, + {file = "greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80"}, + {file = "greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a"}, + {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511"}, + {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395"}, + {file = "greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39"}, + {file = "greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9"}, + {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0"}, + {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942"}, + {file = "greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01"}, + {file = "greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e"}, + {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1"}, + {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c"}, + {file = "greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822"}, + {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01"}, + {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47da355d8687fd65240c364c90a31569a133b7b60de111c255ef5b606f2ae291"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98884ecf2ffb7d7fe6bd517e8eb99d31ff7855a840fa6d0d63cd07c037f6a981"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1d4aeb8891338e60d1ab6127af1fe45def5259def8094b9c7e34690c8858803"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db32b5348615a04b82240cc67983cb315309e88d444a288934ee6ceaebcad6cc"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcc62f31eae24de7f8dce72134c8651c58000d3b1868e01392baea7c32c247de"}, + {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1d3755bcb2e02de341c55b4fca7a745a24a9e7212ac953f6b3a48d117d7257aa"}, + {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b8da394b34370874b4572676f36acabac172602abf054cbc4ac910219f3340af"}, + {file = "greenlet-3.1.1-cp37-cp37m-win32.whl", hash = "sha256:a0dfc6c143b519113354e780a50381508139b07d2177cb6ad6a08278ec655798"}, + {file = "greenlet-3.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:54558ea205654b50c438029505def3834e80f0869a70fb15b871c29b4575ddef"}, + {file = "greenlet-3.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1"}, + {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd"}, + {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7"}, + {file = "greenlet-3.1.1-cp38-cp38-win32.whl", hash = "sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef"}, + {file = "greenlet-3.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d"}, + {file = "greenlet-3.1.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c"}, + {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e"}, + {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e"}, + {file = "greenlet-3.1.1-cp39-cp39-win32.whl", hash = "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c"}, + {file = "greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22"}, + {file = "greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467"}, +] + +[package.extras] +docs = ["Sphinx", "furo"] +test = ["objgraph", "psutil"] + [[package]] name = "gunicorn" version = "23.0.0" @@ -784,6 +966,25 @@ files = [ [package.dependencies] referencing = ">=0.31.0" +[[package]] +name = "mako" +version = "1.3.6" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Mako-1.3.6-py3-none-any.whl", hash = "sha256:a91198468092a2f1a0de86ca92690fb0cfc43ca90ee17e15d93662b4c04b241a"}, + {file = "mako-1.3.6.tar.gz", hash = "sha256:9ec3a1583713479fae654f83ed9fa8c9a4c16b7bb0daba0e6bbebff50c0d983d"}, +] + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +babel = ["Babel"] +lingua = ["lingua"] +testing = ["pytest"] + [[package]] name = "markupsafe" version = "3.0.2" @@ -1123,17 +1324,17 @@ testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] name = "pytest-cov" -version = "5.0.0" +version = "6.0.0" description = "Pytest plugin for measuring coverage." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, - {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, + {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, + {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, ] [package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} +coverage = {version = ">=7.5", extras = ["toml"]} pytest = ">=4.6" [package.extras] @@ -1408,6 +1609,101 @@ files = [ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] +[[package]] +name = "sqlalchemy" +version = "2.0.36" +description = "Database Abstraction Library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "SQLAlchemy-2.0.36-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:59b8f3adb3971929a3e660337f5dacc5942c2cdb760afcabb2614ffbda9f9f72"}, + {file = "SQLAlchemy-2.0.36-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37350015056a553e442ff672c2d20e6f4b6d0b2495691fa239d8aa18bb3bc908"}, + {file = "SQLAlchemy-2.0.36-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8318f4776c85abc3f40ab185e388bee7a6ea99e7fa3a30686580b209eaa35c08"}, + {file = "SQLAlchemy-2.0.36-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c245b1fbade9c35e5bd3b64270ab49ce990369018289ecfde3f9c318411aaa07"}, + {file = "SQLAlchemy-2.0.36-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:69f93723edbca7342624d09f6704e7126b152eaed3cdbb634cb657a54332a3c5"}, + {file = "SQLAlchemy-2.0.36-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f9511d8dd4a6e9271d07d150fb2f81874a3c8c95e11ff9af3a2dfc35fe42ee44"}, + {file = "SQLAlchemy-2.0.36-cp310-cp310-win32.whl", hash = "sha256:c3f3631693003d8e585d4200730616b78fafd5a01ef8b698f6967da5c605b3fa"}, + {file = "SQLAlchemy-2.0.36-cp310-cp310-win_amd64.whl", hash = "sha256:a86bfab2ef46d63300c0f06936bd6e6c0105faa11d509083ba8f2f9d237fb5b5"}, + {file = "SQLAlchemy-2.0.36-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fd3a55deef00f689ce931d4d1b23fa9f04c880a48ee97af488fd215cf24e2a6c"}, + {file = "SQLAlchemy-2.0.36-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f5e9cd989b45b73bd359f693b935364f7e1f79486e29015813c338450aa5a71"}, + {file = "SQLAlchemy-2.0.36-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ddd9db6e59c44875211bc4c7953a9f6638b937b0a88ae6d09eb46cced54eff"}, + {file = "SQLAlchemy-2.0.36-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2519f3a5d0517fc159afab1015e54bb81b4406c278749779be57a569d8d1bb0d"}, + {file = "SQLAlchemy-2.0.36-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59b1ee96617135f6e1d6f275bbe988f419c5178016f3d41d3c0abb0c819f75bb"}, + {file = "SQLAlchemy-2.0.36-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:39769a115f730d683b0eb7b694db9789267bcd027326cccc3125e862eb03bfd8"}, + {file = "SQLAlchemy-2.0.36-cp311-cp311-win32.whl", hash = "sha256:66bffbad8d6271bb1cc2f9a4ea4f86f80fe5e2e3e501a5ae2a3dc6a76e604e6f"}, + {file = "SQLAlchemy-2.0.36-cp311-cp311-win_amd64.whl", hash = "sha256:23623166bfefe1487d81b698c423f8678e80df8b54614c2bf4b4cfcd7c711959"}, + {file = "SQLAlchemy-2.0.36-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7b64e6ec3f02c35647be6b4851008b26cff592a95ecb13b6788a54ef80bbdd4"}, + {file = "SQLAlchemy-2.0.36-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46331b00096a6db1fdc052d55b101dbbfc99155a548e20a0e4a8e5e4d1362855"}, + {file = "SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdf3386a801ea5aba17c6410dd1dc8d39cf454ca2565541b5ac42a84e1e28f53"}, + {file = "SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9dfa18ff2a67b09b372d5db8743c27966abf0e5344c555d86cc7199f7ad83a"}, + {file = "SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:90812a8933df713fdf748b355527e3af257a11e415b613dd794512461eb8a686"}, + {file = "SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1bc330d9d29c7f06f003ab10e1eaced295e87940405afe1b110f2eb93a233588"}, + {file = "SQLAlchemy-2.0.36-cp312-cp312-win32.whl", hash = "sha256:79d2e78abc26d871875b419e1fd3c0bca31a1cb0043277d0d850014599626c2e"}, + {file = "SQLAlchemy-2.0.36-cp312-cp312-win_amd64.whl", hash = "sha256:b544ad1935a8541d177cb402948b94e871067656b3a0b9e91dbec136b06a2ff5"}, + {file = "SQLAlchemy-2.0.36-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5cc79df7f4bc3d11e4b542596c03826063092611e481fcf1c9dfee3c94355ef"}, + {file = "SQLAlchemy-2.0.36-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3c01117dd36800f2ecaa238c65365b7b16497adc1522bf84906e5710ee9ba0e8"}, + {file = "SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bc633f4ee4b4c46e7adcb3a9b5ec083bf1d9a97c1d3854b92749d935de40b9b"}, + {file = "SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e46ed38affdfc95d2c958de328d037d87801cfcbea6d421000859e9789e61c2"}, + {file = "SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b2985c0b06e989c043f1dc09d4fe89e1616aadd35392aea2844f0458a989eacf"}, + {file = "SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a121d62ebe7d26fec9155f83f8be5189ef1405f5973ea4874a26fab9f1e262c"}, + {file = "SQLAlchemy-2.0.36-cp313-cp313-win32.whl", hash = "sha256:0572f4bd6f94752167adfd7c1bed84f4b240ee6203a95e05d1e208d488d0d436"}, + {file = "SQLAlchemy-2.0.36-cp313-cp313-win_amd64.whl", hash = "sha256:8c78ac40bde930c60e0f78b3cd184c580f89456dd87fc08f9e3ee3ce8765ce88"}, + {file = "SQLAlchemy-2.0.36-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:be9812b766cad94a25bc63bec11f88c4ad3629a0cec1cd5d4ba48dc23860486b"}, + {file = "SQLAlchemy-2.0.36-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50aae840ebbd6cdd41af1c14590e5741665e5272d2fee999306673a1bb1fdb4d"}, + {file = "SQLAlchemy-2.0.36-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4557e1f11c5f653ebfdd924f3f9d5ebfc718283b0b9beebaa5dd6b77ec290971"}, + {file = "SQLAlchemy-2.0.36-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:07b441f7d03b9a66299ce7ccf3ef2900abc81c0db434f42a5694a37bd73870f2"}, + {file = "SQLAlchemy-2.0.36-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:28120ef39c92c2dd60f2721af9328479516844c6b550b077ca450c7d7dc68575"}, + {file = "SQLAlchemy-2.0.36-cp37-cp37m-win32.whl", hash = "sha256:b81ee3d84803fd42d0b154cb6892ae57ea6b7c55d8359a02379965706c7efe6c"}, + {file = "SQLAlchemy-2.0.36-cp37-cp37m-win_amd64.whl", hash = "sha256:f942a799516184c855e1a32fbc7b29d7e571b52612647866d4ec1c3242578fcb"}, + {file = "SQLAlchemy-2.0.36-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3d6718667da04294d7df1670d70eeddd414f313738d20a6f1d1f379e3139a545"}, + {file = "SQLAlchemy-2.0.36-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:72c28b84b174ce8af8504ca28ae9347d317f9dba3999e5981a3cd441f3712e24"}, + {file = "SQLAlchemy-2.0.36-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b11d0cfdd2b095e7b0686cf5fabeb9c67fae5b06d265d8180715b8cfa86522e3"}, + {file = "SQLAlchemy-2.0.36-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e32092c47011d113dc01ab3e1d3ce9f006a47223b18422c5c0d150af13a00687"}, + {file = "SQLAlchemy-2.0.36-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6a440293d802d3011028e14e4226da1434b373cbaf4a4bbb63f845761a708346"}, + {file = "SQLAlchemy-2.0.36-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c54a1e53a0c308a8e8a7dffb59097bff7facda27c70c286f005327f21b2bd6b1"}, + {file = "SQLAlchemy-2.0.36-cp38-cp38-win32.whl", hash = "sha256:1e0d612a17581b6616ff03c8e3d5eff7452f34655c901f75d62bd86449d9750e"}, + {file = "SQLAlchemy-2.0.36-cp38-cp38-win_amd64.whl", hash = "sha256:8958b10490125124463095bbdadda5aa22ec799f91958e410438ad6c97a7b793"}, + {file = "SQLAlchemy-2.0.36-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dc022184d3e5cacc9579e41805a681187650e170eb2fd70e28b86192a479dcaa"}, + {file = "SQLAlchemy-2.0.36-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b817d41d692bf286abc181f8af476c4fbef3fd05e798777492618378448ee689"}, + {file = "SQLAlchemy-2.0.36-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4e46a888b54be23d03a89be510f24a7652fe6ff660787b96cd0e57a4ebcb46d"}, + {file = "SQLAlchemy-2.0.36-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4ae3005ed83f5967f961fd091f2f8c5329161f69ce8480aa8168b2d7fe37f06"}, + {file = "SQLAlchemy-2.0.36-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:03e08af7a5f9386a43919eda9de33ffda16b44eb11f3b313e6822243770e9763"}, + {file = "SQLAlchemy-2.0.36-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3dbb986bad3ed5ceaf090200eba750b5245150bd97d3e67343a3cfed06feecf7"}, + {file = "SQLAlchemy-2.0.36-cp39-cp39-win32.whl", hash = "sha256:9fe53b404f24789b5ea9003fc25b9a3988feddebd7e7b369c8fac27ad6f52f28"}, + {file = "SQLAlchemy-2.0.36-cp39-cp39-win_amd64.whl", hash = "sha256:af148a33ff0349f53512a049c6406923e4e02bf2f26c5fb285f143faf4f0e46a"}, + {file = "SQLAlchemy-2.0.36-py3-none-any.whl", hash = "sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e"}, + {file = "sqlalchemy-2.0.36.tar.gz", hash = "sha256:7f2767680b6d2398aea7082e45a774b2b0767b5c8d8ffb9c8b683088ea9b29c5"}, +] + +[package.dependencies] +greenlet = {version = "!=0.4.17", optional = true, markers = "python_version < \"3.13\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\") or extra == \"asyncio\""} +typing-extensions = ">=4.6.0" + +[package.extras] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] +aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] +aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] +asyncio = ["greenlet (!=0.4.17)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"] +mssql = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] +mypy = ["mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx_oracle (>=8)"] +oracle-oracledb = ["oracledb (>=1.0.1)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] +postgresql-pg8000 = ["pg8000 (>=1.29.1)"] +postgresql-psycopg = ["psycopg (>=3.0.7)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] +postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] +pymysql = ["pymysql"] +sqlcipher = ["sqlcipher3_binary"] + [[package]] name = "starlette" version = "0.41.3" @@ -1531,4 +1827,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = ">=3.9,<4" -content-hash = "179ea9463fb2e753a3404991f5cf49d7d6dac0b8192b7e33dbdde0b1124afce3" +content-hash = "53f47b12e26333bed2da5ea3567cf7f3b3990aba550ec2f4143415dcc5f39351" diff --git a/pyproject.toml b/pyproject.toml index b5de32a..741bff0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,8 @@ include = [ [tool.poetry.dependencies] python = ">=3.9,<4" +alembic = "<2" +asyncpg = "<1" authutils = "<7" boto3 = "<2" cdislogging = "<2" @@ -21,6 +23,7 @@ gen3config = ">=2.0,<3" gunicorn = "<24" httpx = "<1" jsonschema = "<5" +sqlalchemy = { extras = ["asyncio"], version = "<3" } uvicorn = "<1" [tool.poetry.dev-dependencies] @@ -28,7 +31,7 @@ freezegun = "<2" moto = "<6" pytest = "<9" pytest-asyncio = "<1" -pytest-cov = "<6" +pytest-cov = "<7" [build-system] requires = ["poetry<2"] diff --git a/tests/conftest.py b/tests/conftest.py index a855067..8a6339a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,8 @@ +""" +See https://github.com/uc-cdis/gen3-user-data-library/blob/main/tests/conftest.py#L1 +""" + +import asyncio import json import os from unittest.mock import MagicMock, patch @@ -7,9 +12,10 @@ import httpx import pytest import pytest_asyncio +from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from starlette.config import environ -# Set GEN3WORKFLOW_CONFIG_PATH *before* loading the app which loads the configuration +# Set GEN3WORKFLOW_CONFIG_PATH *before* loading the app, which loads the configuration CURRENT_DIR = os.path.dirname(os.path.realpath(__file__)) environ["GEN3WORKFLOW_CONFIG_PATH"] = os.path.join( CURRENT_DIR, "test-gen3workflow-config.yaml" @@ -17,12 +23,53 @@ from gen3workflow.app import get_app from gen3workflow.config import config +from gen3workflow.models import Base TEST_USER_ID = "64" NEW_TEST_USER_ID = "784" # a new user that does not already exist in arborist +@pytest_asyncio.fixture(scope="function") +async def engine(): + """ + Non-session scoped engine which recreates the database, yields, then drops the tables + """ + engine = create_async_engine( + config["DB_CONNECTION_STRING"], echo=False, future=True + ) + + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.drop_all) + await conn.run_sync(Base.metadata.create_all) + + yield engine + + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.drop_all) + + await engine.dispose() + + +@pytest_asyncio.fixture() +async def session(engine): + """ + Database session which utilizes the above engine and event loop and sets up a nested transaction before yielding. + It rolls back the nested transaction after yield. + """ + event_loop = asyncio.get_running_loop() + session_maker = async_sessionmaker( + engine, expire_on_commit=False, autocommit=False, autoflush=False + ) + + async with engine.connect() as conn: + tsx = await conn.begin() + async with session_maker(bind=conn) as session: + yield session + + await tsx.rollback() + + @pytest.fixture(scope="function") def access_token_patcher(client, request): """ diff --git a/tests/migrations/migration_utils.py b/tests/migrations/migration_utils.py new file mode 100644 index 0000000..19852e7 --- /dev/null +++ b/tests/migrations/migration_utils.py @@ -0,0 +1,46 @@ +import os + +from alembic import command +from alembic.config import Config +from sqlalchemy.ext.asyncio import create_async_engine + +from gen3workflow.config import config + + +class MigrationRunner: + def __init__(self): + self.action: str = "" + self.target: str = "" + current_dir = os.path.dirname(os.path.realpath(__file__)) + self.alembic_ini_path = os.path.join(current_dir, "../../alembic.ini") + + async def upgrade(self, target: str): + self.action = "upgrade" + self.target = target + await self._run_alembic_command() + + async def downgrade(self, target: str): + self.action = "downgrade" + self.target = target + await self._run_alembic_command() + + async def _run_alembic_command(self): + """ + Args: + action (str): "upgrade" or "downgrade" + target (str): "base", "head" or revision ID + """ + + def _run_command(connection): + alembic_cfg = Config(self.alembic_ini_path) + alembic_cfg.attributes["connection"] = connection + if self.action == "upgrade": + command.upgrade(alembic_cfg, self.target) + elif self.action == "downgrade": + command.downgrade(alembic_cfg, self.target) + else: + raise Exception(f"Unknown MigrationRunner action '{self.action}'") + + async_engine = create_async_engine(config["DB_CONNECTION_STRING"], echo=True) + async with async_engine.begin() as conn: + await conn.run_sync(_run_command) diff --git a/tests/migrations/test_migration_e1886270d9d2.py b/tests/migrations/test_migration_e1886270d9d2.py new file mode 100644 index 0000000..671a8d6 --- /dev/null +++ b/tests/migrations/test_migration_e1886270d9d2.py @@ -0,0 +1,29 @@ +import pytest +from sqlalchemy import exc +from sqlalchemy.future import select + +from gen3workflow.models import SystemKey +from tests.migrations.migration_utils import MigrationRunner + + +@pytest.mark.asyncio +async def test_e1886270d9d2_upgrade(session): + # state before the migration + migration_runner = MigrationRunner() + await migration_runner.downgrade("base") + + # the system_key table should not exist + query = select(SystemKey) + with pytest.raises( + exc.ProgrammingError, match='relation "system_key" does not exist' + ): + result = await session.execute(query) + await session.rollback() + + # run the migration + await migration_runner.upgrade("e1886270d9d2") + + # the system_key table should now exist + query = select(SystemKey) + result = await session.execute(query) + assert list(result.scalars().all()) == [] diff --git a/tests/migrations/test_migrations.py b/tests/migrations/test_migrations.py new file mode 100644 index 0000000..79469b0 --- /dev/null +++ b/tests/migrations/test_migrations.py @@ -0,0 +1,39 @@ +import os + + +CURRENT_DIR = os.path.dirname(os.path.realpath(__file__)) + + +def test_all_migrations_have_tests(): + """ + Check that all the database migrations have corresponding unit tests. The check is based on the + assumption that each migration is under `migrations/versions/_.py` + and has a corresponding test file at `tests/migrations/test_migration_.py`. + """ + # get the list of migrations that do have tests + tests_file = [ + f + for f in os.listdir(CURRENT_DIR) + if os.path.isfile(os.path.join(CURRENT_DIR, f)) + ] + migrations_with_tests = set() + for test_file in tests_file: + try: + migrations_with_tests.add( + test_file.split(".py")[0].split("test_migration_")[1] + ) + except Exception: + pass + + # get the list of existing DB migrations, and check if they have corresponding tests + migrations_path = os.path.join(CURRENT_DIR, "../../migrations/versions") + migration_files = [ + f + for f in os.listdir(migrations_path) + if os.path.isfile(os.path.join(migrations_path, f)) + ] + for migration_file in migration_files: + revision_id = migration_file.split(".py")[0].split("_")[0] + assert ( + revision_id in migrations_with_tests + ), f"Expected migration '{revision_id}' to have a corresponding test file at '{CURRENT_DIR}/test_migration_{revision_id}.py'" diff --git a/tests/test.sh b/tests/test.sh deleted file mode 100755 index ba474d4..0000000 --- a/tests/test.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -poetry run pytest -vv --cov=gen3workflow --cov-report term-missing:skip-covered --cov-report xml tests diff --git a/tests/test_misc.py b/tests/test_misc.py index ece664d..83e292f 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -1,6 +1,6 @@ import pytest -from gen3workflow.aws_utils import get_iam_user_name +from gen3workflow.aws_utils import get_safe_name_from_user_id from gen3workflow.config import config @@ -11,21 +11,21 @@ def reset_config_hostname(): config["HOSTNAME"] = original_hostname -def test_get_iam_user_name(reset_config_hostname): +def test_get_safe_name_from_user_id(reset_config_hostname): user_id = "asdfgh" # test a hostname with a `.`; it should be replaced by a `-` config["HOSTNAME"] = "qwert.qwert" escaped_shortened_hostname = "qwert-qwert" - iam_user_id = get_iam_user_name(user_id) - assert len(iam_user_id) < 64 - assert iam_user_id == f"gen3wf-{escaped_shortened_hostname}-{user_id}" + safe_name = get_safe_name_from_user_id(user_id) + assert len(safe_name) < 63 + assert safe_name == f"gen3wf-{escaped_shortened_hostname}-{user_id}" - # test with a hostname that would result in a name longer than the max (64 chars) + # test with a hostname that would result in a name longer than the max (63 chars) config["HOSTNAME"] = ( "qwertqwert.qwertqwert.qwertqwert.qwertqwert.qwertqwert.qwertqwert" ) - escaped_shortened_hostname = "qwertqwert-qwertqwert-qwertqwert-qwertqwert-qwertq" - iam_user_id = get_iam_user_name(user_id) - assert len(iam_user_id) == 64 - assert iam_user_id == f"gen3wf-{escaped_shortened_hostname}-{user_id}" + escaped_shortened_hostname = "qwertqwert-qwertqwert-qwertqwert-qwertqwert-qwert" + safe_name = get_safe_name_from_user_id(user_id) + assert len(safe_name) == 63 + assert safe_name == f"gen3wf-{escaped_shortened_hostname}-{user_id}" diff --git a/tests/test_storage.py b/tests/test_storage.py index 8261e54..2f503cf 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -6,7 +6,7 @@ from conftest import TEST_USER_ID from gen3workflow import aws_utils from gen3workflow.config import config -from gen3workflow.aws_utils import get_iam_user_name +from gen3workflow.aws_utils import get_safe_name_from_user_id @pytest.mark.asyncio @@ -117,7 +117,8 @@ async def test_list_user_keys_status(client, access_token_patcher): # deactivate the 2nd key access_key = boto3.resource("iam").AccessKey( - get_iam_user_name(TEST_USER_ID), keys[1]["aws_key_id"] + user_name=get_safe_name_from_user_id(TEST_USER_ID), + id=keys[1]["aws_key_id"], ) access_key.deactivate() @@ -193,17 +194,15 @@ async def test_delete_non_existent_key(client, access_token_patcher): @pytest.mark.asyncio async def test_storage_info(client, access_token_patcher): - """ - TODO - """ with mock_aws(): aws_utils.iam_client = boto3.client("iam") + expected_bucket_name = f"gen3wf-{config['HOSTNAME']}-{TEST_USER_ID}" res = await client.get("/storage/info", headers={"Authorization": "bearer 123"}) assert res.status_code == 200, res.text storage_info = res.json() assert storage_info == { - "bucket": "TODO", - "workdir": "s3://TODO/ga4gh-tes", - "region": "us-east-1", + "bucket": expected_bucket_name, + "workdir": f"s3://{expected_bucket_name}/ga4gh-tes", + "region": config["USER_BUCKETS_REGION"], }