diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..f6cc4e8 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,3 @@ +[run] +omit = + app/migrations/* diff --git a/src/.env.alembic b/.env.alembic similarity index 71% rename from src/.env.alembic rename to .env.alembic index a53af2a..8691557 100644 --- a/src/.env.alembic +++ b/.env.alembic @@ -1,5 +1,5 @@ CHROME_APP_IDS='["1234567890"]' ALLOWED_ORIGINS='["allowed"]' BLOCKED_EMAILS='[]' -DATABASE_PATH="sqlite:///./auto-archiver.db" +DATABASE_PATH="sqlite:///./database/auto-archiver.db" API_BEARER_TOKEN=THIS_API_TOKEN_SHOULD_NEVER_BE_USED \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ef3935a --- /dev/null +++ b/.env.example @@ -0,0 +1,38 @@ +# main settings +USER_GROUPS_FILENAME=app/user-groups.yaml +# database +DATABASE_PATH="sqlite:///./database/auto-archiver.db" +DATABASE_QUERY_LIMIT=100 + +# security settings +API_BEARER_TOKEN=TODO-MODIFY-THIS-API-TOKEN +ALLOWED_ORIGINS='["http://localhost:8000","http://localhost:8004","http://localhost:8081","https://auto-archiver.bellingcat.com"]' +CHROME_APP_IDS='[PROJECT_ID.apps.googleusercontent.com"]' +BLOCKED_EMAILS='[]' +# redis configuration +REDIS_PASSWORD=TODO-MODIFY-THIS-REDIS-PASSWORD +REDIS_HOSTNAME="localhost" + +# cronjobs management, enable as needed +CRON_ARCHIVE_SHEETS=true +CRON_DELETE_STALE_SHEETS=true +DELETE_STALE_SHEETS_DAYS=7 +CRON_DELETE_SCHEDULED_ARCHIVES=false +DELETE_SCHEDULED_ARCHIVES_CHECK_EVERY_N_DAYS=14 + +# observability for prometheus +REPEAT_COUNT_METRICS_SECONDS=30 + +# mail service settings, if you want to email users +MAIL_FROM="noreply@auto-archiver.com" +MAIL_FROM_NAME="My Auto Archiver deployment" +MAIL_USERNAME="USERNAME" +MAIL_PASSWORD="PASSWORD" +MAIL_SERVER="mail.server.com" +MAIL_PORT=587 +MAIL_STARTTLS=False +MAIL_SSL_TLS=True + + +# celery workers config +CONCURRENCY=2 \ No newline at end of file diff --git a/src/.env.test b/.env.test similarity index 73% rename from src/.env.test rename to .env.test index 5e5ea24..32318f0 100644 --- a/src/.env.test +++ b/.env.test @@ -5,5 +5,4 @@ BLOCKED_EMAILS='["blocked@example.com"]' DATABASE_PATH="sqlite:///auto-archiver.test.db" API_BEARER_TOKEN=this_is_the_test_api_token -USER_GROUPS_FILENAME=tests/user-groups.test.yaml -SHEET_ORCHESTRATION_YAML=tests/orchestration.test.yaml \ No newline at end of file +USER_GROUPS_FILENAME=app/tests/user-groups.test.yaml \ No newline at end of file diff --git a/.example.env b/.example.env deleted file mode 100644 index e8970b2..0000000 --- a/.example.env +++ /dev/null @@ -1 +0,0 @@ -REDIS_PASSWORD=TODO \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf9ca3b..9b63544 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,25 +21,24 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: '3.10' - - name: Install pipenv - run: pip install pipenv - working-directory: src + - name: Install Poetry + run: pipx install poetry - name: Install dependencies - run: pipenv install --dev - working-directory: src + run: poetry install --no-interaction --with dev + + - name: Set dev environment variable + run: echo "ENVIRONMENT_FILE=.env.test" >> $GITHUB_ENV - name: Run tests with coverage - run: PYTHONPATH=. PIPENV_DOTENV_LOCATION=.env.test pipenv run coverage run -m pytest -v --color=yes tests/ - working-directory: src + run: poetry run coverage run -m pytest -v -ra --color=yes app/tests/ - name: Report coverage - run: pipenv run coverage report - working-directory: src \ No newline at end of file + run: poetry run coverage report \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1df91d8..562885d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,26 +1,32 @@ +user-groups.dev.yaml +user-groups.yaml orchestration.yaml my-archives *.pyc .DS_Store -secrets +secrets/* *.log -__pycache -.pytest_cach +__pycache__ +.pytest_cache .env .env.dev .env.prod *.db redis/data/* .ipynb_checkpoints* -src/user-groups.yaml -src/user-groups.dev.yaml +app/user-groups.yaml +app/user-groups.dev.yaml wit* -src/crawls +app/crawls .coverage -.pytest_cache/* +.pytest_cache/ htmlcov local_archive local_archive_test *db-wal *db-shm -copy-files.sh \ No newline at end of file +copy-files.sh +temp/ +.python-version +orchestration2.yaml +database \ No newline at end of file diff --git a/LICENSE b/LICENSE index f593c11..e10dcd9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Stichting Bellingcat +Copyright (c) 2025 Stichting Bellingcat Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile index e4002d9..2fd462c 100644 --- a/Makefile +++ b/Makefile @@ -3,15 +3,20 @@ clean-dev: docker compose -f docker-compose.yml -f docker-compose.dev.yml down --volumes --remove-orphans dev: - docker compose -f docker-compose.yml -f docker-compose.dev.yml build - docker compose -f docker-compose.yml -f docker-compose.dev.yml up --remove-orphans + docker compose --env-file .env.dev -f docker-compose.yml -f docker-compose.dev.yml build + docker compose --env-file .env.dev -f docker-compose.yml -f docker-compose.dev.yml up --remove-orphans + + +dev-redis-only: + docker compose --env-file .env.dev -f docker-compose.yml -f docker-compose.dev.yml build redis + docker compose --env-file .env.dev -f docker-compose.yml -f docker-compose.dev.yml up --remove-orphans redis stop-dev: docker compose -f docker-compose.yml -f docker-compose.dev.yml down --volumes prod: - docker compose build - docker compose up -d --remove-orphans + docker compose --env-file .env.prod build + docker compose --env-file .env.prod up -d --remove-orphans docker buildx prune --keep-storage 20gb -f docker image prune -f docker system df diff --git a/README.md b/README.md index 5cd1ee6..549e1b4 100644 --- a/README.md +++ b/README.md @@ -1,89 +1,120 @@ # Auto Archiver API -An api that uses celery workers to process URL archive requests via [bellingcat/auto-archiver](https://github.com/bellingcat/auto-archiver), it allows authentication via Google OAuth Apps and enables CORS, everything runs on docker but development can be done without docker (except for redis). +[![CI](https://github.com/bellingcat/auto-archiver-api/workflows/CI/badge.svg)](https://github.com/bellingcat/auto-archiver-api/actions/workflows/ci.yaml) + +A web API that uses celery workers to process URL archive requests via [bellingcat/auto-archiver](https://github.com/bellingcat/auto-archiver), it allows authentication via Google OAuth Apps and enables CORS, everything runs on docker but development can be done without docker (except for redis). + +![image](https://github.com/user-attachments/assets/905d697d-b83e-437b-87d1-cc86d3c8d8bf) + +## setup +To properly set up the API you need to install `docker` and to edit 3 files: +1. a `.env.prod` and `.env.dev` to configure the API, stays at the root level +2. a `user-groups.yaml` to manage user permissions + 1. note that all local files referenced in `user-groups.yaml` and any orchestration.yaml files should be relative to the home directory so if your service account is in `secrets/orchestration.yaml` use that path and not just `orchestration.yaml`. + +Do not commit those files, they are .gitignored by default. +We also advise you to keep any sensitive files in the `secrets/` folder which is pinned and gitignored. -## Development -http://localhost:8004 +We have examples for both of those files (`.env.example` and `user-groups.example.yaml`), and here's how to set them up whether you're in development or production: -TODO: update .env file instructions, should use .env.prod and .env.dev and only use .env for always overwriting dev/prod settings. +### setup for DEVELOPMENT +```bash +# copy and modify the .env.dev file according to your needs +cp .env.example .env.dev +# copy the user-groups.example.yaml and modify it accordingly +cp user-groups.example.yaml user-groups.dev.yaml +# run the APP, make sure VPNs are off +make dev +# check it's running by calling the health endpoint +curl 'http://localhost:8004/health' +# > {"status":"ok"} +``` +now go to http://localhost:8004/docs#/ and you should see the API documentation -requires `src/.env` +### setup for PRODUCTION +```bash +# copy and modify the .env.prod file according to your needs +cp .env.example .env.prod +# copy the user-groups.example.yaml and modify it accordingly +cp user-groups.example.yaml user-groups.yaml +# deploy the app +make prod +# check it's running by calling the health endpoint +curl 'http://localhost:8004/health' +# > {"status":"ok"} +``` +now go to http://localhost:8004/docs#/ and you should see the API documentation + +## User, Domains, Groups, and permissions management +there are 2 ways to access the API +1. via an API token which has full control/privileges to archive/search +2. via a Google Auth token which goes through the user access model + +#### User access model +The permissions are defined solely via the `user-groups.yaml` file +- users belong to groups which determine their access level/quotas/orchestration setup + - users are assigned to groups explicitly (via email) + - users are assigned to groups implicitly (via email domains) as domains can be associated to groups + - users that are not explicitly or implicitly in the system belong to the `default` group, restrict their permissions if you do not wish them to be able to search/archive + - if a user is assigned to one group which is not explicitly defined, a warning will be thrown, it may be necessary to do that if you discontinue a given group but the database still has entries for it and so +- groups determine + - which orchestrator to use for single URL archives and for spreadsheet archives see [GroupPermissions](app/shared/user_groups.py) + - a set of permissions + - `read` can be [`all`], [] or a comma separated list of group names, meaning people in this group can access either all, none, or those belonging to explicitly listed groups. + - the group itself must be included in the list, otherwise the user cannot search archives of that group + - `read_public` a boolean that enables the user to search public archives + - `archive_url` a boolean that enables the user to archive links in this group + - `archive_sheet` a boolean that enables the user to archive spreadsheets + - `manually_trigger_sheet` a boolean that enables the user to manually trigger a sheet archive for sheets in this group + - `sheet_frequency` a list of options for the sheet archiving frequency, currently max permissions is `["hourly", "daily"]` + - `max_sheets` defines the maximum amount of spreadsheets someone can have in total (`-1` means no limit) + - `max_archive_lifespan_months` defines the lifespan of an archive before being deleted from S3, users will be notified 1 month in advance with instructions to download TODO + - `max_monthly_urls` how many total URLs someone can archive per month (`-1` means no limit) + - `max_monthly_mbs` how many MBs of data someone can archive per month (`-1` means no limit) + - `priority` one of `high` or `low`, this will be used to give archiving priority + - group names are all lower-case + + +## development of web/worker without docker -cd /src -* console 1 - `docker compose up redis` optionally add `web` if not running uvicorn locally -* console 2 - `pipenv shell` + `celery worker --app=worker.celery --loglevel=info --logfile=logs/celery_dev.log` - * `celery --app=worker.celery worker --loglevel=info --logfile=logs/celery_dev.log` celery 5 - * or with watchdog for dev auto-reload `watchmedo auto-restart -d ./ -- celery --app=worker.celery worker --loglevel=info --logfile=logs/celery_dev.log` -* console 3 - `pipenv shell` + `uvicorn main:app --host 0.0.0.0 --reload` -orchestration must be from the console(?) -* turn off VPNs if connection to docker is not working +We advise you to use `make prod` but you can also spin up redis and run the API (uvicorn) and worker (celery) individually like so: +* console 1 - `make dev-redis-only` to spin up redis, turn off any VPNs +* console 2 - `export ENVIRONMENT_FILE=.env.dev` then `poetry run celery --app=app.worker.main.celery worker --loglevel=debug --logfile=/aa-api/logs/celery.log -Q high_priority,low_priority --concurrency=1` + * or with watchdog for dev auto-reload `watchmedo auto-restart --patterns="*.py" --recursive --ignore-directories -- celery -- --app=app.worker.main.celery worker --loglevel=debug --logfile=/aa-api/logs/celery.log -Q high_priority,low_priority --concurrency=1` +* console 3 - `export ENVIRONMENT_FILE=.env.dev` then `poetry run uvicorn main:app --host 0.0.0.0 --reload` -## User management -Copy [example.user-groups.yaml](src/example.user-groups.yaml) into a new file and set the environment variable `USER_GROUPS_FILENAME` to that filename (defaults to `user-groups.yaml`). - -This file contains 2 parts user-groups specifications. Each user can archive URLs publicly, privately, or privately for a group so long as they are declared as part of that group. In the example bellow `email1` has 2 groups while `email3` has none. -```yaml -users: - email1@example.com: - - group1 - - group2 - email2@example.com: - - group2 - email3@example-no-group.com: -``` - -Auto-archiver orchestrator files configurations. For each archiving task an orchestrator is chosen, either from a specified group (if group-level visibility) or the first group the user is assigned to in the above file or the `default` orchestration file which is a required config. -```yaml -orchestrators: - group1: secrets/orchestration-group1.yaml - group2: secrets/orchestration-group2.yaml - default: secrets/orchestration-default:orchestration.yaml -``` ## Database migrations check https://alembic.sqlalchemy.org/en/latest/tutorial.html#the-migration-environment - -* create migrations with `alembic revision -m "create account table"` -* if running in the normal pipenv environment use `PIPENV_DOTENV_LOCATION=.env.alembic pipenv run` followed by: - * migrate to most recent with `alembic upgrade head` - * downgrade with `alembic downgrade -1` +```bash +# set the env variables +export ENVIRONMENT_FILE=.env.alembic +# create a new migration with description in app/migrations +poetry run alembic revision -m "create account table" +# perform all migrations +poetry run alembic upgrade head +# downgrade by one migration +poetry run alembic downgrade -1 +``` ## Release -Update `main.py:VERSION`. +Update the version in [config.py](app/web/config.py) -Copy `.env` and `src/.env` to deployment, along with the contents of `secrets/` including `secrets/orchestration.yaml`. +Make sure environment and user-groups files are up to date. Then `make prod`. -#### updating packages/app/access -If pipenv packages are updated: `make prod` to build images with new packages. - -New users should be added to the `src/.env` file `ALLOWED_EMAILS` prop. - -Run `pipenv update auto-archiver` inside `src` to update the auto-archiver version being used, then test with `make dev`. - - -```bash -# CALL /sheet POST endpoint -curl -XPOST -H "Authorization: Bearer GOOGLE_OAUTH_TOKEN" -H "Content-type: application/json" -d '{"sheet_id": "SHEET_ID", "header": 1}' 'http://localhost:8004/sheet' - -``` - ### Testing ```bash -# can be done from top level but let's do it from the src folder for consistency with CI etc -cd src +# set the testing environment variables +export ENVIRONMENT_FILE=.env.test # run tests and generate coverage -PYTHONPATH=. PIPENV_DOTENV_LOCATION=.env.test pipenv run coverage run -m pytest -vv --disable-warnings --color=yes tests/ && pipenv run coverage html - +poetry run coverage run -m pytest -vv --disable-warnings --color=yes app/tests/ # get coverage report in command line -pipenv run coverage report - -# get coverage HTML -pipenv run coverage html - -# > open/run server on htmlcov/index.html to navigate through line coverage -``` \ No newline at end of file +poetry run coverage report +# get coverage report in HTML format +poetry run coverage html +``` diff --git a/src/alembic.ini b/alembic.ini similarity index 98% rename from src/alembic.ini rename to alembic.ini index 62db386..30d7030 100644 --- a/src/alembic.ini +++ b/alembic.ini @@ -2,7 +2,7 @@ [alembic] # path to migration scripts -script_location = migrations +script_location = app/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/src/migrations/env.py b/app/migrations/env.py similarity index 97% rename from src/migrations/env.py rename to app/migrations/env.py index abd7c6e..870ef18 100644 --- a/src/migrations/env.py +++ b/app/migrations/env.py @@ -1,11 +1,10 @@ from logging.config import fileConfig -import os from sqlalchemy import engine_from_config from sqlalchemy import pool from alembic import context -from shared.settings import get_settings +from app.shared.settings import get_settings # this is the Alembic Config object, which provides # access to the values within the .ini file in use. diff --git a/src/migrations/script.py.mako b/app/migrations/script.py.mako similarity index 100% rename from src/migrations/script.py.mako rename to app/migrations/script.py.mako diff --git a/src/logs/.gitkeep b/app/migrations/versions/.gitkeep similarity index 100% rename from src/logs/.gitkeep rename to app/migrations/versions/.gitkeep diff --git a/app/migrations/versions/02b2f6d17ed0_create_archives_store_until_column.py b/app/migrations/versions/02b2f6d17ed0_create_archives_store_until_column.py new file mode 100644 index 0000000..d00fa2c --- /dev/null +++ b/app/migrations/versions/02b2f6d17ed0_create_archives_store_until_column.py @@ -0,0 +1,34 @@ +"""create archives.store_until column + +Revision ID: 02b2f6d17ed0 +Revises: 1636724ec4b1 +Create Date: 2025-02-08 15:22:20.392522 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '02b2f6d17ed0' +down_revision = '1636724ec4b1' +branch_labels = None +depends_on = None +STORE_UNTIL_COL = "store_until" + + +def upgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + columns = [col['name'] for col in inspector.get_columns('archives')] + + if STORE_UNTIL_COL not in columns: + op.add_column('archives', sa.Column(STORE_UNTIL_COL, sa.DateTime(), nullable=True, default=None)) + + +def downgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + columns = [col['name'] for col in inspector.get_columns('archives')] + if STORE_UNTIL_COL in columns: + op.drop_column('archives', STORE_UNTIL_COL) diff --git a/app/migrations/versions/1636724ec4b1_rename_sheets_last_archived_col.py b/app/migrations/versions/1636724ec4b1_rename_sheets_last_archived_col.py new file mode 100644 index 0000000..6c109f3 --- /dev/null +++ b/app/migrations/versions/1636724ec4b1_rename_sheets_last_archived_col.py @@ -0,0 +1,32 @@ +"""rename sheets last_archived col + +Revision ID: 1636724ec4b1 +Revises: a23aaf3ae930 +Create Date: 2025-02-05 19:19:01.984396 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '1636724ec4b1' +down_revision = 'a23aaf3ae930' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + columns = [col['name'] for col in inspector.get_columns('sheets')] + if 'last_archived_at' in columns: + op.alter_column('sheets', 'last_archived_at', new_column_name='last_url_archived_at') + + +def downgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + columns = [col['name'] for col in inspector.get_columns('sheets')] + if 'last_url_archived_at' in columns: + op.alter_column('sheets', 'last_url_archived_at', new_column_name='last_archived_at') diff --git a/app/migrations/versions/63ac79df4ad0_add_new_service_account_email_column_to_.py b/app/migrations/versions/63ac79df4ad0_add_new_service_account_email_column_to_.py new file mode 100644 index 0000000..7067746 --- /dev/null +++ b/app/migrations/versions/63ac79df4ad0_add_new_service_account_email_column_to_.py @@ -0,0 +1,36 @@ +"""add new service_account_email column to groups table + +Revision ID: 63ac79df4ad0 +Revises: 02b2f6d17ed0 +Create Date: 2025-02-11 21:53:23.293274 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '63ac79df4ad0' +down_revision = '02b2f6d17ed0' +branch_labels = None +depends_on = None + +NEW_COL = "service_account_email" +TABLE = "groups" + + +def upgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + columns = [col['name'] for col in inspector.get_columns(TABLE)] + + if NEW_COL not in columns: + op.add_column(TABLE, sa.Column(NEW_COL, sa.String, nullable=True, default=None)) + + +def downgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + columns = [col['name'] for col in inspector.get_columns(TABLE)] + if NEW_COL in columns: + op.drop_column(TABLE, NEW_COL) diff --git a/app/migrations/versions/89121d2c96d8_add_sheet_id_to_archive_table.py b/app/migrations/versions/89121d2c96d8_add_sheet_id_to_archive_table.py new file mode 100644 index 0000000..3011cf6 --- /dev/null +++ b/app/migrations/versions/89121d2c96d8_add_sheet_id_to_archive_table.py @@ -0,0 +1,42 @@ +"""add sheet_id to archive table + +Revision ID: 89121d2c96d8 +Revises: fa012ec405b8 +Create Date: 2024-11-04 11:12:30.237299 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.engine.reflection import Inspector + + +# revision identifiers, used by Alembic. +revision = '89121d2c96d8' +down_revision = 'fa012ec405b8' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + columns = [col['name'] for col in inspector.get_columns('archives')] + + if 'sheet_id' not in columns: + with op.batch_alter_table('archives') as batch_op: + batch_op.add_column(sa.Column('sheet_id', sa.String(), nullable=True, default=None)) + batch_op.create_foreign_key('fk_sheet_id', 'sheets', ['sheet_id'], ['id']) + + +def downgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + foreign_keys = [fk['name'] for fk in inspector.get_foreign_keys('archives')] + columns = [col['name'] for col in inspector.get_columns('archives')] + + with op.batch_alter_table('archives') as batch_op: + if 'fk_sheet_id' in foreign_keys: + batch_op.drop_constraint('fk_sheet_id', type_='foreignkey') + + if 'sheet_id' in columns: + batch_op.drop_column('sheet_id') diff --git a/src/migrations/versions/9369a264945b_modify_archive_url_to_have_uuid_id_.py b/app/migrations/versions/9369a264945b_modify_archive_url_to_have_uuid_id_.py similarity index 100% rename from src/migrations/versions/9369a264945b_modify_archive_url_to_have_uuid_id_.py rename to app/migrations/versions/9369a264945b_modify_archive_url_to_have_uuid_id_.py diff --git a/src/migrations/versions/93a611e4c066_vacuum_database_if_there_s_enough_space.py b/app/migrations/versions/93a611e4c066_vacuum_database_if_there_s_enough_space.py similarity index 100% rename from src/migrations/versions/93a611e4c066_vacuum_database_if_there_s_enough_space.py rename to app/migrations/versions/93a611e4c066_vacuum_database_if_there_s_enough_space.py diff --git a/app/migrations/versions/a23aaf3ae930_drop_active_column.py b/app/migrations/versions/a23aaf3ae930_drop_active_column.py new file mode 100644 index 0000000..912f408 --- /dev/null +++ b/app/migrations/versions/a23aaf3ae930_drop_active_column.py @@ -0,0 +1,34 @@ +"""drop active column + +Revision ID: a23aaf3ae930 +Revises: 89121d2c96d8 +Create Date: 2025-02-04 12:19:20.753570 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'a23aaf3ae930' +down_revision = '89121d2c96d8' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + columns = [col['name'] for col in inspector.get_columns('users')] + + if 'is_active' in columns: + op.drop_column('users', 'is_active') + + +def downgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + columns = [col['name'] for col in inspector.get_columns('users')] + + if 'is_active' not in columns: + op.add_column('users', sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.false())) diff --git a/src/migrations/versions/fa012ec405b8_add_columns_to_groups_table.py b/app/migrations/versions/fa012ec405b8_add_columns_to_groups_table.py similarity index 74% rename from src/migrations/versions/fa012ec405b8_add_columns_to_groups_table.py rename to app/migrations/versions/fa012ec405b8_add_columns_to_groups_table.py index da77d41..f0577ea 100644 --- a/src/migrations/versions/fa012ec405b8_add_columns_to_groups_table.py +++ b/app/migrations/versions/fa012ec405b8_add_columns_to_groups_table.py @@ -19,7 +19,7 @@ depends_on = None def upgrade() -> None: conn = op.get_bind() - inspector = Inspector.from_engine(conn) + inspector = sa.inspect(conn) columns = [col['name'] for col in inspector.get_columns('groups')] if 'description' not in columns: @@ -35,8 +35,11 @@ def upgrade() -> None: def downgrade() -> None: - op.drop_column('groups', 'description') - op.drop_column('groups', 'orchestrator') - op.drop_column('groups', 'orchestrator_sheet') - op.drop_column('groups', 'permissions') - op.drop_column('groups', 'domains') + conn = op.get_bind() + inspector = sa.inspect(conn) + columns = [col['name'] for col in inspector.get_columns('groups')] + + column_names = ['description', 'orchestrator', 'orchestrator_sheet', 'permissions', 'domains'] + for column_name in column_names: + if column_name in columns: + op.drop_column('groups', column_name) diff --git a/src/core/__init__.py b/app/shared/__init__.py similarity index 100% rename from src/core/__init__.py rename to app/shared/__init__.py diff --git a/app/shared/aa_utils.py b/app/shared/aa_utils.py new file mode 100644 index 0000000..393a975 --- /dev/null +++ b/app/shared/aa_utils.py @@ -0,0 +1,32 @@ +# TODO: code in this file should eventually be moved to the auto-archiver code base + +from typing import List +from loguru import logger +from auto_archiver.core import Media, Metadata + +from app.shared.db import models + +def get_all_urls(result: Metadata) -> List[models.ArchiveUrl]: + db_urls = [] + for m in result.media: + for i, url in enumerate(m.urls): db_urls.append(models.ArchiveUrl(url=url, key=m.get("id", f"media_{i}"))) + for k, prop in m.properties.items(): + if prop_converted := convert_if_media(prop): + for i, url in enumerate(prop_converted.urls): db_urls.append(models.ArchiveUrl(url=url, key=prop_converted.get("id", f"{k}_{i}"))) + if isinstance(prop, list): + for i, prop_media in enumerate(prop): + if prop_media := convert_if_media(prop_media): + for j, url in enumerate(prop_media.urls): + db_urls.append(models.ArchiveUrl(url=url, key=prop_media.get("id", f"{k}{prop_media.key}_{i}.{j}"))) + return db_urls + + + +def convert_if_media(media): + if isinstance(media, Media): return media + elif isinstance(media, dict): + try: return Media.from_dict(media) + except Exception as e: + logger.debug(f"error parsing {media} : {e}") + return False + diff --git a/app/shared/business_logic.py b/app/shared/business_logic.py new file mode 100644 index 0000000..ffd9cb2 --- /dev/null +++ b/app/shared/business_logic.py @@ -0,0 +1,16 @@ +# TODO: temporary file for this code, maybe other code belongs here, maybe not. do decide + + +import datetime +from sqlalchemy.orm import Session + +from app.shared.db import worker_crud + + +def get_store_archive_until(db: Session, group_id: str) -> datetime.datetime: + group = worker_crud.get_group(db, group_id) + assert group, f"Group {group_id} not found." + max_lifespan = group.permissions.get("max_archive_lifespan_months", -1) + if max_lifespan == -1: return None + + return datetime.datetime.now() + datetime.timedelta(days=30 * max_lifespan) diff --git a/src/utils/__init__.py b/app/shared/db/__init__.py similarity index 100% rename from src/utils/__init__.py rename to app/shared/db/__init__.py diff --git a/app/shared/db/database.py b/app/shared/db/database.py new file mode 100644 index 0000000..171b97b --- /dev/null +++ b/app/shared/db/database.py @@ -0,0 +1,74 @@ +from functools import lru_cache +from sqlalchemy import Engine, create_engine, event, text +from sqlalchemy.orm import sessionmaker +from contextlib import asynccontextmanager, contextmanager +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, AsyncEngine, async_sessionmaker + +from app.shared.settings import get_settings + + +@lru_cache +def make_engine(database_url: str): + engine = create_engine( + database_url, + connect_args={"check_same_thread": False}, + pool_size=15, # Increase pool size + max_overflow=20, # Allow more temporary connections + pool_recycle=1800 # Recycle connections every 30 minutes + ) + + @event.listens_for(engine, "connect") + def set_sqlite_pragma(conn, _) -> None: + cursor = conn.cursor() + cursor.execute("PRAGMA journal_mode=WAL") + cursor.close() + + return engine + + +def make_session_local(engine: Engine): + session_local = sessionmaker(autocommit=False, autoflush=False, bind=engine) + return session_local + + +@contextmanager +def get_db(): + session = make_session_local(make_engine(get_settings().DATABASE_PATH))() + try: yield session + finally: session.close() + + +def get_db_dependency(): + # to use with Depends and ensure proper session closing + with get_db() as db: + yield db + + +def wal_checkpoint(): + # WAL checkpointing, make sure the .sqlite file receives the latest changes + # to be called at startup as it halts writes + with get_db() as db: + db.execute(text("PRAGMA wal_checkpoint(TRUNCATE)")) + + +# ASYNC connections +async def make_async_engine(database_url: str) -> AsyncEngine: + engine = create_async_engine(database_url, connect_args={"check_same_thread": False}) + + async with engine.begin() as conn: + await conn.run_sync(lambda sync_conn: sync_conn.execute(text("PRAGMA journal_mode=WAL;"))) + + return engine + + +async def make_async_session_local(engine: AsyncEngine) -> AsyncSession: + return async_sessionmaker(engine, expire_on_commit=False, autoflush=False, autocommit=False) + + +@asynccontextmanager +async def get_db_async(): + engine = await make_async_engine(get_settings().ASYNC_DATABASE_PATH) + async_session = await make_async_session_local(engine) + async with async_session() as session: + try: yield session + finally: await engine.dispose() diff --git a/src/db/models.py b/app/shared/db/models.py similarity index 63% rename from src/db/models.py rename to app/shared/db/models.py index 193adba..1736224 100644 --- a/src/db/models.py +++ b/app/shared/db/models.py @@ -6,9 +6,11 @@ import uuid Base = declarative_base() + def generate_uuid(): return str(uuid.uuid4()) + # many to many association tables association_table_archive_tags = Table( "mtm_archives_tags", @@ -23,6 +25,7 @@ association_table_user_groups = Table( Column("group_id", ForeignKey("groups.id")), ) + # data model tables class Archive(Base): __tablename__ = "archives" @@ -30,18 +33,22 @@ class Archive(Base): id = Column(String, primary_key=True, index=True) url = Column(String, index=True) result = Column(JSON, default=None) - public = Column(Boolean, default=True) # if public=false, access to group and author + public = Column(Boolean, default=True) # if public=false, access by group and author deleted = Column(Boolean, default=False) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + store_until = Column(DateTime(timezone=True), default=None) group_id = Column(String, ForeignKey("groups.id"), default=None) author_id = Column(String, ForeignKey("users.email")) + sheet_id = Column(String, ForeignKey("sheets.id"), default=None) tags = relationship("Tag", back_populates="archives", secondary=association_table_archive_tags) group = relationship("Group", back_populates="archives") author = relationship("User", back_populates="archives") urls = relationship("ArchiveUrl", back_populates="archive") + sheet = relationship("Sheet", back_populates="archives") + class ArchiveUrl(Base): __tablename__ = "archive_urls" @@ -61,15 +68,17 @@ class Tag(Base): archives = relationship("Archive", back_populates="tags", secondary=association_table_archive_tags) + class User(Base): __tablename__ = "users" email = Column(String, primary_key=True, index=True) - is_active = Column(Boolean, default=False) archives = relationship("Archive", back_populates="author") + sheets = relationship("Sheet", back_populates="author") groups = relationship("Group", back_populates="users", secondary=association_table_user_groups) + class Group(Base): __tablename__ = "groups" @@ -77,8 +86,29 @@ class Group(Base): description = Column(String, default=None) orchestrator = Column(String, default=None) orchestrator_sheet = Column(String, default=None) - permissions = Column(JSON, default=None) + permissions = Column(JSON, default={}) + service_account_email = Column(String, default=None) domains = Column(JSON, default=[]) archives = relationship("Archive", back_populates="group") - users = relationship("User", back_populates="groups", secondary=association_table_user_groups) \ No newline at end of file + sheets = relationship("Sheet", back_populates="group") + users = relationship("User", back_populates="groups", secondary=association_table_user_groups) + + +class Sheet(Base): + __tablename__ = "sheets" + + id = Column(String, primary_key=True, index=True, doc="Google Sheet ID") + name = Column(String, default=None) + author_id = Column(String, ForeignKey("users.email")) + group_id = Column(String, ForeignKey("groups.id"), doc="Group ID, user must be in a group to create a sheet.") + frequency = Column(String, default="daily", doc="Frequency of archiving: hourly, daily, weekly.") + # TODO: stats is not being used, consider removing + stats = Column(JSON, default={}, doc="Sheet statistics like total links, total rows, ...") + last_url_archived_at = Column(DateTime(timezone=True), server_default=func.now(), doc="Last time a new link was archived.") + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + + group = relationship("Group", back_populates="sheets") + author = relationship("User", back_populates="sheets") + archives = relationship("Archive", back_populates="sheet") diff --git a/app/shared/db/worker_crud.py b/app/shared/db/worker_crud.py new file mode 100644 index 0000000..814689a --- /dev/null +++ b/app/shared/db/worker_crud.py @@ -0,0 +1,60 @@ +from sqlalchemy.orm import Session +from datetime import datetime + +from app.shared.db import models +from app.shared import schemas + +# TODO: isolate database operations away from worker and into WEB +# ONLY WORKER +def update_sheet_last_url_archived_at(db: Session, sheet_id: str): + db_sheet = db.query(models.Sheet).filter(models.Sheet.id == sheet_id).first() + if db_sheet: + db_sheet.last_url_archived_at = datetime.now() + db.commit() + return True + return False + + +# ONLY WORKER and INTEROP + +def get_group(db: Session, group_name: str) -> models.Group: + return db.query(models.Group).filter(models.Group.id == group_name).first() + +def create_or_get_user(db: Session, author_id: str) -> models.User: + if type(author_id) == str: author_id = author_id.lower() + db_user = db.query(models.User).filter(models.User.email == author_id).first() + if not db_user: + db_user = models.User(email=author_id) + db.add(db_user) + db.commit() + db.refresh(db_user) + return db_user + + +def create_tag(db: Session, tag: str) -> models.Tag: + db_tag = db.query(models.Tag).filter(models.Tag.id == tag).first() + if not db_tag: + db_tag = models.Tag(id=tag) + db.add(db_tag) + db.commit() + db.refresh(db_tag) + return db_tag + + +def create_archive(db: Session, archive: schemas.ArchiveCreate, tags: list[models.Tag], urls: list[models.ArchiveUrl]) -> models.Archive: + db_archive = models.Archive(id=archive.id, url=archive.url, result=archive.result, public=archive.public, author_id=archive.author_id, group_id=archive.group_id, sheet_id=archive.sheet_id, store_until=archive.store_until) + db_archive.tags = tags + db_archive.urls = urls + db.add(db_archive) + db.commit() + db.refresh(db_archive) + return db_archive + + +def store_archived_url(db: Session, archive: schemas.ArchiveCreate) -> models.Archive: + # create and load user, tags, if needed + create_or_get_user(db, archive.author_id) + db_tags = [create_tag(db, tag) for tag in (archive.tags or [])] + # insert everything + db_archive = create_archive(db, archive=archive, tags=db_tags, urls=archive.urls) + return db_archive diff --git a/app/shared/log.py b/app/shared/log.py new file mode 100644 index 0000000..68587e2 --- /dev/null +++ b/app/shared/log.py @@ -0,0 +1,13 @@ +import traceback +from loguru import logger + + +# logging configurations +logger.add("logs/api_logs.log", retention="30 days") +logger.add("logs/error_logs.log", retention="30 days", level="ERROR") + + +def log_error(e: Exception, traceback_str: str = None, extra:str = ""): + if not traceback_str: traceback_str = traceback.format_exc() + if extra: extra = f"{extra}\n" + logger.error(f"{extra}{e.__class__.__name__}: {e}\n{traceback_str}") diff --git a/app/shared/schemas.py b/app/shared/schemas.py new file mode 100644 index 0000000..66119f7 --- /dev/null +++ b/app/shared/schemas.py @@ -0,0 +1,100 @@ +from typing import Annotated +from annotated_types import Len +from pydantic import BaseModel +from datetime import datetime + + +class SubmitSheet(BaseModel): + sheet_id: str | None + author_id: str | None = None + group_id: str = "default" + tags: set[str] | None = set() + +class ArchiveUrl(BaseModel): + url: str + public: bool = False + author_id: str | None + group_id: str | None + tags: set[str] | None = set() + +class ArchiveResult(BaseModel): + id: str + url: str + result: dict + created_at: datetime + store_until: datetime | None + + +class Task(BaseModel): + id: str + + +class TaskResult(Task): + status: str + result: str + + +class DeleteResponse(Task): + deleted: bool + + +class ActiveUser(BaseModel): + active: bool + + +class SheetAdd(BaseModel): + id: str + name: str + group_id: str + frequency: str + + +class SheetResponse(SheetAdd): + author_id: str + created_at: datetime + last_url_archived_at: datetime | None + + +class ArchiveTrigger(BaseModel): + author_id: str | None = None + url: Annotated[str, Len(min_length=5)] + public: bool = False + group_id: Annotated[str, Len(min_length=1)] = "default" + tags: set[str] | None = None + + +class ArchiveCreate(ArchiveTrigger): + id: str | None = None + result: dict | None = None + sheet_id: str | None = None + urls: list | None = None + store_until: datetime | None = None + + +class Archive(ArchiveCreate): + created_at: datetime + updated_at: datetime | None + deleted: bool + + model_config = {"from_attributes": True} + + +class Usage(BaseModel): + monthly_urls: int = 0 + monthly_mbs: int = 0 + total_sheets: int = 0 + + +class UsageResponse(Usage): + groups: dict[str, Usage] + + +class CelerySheetTask(BaseModel): + success: bool + sheet_id: str + time: datetime + stats: dict + + +class SubmitManualArchive(ArchiveTrigger): + result: str # should be a Metadata.to_json() diff --git a/app/shared/settings.py b/app/shared/settings.py new file mode 100644 index 0000000..d884f80 --- /dev/null +++ b/app/shared/settings.py @@ -0,0 +1,76 @@ + +from functools import lru_cache +import os +from fastapi_mail import ConnectionConfig +from pydantic_settings import BaseSettings, SettingsConfigDict +from typing import Annotated, Set +from annotated_types import Len + + +class Settings(BaseSettings): + + model_config = SettingsConfigDict(env_file=os.environ.get("ENVIRONMENT_FILE") , env_file_encoding='utf-8', extra='ignore', str_strip_whitespace=True) + + # general + SERVE_LOCAL_ARCHIVE: str | None = None + USER_GROUPS_FILENAME: str = "app/user-groups.yaml" + + # database + DATABASE_PATH: str + DATABASE_QUERY_LIMIT: int = 100 + @property + def ASYNC_DATABASE_PATH(self) -> str: + return self.DATABASE_PATH.replace("sqlite://", "sqlite+aiosqlite://") + + # security + API_BEARER_TOKEN: Annotated[str, Len(min_length=20)] + ALLOWED_ORIGINS: Annotated[Set[str], Len(min_length=1)] + CHROME_APP_IDS: Annotated[Set[Annotated[str, Len(min_length=10)]], Len(min_length=1)] + BLOCKED_EMAILS: Annotated[Set[str], Len(min_length=0)] = set() + + # redis + REDIS_PASSWORD: str = "" + REDIS_HOSTNAME: str = "localhost" + REDIS_EXCEPTIONS_CHANNEL: str = "exceptions-channel" + @property + def CELERY_BROKER_URL(self)-> str: + if self.REDIS_PASSWORD: + return f"redis://:{self.REDIS_PASSWORD}@{self.REDIS_HOSTNAME}:6379" + return f"redis://{self.REDIS_HOSTNAME}:6379" + + # cronjobs + CRON_ARCHIVE_SHEETS: bool = False + CRON_DELETE_STALE_SHEETS: bool = False + DELETE_STALE_SHEETS_DAYS: int = 14 + CRON_DELETE_SCHEDULED_ARCHIVES: bool = False + DELETE_SCHEDULED_ARCHIVES_CHECK_EVERY_N_DAYS: int = 7 + + # observability + REPEAT_COUNT_METRICS_SECONDS: int = 30 + + # email configuration, if needed + MAIL_FROM: str = "noreply@bellingcat.com" + MAIL_FROM_NAME: str = "Bellingcat's Auto Archiver" + MAIL_USERNAME: str = "" + MAIL_PASSWORD: str = "" + MAIL_SERVER: str = "" + MAIL_PORT: int = 587 + MAIL_STARTTLS: bool = False + MAIL_SSL_TLS: bool = True + @property + def MAIL_CONFIG(self) -> str: + return ConnectionConfig( + MAIL_FROM=self.MAIL_FROM, + MAIL_FROM_NAME=self.MAIL_FROM_NAME, + MAIL_USERNAME=self.MAIL_USERNAME, + MAIL_PASSWORD=self.MAIL_PASSWORD, + MAIL_SERVER=self.MAIL_SERVER, + MAIL_PORT=self.MAIL_PORT, + MAIL_STARTTLS=self.MAIL_STARTTLS, + MAIL_SSL_TLS=self.MAIL_SSL_TLS, + ) + + +@lru_cache +def get_settings(): + return Settings() \ No newline at end of file diff --git a/app/shared/task_messaging.py b/app/shared/task_messaging.py new file mode 100644 index 0000000..21fb3d1 --- /dev/null +++ b/app/shared/task_messaging.py @@ -0,0 +1,23 @@ + +from functools import lru_cache +from celery import Celery +import redis + +from app.shared.settings import get_settings + + +@lru_cache +def get_celery(name: str = "") -> Celery: + return Celery( + name, + broker_url=get_settings().CELERY_BROKER_URL, + result_backend=get_settings().CELERY_BROKER_URL, + broker_connection_retry_on_startup=False, + broker_transport_options={ + 'queue_order_strategy': 'priority', + } + ) + + +def get_redis() -> redis.Redis: + return redis.Redis.from_url(get_settings().CELERY_BROKER_URL) diff --git a/app/shared/user_groups.py b/app/shared/user_groups.py new file mode 100644 index 0000000..592e012 --- /dev/null +++ b/app/shared/user_groups.py @@ -0,0 +1,169 @@ +import json +import os +import yaml +from loguru import logger +from pydantic import BaseModel, computed_field, field_validator, Field, model_validator +from typing import Dict, List, Set +from typing_extensions import Self + + +class UserGroups: + def __init__(self, filename): + user_groups = self.read_yaml(filename) + self.validate_and_load(user_groups) + + def read_yaml(self, user_groups_filename): + # read yaml safely + with open(user_groups_filename) as inf: + try: + return yaml.safe_load(inf) + except yaml.YAMLError as e: + logger.error(f"could not open user groups filename {user_groups_filename}: {e}") + raise e + + def validate_and_load(self, user_groups): + try: + configs = UserGroupModel(**user_groups) + self.users = configs.users + self.domains = configs.domains + self.groups = configs.groups + except Exception as e: + logger.error(f"Validation error: {e}") + raise e + + +class GroupPermissions(BaseModel): + read: Set[str] | bool = Field(default_factory=list) + read_public: bool = False + archive_url: bool = False + archive_sheet: bool = False + manually_trigger_sheet: bool = False + sheet_frequency: Set[str] = Field(default_factory=list) + max_sheets: int = 0 + max_archive_lifespan_months: int = 12 + max_monthly_urls: int = 0 + max_monthly_mbs: int = 0 + priority: str = "low" + + @field_validator('max_sheets', 'max_archive_lifespan_months', 'max_monthly_urls', 'max_monthly_mbs', mode='before') + def validate_max_values(cls, v): + if v < -1: + raise ValueError("max_* values should be positive integers or -1 (for no limit).") + return v + + @field_validator('sheet_frequency', mode='before') + def validate_sheet_frequency(cls, v): + if not v: return [] + allowed = ["daily", "hourly"] + for k in v: + if k not in allowed: + raise ValueError(f"Invalid sheet frequency: '{k}', expected one of {allowed}") + return v + + @field_validator('priority', mode='before') + def validate_priority(cls, v): + v = v.lower() + if v not in ["low", "high"]: + raise ValueError("priority must be either 'low' or 'high'.") + return v + + +class GroupModel(BaseModel): + description: str + orchestrator: str + orchestrator_sheet: str + permissions: GroupPermissions + + @field_validator('orchestrator', 'orchestrator_sheet', mode='before') + def validate_orchestrator(cls, v): + if not os.path.exists(v): + raise ValueError(f"Orchestrator file not found with this path: {v}") + return v + + @computed_field + @property + def service_account_email(self) -> str: + if hasattr(self, "_service_account_email"): + return self._service_account_email + orch = yaml.safe_load(open(self.orchestrator_sheet)) + + def find_service_account_email(d): + for k, v in d.items(): + if k == "service_account": + return v + if isinstance(v, dict): + if result := find_service_account_email(v): + return result + return False + + service_account_json = find_service_account_email(orch) + if not service_account_json: + raise ValueError(f"service_account key not found in orchestrator sheet file: {self.orchestrator_sheet}.") + + with open(service_account_json) as f: + self._service_account_email = json.load(f).get("client_email") + + if not self._service_account_email: + raise ValueError(f"Service account email not found in {service_account_json}.") + + return self._service_account_email + + +class UserGroupModel(BaseModel): + users: Dict[str, List[str]] = Field(default_factory=dict) + domains: Dict[str, List[str]] = Field(default_factory=dict) + groups: Dict[str, GroupModel] = Field(default_factory=dict) + + @field_validator('users', mode='before') + @classmethod + def validate_emails(cls, v): + for email in v.keys(): + if '@' not in email: + raise ValueError(f"Invalid user, it should be an address: {email}") + if not v[email]: + raise ValueError(f"User {email} has no explicitly listed groups, only include them here if they should be in a group.") + # all users belong to the default group + return {k.lower().strip(): list(set(["default"] + [g.lower().strip() for g in v])) for k, v in v.items()} + + @field_validator('domains', mode='before') + @classmethod + def validate_domains(cls, v): + for domain, members in v.items(): + if '.' not in domain: + raise ValueError(f"Invalid domain, it should contain a dot: {domain}") + if not members: + raise ValueError(f"Domain {domain} should have at least one member.") + return {k.lower().strip(): list(set([g.lower().strip() for g in v])) for k, v in v.items()} + + @field_validator('groups', mode='before') + @classmethod + def validate_groups(cls, v): + if "default" not in v.keys(): + raise ValueError("Please include a 'default' group.") + if "all" in v.keys(): + raise ValueError("'all' is a reserved group name.") + for group in v.keys(): + if not group == group.lower(): + raise ValueError(f"Group names should be lowercase: {group}") + return v + + @model_validator(mode='after') + def check_groups_consistency(self) -> Self: + groups_in_domains = set([g for domain in self.domains for g in self.domains[domain]]) + groups_in_users = set([g for user in self.users for g in self.users[user]]) + configured_groups = set(self.groups.keys()) + + # groups mentioned in domains and users should be defined, but this is not a ValueError since historical DB data may require it + if groups_in_domains - configured_groups: + logger.warning(f"These groups are associated to DOMAINS but not defined in the GROUPS section, the domains settings may not work as expected: {groups_in_domains - configured_groups}") + if groups_in_users - configured_groups: + logger.warning(f"These groups are associated to USERS but not defined in the GROUPS section, the users settings may not work as expected: {groups_in_users - configured_groups}") + + return self + +# for the API return values + + +class GroupInfo(GroupPermissions): + description: str = "" + service_account_email: str = "" diff --git a/app/shared/utils/misc.py b/app/shared/utils/misc.py new file mode 100644 index 0000000..562b2c3 --- /dev/null +++ b/app/shared/utils/misc.py @@ -0,0 +1,10 @@ + +def fnv1a_hash_mod(s: str, modulo:int) -> int: + # receives a string and returns a number in [0:modulo-1], ensures an even distribution over the modulo range + hash = 0x811c9dc5 # FNV offset basis + fnv_prime = 0x01000193 # FNV prime + for char in s: + hash ^= ord(char) + hash *= fnv_prime + hash &= 0xFFFFFFFF # Keep it 32-bit + return (hash if hash < 0x80000000 else hash - 0x100000000) % modulo \ No newline at end of file diff --git a/app/test.ipynb b/app/test.ipynb new file mode 100644 index 0000000..21cb64c --- /dev/null +++ b/app/test.ipynb @@ -0,0 +1,333 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 45, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The autoreload extension is already loaded. To reload it, use:\n", + " %reload_ext autoreload\n" + ] + } + ], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": {}, + "outputs": [], + "source": [ + "from shared.user_groups import *" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[32m2025-02-03 11:10:33.769\u001b[0m | \u001b[33m\u001b[1mWARNING \u001b[0m | \u001b[36mshared.user_groups\u001b[0m:\u001b[36mcheck_groups_consistency\u001b[0m:\u001b[36m121\u001b[0m - \u001b[33m\u001b[1mThese groups are associated to DOMAINS but not defined in the GROUPS section, the domains settings may not work as expected: {'edmo', 'witness'}\u001b[0m\n", + "\u001b[32m2025-02-03 11:10:33.771\u001b[0m | \u001b[33m\u001b[1mWARNING \u001b[0m | \u001b[36mshared.user_groups\u001b[0m:\u001b[36mcheck_groups_consistency\u001b[0m:\u001b[36m123\u001b[0m - \u001b[33m\u001b[1mThese groups are associated to USERS but not defined in the GROUPS section, the users settings may not work as expected: {'witness'}\u001b[0m\n" + ] + } + ], + "source": [ + "ug = UserGroups(\"user-groups.dev.yaml\")" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "dict_items([('bellingcat', GroupModel(description='Bellingcat staff', orchestrator='secrets/orchestration-bcat.yaml', orchestrator_sheet='secrets/orchestration-bcat-sheet.yaml', permissions=GroupPermissions(read={'all'}, archive_url=True, archive_sheet=True, sheet_frequency={'hourly', 'daily'}, max_sheets=-1, max_archive_lifespan_months=-1, max_monthly_urls=-1, max_monthly_mbs=-1, priority='high'))), ('janda', GroupModel(description='J&A team - Ukraine Project', orchestrator='secrets/orchestration-janda-ukr.yaml', orchestrator_sheet='secrets/orchestration-janda-ukr-sheet.yaml', permissions=GroupPermissions(read={'all'}, archive_url=True, archive_sheet=True, sheet_frequency={'hourly', 'daily'}, max_sheets=-1, max_archive_lifespan_months=-1, max_monthly_urls=-1, max_monthly_mbs=-1, priority='low'))), ('ukraine', GroupModel(description='Members working on the Ukraine Civharm/Witness projects', orchestrator='secrets/orchestration-default.yaml', orchestrator_sheet='secrets/orchestration-default.yaml', permissions=GroupPermissions(read={'ukraine', 'witness'}, archive_url=True, archive_sheet=True, sheet_frequency={'daily'}, max_sheets=5, max_archive_lifespan_months=48, max_monthly_urls=1000, max_monthly_mbs=1000, priority='low'))), ('friends-1', GroupModel(description='Friends of Bellingcat, 1', orchestrator='secrets/orchestration-default.yaml', orchestrator_sheet='secrets/orchestration-default.yaml', permissions=GroupPermissions(read={'friends-1'}, archive_url=False, archive_sheet=False, sheet_frequency=[], max_sheets=0, max_archive_lifespan_months=12, max_monthly_urls=0, max_monthly_mbs=0, priority='low'))), ('default', GroupModel(description='Public access', orchestrator='secrets/orchestration-default.yaml', orchestrator_sheet='secrets/orchestration-default.yaml', permissions=GroupPermissions(read=set(), archive_url=False, archive_sheet=False, sheet_frequency=[], max_sheets=0, max_archive_lifespan_months=12, max_monthly_urls=0, max_monthly_mbs=0, priority='low')))])" + ] + }, + "execution_count": 48, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ug.groups.items()" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "GroupModel(description='Bellingcat staff', orchestrator='secrets/orchestration-bcat.yaml', orchestrator_sheet='secrets/orchestration-bcat-sheet.yaml', permissions=GroupPermissions(read={'all'}, archive_url=True, archive_sheet=True, sheet_frequency={'hourly', 'daily'}, max_sheets=-1, max_archive_lifespan_months=-1, max_monthly_urls=-1, max_monthly_mbs=-1, priority='high'))" + ] + }, + "execution_count": 49, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ug.groups[\"bellingcat\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"bellingcat\" in ug.groups" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "AssertionError\n", + "that was unexpected\n" + ] + } + ], + "source": [ + "try:\n", + "\tassert 123 == \"as\", \"that was unexpected\"\n", + "\traise ValueError(\"this is a test\")\n", + "except Exception as e:\n", + "\tprint(type(e))\n", + "\tprint(e.__class__.__name__)\n", + "\tprint(e)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "def custom_hash(s: str) -> int:\n", + " hash = 0\n", + " for char in s:\n", + " hash = (hash * 31 + ord(char)) & 0xFFFFFFFF # 31 is a prime number, used in Java hash\n", + " return hash % 60 # Ensure it fits within 60 minutes" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "def fnv1a_hash_mod(s: str, modulo:int) -> int:\n", + " hash = 0x811c9dc5 # FNV offset basis\n", + " fnv_prime = 0x01000193 # FNV prime\n", + " for char in s:\n", + " hash ^= ord(char)\n", + " hash *= fnv_prime\n", + " hash &= 0xFFFFFFFF # Keep it 32-bit\n", + " return (hash if hash < 0x80000000 else hash - 0x100000000) % modulo" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'N7wlA3HmjrDxENPpshxpkiTY6FVUB4OlKGgIMuui1M0p'" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "#generate random google sheet ids\n", + "import random\n", + "import string\n", + "def random_sheet_id():\n", + " return ''.join(random.choices(string.ascii_uppercase + string.ascii_lowercase + string.digits, k=44))\n", + "random_sheet_id()" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "from tqdm import tqdm\n", + "from collections import defaultdict, Counter\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 1000000/1000000 [00:16<00:00, 60730.44it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Counter({1: 42205, 2: 41980, 4: 41915, 7: 41851, 14: 41785, 12: 41781, 10: 41744, 23: 41734, 6: 41709, 5: 41707, 18: 41705, 8: 41697, 15: 41654, 16: 41651, 9: 41642, 11: 41562, 22: 41555, 21: 41533, 3: 41481, 13: 41461, 19: 41453, 20: 41453, 0: 41447, 17: 41295})\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "modulo_counter = Counter()\n", + "for i in tqdm(range(1_000_000)):\n", + "\tid = random_sheet_id()\n", + "\thash = fnv1a_hash_mod(id, 24)\n", + "\t# hash = custom_hash(id)\n", + "\tmodulo_counter[hash] += 1\n", + "print(modulo_counter)\n", + "# custom: [00:16<00:00, 60651.26it/s]" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjkAAAGdCAYAAADwjmIIAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAMXxJREFUeJzt3X9QlOe9//8XoLuIuhi0sHJEJDFRiaIRK27bZDRSV2RyYkMzJnESYogZPZCp7KmmfMai0XZMbf2VSkJ7EiWdSqN2mvRELEowYFNRE5Sj0YRJPHawRxfSJLKRKCjs948O99etil1/bbh4PmauKXtf7/va9313aV9ze99smN/v9wsAAMAw4aFuAAAA4GYg5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjNQr1A2EUkdHh06ePKn+/fsrLCws1O0AAIB/gd/v15dffqn4+HiFh1/5ek2PDjknT55UQkJCqNsAAADX4MSJExoyZMgV53t0yOnfv7+kf5wkh8MR4m4AAMC/wufzKSEhwfr/8Svp0SGn85+oHA4HIQcAgG7mareacOMxAAAwEiEHAAAYiZADAACMRMgBAABGIuQAAAAjEXIAAICRCDkAAMBIhBwAAGAkQg4AADASIQcAABiJkAMAAIxEyAEAAEYi5AAAACMRcgAAgJF6hboBXN2wH5Vd875/fSHzBnYCAED3wZUcAABgJK7k9DBcFQIA9BSEHHwt9ITw1ROOEQC+Tgg5MM6NDBMEk+B8Hc/X9fQk9by+vq6+jp+tnqI7n3tCDtANEeRwMT4PwOURcnDN+B9D3Exfx88XV19wMxFWbzxCzk3CBwwAvp4IEz0HIQcAcFMQABBq/J0cAABgJEIOAAAwEiEHAAAYiZADAACMRMgBAABGuq6Q88ILLygsLEwLFiywtp07d065ubkaOHCg+vXrp6ysLDU2Ngbs19DQoMzMTEVFRSk2NlYLFy7UhQsXAmqqqqo0fvx42e12DR8+XCUlJZe8f1FRkYYNG6bIyEilpaVp//7913M4AADAINccct577z396le/UkpKSsD2/Px8vfXWW9q6dauqq6t18uRJPfTQQ9Z8e3u7MjMz1dbWpj179ui1115TSUmJCgsLrZrjx48rMzNTU6ZMUV1dnRYsWKCnn35aO3bssGo2b94sj8ejJUuW6MCBAxo7dqzcbreampqu9ZAAAIBBrinknDlzRrNnz9Z//dd/6bbbbrO2Nzc369VXX9Xq1at1//33KzU1VRs3btSePXu0d+9eSdLOnTt19OhR/fa3v9W4ceOUkZGh5cuXq6ioSG1tbZKk4uJiJSUladWqVRo1apTy8vL0/e9/X2vWrLHea/Xq1Zo7d67mzJmj5ORkFRcXKyoqShs2bLie8wEAAAxxTSEnNzdXmZmZSk9PD9heW1ur8+fPB2wfOXKkhg4dqpqaGklSTU2NxowZo7i4OKvG7XbL5/PpyJEjVs0/r+12u6012traVFtbG1ATHh6u9PR0qwYAAPRsQf/F49dff10HDhzQe++9d8mc1+uVzWbTgAEDArbHxcXJ6/VaNRcHnM75zrmuanw+n86ePasvvvhC7e3tl6356KOPrth7a2urWltbrdc+n+8qRwsAALqroK7knDhxQj/4wQ+0adMmRUZG3qyebpoVK1YoOjraGgkJCaFuCQAA3CRBhZza2lo1NTVp/Pjx6tWrl3r16qXq6mq9+OKL6tWrl+Li4tTW1qbTp08H7NfY2Cin0ylJcjqdlzxt1fn6ajUOh0N9+vTRoEGDFBERcdmazjUup6CgQM3NzdY4ceJEMIcPAAC6kaBCztSpU3X48GHV1dVZY8KECZo9e7b1c+/evVVZWWntU19fr4aGBrlcLkmSy+XS4cOHA56CqqiokMPhUHJyslVz8RqdNZ1r2Gw2paamBtR0dHSosrLSqrkcu90uh8MRMAAAgJmCuienf//+Gj16dMC2vn37auDAgdb2nJwceTwexcTEyOFw6Nlnn5XL5dKkSZMkSdOmTVNycrIef/xxrVy5Ul6vV4sXL1Zubq7sdrskad68eVq/fr0WLVqkp556Srt27dKWLVtUVvb/f6Otx+NRdna2JkyYoIkTJ2rt2rVqaWnRnDlzruuEAAAAMwR94/HVrFmzRuHh4crKylJra6vcbrdeeuklaz4iIkLbtm3T/Pnz5XK51LdvX2VnZ2vZsmVWTVJSksrKypSfn69169ZpyJAheuWVV+R2u62aWbNm6dNPP1VhYaG8Xq/GjRun8vLyS25GBgAAPdN1h5yqqqqA15GRkSoqKlJRUdEV90lMTNT27du7XHfy5Mk6ePBglzV5eXnKy8v7l3sFAAA9B99dBQAAjETIAQAARiLkAAAAIxFyAACAkQg5AADASIQcAABgJEIOAAAwEiEHAAAYiZADAACMRMgBAABGIuQAAAAjEXIAAICRCDkAAMBIhBwAAGAkQg4AADASIQcAABiJkAMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAkQg5AADASIQcAABgJEIOAAAwUlAh5+WXX1ZKSoocDoccDodcLpf+9Kc/WfOTJ09WWFhYwJg3b17AGg0NDcrMzFRUVJRiY2O1cOFCXbhwIaCmqqpK48ePl91u1/Dhw1VSUnJJL0VFRRo2bJgiIyOVlpam/fv3B3MoAADAcEGFnCFDhuiFF15QbW2t3n//fd1///168MEHdeTIEatm7ty5OnXqlDVWrlxpzbW3tyszM1NtbW3as2ePXnvtNZWUlKiwsNCqOX78uDIzMzVlyhTV1dVpwYIFevrpp7Vjxw6rZvPmzfJ4PFqyZIkOHDigsWPHyu12q6mp6XrOBQAAMEhQIeeBBx7QjBkzdOedd+quu+7ST3/6U/Xr10979+61aqKiouR0Oq3hcDisuZ07d+ro0aP67W9/q3HjxikjI0PLly9XUVGR2traJEnFxcVKSkrSqlWrNGrUKOXl5en73/++1qxZY62zevVqzZ07V3PmzFFycrKKi4sVFRWlDRs2XO/5AAAAhrjme3La29v1+uuvq6WlRS6Xy9q+adMmDRo0SKNHj1ZBQYG++uora66mpkZjxoxRXFyctc3tdsvn81lXg2pqapSenh7wXm63WzU1NZKktrY21dbWBtSEh4crPT3dqrmS1tZW+Xy+gAEAAMzUK9gdDh8+LJfLpXPnzqlfv3564403lJycLEl67LHHlJiYqPj4eB06dEjPPfec6uvr9Yc//EGS5PV6AwKOJOu11+vtssbn8+ns2bP64osv1N7eftmajz76qMveV6xYoeeffz7YQwYAAN1Q0CFnxIgRqqurU3Nzs37/+98rOztb1dXVSk5O1jPPPGPVjRkzRoMHD9bUqVN17Ngx3XHHHTe08WtRUFAgj8djvfb5fEpISAhhRwAA4GYJOuTYbDYNHz5ckpSamqr33ntP69at069+9atLatPS0iRJn3zyie644w45nc5LnoJqbGyUJDmdTus/O7ddXONwONSnTx9FREQoIiLisjWda1yJ3W6X3W4P4mgBAEB3dd1/J6ejo0Otra2Xnaurq5MkDR48WJLkcrl0+PDhgKegKioq5HA4rH/ycrlcqqysDFinoqLCuu/HZrMpNTU1oKajo0OVlZUB9wYBAICeLagrOQUFBcrIyNDQoUP15ZdfqrS0VFVVVdqxY4eOHTum0tJSzZgxQwMHDtShQ4eUn5+v++67TykpKZKkadOmKTk5WY8//rhWrlwpr9erxYsXKzc317rCMm/ePK1fv16LFi3SU089pV27dmnLli0qKyuz+vB4PMrOztaECRM0ceJErV27Vi0tLZozZ84NPDUAAKA7CyrkNDU16YknntCpU6cUHR2tlJQU7dixQ9/97nd14sQJvf3221bgSEhIUFZWlhYvXmztHxERoW3btmn+/PlyuVzq27evsrOztWzZMqsmKSlJZWVlys/P17p16zRkyBC98sorcrvdVs2sWbP06aefqrCwUF6vV+PGjVN5efklNyMDAICeK6iQ8+qrr15xLiEhQdXV1VddIzExUdu3b++yZvLkyTp48GCXNXl5ecrLy7vq+wEAgJ6J764CAABGIuQAAAAjEXIAAICRCDkAAMBIhBwAAGAkQg4AADASIQcAABiJkAMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAkQg5AADASIQcAABgJEIOAAAwEiEHAAAYiZADAACMRMgBAABGIuQAAAAjEXIAAICRCDkAAMBIhBwAAGAkQg4AADASIQcAABgpqJDz8ssvKyUlRQ6HQw6HQy6XS3/605+s+XPnzik3N1cDBw5Uv379lJWVpcbGxoA1GhoalJmZqaioKMXGxmrhwoW6cOFCQE1VVZXGjx8vu92u4cOHq6Sk5JJeioqKNGzYMEVGRiotLU379+8P5lAAAIDhggo5Q4YM0QsvvKDa2lq9//77uv/++/Xggw/qyJEjkqT8/Hy99dZb2rp1q6qrq3Xy5Ek99NBD1v7t7e3KzMxUW1ub9uzZo9dee00lJSUqLCy0ao4fP67MzExNmTJFdXV1WrBggZ5++mnt2LHDqtm8ebM8Ho+WLFmiAwcOaOzYsXK73Wpqarre8wEAAAwRVMh54IEHNGPGDN15552666679NOf/lT9+vXT3r171dzcrFdffVWrV6/W/fffr9TUVG3cuFF79uzR3r17JUk7d+7U0aNH9dvf/lbjxo1TRkaGli9frqKiIrW1tUmSiouLlZSUpFWrVmnUqFHKy8vT97//fa1Zs8bqY/Xq1Zo7d67mzJmj5ORkFRcXKyoqShs2bLiBpwYAAHRn13xPTnt7u15//XW1tLTI5XKptrZW58+fV3p6ulUzcuRIDR06VDU1NZKkmpoajRkzRnFxcVaN2+2Wz+ezrgbV1NQErNFZ07lGW1ubamtrA2rCw8OVnp5u1VxJa2urfD5fwAAAAGYKOuQcPnxY/fr1k91u17x58/TGG28oOTlZXq9XNptNAwYMCKiPi4uT1+uVJHm93oCA0znfOddVjc/n09mzZ/X3v/9d7e3tl63pXONKVqxYoejoaGskJCQEe/gAAKCbCDrkjBgxQnV1ddq3b5/mz5+v7OxsHT169Gb0dsMVFBSoubnZGidOnAh1SwAA4CbpFewONptNw4cPlySlpqbqvffe07p16zRr1iy1tbXp9OnTAVdzGhsb5XQ6JUlOp/OSp6A6n766uOafn8hqbGyUw+FQnz59FBERoYiIiMvWdK5xJXa7XXa7PdhDBgAA3dB1/52cjo4Otba2KjU1Vb1791ZlZaU1V19fr4aGBrlcLkmSy+XS4cOHA56CqqiokMPhUHJyslVz8RqdNZ1r2Gw2paamBtR0dHSosrLSqgEAAAjqSk5BQYEyMjI0dOhQffnllyotLVVVVZV27Nih6Oho5eTkyOPxKCYmRg6HQ88++6xcLpcmTZokSZo2bZqSk5P1+OOPa+XKlfJ6vVq8eLFyc3OtKyzz5s3T+vXrtWjRIj311FPatWuXtmzZorKyMqsPj8ej7OxsTZgwQRMnTtTatWvV0tKiOXPm3MBTAwAAurOgQk5TU5OeeOIJnTp1StHR0UpJSdGOHTv03e9+V5K0Zs0ahYeHKysrS62trXK73XrppZes/SMiIrRt2zbNnz9fLpdLffv2VXZ2tpYtW2bVJCUlqaysTPn5+Vq3bp2GDBmiV155RW6326qZNWuWPv30UxUWFsrr9WrcuHEqLy+/5GZkAADQcwUVcl599dUu5yMjI1VUVKSioqIr1iQmJmr79u1drjN58mQdPHiwy5q8vDzl5eV1WQMAAHouvrsKAAAYiZADAACMRMgBAABGIuQAAAAjEXIAAICRCDkAAMBIhBwAAGAkQg4AADASIQcAABiJkAMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAkQg5AADASIQcAABgJEIOAAAwEiEHAAAYiZADAACMRMgBAABGIuQAAAAjEXIAAICRCDkAAMBIhBwAAGCkoELOihUr9M1vflP9+/dXbGysZs6cqfr6+oCayZMnKywsLGDMmzcvoKahoUGZmZmKiopSbGysFi5cqAsXLgTUVFVVafz48bLb7Ro+fLhKSkou6aeoqEjDhg1TZGSk0tLStH///mAOBwAAGCyokFNdXa3c3Fzt3btXFRUVOn/+vKZNm6aWlpaAurlz5+rUqVPWWLlypTXX3t6uzMxMtbW1ac+ePXrttddUUlKiwsJCq+b48ePKzMzUlClTVFdXpwULFujpp5/Wjh07rJrNmzfL4/FoyZIlOnDggMaOHSu3262mpqZrPRcAAMAgvYIpLi8vD3hdUlKi2NhY1dbW6r777rO2R0VFyel0XnaNnTt36ujRo3r77bcVFxencePGafny5Xruuee0dOlS2Ww2FRcXKykpSatWrZIkjRo1Su+++67WrFkjt9stSVq9erXmzp2rOXPmSJKKi4tVVlamDRs26Ec/+lEwhwUAAAx0XffkNDc3S5JiYmICtm/atEmDBg3S6NGjVVBQoK+++sqaq6mp0ZgxYxQXF2dtc7vd8vl8OnLkiFWTnp4esKbb7VZNTY0kqa2tTbW1tQE14eHhSk9Pt2oup7W1VT6fL2AAAAAzBXUl52IdHR1asGCBvv3tb2v06NHW9scee0yJiYmKj4/XoUOH9Nxzz6m+vl5/+MMfJElerzcg4EiyXnu93i5rfD6fzp49qy+++ELt7e2Xrfnoo4+u2POKFSv0/PPPX+shAwCAbuSaQ05ubq4++OADvfvuuwHbn3nmGevnMWPGaPDgwZo6daqOHTumO+6449o7vQEKCgrk8Xis1z6fTwkJCSHsCAAA3CzXFHLy8vK0bds27d69W0OGDOmyNi0tTZL0ySef6I477pDT6bzkKajGxkZJsu7jcTqd1raLaxwOh/r06aOIiAhFRERctuZK9wJJkt1ul91u/9cOEgAAdGtB3ZPj9/uVl5enN954Q7t27VJSUtJV96mrq5MkDR48WJLkcrl0+PDhgKegKioq5HA4lJycbNVUVlYGrFNRUSGXyyVJstlsSk1NDajp6OhQZWWlVQMAAHq2oK7k5ObmqrS0VH/84x/Vv39/6x6a6Oho9enTR8eOHVNpaalmzJihgQMH6tChQ8rPz9d9992nlJQUSdK0adOUnJysxx9/XCtXrpTX69XixYuVm5trXWWZN2+e1q9fr0WLFumpp57Srl27tGXLFpWVlVm9eDweZWdna8KECZo4caLWrl2rlpYW62krAADQswUVcl5++WVJ//iDfxfbuHGjnnzySdlsNr399ttW4EhISFBWVpYWL15s1UZERGjbtm2aP3++XC6X+vbtq+zsbC1btsyqSUpKUllZmfLz87Vu3ToNGTJEr7zyivX4uCTNmjVLn376qQoLC+X1ejVu3DiVl5dfcjMyAADomYIKOX6/v8v5hIQEVVdXX3WdxMREbd++vcuayZMn6+DBg13W5OXlKS8v76rvBwAAeh6+uwoAABiJkAMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAkQg5AADASIQcAABgJEIOAAAwEiEHAAAYiZADAACMRMgBAABGIuQAAAAjEXIAAICRCDkAAMBIhBwAAGAkQg4AADASIQcAABiJkAMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEhBhZwVK1bom9/8pvr376/Y2FjNnDlT9fX1ATXnzp1Tbm6uBg4cqH79+ikrK0uNjY0BNQ0NDcrMzFRUVJRiY2O1cOFCXbhwIaCmqqpK48ePl91u1/Dhw1VSUnJJP0VFRRo2bJgiIyOVlpam/fv3B3M4AADAYEGFnOrqauXm5mrv3r2qqKjQ+fPnNW3aNLW0tFg1+fn5euutt7R161ZVV1fr5MmTeuihh6z59vZ2ZWZmqq2tTXv27NFrr72mkpISFRYWWjXHjx9XZmampkyZorq6Oi1YsEBPP/20duzYYdVs3rxZHo9HS5Ys0YEDBzR27Fi53W41NTVdz/kAAACG6BVMcXl5ecDrkpISxcbGqra2Vvfdd5+am5v16quvqrS0VPfff78kaePGjRo1apT27t2rSZMmaefOnTp69KjefvttxcXFady4cVq+fLmee+45LV26VDabTcXFxUpKStKqVaskSaNGjdK7776rNWvWyO12S5JWr16tuXPnas6cOZKk4uJilZWVacOGDfrRj3503ScGAAB0b9d1T05zc7MkKSYmRpJUW1ur8+fPKz093aoZOXKkhg4dqpqaGklSTU2NxowZo7i4OKvG7XbL5/PpyJEjVs3Fa3TWdK7R1tam2tragJrw8HClp6dbNZfT2toqn88XMAAAgJmuOeR0dHRowYIF+va3v63Ro0dLkrxer2w2mwYMGBBQGxcXJ6/Xa9VcHHA65zvnuqrx+Xw6e/as/v73v6u9vf2yNZ1rXM6KFSsUHR1tjYSEhOAPHAAAdAvXHHJyc3P1wQcf6PXXX7+R/dxUBQUFam5utsaJEydC3RIAALhJgronp1NeXp62bdum3bt3a8iQIdZ2p9OptrY2nT59OuBqTmNjo5xOp1Xzz09BdT59dXHNPz+R1djYKIfDoT59+igiIkIRERGXrelc43LsdrvsdnvwBwwAALqdoK7k+P1+5eXl6Y033tCuXbuUlJQUMJ+amqrevXursrLS2lZfX6+Ghga5XC5Jksvl0uHDhwOegqqoqJDD4VBycrJVc/EanTWda9hsNqWmpgbUdHR0qLKy0qoBAAA9W1BXcnJzc1VaWqo//vGP6t+/v3X/S3R0tPr06aPo6Gjl5OTI4/EoJiZGDodDzz77rFwulyZNmiRJmjZtmpKTk/X4449r5cqV8nq9Wrx4sXJzc62rLPPmzdP69eu1aNEiPfXUU9q1a5e2bNmisrIyqxePx6Ps7GxNmDBBEydO1Nq1a9XS0mI9bQUAAHq2oELOyy+/LEmaPHlywPaNGzfqySeflCStWbNG4eHhysrKUmtrq9xut1566SWrNiIiQtu2bdP8+fPlcrnUt29fZWdna9myZVZNUlKSysrKlJ+fr3Xr1mnIkCF65ZVXrMfHJWnWrFn69NNPVVhYKK/Xq3Hjxqm8vPySm5EBAEDPFFTI8fv9V62JjIxUUVGRioqKrliTmJio7du3d7nO5MmTdfDgwS5r8vLylJeXd9WeAABAz8N3VwEAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAkQg5AADASIQcAABgJEIOAAAwEiEHAAAYiZADAACMRMgBAABGIuQAAAAjEXIAAICRCDkAAMBIhBwAAGAkQg4AADASIQcAABiJkAMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjBR0yNm9e7ceeOABxcfHKywsTG+++WbA/JNPPqmwsLCAMX369ICazz//XLNnz5bD4dCAAQOUk5OjM2fOBNQcOnRI9957ryIjI5WQkKCVK1de0svWrVs1cuRIRUZGasyYMdq+fXuwhwMAAAwVdMhpaWnR2LFjVVRUdMWa6dOn69SpU9b43e9+FzA/e/ZsHTlyRBUVFdq2bZt2796tZ555xpr3+XyaNm2aEhMTVVtbq5///OdaunSpfv3rX1s1e/bs0aOPPqqcnBwdPHhQM2fO1MyZM/XBBx8Ee0gAAMBAvYLdISMjQxkZGV3W2O12OZ3Oy859+OGHKi8v13vvvacJEyZIkn75y19qxowZ+sUvfqH4+Hht2rRJbW1t2rBhg2w2m+6++27V1dVp9erVVhhat26dpk+froULF0qSli9froqKCq1fv17FxcXBHhYAADDMTbknp6qqSrGxsRoxYoTmz5+vzz77zJqrqanRgAEDrIAjSenp6QoPD9e+ffusmvvuu082m82qcbvdqq+v1xdffGHVpKenB7yv2+1WTU3NFftqbW2Vz+cLGAAAwEw3PORMnz5dv/nNb1RZWamf/exnqq6uVkZGhtrb2yVJXq9XsbGxAfv06tVLMTEx8nq9Vk1cXFxATefrq9V0zl/OihUrFB0dbY2EhITrO1gAAPC1FfQ/V13NI488Yv08ZswYpaSk6I477lBVVZWmTp16o98uKAUFBfJ4PNZrn89H0AEAwFA3/RHy22+/XYMGDdInn3wiSXI6nWpqagqouXDhgj7//HPrPh6n06nGxsaAms7XV6u50r1A0j/uFXI4HAEDAACY6aaHnL/97W/67LPPNHjwYEmSy+XS6dOnVVtba9Xs2rVLHR0dSktLs2p2796t8+fPWzUVFRUaMWKEbrvtNqumsrIy4L0qKirkcrlu9iEBAIBuIOiQc+bMGdXV1amurk6SdPz4cdXV1amhoUFnzpzRwoULtXfvXv31r39VZWWlHnzwQQ0fPlxut1uSNGrUKE2fPl1z587V/v379Ze//EV5eXl65JFHFB8fL0l67LHHZLPZlJOToyNHjmjz5s1at25dwD81/eAHP1B5eblWrVqljz76SEuXLtX777+vvLy8G3BaAABAdxd0yHn//fd1zz336J577pEkeTwe3XPPPSosLFRERIQOHTqkf//3f9ddd92lnJwcpaam6s9//rPsdru1xqZNmzRy5EhNnTpVM2bM0He+852Av4ETHR2tnTt36vjx40pNTdV//ud/qrCwMOBv6XzrW99SaWmpfv3rX2vs2LH6/e9/rzfffFOjR4++nvMBAAAMEfSNx5MnT5bf77/i/I4dO666RkxMjEpLS7usSUlJ0Z///Ocuax5++GE9/PDDV30/AADQ8/DdVQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAkQg5AADASIQcAABgJEIOAAAwEiEHAAAYiZADAACMRMgBAABGIuQAAAAjEXIAAICRCDkAAMBIhBwAAGAkQg4AADASIQcAABiJkAMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIwUdcnbv3q0HHnhA8fHxCgsL05tvvhkw7/f7VVhYqMGDB6tPnz5KT0/Xxx9/HFDz+eefa/bs2XI4HBowYIBycnJ05syZgJpDhw7p3nvvVWRkpBISErRy5cpLetm6datGjhypyMhIjRkzRtu3bw/2cAAAgKGCDjktLS0aO3asioqKLju/cuVKvfjiiyouLta+ffvUt29fud1unTt3zqqZPXu2jhw5ooqKCm3btk27d+/WM888Y837fD5NmzZNiYmJqq2t1c9//nMtXbpUv/71r62aPXv26NFHH1VOTo4OHjyomTNnaubMmfrggw+CPSQAAGCgXsHukJGRoYyMjMvO+f1+rV27VosXL9aDDz4oSfrNb36juLg4vfnmm3rkkUf04Ycfqry8XO+9954mTJggSfrlL3+pGTNm6Be/+IXi4+O1adMmtbW1acOGDbLZbLr77rtVV1en1atXW2Fo3bp1mj59uhYuXChJWr58uSoqKrR+/XoVFxdf08kAAADmuKH35Bw/flxer1fp6enWtujoaKWlpammpkaSVFNTowEDBlgBR5LS09MVHh6uffv2WTX33XefbDabVeN2u1VfX68vvvjCqrn4fTprOt/nclpbW+Xz+QIGAAAw0w0NOV6vV5IUFxcXsD0uLs6a83q9io2NDZjv1auXYmJiAmout8bF73Glms75y1mxYoWio6OtkZCQEOwhAgCAbqJHPV1VUFCg5uZma5w4cSLULQEAgJvkhoYcp9MpSWpsbAzY3tjYaM05nU41NTUFzF+4cEGff/55QM3l1rj4Pa5U0zl/OXa7XQ6HI2AAAAAz3dCQk5SUJKfTqcrKSmubz+fTvn375HK5JEkul0unT59WbW2tVbNr1y51dHQoLS3Nqtm9e7fOnz9v1VRUVGjEiBG67bbbrJqL36ezpvN9AABAzxZ0yDlz5ozq6upUV1cn6R83G9fV1amhoUFhYWFasGCBfvKTn+i///u/dfjwYT3xxBOKj4/XzJkzJUmjRo3S9OnTNXfuXO3fv19/+ctflJeXp0ceeUTx8fGSpMcee0w2m005OTk6cuSINm/erHXr1snj8Vh9/OAHP1B5eblWrVqljz76SEuXLtX777+vvLy86z8rAACg2wv6EfL3339fU6ZMsV53Bo/s7GyVlJRo0aJFamlp0TPPPKPTp0/rO9/5jsrLyxUZGWnts2nTJuXl5Wnq1KkKDw9XVlaWXnzxRWs+OjpaO3fuVG5urlJTUzVo0CAVFhYG/C2db33rWyotLdXixYv1//7f/9Odd96pN998U6NHj76mEwEAAMwSdMiZPHmy/H7/FefDwsK0bNkyLVu27Io1MTExKi0t7fJ9UlJS9Oc//7nLmocfflgPP/xw1w0DAIAeqUc9XQUAAHoOQg4AADASIQcAABiJkAMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAkQg5AADASIQcAABgJEIOAAAwEiEHAAAYiZADAACMRMgBAABGIuQAAAAjEXIAAICRCDkAAMBIhBwAAGAkQg4AADASIQcAABiJkAMAAIxEyAEAAEYi5AAAACPd8JCzdOlShYWFBYyRI0da8+fOnVNubq4GDhyofv36KSsrS42NjQFrNDQ0KDMzU1FRUYqNjdXChQt14cKFgJqqqiqNHz9edrtdw4cPV0lJyY0+FAAA0I3dlCs5d999t06dOmWNd99915rLz8/XW2+9pa1bt6q6ulonT57UQw89ZM23t7crMzNTbW1t2rNnj1577TWVlJSosLDQqjl+/LgyMzM1ZcoU1dXVacGCBXr66ae1Y8eOm3E4AACgG+p1Uxbt1UtOp/OS7c3NzXr11VdVWlqq+++/X5K0ceNGjRo1Snv37tWkSZO0c+dOHT16VG+//bbi4uI0btw4LV++XM8995yWLl0qm82m4uJiJSUladWqVZKkUaNG6d1339WaNWvkdrtvxiEBAIBu5qZcyfn4448VHx+v22+/XbNnz1ZDQ4Mkqba2VufPn1d6erpVO3LkSA0dOlQ1NTWSpJqaGo0ZM0ZxcXFWjdvtls/n05EjR6yai9forOlcAwAA4IZfyUlLS1NJSYlGjBihU6dO6fnnn9e9996rDz74QF6vVzabTQMGDAjYJy4uTl6vV5Lk9XoDAk7nfOdcVzU+n09nz55Vnz59Lttba2urWltbrdc+n++6jhUAAHx93fCQk5GRYf2ckpKitLQ0JSYmasuWLVcMH7fKihUr9Pzzz4e0BwAAcGvc9EfIBwwYoLvuukuffPKJnE6n2tradPr06YCaxsZG6x4ep9N5ydNWna+vVuNwOLoMUgUFBWpubrbGiRMnrvfwAADA19RNDzlnzpzRsWPHNHjwYKWmpqp3796qrKy05uvr69XQ0CCXyyVJcrlcOnz4sJqamqyaiooKORwOJScnWzUXr9FZ07nGldjtdjkcjoABAADMdMNDzg9/+ENVV1frr3/9q/bs2aPvfe97ioiI0KOPPqro6Gjl5OTI4/HonXfeUW1trebMmSOXy6VJkyZJkqZNm6bk5GQ9/vjj+p//+R/t2LFDixcvVm5urux2uyRp3rx5+t///V8tWrRIH330kV566SVt2bJF+fn5N/pwAABAN3XD78n529/+pkcffVSfffaZvvGNb+g73/mO9u7dq2984xuSpDVr1ig8PFxZWVlqbW2V2+3WSy+9ZO0fERGhbdu2af78+XK5XOrbt6+ys7O1bNkyqyYpKUllZWXKz8/XunXrNGTIEL3yyis8Pg4AACw3POS8/vrrXc5HRkaqqKhIRUVFV6xJTEzU9u3bu1xn8uTJOnjw4DX1CAAAzMd3VwEAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAkQg5AADASIQcAABgJEIOAAAwEiEHAAAYiZADAACMRMgBAABGIuQAAAAjEXIAAICRCDkAAMBIhBwAAGAkQg4AADASIQcAABiJkAMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjNTtQ05RUZGGDRumyMhIpaWlaf/+/aFuCQAAfA1065CzefNmeTweLVmyRAcOHNDYsWPldrvV1NQU6tYAAECIdeuQs3r1as2dO1dz5sxRcnKyiouLFRUVpQ0bNoS6NQAAEGK9Qt3AtWpra1Ntba0KCgqsbeHh4UpPT1dNTc1l92ltbVVra6v1urm5WZLk8/lueH8drV9d877/3A9rsRZrXfta17NOT1irO/x3yFrmrHWjdK7r9/u7LvR3U//3f//nl+Tfs2dPwPaFCxf6J06ceNl9lixZ4pfEYDAYDAbDgHHixIkus0K3vZJzLQoKCuTxeKzXHR0d+vzzzzVw4ECFhYXdsj58Pp8SEhJ04sQJORyOW/a+PR3nPXQ496HDuQ8dzv3N4/f79eWXXyo+Pr7Lum4bcgYNGqSIiAg1NjYGbG9sbJTT6bzsPna7XXa7PWDbgAEDblaLV+VwOPjghwDnPXQ496HDuQ8dzv3NER0dfdWabnvjsc1mU2pqqiorK61tHR0dqqyslMvlCmFnAADg66DbXsmRJI/Ho+zsbE2YMEETJ07U2rVr1dLSojlz5oS6NQAAEGLdOuTMmjVLn376qQoLC+X1ejVu3DiVl5crLi4u1K11yW63a8mSJZf80xluLs576HDuQ4dzHzqc+9AL8/uv9vwVAABA99Nt78kBAADoCiEHAAAYiZADAACMRMgBAABGIuTcYkVFRRo2bJgiIyOVlpam/fv3h7ol4y1dulRhYWEBY+TIkaFuy0i7d+/WAw88oPj4eIWFhenNN98MmPf7/SosLNTgwYPVp08fpaen6+OPPw5Ns4a52rl/8sknL/k9mD59emiaNciKFSv0zW9+U/3791dsbKxmzpyp+vr6gJpz584pNzdXAwcOVL9+/ZSVlXXJH7LFzUHIuYU2b94sj8ejJUuW6MCBAxo7dqzcbreamppC3Zrx7r77bp06dcoa7777bqhbMlJLS4vGjh2roqKiy86vXLlSL774ooqLi7Vv3z717dtXbrdb586du8Wdmudq516Spk+fHvB78Lvf/e4Wdmim6upq5ebmau/evaqoqND58+c1bdo0tbS0WDX5+fl66623tHXrVlVXV+vkyZN66KGHQth1D3JDvi0T/5KJEyf6c3Nzrdft7e3++Ph4/4oVK0LYlfmWLFniHzt2bKjb6HEk+d944w3rdUdHh9/pdPp//vOfW9tOnz7tt9vt/t/97nch6NBc/3zu/X6/Pzs72//ggw+GpJ+epKmpyS/JX11d7ff7//EZ7927t3/r1q1WzYcffuiX5K+pqQlVmz0GV3Jukba2NtXW1io9Pd3aFh4ervT0dNXU1ISws57h448/Vnx8vG6//XbNnj1bDQ0NoW6pxzl+/Li8Xm/A70B0dLTS0tL4HbhFqqqqFBsbqxEjRmj+/Pn67LPPQt2ScZqbmyVJMTExkqTa2lqdP38+4HM/cuRIDR06lM/9LUDIuUX+/ve/q729/ZK/xhwXFyev1xuirnqGtLQ0lZSUqLy8XC+//LKOHz+ue++9V19++WWoW+tROj/n/A6ExvTp0/Wb3/xGlZWV+tnPfqbq6mplZGSovb091K0Zo6OjQwsWLNC3v/1tjR49WtI/Pvc2m+2SL4Pmc39rdOuvdQD+FRkZGdbPKSkpSktLU2JiorZs2aKcnJwQdgbcOo888oj185gxY5SSkqI77rhDVVVVmjp1agg7M0dubq4++OAD7vn7GuFKzi0yaNAgRUREXHJHfWNjo5xOZ4i66pkGDBigu+66S5988kmoW+lROj/n/A58Pdx+++0aNGgQvwc3SF5enrZt26Z33nlHQ4YMsbY7nU61tbXp9OnTAfV87m8NQs4tYrPZlJqaqsrKSmtbR0eHKisr5XK5QthZz3PmzBkdO3ZMgwcPDnUrPUpSUpKcTmfA74DP59O+ffv4HQiBv/3tb/rss8/4PbhOfr9feXl5euONN7Rr1y4lJSUFzKempqp3794Bn/v6+no1NDTwub8F+OeqW8jj8Sg7O1sTJkzQxIkTtXbtWrW0tGjOnDmhbs1oP/zhD/XAAw8oMTFRJ0+e1JIlSxQREaFHH3001K0Z58yZMwFXBo4fP666ujrFxMRo6NChWrBggX7yk5/ozjvvVFJSkn784x8rPj5eM2fODF3Thujq3MfExOj5559XVlaWnE6njh07pkWLFmn48OFyu90h7Lr7y83NVWlpqf74xz+qf//+1n020dHR6tOnj6Kjo5WTkyOPx6OYmBg5HA49++yzcrlcmjRpUoi77wFC/XhXT/PLX/7SP3ToUL/NZvNPnDjRv3fv3lC3ZLxZs2b5Bw8e7LfZbP5/+7d/88+aNcv/ySefhLotI73zzjt+SZeM7Oxsv9//j8fIf/zjH/vj4uL8drvdP3XqVH99fX1omzZEV+f+q6++8k+bNs3/jW98w9+7d29/YmKif+7cuX6v1xvqtru9y51zSf6NGzdaNWfPnvX/x3/8h/+2227zR0VF+b/3ve/5T506Fbqme5Awv9/vv/XRCgAA4ObinhwAAGAkQg4AADASIQcAABiJkAMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjPT/Adh+q6MgnRINAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.bar(modulo_counter.keys(), modulo_counter.values())\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjkAAAGdCAYAAADwjmIIAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAMXVJREFUeJzt3X9UVWW+x/EPiAfMBEQvHM8NiTs14m9Ni06ZY8kSlRwtx5tF5ZpIbw1USsvKe40crSjK3zIyTmM6a2Ay545OaYMyOEoloqJclRyyGSa4OQfuXQonLQFl3z9msW8n0aQOIo/v11rPWp7n+e69n/0Ap0+bvTkBlmVZAgAAMExgR08AAACgPRByAACAkQg5AADASIQcAABgJEIOAAAwEiEHAAAYiZADAACMRMgBAABGCuroCXSk5uZmHT9+XD169FBAQEBHTwcAAFwCy7L0+eefy+VyKTDwwtdrruqQc/z4cUVHR3f0NAAAwLdQXV2t66677oLjV3XI6dGjh6R/LFJoaGgHzwYAAFwKr9er6Oho+7/jF3JVh5yWX1GFhoYScgAA6GS+6VYTbjwGAABGIuQAAAAjEXIAAICRCDkAAMBIhBwAAGAkQg4AADASIQcAABiJkAMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMFJQR08AwPmuf25rq/1/eyXpMs8EADovruQAAAAjcSUHPriCcOlYKwC4shFyDMN/eNFR+N7zxXoAHY+QAwCXWWcMQJ1xzp0Va+0/hJxO6Lv8AHTGH56OmvOVulZX29f/asTX6crH16hzIOS0E34ALh/W+sr3TV+jK/VreCXO60qck3Tlzuu76Iz/Q9FZf9baCyEHlwU/eJ0fXyPztdfX+EL7bdn3d3l/uNreW9rzfDpjqPsmhBxcsiv1m/hirtQ3QN6oLt3V+DX8Lr5LIABMQ8jpILzZAMDViff/y4eQA+CqxX9srm58/dumM64XIecK1Bm/keCLryEAdDxCDgBjfdMNrwDMRsiB33D1AgBwJeEDOgEAgJEIOQAAwEhtDjlFRUWaNGmSXC6XAgICtHnz5vNqjh49qh/+8IcKCwtT9+7ddfPNN6uqqsoeP3PmjFJTU9WrVy9de+21mjp1qmpqanz2UVVVpaSkJF1zzTWKjIzU3LlzdfbsWZ+anTt36qabblJwcLBuuOEGrVu3rq2nAwAADNXmkHP69GkNHTpU2dnZrY7/5S9/0ahRoxQXF6edO3fq0KFDev755xUSEmLXzJkzR++++642btyoXbt26fjx47r33nvt8XPnzikpKUmNjY3avXu31q9fr3Xr1ikjI8OuqaysVFJSku68806VlZVp9uzZevTRR7Vt27a2nhIAADBQm288njBhgiZMmHDB8f/4j//QxIkTlZWVZfd973vfs/9dX1+vX/7yl8rLy9Ndd90lSXrzzTfVv39/7dmzR7feequ2b9+ujz76SH/84x8VFRWlYcOGadGiRXr22We1YMECORwO5eTkKDY2VosXL5Yk9e/fXx988IGWLl2qxMTEtp4WAAAwjF/vyWlubtbWrVv1/e9/X4mJiYqMjFR8fLzPr7RKS0vV1NSkhIQEuy8uLk59+/ZVcXGxJKm4uFiDBw9WVFSUXZOYmCiv16vy8nK75qv7aKlp2UdrGhoa5PV6fRoAADCTX0NObW2tTp06pVdeeUXjx4/X9u3bdc899+jee+/Vrl27JEkej0cOh0Ph4eE+20ZFRcnj8dg1Xw04LeMtYxer8Xq9+vLLL1udX2ZmpsLCwuwWHR39nc8ZAABcmfx+JUeSJk+erDlz5mjYsGF67rnndPfddysnJ8efh/pW5s2bp/r6ertVV1d39JQAAEA78WvI6d27t4KCgjRgwACf/v79+9tPVzmdTjU2Nqqurs6npqamRk6n0675+tNWLa+/qSY0NFTdunVrdX7BwcEKDQ31aQAAwEx+DTkOh0M333yzKioqfPo//vhjxcTESJJGjBihrl27qrCw0B6vqKhQVVWV3G63JMntduvw4cOqra21awoKChQaGmoHKLfb7bOPlpqWfQAAgKtbm5+uOnXqlD755BP7dWVlpcrKyhQREaG+fftq7ty5uu+++zR69Gjdeeedys/P17vvvqudO3dKksLCwpSSkqL09HRFREQoNDRUTzzxhNxut2699VZJ0rhx4zRgwAA99NBDysrKksfj0fz585Wamqrg4GBJ0mOPPaZVq1bpmWee0SOPPKIdO3bo7bff1tatF/6sGgAAcPVoc8jZv3+/7rzzTvt1enq6JGnGjBlat26d7rnnHuXk5CgzM1NPPvmk+vXrp//8z//UqFGj7G2WLl2qwMBATZ06VQ0NDUpMTNTPfvYze7xLly7asmWLHn/8cbndbnXv3l0zZszQwoUL7ZrY2Fht3bpVc+bM0fLly3XdddfpjTfe4PFxAAAg6VuEnDFjxsiyrIvWPPLII3rkkUcuOB4SEqLs7OwL/kFBSYqJidF77733jXM5ePDgxScMAACuSnx2FQAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAkQg5AADASIQcAABgJEIOAAAwEiEHAAAYiZADAACMRMgBAABGIuQAAAAjEXIAAICRCDkAAMBIhBwAAGAkQg4AADASIQcAABiJkAMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAkQg5AADASG0OOUVFRZo0aZJcLpcCAgK0efPmC9Y+9thjCggI0LJly3z6T5w4oeTkZIWGhio8PFwpKSk6deqUT82hQ4d0xx13KCQkRNHR0crKyjpv/xs3blRcXJxCQkI0ePBgvffee209HQAAYKg2h5zTp09r6NChys7Ovmjdpk2btGfPHrlcrvPGkpOTVV5eroKCAm3ZskVFRUWaNWuWPe71ejVu3DjFxMSotLRUr732mhYsWKA1a9bYNbt379b999+vlJQUHTx4UFOmTNGUKVN05MiRtp4SAAAwUFBbN5gwYYImTJhw0ZrPPvtMTzzxhLZt26akpCSfsaNHjyo/P1/79u3TyJEjJUkrV67UxIkT9frrr8vlcik3N1eNjY1au3atHA6HBg4cqLKyMi1ZssQOQ8uXL9f48eM1d+5cSdKiRYtUUFCgVatWKScnp62nBQAADOP3e3Kam5v10EMPae7cuRo4cOB548XFxQoPD7cDjiQlJCQoMDBQJSUlds3o0aPlcDjsmsTERFVUVOjkyZN2TUJCgs++ExMTVVxcfMG5NTQ0yOv1+jQAAGAmv4ecV199VUFBQXryySdbHfd4PIqMjPTpCwoKUkREhDwej10TFRXlU9Py+ptqWsZbk5mZqbCwMLtFR0e37eQAAECn4deQU1paquXLl2vdunUKCAjw5679Yt68eaqvr7dbdXV1R08JAAC0E7+GnPfff1+1tbXq27evgoKCFBQUpE8//VRPP/20rr/+ekmS0+lUbW2tz3Znz57ViRMn5HQ67ZqamhqfmpbX31TTMt6a4OBghYaG+jQAAGAmv4achx56SIcOHVJZWZndXC6X5s6dq23btkmS3G636urqVFpaam+3Y8cONTc3Kz4+3q4pKipSU1OTXVNQUKB+/fqpZ8+edk1hYaHP8QsKCuR2u/15SgAAoJNq89NVp06d0ieffGK/rqysVFlZmSIiItS3b1/16tXLp75r165yOp3q16+fJKl///4aP368Zs6cqZycHDU1NSktLU3Tp0+3Hzd/4IEH9NOf/lQpKSl69tlndeTIES1fvlxLly619/vUU0/pBz/4gRYvXqykpCS99dZb2r9/v89j5gAA4OrV5is5+/fv1/DhwzV8+HBJUnp6uoYPH66MjIxL3kdubq7i4uI0duxYTZw4UaNGjfIJJ2FhYdq+fbsqKys1YsQIPf3008rIyPD5Wzq33Xab8vLytGbNGg0dOlS//e1vtXnzZg0aNKitpwQAAAzU5is5Y8aMkWVZl1z/t7/97by+iIgI5eXlXXS7IUOG6P33379ozbRp0zRt2rRLngsAALh68NlVAADASIQcAABgJEIOAAAwEiEHAAAYiZADAACMRMgBAABGIuQAAAAjEXIAAICRCDkAAMBIhBwAAGAkQg4AADASIQcAABiJkAMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAkQg5AADASIQcAABgJEIOAAAwEiEHAAAYiZADAACMRMgBAABGanPIKSoq0qRJk+RyuRQQEKDNmzfbY01NTXr22Wc1ePBgde/eXS6XSw8//LCOHz/us48TJ04oOTlZoaGhCg8PV0pKik6dOuVTc+jQId1xxx0KCQlRdHS0srKyzpvLxo0bFRcXp5CQEA0ePFjvvfdeW08HAAAYqs0h5/Tp0xo6dKiys7PPG/viiy904MABPf/88zpw4IB+97vfqaKiQj/84Q996pKTk1VeXq6CggJt2bJFRUVFmjVrlj3u9Xo1btw4xcTEqLS0VK+99poWLFigNWvW2DW7d+/W/fffr5SUFB08eFBTpkzRlClTdOTIkbaeEgAAMFBQWzeYMGGCJkyY0OpYWFiYCgoKfPpWrVqlW265RVVVVerbt6+OHj2q/Px87du3TyNHjpQkrVy5UhMnTtTrr78ul8ul3NxcNTY2au3atXI4HBo4cKDKysq0ZMkSOwwtX75c48eP19y5cyVJixYtUkFBgVatWqWcnJy2nhYAADBMu9+TU19fr4CAAIWHh0uSiouLFR4ebgccSUpISFBgYKBKSkrsmtGjR8vhcNg1iYmJqqio0MmTJ+2ahIQEn2MlJiaquLj4gnNpaGiQ1+v1aQAAwEztGnLOnDmjZ599Vvfff79CQ0MlSR6PR5GRkT51QUFBioiIkMfjsWuioqJ8alpef1NNy3hrMjMzFRYWZrfo6OjvdoIAAOCK1W4hp6mpSf/6r/8qy7K0evXq9jpMm8ybN0/19fV2q66u7ugpAQCAdtLme3IuRUvA+fTTT7Vjxw77Ko4kOZ1O1dbW+tSfPXtWJ06ckNPptGtqamp8alpef1NNy3hrgoODFRwc/O1PDAAAdBp+v5LTEnCOHTumP/7xj+rVq5fPuNvtVl1dnUpLS+2+HTt2qLm5WfHx8XZNUVGRmpqa7JqCggL169dPPXv2tGsKCwt99l1QUCC32+3vUwIAAJ1Qm0POqVOnVFZWprKyMklSZWWlysrKVFVVpaamJv3oRz/S/v37lZubq3Pnzsnj8cjj8aixsVGS1L9/f40fP14zZ87U3r179eGHHyotLU3Tp0+Xy+WSJD3wwANyOBxKSUlReXm5NmzYoOXLlys9Pd2ex1NPPaX8/HwtXrxYf/7zn7VgwQLt379faWlpflgWAADQ2bU55Ozfv1/Dhw/X8OHDJUnp6ekaPny4MjIy9Nlnn+mdd97Rf//3f2vYsGHq06eP3Xbv3m3vIzc3V3FxcRo7dqwmTpyoUaNG+fwNnLCwMG3fvl2VlZUaMWKEnn76aWVkZPj8LZ3bbrtNeXl5WrNmjYYOHarf/va32rx5swYNGvRd1gMAABiizffkjBkzRpZlXXD8YmMtIiIilJeXd9GaIUOG6P33379ozbRp0zRt2rRvPB4AALj68NlVAADASIQcAABgJEIOAAAwEiEHAAAYiZADAACMRMgBAABGIuQAAAAjEXIAAICRCDkAAMBIhBwAAGAkQg4AADASIQcAABiJkAMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAkQg5AADASIQcAABgJEIOAAAwEiEHAAAYiZADAACMRMgBAABGIuQAAAAjtTnkFBUVadKkSXK5XAoICNDmzZt9xi3LUkZGhvr06aNu3bopISFBx44d86k5ceKEkpOTFRoaqvDwcKWkpOjUqVM+NYcOHdIdd9yhkJAQRUdHKysr67y5bNy4UXFxcQoJCdHgwYP13nvvtfV0AACAodocck6fPq2hQ4cqOzu71fGsrCytWLFCOTk5KikpUffu3ZWYmKgzZ87YNcnJySovL1dBQYG2bNmioqIizZo1yx73er0aN26cYmJiVFpaqtdee00LFizQmjVr7Jrdu3fr/vvvV0pKig4ePKgpU6ZoypQpOnLkSFtPCQAAGCiorRtMmDBBEyZMaHXMsiwtW7ZM8+fP1+TJkyVJv/rVrxQVFaXNmzdr+vTpOnr0qPLz87Vv3z6NHDlSkrRy5UpNnDhRr7/+ulwul3Jzc9XY2Ki1a9fK4XBo4MCBKisr05IlS+wwtHz5co0fP15z586VJC1atEgFBQVatWqVcnJyvtViAAAAc/j1npzKykp5PB4lJCTYfWFhYYqPj1dxcbEkqbi4WOHh4XbAkaSEhAQFBgaqpKTErhk9erQcDoddk5iYqIqKCp08edKu+epxWmpajtOahoYGeb1enwYAAMzk15Dj8XgkSVFRUT79UVFR9pjH41FkZKTPeFBQkCIiInxqWtvHV49xoZqW8dZkZmYqLCzMbtHR0W09RQAA0ElcVU9XzZs3T/X19Xarrq7u6CkBAIB24teQ43Q6JUk1NTU+/TU1NfaY0+lUbW2tz/jZs2d14sQJn5rW9vHVY1yopmW8NcHBwQoNDfVpAADATH4NObGxsXI6nSosLLT7vF6vSkpK5Ha7JUlut1t1dXUqLS21a3bs2KHm5mbFx8fbNUVFRWpqarJrCgoK1K9fP/Xs2dOu+epxWmpajgMAAK5ubQ45p06dUllZmcrKyiT942bjsrIyVVVVKSAgQLNnz9aLL76od955R4cPH9bDDz8sl8ulKVOmSJL69++v8ePHa+bMmdq7d68+/PBDpaWlafr06XK5XJKkBx54QA6HQykpKSovL9eGDRu0fPlypaen2/N46qmnlJ+fr8WLF+vPf/6zFixYoP379ystLe27rwoAAOj02vwI+f79+3XnnXfar1uCx4wZM7Ru3To988wzOn36tGbNmqW6ujqNGjVK+fn5CgkJsbfJzc1VWlqaxo4dq8DAQE2dOlUrVqywx8PCwrR9+3alpqZqxIgR6t27tzIyMnz+ls5tt92mvLw8zZ8/X//+7/+uG2+8UZs3b9agQYO+1UIAAACztDnkjBkzRpZlXXA8ICBACxcu1MKFCy9YExERoby8vIseZ8iQIXr//fcvWjNt2jRNmzbt4hMGAABXpavq6SoAAHD1IOQAAAAjEXIAAICRCDkAAMBIhBwAAGAkQg4AADASIQcAABiJkAMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAkQg5AADASIQcAABgJEIOAAAwEiEHAAAYiZADAACMRMgBAABGIuQAAAAjEXIAAICRCDkAAMBIhBwAAGAkQg4AADCS30POuXPn9Pzzzys2NlbdunXT9773PS1atEiWZdk1lmUpIyNDffr0Ubdu3ZSQkKBjx4757OfEiRNKTk5WaGiowsPDlZKSolOnTvnUHDp0SHfccYdCQkIUHR2trKwsf58OAADopPwecl599VWtXr1aq1at0tGjR/Xqq68qKytLK1eutGuysrK0YsUK5eTkqKSkRN27d1diYqLOnDlj1yQnJ6u8vFwFBQXasmWLioqKNGvWLHvc6/Vq3LhxiomJUWlpqV577TUtWLBAa9as8fcpAQCATijI3zvcvXu3Jk+erKSkJEnS9ddfr9/85jfau3evpH9cxVm2bJnmz5+vyZMnS5J+9atfKSoqSps3b9b06dN19OhR5efna9++fRo5cqQkaeXKlZo4caJef/11uVwu5ebmqrGxUWvXrpXD4dDAgQNVVlamJUuW+IQhAABwdfL7lZzbbrtNhYWF+vjjjyVJ//Vf/6UPPvhAEyZMkCRVVlbK4/EoISHB3iYsLEzx8fEqLi6WJBUXFys8PNwOOJKUkJCgwMBAlZSU2DWjR4+Ww+GwaxITE1VRUaGTJ0+2OreGhgZ5vV6fBgAAzOT3KznPPfecvF6v4uLi1KVLF507d04vvfSSkpOTJUkej0eSFBUV5bNdVFSUPebxeBQZGek70aAgRURE+NTExsaet4+WsZ49e543t8zMTP30pz/1w1kCAIArnd+v5Lz99tvKzc1VXl6eDhw4oPXr1+v111/X+vXr/X2oNps3b57q6+vtVl1d3dFTAgAA7cTvV3Lmzp2r5557TtOnT5ckDR48WJ9++qkyMzM1Y8YMOZ1OSVJNTY369Oljb1dTU6Nhw4ZJkpxOp2pra332e/bsWZ04ccLe3ul0qqamxqem5XVLzdcFBwcrODj4u58kAAC44vn9Ss4XX3yhwEDf3Xbp0kXNzc2SpNjYWDmdThUWFtrjXq9XJSUlcrvdkiS32626ujqVlpbaNTt27FBzc7Pi4+PtmqKiIjU1Ndk1BQUF6tevX6u/qgIAAFcXv4ecSZMm6aWXXtLWrVv1t7/9TZs2bdKSJUt0zz33SJICAgI0e/Zsvfjii3rnnXd0+PBhPfzww3K5XJoyZYokqX///ho/frxmzpypvXv36sMPP1RaWpqmT58ul8slSXrggQfkcDiUkpKi8vJybdiwQcuXL1d6erq/TwkAAHRCfv911cqVK/X888/rJz/5iWpra+VyufRv//ZvysjIsGueeeYZnT59WrNmzVJdXZ1GjRql/Px8hYSE2DW5ublKS0vT2LFjFRgYqKlTp2rFihX2eFhYmLZv367U1FSNGDFCvXv3VkZGBo+PAwAASe0Qcnr06KFly5Zp2bJlF6wJCAjQwoULtXDhwgvWREREKC8v76LHGjJkiN5///1vO1UAAGAwPrsKAAAYiZADAACMRMgBAABGIuQAAAAjEXIAAICRCDkAAMBIhBwAAGAkQg4AADASIQcAABiJkAMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAkQg5AADASIQcAABgJEIOAAAwEiEHAAAYiZADAACMRMgBAABGIuQAAAAjEXIAAICRCDkAAMBIhBwAAGCkdgk5n332mR588EH16tVL3bp10+DBg7V//3573LIsZWRkqE+fPurWrZsSEhJ07Ngxn32cOHFCycnJCg0NVXh4uFJSUnTq1CmfmkOHDumOO+5QSEiIoqOjlZWV1R6nAwAAOiG/h5yTJ0/q9ttvV9euXfWHP/xBH330kRYvXqyePXvaNVlZWVqxYoVycnJUUlKi7t27KzExUWfOnLFrkpOTVV5eroKCAm3ZskVFRUWaNWuWPe71ejVu3DjFxMSotLRUr732mhYsWKA1a9b4+5QAAEAnFOTvHb766quKjo7Wm2++affFxsba/7YsS8uWLdP8+fM1efJkSdKvfvUrRUVFafPmzZo+fbqOHj2q/Px87du3TyNHjpQkrVy5UhMnTtTrr78ul8ul3NxcNTY2au3atXI4HBo4cKDKysq0ZMkSnzAEAACuTn6/kvPOO+9o5MiRmjZtmiIjIzV8+HD94he/sMcrKyvl8XiUkJBg94WFhSk+Pl7FxcWSpOLiYoWHh9sBR5ISEhIUGBiokpISu2b06NFyOBx2TWJioioqKnTy5MlW59bQ0CCv1+vTAACAmfwecv76179q9erVuvHGG7Vt2zY9/vjjevLJJ7V+/XpJksfjkSRFRUX5bBcVFWWPeTweRUZG+owHBQUpIiLCp6a1fXz1GF+XmZmpsLAwu0VHR3/HswUAAFcqv4ec5uZm3XTTTXr55Zc1fPhwzZo1SzNnzlROTo6/D9Vm8+bNU319vd2qq6s7ekoAAKCd+D3k9OnTRwMGDPDp69+/v6qqqiRJTqdTklRTU+NTU1NTY485nU7V1tb6jJ89e1YnTpzwqWltH189xtcFBwcrNDTUpwEAADP5PeTcfvvtqqio8On7+OOPFRMTI+kfNyE7nU4VFhba416vVyUlJXK73ZIkt9uturo6lZaW2jU7duxQc3Oz4uPj7ZqioiI1NTXZNQUFBerXr5/Pk1wAAODq5PeQM2fOHO3Zs0cvv/yyPvnkE+Xl5WnNmjVKTU2VJAUEBGj27Nl68cUX9c477+jw4cN6+OGH5XK5NGXKFEn/uPIzfvx4zZw5U3v37tWHH36otLQ0TZ8+XS6XS5L0wAMPyOFwKCUlReXl5dqwYYOWL1+u9PR0f58SAADohPz+CPnNN9+sTZs2ad68eVq4cKFiY2O1bNkyJScn2zXPPPOMTp8+rVmzZqmurk6jRo1Sfn6+QkJC7Jrc3FylpaVp7NixCgwM1NSpU7VixQp7PCwsTNu3b1dqaqpGjBih3r17KyMjg8fHAQCApHYIOZJ099136+67777geEBAgBYuXKiFCxdesCYiIkJ5eXkXPc6QIUP0/vvvf+t5AgAAc/HZVQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAkQg5AADASIQcAABgJEIOAAAwEiEHAAAYiZADAACMRMgBAABGIuQAAAAjEXIAAICRCDkAAMBIhBwAAGAkQg4AADASIQcAABiJkAMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARmr3kPPKK68oICBAs2fPtvvOnDmj1NRU9erVS9dee62mTp2qmpoan+2qqqqUlJSka665RpGRkZo7d67Onj3rU7Nz507ddNNNCg4O1g033KB169a19+kAAIBOol1Dzr59+/Tzn/9cQ4YM8emfM2eO3n33XW3cuFG7du3S8ePHde+999rj586dU1JSkhobG7V7926tX79e69atU0ZGhl1TWVmppKQk3XnnnSorK9Ps2bP16KOPatu2be15SgAAoJNot5Bz6tQpJScn6xe/+IV69uxp99fX1+uXv/yllixZorvuuksjRozQm2++qd27d2vPnj2SpO3bt+ujjz7Sr3/9aw0bNkwTJkzQokWLlJ2drcbGRklSTk6OYmNjtXjxYvXv319paWn60Y9+pKVLl7bXKQEAgE6k3UJOamqqkpKSlJCQ4NNfWlqqpqYmn/64uDj17dtXxcXFkqTi4mINHjxYUVFRdk1iYqK8Xq/Ky8vtmq/vOzEx0d5HaxoaGuT1en0aAAAwU1B77PStt97SgQMHtG/fvvPGPB6PHA6HwsPDffqjoqLk8Xjsmq8GnJbxlrGL1Xi9Xn355Zfq1q3becfOzMzUT3/60299XgAAoPPw+5Wc6upqPfXUU8rNzVVISIi/d/+dzJs3T/X19Xarrq7u6CkBAIB24veQU1paqtraWt10000KCgpSUFCQdu3apRUrVigoKEhRUVFqbGxUXV2dz3Y1NTVyOp2SJKfTed7TVi2vv6kmNDS01as4khQcHKzQ0FCfBgAAzOT3kDN27FgdPnxYZWVldhs5cqSSk5Ptf3ft2lWFhYX2NhUVFaqqqpLb7ZYkud1uHT58WLW1tXZNQUGBQkNDNWDAALvmq/toqWnZBwAAuLr5/Z6cHj16aNCgQT593bt3V69evez+lJQUpaenKyIiQqGhoXriiSfkdrt16623SpLGjRunAQMG6KGHHlJWVpY8Ho/mz5+v1NRUBQcHS5Iee+wxrVq1Ss8884weeeQR7dixQ2+//ba2bt3q71MCAACdULvcePxNli5dqsDAQE2dOlUNDQ1KTEzUz372M3u8S5cu2rJlix5//HG53W51795dM2bM0MKFC+2a2NhYbd26VXPmzNHy5ct13XXX6Y033lBiYmJHnBIAALjCXJaQs3PnTp/XISEhys7OVnZ29gW3iYmJ0XvvvXfR/Y4ZM0YHDx70xxQBAIBh+OwqAABgJEIOAAAwEiEHAAAYiZADAACMRMgBAABGIuQAAAAjEXIAAICRCDkAAMBIhBwAAGAkQg4AADASIQcAABiJkAMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAkQg5AADASIQcAABgJEIOAAAwEiEHAAAYiZADAACMRMgBAABGIuQAAAAjEXIAAICR/B5yMjMzdfPNN6tHjx6KjIzUlClTVFFR4VNz5swZpaamqlevXrr22ms1depU1dTU+NRUVVUpKSlJ11xzjSIjIzV37lydPXvWp2bnzp266aabFBwcrBtuuEHr1q3z9+kAAIBOyu8hZ9euXUpNTdWePXtUUFCgpqYmjRs3TqdPn7Zr5syZo3fffVcbN27Url27dPz4cd177732+Llz55SUlKTGxkbt3r1b69ev17p165SRkWHXVFZWKikpSXfeeafKyso0e/ZsPfroo9q2bZu/TwkAAHRCQf7eYX5+vs/rdevWKTIyUqWlpRo9erTq6+v1y1/+Unl5ebrrrrskSW+++ab69++vPXv26NZbb9X27dv10Ucf6Y9//KOioqI0bNgwLVq0SM8++6wWLFggh8OhnJwcxcbGavHixZKk/v3764MPPtDSpUuVmJjo79MCAACdTLvfk1NfXy9JioiIkCSVlpaqqalJCQkJdk1cXJz69u2r4uJiSVJxcbEGDx6sqKgouyYxMVFer1fl5eV2zVf30VLTso/WNDQ0yOv1+jQAAGCmdg05zc3Nmj17tm6//XYNGjRIkuTxeORwOBQeHu5TGxUVJY/HY9d8NeC0jLeMXazG6/Xqyy+/bHU+mZmZCgsLs1t0dPR3PkcAAHBlateQk5qaqiNHjuitt95qz8Ncsnnz5qm+vt5u1dXVHT0lAADQTvx+T06LtLQ0bdmyRUVFRbruuuvsfqfTqcbGRtXV1flczampqZHT6bRr9u7d67O/lqevvlrz9SeyampqFBoaqm7durU6p+DgYAUHB3/ncwMAAFc+v1/JsSxLaWlp2rRpk3bs2KHY2Fif8REjRqhr164qLCy0+yoqKlRVVSW32y1JcrvdOnz4sGpra+2agoIChYaGasCAAXbNV/fRUtOyDwAAcHXz+5Wc1NRU5eXl6fe//7169Ohh30MTFhambt26KSwsTCkpKUpPT1dERIRCQ0P1xBNPyO1269Zbb5UkjRs3TgMGDNBDDz2krKwseTwezZ8/X6mpqfaVmMcee0yrVq3SM888o0ceeUQ7duzQ22+/ra1bt/r7lAAAQCfk9ys5q1evVn19vcaMGaM+ffrYbcOGDXbN0qVLdffdd2vq1KkaPXq0nE6nfve739njXbp00ZYtW9SlSxe53W49+OCDevjhh7Vw4UK7JjY2Vlu3blVBQYGGDh2qxYsX64033uDxcQAAIKkdruRYlvWNNSEhIcrOzlZ2dvYFa2JiYvTee+9ddD9jxozRwYMH2zxHAABgPj67CgAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAkQg5AADASIQcAABgJEIOAAAwEiEHAAAYiZADAACMRMgBAABGIuQAAAAjEXIAAICRCDkAAMBIhBwAAGAkQg4AADASIQcAABiJkAMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAkQg5AADASJ0+5GRnZ+v6669XSEiI4uPjtXfv3o6eEgAAuAJ06pCzYcMGpaen64UXXtCBAwc0dOhQJSYmqra2tqOnBgAAOlinDjlLlizRzJkz9eMf/1gDBgxQTk6OrrnmGq1du7ajpwYAADpYUEdP4NtqbGxUaWmp5s2bZ/cFBgYqISFBxcXFrW7T0NCghoYG+3V9fb0kyev1+n1+zQ1ftNrfcqyLjXfUtlfqvK62ba/UeZm07ZU6r6tt2yt1XqZu25Hz8reW/VqWdfFCq5P67LPPLEnW7t27ffrnzp1r3XLLLa1u88ILL1iSaDQajUajGdCqq6svmhU67ZWcb2PevHlKT0+3Xzc3N+vEiRPq1auXAgIC2uWYXq9X0dHRqq6uVmhoaLscwySs16VjrS4da9U2rNelY63axl/rZVmWPv/8c7lcrovWddqQ07t3b3Xp0kU1NTU+/TU1NXI6na1uExwcrODgYJ++8PDw9pqij9DQUH4A2oD1unSs1aVjrdqG9bp0rFXb+GO9wsLCvrGm09547HA4NGLECBUWFtp9zc3NKiwslNvt7sCZAQCAK0GnvZIjSenp6ZoxY4ZGjhypW265RcuWLdPp06f14x//uKOnBgAAOlinDjn33Xef/ud//kcZGRnyeDwaNmyY8vPzFRUV1dFTswUHB+uFF14479dkaB3rdelYq0vHWrUN63XpWKu2udzrFWBZ3/T8FQAAQOfTae/JAQAAuBhCDgAAMBIhBwAAGImQAwAAjETIaWfZ2dm6/vrrFRISovj4eO3du7ejp9ThioqKNGnSJLlcLgUEBGjz5s0+45ZlKSMjQ3369FG3bt2UkJCgY8eOdcxkO1hmZqZuvvlm9ejRQ5GRkZoyZYoqKip8as6cOaPU1FT16tVL1157raZOnXreH8m8WqxevVpDhgyx/9CY2+3WH/7wB3uctbqwV155RQEBAZo9e7bdx3r9vwULFiggIMCnxcXF2eOsla/PPvtMDz74oHr16qVu3bpp8ODB2r9/vz1+ud7nCTntaMOGDUpPT9cLL7ygAwcOaOjQoUpMTFRtbW1HT61DnT59WkOHDlV2dnar41lZWVqxYoVycnJUUlKi7t27KzExUWfOnLnMM+14u3btUmpqqvbs2aOCggI1NTVp3LhxOn36tF0zZ84cvfvuu9q4caN27dql48eP69577+3AWXec6667Tq+88opKS0u1f/9+3XXXXZo8ebLKy8slsVYXsm/fPv385z/XkCFDfPpZL18DBw7U3//+d7t98MEH9hhr9f9Onjyp22+/XV27dtUf/vAHffTRR1q8eLF69uxp11y293l/fFgmWnfLLbdYqamp9utz585ZLpfLyszM7MBZXVkkWZs2bbJfNzc3W06n03rttdfsvrq6Ois4ONj6zW9+0wEzvLLU1tZakqxdu3ZZlvWPtenatau1ceNGu+bo0aOWJKu4uLijpnlF6dmzp/XGG2+wVhfw+eefWzfeeKNVUFBg/eAHP7Ceeuopy7L43vq6F154wRo6dGirY6yVr2effdYaNWrUBccv5/s8V3LaSWNjo0pLS5WQkGD3BQYGKiEhQcXFxR04sytbZWWlPB6Pz7qFhYUpPj6edZNUX18vSYqIiJAklZaWqqmpyWe94uLi1Ldv36t+vc6dO6e33npLp0+fltvtZq0uIDU1VUlJST7rIvG91Zpjx47J5XLpX/7lX5ScnKyqqipJrNXXvfPOOxo5cqSmTZumyMhIDR8+XL/4xS/s8cv5Pk/IaSf/+7//q3Pnzp3315ejoqLk8Xg6aFZXvpa1Yd3O19zcrNmzZ+v222/XoEGDJP1jvRwOx3kfNHs1r9fhw4d17bXXKjg4WI899pg2bdqkAQMGsFateOutt3TgwAFlZmaeN8Z6+YqPj9e6deuUn5+v1atXq7KyUnfccYc+//xz1upr/vrXv2r16tW68cYbtW3bNj3++ON68skntX79ekmX932+U3+sA3A1SU1N1ZEjR3zuA8D5+vXrp7KyMtXX1+u3v/2tZsyYoV27dnX0tK441dXVeuqpp1RQUKCQkJCOns4Vb8KECfa/hwwZovj4eMXExOjtt99Wt27dOnBmV57m5maNHDlSL7/8siRp+PDhOnLkiHJycjRjxozLOheu5LST3r17q0uXLufdXV9TUyOn09lBs7rytawN6+YrLS1NW7Zs0Z/+9Cddd911dr/T6VRjY6Pq6up86q/m9XI4HLrhhhs0YsQIZWZmaujQoVq+fDlr9TWlpaWqra3VTTfdpKCgIAUFBWnXrl1asWKFgoKCFBUVxXpdRHh4uL7//e/rk08+4Xvra/r06aMBAwb49PXv39/+9d7lfJ8n5LQTh8OhESNGqLCw0O5rbm5WYWGh3G53B87syhYbGyun0+mzbl6vVyUlJVflulmWpbS0NG3atEk7duxQbGysz/iIESPUtWtXn/WqqKhQVVXVVblerWlublZDQwNr9TVjx47V4cOHVVZWZreRI0cqOTnZ/jfrdWGnTp3SX/7yF/Xp04fvra+5/fbbz/tTFx9//LFiYmIkXeb3eb/exgwfb731lhUcHGytW7fO+uijj6xZs2ZZ4eHhlsfj6eipdajPP//cOnjwoHXw4EFLkrVkyRLr4MGD1qeffmpZlmW98sorVnh4uPX73//eOnTokDV58mQrNjbW+vLLLzt45pff448/boWFhVk7d+60/v73v9vtiy++sGsee+wxq2/fvtaOHTus/fv3W26323K73R04647z3HPPWbt27bIqKyutQ4cOWc8995wVEBBgbd++3bIs1uqbfPXpKstivb7q6aeftnbu3GlVVlZaH374oZWQkGD17t3bqq2ttSyLtfqqvXv3WkFBQdZLL71kHTt2zMrNzbWuueYa69e//rVdc7ne5wk57WzlypVW3759LYfDYd1yyy3Wnj17OnpKHe5Pf/qTJem8NmPGDMuy/vF44fPPP29FRUVZwcHB1tixY62KioqOnXQHaW2dJFlvvvmmXfPll19aP/nJT6yePXta11xzjXXPPfdYf//73ztu0h3okUcesWJiYiyHw2H90z/9kzV27Fg74FgWa/VNvh5yWK//d99991l9+vSxHA6H9c///M/WfffdZ33yySf2OGvl691337UGDRpkBQcHW3FxcdaaNWt8xi/X+3yAZVmWf68NAQAAdDzuyQEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAkQg5AADASP8He2oeTQhz1ZAAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.bar(modulo_counter.keys(), modulo_counter.values())\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "leaks-ArvCfdH_", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/app/tests/conftest.py b/app/tests/conftest.py new file mode 100644 index 0000000..afa76f9 --- /dev/null +++ b/app/tests/conftest.py @@ -0,0 +1,160 @@ +import os +from typing import AsyncGenerator +from fastapi.testclient import TestClient +import pytest +from unittest.mock import patch +import pytest_asyncio +from sqlalchemy.ext.asyncio import AsyncSession, AsyncEngine +from app.web.config import ALLOW_ANY_EMAIL +from app.shared.settings import Settings +from app.web.db.user_state import UserState + + +@pytest.fixture(autouse=True) +def mock_logger_add(): + """Fixture to mock loguru.logger.add for all tests.""" + with patch('loguru.logger.add') as mock_add: + yield mock_add # This makes the mock available to tests + + +@pytest.fixture() +def get_settings(): + return Settings(_env_file=".env.test") + + +@pytest.fixture(autouse=True) +def mock_settings(): + with patch('app.shared.settings.Settings', return_value=Settings(_env_file=".env.test")) as mock_settings: + yield mock_settings + + +@pytest.fixture() +def test_db(get_settings: Settings): + from app.shared.db import models + from app.shared.db.database import make_engine + from app.web.db.crud import get_user_group_names + + get_user_group_names.cache_clear() + make_engine.cache_clear() + engine = make_engine(get_settings.DATABASE_PATH) + + fs = get_settings.DATABASE_PATH.replace("sqlite:///", "") + if not os.path.exists(fs): + open(fs, 'w').close() + + models.Base.metadata.create_all(engine) + + connection = engine.connect() + yield connection + connection.close() + + models.Base.metadata.drop_all(bind=engine) + for suffix in ["", "-wal", "-shm"]: + new_fs = fs + suffix + if os.path.exists(new_fs): + os.remove(new_fs) + + +@pytest.fixture() +def db_session(test_db): + from app.shared.db.database import make_session_local + session_local = make_session_local(test_db) + with session_local() as session: + yield session + + +@pytest_asyncio.fixture() +async def async_test_db(get_settings: Settings): + from app.shared.db import models + from app.shared.db.database import make_async_engine + from app.web.db.crud import get_user_group_names + import asyncio + + get_user_group_names.cache_clear() + engine = await make_async_engine(get_settings.ASYNC_DATABASE_PATH) + + fs = get_settings.ASYNC_DATABASE_PATH.replace("sqlite+aiosqlite:///", "") + if not os.path.exists(fs): + open(fs, 'w').close() + + async def create_all(): + async with engine.begin() as conn: + await conn.run_sync(models.Base.metadata.create_all) + + await create_all() + + yield engine + + async def drop_all(): + async with engine.begin() as conn: + await conn.run_sync(models.Base.metadata.drop_all) + + await drop_all() + + engine.dispose() + for suffix in ["", "-wal", "-shm"]: + new_fs = fs + suffix + if os.path.exists(new_fs): + os.remove(new_fs) + + +@pytest_asyncio.fixture() +async def async_db_session(async_test_db: AsyncEngine) -> AsyncGenerator[AsyncSession, None]: + from app.shared.db.database import make_async_session_local + session_local = await make_async_session_local(async_test_db) + async with session_local() as session: + yield session + + +@pytest.fixture() +def app(db_session): + from app.web.main import app_factory + from app.web.db import crud + app = app_factory() + crud.upsert_user_groups(db_session) + return app + + +@pytest.fixture() +def client(app): + client = TestClient(app) + return client + + +@pytest.fixture() +def app_with_auth(app, db_session): + from app.web.security import get_token_or_user_auth, get_user_auth, get_user_state + app.dependency_overrides[get_token_or_user_auth] = lambda: "rick@example.com" + app.dependency_overrides[get_user_auth] = lambda: "morty@example.com" + app.dependency_overrides[get_user_state] = lambda: UserState(db_session, "MORTY@example.com") + return app + + +@pytest.fixture() +def client_with_auth(app_with_auth): + client = TestClient(app_with_auth) + return client + + +@pytest.fixture() +def app_with_token(app): + from app.web.security import token_api_key_auth, get_token_or_user_auth + app.dependency_overrides[token_api_key_auth] = lambda: ALLOW_ANY_EMAIL + app.dependency_overrides[get_token_or_user_auth] = lambda: ALLOW_ANY_EMAIL + return app + + +@pytest.fixture() +def client_with_token(app_with_token): + client = TestClient(app_with_token) + return client + + +@pytest.fixture() +def test_no_auth(): + # reusable code to ensure a method/endpoint combination is unauthorized + def no_auth(http_method, endpoint): + response = http_method(endpoint) + assert response.status_code == 403 + assert response.json() == {"detail": "Not authenticated"} + return no_auth diff --git a/app/tests/fake_service_account.json b/app/tests/fake_service_account.json new file mode 100644 index 0000000..3d41bd9 --- /dev/null +++ b/app/tests/fake_service_account.json @@ -0,0 +1,3 @@ +{ + "client_email": "fake_service_account@fake_service_account.iam.gserviceaccount.com" +} \ No newline at end of file diff --git a/src/tests/orchestration.test.yaml b/app/tests/orchestration.test.yaml similarity index 85% rename from src/tests/orchestration.test.yaml rename to app/tests/orchestration.test.yaml index cb79ea9..4ee1880 100644 --- a/src/tests/orchestration.test.yaml +++ b/app/tests/orchestration.test.yaml @@ -12,6 +12,8 @@ steps: - console_db configurations: + gsheet_feeder: + service_account: "app/tests/fake_service_account.json" cli_feeder: urls: - "url1" diff --git a/src/tests/db/test_models.py b/app/tests/shared/db/test_models.py similarity index 75% rename from src/tests/db/test_models.py rename to app/tests/shared/db/test_models.py index d5ced1e..35ba368 100644 --- a/src/tests/db/test_models.py +++ b/app/tests/shared/db/test_models.py @@ -1,5 +1,5 @@ def test_generate_uuid(): - from db.models import generate_uuid + from app.shared.db.models import generate_uuid assert generate_uuid() != generate_uuid() assert len(generate_uuid()) == 36 diff --git a/app/tests/shared/db/test_worker_crud.py b/app/tests/shared/db/test_worker_crud.py new file mode 100644 index 0000000..1098cbe --- /dev/null +++ b/app/tests/shared/db/test_worker_crud.py @@ -0,0 +1,117 @@ +from app.shared.db import models +from app.shared.db import worker_crud, models +from datetime import datetime + + +from app.tests.web.db.test_crud import test_data + +def test_update_sheet_last_url_archived_at(db_session): + + # Create test sheet + test_sheet = models.Sheet(id="sheet-123") + db_session.add(test_sheet) + db_session.commit() + + # Test updating existing sheet + assert isinstance(test_sheet.last_url_archived_at, datetime) + before = test_sheet.last_url_archived_at + assert worker_crud.update_sheet_last_url_archived_at(db_session, "sheet-123") is True + db_session.refresh(test_sheet) + assert isinstance(test_sheet.last_url_archived_at, datetime) + assert test_sheet.last_url_archived_at > before + + # Test non-existent sheet + assert worker_crud.update_sheet_last_url_archived_at(db_session, "non-existent-sheet") is False + +def test_get_group(test_data, db_session): + from app.shared.db import worker_crud + + assert worker_crud.get_group(db_session, "spaceship") is not None + assert worker_crud.get_group(db_session, "interdimensional") is not None + assert worker_crud.get_group(db_session, "animated-characters") is not None + assert worker_crud.get_group(db_session, "non-existent!@#!%!") is None + + +def test_create_or_get_user(test_data, db_session): + from app.shared.db import worker_crud + + assert db_session.query(models.User).count() == 3 + + # already exists + assert (u1 := worker_crud.create_or_get_user(db_session, "rick@example.com")) is not None + assert u1.email == "rick@example.com" + + # new user + assert (u2 := worker_crud.create_or_get_user(db_session, "beth@example.com")) is not None + assert u2.email == "beth@example.com" + + assert db_session.query(models.User).count() == 4 + + +def test_create_tag(db_session): + from app.shared.db import worker_crud + + assert db_session.query(models.Tag).count() == 0 + + # create first + create_tag = worker_crud.create_tag(db_session, "tag-101") + assert create_tag is not None + assert create_tag.id == "tag-101" + assert db_session.query(models.Tag).count() == 1 + assert db_session.query(models.Tag).filter(models.Tag.id == "tag-101").first() == create_tag + + # same id does not add new db entry + existing_tag = worker_crud.create_tag(db_session, "tag-101") + assert existing_tag == create_tag + assert db_session.query(models.Tag).count() == 1 + + # create second + second_tag = worker_crud.create_tag(db_session, "tag-102") + assert second_tag is not None + assert second_tag.id == "tag-102" + assert db_session.query(models.Tag).count() == 2 + + +def test_create_task(db_session): + from app.shared.db import worker_crud + from app.shared import schemas + + task = schemas.ArchiveCreate( + id="archive-id-456-101", + url="https://example-0.com", + result={}, + public=False, + author_id="rick@example.com", + group_id="spaceship", + tags=[], + urls=[] + ) + + # with tags and urls + nt = worker_crud.create_archive(db_session, task, [models.Tag(id="tag-101")], [models.ArchiveUrl(url="https://example-0.com/0", key="media_0")]) + + assert nt is not None + assert nt.id == "archive-id-456-101" + assert nt.url == "https://example-0.com" + assert nt.author_id == "rick@example.com" + assert nt.public == False + assert nt.group_id == "spaceship" + assert len(nt.tags) == 1 + assert nt.tags[0].id == "tag-101" + assert len(nt.urls) == 1 + assert nt.urls[0].url == "https://example-0.com/0" + assert nt.urls[0].key == "media_0" + assert nt.created_at is not None + + # without tags and urls + task.id = "archive-id-456-102" + nt = worker_crud.create_archive(db_session, task, [], []) + assert nt is not None + assert nt.id == "archive-id-456-102" + assert nt.url == "https://example-0.com" + assert nt.author_id == "rick@example.com" + assert nt.public == False + assert nt.group_id == "spaceship" + assert len(nt.tags) == 0 + assert len(nt.urls) == 0 + assert nt.created_at is not None \ No newline at end of file diff --git a/app/tests/shared/test_business_logic.py b/app/tests/shared/test_business_logic.py new file mode 100644 index 0000000..80eecb3 --- /dev/null +++ b/app/tests/shared/test_business_logic.py @@ -0,0 +1,36 @@ +from datetime import datetime, timedelta +from unittest.mock import MagicMock, patch +import pytest +from app.shared.business_logic import get_store_archive_until + +class Test_get_store_archive_until: + GROUP_ID = "test-group" + + def test_group_not_found(self, db_session): + with pytest.raises(AssertionError) as exc: + get_store_archive_until(db_session, self.GROUP_ID) + assert str(exc.value) == f"Group {self.GROUP_ID} not found." + + @patch("app.shared.db.worker_crud.get_group") + def test_no_max_lifespan(self, mock_get_group, db_session): + group = MagicMock() + group.permissions = {"max_archive_lifespan_months": -1} + mock_get_group.return_value = group + + result = get_store_archive_until(db_session, self.GROUP_ID) + assert result is None + mock_get_group.assert_called_once_with(db_session, self.GROUP_ID) + + @patch("app.shared.db.worker_crud.get_group") + def test_with_max_lifespan(self, mock_get_group, db_session): + group = MagicMock() + group.permissions = {"max_archive_lifespan_months": 6} + mock_get_group.return_value = group + + result = get_store_archive_until(db_session, self.GROUP_ID) + expected = datetime.now() + timedelta(days=180) # 6 months + + assert isinstance(result, datetime) + # Allow 1 second difference due to execution time + assert abs(result - expected) < timedelta(seconds=1) + mock_get_group.assert_called_once_with(db_session, self.GROUP_ID) \ No newline at end of file diff --git a/app/tests/shared/utils/test_misc.py b/app/tests/shared/utils/test_misc.py new file mode 100644 index 0000000..d7595c8 --- /dev/null +++ b/app/tests/shared/utils/test_misc.py @@ -0,0 +1,31 @@ +from app.shared.utils.misc import fnv1a_hash_mod + + +def test_fnv1a_hash_mod(): + # Test basic string hashing + assert fnv1a_hash_mod("test", 10) == fnv1a_hash_mod("test", 10) + assert 0 <= fnv1a_hash_mod("test", 10) < 10 + + # Test different strings give different hashes + assert fnv1a_hash_mod("test1", 100) != fnv1a_hash_mod("test2", 100) + + # Test different modulos + hash1 = fnv1a_hash_mod("test", 5) + hash2 = fnv1a_hash_mod("test", 10) + assert 0 <= hash1 < 5 + assert 0 <= hash2 < 10 + + # Test empty string + assert isinstance(fnv1a_hash_mod("", 10), int) + assert 0 <= fnv1a_hash_mod("", 10) < 10 + + # Test long string + long_str = "a" * 1000 + assert 0 <= fnv1a_hash_mod(long_str, 20) < 20 + + # Test unicode string + assert isinstance(fnv1a_hash_mod("测试", 10), int) + assert 0 <= fnv1a_hash_mod("测试", 10) < 10 + + # Test modulo = 1 edge case + assert fnv1a_hash_mod("test", 1) == 0 \ No newline at end of file diff --git a/src/tests/user-groups.test.broken.yaml b/app/tests/user-groups.test.broken.yaml similarity index 100% rename from src/tests/user-groups.test.broken.yaml rename to app/tests/user-groups.test.broken.yaml diff --git a/app/tests/user-groups.test.yaml b/app/tests/user-groups.test.yaml new file mode 100644 index 0000000..16a3ba7 --- /dev/null +++ b/app/tests/user-groups.test.yaml @@ -0,0 +1,87 @@ +# NOTE: all emails should be lower-cased +users: + rick@example.com: + - spaceship + - interdimensional + morty@example.com: + - spaceship + jerry@example.com: + - the-jerrys-club + # summer@herself.com: + # badyemail.com: + +domains: + example.com: + - animated-characters + birdy.com: + - animated-characters + - this-does-not-exist + + +orchestrators: + spaceship: app/tests/orchestration.test.yaml + interdimensional: app/tests/orchestration.test.yaml + default: app/tests/orchestration.test.yaml + +default_orchestrator: app/tests/orchestration.test.yaml + +groups: + spaceship: + description: "The spaceship crew" + orchestrator: app/tests/orchestration.test.yaml + orchestrator_sheet: app/tests/orchestration.test.yaml + permissions: + read: ["all"] + archive_url: true + archive_sheet: true + manually_trigger_sheet: true + sheet_frequency: ["hourly", "daily"] + max_sheets: -1 + max_archive_lifespan_months: -1 + max_monthly_urls: -1 + max_monthly_mbs: -1 + priority: "high" + interdimensional: + description: "Interdimensional travelers" + orchestrator: app/tests/orchestration.test.yaml + orchestrator_sheet: app/tests/orchestration.test.yaml + permissions: + read: ["interdimensional", "animated-characters"] + archive_url: true + archive_sheet: true + manually_trigger_sheet: true + sheet_frequency: ["hourly", "daily"] + max_sheets: 5 + max_archive_lifespan_months: 12 + max_monthly_urls: 1000 + max_monthly_mbs: 1000 + priority: "high" + animated-characters: + description: "Animated characters" + orchestrator: app/tests/orchestration.test.yaml + orchestrator_sheet: app/tests/orchestration.test.yaml + permissions: + read: ["animated-characters"] + archive_url: true + archive_sheet: true + sheet_frequency: ["daily"] + max_sheets: 1 + max_archive_lifespan_months: 12 + max_monthly_urls: 2 + max_monthly_mbs: 10 + priority: "low" + default: + description: "Public access" + orchestrator: app/tests/orchestration.test.yaml + orchestrator_sheet: app/tests/orchestration.test.yaml + permissions: + # read: [] + archive_url: true + # manually_trigger_sheet: false + # archive_sheet: false + # sheet_frequency: [] + # max_sheets: 0 + # max_archive_lifespan_months: 12 + max_monthly_urls: 1 + # max_monthly_mbs: 50 + priority: "low" \ No newline at end of file diff --git a/app/tests/web/db/test_crud.py b/app/tests/web/db/test_crud.py new file mode 100644 index 0000000..aad9d4c --- /dev/null +++ b/app/tests/web/db/test_crud.py @@ -0,0 +1,438 @@ +from datetime import datetime, timedelta +from unittest.mock import patch + +import pytest +import yaml +from app.shared.db import models +from app.shared.settings import Settings + +from app.web.db import crud +authors = ["rick@example.com", "morty@example.com", "jerry@example.com"] + + +@pytest.fixture() +def test_data(db_session): + + # creates 3 users + for email in authors: + db_session.add(models.User(email=email)) + db_session.commit() + assert db_session.query(models.User).count() == 3 + + # creates 100 archives for 3 users over 2 months with repeating URLs + for i in range(100): + author = authors[i % 3] + archive = models.Archive( + id=f"archive-id-456-{i}", + url=f"https://example-{i%3}.com", + result={}, + public=author == "jerry@example.com", + author_id=author, + group_id="spaceship" if author == "morty@example.com" and i % 2 == 0 else None, + created_at=datetime(2021, (i % 2) + 1, (i % 25) + 1) + ) + if i % 5 == 0: + archive.tags.append(models.Tag(id=f"tag-{i}")) + if i % 10 == 0: + archive.tags.append(models.Tag(id=f"tag-second-{i}")) + if i % 4 == 0: + archive.tags.append(models.Tag(id=f"tag-third-{i}")) + for j in range(10): + archive.urls.append(models.ArchiveUrl(url=f"https://example-{i}.com/{j}", key=f"media_{j}")) + db_session.add(archive) + + # creates a sheet for each user + for i, email in enumerate(authors): + db_session.add(models.Sheet(id=f"sheet-{i}", name=f"sheet-{i}", author_id=email, group_id=None, frequency="daily")) + if email == "rick@example.com": + db_session.add(models.Sheet(id=f"sheet-{i}-2", name=f"sheet-{i}-2", author_id=email, group_id="spaceship", frequency="hourly")) + + db_session.commit() + + assert db_session.query(models.Archive).count() == 100 + assert db_session.query(models.Tag).count() == 20 + 10 + 25 + assert db_session.query(models.ArchiveUrl).count() == 1000 + assert db_session.query(models.ArchiveUrl).filter(models.ArchiveUrl.archive_id == "archive-id-456-0").count() == 10 + + # setup groups + assert db_session.query(models.Group).count() == 0 + from app.web.db import crud + crud.upsert_user_groups(db_session) + assert db_session.query(models.Group).count() == 4 + assert db_session.query(models.User).count() == 3 + + +def test_search_archives_by_url(test_data, db_session): + from app.web.config import ALLOW_ANY_EMAIL + + # rick's archives are private + assert len(crud.search_archives_by_url(db_session, "https://example-0.com", "rick@example.com", True, False)) == 34 + assert len(crud.search_archives_by_url(db_session, "https://example-0.com", "rick@example.com", [], False)) == 34 + assert len(crud.search_archives_by_url(db_session, "https://example-0.com", "rick@example.com", [], True)) == 34 + assert len(crud.search_archives_by_url(db_session, "https://example-0.com", ALLOW_ANY_EMAIL, [], False)) == 34 + assert len(crud.search_archives_by_url(db_session, "https://example-0.com", ALLOW_ANY_EMAIL, True, False)) == 34 + assert len(crud.search_archives_by_url(db_session, "https://example-0.com", "morty@example.com", [], False)) == 0 + assert len(crud.search_archives_by_url(db_session, "https://example-0.com", "morty@example.com", [], True)) == 0 + + # morty's archives are public but half are in spaceship group + assert len(crud.search_archives_by_url(db_session, "https://example-1.com", "rick@example.com", ["spaceship"], False)) == 16 + assert len(crud.search_archives_by_url(db_session, "https://example-1.com", "rick@example.com", True, False)) == 16 + assert len(crud.search_archives_by_url(db_session, "https://example-1.com", "jerry@example.com", True, True)) == 16 + + # jerry's archives are public + assert len(crud.search_archives_by_url(db_session, "https://example-2.com", "jerry@example.com", [], True)) == 33 + assert len(crud.search_archives_by_url(db_session, "https://example-2.com", "rick@example.com", [], True)) == 33 + + # fuzzy search + assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, False, False)) == 100 + assert len(crud.search_archives_by_url(db_session, "https://EXAMPLE", ALLOW_ANY_EMAIL, False, False)) == 100 + assert len(crud.search_archives_by_url(db_session, "2.com", ALLOW_ANY_EMAIL, False, False)) == 33 + + # absolute search + assert len(crud.search_archives_by_url(db_session, "example-2.com", ALLOW_ANY_EMAIL, [], False, absolute_search=True)) == 0 + assert len(crud.search_archives_by_url(db_session, "https://example-2.com", ALLOW_ANY_EMAIL, [], False, absolute_search=True)) == 33 + + # archived_after + assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, True, True, archived_after=datetime(2010, 1, 1))) == 100 + assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, False, False, archived_after=datetime(2021, 1, 15))) == 70 + assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, False, False, archived_after=datetime(2031, 1, 1))) == 0 + + # archived before + assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, False, False, archived_before=datetime(2010, 1, 1))) == 0 + assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, False, False, archived_before=datetime(2021, 1, 15))) == 28 + assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, False, False, archived_before=datetime(2031, 1, 1))) == 100 + + # archived before and after + assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, False, False, archived_after=datetime(2001, 1, 1), archived_before=datetime(2031, 1, 11))) == 100 + assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, False, False, archived_after=datetime(2021, 1, 14), archived_before=datetime(2021, 1, 16))) == 2 + + # limit + assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, False, False, limit=10)) == 10 + assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, False, False, limit=-1)) == 1 + + # skip + assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, False, False, skip=10)) == 90 + + +def test_search_archives_by_email(test_data, db_session): + from app.web.config import ALLOW_ANY_EMAIL + + # lower/upper case + assert len(crud.search_archives_by_email(db_session, "rick@example.com")) == 34 + + # ALLOW_ANY_EMAIL is not a user + assert len(crud.search_archives_by_email(db_session, ALLOW_ANY_EMAIL)) == 0 + + # most recent first + a1 = crud.search_archives_by_email(db_session, "rick@example.com", limit=1) + assert len(a1) == 1 + assert a1[0].created_at == datetime(2021, 2, 25) + + # earliest is the last + a2 = crud.search_archives_by_email(db_session, "rick@example.com", skip=33) + assert len(a2) == 1 + assert a2[0].created_at == datetime(2021, 1, 1) + + +@patch("app.web.db.crud.DATABASE_QUERY_LIMIT", new=25) +def test_max_query_limit(test_data, db_session): + from app.web.config import ALLOW_ANY_EMAIL + + assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, [], False)) == 25 + assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, True, True, limit=1000)) == 25 + + assert len(crud.search_archives_by_email(db_session, "rick@example.com")) == 25 + assert len(crud.search_archives_by_email(db_session, "rick@example.com", limit=1000)) == 25 + + +def test_soft_delete(test_data, db_session): + # none deleted yet + db_session.query(models.Archive).filter(models.Archive.id == "archive-id-456-0").first() is not None + assert db_session.query(models.Archive).filter(models.Archive.deleted == True).count() == 0 + + # delete + assert crud.soft_delete_archive(db_session, "archive-id-456-0", "rick@example.com") == True + + # ensure soft delete + assert db_session.query(models.Archive).filter(models.Archive.deleted == True).count() == 1 + db_session.query(models.Archive).filter(models.Archive.id == "archive-id-456-0").first() is None + + # already deleted + assert crud.soft_delete_archive(db_session, "archive-id-456-0", "rick@example.com") == False + + +def test_count_archives(test_data, db_session): + assert crud.count_archives(db_session) == 100 + db_session.query(models.Archive).filter(models.Archive.id == "archive-id-456-0").delete() + db_session.commit() + assert crud.count_archives(db_session) == 99 + + +def test_count_archive_urls(test_data, db_session): + assert crud.count_archive_urls(db_session) == 1000 + db_session.query(models.ArchiveUrl).filter(models.ArchiveUrl.url == "https://example-0.com/0").delete() + db_session.commit() + assert crud.count_archive_urls(db_session) == 999 + + db_session.query(models.Archive).filter(models.Archive.id == "archive-id-456-0").delete() + db_session.commit() + # no Cascade is enabled + assert crud.count_archives(db_session) == 99 + assert crud.count_archive_urls(db_session) == 999 + + +def test_count_users(test_data, db_session): + assert crud.count_users(db_session) == 3 + db_session.query(models.User).filter(models.User.email == "rick@example.com").delete() + db_session.commit() + assert crud.count_users(db_session) == 2 + + +def test_count_by_users_since(test_data, db_session): + from app.web.db import crud + + # 100y window + assert len(cu := crud.count_by_user_since(db_session, 60 * 60 * 24 * 31 * 12 * 100)) == 3 + assert cu[0].total == 34 + assert cu[1].total == 33 + assert cu[2].total == 33 + + +def test_upsert_group(test_data, db_session): + assert db_session.query(models.Group).count() == 4 + + repeatable_params = ["desc 1", "orch.yaml", "sheet.yaml", "service_account_email@example.com", {"read": ["all"]}, ["example.com"]] + + assert (g1 := crud.upsert_group(db_session, "spaceship", *repeatable_params)) is not None + assert g1.id == "spaceship" + assert g1.description == "desc 1" + assert g1.orchestrator == "orch.yaml" + assert g1.orchestrator_sheet == "sheet.yaml" + assert g1.service_account_email == "service_account_email@example.com" + assert g1.permissions == {"read": ["all"]} + assert g1.domains == ["example.com"] + assert len(g1.users) == 2 + assert [u.email for u in g1.users] == ["rick@example.com", "morty@example.com"] + + assert (g2 := crud.upsert_group(db_session, "interdimensional", *repeatable_params)) is not None + assert g2.id == "interdimensional" + assert len(g2.users) == 1 + assert [u.email for u in g2.users] == ["rick@example.com"] + + assert (g3 := crud.upsert_group(db_session, "this-is-a-new-group", *repeatable_params)) is not None + assert g3.id == "this-is-a-new-group" + assert len(g3.users) == 0 + + assert db_session.query(models.Group).count() == 5 + + +def test_upsert_user_groups(db_session): + @patch('app.web.db.crud.get_settings', new=lambda: bad_setings) + def test_missing_yaml(db_session): + with pytest.raises(FileNotFoundError): + crud.upsert_user_groups(db_session) + + @patch('app.web.db.crud.get_settings', new=lambda: bad_setings) + def test_broken_yaml(db_session): + with pytest.raises(yaml.YAMLError): + crud.upsert_user_groups(db_session) + + bad_setings = Settings(_env_file=".env.test") + + bad_setings.USER_GROUPS_FILENAME = "app/tests/user-groups.test.missing.yaml" + test_missing_yaml(db_session) + + bad_setings.USER_GROUPS_FILENAME = "app/tests/user-groups.test.broken.yaml" + test_broken_yaml(db_session) + + +def test_create_sheet(db_session): + assert db_session.query(models.Sheet).count() == 0 + + s = crud.create_sheet(db_session, "sheet-id-123", "sheet name", "email@example.com", "group-id", "hourly") + assert s is not None + assert s.id == "sheet-id-123" + assert s.name == "sheet name" + assert s.author_id == "email@example.com" + assert s.group_id == "group-id" + assert s.frequency == "hourly" + + assert db_session.query(models.Sheet).count() == 1 + + # duplicate id + import sqlalchemy + with pytest.raises(sqlalchemy.exc.IntegrityError): + crud.create_sheet(db_session, "sheet-id-123", "I thought this was another sheet", "email", "group-id", "hourly") + + +def test_get_user_sheet(test_data, db_session): + assert crud.get_user_sheet(db_session, "", "sheet-0") is None + assert crud.get_user_sheet(db_session, "morty@example.com", "sheet-0") is None + + assert crud.get_user_sheet(db_session, "rick@example.com", "sheet-0") is not None + assert crud.get_user_sheet(db_session, "rick@example.com", "sheet-0-2") is not None + assert crud.get_user_sheet(db_session, "morty@example.com", "sheet-1") is not None + + +def test_get_user_sheets(test_data, db_session): + assert len(crud.get_user_sheets(db_session, "")) == 0 + rick_sheets = crud.get_user_sheets(db_session, "rick@example.com") + assert len(rick_sheets) == 2 + assert [s.id for s in rick_sheets] == ["sheet-0", "sheet-0-2"] + assert len(crud.get_user_sheets(db_session, "morty@example.com")) == 1 + + +def test_delete_sheet(test_data, db_session): + assert crud.delete_sheet(db_session, "sheet-0", "") == False + assert crud.delete_sheet(db_session, "sheet-0", "rick@example.com") == True + assert crud.delete_sheet(db_session, "sheet-0", "rick@example.com") == False + + +@pytest.mark.asyncio +async def test_find_by_store_until(async_db_session): + # Add archives with different store_until dates + now = datetime.now() + archive1 = models.Archive( + id="archive-expired-1", + url="https://example-expired-1.com", + result={}, + author_id="rick@example.com", + store_until=now - timedelta(days=1) + ) + archive2 = models.Archive( + id="archive-expired-2", + url="https://example-expired-2.com", + result={}, + author_id="rick@example.com", + store_until=now - timedelta(hours=1) + ) + archive3 = models.Archive( + id="archive-active", + url="https://example-active.com", + result={}, + author_id="rick@example.com", + store_until=now + timedelta(days=1) + ) + async_db_session.add_all([archive1, archive2, archive3]) + await async_db_session.commit() + + # Should find 2 expired archives + expired = await crud.find_by_store_until(async_db_session, now) + assert len(list(expired)) == 2 + + # Should find 1 archive expired before 2 hours ago + expired = await crud.find_by_store_until(async_db_session, now - timedelta(hours=2)) + assert len(list(expired)) == 1 + + # Should find no archives expired before 2 days ago + expired = await crud.find_by_store_until(async_db_session, now - timedelta(days=2)) + assert len(list(expired)) == 0 + + # Should not find deleted archives + archive1.deleted = True + await async_db_session.commit() + expired = await crud.find_by_store_until(async_db_session, now) + assert len(list(expired)) == 1 + + +@pytest.mark.asyncio +async def test_get_sheets_by_id_hash(async_db_session): + # Add test data + authors = ["rick@example.com", "morty@example.com", "jerry@example.com"] + sheets = [ + models.Sheet(id="sheet-0", name="sheet-0", author_id=authors[0], group_id=None, frequency="daily"), + models.Sheet(id="sheet-0-2", name="sheet-0-2", author_id=authors[0], group_id="spaceship", frequency="hourly"), + models.Sheet(id="sheet-1", name="sheet-1", author_id=authors[1], group_id=None, frequency="daily"), + models.Sheet(id="sheet-2", name="sheet-2", author_id=authors[2], group_id=None, frequency="daily") + ] + async_db_session.add_all(sheets) + await async_db_session.commit() + + with patch("app.web.db.crud.fnv1a_hash_mod", return_value=1): + # Test retrieving hourly sheets + hourly_sheets = await crud.get_sheets_by_id_hash(async_db_session, "hourly", 4, 1) + assert len(hourly_sheets) == 1 + assert hourly_sheets[0].id == "sheet-0-2" + assert hourly_sheets[0].frequency == "hourly" + + # Test retrieving daily sheets + daily_sheets = await crud.get_sheets_by_id_hash(async_db_session, "daily", 4, 1) + assert len(daily_sheets) == 3 + assert all(sheet.frequency == "daily" for sheet in daily_sheets) + assert {sheet.id for sheet in daily_sheets} == {"sheet-0", "sheet-1", "sheet-2"} + + # Test with non-matching hash + no_sheets = await crud.get_sheets_by_id_hash(async_db_session, "daily", 4, 3) + assert len(no_sheets) == 0 + + # Test with non-existent frequency + weekly_sheets = await crud.get_sheets_by_id_hash(async_db_session, "weekly", 4, 1) + assert len(weekly_sheets) == 0 + + +@pytest.mark.asyncio +async def test_delete_stale_sheets(async_db_session): + from datetime import datetime, timedelta + from sqlalchemy.sql import select + + now = datetime.now() + active_date = now - timedelta(days=5) + stale_date = now - timedelta(days=15) + + # Create test sheets with different last_url_archived_at dates + sheets = [ + models.Sheet( + id="sheet-active-1", + name="Active Sheet 1", + author_id="rick@example.com", + frequency="daily", + last_url_archived_at=active_date + ), + models.Sheet( + id="sheet-active-2", + name="Active Sheet 2", + author_id="morty@example.com", + frequency="hourly", + last_url_archived_at=active_date + ), + models.Sheet( + id="sheet-stale-1", + name="Stale Sheet 1", + author_id="rick@example.com", + frequency="daily", + last_url_archived_at=stale_date + ), + models.Sheet( + id="sheet-stale-2", + name="Stale Sheet 2", + author_id="morty@example.com", + frequency="daily", + last_url_archived_at=stale_date + ) + ] + async_db_session.add_all(sheets) + await async_db_session.commit() + + # Should not delete sheets with 20 days inactivity threshold + deleted = await crud.delete_stale_sheets(async_db_session, 20) + assert len(deleted) == 0 # No sheets should be deleted + result = await async_db_session.execute(select(models.Sheet)) + assert len(list(result.scalars())) == 4 # All sheets should remain + + # Should delete sheets with 7 days inactivity threshold + deleted = await crud.delete_stale_sheets(async_db_session, 7) + assert len(deleted) == 2 # Two authors affected + assert len(deleted["rick@example.com"]) == 1 # One sheet deleted for Rick + assert len(deleted["morty@example.com"]) == 1 # One sheet deleted for Morty + assert deleted["rick@example.com"][0].id == "sheet-stale-1" + assert deleted["morty@example.com"][0].id == "sheet-stale-2" + + # Verify only active sheets remain + result = await async_db_session.execute(select(models.Sheet)) + remaining = list(result.scalars()) + assert len(remaining) == 2 + assert {s.id for s in remaining} == {"sheet-active-1", "sheet-active-2"} + + # Running again should not delete anything + deleted = await crud.delete_stale_sheets(async_db_session, 7) + assert len(deleted) == 0 \ No newline at end of file diff --git a/app/tests/web/db/test_user_state.py b/app/tests/web/db/test_user_state.py new file mode 100644 index 0000000..42c61d1 --- /dev/null +++ b/app/tests/web/db/test_user_state.py @@ -0,0 +1,433 @@ + +from unittest.mock import MagicMock, PropertyMock, patch +import pytest + +from app.shared.db import models +from app.shared.user_groups import GroupInfo, GroupPermissions +from app.web.db.user_state import UserState + + +def fresh_user_state(): + return UserState(None, email="test@example.com") + + +@pytest.fixture +def user_state(): + return fresh_user_state() + + +@pytest.fixture +def user_state_with_groups(user_state): + user_groups = [ + models.Group(id="no-permissions", permissions={}), + models.Group(id="group1", description="this is g1", service_account_email="sa1@example.com", permissions={"read": ["group1", "no-permissions"], "read_public": True, "archive_url": True, "archive_sheet": True, "max_archive_lifespan_months": 24, "max_monthly_urls": 100, "max_monthly_mbs": 1000, "priority": "high"}), + models.Group(id="group2", description="this is g2", service_account_email="sa2@example.com", permissions={"read": ["all"], "read_public": True, "archive_url": False, "archive_sheet": False, "max_archive_lifespan_months": -1, "max_monthly_urls": -1, "max_monthly_mbs": -1, "priority": "low", "sheet_frequency": {"daily"}}), + ] + + with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=user_groups): + yield user_state + + +def test_permissions(user_state_with_groups): + permissions = user_state_with_groups.permissions + + assert permissions["all"].read == True + assert permissions["all"].read_public == True + assert permissions["all"].archive_url == True + assert permissions["all"].archive_sheet == True + assert permissions["all"].max_archive_lifespan_months == -1 + assert permissions["all"].max_monthly_urls == -1 + assert permissions["all"].max_monthly_mbs == -1 + assert permissions["all"].priority == "high" + + assert permissions["group1"].read == set(["group1", "no-permissions"]) + assert permissions["group1"].read_public == True + assert permissions["group1"].archive_url == True + assert permissions["group1"].archive_sheet == True + assert permissions["group1"].max_archive_lifespan_months == 24 + assert permissions["group1"].max_monthly_urls == 100 + assert permissions["group1"].max_monthly_mbs == 1000 + assert permissions["group1"].priority == "high" + + assert permissions["group2"].read == set(["all"]) + assert permissions["group2"].read_public == True + assert permissions["group2"].archive_url == False + assert permissions["group2"].archive_sheet == False + assert permissions["group2"].max_archive_lifespan_months == -1 + assert permissions["group2"].max_monthly_urls == -1 + assert permissions["group2"].max_monthly_mbs == -1 + assert permissions["group2"].priority == "low" + + assert len(permissions) == 3 + + +def test_user_groups_names(user_state): + with patch('app.web.db.crud.get_user_group_names', return_value=["group1", "group2"]) as mock: + assert user_state.user_groups_names == ["group1", "group2", "default"] + mock.assert_called_once_with(None, "test@example.com") + + +def test_user_groups(user_state): + with patch('app.web.db.crud.get_user_groups_by_name', return_value=[MagicMock(), MagicMock()]) as mock: + user_state._user_groups_names = ["group1", "group2"] + assert len(user_state.user_groups) == 2 + mock.assert_called_once_with(None, ["group1", "group2"]) + + +def test_read(): + us = fresh_user_state() + + with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="no-permissions", permissions={})]) as mock: + assert not hasattr(us, "_read") + assert us.read == set() + assert us._read == set() + mock.assert_called_once() + + us = fresh_user_state() + with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"read": ["group1", "no-permissions"]})]): + assert us.read == set(["group1", "no-permissions"]) + + us = fresh_user_state() + with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"read": ["all"]})]): + assert us.read == True + + +def test_read_public(): + us = fresh_user_state() + + with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="no-permissions", permissions={})]) as mock: + assert not hasattr(us, "_read_public") + assert us.read_public == False + assert us._read_public == False + mock.assert_called_once() + # no new calls + assert us.read_public == False + mock.assert_called_once() + + us = fresh_user_state() + with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"read_public": True})]): + assert us.read_public == True + + us = fresh_user_state() + with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"read_public": False})]): + assert us.read_public == False + + +def test_archive_url(): + us = fresh_user_state() + with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="no-permissions", permissions={})]) as mock: + assert not hasattr(us, "_archive_url") + assert us.archive_url == False + assert us._archive_url == False + mock.assert_called_once() + # no new calls + assert us.archive_url == False + mock.assert_called_once() + + us = fresh_user_state() + with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"archive_url": False})]): + assert us.archive_url == False + + us = fresh_user_state() + with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"archive_url": True})]): + assert us.archive_url == True + + +def test_archive_sheet(): + us = fresh_user_state() + with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="no-permissions", permissions={})]) as mock: + assert not hasattr(us, "_archive_sheet") + assert us.archive_sheet == False + assert us._archive_sheet == False + mock.assert_called_once() + # no new calls + assert us.archive_sheet == False + mock.assert_called_once() + + us = fresh_user_state() + with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"archive_sheet": False})]): + assert us.archive_sheet == False + + us = fresh_user_state() + with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"archive_sheet": True})]): + assert us.archive_sheet == True + + +def test_sheet_frequency(): + us = fresh_user_state() + with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="no-permissions", permissions={})]) as mock: + assert not hasattr(us, "_sheet_frequency") + assert us.sheet_frequency == set() + assert us._sheet_frequency == set() + mock.assert_called_once() + # no new calls + assert us.sheet_frequency == set() + mock.assert_called_once() + + us = fresh_user_state() + with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"sheet_frequency": ["daily", "hourly"]})]): + assert us.sheet_frequency == {"daily", "hourly"} + + us = fresh_user_state() + with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"sheet_frequency": []})]): + assert us.sheet_frequency == set() + + +def test_max_archive_lifespan_months(): + us = fresh_user_state() + default = GroupPermissions.model_fields["max_archive_lifespan_months"].default + with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="no-permissions", permissions={})]) as mock: + assert not hasattr(us, "_max_archive_lifespan_months") + assert us.max_archive_lifespan_months == default + assert us._max_archive_lifespan_months == default + mock.assert_called_once() + # no new calls + assert us.max_archive_lifespan_months == default + mock.assert_called_once() + + us = fresh_user_state() + with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"max_archive_lifespan_months": 24})]): + assert us.max_archive_lifespan_months == 24 + + us = fresh_user_state() + with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"max_archive_lifespan_months": 150}), models.Group(id="group2", permissions={"max_archive_lifespan_months": -1})]): + assert us.max_archive_lifespan_months == -1 + + +def test_max_monthly_urls(): + us = fresh_user_state() + default = GroupPermissions.model_fields["max_monthly_urls"].default + with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="no-permissions", permissions={})]) as mock: + assert not hasattr(us, "_max_monthly_urls") + assert us.max_monthly_urls == default + assert us._max_monthly_urls == default + mock.assert_called_once() + # no new calls + assert us.max_monthly_urls == default + mock.assert_called_once() + + us = fresh_user_state() + with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"max_monthly_urls": 100})]): + assert us.max_monthly_urls == 100 + + us = fresh_user_state() + with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"max_monthly_urls": 150}), models.Group(id="group2", permissions={"max_monthly_urls": -1})]): + assert us.max_monthly_urls == -1 + + +def test_max_monthly_mbs(): + us = fresh_user_state() + default = GroupPermissions.model_fields["max_monthly_mbs"].default + with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="no-permissions", permissions={})]) as mock: + assert not hasattr(us, "_max_monthly_mbs") + assert us.max_monthly_mbs == default + assert us._max_monthly_mbs == default + mock.assert_called_once() + # no new calls + assert us.max_monthly_mbs == default + mock.assert_called_once() + + us = fresh_user_state() + with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"max_monthly_mbs": 1000})]): + assert us.max_monthly_mbs == 1000 + + us = fresh_user_state() + with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"max_monthly_mbs": 1500}), models.Group(id="group2", permissions={"max_monthly_mbs": -1})]): + assert us.max_monthly_mbs == -1 + + +def test_priority(user_state): + default = GroupPermissions.model_fields["priority"].default + with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="no-permissions", permissions={})]) as mock: + assert not hasattr(user_state, "_priority") + assert user_state.priority == default + assert user_state._priority == default + mock.assert_called_once() + # no new calls + assert user_state.priority == default + mock.assert_called_once() + + us = fresh_user_state() + with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"priority": "high"})]): + assert us.priority == "high" + + us = fresh_user_state() + with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"priority": "low"}), models.Group(id="group2", permissions={"priority": "medium"})]): + assert us.priority == "low" + + +def test_active(): + for read, read_public, archive_url, archive_sheet, is_active in [ + (False, False, False, False, False), + (True, False, False, False, True), + (False, True, False, False, True), + (False, False, True, False, True), + (False, False, False, True, True) + ]: + us = fresh_user_state() + with patch.object(UserState, 'read', new_callable=PropertyMock, return_value=read), \ + patch.object(UserState, 'read_public', new_callable=PropertyMock, return_value=read_public), \ + patch.object(UserState, 'archive_url', new_callable=PropertyMock, return_value=archive_url), \ + patch.object(UserState, 'archive_sheet', new_callable=PropertyMock, return_value=archive_sheet): + assert us.active == is_active + + +def test_in_group(user_state): + with patch.object(UserState, 'user_groups_names', new_callable=PropertyMock, return_value=["group1", "group2"]): + assert user_state.in_group("group1") == True + assert user_state.in_group("group2") == True + assert user_state.in_group("group3") == False + + +def test_usage(db_session): + user_state = UserState(db_session, email="test@example.com") + user_sheets = [ + MagicMock(group_id="group1", sheet_count=5), + MagicMock(group_id="group2", sheet_count=10), + MagicMock(group_id="group3", sheet_count=100), + ] + bytes = [1000000, 2000000, 3000000] + urls_by_group = [ + MagicMock(group_id="group1", url_count=50, total_bytes=bytes[0]), + MagicMock(group_id="group2", url_count=100, total_bytes=bytes[1]), + MagicMock(group_id="group4", url_count=5, total_bytes=bytes[2]), + ] + megabytes = int(sum(bytes) / 1024 / 1024) + + with patch.object(db_session, 'query', side_effect=[ + MagicMock(filter=MagicMock(return_value=MagicMock(group_by=MagicMock(return_value=MagicMock(all=MagicMock(return_value=user_sheets)))))), + MagicMock(filter=MagicMock(return_value=MagicMock(group_by=MagicMock(return_value=MagicMock(all=MagicMock(return_value=urls_by_group)))))) + ]): + usage_response = user_state.usage() + + assert usage_response.monthly_urls == 155 + assert usage_response.monthly_mbs == megabytes + assert usage_response.total_sheets == 115 + + assert usage_response.groups["group1"].monthly_urls == 50 + assert usage_response.groups["group1"].monthly_mbs == int(bytes[0] / 1024 / 1024) + assert usage_response.groups["group1"].total_sheets == 5 + + assert usage_response.groups["group2"].monthly_urls == 100 + assert usage_response.groups["group2"].monthly_mbs == int(bytes[1] / 1024 / 1024) + assert usage_response.groups["group2"].total_sheets == 10 + + assert usage_response.groups["group3"].monthly_urls == 0 + assert usage_response.groups["group3"].monthly_mbs == 0 + assert usage_response.groups["group3"].total_sheets == 100 + + assert usage_response.groups["group4"].monthly_urls == 5 + assert usage_response.groups["group4"].monthly_mbs == int(bytes[2] / 1024 / 1024) + assert usage_response.groups["group4"].total_sheets == 0 + + +def test_has_quota_monthly_sheets(db_session): + us = UserState(db_session, email="test@example.com") + + test_cases = [ + ({"unkonwn": GroupInfo(max_sheets=5)}, 1, False), + ({"group1": GroupInfo(max_sheets=-1)}, 1000, True), + ({"group1": GroupInfo(max_sheets=5)}, 3, True), + ({"group1": GroupInfo(max_sheets=5)}, 5, False), + ({"group1": GroupInfo(max_sheets=5)}, 6, False), + ] + + for permissions, count, expected in test_cases: + with patch.object(UserState, 'permissions', new_callable=PropertyMock, return_value=permissions): + with patch.object(us.db, 'query', return_value=MagicMock(filter=MagicMock(return_value=MagicMock(count=MagicMock(return_value=count))))): + assert us.has_quota_monthly_sheets("group1") == expected + + +def test_has_quota_max_monthly_urls(db_session): + us = UserState(db_session, email="test@example.com") + + test_cases = [ + ({"group1": GroupInfo(max_monthly_urls=-1)}, 1000, True), + ({"group1": GroupInfo(max_monthly_urls=100)}, 50, True), + ({"group1": GroupInfo(max_monthly_urls=100)}, 100, False), + ({"group1": GroupInfo(max_monthly_urls=100)}, 150, False), + ] + + for permissions, count, expected in test_cases: + with patch.object(UserState, 'permissions', new_callable=PropertyMock, return_value=permissions): + with patch.object(us.db, 'query', return_value=MagicMock(filter=MagicMock(return_value=MagicMock(count=MagicMock(return_value=count))))): + assert us.has_quota_max_monthly_urls("group1") == expected + test_cases = [ + (-1, 1000, True), + (100, 50, True), + (100, 100, False), + (100, 150, False), + ] + + for max_urls, count, expected in test_cases: + with patch.object(UserState, 'max_monthly_urls', new_callable=PropertyMock, return_value=max_urls): + with patch.object(us.db, 'query', return_value=MagicMock(filter=MagicMock(return_value=MagicMock(count=MagicMock(return_value=count))))): + assert us.has_quota_max_monthly_urls("") == expected + + +def test_has_quota_max_monthly_mbs(db_session): + us = UserState(db_session, email="test@example.com") + + test_cases = [ + ({"group1": GroupInfo(max_monthly_mbs=-1)}, 1000, True), + ({"group1": GroupInfo(max_monthly_mbs=100)}, 50, True), + ({"group1": GroupInfo(max_monthly_mbs=100)}, 100, False), + ({"group1": GroupInfo(max_monthly_mbs=100)}, 150, False), + ] + + for permissions, mbs, expected in test_cases: + with patch.object(UserState, 'permissions', new_callable=PropertyMock, return_value=permissions): + with patch.object(us.db, 'query', return_value=MagicMock(filter=MagicMock(return_value=MagicMock(with_entities=MagicMock(return_value=MagicMock(scalar=MagicMock(return_value=mbs * 1024 * 1024))))))): + assert us.has_quota_max_monthly_mbs("group1") == expected + + test_cases = [ + (-1, 1000, True), + (100, 50, True), + (100, 100, False), + (100, 150, False), + ] + + for max_mbs, mbs, expected in test_cases: + with patch.object(UserState, 'max_monthly_mbs', new_callable=PropertyMock, return_value=max_mbs): + with patch.object(us.db, 'query', return_value=MagicMock(filter=MagicMock(return_value=MagicMock(with_entities=MagicMock(return_value=MagicMock(scalar=MagicMock(return_value=mbs * 1024 * 1024))))))): + assert us.has_quota_max_monthly_mbs("") == expected + + +def test_can_manually_trigger(user_state): + permissions = { + "group1": GroupInfo(manually_trigger_sheet=True), + "group2": GroupInfo(manually_trigger_sheet=False), + } + + with patch.object(UserState, 'permissions', new_callable=PropertyMock, return_value=permissions): + assert user_state.can_manually_trigger("group1") == True + assert user_state.can_manually_trigger("group2") == False + assert user_state.can_manually_trigger("group3") == False + + +def test_is_sheet_frequency_allowed(user_state): + permissions = { + "group1": GroupInfo(sheet_frequency={"daily", "hourly"}), + "group2": GroupInfo(sheet_frequency={"daily"}), + } + + with patch.object(UserState, 'permissions', new_callable=PropertyMock, return_value=permissions): + assert user_state.is_sheet_frequency_allowed("group1", "daily") == True + assert user_state.is_sheet_frequency_allowed("group1", "hourly") == True + assert user_state.is_sheet_frequency_allowed("group1", "weekly") == False + assert user_state.is_sheet_frequency_allowed("group2", "hourly") == False + assert user_state.is_sheet_frequency_allowed("group2", "daily") == True + assert user_state.is_sheet_frequency_allowed("group3", "daily") == False + + +def test_priority_group(user_state): + from app.web.utils.misc import convert_priority_to_queue_dict + with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[ + models.Group(id="group1", permissions={"priority": "high"}), + models.Group(id="group2", permissions={"priority": "medium"}), + models.Group(id="group3", permissions={"priority": "low"}), + ]): + assert user_state.priority_group("group1") == convert_priority_to_queue_dict("high") + assert user_state.priority_group("group2") == convert_priority_to_queue_dict("medium") + assert user_state.priority_group("group3") == convert_priority_to_queue_dict("low") + assert user_state.priority_group("group4") == convert_priority_to_queue_dict("low") diff --git a/app/tests/web/endpoints/test_default.py b/app/tests/web/endpoints/test_default.py new file mode 100644 index 0000000..401a164 --- /dev/null +++ b/app/tests/web/endpoints/test_default.py @@ -0,0 +1,175 @@ +from unittest.mock import MagicMock +from fastapi.testclient import TestClient +import pytest +from app.shared.schemas import Usage, UsageResponse +from app.shared.user_groups import GroupInfo +from app.web.config import VERSION +from app.tests.web.db.test_crud import test_data + + +def test_endpoint_home(client_with_auth): + r = client_with_auth.get("/") + assert r.status_code == 200 + j = r.json() + assert "version" in j and j["version"] == VERSION + assert "breakingChanges" in j + assert "groups" not in j + + +def test_endpoint_health(client_with_auth): + r = client_with_auth.get("/health") + assert r.status_code == 200 + assert r.json() == {"status": "ok"} + + +def test_endpoint_active_no_auth(client, test_no_auth): + test_no_auth(client.get, "/user/active") + + +def test_endpoint_active(app): + m_user_state = MagicMock() + + from app.web.security import get_user_state + app.dependency_overrides[get_user_state] = lambda: m_user_state + + # inactive user + m_user_state.active = False + client = TestClient(app) + r = client.get("/user/active") + assert r.status_code == 200 + assert r.json() == {"active": False} + + # active user + m_user_state.active = True + client = TestClient(app) + r = client.get("/user/active") + assert r.status_code == 200 + assert r.json() == {"active": True} + + +def test_no_serve_local_archive_by_default(client_with_auth): + r = client_with_auth.get("/app/local_archive_test/temp.txt") + assert r.status_code == 404 + + +def test_favicon(client_with_auth): + r = client_with_auth.get("/favicon.ico") + assert r.status_code == 200 + assert r.headers["content-type"] == "image/vnd.microsoft.icon" + + +def test_endpoint_test_prometheus_no_auth(client, test_no_auth): + test_no_auth(client.get, "/metrics") + + +def test_endpoint_test_prometheus_no_user_auth(client_with_auth, test_no_auth): + test_no_auth(client_with_auth.get, "/metrics") + + +@pytest.mark.asyncio +async def test_prometheus_metrics(test_data, client_with_token, get_settings): + # before metrics calculation + r = client_with_token.get("/metrics") + assert r.status_code == 200 + assert r.headers["content-type"] == "text/plain; version=0.0.4; charset=utf-8" + assert "disk_utilization" in r.text + assert "database_metrics" in r.text + assert "exceptions" in r.text + assert "worker_exceptions_total" in r.text + assert 'disk_utilization{type="used"}' not in r.text + + # after metrics calculation + from app.web.utils.metrics import measure_regular_metrics + await measure_regular_metrics(get_settings.DATABASE_PATH, 60 * 60 * 24 * 31 * 12 * 100) + r2 = client_with_token.get("/metrics") + assert 'disk_utilization{type="used"}' in r2.text + assert 'disk_utilization{type="free"}' in r2.text + assert 'disk_utilization{type="database"}' in r2.text + assert 'database_metrics{query="count_archives"} 100.0' in r2.text + assert 'database_metrics{query="count_archive_urls"} 1000.0' in r2.text + assert 'database_metrics{query="count_users"} 3.0' in r2.text + assert 'database_metrics_counter_total{query="count_by_user",user="rick@example.com"} 34.0' in r2.text + assert 'database_metrics_counter_total{query="count_by_user",user="morty@example.com"} 33.0' in r2.text + assert 'database_metrics_counter_total{query="count_by_user",user="jerry@example.com"} 33.0' in r2.text + + # 30s window, should not change the gauges nor the total in the counters + from app.web.utils.metrics import measure_regular_metrics + await measure_regular_metrics(get_settings.DATABASE_PATH, 30) + r3 = client_with_token.get("/metrics") + assert 'database_metrics{query="count_archives"} 100.0' in r3.text + assert 'database_metrics{query="count_archive_urls"} 1000.0' in r3.text + assert 'database_metrics{query="count_users"} 3.0' in r3.text + assert 'database_metrics_counter_total{query="count_by_user",user="rick@example.com"} 34.0' in r3.text + assert 'database_metrics_counter_total{query="count_by_user",user="morty@example.com"} 33.0' in r3.text + assert 'database_metrics_counter_total{query="count_by_user",user="jerry@example.com"} 33.0' in r3.text + + +def test_endpoint_get_user_permissions_no_user_auth(client, test_no_auth): + test_no_auth(client.get, "/user/permissions") + + +def test_endpoint_get_user_permissions(app): + from app.web.security import get_user_state + + m_user_state = MagicMock() + rv = { + "all": GroupInfo(read=True), + "group1": GroupInfo(archive_url=True), + } + from loguru import logger + logger.info(rv) + m_user_state.permissions = rv + + app.dependency_overrides[get_user_state] = lambda: m_user_state + + client = TestClient(app) + r = client.get("/user/permissions") + assert r.status_code == 200 + response = r.json() + assert response.keys() == {"all", "group1"} + assert response["all"]["read"] + assert response["group1"]["read"] == [] + assert response["group1"]["archive_url"] + assert response["all"]["archive_url"] == False + + +def test_endpoint_get_user_usage_no_user_auth(client, test_no_auth): + test_no_auth(client.get, "/user/usage") + + +def test_endpoint_get_user_usage_inactive(app): + from app.web.security import get_user_state + + m_user_state = MagicMock() + m_user_state.active = False + + app.dependency_overrides[get_user_state] = lambda: m_user_state + + client = TestClient(app) + r = client.get("/user/usage") + assert r.status_code == 403 + assert r.json() == {"detail": "User is not active."} + + +def test_endpoint_get_user_usage_active(app): + from app.web.security import get_user_state + + m_user_state = MagicMock() + m_user_state.active = True + mock_usage = UsageResponse( + monthly_urls=1, + monthly_mbs=2, + total_sheets=3, + groups={ + "group1": Usage(monthly_urls=4, monthly_mbs=5, total_sheets=6), + "group2": Usage(monthly_urls=7, monthly_mbs=8, total_sheets=9) + } + ) + m_user_state.usage.return_value = mock_usage + + app.dependency_overrides[get_user_state] = lambda: m_user_state + + client = TestClient(app) + r = client.get("/user/usage") + assert r.status_code == 200 + assert UsageResponse(**r.json()) == mock_usage diff --git a/app/tests/web/endpoints/test_interoperability.py b/app/tests/web/endpoints/test_interoperability.py new file mode 100644 index 0000000..14cd245 --- /dev/null +++ b/app/tests/web/endpoints/test_interoperability.py @@ -0,0 +1,56 @@ +from datetime import datetime +import json +from unittest.mock import MagicMock, patch + +from app.shared.db import models +from app.web.config import ALLOW_ANY_EMAIL +from app.web.db import crud + + +def test_submit_manual_archive_unauthenticated(client, test_no_auth): + test_no_auth(client.post, "/interop/submit-archive") + + +def test_submit_manual_archive_not_user_auth(client_with_auth, test_no_auth): + test_no_auth(client_with_auth.post, "/interop/submit-archive") + + +@patch("app.web.endpoints.interoperability.business_logic", return_value=MagicMock(get_store_archive_until=MagicMock(return_value=datetime))) +def test_submit_manual_archive(m1, client_with_token, db_session): + # normal workflow + aa_metadata = json.dumps({"status": "test: success", "metadata": {"url": "http://example.com"}, "media": [{"filename": "fn1", "urls": ["http://example.s3.com"]}]}) + r = client_with_token.post("/interop/submit-archive", json={"result": aa_metadata, "public": True, "author_id": "jerry@gmail.com", "group_id": "spaceship", "tags": ["test"], "url": "http://example.com"}) + assert r.status_code == 201 + assert "id" in r.json() + + inserted = db_session.query(models.Archive).filter(models.Archive.id == r.json()["id"]).first() + assert inserted.url == "http://example.com" + assert inserted.group_id == "spaceship" + assert inserted.author_id == "jerry@gmail.com" + assert sorted([t.id for t in inserted.tags]) == sorted(["test", "manual"]) + assert inserted.public + assert type(inserted.result) == dict + assert [u.url for u in inserted.urls] == ["http://example.s3.com"] + assert type(inserted.store_until) == datetime + + # cannot have the same URL twice + aa_metadata = json.dumps({"status": "test: success", "metadata": {"url": "http://example.com"}, "media": [{"filename": "fn1", "urls": ["http://example.com", "http://example.com"]}]}) + r = client_with_token.post("/interop/submit-archive", json={"result": aa_metadata, "public": False, "author_id": "jerry@gmail.com", "tags": ["test"], "url": "http://example.com"}) + assert r.status_code == 422 + assert r.json() == {"detail": "Cannot insert into DB due to integrity error, likely duplicate urls."} + + +# test with invalid JSON +def test_submit_manual_archive_invalid_json(client_with_token): + r = client_with_token.post("/interop/submit-archive", json={"result": "invalid json", "public": False, "author_id": "jer", "tags": ["test"], "url": "http://example.com"}) + assert r.status_code == 422 + assert r.json() == {"detail": "Invalid JSON in result field."} + + +@patch("app.web.endpoints.interoperability.business_logic") +def test_submit_manual_archive_no_store_until(m_b, client_with_token, db_session): + m_b.get_store_archive_until.side_effect = AssertionError("AssertionError") + aa_metadata = json.dumps({"status": "test: success", "metadata": {"url": "http://example.com"}, "media": [{"filename": "fn1", "urls": ["http://example.s3.com"]}]}) + r = client_with_token.post("/interop/submit-archive", json={"result": aa_metadata, "public": True, "author_id": "jerry@gmail.com", "group_id": "spaceship", "tags": ["test"], "url": "http://example.com"}) + assert r.status_code == 422 + assert r.json() == {"detail": "AssertionError"} diff --git a/app/tests/web/endpoints/test_sheet.py b/app/tests/web/endpoints/test_sheet.py new file mode 100644 index 0000000..1396d85 --- /dev/null +++ b/app/tests/web/endpoints/test_sheet.py @@ -0,0 +1,193 @@ +from datetime import datetime +import json +from unittest.mock import MagicMock, patch + +from fastapi.testclient import TestClient + +from app.shared.schemas import TaskResult + + +def test_endpoints_no_auth(client, test_no_auth): + test_no_auth(client.post, "/sheet/create") + test_no_auth(client.get, "/sheet/mine") + test_no_auth(client.delete, "/sheet/123-sheet-id") + test_no_auth(client.post, "/sheet/123-sheet-id/archive") + + +def test_create_sheet_endpoint(app_with_auth, db_session): + client_with_auth = TestClient(app_with_auth) + good_data = { + "id": "123-sheet-id", + "name": "Test Sheet", + "group_id": "spaceship", + "frequency": "daily" + } + + # with good data + response = client_with_auth.post("/sheet/create", json=good_data) + assert response.status_code == 201 + j = response.json() + assert datetime.fromisoformat(j.pop("created_at")) + assert datetime.fromisoformat(j.pop("last_url_archived_at")) + assert j.pop("author_id") == 'morty@example.com' + assert j == good_data + + # already exists + response = client_with_auth.post("/sheet/create", json=good_data) + assert response.status_code == 400 + assert response.json() == {"detail": "Sheet with this ID is already being archived."} + + # bad group + bad_data = good_data.copy() + bad_data["group_id"] = "not a group" + response = client_with_auth.post("/sheet/create", json=bad_data) + assert response.status_code == 403 + assert response.json() == {"detail": "User does not have access to this group."} + + # switch to jerry who's got less quota/permissions + from app.web.security import get_user_state + from app.web.db.user_state import UserState + app_with_auth.dependency_overrides[get_user_state] = lambda: UserState(db_session, "jerry@example.com") + client_jerry = TestClient(app_with_auth) + + # frequency not allowed + jerry_data = good_data.copy() + jerry_data["group_id"] = "animated-characters" + jerry_data["frequency"] = "hourly" + jerry_data["id"] = "jerry-sheet-id" + response = client_jerry.post("/sheet/create", json=jerry_data) + assert response.status_code == 422 + assert response.json() == {"detail": "Invalid frequency selected for this group."} + + jerry_data["frequency"] = "daily" + # success for the first sheet, bad quota on second + response = client_jerry.post("/sheet/create", json=jerry_data) + assert response.status_code == 201 + + response = client_jerry.post("/sheet/create", json=jerry_data) + assert response.status_code == 429 + assert response.json() == {"detail": "User has reached their sheet quota for this group."} + + +def test_get_user_sheets_endpoint(client_with_auth, db_session): + # no data + response = client_with_auth.get("/sheet/mine") + assert response.status_code == 200 + assert response.json() == [] + + # with data + from app.shared.db import models + db_session.add( + models.Sheet(id="123", name="Test Sheet 1", author_id="morty@example.com", group_id="spaceship", frequency="hourly") + ) + db_session.commit() + db_session.add_all([ + models.Sheet(id="456", name="Test Sheet 2", author_id="morty@example.com", group_id="interdimensional", frequency="daily"), + models.Sheet(id="789", name="Test Sheet 3", author_id="rick@example.com", group_id="interdimensional", frequency="hourly"), + ]) + db_session.commit() + + response = client_with_auth.get("/sheet/mine") + assert response.status_code == 200 + r = response.json() + assert isinstance(r, list) + assert len(r) == 2 + assert datetime.fromisoformat(r[0].pop("created_at")) + assert datetime.fromisoformat(r[0].pop("last_url_archived_at")) + assert datetime.fromisoformat(r[1].pop("created_at")) + assert datetime.fromisoformat(r[1].pop("last_url_archived_at")) + assert r[0] == { + 'id': '123', + 'author_id': 'morty@example.com', + 'frequency': 'hourly', + 'group_id': 'spaceship', + 'name': 'Test Sheet 1', + } + assert r[1] == { + 'id': '456', + 'author_id': 'morty@example.com', + 'frequency': 'daily', + 'group_id': 'interdimensional', + 'name': 'Test Sheet 2', + } + + +def test_delete_sheet_endpoint(client_with_auth, db_session): + # missing sheet + response = client_with_auth.delete("/sheet/123-sheet-id") + assert response.status_code == 200 + assert response.json() == { + "id": "123-sheet-id", + "deleted": False + } + + # add sheets for deletion + from app.shared.db import models + db_session.add_all([ + models.Sheet(id="123-sheet-id", name="Test Sheet 1", author_id="morty@example.com", group_id="interdimensional", frequency="daily"), + models.Sheet(id="456-sheet-id", name="Test Sheet 2", author_id="rick@example.com", group_id="spaceship", frequency="hourly"), + ]) + db_session.commit() + + # morty can delete his + response = client_with_auth.delete("/sheet/123-sheet-id") + assert response.status_code == 200 + assert response.json() == {"id": "123-sheet-id", "deleted": True} + # but only once + response = client_with_auth.delete("/sheet/123-sheet-id") + assert response.status_code == 200 + assert response.json() == {"id": "123-sheet-id", "deleted": False} + # and not rick's + response = client_with_auth.delete("/sheet/456-sheet-id") + assert response.status_code == 200 + assert response.json() == {"id": "456-sheet-id", "deleted": False} + + +class TestArchiveUserSheetEndpoint: + @patch("app.web.endpoints.sheet.celery", return_value=MagicMock()) + def test_normal_flow(self, m_celery, client_with_auth, db_session): + from app.shared.db import models + db_session.add(models.Sheet(id="123-sheet-id", name="Test Sheet 1", author_id="morty@example.com", group_id="spaceship", frequency="hourly")) + db_session.commit() + + m_signature = MagicMock() + m_signature.apply_async.return_value = TaskResult(id="123-taskid", status="PENDING", result="") + m_celery.signature.return_value = m_signature + + r = client_with_auth.post("/sheet/123-sheet-id/archive") + assert r.status_code == 201 + assert r.json() == {"id": "123-taskid"} + m_celery.signature.assert_called_once() + m_signature.apply_async.assert_called_once() + + def test_token_auth(self, client_with_token, test_no_auth): + test_no_auth(client_with_token.post, "/sheet/123-sheet-id/archive") + + def test_missing_data(self, client_with_auth): + r = client_with_auth.post("/sheet/123-sheet-id/archive") + assert r.status_code == 403 + assert r.json() == {"detail": "No access to this sheet."} + + def test_no_access(self, client_with_auth, db_session): + from app.shared.db import models + db_session.add(models.Sheet(id="123-sheet-id", name="Test Sheet 1", author_id="rick@example.com", group_id="spaceship", frequency="hourly")) + db_session.commit() + r = client_with_auth.post("/sheet/123-sheet-id/archive") + assert r.status_code == 403 + assert r.json() == {"detail": "No access to this sheet."} + + def test_user_not_in_group(self, client_with_auth, db_session): + from app.shared.db import models + db_session.add(models.Sheet(id="123-sheet-id", name="Test Sheet 1", author_id="morty@example.com", group_id="interdimensional", frequency="hourly")) + db_session.commit() + r = client_with_auth.post("/sheet/123-sheet-id/archive") + assert r.status_code == 403 + assert r.json() == {"detail": "User does not have access to this group."} + + def test_user_cannot_manually_trigger(self, client_with_auth, db_session): + from app.shared.db import models + db_session.add(models.Sheet(id="123-sheet-id", name="Test Sheet 1", author_id="morty@example.com", group_id="default", frequency="hourly")) + db_session.commit() + r = client_with_auth.post("/sheet/123-sheet-id/archive") + assert r.status_code == 429 + assert r.json() == {"detail": "User cannot manually trigger sheet archiving in this group."} diff --git a/src/tests/endpoints/test_task.py b/app/tests/web/endpoints/test_task.py similarity index 91% rename from src/tests/endpoints/test_task.py rename to app/tests/web/endpoints/test_task.py index 9585c39..937ad46 100644 --- a/src/tests/endpoints/test_task.py +++ b/app/tests/web/endpoints/test_task.py @@ -5,7 +5,7 @@ def test_endpoint_task_status_no_auth(client, test_no_auth): test_no_auth(client.get, "/task/test-task-id") -@patch("endpoints.task.AsyncResult") +@patch("app.web.endpoints.task.AsyncResult") def test_get_status_success(mock_async_result, client_with_auth): mock_async_result.return_value.status = "SUCCESS" mock_async_result.return_value.result = {"data": "some result"} @@ -20,7 +20,7 @@ def test_get_status_success(mock_async_result, client_with_auth): } -@patch("endpoints.task.AsyncResult") +@patch("app.web.endpoints.task.AsyncResult") def test_get_status_failure(mock_async_result, client_with_auth): mock_async_result.return_value.status = "FAILURE" @@ -36,7 +36,7 @@ def test_get_status_failure(mock_async_result, client_with_auth): } -@patch("endpoints.task.AsyncResult") +@patch("app.web.endpoints.task.AsyncResult") def test_get_status_pending(mock_async_result, client_with_auth): mock_async_result.return_value.status = "PENDING" mock_async_result.return_value.result = None diff --git a/app/tests/web/endpoints/test_url.py b/app/tests/web/endpoints/test_url.py new file mode 100644 index 0000000..4a6f342 --- /dev/null +++ b/app/tests/web/endpoints/test_url.py @@ -0,0 +1,193 @@ +import json +from unittest.mock import MagicMock, patch + +from app.shared.schemas import ArchiveCreate, TaskResult + + +def test_archive_url_unauthenticated(client, test_no_auth): + test_no_auth(client.post, "/url/archive") + + +@patch("app.web.endpoints.url.UserState") +@patch("app.web.endpoints.url.celery", return_value=MagicMock()) +def test_archive_url(m_celery, m2, client_with_auth): + m_signature = MagicMock() + m_signature.apply_async.return_value = TaskResult(id="123-456-789", status="PENDING", result="") + m_celery.signature.return_value = m_signature + + m_user_state = MagicMock() + m2.return_value = m_user_state + + # url is too short + response = client_with_auth.post("/url/archive", json={"url": "bad"}) + assert response.status_code == 422 + assert response.json()["detail"][0]["msg"] == 'String should have at least 5 characters' + m_celery.signature.assert_not_called() + + # url is invalid + response = client_with_auth.post("/url/archive", json={"url": "example.com"}) + assert response.status_code == 400 + assert response.json()["detail"] == "Invalid URL received." + + # valid request + m_user_state.has_quota_max_monthly_urls.return_value = True + m_user_state.has_quota_max_monthly_mbs.return_value = True + response = client_with_auth.post("/url/archive", json={"url": "https://example.com"}) + assert response.status_code == 201 + assert response.json() == {'id': '123-456-789'} + m_celery.signature.assert_called_once() + m_signature.apply_async.assert_called_once() + called_val = m_celery.signature.call_args + assert called_val[0][0] == "create_archive_task" + assert json.loads(called_val[1]['args'][0]) == {"id": None, "url": "https://example.com", "result": None, "public": False, "author_id": "rick@example.com", "group_id": "default", "tags": None, "sheet_id": None, "store_until": None, "urls": None} + m_user_state.has_quota_max_monthly_urls.assert_called_once() + m_user_state.has_quota_max_monthly_mbs.assert_called_once() + m_user_state.in_group.assert_called_once_with("default") + + # user is not in group + m_user_state.in_group.return_value = False + response = client_with_auth.post("/url/archive", json={"url": "https://example.com", "group_id": "new-group"}) + assert response.status_code == 403 + assert response.json()["detail"] == "User does not have access to this group." + m_user_state.in_group.assert_called_with("new-group") + + # user is in group + m_user_state.in_group.return_value = True + response = client_with_auth.post("/url/archive", json={"url": "https://example.com", "group_id": "spaceship"}) + assert response.status_code == 201 + assert response.json() == {'id': '123-456-789'} + assert m_celery.signature.call_count == 2 + assert m_signature.apply_async.call_count == 2 + called_val = m_celery.signature.call_args + assert json.loads(called_val[1]['args'][0])["group_id"] == "spaceship" + m_user_state.in_group.assert_called_with("spaceship") + + # user is over monthly URL quota + m_user_state.has_quota_max_monthly_urls.return_value = False + m_user_state.has_quota_max_monthly_mbs.return_value = True + response = client_with_auth.post("/url/archive", json={"url": "https://example.com", "group_id": "spaceship"}) + assert response.status_code == 429 + assert response.json()["detail"] == "User has reached their monthly URL quota." + m_user_state.has_quota_max_monthly_urls.assert_called_with("spaceship") + + # user is over monthly MB quota + m_user_state.has_quota_max_monthly_urls.return_value = True + m_user_state.has_quota_max_monthly_mbs.return_value = False + response = client_with_auth.post("/url/archive", json={"url": "https://example.com", "group_id": "spacesuit"}) + assert response.status_code == 429 + assert response.json()["detail"] == "User has reached their monthly MB quota." + m_user_state.has_quota_max_monthly_mbs.assert_called_with("spacesuit") + assert m_celery.signature.call_count == 2 + assert m_signature.apply_async.call_count == 2 + + +@patch("app.web.endpoints.url.UserState") +def test_archive_url_quotas(m1, client_with_auth): + m_user_state = MagicMock() + m1.return_value = m_user_state + + # misses on monthly URLs quota + m_user_state.has_quota_max_monthly_urls.return_value = False + response = client_with_auth.post("/url/archive", json={"url": "https://example.com"}) + assert response.status_code == 429 + assert response.json()["detail"] == "User has reached their monthly URL quota." + m_user_state.has_quota_max_monthly_urls.assert_called_once() + + # misses on monthly MBs quota + m_user_state.has_quota_max_monthly_urls.return_value = True + m_user_state.has_quota_max_monthly_mbs.return_value = False + response = client_with_auth.post("/url/archive", json={"url": "https://example.com"}) + assert response.status_code == 429 + assert response.json()["detail"] == "User has reached their monthly MB quota." + m_user_state.has_quota_max_monthly_mbs.assert_called_once() + + +@patch("app.web.endpoints.url.celery", return_value=MagicMock()) +def test_archive_url_with_api_token(m_celery, client_with_token): + m_signature = MagicMock() + m_signature.apply_async.return_value = TaskResult(id="123-456-789", status="PENDING", result="") + m_celery.signature.return_value = m_signature + response = client_with_token.post("/url/archive", json={"url": "https://example.com"}) + assert response.status_code == 201 + assert response.json() == {'id': '123-456-789'} + m_celery.signature.assert_called_once() + m_signature.apply_async.assert_called_once() + called_val = m_celery.signature.call_args + assert called_val[0][0] == "create_archive_task" + + +def test_search_by_url_unauthenticated(client, test_no_auth): + test_no_auth(client.get, "/url/search") + + +def test_search_by_url(client_with_auth, client_with_token, db_session): + # tests the search endpoint, including through some db data for the endpoint params + response = client_with_auth.get("/url/search") + assert response.status_code == 422 + assert response.json()["detail"][0]["msg"] == "Field required" + + response = client_with_auth.get("/url/search?url=https://example.com") + assert response.status_code == 200 + assert response.json() == [] + + from app.shared import schemas + from app.shared.db import worker_crud + for i in range(11): + worker_crud.create_archive(db_session, ArchiveCreate(id=f"url-456-{i}", url="https://example.com" if i < 10 else "https://something-else.com", result={}, public=True, author_id="rick@example.com"), [], []) + # NB: this insertion is too fast for the ordering to be correct as they are within the same second + + response = client_with_auth.get("/url/search?url=https://example.com") + assert response.status_code == 200 + assert len(j := response.json()) == 10 + assert "url-456-0" in [i["id"] for i in j] + assert "url-456-9" in [i["id"] for i in j] + assert "url-456-10" not in [i["id"] for i in j] + assert j[0].keys() == schemas.ArchiveResult.model_fields.keys() + + response = client_with_auth.get("/url/search?url=https://example.com&limit=5") + assert response.status_code == 200 + assert len(response.json()) == 5 + + response = client_with_auth.get("/url/search?url=https://example.com&skip=5&limit=2") + assert response.status_code == 200 + assert len(response.json()) == 2 + + response = client_with_auth.get("/url/search?url=https://example.com&archived_before=2010-01-01") + assert response.status_code == 200 + assert len(response.json()) == 0 + + response = client_with_auth.get("/url/search?url=https://example.com&archived_after=2010-01-01") + assert response.status_code == 200 + assert len(response.json()) == 10 + + # API token will also work + response = client_with_token.get("/url/search?url=https://example.com&archived_after=2010-01-01") + assert response.status_code == 200 + assert len(response.json()) == 10 + + +@patch("app.web.endpoints.url.UserState") +def test_search_no_read_access(mock_user_state, client_with_auth): + mock_user_state.return_value.read = False + mock_user_state.return_value.read_public = False + + response = client_with_auth.get("/url/search?url=https://example.com") + assert response.status_code == 403 + assert response.json() == {"detail": "User does not have read access."} + + +def test_delete_task_unauthenticated(client, test_no_auth): + test_no_auth(client.delete, "/url/123-456-789") + + +def test_delete_task(client_with_auth, db_session): + response = client_with_auth.delete("/url/delete-123-456-789") + assert response.status_code == 200 + assert response.json() == {"id": "delete-123-456-789", "deleted": False} + + from app.shared.db import worker_crud + worker_crud.create_archive(db_session, ArchiveCreate(id="delete-123-456-789", url="https://example.com", result={}, public=True, author_id="morty@example.com"), [], []) + + response = client_with_auth.delete("/url/delete-123-456-789") + assert response.status_code == 200 + assert response.json() == {"id": "delete-123-456-789", "deleted": True} diff --git a/src/tests/web/test_main.py b/app/tests/web/test_main.py similarity index 85% rename from src/tests/web/test_main.py rename to app/tests/web/test_main.py index e880311..f77d368 100644 --- a/src/tests/web/test_main.py +++ b/app/tests/web/test_main.py @@ -17,12 +17,12 @@ def test_alembic(db_session): alembic.config.main(argv=['--raiseerr', 'upgrade', 'head']) alembic.config.main(argv=['--raiseerr', 'downgrade', 'base']) -@patch("endpoints.default.crud.get_user_groups", side_effect=Exception('mocked error')) +@patch("app.web.endpoints.url.crud.soft_delete_archive", side_effect=Exception('mocked error')) def test_logging_middleware(m1, client_with_auth): - from utils.metrics import EXCEPTION_COUNTER + from app.web.utils.metrics import EXCEPTION_COUNTER assert len(EXCEPTION_COUNTER.collect()[0].samples) == 0 with pytest.raises(Exception, match="mocked error"): - client_with_auth.get("/groups") + client_with_auth.delete("/url/123") # creates one empty and one from above assert len(EXCEPTION_COUNTER.collect()[0].samples) == 2 @@ -36,7 +36,7 @@ def test_serve_local_archive_logic(get_settings): try: # modify the settings get_settings.SERVE_LOCAL_ARCHIVE = "/app/local_archive_test" - from web.main import app_factory + from app.web.main import app_factory app = app_factory(get_settings) # test diff --git a/src/tests/web/test_security.py b/app/tests/web/test_security.py similarity index 76% rename from src/tests/web/test_security.py rename to app/tests/web/test_security.py index 64fe4d4..1a6c00b 100644 --- a/src/tests/web/test_security.py +++ b/app/tests/web/test_security.py @@ -1,14 +1,14 @@ -from unittest.mock import patch +from unittest.mock import Mock, patch from fastapi import HTTPException from fastapi.security import HTTPAuthorizationCredentials import pytest -from core.config import ALLOW_ANY_EMAIL +from app.web.config import ALLOW_ANY_EMAIL def test_secure_compare(): - from web.security import secure_compare + from app.web.security import secure_compare assert secure_compare("test", "test") assert not secure_compare("test", "test2") @@ -16,14 +16,14 @@ def test_secure_compare(): @pytest.mark.asyncio async def test_get_token_or_user_auth_with_api(): - from web.security import get_token_or_user_auth + from app.web.security import get_token_or_user_auth mock_api = HTTPAuthorizationCredentials(scheme="lorem", credentials="this_is_the_test_api_token") assert await get_token_or_user_auth(mock_api) == ALLOW_ANY_EMAIL @pytest.mark.asyncio async def test_get_token_or_user_auth_with_user(): - from web.security import get_token_or_user_auth + from app.web.security import get_token_or_user_auth bad_user = HTTPAuthorizationCredentials(scheme="ipsum", credentials="invalid") e: pytest.ExceptionInfo = None with pytest.raises(HTTPException) as e: @@ -32,18 +32,18 @@ async def test_get_token_or_user_auth_with_user(): assert e.value.detail == "invalid access_token" -@patch("web.security.authenticate_user", return_value=(True, "summer@example.com")) +@patch("app.web.security.authenticate_user", return_value=(True, "summer@example.com")) @pytest.mark.asyncio async def test_get_user_auth(m1): - from web.security import get_user_auth - bad_user = HTTPAuthorizationCredentials(scheme="ipsum", credentials="valid-and-good") - assert await get_user_auth(bad_user) == "summer@example.com" + from app.web.security import get_user_auth + good_user = HTTPAuthorizationCredentials(scheme="ipsum", credentials="valid-and-good") + assert await get_user_auth(good_user) == "summer@example.com" -@patch("web.security.secure_compare", return_value=False) +@patch("app.web.security.secure_compare", return_value=False) @pytest.mark.asyncio async def test_token_api_key_auth_exception(m1): - from web.security import token_api_key_auth + from app.web.security import token_api_key_auth e: pytest.ExceptionInfo = None with pytest.raises(HTTPException) as e: @@ -54,12 +54,12 @@ async def test_token_api_key_auth_exception(m1): @pytest.mark.asyncio async def test_authenticate_user(): - from web.security import authenticate_user + from app.web.security import authenticate_user assert authenticate_user("test") == (False, "invalid access_token") assert authenticate_user(123) == (False, "invalid access_token") - with patch("web.security.requests.get") as mock_get: + with patch("app.web.security.requests.get") as mock_get: # bad response from oauth2 mock_get.return_value.status_code = 403 assert authenticate_user("this-will-call-requests") == (False, "invalid token") @@ -100,9 +100,22 @@ async def test_authenticate_user(): @pytest.mark.asyncio async def test_authenticate_user_exception(): - from web.security import authenticate_user - - with patch("web.security.requests.get") as mock_get: + from app.web.security import authenticate_user + with patch("app.web.security.requests.get") as mock_get: mock_get.return_value.status_code = 200 mock_get.return_value.json.side_effect = Exception("mocked error") assert authenticate_user("this-will-call-requests") == (False, "exception occurred") + + +def test_get_user_state(): + from app.web.security import get_user_state + from app.web.db.user_state import UserState + + mock_session = Mock() + test_email = "test@example.com" + + state = get_user_state(test_email, mock_session) + + assert isinstance(state, UserState) + assert state.email == test_email + assert state.db == mock_session diff --git a/app/tests/worker/test_worker_main.py b/app/tests/worker/test_worker_main.py new file mode 100644 index 0000000..d40c457 --- /dev/null +++ b/app/tests/worker/test_worker_main.py @@ -0,0 +1,137 @@ +from datetime import datetime + +from unittest.mock import patch + +import pytest + +from app.shared.db import models +from app.shared import schemas +from auto_archiver.core import Media, Metadata + + +class Test_create_archive_task(): + URL = "https://example-live.com" + archive = schemas.ArchiveCreate(url=URL, tags=["tag-celery"], public=True, author_id="rick@example.com", group_id="interstellar") + + @patch("app.worker.main.ArchivingOrchestrator") + @patch("app.worker.main.get_all_urls", return_value=[]) + @patch("app.worker.main.insert_result_into_db") + @patch("app.worker.main.get_store_until", return_value=datetime.now()) + @patch("app.worker.main.get_orchestrator_args", return_value=["arg1", "arg2"]) + @patch("celery.app.task.Task.request") + def test_success(self, m_req, m_args, m_store, m_insert, m_urls, m_orchestrator, db_session): + from app.worker.main import create_archive_task + + m_req.id = "this-just-in" + m_orchestrator.return_value.feed.return_value = iter([Metadata().set_url(self.URL).success()]) + + task = create_archive_task(self.archive.model_dump_json()) + + m_args.assert_called_once() + m_store.assert_called_once_with("interstellar") + m_insert.assert_called_once() + m_urls.assert_called_once() + m_orchestrator.return_value.feed.assert_called_once() + m_orchestrator.return_value.setup.assert_called_once() + + assert task["status"] == "success" + assert task["metadata"]["url"] == self.URL + assert len(task["media"]) == 0 + + def test_raise_invalid(self): + from app.worker.main import create_archive_task + with pytest.raises(Exception): + create_archive_task(self.archive.model_dump_json()) + + @patch("app.worker.main.ArchivingOrchestrator") + @patch("app.worker.main.get_orchestrator_args") + def test_raise_db_error(self, m_args, m_orchestrator): + from app.worker.main import create_archive_task + m_orchestrator.return_value.feed.side_effect = Exception("Orchestrator failed") + + with pytest.raises(Exception) as e: + create_archive_task(self.archive.model_dump_json()) + assert str(e.value) == "Orchestrator failed" + m_args.assert_called_once() + m_orchestrator.return_value.feed.assert_called_once() + + @patch("app.worker.main.ArchivingOrchestrator") + @patch("app.worker.main.insert_result_into_db", return_value=None) + @patch("app.worker.main.get_orchestrator_args") + def test_raise_empty_result(self, m_args, m_insert, m_orchestrator): + from app.worker.main import create_archive_task + m_orchestrator.return_value.feed.return_value = iter([None]) + + with pytest.raises(Exception) as e: + create_archive_task(self.archive.model_dump_json()) + assert str(e.value) == "UNABLE TO archive: https://example-live.com" + m_orchestrator.return_value.feed.assert_called_once() + + +class Test_create_sheet_task(): + URL = "https://example-live.com" + sheet = schemas.SubmitSheet(sheet_id="123", author_id="rick@example.com", group_id="interstellar", tags=["spaceship"]) + + @patch("app.worker.main.get_all_urls", return_value=[]) + @patch("app.worker.main.ArchivingOrchestrator") + @patch("app.worker.main.models.generate_uuid", return_value="constant-uuid") + @patch("app.worker.main.get_store_until", return_value=datetime.now()) + @patch("app.worker.main.get_orchestrator_args") + def test_success(self, m_args, m_store, m_uuid, m_orchestrator, m_urls, db_session): + from app.worker.main import create_sheet_task + + assert db_session.query(models.Archive).filter(models.Archive.url == self.URL).count() == 0 + + mock_metadata = Metadata().set_url(self.URL).success() + mock_metadata.add_media(Media("fn1.txt", urls=["outcome1.com"])) + + m_orchestrator.return_value.feed.return_value = iter([False, mock_metadata, mock_metadata]) + + res = create_sheet_task(self.sheet.model_dump_json()) + + m_args.assert_called_once_with("interstellar", True, ["--gsheet_feeder.sheet_id", "123"]) + m_orchestrator.return_value.setup.assert_called_once() + m_orchestrator.return_value.feed.assert_called_once() + m_store.assert_called_with("interstellar") + m_store.call_count == 2 + m_uuid.call_count == 2 + assert type(res) == dict + assert res["stats"]["archived"] == 1 + assert res["stats"]["failed"] == 1 + assert len(res["stats"]["errors"]) == 1 + assert res["sheet_id"] == "123" + assert res["success"] + assert type(res["time"]) == datetime + + # query created archive entry + inserted = db_session.query(models.Archive).filter(models.Archive.url == self.URL).one() + assert inserted is not None + assert inserted.url == self.URL + assert len(inserted.tags) == 1 + assert inserted.tags[0].id == "spaceship" + assert inserted.group_id == "interstellar" + assert inserted.author_id == "rick@example.com" + assert inserted.public == False + + +def test_get_all_urls(db_session): + from app.worker.main import get_all_urls + + meta = Metadata().set_url("https://example.com") + m1 = meta.add_media(Media("fn1.txt", urls=["outcome1.com"])) + m2 = meta.add_media(Media("fn2.txt", urls=["outcome2.com"])) + m3 = meta.add_media(Media("fn3.txt", urls=["outcome3.com"])) + m1.set("screenshot", Media("screenshot.png", urls=["screenshot.com"])) + m2.set("thumbnails", [Media("thumb1.png", urls=["thumb1.com"]), Media("thumb2.png", urls=["thumb2.com"])]) + m3.set("ssl_data", Media("ssl_data.txt", urls=["ssl_data.com"]).to_dict()) + m3.set("bad_data", {"bad": "dict is ignored"}) + + urls = [u.url for u in get_all_urls(meta)] + assert len(urls) == 7 + assert "outcome1.com" in urls + assert "outcome2.com" in urls + assert "outcome3.com" in urls + assert "screenshot.com" in urls + assert "thumb1.com" in urls + assert "thumb2.com" in urls + assert "ssl_data.com" in urls diff --git a/app/web/__init__.py b/app/web/__init__.py new file mode 100644 index 0000000..a817e9e --- /dev/null +++ b/app/web/__init__.py @@ -0,0 +1,3 @@ +from app.web.main import app_factory + +app = app_factory \ No newline at end of file diff --git a/src/core/config.py b/app/web/config.py similarity index 88% rename from src/core/config.py rename to app/web/config.py index dcfd135..548e02c 100644 --- a/src/core/config.py +++ b/app/web/config.py @@ -1,4 +1,5 @@ -VERSION = "0.8.0" +VERSION = "0.9.0" + API_DESCRIPTION = """ #### API for the Auto-Archiver project, a tool to archive web pages and Google Sheets. @@ -7,7 +8,7 @@ API_DESCRIPTION = """ - You can use this API to archive single URLs or entire Google Sheets. - Once you submit a URL or Sheet for archiving, the API will return a task_id that you can use to check the status of the archiving process. It works asynchronously. """ -BREAKING_CHANGES = {"minVersion": "0.3.1", "message": "The latest update has breaking changes, please update the extension to the most recent version."} +BREAKING_CHANGES = {"minVersion": "0.4.0", "message": "The latest update has breaking changes, please update the extension to the most recent version."} # changing this will corrupt the database logic ALLOW_ANY_EMAIL = "*" diff --git a/app/web/db/crud.py b/app/web/db/crud.py new file mode 100644 index 0000000..c16b09a --- /dev/null +++ b/app/web/db/crud.py @@ -0,0 +1,273 @@ +from collections import defaultdict +from functools import lru_cache +from sqlalchemy.orm import Session, load_only +from sqlalchemy import Column, or_, func, select +from loguru import logger +from datetime import datetime, timedelta +from sqlalchemy.ext.asyncio import AsyncSession +from cachetools import LRUCache, cached +from cachetools.keys import hashkey + +from app.web.config import ALLOW_ANY_EMAIL +from app.shared.db import models +from app.shared.settings import get_settings +from app.shared.user_groups import UserGroups +from app.shared.utils.misc import fnv1a_hash_mod +from app.web.utils.misc import convert_priority_to_queue_dict + + +DATABASE_QUERY_LIMIT = get_settings().DATABASE_QUERY_LIMIT + + +def get_limit(user_limit: int): + return max(1, min(user_limit, DATABASE_QUERY_LIMIT)) + +# --------------- TASK = Archive + + +def base_query(db: Session): + # NOTE: load_only is for optimization and not obfuscation, use .with_entities() if needed + return db.query(models.Archive)\ + .filter(models.Archive.deleted == False)\ + .options(load_only(models.Archive.id, models.Archive.created_at, models.Archive.url, models.Archive.result, models.Archive.store_until)) + + +def search_archives_by_url(db: Session, url: str, email: str, read_groups: bool | set[str], read_public: bool, skip: int = 0, limit: int = 100, archived_after: datetime = None, archived_before: datetime = None, absolute_search: bool = False) -> list[models.Archive]: + # searches for partial URLs, if email is * no ownership (or read/read_public) filtering happens + query = base_query(db) + if email != ALLOW_ANY_EMAIL: + or_filters = [models.Archive.author_id == email] + if read_public: + or_filters.append(models.Archive.public == True) + if read_groups == True: + or_filters.append(models.Archive.group_id.isnot(None)) + else: + or_filters.append(models.Archive.group_id.in_(read_groups)) + query = query.filter(or_(*or_filters)) + if absolute_search: + query = query.filter(models.Archive.url == url) + else: + query = query.filter(models.Archive.url.like(f'%{url}%')) + if archived_after: + query = query.filter(models.Archive.created_at > archived_after) + if archived_before: + query = query.filter(models.Archive.created_at < archived_before) + return query.order_by(models.Archive.created_at.desc()).offset(skip).limit(get_limit(limit)).all() + + +def search_archives_by_email(db: Session, email: str, skip: int = 0, limit: int = 100): + return base_query(db).filter(models.Archive.author_id == email).order_by(models.Archive.created_at.desc()).offset(skip).limit(get_limit(limit)).all() + + +def soft_delete_archive(db: Session, id: str, email: str) -> bool: + # TODO: implement hard-delete with cronjob that deletes from S3 + db_archive = db.query(models.Archive).filter(models.Archive.id == id, models.Archive.author_id == email, models.Archive.deleted == False).first() + if db_archive: + db_archive.deleted = True + db.commit() + return db_archive is not None + + +def count_archives(db: Session): + return db.query(func.count(models.Archive.id)).scalar() + + +def count_archive_urls(db: Session): + return db.query(func.count(models.ArchiveUrl.url)).scalar() + + +def count_users(db: Session): + return db.query(func.count(models.User.email)).scalar() + + +def count_by_user_since(db: Session, seconds_delta: int = 15): + time_threshold = datetime.now() - timedelta(seconds=seconds_delta) + return db.query(models.Archive.author_id, func.count().label('total'))\ + .filter(models.Archive.created_at >= time_threshold)\ + .group_by(models.Archive.author_id)\ + .order_by(func.count().desc())\ + .limit(500).all() + + +async def find_by_store_until(db: AsyncSession, store_until_is_before: datetime) -> list[models.Archive]: + res = await db.execute( + select(models.Archive) + .filter(models.Archive.deleted == False, models.Archive.store_until < store_until_is_before) + ) + return res.scalars() + + +async def soft_delete_expired_archives(db: AsyncSession) -> dict: + to_delete = await find_by_store_until(db, datetime.now()) + counter = 0 + for archive in to_delete: + archive.deleted = True + counter += 1 + await db.commit() + return counter +# --------------- TAG + + +async def get_group_priority_async(db: AsyncSession, group_id: str) -> dict: + db_group = await db.get(models.Group, group_id) + priority = db_group.permissions.get("priority", "low") if db_group else "low" + return convert_priority_to_queue_dict(priority) + + +@cached(cache=LRUCache(maxsize=128), key=lambda db, email: hashkey(email)) +def get_user_group_names(db: Session, email: str) -> list[str]: + """ + given an email retrieves the user groups from the DB and then the email-domain groups from a global variable, the email does not need to belong to an existing user. + """ + # TODO: the read: [group1, group2] permissions don't currently work + if not email or not len(email) or "@" not in email: return [] + + # get user groups + user_groups = db.query(models.association_table_user_groups).filter_by(user_id=email).with_entities(Column("group_id")).all() + user_level_groups_names = [g[0] for g in user_groups] + + # get domain groups + domain = email.split('@')[1] + domain_level_groups = db.query(models.Group.id).filter(models.Group.domains.contains(domain)).with_entities(Column("id")).all() + domain_level_groups_names = [g[0] for g in domain_level_groups] + + return list(set(user_level_groups_names + domain_level_groups_names)) + + +def get_user_groups_by_name(db: Session, groups: list[str]) -> list[models.Group]: + return db.query(models.Group).filter( + models.Group.id.in_(groups) + ).all() + +# --------------- INIT User-Groups + + +def upsert_group(db: Session, group_name: str, description: str, orchestrator: str, orchestrator_sheet: str, service_account_email: str, permissions: dict, domains: list) -> models.Group: + db_group = db.query(models.Group).filter(models.Group.id == group_name).first() + if db_group is None: + db_group = models.Group(id=group_name, description=description, orchestrator=orchestrator, orchestrator_sheet=orchestrator_sheet, service_account_email=service_account_email, permissions=permissions, domains=domains) + db.add(db_group) + else: + db_group.description = description + db_group.orchestrator = orchestrator + db_group.orchestrator_sheet = orchestrator_sheet + db_group.service_account_email = service_account_email + db_group.permissions = permissions + db_group.domains = domains + db.commit() + db.refresh(db_group) + return db_group + + +def upsert_user(db: Session, email: str): + email = email.lower() + db_user = db.query(models.User).filter(models.User.email == email).first() + if db_user is None: + db_user = models.User(email=email) + db.add(db_user) + db.commit() + return db_user + + +def upsert_user_groups(db: Session): + def display_email_pii(email: str): + return f"'{email[0:3]}...@{email.split('@')[1]}'" + """ + reads the user_groups yaml file and inserts any new users, groups, + along with new participation of users in groups + """ + filename = get_settings().USER_GROUPS_FILENAME + logger.debug(f"Updating user-groups configuration with file {filename}.") + + ug = UserGroups(filename) + + # delete all user-groups relationships + db.query(models.association_table_user_groups).delete() + + # create a map of group_id -> domains and another of domain -> groups + group_domains = defaultdict(set) + domain_groups = defaultdict(list) + for domain, explicit_groups in ug.domains.items(): + domain_groups[domain] = list(set(explicit_groups)) + for group in explicit_groups: + group_domains[group].add(domain) + import json + # upsert groups and save a map of groupid -> dbobject + for group_id, g in ug.groups.items(): + upsert_group(db, group_id, g.description, g.orchestrator, g.orchestrator_sheet, g.service_account_email, json.loads(g.permissions.model_dump_json()), list(group_domains.get(group_id, []))) + db_groups: dict[str, models.Group] = {g.id: g for g in db.query(models.Group).all()} + + # integrity checks + for group_in_domains in group_domains: + if group_in_domains not in db_groups: + logger.warning(f"[CONFIG] Group '{group_in_domains}' does not exist in the database: domains setting will not work.") + + # reinsert users in their EXPLICITLY DEFINED groups + # domain groups are check live, as there may be new users that are not explicitly registered but belong to a domain + for email, explicit_groups in ug.users.items(): + explicit_groups = explicit_groups or [] + logger.info(f"EXPLICIT {display_email_pii(email)} => {explicit_groups}") + + db_user = upsert_user(db, email) + + # connect users to groups + for group_id in explicit_groups: + if group_id not in db_groups: + logger.warning(f"[CONFIG] Group {group_id} does not exist in config file, skipping for email={display_email_pii(email)}.") + continue + db_groups[group_id].users.append(db_user) + + db.commit() + count_user_groups = db.query(models.association_table_user_groups).count() + count_groups = db.query(func.count(models.Group.id)).scalar() + + logger.success(f"[CONFIG] DONE: [users={count_users(db)}, groups={count_groups}, explicit user groups={count_user_groups}].") + + +# --------------- SHEET +def create_sheet(db: Session, sheet_id: str, name: str, email: str, group_id: str, frequency: str): + db_sheet = models.Sheet(id=sheet_id, name=name, author_id=email, group_id=group_id, frequency=frequency) + db.add(db_sheet) + db.commit() + db.refresh(db_sheet) + return db_sheet + + +def get_user_sheet(db: Session, email: str, sheet_id: str) -> models.Sheet: + return db.query(models.Sheet).filter(models.Sheet.author_id == email, models.Sheet.id == sheet_id).first() + + +def get_user_sheets(db: Session, email: str) -> list[models.Sheet]: + return db.query(models.Sheet).filter(models.Sheet.author_id == email).order_by(models.Sheet.last_url_archived_at.desc()).all() + + +async def get_sheets_by_id_hash(db: AsyncSession, frequency: str, modulo: str, id_hash: int) -> list[models.Sheet]: + result = await db.execute( + select(models.Sheet).filter(models.Sheet.frequency == frequency) + ) + filtered = [] + for sheet in result.scalars(): + if fnv1a_hash_mod(sheet.id, modulo) == id_hash: + filtered.append(sheet) + return filtered + + +async def delete_stale_sheets(db: AsyncSession, inactivity_days: int) -> dict: + time_threshold = datetime.now() - timedelta(days=inactivity_days) + result = await db.execute( + select(models.Sheet).filter(models.Sheet.last_url_archived_at < time_threshold) + ) + deleted = defaultdict(list) + for sheet in result.scalars(): + await db.delete(sheet) + deleted[sheet.author_id].append(sheet) + await db.commit() + return dict(deleted) + + +def delete_sheet(db: Session, sheet_id: str, email: str) -> bool: + db_sheet = db.query(models.Sheet).filter(models.Sheet.id == sheet_id, models.Sheet.author_id == email).first() + if db_sheet: + db.delete(db_sheet) + db.commit() + return db_sheet is not None diff --git a/app/web/db/user_state.py b/app/web/db/user_state.py new file mode 100644 index 0000000..968e1bd --- /dev/null +++ b/app/web/db/user_state.py @@ -0,0 +1,341 @@ + +from typing import Dict, Set +import sqlalchemy +from sqlalchemy.orm import Session +from sqlalchemy import func +from datetime import datetime + +from app.shared.db import models +from app.shared.user_groups import GroupInfo, GroupPermissions +from app.shared.schemas import Usage, UsageResponse +from app.web.db import crud +from app.web.utils.misc import convert_priority_to_queue_dict + + +class UserState: + """ + Manage a user's state and permissions + """ + + def __init__(self, db: Session, email: str): + self.db = db + self.email = email.lower() + + @property + def permissions(self) -> Dict[str, GroupInfo]: + """ + Returns a dict of all group permissions and a special {"all": read/archive_url/archive_sheet} key + """ + if not hasattr(self, '_permissions'): + self._permissions = {} + self._permissions["all"] = GroupInfo( + read=self.read, + read_public=self.read_public, + archive_url=self.archive_url, + archive_sheet=self.archive_sheet, + # below are relevant only for /url endpoints + max_archive_lifespan_months=self.max_archive_lifespan_months, + max_monthly_urls=self.max_monthly_urls, + max_monthly_mbs=self.max_monthly_mbs, + priority=self.priority + ) + for group in self.user_groups: + if not group.permissions: continue + self._permissions[group.id] = GroupInfo(**group.permissions, description=group.description, service_account_email=group.service_account_email) + return self._permissions + + @property + def user_groups_names(self): + if not hasattr(self, '_user_groups_names'): + self._user_groups_names = crud.get_user_group_names(self.db, self.email) + ["default"] + return self._user_groups_names + + @property + def user_groups(self): + if not hasattr(self, '_user_groups'): + self._user_groups = crud.get_user_groups_by_name(self.db, self.user_groups_names) + return self._user_groups + + @property + def read(self) -> Set[str] | bool: + """ + Read can be a list of group names or True, if all can be read. + """ + if not hasattr(self, '_read'): + self._read = set() + for group in self.user_groups: + if not group.permissions: continue + group_read_permissions = group.permissions.get("read", []) + if "all" in group_read_permissions: + self._read = True + return self._read + else: + self._read.update(group.permissions.get("read", [])) + return self._read + + @property + def read_public(self) -> bool: + """ + Read public permission + """ + if not hasattr(self, '_read_public'): + self._read_public = False + for group in self.user_groups: + if not group.permissions: continue + if group.permissions.get("read_public", False): + self._read_public = True + return self._read_public + return self._read_public + + @property + def archive_url(self) -> bool: + """ + Archive URL permission + """ + if not hasattr(self, '_archive_url'): + self._archive_url = False + for group in self.user_groups: + if not group.permissions: continue + if group.permissions.get("archive_url", False): + self._archive_url = True + return self._archive_url + return self._archive_url + + @property + def archive_sheet(self) -> bool: + """ + Archive sheet permission + """ + if not hasattr(self, '_archive_sheet'): + self._archive_sheet = False + for group in self.user_groups: + if not group.permissions: continue + if group.permissions.get("archive_sheet", False): + self._archive_sheet = True + return self._archive_sheet + return self._archive_sheet + + @property + def sheet_frequency(self): + if not hasattr(self, '_sheet_frequency'): + self._sheet_frequency = set() + for group in self.user_groups: + if not group.permissions: continue + self._sheet_frequency.update(group.permissions.get("sheet_frequency", None)) + return self._sheet_frequency + + @property + def max_archive_lifespan_months(self) -> int: + if not hasattr(self, '_max_archive_lifespan_months'): + self._max_archive_lifespan_months = self._helper_for_grouping_max_numerical_permissions("max_archive_lifespan_months") + return self._max_archive_lifespan_months + + @property + def max_monthly_urls(self) -> int: + if not hasattr(self, '_max_monthly_urls'): + self._max_monthly_urls = self._helper_for_grouping_max_numerical_permissions("max_monthly_urls") + return self._max_monthly_urls + + @property + def max_monthly_mbs(self) -> int: + if not hasattr(self, '_max_monthly_mbs'): + self._max_monthly_mbs = self._helper_for_grouping_max_numerical_permissions("max_monthly_mbs") + return self._max_monthly_mbs + + @property + def priority(self) -> str: + if not hasattr(self, '_priority'): + self._priority = "low" + for group in self.user_groups: + if not group.permissions: continue + if group.permissions.get("priority", self._priority) == "high": + self._priority = "high" + break + return self._priority + + @property + def active(self) -> bool: + """ + A user is active if they can read/archive anything + """ + if not hasattr(self, '_active'): + self._active = bool(self.read or self.read_public or self.archive_url or self.archive_sheet) + return self._active + + def _helper_for_grouping_max_numerical_permissions(self, permission_name: str) -> int: + """ + Iterates one of the numerical permissions where -1 means no restrictions and returns either -1 or the maximum value, defaults according to GroupPermissions + """ + default = GroupPermissions.model_fields[permission_name].default + max_value = default + for group in self.user_groups: + if not group.permissions: continue + group_value = group.permissions.get(permission_name, default) + if group_value == -1: + max_value = -1 + return max_value + max_value = max(max_value, group_value) + return max_value + + def in_group(self, group_id: str) -> bool: + return group_id in self.user_groups_names + + def usage(self) -> Dict: + """ + returns the monthly quotas for the URLs/MBs and the totals for Sheets + """ + current_month = datetime.now().month + current_year = datetime.now().year + + # find and sum all user sheets over this month + user_sheets = self.db.query( + models.Sheet.group_id, + func.count(models.Sheet.id).label('sheet_count') + ).filter(models.Sheet.author_id == self.email).group_by(models.Sheet.group_id).all() + + sheets_by_group = {sheet.group_id: sheet.sheet_count for sheet in user_sheets} + + # find and sum all user urls over this month + urls_by_group = self.db.query( + models.Archive.group_id, + func.count(models.Archive.id).label('url_count'), + func.coalesce(func.sum( + func.coalesce( + func.cast( + func.json_extract(models.Archive.result, '$.metadata.total_bytes'), + sqlalchemy.Integer + ), 0 + ) + ), 0).label('total_bytes') + ).filter( + models.Archive.author_id == self.email, + func.extract('month', models.Archive.created_at) == current_month, + func.extract('year', models.Archive.created_at) == current_year + ).group_by(models.Archive.group_id).all() + + # merge the two queries + usage_by_group: Dict[str, Usage] = { + (url.group_id or ""): + Usage(monthly_urls=url.url_count, monthly_mbs=int(url.total_bytes / 1024 / 1024)) + for url in urls_by_group + } + for group_id, sheet_count in sheets_by_group.items(): + group_id = group_id or "" + if group_id in usage_by_group: + usage_by_group[group_id].total_sheets = sheet_count + else: + usage_by_group[group_id] = Usage(total_sheets=sheet_count) + + # calculate totals + total_sheets = sum([sheet.sheet_count for sheet in user_sheets]) + total_bytes = sum([url.total_bytes for url in urls_by_group]) + total_urls = sum([url.url_count for url in urls_by_group]) + + return UsageResponse( + monthly_urls=total_urls, + monthly_mbs=int(total_bytes / 1024 / 1024), + total_sheets=total_sheets, + groups=usage_by_group + ) + + def has_quota_monthly_sheets(self, group_id: str) -> bool: + """ + checks if a user has reached their sheet quota for a given group + """ + if group_id not in self.permissions: + return False + + user_sheets = self.db.query(models.Sheet).filter(models.Sheet.author_id == self.email, models.Sheet.group_id == group_id).count() + + sheet_quota = self.permissions[group_id].max_sheets + if sheet_quota == -1: + return True + return user_sheets < sheet_quota + + def has_quota_max_monthly_urls(self, group_id: str) -> bool: + """ + checks if a user has reached their monthly url quota for a group, if global then group should be empty string + """ + quota = 0 + if not group_id: + quota = self.max_monthly_urls + else: + if group_id not in self.permissions: return False + quota = self.permissions[group_id].max_monthly_urls + + if quota == -1: + return True + + current_month = datetime.now().month + current_year = datetime.now().year + user_urls = self.db.query(models.Archive).filter( + models.Archive.author_id == self.email, + models.Archive.group_id == group_id, + func.extract('month', models.Archive.created_at) == current_month, + func.extract('year', models.Archive.created_at) == current_year + ).count() + + return user_urls < quota + + def has_quota_max_monthly_mbs(self, group_id: str) -> bool: + """ + checks if a user has reached their monthly MBs quota for a group, if global then group should be empty string + """ + quota = 0 + if not group_id: + quota = self.max_monthly_mbs + else: + if group_id not in self.permissions: return False + quota = self.permissions[group_id].max_monthly_mbs + + if quota == -1: + return True + + current_month = datetime.now().month + current_year = datetime.now().year + + # find and sum all user bytes over this month + user_bytes = self.db.query(models.Archive).filter( + models.Archive.author_id == self.email, + models.Archive.group_id == group_id, + func.extract('month', models.Archive.created_at) == current_month, + func.extract('year', models.Archive.created_at) == current_year + ).with_entities(func.coalesce(func.sum( + func.coalesce( + func.cast( + func.json_extract(models.Archive.result, '$.metadata.total_bytes'), + sqlalchemy.Integer + ), 0 + ) + ), 0).label('total')).scalar() + + # convert bytes to mb + user_mbs = int(user_bytes / 1024 / 1024) + return user_mbs < quota + + def can_manually_trigger(self, group_id: str) -> bool: + """ + checks if a user is allowed to manually trigger a sheet + """ + if group_id not in self.permissions: + return False + + return self.permissions[group_id].manually_trigger_sheet + + def is_sheet_frequency_allowed(self, group_id: str, frequency: str) -> bool: + """ + checks if a user is allowed to create a sheet with this frequency for this group + """ + if group_id not in self.permissions: + return False + + return frequency in self.permissions[group_id].sheet_frequency + + def priority_group(self, group_id: str) -> str: + priority = "low" + for group in self.user_groups: + if group.id != group_id: continue + if not group.permissions: continue + priority = group.permissions.get("priority", priority) + break + return convert_priority_to_queue_dict(priority) diff --git a/src/worker/__init__.py b/app/web/endpoints/__init__.py similarity index 100% rename from src/worker/__init__.py rename to app/web/endpoints/__init__.py diff --git a/app/web/endpoints/default.py b/app/web/endpoints/default.py new file mode 100644 index 0000000..9271992 --- /dev/null +++ b/app/web/endpoints/default.py @@ -0,0 +1,50 @@ + +from typing import Dict +from fastapi import APIRouter, Depends, HTTPException +from fastapi.responses import FileResponse, JSONResponse + +from app.web.config import VERSION, BREAKING_CHANGES +from app.shared.schemas import ActiveUser, UsageResponse +from app.web.db.user_state import UserState +from app.web.security import get_user_state +from app.shared.user_groups import GroupInfo + +default_router = APIRouter() + + +@default_router.get("/") +async def home(): + return JSONResponse({"version": VERSION, "breakingChanges": BREAKING_CHANGES}) + + +@default_router.get("/health") +async def health(): + return JSONResponse({"status": "ok"}) + + +@default_router.get("/user/active", summary="Check if the user is active and can use the tool.") +async def active( + user: UserState = Depends(get_user_state), +) -> ActiveUser: + return {"active": user.active} + + +@default_router.get("/user/permissions", summary="Get the user's global 'all' permissions and the permissions for each group they belong to.") +def get_user_permissions( + user: UserState = Depends(get_user_state), +) -> Dict[str, GroupInfo]: + return user.permissions + +@default_router.get("/user/usage", summary="Get the user's monthly URLs/MBs usage along with the total active sheets, breakdown by group.") +def get_user_usage( + user: UserState = Depends(get_user_state), +) -> UsageResponse: + if not user.active: + raise HTTPException(status_code=403, detail="User is not active.") + return user.usage() + + + +@default_router.get('/favicon.ico', include_in_schema=False) +async def favicon() -> FileResponse: + return FileResponse("app/web/static/favicon.ico") diff --git a/app/web/endpoints/interoperability.py b/app/web/endpoints/interoperability.py new file mode 100644 index 0000000..085bacc --- /dev/null +++ b/app/web/endpoints/interoperability.py @@ -0,0 +1,61 @@ +import json +from fastapi import APIRouter, Depends, HTTPException +from fastapi.responses import JSONResponse +from loguru import logger +import sqlalchemy +from auto_archiver.core import Metadata +from sqlalchemy.orm import Session + +from app.shared.aa_utils import get_all_urls +from app.web.config import ALLOW_ANY_EMAIL +from app.shared import business_logic, schemas +from app.shared.db import worker_crud +from app.shared.db.database import get_db_dependency +from app.web.security import token_api_key_auth +from app.shared.db import models +from app.shared.log import log_error + + +interoperability_router = APIRouter(prefix="/interop", tags=["Interoperability endpoints."]) + + +# ----- endpoint to submit data archived elsewhere +@interoperability_router.post("/submit-archive", status_code=201, summary="Submit a manual archive entry, for data that was archived elsewhere.") +def submit_manual_archive( + manual: schemas.SubmitManualArchive, + auth=Depends(token_api_key_auth), + db: Session = Depends(get_db_dependency) +): + try: + result: Metadata = Metadata.from_json(manual.result) + except json.JSONDecodeError as e: + log_error(e) + raise HTTPException(status_code=422, detail="Invalid JSON in result field.") + manual.author_id = manual.author_id or ALLOW_ANY_EMAIL + manual.tags.add("manual") + + try: + store_until=business_logic.get_store_archive_until(db, manual.group_id) + except AssertionError as e: + log_error(e) + raise HTTPException(status_code=422, detail=str(e)) + + try: + archive = schemas.ArchiveCreate( + author_id=manual.author_id, + url=result.get_url(), + public=manual.public, + group_id=manual.group_id, + tags=manual.tags, + id=models.generate_uuid(), + result=json.loads(result.to_json()), + urls=get_all_urls(result), + store_until=store_until, + ) + + db_archive = worker_crud.store_archived_url(db, archive) + logger.debug(f"[MANUAL ARCHIVE STORED] {db_archive.author_id} {db_archive.url}") + return JSONResponse({"id": db_archive.id}, status_code=201) + except sqlalchemy.exc.IntegrityError as e: + log_error(e) + raise HTTPException(status_code=422, detail=f"Cannot insert into DB due to integrity error, likely duplicate urls.") diff --git a/app/web/endpoints/sheet.py b/app/web/endpoints/sheet.py new file mode 100644 index 0000000..7848b5e --- /dev/null +++ b/app/web/endpoints/sheet.py @@ -0,0 +1,81 @@ + +from fastapi import APIRouter, Depends, HTTPException +from fastapi.responses import JSONResponse + +from sqlalchemy import exc +from sqlalchemy.orm import Session + +from app.web.db.user_state import UserState +from app.shared import schemas +from app.shared.task_messaging import get_celery +from app.web.security import get_user_state +from app.web.db import crud +from app.shared.db.database import get_db_dependency + +sheet_router = APIRouter(prefix="/sheet", tags=["Google Spreadsheet operations"]) + +celery = get_celery() + +@sheet_router.post("/create", status_code=201, summary="Store a new Google Sheet for regular archiving.") +def create_sheet( + sheet: schemas.SheetAdd, + user: UserState = Depends(get_user_state), + db: Session = Depends(get_db_dependency), +) -> schemas.SheetResponse: + + if not user.in_group(sheet.group_id): + raise HTTPException(status_code=403, detail="User does not have access to this group.") + + if not user.has_quota_monthly_sheets(sheet.group_id): + raise HTTPException(status_code=429, detail="User has reached their sheet quota for this group.") + + if not user.is_sheet_frequency_allowed(sheet.group_id, sheet.frequency): + raise HTTPException(status_code=422, detail="Invalid frequency selected for this group.") + + try: + return crud.create_sheet(db, sheet.id, sheet.name, user.email, sheet.group_id, sheet.frequency) + except exc.IntegrityError as e: + raise HTTPException(status_code=400, detail="Sheet with this ID is already being archived.") from e + + +@sheet_router.get("/mine", status_code=200, summary="Get the authenticated user's Google Sheets.") +def get_user_sheets( + user: UserState = Depends(get_user_state), + db: Session = Depends(get_db_dependency) +) -> list[schemas.SheetResponse]: + return crud.get_user_sheets(db, user.email) + + +@sheet_router.delete("/{id}", summary="Delete a Google Sheet by ID.") +def delete_sheet( + id: str, + user: UserState = Depends(get_user_state), + db: Session = Depends(get_db_dependency), +) -> schemas.DeleteResponse: + return JSONResponse({ + "id": id, + "deleted": crud.delete_sheet(db, id, user.email) + }) + + +@sheet_router.post("/{id}/archive", status_code=201, summary="Trigger an archiving task for a GSheet you own.", response_description="task_id for the archiving task.") +def archive_user_sheet( + id: str, + user: UserState = Depends(get_user_state), + db: Session = Depends(get_db_dependency), +) -> schemas.Task: + + sheet = crud.get_user_sheet(db, user.email, sheet_id=id) + if not sheet: + raise HTTPException(status_code=403, detail="No access to this sheet.") + + if not user.in_group(sheet.group_id): + raise HTTPException(status_code=403, detail="User does not have access to this group.") + + if not user.can_manually_trigger(sheet.group_id): + raise HTTPException(status_code=429, detail="User cannot manually trigger sheet archiving in this group.") + + group_queue = user.priority_group(sheet.group_id) + task = celery.signature("create_sheet_task", args=[schemas.SubmitSheet(sheet_id=id, author_id=user.email, group_id=sheet.group_id).model_dump_json()]).apply_async(**group_queue) + + return JSONResponse({"id": task.id}, status_code=201) \ No newline at end of file diff --git a/src/endpoints/task.py b/app/web/endpoints/task.py similarity index 82% rename from src/endpoints/task.py rename to app/web/endpoints/task.py index f446d12..610c579 100644 --- a/src/endpoints/task.py +++ b/app/web/endpoints/task.py @@ -3,21 +3,19 @@ from fastapi import APIRouter, Depends from fastapi.encoders import jsonable_encoder from fastapi.responses import JSONResponse -from loguru import logger -from web.security import get_token_or_user_auth - -from db import schemas -from core.logging import log_error -from worker.main import celery -from utils.mics import custom_jsonable_encoder +from app.shared.task_messaging import get_celery +from app.web.security import get_token_or_user_auth +from app.shared import schemas +from app.shared.log import log_error +from app.web.utils.misc import custom_jsonable_encoder task_router = APIRouter(prefix="/task", tags=["Async task operations"]) +celery = get_celery() @task_router.get("/{task_id}", summary="Check the status of an async task by its id, works for URLs and Sheet tasks.") def get_status(task_id, email=Depends(get_token_or_user_auth)) -> schemas.TaskResult: - logger.info(f"status check for user {email} task {task_id}") task = AsyncResult(task_id, app=celery) try: if task.status == "FAILURE": diff --git a/app/web/endpoints/url.py b/app/web/endpoints/url.py new file mode 100644 index 0000000..c237893 --- /dev/null +++ b/app/web/endpoints/url.py @@ -0,0 +1,84 @@ + +from fastapi import APIRouter, Depends, HTTPException +from fastapi.responses import JSONResponse +from datetime import datetime +from loguru import logger +from sqlalchemy.orm import Session + +from app.web.config import ALLOW_ANY_EMAIL +from app.shared import schemas +from app.shared.task_messaging import get_celery +from app.web.security import get_token_or_user_auth, get_user_state +from app.web.db import crud +from app.web.db.user_state import UserState +from app.shared.db.database import get_db_dependency + +from urllib.parse import urlparse + +from app.web.utils.misc import convert_priority_to_queue_dict + +url_router = APIRouter(prefix="/url", tags=["Single URL operations"]) + +celery = get_celery() + +@url_router.post("/archive", status_code=201, summary="Submit a single URL archive request, starts an archiving task.", response_description="task_id for the archiving task, will match the archive id.") +def archive_url( + archive: schemas.ArchiveTrigger, + email=Depends(get_token_or_user_auth), + db: Session = Depends(get_db_dependency) +) -> schemas.Task: + archive.author_id = email + logger.info(f"new {archive.public=} task for {email=} and {archive.group_id=}: {archive.url}") + + parsed_url = urlparse(archive.url) + if not all([parsed_url.scheme, parsed_url.netloc]): + raise HTTPException(status_code=400, detail="Invalid URL received.") + + archive_create = schemas.ArchiveCreate(**archive.model_dump()) + if email != ALLOW_ANY_EMAIL: + user = UserState(db, email) + if archive.group_id and not user.in_group(archive.group_id): + raise HTTPException(status_code=403, detail="User does not have access to this group.") + if not user.has_quota_max_monthly_urls(archive.group_id): + raise HTTPException(status_code=429, detail="User has reached their monthly URL quota.") + if not user.has_quota_max_monthly_mbs(archive.group_id): + raise HTTPException(status_code=429, detail="User has reached their monthly MB quota.") + group_queue = user.priority_group(archive_create.group_id) + else: + group_queue = convert_priority_to_queue_dict("high") + + + task = celery.signature("create_archive_task", args=[archive_create.model_dump_json()]).apply_async(**group_queue) + task_response = schemas.Task(id=task.id) + return JSONResponse(task_response.model_dump(), status_code=201) + + +@url_router.get("/search", summary="Search for archive entries by URL.") +def search_by_url( + url: str, skip: int = 0, limit: int = 25, + archived_after: datetime = None, archived_before: datetime = None, + db: Session = Depends(get_db_dependency), + email: str = Depends(get_token_or_user_auth) +) -> list[schemas.ArchiveResult]: + + read_groups, read_public = False, False + if email != ALLOW_ANY_EMAIL: + user = UserState(db, email) + if not user.read and not user.read_public: + raise HTTPException(status_code=403, detail="User does not have read access.") + read_groups = user.read + read_public = user.read_public + return crud.search_archives_by_url(db, url.strip(), email, read_groups, read_public, skip=skip, limit=limit, archived_after=archived_after, archived_before=archived_before) + + +@url_router.delete("/{id}", summary="Delete a single URL archive by id.") +def delete_archive( + id:str, + user: UserState = Depends(get_user_state), + db: Session = Depends(get_db_dependency) +) -> schemas.DeleteResponse: + logger.info(f"deleting url archive task {id} request by {user.email}") + return JSONResponse({ + "id": id, + "deleted": crud.soft_delete_archive(db, id, user.email) + }) diff --git a/app/web/events.py b/app/web/events.py new file mode 100644 index 0000000..625731a --- /dev/null +++ b/app/web/events.py @@ -0,0 +1,186 @@ +import asyncio +from collections import defaultdict +import datetime +import logging +import alembic.config +from fastapi import FastAPI +from contextlib import asynccontextmanager +from fastapi_utils.tasks import repeat_every +from loguru import logger +from fastapi_mail import FastMail, MessageSchema, MessageType + +from app.shared.db import models +from app.shared.db.database import get_db, get_db_async, make_engine, wal_checkpoint +from app.shared import schemas +from app.shared.settings import get_settings +from app.shared.task_messaging import get_celery +from app.web.db import crud +from app.web.middleware import increase_exceptions_counter +from app.web.utils.metrics import measure_regular_metrics, redis_subscribe_worker_exceptions + +celery = get_celery() + + +@asynccontextmanager +async def lifespan(app: FastAPI): + # see https://fastapi.tiangolo.com/advanced/events/#lifespan + + # STARTUP + engine = make_engine(get_settings().DATABASE_PATH) + models.Base.metadata.create_all(bind=engine) + alembic.config.main(prog="alembic", argv=['--raiseerr', 'upgrade', 'head']) + logging.getLogger("uvicorn.access").disabled = True # loguru + asyncio.create_task(redis_subscribe_worker_exceptions(get_settings().REDIS_EXCEPTIONS_CHANNEL)) + asyncio.create_task(repeat_measure_regular_metrics()) + with get_db() as db: + crud.upsert_user_groups(db) + + # setup archive cronjobs + if get_settings().CRON_ARCHIVE_SHEETS: + asyncio.create_task(archive_hourly_sheets_cronjob()) + asyncio.create_task(archive_daily_sheets_cronjob()) + else: + logger.warning("[CRON] Sheet archive cronjobs are disabled.") + + if get_settings().CRON_DELETE_STALE_SHEETS: + asyncio.create_task(delete_stale_sheets()) + else: + logger.warning("[CRON] Delete stale sheets cronjob is disabled.") + + if get_settings().CRON_DELETE_SCHEDULED_ARCHIVES: + asyncio.create_task(notify_about_expired_archives()) + else: + logger.warning("[CRON] Delete scheduled archives cronjob is disabled.") + + wal_checkpoint() + + yield # separates startup from shutdown instructions + + # SHUTDOWN + logger.info("shutting down") + + +# CRON JOBS +@repeat_every(seconds=get_settings().REPEAT_COUNT_METRICS_SECONDS, on_exception=increase_exceptions_counter) +async def repeat_measure_regular_metrics(): + await measure_regular_metrics(get_settings().DATABASE_PATH, get_settings().REPEAT_COUNT_METRICS_SECONDS) + + +@repeat_every(seconds=60, wait_first=120, on_exception=increase_exceptions_counter) +async def archive_hourly_sheets_cronjob(): + await archive_sheets_cronjob("hourly", 60, datetime.datetime.now().minute) + + +@repeat_every(seconds=3600, wait_first=120, on_exception=increase_exceptions_counter) +async def archive_daily_sheets_cronjob(): + await archive_sheets_cronjob("daily", 24, datetime.datetime.now().hour) + + +async def archive_sheets_cronjob(frequency: str, interval: int, current_time_unit: int): + triggered_jobs = [] + + async with get_db_async() as db: + sheets = await crud.get_sheets_by_id_hash(db, frequency, interval, current_time_unit) + for s in sheets: + group_queue = await crud.get_group_priority_async(db, s.group_id) + task = celery.signature("create_sheet_task", args=[schemas.SubmitSheet(sheet_id=s.id, author_id=s.author_id, group_id=s.group_id).model_dump_json()]).apply_async(**group_queue) + + triggered_jobs.append({"sheet_id": s.id, "task_id": task.id}) + logger.debug(f"[CRON {frequency.upper()}:{current_time_unit}] Triggered {len(triggered_jobs)} sheet tasks: {triggered_jobs}") + + +# TODO: on exception should logerror but also prometheus counter +DELETE_WINDOW = get_settings().DELETE_SCHEDULED_ARCHIVES_CHECK_EVERY_N_DAYS * 24 * 60 * 60 + + +@repeat_every(seconds=DELETE_WINDOW, wait_first=180, on_exception=increase_exceptions_counter) +async def notify_about_expired_archives(): + notify_from = datetime.datetime.now() + datetime.timedelta(days=get_settings().DELETE_SCHEDULED_ARCHIVES_CHECK_EVERY_N_DAYS) + async with get_db_async() as db: + scheduled_deletions = await crud.find_by_store_until(db, notify_from) + + user_archives = defaultdict(list) + for archive in scheduled_deletions: + user_archives[archive.author_id].append(archive) + + if user_archives: + fastmail = FastMail(get_settings().MAIL_CONFIG) + # notify users + for email in user_archives: + list_of_archives = "\n".join([f'{a.url}, {a.id}, {a.store_until.isoformat()}
' for a in user_archives[email]]) + # TODO: how can users download them in bulk? + message = MessageSchema( + subject="Auto Archiver: Archives Scheduled for Deletion", + recipients=[email], + body=f""" + + +

Hi {email},

+

Some of your archives will be deleted in the next {get_settings().DELETE_SCHEDULED_ARCHIVES_CHECK_EVERY_N_DAYS} days, as they are reaching their expiration date according to our retention policy for their groups.

+

If you want to preserve any, make sure to download them now.

+

Here is a CSV list of URLs:

+ + url,archive_id,time_of_deletion
+ {list_of_archives} +
+

Best,
The Auto Archiver team

+ + + """, + subtype=MessageType.html + ) + await fastmail.send_message(message) + logger.debug(f"[CRON] Email sent to {email} about {len(user_archives[email])} scheduled archives deletion.") + + # now schedule the deletion event + asyncio.create_task(delete_expired_archives()) + + +@repeat_every(max_repetitions=1, wait_first=10, seconds=0, on_exception=increase_exceptions_counter) +async def delete_expired_archives(): + async with get_db_async() as db: + count_deleted = await crud.soft_delete_expired_archives(db) + if count_deleted: + logger.debug(f"[CRON] Deleted {count_deleted} archives.") + + +@repeat_every(seconds=86400, wait_first=150, on_exception=increase_exceptions_counter) +async def delete_stale_sheets(): + STALE_DAYS = get_settings().DELETE_STALE_SHEETS_DAYS + logger.debug(f"[CRON] Deleting stale sheets older than {STALE_DAYS} days.") + async with get_db_async() as db: + user_sheets = await crud.delete_stale_sheets(db, STALE_DAYS) + + if not user_sheets: return + + fastmail = FastMail(get_settings().MAIL_CONFIG) + # notify users + for email in user_sheets: + list_of_sheets = "\n".join([f'
  • {s.name}
  • ' for s in user_sheets[email]]) + message = MessageSchema( + subject="Auto Archiver: Stale Sheets Removed", + recipients=[email], + body=f""" + + +

    Hi {email},

    +

    Your stale sheets have been removed from our system as no new URL was archived in the past {STALE_DAYS} days:

    +
      + {list_of_sheets} +
    +

    You can always re-add them at https://auto-archiver.bellingcat.com/.

    +

    Best,
    The Auto Archiver team

    + + + """, + subtype=MessageType.html + ) + await fastmail.send_message(message) + logger.debug(f"[CRON] Email sent to {email} about stale sheets deletion.") + + +# @repeat_at +async def generate_users_export_csv(): + #TODO: implement a cronjob that regularly requested user data to a CSV file + # see https://colab.research.google.com/drive/1QDbo3QXHPBdiTuANlA1AWVvN-rqxuCPa?authuser=0#scrollTo=4nPXeSdK8RBT + pass \ No newline at end of file diff --git a/app/web/main.py b/app/web/main.py new file mode 100644 index 0000000..ff2266e --- /dev/null +++ b/app/web/main.py @@ -0,0 +1,60 @@ +import os +from fastapi import FastAPI, Depends +from fastapi.staticfiles import StaticFiles +from fastapi.middleware.cors import CORSMiddleware +from prometheus_fastapi_instrumentator import Instrumentator +from loguru import logger + +from app.web.middleware import logging_middleware +from app.shared.task_messaging import get_celery + +from app.web.security import token_api_key_auth +from app.web.config import VERSION, API_DESCRIPTION +from app.web.events import lifespan +from app.shared.settings import get_settings + + +from app.web.endpoints.default import default_router +from app.web.endpoints.url import url_router +from app.web.endpoints.sheet import sheet_router +from app.web.endpoints.task import task_router +from app.web.endpoints.interoperability import interoperability_router + +celery = get_celery() + +def app_factory(settings = get_settings()): + app = FastAPI( + title="Auto-Archiver API", + description=API_DESCRIPTION, + version=VERSION, + contact={"name": "GitHub", "url": "https://github.com/bellingcat/auto-archiver-api"}, + lifespan=lifespan + ) + + app.add_middleware( + CORSMiddleware, + allow_origins=settings.ALLOWED_ORIGINS, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + app.middleware("http")(logging_middleware) + + app.include_router(default_router) + app.include_router(url_router) + app.include_router(sheet_router) + app.include_router(task_router) + app.include_router(interoperability_router) + + # prometheus exposed in /metrics with authentication + Instrumentator(should_group_status_codes=False, excluded_handlers=["/metrics", "/health", "/openapi.json", "/favicon.ico"]).instrument(app).expose(app, dependencies=[Depends(token_api_key_auth)]) + + if settings.SERVE_LOCAL_ARCHIVE: + local_dir = settings.SERVE_LOCAL_ARCHIVE + if not os.path.isdir(local_dir) and os.path.isdir(local_dir.replace("/app", ".")): + local_dir = local_dir.replace("/app", ".") + if len(settings.SERVE_LOCAL_ARCHIVE) > 1 and os.path.isdir(local_dir): + logger.warning(f"MOUNTing local archive, use this in development only {settings.SERVE_LOCAL_ARCHIVE}") + app.mount(settings.SERVE_LOCAL_ARCHIVE, StaticFiles(directory=local_dir), name=settings.SERVE_LOCAL_ARCHIVE) + + return app \ No newline at end of file diff --git a/app/web/middleware.py b/app/web/middleware.py new file mode 100644 index 0000000..52da626 --- /dev/null +++ b/app/web/middleware.py @@ -0,0 +1,31 @@ + +import traceback +from loguru import logger +from fastapi import Request +from app.shared.log import log_error +from app.web.utils.metrics import EXCEPTION_COUNTER + + +async def logging_middleware(request: Request, call_next): + try: + response = await call_next(request) + #TODO: use Origin to have summary prometheus metrics on where requests come from + # origin = request.headers.get("origin") + logger.info(f"{request.client.host}:{request.client.port} {request.method} {request.url._url} - HTTP {response.status_code}") + return response + except Exception as e: + location = f"{request.method} {request.url._url}" + await increase_exceptions_counter(e, location) + logger.info(f"{request.client.host}:{request.client.port} {location} - {e.__class__.__name__} {e}") + raise e + +async def increase_exceptions_counter(e: Exception, location:str="cronjob"): + if location == "cronjob": + try: + last_trace = traceback.extract_tb(e.__traceback__)[-1] + _file, _line, func_name, _text = last_trace + location = func_name + except Exception as e: + logger.error(f"Unable to get function name from cronjob exception traceback: {e}") + EXCEPTION_COUNTER.labels(type=e.__class__.__name__, location=location).inc() + log_error(e) \ No newline at end of file diff --git a/src/web/security.py b/app/web/security.py similarity index 87% rename from src/web/security.py rename to app/web/security.py index bfa678a..12115af 100644 --- a/src/web/security.py +++ b/app/web/security.py @@ -2,8 +2,12 @@ from loguru import logger import requests, secrets from fastapi import HTTPException, status, Depends from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials -from core.config import ALLOW_ANY_EMAIL -from shared.settings import get_settings +from sqlalchemy.orm import Session + +from app.web.config import ALLOW_ANY_EMAIL +from app.shared.settings import get_settings +from app.shared.db.database import get_db_dependency +from app.web.db.user_state import UserState settings = get_settings() bearer_security = HTTPBearer() @@ -45,7 +49,7 @@ async def get_user_auth(credentials: HTTPAuthorizationCredentials = Depends(bear # validates the Bearer token in the case that it requires it valid_user, info = authenticate_user(credentials.credentials) if valid_user: - return info + return info.lower() logger.debug(f"TOKEN FAILURE: {valid_user=} {info=}") raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -69,7 +73,11 @@ def authenticate_user(access_token): return False, f"email '{j.get('email')}' not verified" if int(j.get("expires_in", -1)) <= 0: return False, "Token expired" - return True, j.get('email') + return True, j.get('email').lower() except Exception as e: logger.warning(f"AUTH EXCEPTION occurred: {e}") return False, "exception occurred" + + +def get_user_state(email:str=Depends(get_user_auth), db:Session=Depends(get_db_dependency)): + return UserState(db, email) \ No newline at end of file diff --git a/src/migrations/versions/.gitkeep b/app/web/static/.gitkeep similarity index 100% rename from src/migrations/versions/.gitkeep rename to app/web/static/.gitkeep diff --git a/src/static/favicon.ico b/app/web/static/favicon.ico similarity index 100% rename from src/static/favicon.ico rename to app/web/static/favicon.ico diff --git a/src/static/.gitkeep b/app/web/utils/__init__.py similarity index 100% rename from src/static/.gitkeep rename to app/web/utils/__init__.py diff --git a/src/utils/metrics.py b/app/web/utils/metrics.py similarity index 81% rename from src/utils/metrics.py rename to app/web/utils/metrics.py index 8d513e6..a885b9a 100644 --- a/src/utils/metrics.py +++ b/app/web/utils/metrics.py @@ -3,23 +3,23 @@ import json import os import shutil from prometheus_client import Counter, Gauge -import redis -from db import crud -from db.database import get_db -from core.logging import log_error +from app.web.db import crud +from app.shared.db.database import get_db +from app.shared.log import log_error +from app.shared.task_messaging import get_redis # Custom metrics EXCEPTION_COUNTER = Counter( "exceptions", "Number of times a certain exception has occurred.", - labelnames=["types"] + labelnames=["type", "location"] ) WORKER_EXCEPTION = Counter( "worker_exceptions_total", "Number of times a certain exception has occurred on the worker.", - labelnames=["types", "exception", "task", "traceback"] + labelnames=["type", "exception", "task", "traceback"] ) DISK_UTILIZATION = Gauge( "disk_utilization", @@ -38,16 +38,16 @@ DATABASE_METRICS_COUNTER = Counter( ) -async def redis_subscribe_worker_exceptions(REDIS_EXCEPTIONS_CHANNEL, CELERY_BROKER_URL): +async def redis_subscribe_worker_exceptions(REDIS_EXCEPTIONS_CHANNEL: str): # Subscribe to Redis channel and increment the counter for each exception with info on the exception and task - Rdis = redis.Redis.from_url(CELERY_BROKER_URL) - PubSubExceptions = Rdis.pubsub() + Redis = get_redis() + PubSubExceptions = Redis.pubsub() PubSubExceptions.subscribe(REDIS_EXCEPTIONS_CHANNEL) while True: message = PubSubExceptions.get_message() if message and message["type"] == "message": data = json.loads(message["data"].decode("utf-8")) - WORKER_EXCEPTION.labels(types=type(data["exception"]).__name__, exception=data["exception"], task=data["task"], traceback=data["traceback"]).inc() + WORKER_EXCEPTION.labels(type=data["type"], exception=data["exception"], task=data["task"], traceback=data["traceback"]).inc() await asyncio.sleep(1) diff --git a/app/web/utils/misc.py b/app/web/utils/misc.py new file mode 100644 index 0000000..870a60b --- /dev/null +++ b/app/web/utils/misc.py @@ -0,0 +1,15 @@ +import base64 +from fastapi.encoders import jsonable_encoder + + +def custom_jsonable_encoder(obj): + if isinstance(obj, bytes): + return base64.b64encode(obj).decode('utf-8') + return jsonable_encoder(obj) + + +def convert_priority_to_queue_dict(priority: str) -> dict: + return { + "priority": 0 if priority == "high" else 10, + "queue": f"{priority}_priority" + } diff --git a/app/worker/__init__.py b/app/worker/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/worker/main.py b/app/worker/main.py new file mode 100644 index 0000000..d3e50b8 --- /dev/null +++ b/app/worker/main.py @@ -0,0 +1,147 @@ +import json + +import traceback, datetime +from celery.signals import task_failure +from loguru import logger +from sqlalchemy import exc +from auto_archiver.core.orchestrator import ArchivingOrchestrator + +from app.shared.db import models +from app.shared.db.database import get_db +from app.shared import business_logic, schemas +from app.shared.task_messaging import get_celery, get_redis +from app.shared.settings import get_settings +from app.shared.log import log_error +from app.shared.aa_utils import get_all_urls +from app.shared.db import worker_crud + +settings = get_settings() + +celery = get_celery("worker") +Redis = get_redis() + +USER_GROUPS_FILENAME = settings.USER_GROUPS_FILENAME + +# TODO: these are temporary PATCHES for new aa's functionality +# logger.add("app/worker/worker_log.log", level="DEBUG") +logger.remove = lambda x: print(f"logger.remove({x})") + +# TODO: after release, as it requires updating past entries with sheet_id where tag is used, drop tags +@celery.task(name="create_archive_task", bind=True, autoretry_for=(Exception,), retry_backoff=True, retry_kwargs={'max_retries': 1}) +def create_archive_task(self, archive_json: str): + archive = schemas.ArchiveCreate.model_validate_json(archive_json) + + # call auto-archiver + args = get_orchestrator_args(archive.group_id, False, [archive.url]) + try: + orchestrator = ArchivingOrchestrator() + orchestrator.setup(args) + result = next(orchestrator.feed()) + except SystemExit as e: + log_error(e, f"create_archive_task: SystemExit from AA") + except Exception as e: + log_error(e, f"create_archive_task") + raise e + assert result, f"UNABLE TO archive: {archive.url}" + + # prepare and insert in DB + archive.store_until = get_store_until(archive.group_id) + archive.id = self.request.id + archive.urls = get_all_urls(result) + archive.result = json.loads(result.to_json()) + insert_result_into_db(archive) + + return archive.result + + +@celery.task(name="create_sheet_task", bind=True) +def create_sheet_task(self, sheet_json: str): + sheet = schemas.SubmitSheet.model_validate_json(sheet_json) + queue_name = (create_sheet_task.request.delivery_info or {}).get('routing_key', 'unknown') + logger.info(f"[queue={queue_name}] SHEET START {sheet=}") + + args = get_orchestrator_args(sheet.group_id, True, ["--gsheet_feeder.sheet_id", sheet.sheet_id]) + orchestrator = ArchivingOrchestrator() + orchestrator.setup(args) + + stats = {"archived": 0, "failed": 0, "errors": []} + try: + for result in orchestrator.feed(): + try: + assert result, f"ERROR archiving URL for sheet {sheet.sheet_id}" + archive = schemas.ArchiveCreate( + author_id=sheet.author_id, + url=result.get_url(), + group_id=sheet.group_id, + tags=sheet.tags, + id=models.generate_uuid(), + result=json.loads(result.to_json()), + sheet_id=sheet.sheet_id, + urls=get_all_urls(result), + store_until=get_store_until(sheet.group_id) + ) + insert_result_into_db(archive) + stats["archived"] += 1 + except exc.IntegrityError as e: + logger.warning(f"cached result detected: {e}") + except Exception as e: + log_error(e, extra=f"{self.name}: {sheet_json}") + redis_publish_exception(e, self.name, traceback.format_exc()) + stats["failed"] += 1 + stats["errors"].append(str(e)) + + except SystemExit as e: + log_error(e, f"create_sheet_task: SystemExit from AA") + + if stats["archived"] > 0: + with get_db() as session: + worker_crud.update_sheet_last_url_archived_at(session, sheet.sheet_id) + + logger.info(f"SHEET DONE {sheet=}") + # TODO: is this used anywhere? maybe drop it + return schemas.CelerySheetTask(success=True, sheet_id=sheet.sheet_id, time=datetime.datetime.now().isoformat(), stats=stats).model_dump() + + +def get_orchestrator_args(group_id: str, orchestrator_for_sheet: bool, cli_args: list = []) -> list: + aa_configs = [] + with get_db() as session: + group = worker_crud.get_group(session, group_id) + if orchestrator_for_sheet: + orchestrator_fn = group.orchestrator_sheet + else: + orchestrator_fn = worker_crud.get_group(session, group_id).orchestrator + assert orchestrator_fn, f"no orchestrator found for {group_id}" + aa_configs.extend(["--config", orchestrator_fn]) + aa_configs.extend(cli_args) + return aa_configs + + +def insert_result_into_db(archive: schemas.ArchiveCreate) -> str: + with get_db() as session: + db_archive = worker_crud.store_archived_url(session, archive) + logger.debug(f"[ARCHIVE STORED] {db_archive.author_id} {db_archive.url}") + return db_archive.id + + +def get_store_until(group_id: str) -> datetime.datetime: + with get_db() as session: + return business_logic.get_store_archive_until(session, group_id) + + +def redis_publish_exception(exception, task_name, traceback: str = ""): + REDIS_EXCEPTIONS_CHANNEL = settings.REDIS_EXCEPTIONS_CHANNEL + try: + exception_data = {"task": task_name, "type": exception.__class__.__name__, "exception": exception, "traceback": traceback} + Redis.publish(REDIS_EXCEPTIONS_CHANNEL, json.dumps(exception_data, default=str)) + except Exception as e: + log_error(e, f"[CRITICAL] Could not publish to {REDIS_EXCEPTIONS_CHANNEL}") + + +@task_failure.connect(sender=create_sheet_task) +@task_failure.connect(sender=create_archive_task) +def task_failure_notifier(sender, **kwargs): + # automatically capture exceptions in the worker tasks + logger.warning(f"⚠️ worker task failed: {sender.name}") + traceback_msg = "\n".join(traceback.format_list(traceback.extract_tb(kwargs['traceback']))) + log_error(kwargs['exception'], traceback_msg, f"task_failure: {sender.name}") + redis_publish_exception(kwargs['exception'], sender.name, traceback_msg) diff --git a/database/.gitkeep b/database/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 3782477..a4b42f1 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,19 +1,30 @@ services: web: + command: uvicorn app.web:app --factory --host 0.0.0.0 --reload restart: "no" - env_file: src/.env.dev + env_file: .env.dev + volumes: + - ./app/web:/aa-api/app/web # for --reload to work + - ./app/shared:/aa-api/app/shared # for --reload to work environment: - - SERVE_LOCAL_ARCHIVE=/app/local_archive # See orchestration.yaml local_storage.save_to - - ALLOWED_ORIGINS=http://localhost:8004,chrome-extension://ojcimmjndnlmmlgnjaeojoebaceokpdp - - USER_GROUPS_FILENAME=user-groups.dev.yaml - - DATABASE_PATH=sqlite:////app/auto-archiver.db + - ENVIRONMENT_FILE=.env.dev + - SERVE_LOCAL_ARCHIVE=/aa-api/app/local_archive # See orchestration.yaml local_storage.save_to + - ALLOWED_ORIGINS=["http://localhost:8000","http://localhost:8004","http://localhost:8081","chrome-extension://ojcimmjndnlmmlgnjaeojoebaceokpdp"] + - USER_GROUPS_FILENAME=/aa-api/app/user-groups.dev.yaml + - DATABASE_PATH=sqlite:////aa-api/database/auto-archiver.db + worker: + # command: watchmedo auto-restart --patterns="*.py" --recursive --ignore-directories -- celery -- --app=app.worker.main.celery worker --loglevel=debug --logfile=/aa-api/logs/celery.log -Q high_priority,low_priority --concurrency=${CONCURRENCY} + command: celery --app=app.worker.main.celery worker --loglevel=debug --logfile=/aa-api/logs/celery.log -Q high_priority,low_priority --concurrency=${CONCURRENCY} restart: "no" - env_file: src/.env.dev + env_file: .env.dev + volumes: + - ./app/worker:/aa-api/app/worker # for watchmedo to work + - ./app/shared:/aa-api/app/shared # for watchmedo to work redis: restart: "no" - env_file: src/.env.dev + env_file: .env.dev ports: - 6379:6379 diff --git a/docker-compose.yml b/docker-compose.yml index 1ffd164..f9d7253 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,13 +1,3 @@ -# reusable YAML variables -x-broker-url: &broker-url "redis://:${REDIS_PASSWORD}@redis:6379/0" - -x-base-setup: &base-setup - build: ./src - restart: always - env_file: src/.env.prod - environment: - CELERY_BROKER_URL: *broker-url - CELERY_RESULT_BACKEND: *broker-url volumes: crawls: @@ -15,31 +5,45 @@ volumes: name: "auto-archiver-api" services: web: - <<: *base-setup + build: + context: . + dockerfile: web.Dockerfile + restart: always + env_file: .env.prod + environment: + ENVIRONMENT_FILE: .env.prod + REDIS_HOSTNAME: redis ports: - "127.0.0.1:8004:8000" - command: uvicorn web:app --factory --host 0.0.0.0 --reload + command: uvicorn app.web:app --factory --host 0.0.0.0 volumes: - - ./src:/app + - ./logs:/aa-api/logs + - ./database:/aa-api/database + - ./secrets:/aa-api/secrets depends_on: - redis healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + test: ["CMD", "python3", "-c", 'import sys, urllib.request; sys.exit(urllib.request.urlopen("http://localhost:8000/health").getcode() != 200)'] interval: 30s timeout: 10s retries: 3 worker: - <<: *base-setup - command: celery --app=worker.main.celery worker --loglevel=info --logfile=logs/celery.log + build: + context: . + dockerfile: worker.Dockerfile + restart: always + env_file: .env.prod + command: celery --app=app.worker.main.celery worker --loglevel=warning --logfile=/aa-api/logs/celery.log -Q high_priority,low_priority --concurrency=${CONCURRENCY} volumes: - - ./src:/app + - ./logs:/aa-api/logs + - ./database:/aa-api/database + - ./secrets:/aa-api/secrets - /var/run/docker.sock:/var/run/docker.sock - crawls:/crawls # BROWSERTRIX_HOME_HOST:BROWSERTRIX_HOME_CONTAINER, do not change /crawls environment: - # celery broker-url needs to be duplicated here, do not remove - CELERY_BROKER_URL: *broker-url - CELERY_RESULT_BACKEND: *broker-url + REDIS_HOSTNAME: redis + ENVIRONMENT_FILE: .env.prod WACZ_ENABLE_DOCKER: 1 # Enable calling docker from this container BROWSERTRIX_HOME_HOST: auto-archiver-api_crawls BROWSERTRIX_HOME_CONTAINER: /crawls @@ -47,7 +51,7 @@ services: - web - redis healthcheck: - test: ["CMD", "pipenv", "run", "celery", "-A", "worker.main.celery", "status"] + test: ["CMD-SHELL", "./poetry-venv/bin/poetry run celery -A app.worker.main.celery inspect ping || exit 1"] interval: 30s timeout: 10s retries: 3 @@ -55,10 +59,11 @@ services: redis: image: redis:6-alpine restart: always + env_file: .env.prod command: redis-server /conf/redis.conf --requirepass ${REDIS_PASSWORD} volumes: - - "./redis/data:/data" - - "./redis/config:/conf" + - ./redis/data:/data + - ./redis/config:/conf healthcheck: test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] interval: 30s diff --git a/logs/.gitkeep b/logs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..79f7907 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,3690 @@ +# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. + +[[package]] +name = "aiosmtplib" +version = "3.0.2" +description = "asyncio SMTP client" +optional = false +python-versions = ">=3.8" +groups = ["web"] +files = [ + {file = "aiosmtplib-3.0.2-py3-none-any.whl", hash = "sha256:8783059603a34834c7c90ca51103c3aa129d5922003b5ce98dbaa6d4440f10fc"}, + {file = "aiosmtplib-3.0.2.tar.gz", hash = "sha256:08fd840f9dbc23258025dca229e8a8f04d2ccf3ecb1319585615bfc7933f7f47"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.0.0)", "sphinx-autodoc-typehints (>=1.24.0)", "sphinx-copybutton (>=0.5.0)"] +uvloop = ["uvloop (>=0.18)"] + +[[package]] +name = "aiosqlite" +version = "0.21.0" +description = "asyncio bridge to the standard sqlite3 module" +optional = false +python-versions = ">=3.9" +groups = ["web"] +files = [ + {file = "aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0"}, + {file = "aiosqlite-0.21.0.tar.gz", hash = "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3"}, +] + +[package.dependencies] +typing_extensions = ">=4.0" + +[package.extras] +dev = ["attribution (==1.7.1)", "black (==24.3.0)", "build (>=1.2)", "coverage[toml] (==7.6.10)", "flake8 (==7.0.0)", "flake8-bugbear (==24.12.12)", "flit (==3.10.1)", "mypy (==1.14.1)", "ufmt (==2.5.1)", "usort (==1.0.8.post1)"] +docs = ["sphinx (==8.1.3)", "sphinx-mdinclude (==0.6.1)"] + +[[package]] +name = "alembic" +version = "1.14.1" +description = "A database migration tool for SQLAlchemy." +optional = false +python-versions = ">=3.8" +groups = ["web"] +files = [ + {file = "alembic-1.14.1-py3-none-any.whl", hash = "sha256:1acdd7a3a478e208b0503cd73614d5e4c6efafa4e73518bb60e4f2846a37b1c5"}, + {file = "alembic-1.14.1.tar.gz", hash = "sha256:496e888245a53adf1498fcab31713a469c65836f8de76e01399aa1c3e90dd213"}, +] + +[package.dependencies] +Mako = "*" +SQLAlchemy = ">=1.3.0" +typing-extensions = ">=4" + +[package.extras] +tz = ["backports.zoneinfo", "tzdata"] + +[[package]] +name = "amqp" +version = "5.3.1" +description = "Low-level AMQP client for Python (fork of amqplib)." +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2"}, + {file = "amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432"}, +] + +[package.dependencies] +vine = ">=5.0.0,<6.0.0" + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +groups = ["main", "web"] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "anyio" +version = "4.8.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.9" +groups = ["dev", "web"] +files = [ + {file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"}, + {file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} + +[package.extras] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] +trio = ["trio (>=0.26.1)"] + +[[package]] +name = "asn1crypto" +version = "1.5.1" +description = "Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67"}, + {file = "asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c"}, +] + +[[package]] +name = "attrs" +version = "25.1.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a"}, + {file = "attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e"}, +] + +[package.extras] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] + +[[package]] +name = "authlib" +version = "1.4.1" +description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "Authlib-1.4.1-py2.py3-none-any.whl", hash = "sha256:edc29c3f6a3e72cd9e9f45fff67fc663a2c364022eb0371c003f22d5405915c1"}, + {file = "authlib-1.4.1.tar.gz", hash = "sha256:30ead9ea4993cdbab821dc6e01e818362f92da290c04c7f6a1940f86507a790d"}, +] + +[package.dependencies] +cryptography = "*" + +[[package]] +name = "auto-archiver" +version = "0.13.4" +description = "Automatically archive links to videos, images, and social media content from Google Sheets (and more)." +optional = false +python-versions = "<3.13,>=3.10" +groups = ["main"] +files = [ + {file = "auto_archiver-0.13.4-py3-none-any.whl", hash = "sha256:490ee0dbc86e3481ee06cdbfbbaf397cbc9733b4aaac8cac233f29af5dc4ba53"}, + {file = "auto_archiver-0.13.4.tar.gz", hash = "sha256:dac206f643e8101bb1efdea2e6cbdfaca1e3ae50cfe3fa34b466b7518337d675"}, +] + +[package.dependencies] +beautifulsoup4 = ">=0.0.0" +boto3 = ">=1.28.0,<2.0.0" +bs4 = ">=0.0.0" +certvalidator = ">=0.0.0" +cryptography = ">=41.0.0,<42.0.0" +dataclasses-json = ">=0.0.0" +dateparser = ">=0.0.0" +ffmpeg-python = ">=0.0.0" +google-api-python-client = ">=0.0.0" +google-auth-httplib2 = ">=0.0.0" +google-auth-oauthlib = ">=0.0.0" +gspread = ">=0.0.0" +instaloader = ">=0.0.0" +jinja2 = ">=0.0.0" +jsonlines = ">=0.0.0" +loguru = ">=0.0.0" +numpy = "2.1.3" +oauth2client = ">=0.0.0" +pdqhash = ">=0.0.0" +pillow = ">=0.0.0" +pyOpenSSL = "24.2.1" +pysubs2 = ">=0.0.0" +python-slugify = ">=0.0.0" +python-twitter-v2 = ">=0.0.0" +requests = {version = ">=0.0.0", extras = ["socks"]} +retrying = ">=0.0.0" +rich-argparse = ">=1.6.0,<2.0.0" +ruamel-yaml = ">=0.18.10,<0.19.0" +selenium = ">=0.0.0" +telethon = ">=0.0.0" +tqdm = ">=0.0.0" +tsp-client = ">=0.0.0" +vk-url-scraper = ">=0.0.0" +warcio = ">=0.0.0" +yt-dlp = ">=2025.1.26,<2026.0.0" + +[[package]] +name = "beautifulsoup4" +version = "4.13.3" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.7.0" +groups = ["main"] +files = [ + {file = "beautifulsoup4-4.13.3-py3-none-any.whl", hash = "sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16"}, + {file = "beautifulsoup4-4.13.3.tar.gz", hash = "sha256:1bd32405dacc920b42b83ba01644747ed77456a65760e285fbc47633ceddaf8b"}, +] + +[package.dependencies] +soupsieve = ">1.2" +typing-extensions = ">=4.0.0" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +name = "billiard" +version = "4.2.1" +description = "Python multiprocessing fork with improvements and bugfixes" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "billiard-4.2.1-py3-none-any.whl", hash = "sha256:40b59a4ac8806ba2c2369ea98d876bc6108b051c227baffd928c644d15d8f3cb"}, + {file = "billiard-4.2.1.tar.gz", hash = "sha256:12b641b0c539073fc8d3f5b8b7be998956665c4233c7c1fcd66a7e677c4fb36f"}, +] + +[[package]] +name = "blinker" +version = "1.9.0" +description = "Fast, simple object-to-object and broadcast signaling" +optional = false +python-versions = ">=3.9" +groups = ["web"] +files = [ + {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, + {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, +] + +[[package]] +name = "boto3" +version = "1.36.21" +description = "The AWS SDK for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "boto3-1.36.21-py3-none-any.whl", hash = "sha256:f94faa7cf932d781f474d87f8b4c14a033af95ac1460136b40d75e7a30086ef0"}, + {file = "boto3-1.36.21.tar.gz", hash = "sha256:41eb2b73eb612d300e629e3328b83f1ffea0fc6633e75c241a72a76746c1db26"}, +] + +[package.dependencies] +botocore = ">=1.36.21,<1.37.0" +jmespath = ">=0.7.1,<2.0.0" +s3transfer = ">=0.11.0,<0.12.0" + +[package.extras] +crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] + +[[package]] +name = "botocore" +version = "1.36.21" +description = "Low-level, data-driven core of boto 3." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "botocore-1.36.21-py3-none-any.whl", hash = "sha256:24a7052e792639dc2726001bd474cd0aaa959c1e18ddd92c17f3adc6efa1b132"}, + {file = "botocore-1.36.21.tar.gz", hash = "sha256:da746240e2ad64fd4997f7f3664a0a8e303d18075fc1d473727cb6375080ea16"}, +] + +[package.dependencies] +jmespath = ">=0.7.1,<2.0.0" +python-dateutil = ">=2.1,<3.0.0" +urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} + +[package.extras] +crt = ["awscrt (==0.23.8)"] + +[[package]] +name = "brotli" +version = "1.1.0" +description = "Python bindings for the Brotli compression library" +optional = false +python-versions = "*" +groups = ["main"] +markers = "platform_python_implementation >= \"CPython\"" +files = [ + {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752"}, + {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec"}, + {file = "Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2"}, + {file = "Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128"}, + {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc"}, + {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b"}, + {file = "Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50"}, + {file = "Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839"}, + {file = "Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0"}, + {file = "Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951"}, + {file = "Brotli-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5"}, + {file = "Brotli-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8"}, + {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f"}, + {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648"}, + {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0"}, + {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089"}, + {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368"}, + {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c"}, + {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284"}, + {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7"}, + {file = "Brotli-1.1.0-cp313-cp313-win32.whl", hash = "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0"}, + {file = "Brotli-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b"}, + {file = "Brotli-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:aea440a510e14e818e67bfc4027880e2fb500c2ccb20ab21c7a7c8b5b4703d75"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:6974f52a02321b36847cd19d1b8e381bf39939c21efd6ee2fc13a28b0d99348c"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:a7e53012d2853a07a4a79c00643832161a910674a893d296c9f1259859a289d2"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:d7702622a8b40c49bffb46e1e3ba2e81268d5c04a34f460978c6b5517a34dd52"}, + {file = "Brotli-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460"}, + {file = "Brotli-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579"}, + {file = "Brotli-1.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:cb1dac1770878ade83f2ccdf7d25e494f05c9165f5246b46a621cc849341dc01"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:3ee8a80d67a4334482d9712b8e83ca6b1d9bc7e351931252ebef5d8f7335a547"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:5e55da2c8724191e5b557f8e18943b1b4839b8efc3ef60d65985bcf6f587dd38"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:d342778ef319e1026af243ed0a07c97acf3bad33b9f29e7ae6a1f68fd083e90c"}, + {file = "Brotli-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95"}, + {file = "Brotli-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68"}, + {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3"}, + {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a"}, + {file = "Brotli-1.1.0-cp38-cp38-win32.whl", hash = "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b"}, + {file = "Brotli-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0"}, + {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a"}, + {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb"}, + {file = "Brotli-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64"}, + {file = "Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467"}, + {file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"}, +] + +[[package]] +name = "bs4" +version = "0.0.2" +description = "Dummy package for Beautiful Soup (beautifulsoup4)" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "bs4-0.0.2-py2.py3-none-any.whl", hash = "sha256:abf8742c0805ef7f662dce4b51cca104cffe52b835238afc169142ab9b3fbccc"}, + {file = "bs4-0.0.2.tar.gz", hash = "sha256:a48685c58f50fe127722417bae83fe6badf500d54b55f7e39ffe43b798653925"}, +] + +[package.dependencies] +beautifulsoup4 = "*" + +[[package]] +name = "cachetools" +version = "5.5.1" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "cachetools-5.5.1-py3-none-any.whl", hash = "sha256:b76651fdc3b24ead3c648bbdeeb940c1b04d365b38b4af66788f9ec4a81d42bb"}, + {file = "cachetools-5.5.1.tar.gz", hash = "sha256:70f238fbba50383ef62e55c6aff6d9673175fe59f7c6782c7a0b9e38f4a9df95"}, +] + +[[package]] +name = "celery" +version = "5.4.0" +description = "Distributed Task Queue." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "celery-5.4.0-py3-none-any.whl", hash = "sha256:369631eb580cf8c51a82721ec538684994f8277637edde2dfc0dacd73ed97f64"}, + {file = "celery-5.4.0.tar.gz", hash = "sha256:504a19140e8d3029d5acad88330c541d4c3f64c789d85f94756762d8bca7e706"}, +] + +[package.dependencies] +billiard = ">=4.2.0,<5.0" +click = ">=8.1.2,<9.0" +click-didyoumean = ">=0.3.0" +click-plugins = ">=1.1.1" +click-repl = ">=0.2.0" +kombu = ">=5.3.4,<6.0" +python-dateutil = ">=2.8.2" +tzdata = ">=2022.7" +vine = ">=5.1.0,<6.0" + +[package.extras] +arangodb = ["pyArango (>=2.0.2)"] +auth = ["cryptography (==42.0.5)"] +azureblockblob = ["azure-storage-blob (>=12.15.0)"] +brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"] +cassandra = ["cassandra-driver (>=3.25.0,<4)"] +consul = ["python-consul2 (==0.1.5)"] +cosmosdbsql = ["pydocumentdb (==2.3.5)"] +couchbase = ["couchbase (>=3.0.0)"] +couchdb = ["pycouchdb (==1.14.2)"] +django = ["Django (>=2.2.28)"] +dynamodb = ["boto3 (>=1.26.143)"] +elasticsearch = ["elastic-transport (<=8.13.0)", "elasticsearch (<=8.13.0)"] +eventlet = ["eventlet (>=0.32.0)"] +gcs = ["google-cloud-storage (>=2.10.0)"] +gevent = ["gevent (>=1.5.0)"] +librabbitmq = ["librabbitmq (>=2.0.0)"] +memcache = ["pylibmc (==1.6.3)"] +mongodb = ["pymongo[srv] (>=4.0.2)"] +msgpack = ["msgpack (==1.0.8)"] +pymemcache = ["python-memcached (>=1.61)"] +pyro = ["pyro4 (==4.82)"] +pytest = ["pytest-celery[all] (>=1.0.0)"] +redis = ["redis (>=4.5.2,!=4.5.5,<6.0.0)"] +s3 = ["boto3 (>=1.26.143)"] +slmq = ["softlayer-messaging (>=1.0.3)"] +solar = ["ephem (==4.1.5)"] +sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] +sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.3.4)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] +tblib = ["tblib (>=1.3.0)", "tblib (>=1.5.0)"] +yaml = ["PyYAML (>=3.10)"] +zookeeper = ["kazoo (>=1.3.1)"] +zstd = ["zstandard (==0.22.0)"] + +[[package]] +name = "certifi" +version = "2025.1.31" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +groups = ["main", "dev", "web"] +files = [ + {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, + {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, +] + +[[package]] +name = "certvalidator" +version = "0.11.1" +description = "Validates X.509 certificates and paths" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "certvalidator-0.11.1-py2.py3-none-any.whl", hash = "sha256:77520b269f516d4fb0902998d5bd0eb3727fe153b659aa1cb828dcf12ea6b8de"}, + {file = "certvalidator-0.11.1.tar.gz", hash = "sha256:922d141c94393ab285ca34338e18dd4093e3ae330b1f278e96c837cb62cffaad"}, +] + +[package.dependencies] +asn1crypto = ">=0.18.1" +oscrypto = ">=0.16.1" + +[[package]] +name = "cffi" +version = "1.17.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["main", "web"] +files = [ + {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, + {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, + {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, +] + +[[package]] +name = "click" +version = "8.1.8" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +groups = ["main", "web"] +files = [ + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "click-didyoumean" +version = "0.3.1" +description = "Enables git-like *did-you-mean* feature in click" +optional = false +python-versions = ">=3.6.2" +groups = ["main"] +files = [ + {file = "click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c"}, + {file = "click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463"}, +] + +[package.dependencies] +click = ">=7" + +[[package]] +name = "click-plugins" +version = "1.1.1" +description = "An extension module for click to enable registering CLI commands via setuptools entry-points." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b"}, + {file = "click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8"}, +] + +[package.dependencies] +click = ">=4.0" + +[package.extras] +dev = ["coveralls", "pytest (>=3.6)", "pytest-cov", "wheel"] + +[[package]] +name = "click-repl" +version = "0.3.0" +description = "REPL plugin for Click" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9"}, + {file = "click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812"}, +] + +[package.dependencies] +click = ">=7.0" +prompt-toolkit = ">=3.0.36" + +[package.extras] +testing = ["pytest (>=7.2.1)", "pytest-cov (>=4.0.0)", "tox (>=4.4.3)"] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev", "web"] +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] +markers = {main = "sys_platform == \"win32\" or platform_system == \"Windows\"", dev = "sys_platform == \"win32\"", web = "platform_system == \"Windows\""} + +[[package]] +name = "coverage" +version = "7.6.12" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "coverage-7.6.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:704c8c8c6ce6569286ae9622e534b4f5b9759b6f2cd643f1c1a61f666d534fe8"}, + {file = "coverage-7.6.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ad7525bf0241e5502168ae9c643a2f6c219fa0a283001cee4cf23a9b7da75879"}, + {file = "coverage-7.6.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06097c7abfa611c91edb9e6920264e5be1d6ceb374efb4986f38b09eed4cb2fe"}, + {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:220fa6c0ad7d9caef57f2c8771918324563ef0d8272c94974717c3909664e674"}, + {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3688b99604a24492bcfe1c106278c45586eb819bf66a654d8a9a1433022fb2eb"}, + {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1a987778b9c71da2fc8948e6f2656da6ef68f59298b7e9786849634c35d2c3c"}, + {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cec6b9ce3bd2b7853d4a4563801292bfee40b030c05a3d29555fd2a8ee9bd68c"}, + {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ace9048de91293e467b44bce0f0381345078389814ff6e18dbac8fdbf896360e"}, + {file = "coverage-7.6.12-cp310-cp310-win32.whl", hash = "sha256:ea31689f05043d520113e0552f039603c4dd71fa4c287b64cb3606140c66f425"}, + {file = "coverage-7.6.12-cp310-cp310-win_amd64.whl", hash = "sha256:676f92141e3c5492d2a1596d52287d0d963df21bf5e55c8b03075a60e1ddf8aa"}, + {file = "coverage-7.6.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e18aafdfb3e9ec0d261c942d35bd7c28d031c5855dadb491d2723ba54f4c3015"}, + {file = "coverage-7.6.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66fe626fd7aa5982cdebad23e49e78ef7dbb3e3c2a5960a2b53632f1f703ea45"}, + {file = "coverage-7.6.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ef01d70198431719af0b1f5dcbefc557d44a190e749004042927b2a3fed0702"}, + {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e92ae5a289a4bc4c0aae710c0948d3c7892e20fd3588224ebe242039573bf0"}, + {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e695df2c58ce526eeab11a2e915448d3eb76f75dffe338ea613c1201b33bab2f"}, + {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d74c08e9aaef995f8c4ef6d202dbd219c318450fe2a76da624f2ebb9c8ec5d9f"}, + {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e995b3b76ccedc27fe4f477b349b7d64597e53a43fc2961db9d3fbace085d69d"}, + {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b1f097878d74fe51e1ddd1be62d8e3682748875b461232cf4b52ddc6e6db0bba"}, + {file = "coverage-7.6.12-cp311-cp311-win32.whl", hash = "sha256:1f7ffa05da41754e20512202c866d0ebfc440bba3b0ed15133070e20bf5aeb5f"}, + {file = "coverage-7.6.12-cp311-cp311-win_amd64.whl", hash = "sha256:e216c5c45f89ef8971373fd1c5d8d1164b81f7f5f06bbf23c37e7908d19e8558"}, + {file = "coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad"}, + {file = "coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3"}, + {file = "coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574"}, + {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985"}, + {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750"}, + {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea"}, + {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3"}, + {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a"}, + {file = "coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95"}, + {file = "coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288"}, + {file = "coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1"}, + {file = "coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd"}, + {file = "coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9"}, + {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e"}, + {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4"}, + {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6"}, + {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3"}, + {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc"}, + {file = "coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3"}, + {file = "coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef"}, + {file = "coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e"}, + {file = "coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703"}, + {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0"}, + {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924"}, + {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b"}, + {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d"}, + {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827"}, + {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9"}, + {file = "coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3"}, + {file = "coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f"}, + {file = "coverage-7.6.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e7575ab65ca8399c8c4f9a7d61bbd2d204c8b8e447aab9d355682205c9dd948d"}, + {file = "coverage-7.6.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8161d9fbc7e9fe2326de89cd0abb9f3599bccc1287db0aba285cb68d204ce929"}, + {file = "coverage-7.6.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a1e465f398c713f1b212400b4e79a09829cd42aebd360362cd89c5bdc44eb87"}, + {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f25d8b92a4e31ff1bd873654ec367ae811b3a943583e05432ea29264782dc32c"}, + {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a936309a65cc5ca80fa9f20a442ff9e2d06927ec9a4f54bcba9c14c066323f2"}, + {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aa6f302a3a0b5f240ee201297fff0bbfe2fa0d415a94aeb257d8b461032389bd"}, + {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f973643ef532d4f9be71dd88cf7588936685fdb576d93a79fe9f65bc337d9d73"}, + {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:78f5243bb6b1060aed6213d5107744c19f9571ec76d54c99cc15938eb69e0e86"}, + {file = "coverage-7.6.12-cp39-cp39-win32.whl", hash = "sha256:69e62c5034291c845fc4df7f8155e8544178b6c774f97a99e2734b05eb5bed31"}, + {file = "coverage-7.6.12-cp39-cp39-win_amd64.whl", hash = "sha256:b01a840ecc25dce235ae4c1b6a0daefb2a203dba0e6e980637ee9c2f6ee0df57"}, + {file = "coverage-7.6.12-pp39.pp310-none-any.whl", hash = "sha256:7e39e845c4d764208e7b8f6a21c541ade741e2c41afabdfa1caa28687a3c98cf"}, + {file = "coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953"}, + {file = "coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2"}, +] + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "cryptography" +version = "41.0.7" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf"}, + {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1"}, + {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157"}, + {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406"}, + {file = "cryptography-41.0.7-cp37-abi3-win32.whl", hash = "sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d"}, + {file = "cryptography-41.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309"}, + {file = "cryptography-41.0.7.tar.gz", hash = "sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc"}, +] + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] +nox = ["nox"] +pep8test = ["black", "check-sdist", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "dataclasses-json" +version = "0.6.7" +description = "Easily serialize dataclasses to and from JSON." +optional = false +python-versions = "<4.0,>=3.7" +groups = ["main"] +files = [ + {file = "dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a"}, + {file = "dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0"}, +] + +[package.dependencies] +marshmallow = ">=3.18.0,<4.0.0" +typing-inspect = ">=0.4.0,<1" + +[[package]] +name = "dateparser" +version = "1.2.1" +description = "Date parsing library designed to parse dates from HTML pages" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "dateparser-1.2.1-py3-none-any.whl", hash = "sha256:bdcac262a467e6260030040748ad7c10d6bacd4f3b9cdb4cfd2251939174508c"}, + {file = "dateparser-1.2.1.tar.gz", hash = "sha256:7e4919aeb48481dbfc01ac9683c8e20bfe95bb715a38c1e9f6af889f4f30ccc3"}, +] + +[package.dependencies] +python-dateutil = ">=2.7.0" +pytz = ">=2024.2" +regex = ">=2015.06.24,<2019.02.19 || >2019.02.19,<2021.8.27 || >2021.8.27" +tzlocal = ">=0.2" + +[package.extras] +calendars = ["convertdate (>=2.2.1)", "hijridate"] +fasttext = ["fasttext (>=0.9.1)", "numpy (>=1.19.3,<2)"] +langdetect = ["langdetect (>=1.0.0)"] + +[[package]] +name = "dnspython" +version = "2.7.0" +description = "DNS toolkit" +optional = false +python-versions = ">=3.9" +groups = ["web"] +files = [ + {file = "dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86"}, + {file = "dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1"}, +] + +[package.extras] +dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "hypercorn (>=0.16.0)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "quart-trio (>=0.11.0)", "sphinx (>=7.2.0)", "sphinx-rtd-theme (>=2.0.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] +dnssec = ["cryptography (>=43)"] +doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] +doq = ["aioquic (>=1.0.0)"] +idna = ["idna (>=3.7)"] +trio = ["trio (>=0.23)"] +wmi = ["wmi (>=1.5.1)"] + +[[package]] +name = "email-validator" +version = "2.2.0" +description = "A robust email address syntax and deliverability validation library." +optional = false +python-versions = ">=3.8" +groups = ["web"] +files = [ + {file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"}, + {file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"}, +] + +[package.dependencies] +dnspython = ">=2.0.0" +idna = ">=2.0.0" + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev", "web"] +markers = "python_version < \"3.11\"" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "fastapi" +version = "0.115.8" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.8" +groups = ["web"] +files = [ + {file = "fastapi-0.115.8-py3-none-any.whl", hash = "sha256:753a96dd7e036b34eeef8babdfcfe3f28ff79648f86551eb36bfc1b0bf4a8cbf"}, + {file = "fastapi-0.115.8.tar.gz", hash = "sha256:0ce9111231720190473e222cdf0f07f7206ad7e53ea02beb1d2dc36e2f0741e9"}, +] + +[package.dependencies] +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.40.0,<0.46.0" +typing-extensions = ">=4.8.0" + +[package.extras] +all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] + +[[package]] +name = "fastapi-mail" +version = "1.4.2" +description = "Simple lightweight mail library for FastApi" +optional = false +python-versions = "<4.0,>=3.8.1" +groups = ["web"] +files = [ + {file = "fastapi_mail-1.4.2-py3-none-any.whl", hash = "sha256:3525cf342ff91f6bcb3298570d1783498082e586957f668ee4164a0aab6ec743"}, + {file = "fastapi_mail-1.4.2.tar.gz", hash = "sha256:04bde1005c624f42dfc0a9c1e313fcc544499fdd6b3531e606c500d80ac2ffcb"}, +] + +[package.dependencies] +aiosmtplib = ">=3.0.2,<4.0.0" +blinker = ">=1.5,<2.0" +email-validator = ">=2.2.0,<3.0.0" +Jinja2 = ">=3.0,<4.0" +pydantic = ">=2.10.1,<3.0.0" +pydantic-settings = ">=2.6.1,<3.0.0" +starlette = ">=0.24,<1.0" + +[package.extras] +httpx = ["httpx[httpx] (>=0.23,<0.24)"] +redis = ["redis[redis] (>=4.3,<5.0)"] + +[[package]] +name = "fastapi-utils" +version = "0.8.0" +description = "Reusable utilities for FastAPI" +optional = false +python-versions = "<4.0,>=3.8" +groups = ["web"] +files = [ + {file = "fastapi_utils-0.8.0-py3-none-any.whl", hash = "sha256:6c4d507a76bab9a016cee0c4fa3a4638c636b2b2689e39c62254b1b2e4e81825"}, + {file = "fastapi_utils-0.8.0.tar.gz", hash = "sha256:eca834e80c09f85df30004fe5e861981262b296f60c93d5a1a1416fe4c784140"}, +] + +[package.dependencies] +fastapi = ">=0.89,<1.0" +psutil = ">=5,<6" +pydantic = ">1.0,<3.0" + +[package.extras] +all = ["pydantic-settings (>=2.0.1,<3.0.0)", "sqlalchemy (>=1.4,<3.0)", "typing-inspect (>=0.9.0,<0.10.0)"] +session = ["sqlalchemy (>=1.4,<3.0)"] + +[[package]] +name = "ffmpeg-python" +version = "0.2.0" +description = "Python bindings for FFmpeg - with complex filtering support" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "ffmpeg-python-0.2.0.tar.gz", hash = "sha256:65225db34627c578ef0e11c8b1eb528bb35e024752f6f10b78c011f6f64c4127"}, + {file = "ffmpeg_python-0.2.0-py3-none-any.whl", hash = "sha256:ac441a0404e053f8b6a1113a77c0f452f1cfc62f6344a769475ffdc0f56c23c5"}, +] + +[package.dependencies] +future = "*" + +[package.extras] +dev = ["Sphinx (==2.1.0)", "future (==0.17.1)", "numpy (==1.16.4)", "pytest (==4.6.1)", "pytest-mock (==1.10.4)", "tox (==3.12.1)"] + +[[package]] +name = "future" +version = "1.0.0" +description = "Clean single-source support for Python 3 and 2" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["main"] +files = [ + {file = "future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216"}, + {file = "future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05"}, +] + +[[package]] +name = "google-api-core" +version = "2.24.1" +description = "Google API client core library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "google_api_core-2.24.1-py3-none-any.whl", hash = "sha256:bc78d608f5a5bf853b80bd70a795f703294de656c096c0968320830a4bc280f1"}, + {file = "google_api_core-2.24.1.tar.gz", hash = "sha256:f8b36f5456ab0dd99a1b693a40a31d1e7757beea380ad1b38faaf8941eae9d8a"}, +] + +[package.dependencies] +google-auth = ">=2.14.1,<3.0.dev0" +googleapis-common-protos = ">=1.56.2,<2.0.dev0" +proto-plus = ">=1.22.3,<2.0.0dev" +protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" +requests = ">=2.18.0,<3.0.0.dev0" + +[package.extras] +async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.dev0)"] +grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0)"] +grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] +grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] + +[[package]] +name = "google-api-python-client" +version = "2.161.0" +description = "Google API Client Library for Python" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "google_api_python_client-2.161.0-py2.py3-none-any.whl", hash = "sha256:9476a5a4f200bae368140453df40f9cda36be53fa7d0e9a9aac4cdb859a26448"}, + {file = "google_api_python_client-2.161.0.tar.gz", hash = "sha256:324c0cce73e9ea0a0d2afd5937e01b7c2d6a4d7e2579cdb6c384f9699d6c9f37"}, +] + +[package.dependencies] +google-api-core = ">=1.31.5,<2.0.dev0 || >2.3.0,<3.0.0.dev0" +google-auth = ">=1.32.0,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0.dev0" +google-auth-httplib2 = ">=0.2.0,<1.0.0" +httplib2 = ">=0.19.0,<1.dev0" +uritemplate = ">=3.0.1,<5" + +[[package]] +name = "google-auth" +version = "2.38.0" +description = "Google Authentication Library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "google_auth-2.38.0-py2.py3-none-any.whl", hash = "sha256:e7dae6694313f434a2727bf2906f27ad259bae090d7aa896590d86feec3d9d4a"}, + {file = "google_auth-2.38.0.tar.gz", hash = "sha256:8285113607d3b80a3f1543b75962447ba8a09fe85783432a784fdeef6ac094c4"}, +] + +[package.dependencies] +cachetools = ">=2.0.0,<6.0" +pyasn1-modules = ">=0.2.1" +rsa = ">=3.1.4,<5" + +[package.extras] +aiohttp = ["aiohttp (>=3.6.2,<4.0.0.dev0)", "requests (>=2.20.0,<3.0.0.dev0)"] +enterprise-cert = ["cryptography", "pyopenssl"] +pyjwt = ["cryptography (>=38.0.3)", "pyjwt (>=2.0)"] +pyopenssl = ["cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] +reauth = ["pyu2f (>=0.1.5)"] +requests = ["requests (>=2.20.0,<3.0.0.dev0)"] + +[[package]] +name = "google-auth-httplib2" +version = "0.2.0" +description = "Google Authentication Library: httplib2 transport" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "google-auth-httplib2-0.2.0.tar.gz", hash = "sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05"}, + {file = "google_auth_httplib2-0.2.0-py2.py3-none-any.whl", hash = "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d"}, +] + +[package.dependencies] +google-auth = "*" +httplib2 = ">=0.19.0" + +[[package]] +name = "google-auth-oauthlib" +version = "1.2.1" +description = "Google Authentication Library" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "google_auth_oauthlib-1.2.1-py2.py3-none-any.whl", hash = "sha256:2d58a27262d55aa1b87678c3ba7142a080098cbc2024f903c62355deb235d91f"}, + {file = "google_auth_oauthlib-1.2.1.tar.gz", hash = "sha256:afd0cad092a2eaa53cd8e8298557d6de1034c6cb4a740500b5357b648af97263"}, +] + +[package.dependencies] +google-auth = ">=2.15.0" +requests-oauthlib = ">=0.7.0" + +[package.extras] +tool = ["click (>=6.0.0)"] + +[[package]] +name = "googleapis-common-protos" +version = "1.67.0" +description = "Common protobufs used in Google APIs" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "googleapis_common_protos-1.67.0-py2.py3-none-any.whl", hash = "sha256:579de760800d13616f51cf8be00c876f00a9f146d3e6510e19d1f4111758b741"}, + {file = "googleapis_common_protos-1.67.0.tar.gz", hash = "sha256:21398025365f138be356d5923e9168737d94d46a72aefee4a6110a1f23463c86"}, +] + +[package.dependencies] +protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" + +[package.extras] +grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] + +[[package]] +name = "greenlet" +version = "3.1.1" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.7" +groups = ["main", "web"] +markers = "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\"" +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 = "gspread" +version = "6.1.4" +description = "Google Spreadsheets Python API" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "gspread-6.1.4-py3-none-any.whl", hash = "sha256:c34781c426031a243ad154952b16f21ac56a5af90687885fbee3d1fba5280dcd"}, + {file = "gspread-6.1.4.tar.gz", hash = "sha256:b8eec27de7cadb338bb1b9f14a9be168372dee8965c0da32121816b5050ac1de"}, +] + +[package.dependencies] +google-auth = ">=1.12.0" +google-auth-oauthlib = ">=0.4.1" + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev", "web"] +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httpcore" +version = "1.0.7" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, + {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httplib2" +version = "0.22.0" +description = "A comprehensive HTTP client library." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +files = [ + {file = "httplib2-0.22.0-py3-none-any.whl", hash = "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc"}, + {file = "httplib2-0.22.0.tar.gz", hash = "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81"}, +] + +[package.dependencies] +pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0.2,<3.0.3 || >3.0.3,<4", markers = "python_version > \"3.0\""} + +[[package]] +name = "httpx" +version = "0.28.1" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +groups = ["main", "dev", "web"] +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "instaloader" +version = "4.14.1" +description = "Download pictures (or videos) along with their captions and other metadata from Instagram." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "instaloader-4.14.1-py3-none-any.whl", hash = "sha256:43356f696231621ea5a93354f9a4578124fe131940ee9aa1e83c20f57e18f26d"}, + {file = "instaloader-4.14.1.tar.gz", hash = "sha256:a41a7372a18fb096b3ed545469479884de9cf768e12020c0e0e67c488d9d599c"}, +] + +[package.dependencies] +requests = ">=2.25" + +[package.extras] +browser-cookie3 = ["browser_cookie3 (>=0.19.1)"] + +[[package]] +name = "jinja2" +version = "3.1.5" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["main", "web"] +files = [ + {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, + {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "jmespath" +version = "1.0.1" +description = "JSON Matching Expressions" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, + {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, +] + +[[package]] +name = "jsonlines" +version = "4.0.0" +description = "Library with helpers for the jsonlines file format" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "jsonlines-4.0.0-py3-none-any.whl", hash = "sha256:185b334ff2ca5a91362993f42e83588a360cf95ce4b71a73548502bda52a7c55"}, + {file = "jsonlines-4.0.0.tar.gz", hash = "sha256:0c6d2c09117550c089995247f605ae4cf77dd1533041d366351f6f298822ea74"}, +] + +[package.dependencies] +attrs = ">=19.2.0" + +[[package]] +name = "kombu" +version = "5.4.2" +description = "Messaging library for Python." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "kombu-5.4.2-py3-none-any.whl", hash = "sha256:14212f5ccf022fc0a70453bb025a1dcc32782a588c49ea866884047d66e14763"}, + {file = "kombu-5.4.2.tar.gz", hash = "sha256:eef572dd2fd9fc614b37580e3caeafdd5af46c1eff31e7fba89138cdb406f2cf"}, +] + +[package.dependencies] +amqp = ">=5.1.1,<6.0.0" +tzdata = {version = "*", markers = "python_version >= \"3.9\""} +vine = "5.1.0" + +[package.extras] +azureservicebus = ["azure-servicebus (>=7.10.0)"] +azurestoragequeues = ["azure-identity (>=1.12.0)", "azure-storage-queue (>=12.6.0)"] +confluentkafka = ["confluent-kafka (>=2.2.0)"] +consul = ["python-consul2 (==0.1.5)"] +librabbitmq = ["librabbitmq (>=2.0.0)"] +mongodb = ["pymongo (>=4.1.1)"] +msgpack = ["msgpack (==1.1.0)"] +pyro = ["pyro4 (==4.82)"] +qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"] +redis = ["redis (>=4.5.2,!=4.5.5,!=5.0.2)"] +slmq = ["softlayer-messaging (>=1.0.3)"] +sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] +sqs = ["boto3 (>=1.26.143)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] +yaml = ["PyYAML (>=3.10)"] +zookeeper = ["kazoo (>=2.8.0)"] + +[[package]] +name = "loguru" +version = "0.7.3" +description = "Python logging made (stupidly) simple" +optional = false +python-versions = "<4.0,>=3.5" +groups = ["main"] +files = [ + {file = "loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c"}, + {file = "loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["Sphinx (==8.1.3)", "build (==1.2.2)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.5.0)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.13.0)", "mypy (==v1.4.1)", "myst-parser (==4.0.0)", "pre-commit (==4.0.1)", "pytest (==6.1.2)", "pytest (==8.3.2)", "pytest-cov (==2.12.1)", "pytest-cov (==5.0.0)", "pytest-cov (==6.0.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.1.0)", "sphinx-rtd-theme (==3.0.2)", "tox (==3.27.1)", "tox (==4.23.2)", "twine (==6.0.1)"] + +[[package]] +name = "mako" +version = "1.3.9" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +optional = false +python-versions = ">=3.8" +groups = ["web"] +files = [ + {file = "Mako-1.3.9-py3-none-any.whl", hash = "sha256:95920acccb578427a9aa38e37a186b1e43156c87260d7ba18ca63aa4c7cbd3a1"}, + {file = "mako-1.3.9.tar.gz", hash = "sha256:b5d65ff3462870feec922dbccf38f6efb44e5714d7b593a656be86663d8600ac"}, +] + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +babel = ["Babel"] +lingua = ["lingua"] +testing = ["pytest"] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["main", "web"] +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "marshmallow" +version = "3.26.1" +description = "A lightweight library for converting complex datatypes to and from native Python datatypes." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c"}, + {file = "marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6"}, +] + +[package.dependencies] +packaging = ">=17.0" + +[package.extras] +dev = ["marshmallow[tests]", "pre-commit (>=3.5,<5.0)", "tox"] +docs = ["autodocsumm (==0.2.14)", "furo (==2024.8.6)", "sphinx (==8.1.3)", "sphinx-copybutton (==0.5.2)", "sphinx-issues (==5.0.0)", "sphinxext-opengraph (==0.9.1)"] +tests = ["pytest", "simplejson"] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "mutagen" +version = "1.47.0" +description = "read and write audio tags for many formats" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "mutagen-1.47.0-py3-none-any.whl", hash = "sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719"}, + {file = "mutagen-1.47.0.tar.gz", hash = "sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99"}, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +groups = ["main"] +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "numpy" +version = "2.1.3" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "numpy-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c894b4305373b9c5576d7a12b473702afdf48ce5369c074ba304cc5ad8730dff"}, + {file = "numpy-2.1.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b47fbb433d3260adcd51eb54f92a2ffbc90a4595f8970ee00e064c644ac788f5"}, + {file = "numpy-2.1.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:825656d0743699c529c5943554d223c021ff0494ff1442152ce887ef4f7561a1"}, + {file = "numpy-2.1.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:6a4825252fcc430a182ac4dee5a505053d262c807f8a924603d411f6718b88fd"}, + {file = "numpy-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e711e02f49e176a01d0349d82cb5f05ba4db7d5e7e0defd026328e5cfb3226d3"}, + {file = "numpy-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78574ac2d1a4a02421f25da9559850d59457bac82f2b8d7a44fe83a64f770098"}, + {file = "numpy-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c7662f0e3673fe4e832fe07b65c50342ea27d989f92c80355658c7f888fcc83c"}, + {file = "numpy-2.1.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fa2d1337dc61c8dc417fbccf20f6d1e139896a30721b7f1e832b2bb6ef4eb6c4"}, + {file = "numpy-2.1.3-cp310-cp310-win32.whl", hash = "sha256:72dcc4a35a8515d83e76b58fdf8113a5c969ccd505c8a946759b24e3182d1f23"}, + {file = "numpy-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:ecc76a9ba2911d8d37ac01de72834d8849e55473457558e12995f4cd53e778e0"}, + {file = "numpy-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4d1167c53b93f1f5d8a139a742b3c6f4d429b54e74e6b57d0eff40045187b15d"}, + {file = "numpy-2.1.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c80e4a09b3d95b4e1cac08643f1152fa71a0a821a2d4277334c88d54b2219a41"}, + {file = "numpy-2.1.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:576a1c1d25e9e02ed7fa5477f30a127fe56debd53b8d2c89d5578f9857d03ca9"}, + {file = "numpy-2.1.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:973faafebaae4c0aaa1a1ca1ce02434554d67e628b8d805e61f874b84e136b09"}, + {file = "numpy-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:762479be47a4863e261a840e8e01608d124ee1361e48b96916f38b119cfda04a"}, + {file = "numpy-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc6f24b3d1ecc1eebfbf5d6051faa49af40b03be1aaa781ebdadcbc090b4539b"}, + {file = "numpy-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:17ee83a1f4fef3c94d16dc1802b998668b5419362c8a4f4e8a491de1b41cc3ee"}, + {file = "numpy-2.1.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15cb89f39fa6d0bdfb600ea24b250e5f1a3df23f901f51c8debaa6a5d122b2f0"}, + {file = "numpy-2.1.3-cp311-cp311-win32.whl", hash = "sha256:d9beb777a78c331580705326d2367488d5bc473b49a9bc3036c154832520aca9"}, + {file = "numpy-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:d89dd2b6da69c4fff5e39c28a382199ddedc3a5be5390115608345dec660b9e2"}, + {file = "numpy-2.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f55ba01150f52b1027829b50d70ef1dafd9821ea82905b63936668403c3b471e"}, + {file = "numpy-2.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13138eadd4f4da03074851a698ffa7e405f41a0845a6b1ad135b81596e4e9958"}, + {file = "numpy-2.1.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a6b46587b14b888e95e4a24d7b13ae91fa22386c199ee7b418f449032b2fa3b8"}, + {file = "numpy-2.1.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:0fa14563cc46422e99daef53d725d0c326e99e468a9320a240affffe87852564"}, + {file = "numpy-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8637dcd2caa676e475503d1f8fdb327bc495554e10838019651b76d17b98e512"}, + {file = "numpy-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2312b2aa89e1f43ecea6da6ea9a810d06aae08321609d8dc0d0eda6d946a541b"}, + {file = "numpy-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a38c19106902bb19351b83802531fea19dee18e5b37b36454f27f11ff956f7fc"}, + {file = "numpy-2.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02135ade8b8a84011cbb67dc44e07c58f28575cf9ecf8ab304e51c05528c19f0"}, + {file = "numpy-2.1.3-cp312-cp312-win32.whl", hash = "sha256:e6988e90fcf617da2b5c78902fe8e668361b43b4fe26dbf2d7b0f8034d4cafb9"}, + {file = "numpy-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:0d30c543f02e84e92c4b1f415b7c6b5326cbe45ee7882b6b77db7195fb971e3a"}, + {file = "numpy-2.1.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96fe52fcdb9345b7cd82ecd34547fca4321f7656d500eca497eb7ea5a926692f"}, + {file = "numpy-2.1.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f653490b33e9c3a4c1c01d41bc2aef08f9475af51146e4a7710c450cf9761598"}, + {file = "numpy-2.1.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dc258a761a16daa791081d026f0ed4399b582712e6fc887a95af09df10c5ca57"}, + {file = "numpy-2.1.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:016d0f6f5e77b0f0d45d77387ffa4bb89816b57c835580c3ce8e099ef830befe"}, + {file = "numpy-2.1.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c181ba05ce8299c7aa3125c27b9c2167bca4a4445b7ce73d5febc411ca692e43"}, + {file = "numpy-2.1.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5641516794ca9e5f8a4d17bb45446998c6554704d888f86df9b200e66bdcce56"}, + {file = "numpy-2.1.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ea4dedd6e394a9c180b33c2c872b92f7ce0f8e7ad93e9585312b0c5a04777a4a"}, + {file = "numpy-2.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0df3635b9c8ef48bd3be5f862cf71b0a4716fa0e702155c45067c6b711ddcef"}, + {file = "numpy-2.1.3-cp313-cp313-win32.whl", hash = "sha256:50ca6aba6e163363f132b5c101ba078b8cbd3fa92c7865fd7d4d62d9779ac29f"}, + {file = "numpy-2.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:747641635d3d44bcb380d950679462fae44f54b131be347d5ec2bce47d3df9ed"}, + {file = "numpy-2.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:996bb9399059c5b82f76b53ff8bb686069c05acc94656bb259b1d63d04a9506f"}, + {file = "numpy-2.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:45966d859916ad02b779706bb43b954281db43e185015df6eb3323120188f9e4"}, + {file = "numpy-2.1.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:baed7e8d7481bfe0874b566850cb0b85243e982388b7b23348c6db2ee2b2ae8e"}, + {file = "numpy-2.1.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f7f672a3388133335589cfca93ed468509cb7b93ba3105fce780d04a6576a0"}, + {file = "numpy-2.1.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7aac50327da5d208db2eec22eb11e491e3fe13d22653dce51b0f4109101b408"}, + {file = "numpy-2.1.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4394bc0dbd074b7f9b52024832d16e019decebf86caf909d94f6b3f77a8ee3b6"}, + {file = "numpy-2.1.3-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:50d18c4358a0a8a53f12a8ba9d772ab2d460321e6a93d6064fc22443d189853f"}, + {file = "numpy-2.1.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:14e253bd43fc6b37af4921b10f6add6925878a42a0c5fe83daee390bca80bc17"}, + {file = "numpy-2.1.3-cp313-cp313t-win32.whl", hash = "sha256:08788d27a5fd867a663f6fc753fd7c3ad7e92747efc73c53bca2f19f8bc06f48"}, + {file = "numpy-2.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2564fbdf2b99b3f815f2107c1bbc93e2de8ee655a69c261363a1172a79a257d4"}, + {file = "numpy-2.1.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4f2015dfe437dfebbfce7c85c7b53d81ba49e71ba7eadbf1df40c915af75979f"}, + {file = "numpy-2.1.3-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:3522b0dfe983a575e6a9ab3a4a4dfe156c3e428468ff08ce582b9bb6bd1d71d4"}, + {file = "numpy-2.1.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c006b607a865b07cd981ccb218a04fc86b600411d83d6fc261357f1c0966755d"}, + {file = "numpy-2.1.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e14e26956e6f1696070788252dcdff11b4aca4c3e8bd166e0df1bb8f315a67cb"}, + {file = "numpy-2.1.3.tar.gz", hash = "sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761"}, +] + +[[package]] +name = "oauth2client" +version = "4.1.3" +description = "OAuth 2.0 client library" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "oauth2client-4.1.3-py2.py3-none-any.whl", hash = "sha256:b8a81cc5d60e2d364f0b1b98f958dbd472887acaf1a5b05e21c28c31a2d6d3ac"}, + {file = "oauth2client-4.1.3.tar.gz", hash = "sha256:d486741e451287f69568a4d26d70d9acd73a2bbfa275746c535b4209891cccc6"}, +] + +[package.dependencies] +httplib2 = ">=0.9.1" +pyasn1 = ">=0.1.7" +pyasn1-modules = ">=0.0.5" +rsa = ">=3.1.4" +six = ">=1.6.1" + +[[package]] +name = "oauthlib" +version = "3.2.2" +description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, + {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, +] + +[package.extras] +rsa = ["cryptography (>=3.0.0)"] +signals = ["blinker (>=1.4.0)"] +signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] + +[[package]] +name = "oscrypto" +version = "1.3.0" +description = "TLS (SSL) sockets, key generation, encryption, decryption, signing, verification and KDFs using the OS crypto libraries. Does not require a compiler, and relies on the OS for patching. Works on Windows, OS X and Linux/BSD." +optional = false +python-versions = "*" +groups = ["main"] +files = [] +develop = false + +[package.dependencies] +asn1crypto = ">=1.5.1" + +[package.source] +type = "git" +url = "https://github.com/wbond/oscrypto.git" +reference = "d5f3437ed24257895ae1edd9e503cfb352e635a8" +resolved_reference = "d5f3437ed24257895ae1edd9e503cfb352e635a8" + +[[package]] +name = "outcome" +version = "1.3.0.post0" +description = "Capture the outcome of Python function calls." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"}, + {file = "outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8"}, +] + +[package.dependencies] +attrs = ">=19.2.0" + +[[package]] +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "pdqhash" +version = "0.2.7" +description = "\"Python bindings for Facebook's PDQ hash\"" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "pdqhash-0.2.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7efd5b4e1ded44ec2a32ea2d32c29fef37d1adca03ce0867975526aea0ffe7fe"}, + {file = "pdqhash-0.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:33bc5d22c458e5245c2649c0e54968d13e7b5940f6ace268f4a05016481b2253"}, + {file = "pdqhash-0.2.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:91652e70d017c8fd60003ea0bfcf4eefeceb3896b93deb2dec43a2ae896da6cd"}, + {file = "pdqhash-0.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:68549e2071499a5a19748505eb00f52384f24830fe14a437f982d046a8edb498"}, + {file = "pdqhash-0.2.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6eab8a3112853f18adfaf1482b48bff8003d822de927b7397a421c5e7f0f76d7"}, + {file = "pdqhash-0.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:262ec2881b80877a4005f408000f27e492d08b0d4e84269e1cfcc8a31e96c3bf"}, + {file = "pdqhash-0.2.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a7d943874df8b2ca8c97755f60d1b6ae66e654fe8b2bb6ac8e8be216cae7d130"}, + {file = "pdqhash-0.2.7-cp313-cp313-win_amd64.whl", hash = "sha256:d63886b1edea4134eaa9862987393391e8958f35569918b91804b528f23c5e6c"}, + {file = "pdqhash-0.2.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:fc6c53cdc395f5421c857e4e30a92862cc918e18f91d7e4452bb5eb746e454f2"}, + {file = "pdqhash-0.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:9648abdfdbccb5edfc55fa2a61183766e51d140080fe08213f5daa885c3d5c66"}, + {file = "pdqhash-0.2.7.tar.gz", hash = "sha256:df6375fef513089191cadcbf07c90715d40e882867141e04360a39d8a0861cb5"}, +] + +[[package]] +name = "pillow" +version = "11.1.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pillow-11.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8"}, + {file = "pillow-11.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192"}, + {file = "pillow-11.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a07dba04c5e22824816b2615ad7a7484432d7f540e6fa86af60d2de57b0fcee2"}, + {file = "pillow-11.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e267b0ed063341f3e60acd25c05200df4193e15a4a5807075cd71225a2386e26"}, + {file = "pillow-11.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd165131fd51697e22421d0e467997ad31621b74bfc0b75956608cb2906dda07"}, + {file = "pillow-11.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:abc56501c3fd148d60659aae0af6ddc149660469082859fa7b066a298bde9482"}, + {file = "pillow-11.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:54ce1c9a16a9561b6d6d8cb30089ab1e5eb66918cb47d457bd996ef34182922e"}, + {file = "pillow-11.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:73ddde795ee9b06257dac5ad42fcb07f3b9b813f8c1f7f870f402f4dc54b5269"}, + {file = "pillow-11.1.0-cp310-cp310-win32.whl", hash = "sha256:3a5fe20a7b66e8135d7fd617b13272626a28278d0e578c98720d9ba4b2439d49"}, + {file = "pillow-11.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:b6123aa4a59d75f06e9dd3dac5bf8bc9aa383121bb3dd9a7a612e05eabc9961a"}, + {file = "pillow-11.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:a76da0a31da6fcae4210aa94fd779c65c75786bc9af06289cd1c184451ef7a65"}, + {file = "pillow-11.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457"}, + {file = "pillow-11.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35"}, + {file = "pillow-11.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2"}, + {file = "pillow-11.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070"}, + {file = "pillow-11.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6"}, + {file = "pillow-11.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1"}, + {file = "pillow-11.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2"}, + {file = "pillow-11.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96"}, + {file = "pillow-11.1.0-cp311-cp311-win32.whl", hash = "sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f"}, + {file = "pillow-11.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761"}, + {file = "pillow-11.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71"}, + {file = "pillow-11.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a"}, + {file = "pillow-11.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b"}, + {file = "pillow-11.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3"}, + {file = "pillow-11.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a"}, + {file = "pillow-11.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1"}, + {file = "pillow-11.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f"}, + {file = "pillow-11.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91"}, + {file = "pillow-11.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c"}, + {file = "pillow-11.1.0-cp312-cp312-win32.whl", hash = "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6"}, + {file = "pillow-11.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf"}, + {file = "pillow-11.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5"}, + {file = "pillow-11.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc"}, + {file = "pillow-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0"}, + {file = "pillow-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1"}, + {file = "pillow-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec"}, + {file = "pillow-11.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5"}, + {file = "pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114"}, + {file = "pillow-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352"}, + {file = "pillow-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3"}, + {file = "pillow-11.1.0-cp313-cp313-win32.whl", hash = "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9"}, + {file = "pillow-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c"}, + {file = "pillow-11.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65"}, + {file = "pillow-11.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861"}, + {file = "pillow-11.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081"}, + {file = "pillow-11.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c"}, + {file = "pillow-11.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547"}, + {file = "pillow-11.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab"}, + {file = "pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9"}, + {file = "pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe"}, + {file = "pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756"}, + {file = "pillow-11.1.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:bf902d7413c82a1bfa08b06a070876132a5ae6b2388e2712aab3a7cbc02205c6"}, + {file = "pillow-11.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c1eec9d950b6fe688edee07138993e54ee4ae634c51443cfb7c1e7613322718e"}, + {file = "pillow-11.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e275ee4cb11c262bd108ab2081f750db2a1c0b8c12c1897f27b160c8bd57bbc"}, + {file = "pillow-11.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4db853948ce4e718f2fc775b75c37ba2efb6aaea41a1a5fc57f0af59eee774b2"}, + {file = "pillow-11.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:ab8a209b8485d3db694fa97a896d96dd6533d63c22829043fd9de627060beade"}, + {file = "pillow-11.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:54251ef02a2309b5eec99d151ebf5c9904b77976c8abdcbce7891ed22df53884"}, + {file = "pillow-11.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5bb94705aea800051a743aa4874bb1397d4695fb0583ba5e425ee0328757f196"}, + {file = "pillow-11.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89dbdb3e6e9594d512780a5a1c42801879628b38e3efc7038094430844e271d8"}, + {file = "pillow-11.1.0-cp39-cp39-win32.whl", hash = "sha256:e5449ca63da169a2e6068dd0e2fcc8d91f9558aba89ff6d02121ca8ab11e79e5"}, + {file = "pillow-11.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:3362c6ca227e65c54bf71a5f88b3d4565ff1bcbc63ae72c34b07bbb1cc59a43f"}, + {file = "pillow-11.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:b20be51b37a75cc54c2c55def3fa2c65bb94ba859dde241cd0a4fd302de5ae0a"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8c730dc3a83e5ac137fbc92dfcfe1511ce3b2b5d7578315b63dbbb76f7f51d90"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7d33d2fae0e8b170b6a6c57400e077412240f6f5bb2a342cf1ee512a787942bb"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8d65b38173085f24bc07f8b6c505cbb7418009fa1a1fcb111b1f4961814a442"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:015c6e863faa4779251436db398ae75051469f7c903b043a48f078e437656f83"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d44ff19eea13ae4acdaaab0179fa68c0c6f2f45d66a4d8ec1eda7d6cecbcc15f"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d3d8da4a631471dfaf94c10c85f5277b1f8e42ac42bade1ac67da4b4a7359b73"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4637b88343166249fe8aa94e7c4a62a180c4b3898283bb5d3d2fd5fe10d8e4e0"}, + {file = "pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "trove-classifiers (>=2024.10.12)"] +typing = ["typing-extensions"] +xmp = ["defusedxml"] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "prometheus-client" +version = "0.21.1" +description = "Python client for the Prometheus monitoring system." +optional = false +python-versions = ">=3.8" +groups = ["web"] +files = [ + {file = "prometheus_client-0.21.1-py3-none-any.whl", hash = "sha256:594b45c410d6f4f8888940fe80b5cc2521b305a1fafe1c58609ef715a001f301"}, + {file = "prometheus_client-0.21.1.tar.gz", hash = "sha256:252505a722ac04b0456be05c05f75f45d760c2911ffc45f2a06bcaed9f3ae3fb"}, +] + +[package.extras] +twisted = ["twisted"] + +[[package]] +name = "prometheus-fastapi-instrumentator" +version = "7.0.2" +description = "Instrument your FastAPI app with Prometheus metrics" +optional = false +python-versions = ">=3.8" +groups = ["web"] +files = [ + {file = "prometheus_fastapi_instrumentator-7.0.2-py3-none-any.whl", hash = "sha256:975e39992acb7a112758ff13ba95317e6c54d1bbf605f9156f31ac9f2800c32d"}, + {file = "prometheus_fastapi_instrumentator-7.0.2.tar.gz", hash = "sha256:8a4d8fb13dbe19d2882ac6af9ce236e4e1f98dc48e3fa44fe88d8e23ac3c953f"}, +] + +[package.dependencies] +prometheus-client = ">=0.8.0,<1.0.0" +starlette = ">=0.30.0,<1.0.0" + +[[package]] +name = "prompt-toolkit" +version = "3.0.50" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.8.0" +groups = ["main"] +files = [ + {file = "prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198"}, + {file = "prompt_toolkit-3.0.50.tar.gz", hash = "sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab"}, +] + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "proto-plus" +version = "1.26.0" +description = "Beautiful, Pythonic protocol buffers" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "proto_plus-1.26.0-py3-none-any.whl", hash = "sha256:bf2dfaa3da281fc3187d12d224c707cb57214fb2c22ba854eb0c105a3fb2d4d7"}, + {file = "proto_plus-1.26.0.tar.gz", hash = "sha256:6e93d5f5ca267b54300880fff156b6a3386b3fa3f43b1da62e680fc0c586ef22"}, +] + +[package.dependencies] +protobuf = ">=3.19.0,<6.0.0dev" + +[package.extras] +testing = ["google-api-core (>=1.31.5)"] + +[[package]] +name = "protobuf" +version = "5.29.3" +description = "" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "protobuf-5.29.3-cp310-abi3-win32.whl", hash = "sha256:3ea51771449e1035f26069c4c7fd51fba990d07bc55ba80701c78f886bf9c888"}, + {file = "protobuf-5.29.3-cp310-abi3-win_amd64.whl", hash = "sha256:a4fa6f80816a9a0678429e84973f2f98cbc218cca434abe8db2ad0bffc98503a"}, + {file = "protobuf-5.29.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a8434404bbf139aa9e1300dbf989667a83d42ddda9153d8ab76e0d5dcaca484e"}, + {file = "protobuf-5.29.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:daaf63f70f25e8689c072cfad4334ca0ac1d1e05a92fc15c54eb9cf23c3efd84"}, + {file = "protobuf-5.29.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:c027e08a08be10b67c06bf2370b99c811c466398c357e615ca88c91c07f0910f"}, + {file = "protobuf-5.29.3-cp38-cp38-win32.whl", hash = "sha256:84a57163a0ccef3f96e4b6a20516cedcf5bb3a95a657131c5c3ac62200d23252"}, + {file = "protobuf-5.29.3-cp38-cp38-win_amd64.whl", hash = "sha256:b89c115d877892a512f79a8114564fb435943b59067615894c3b13cd3e1fa107"}, + {file = "protobuf-5.29.3-cp39-cp39-win32.whl", hash = "sha256:0eb32bfa5219fc8d4111803e9a690658aa2e6366384fd0851064b963b6d1f2a7"}, + {file = "protobuf-5.29.3-cp39-cp39-win_amd64.whl", hash = "sha256:6ce8cc3389a20693bfde6c6562e03474c40851b44975c9b2bf6df7d8c4f864da"}, + {file = "protobuf-5.29.3-py3-none-any.whl", hash = "sha256:0a18ed4a24198528f2333802eb075e59dea9d679ab7a6c5efb017a59004d849f"}, + {file = "protobuf-5.29.3.tar.gz", hash = "sha256:5da0f41edaf117bde316404bad1a486cb4ededf8e4a54891296f648e8e076620"}, +] + +[[package]] +name = "psutil" +version = "5.9.8" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +groups = ["web"] +files = [ + {file = "psutil-5.9.8-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:26bd09967ae00920df88e0352a91cff1a78f8d69b3ecabbfe733610c0af486c8"}, + {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:05806de88103b25903dff19bb6692bd2e714ccf9e668d050d144012055cbca73"}, + {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:611052c4bc70432ec770d5d54f64206aa7203a101ec273a0cd82418c86503bb7"}, + {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:50187900d73c1381ba1454cf40308c2bf6f34268518b3f36a9b663ca87e65e36"}, + {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:02615ed8c5ea222323408ceba16c60e99c3f91639b07da6373fb7e6539abc56d"}, + {file = "psutil-5.9.8-cp27-none-win32.whl", hash = "sha256:36f435891adb138ed3c9e58c6af3e2e6ca9ac2f365efe1f9cfef2794e6c93b4e"}, + {file = "psutil-5.9.8-cp27-none-win_amd64.whl", hash = "sha256:bd1184ceb3f87651a67b2708d4c3338e9b10c5df903f2e3776b62303b26cb631"}, + {file = "psutil-5.9.8-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:aee678c8720623dc456fa20659af736241f575d79429a0e5e9cf88ae0605cc81"}, + {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cb6403ce6d8e047495a701dc7c5bd788add903f8986d523e3e20b98b733e421"}, + {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d06016f7f8625a1825ba3732081d77c94589dca78b7a3fc072194851e88461a4"}, + {file = "psutil-5.9.8-cp36-cp36m-win32.whl", hash = "sha256:7d79560ad97af658a0f6adfef8b834b53f64746d45b403f225b85c5c2c140eee"}, + {file = "psutil-5.9.8-cp36-cp36m-win_amd64.whl", hash = "sha256:27cc40c3493bb10de1be4b3f07cae4c010ce715290a5be22b98493509c6299e2"}, + {file = "psutil-5.9.8-cp37-abi3-win32.whl", hash = "sha256:bc56c2a1b0d15aa3eaa5a60c9f3f8e3e565303b465dbf57a1b730e7a2b9844e0"}, + {file = "psutil-5.9.8-cp37-abi3-win_amd64.whl", hash = "sha256:8db4c1b57507eef143a15a6884ca10f7c73876cdf5d51e713151c1236a0e68cf"}, + {file = "psutil-5.9.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8"}, + {file = "psutil-5.9.8.tar.gz", hash = "sha256:6be126e3225486dff286a8fb9a06246a5253f4c7c53b475ea5f5ac934e64194c"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] + +[[package]] +name = "pyaes" +version = "1.6.1" +description = "Pure-Python Implementation of the AES block-cipher and common modes of operation" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "pyaes-1.6.1.tar.gz", hash = "sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f"}, +] + +[[package]] +name = "pyasn1" +version = "0.6.1" +description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, + {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.1" +description = "A collection of ASN.1-based protocols modules" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd"}, + {file = "pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c"}, +] + +[package.dependencies] +pyasn1 = ">=0.4.6,<0.7.0" + +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + +[[package]] +name = "pycryptodomex" +version = "3.21.0" +description = "Cryptographic library for Python" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +groups = ["main"] +files = [ + {file = "pycryptodomex-3.21.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:dbeb84a399373df84a69e0919c1d733b89e049752426041deeb30d68e9867822"}, + {file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a192fb46c95489beba9c3f002ed7d93979423d1b2a53eab8771dbb1339eb3ddd"}, + {file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:1233443f19d278c72c4daae749872a4af3787a813e05c3561c73ab0c153c7b0f"}, + {file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbb07f88e277162b8bfca7134b34f18b400d84eac7375ce73117f865e3c80d4c"}, + {file = "pycryptodomex-3.21.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:e859e53d983b7fe18cb8f1b0e29d991a5c93be2c8dd25db7db1fe3bd3617f6f9"}, + {file = "pycryptodomex-3.21.0-cp27-cp27m-win32.whl", hash = "sha256:ef046b2e6c425647971b51424f0f88d8a2e0a2a63d3531817968c42078895c00"}, + {file = "pycryptodomex-3.21.0-cp27-cp27m-win_amd64.whl", hash = "sha256:da76ebf6650323eae7236b54b1b1f0e57c16483be6e3c1ebf901d4ada47563b6"}, + {file = "pycryptodomex-3.21.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:c07e64867a54f7e93186a55bec08a18b7302e7bee1b02fd84c6089ec215e723a"}, + {file = "pycryptodomex-3.21.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:56435c7124dd0ce0c8bdd99c52e5d183a0ca7fdcd06c5d5509423843f487dd0b"}, + {file = "pycryptodomex-3.21.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65d275e3f866cf6fe891411be9c1454fb58809ccc5de6d3770654c47197acd65"}, + {file = "pycryptodomex-3.21.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:5241bdb53bcf32a9568770a6584774b1b8109342bd033398e4ff2da052123832"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:34325b84c8b380675fd2320d0649cdcbc9cf1e0d1526edbe8fce43ed858cdc7e"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:103c133d6cd832ae7266feb0a65b69e3a5e4dbbd6f3a3ae3211a557fd653f516"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77ac2ea80bcb4b4e1c6a596734c775a1615d23e31794967416afc14852a639d3"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9aa0cf13a1a1128b3e964dc667e5fe5c6235f7d7cfb0277213f0e2a783837cc2"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46eb1f0c8d309da63a2064c28de54e5e614ad17b7e2f88df0faef58ce192fc7b"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:cc7e111e66c274b0df5f4efa679eb31e23c7545d702333dfd2df10ab02c2a2ce"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-musllinux_1_2_i686.whl", hash = "sha256:770d630a5c46605ec83393feaa73a9635a60e55b112e1fb0c3cea84c2897aa0a"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:52e23a0a6e61691134aa8c8beba89de420602541afaae70f66e16060fdcd677e"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-win32.whl", hash = "sha256:a3d77919e6ff56d89aada1bd009b727b874d464cb0e2e3f00a49f7d2e709d76e"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b0e9765f93fe4890f39875e6c90c96cb341767833cfa767f41b490b506fa9ec0"}, + {file = "pycryptodomex-3.21.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:feaecdce4e5c0045e7a287de0c4351284391fe170729aa9182f6bd967631b3a8"}, + {file = "pycryptodomex-3.21.0-pp27-pypy_73-win32.whl", hash = "sha256:365aa5a66d52fd1f9e0530ea97f392c48c409c2f01ff8b9a39c73ed6f527d36c"}, + {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3efddfc50ac0ca143364042324046800c126a1d63816d532f2e19e6f2d8c0c31"}, + {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0df2608682db8279a9ebbaf05a72f62a321433522ed0e499bc486a6889b96bf3"}, + {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5823d03e904ea3e53aebd6799d6b8ec63b7675b5d2f4a4bd5e3adcb512d03b37"}, + {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:27e84eeff24250ffec32722334749ac2a57a5fd60332cd6a0680090e7c42877e"}, + {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8ef436cdeea794015263853311f84c1ff0341b98fc7908e8a70595a68cefd971"}, + {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a1058e6dfe827f4209c5cae466e67610bcd0d66f2f037465daa2a29d92d952b"}, + {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ba09a5b407cbb3bcb325221e346a140605714b5e880741dc9a1e9ecf1688d42"}, + {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8a9d8342cf22b74a746e3c6c9453cb0cfbb55943410e3a2619bd9164b48dc9d9"}, + {file = "pycryptodomex-3.21.0.tar.gz", hash = "sha256:222d0bd05381dd25c32dd6065c071ebf084212ab79bab4599ba9e6a3e0009e6c"}, +] + +[[package]] +name = "pydantic" +version = "2.10.6" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +groups = ["main", "web"] +files = [ + {file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"}, + {file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.27.2" +typing-extensions = ">=4.12.2" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] + +[[package]] +name = "pydantic-core" +version = "2.27.2" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +groups = ["main", "web"] +files = [ + {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, + {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, + {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, + {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, + {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, + {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, + {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, + {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, + {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"}, + {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"}, + {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"}, + {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"}, + {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"}, + {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"}, + {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"}, + {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"}, + {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pydantic-settings" +version = "2.7.1" +description = "Settings management using Pydantic" +optional = false +python-versions = ">=3.8" +groups = ["main", "web"] +files = [ + {file = "pydantic_settings-2.7.1-py3-none-any.whl", hash = "sha256:590be9e6e24d06db33a4262829edef682500ef008565a969c73d39d5f8bfb3fd"}, + {file = "pydantic_settings-2.7.1.tar.gz", hash = "sha256:10c9caad35e64bfb3c2fbf70a078c0e25cc92499782e5200747f942a065dec93"}, +] + +[package.dependencies] +pydantic = ">=2.7.0" +python-dotenv = ">=0.21.0" + +[package.extras] +azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] +toml = ["tomli (>=2.0.1)"] +yaml = ["pyyaml (>=6.0.1)"] + +[[package]] +name = "pygments" +version = "2.19.1" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, + {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pyopenssl" +version = "24.2.1" +description = "Python wrapper module around the OpenSSL library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "pyOpenSSL-24.2.1-py3-none-any.whl", hash = "sha256:967d5719b12b243588573f39b0c677637145c7a1ffedcd495a487e58177fbb8d"}, + {file = "pyopenssl-24.2.1.tar.gz", hash = "sha256:4247f0dbe3748d560dcbb2ff3ea01af0f9a1a001ef5f7c4c647956ed8cbf0e95"}, +] + +[package.dependencies] +cryptography = ">=41.0.5,<44" + +[package.extras] +docs = ["sphinx (!=5.2.0,!=5.2.0.post0,!=7.2.5)", "sphinx-rtd-theme"] +test = ["pretend", "pytest (>=3.0.1)", "pytest-rerunfailures"] + +[[package]] +name = "pyparsing" +version = "3.2.1" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1"}, + {file = "pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pysocks" +version = "1.7.1" +description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +files = [ + {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"}, + {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, + {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, +] + +[[package]] +name = "pysubs2" +version = "1.8.0" +description = "A library for editing subtitle files" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pysubs2-1.8.0-py3-none-any.whl", hash = "sha256:05716f5039a9ebe32cd4d7673f923cf36204f3a3e99987f823ab83610b7035a0"}, + {file = "pysubs2-1.8.0.tar.gz", hash = "sha256:3397bb58a4a15b1325ba2ae3fd4d7c214e2c0ddb9f33190d6280d783bb433b20"}, +] + +[[package]] +name = "pytest" +version = "8.3.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.25.3" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest_asyncio-0.25.3-py3-none-any.whl", hash = "sha256:9e89518e0f9bd08928f97a3482fdc4e244df17529460bc038291ccaf8f85c7c3"}, + {file = "pytest_asyncio-0.25.3.tar.gz", hash = "sha256:fc1da2cf9f125ada7e710b4ddad05518d4cee187ae9412e9ac9271003497f07a"}, +] + +[package.dependencies] +pytest = ">=8.2,<9" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-dotenv" +version = "1.0.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.8" +groups = ["main", "web"] +files = [ + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "python-slugify" +version = "8.0.4" +description = "A Python slugify application that also handles Unicode" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856"}, + {file = "python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8"}, +] + +[package.dependencies] +text-unidecode = ">=1.3" + +[package.extras] +unidecode = ["Unidecode (>=1.1.1)"] + +[[package]] +name = "python-twitter-v2" +version = "0.9.2" +description = "A simple Python wrapper for Twitter API v2 ✨ 🍰 ✨" +optional = false +python-versions = "<4.0,>=3.7" +groups = ["main"] +files = [ + {file = "python_twitter_v2-0.9.2-py3-none-any.whl", hash = "sha256:c032c0b90e824ccd605620eb67cc59601f48a100fe7424090aaf37f243239e82"}, + {file = "python_twitter_v2-0.9.2.tar.gz", hash = "sha256:dcd41ebfbc1b0ca6a1212870b0ff68b85e2111655e09027a0e42829fe3a63460"}, +] + +[package.dependencies] +Authlib = ">=1.0.0" +dataclasses-json = ">=0.5.7" +requests = ">=2.28" + +[[package]] +name = "pytz" +version = "2025.1" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57"}, + {file = "pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["web"] +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "redis" +version = "3.5.3" +description = "Python client for Redis key-value store" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["main"] +files = [ + {file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"}, + {file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"}, +] + +[package.extras] +hiredis = ["hiredis (>=0.1.3)"] + +[[package]] +name = "regex" +version = "2024.11.6" +description = "Alternative regular expression module, to replace re." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"}, + {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"}, + {file = "regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62"}, + {file = "regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e"}, + {file = "regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45"}, + {file = "regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9"}, + {file = "regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad"}, + {file = "regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54"}, + {file = "regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d"}, + {file = "regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff"}, + {file = "regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3a51ccc315653ba012774efca4f23d1d2a8a8f278a6072e29c7147eee7da446b"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ad182d02e40de7459b73155deb8996bbd8e96852267879396fb274e8700190e3"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba9b72e5643641b7d41fa1f6d5abda2c9a263ae835b917348fc3c928182ad467"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40291b1b89ca6ad8d3f2b82782cc33807f1406cf68c8d440861da6304d8ffbbd"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdf58d0e516ee426a48f7b2c03a332a4114420716d55769ff7108c37a09951bf"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a36fdf2af13c2b14738f6e973aba563623cb77d753bbbd8d414d18bfaa3105dd"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1cee317bfc014c2419a76bcc87f071405e3966da434e03e13beb45f8aced1a6"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50153825ee016b91549962f970d6a4442fa106832e14c918acd1c8e479916c4f"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea1bfda2f7162605f6e8178223576856b3d791109f15ea99a9f95c16a7636fb5"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:df951c5f4a1b1910f1a99ff42c473ff60f8225baa1cdd3539fe2819d9543e9df"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:072623554418a9911446278f16ecb398fb3b540147a7828c06e2011fa531e773"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f654882311409afb1d780b940234208a252322c24a93b442ca714d119e68086c"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:89d75e7293d2b3e674db7d4d9b1bee7f8f3d1609428e293771d1a962617150cc"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f65557897fc977a44ab205ea871b690adaef6b9da6afda4790a2484b04293a5f"}, + {file = "regex-2024.11.6-cp38-cp38-win32.whl", hash = "sha256:6f44ec28b1f858c98d3036ad5d7d0bfc568bdd7a74f9c24e25f41ef1ebfd81a4"}, + {file = "regex-2024.11.6-cp38-cp38-win_amd64.whl", hash = "sha256:bb8f74f2f10dbf13a0be8de623ba4f9491faf58c24064f32b65679b021ed0001"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b"}, + {file = "regex-2024.11.6-cp39-cp39-win32.whl", hash = "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57"}, + {file = "regex-2024.11.6-cp39-cp39-win_amd64.whl", hash = "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983"}, + {file = "regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519"}, +] + +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +groups = ["main", "web"] +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +PySocks = {version = ">=1.5.6,<1.5.7 || >1.5.7", optional = true, markers = "extra == \"socks\""} +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +description = "OAuthlib authentication support for Requests." +optional = false +python-versions = ">=3.4" +groups = ["main"] +files = [ + {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, + {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, +] + +[package.dependencies] +oauthlib = ">=3.0.0" +requests = ">=2.0.0" + +[package.extras] +rsa = ["oauthlib[signedtoken] (>=3.0.0)"] + +[[package]] +name = "retrying" +version = "1.3.4" +description = "Retrying" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "retrying-1.3.4-py3-none-any.whl", hash = "sha256:8cc4d43cb8e1125e0ff3344e9de678fefd85db3b750b81b2240dc0183af37b35"}, + {file = "retrying-1.3.4.tar.gz", hash = "sha256:345da8c5765bd982b1d1915deb9102fd3d1f7ad16bd84a9700b85f64d24e8f3e"}, +] + +[package.dependencies] +six = ">=1.7.0" + +[[package]] +name = "rich" +version = "13.9.4" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.8.0" +groups = ["main"] +files = [ + {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, + {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "rich-argparse" +version = "1.7.0" +description = "Rich help formatters for argparse and optparse" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "rich_argparse-1.7.0-py3-none-any.whl", hash = "sha256:b8ec8943588e9731967f4f97b735b03dc127c416f480a083060433a97baf2fd3"}, + {file = "rich_argparse-1.7.0.tar.gz", hash = "sha256:f31d809c465ee43f367d599ccaf88b73bc2c4d75d74ed43f2d538838c53544ba"}, +] + +[package.dependencies] +rich = ">=11.0.0" + +[[package]] +name = "rsa" +version = "4.9" +description = "Pure-Python RSA implementation" +optional = false +python-versions = ">=3.6,<4" +groups = ["main"] +files = [ + {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, + {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, +] + +[package.dependencies] +pyasn1 = ">=0.1.3" + +[[package]] +name = "ruamel-yaml" +version = "0.18.10" +description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "ruamel.yaml-0.18.10-py3-none-any.whl", hash = "sha256:30f22513ab2301b3d2b577adc121c6471f28734d3d9728581245f1e76468b4f1"}, + {file = "ruamel.yaml-0.18.10.tar.gz", hash = "sha256:20c86ab29ac2153f80a428e1254a8adf686d3383df04490514ca3b79a362db58"}, +] + +[package.dependencies] +"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.13\""} + +[package.extras] +docs = ["mercurial (>5.7)", "ryd"] +jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] + +[[package]] +name = "ruamel-yaml-clib" +version = "0.2.12" +description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "platform_python_implementation == \"CPython\"" +files = [ + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd5415dded15c3822597455bc02bcd66e81ef8b7a48cb71a33628fc9fdde39df"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:943f32bc9dedb3abff9879edc134901df92cfce2c3d5c9348f172f62eb2d771d"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95c3829bb364fdb8e0332c9931ecf57d9be3519241323c5274bd82f709cebc0c"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:e7e3736715fbf53e9be2a79eb4db68e4ed857017344d697e8b9749444ae57475"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bc5f1e1c28e966d61d2519f2a3d451ba989f9ea0f2307de7bc45baa526de9e45"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a0e060aace4c24dcaf71023bbd7d42674e3b230f7e7b97317baf1e953e5b519"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"}, + {file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"}, +] + +[[package]] +name = "s3transfer" +version = "0.11.2" +description = "An Amazon S3 Transfer Manager" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "s3transfer-0.11.2-py3-none-any.whl", hash = "sha256:be6ecb39fadd986ef1701097771f87e4d2f821f27f6071c872143884d2950fbc"}, + {file = "s3transfer-0.11.2.tar.gz", hash = "sha256:3b39185cb72f5acc77db1a58b6e25b977f28d20496b6e58d6813d75f464d632f"}, +] + +[package.dependencies] +botocore = ">=1.36.0,<2.0a.0" + +[package.extras] +crt = ["botocore[crt] (>=1.36.0,<2.0a.0)"] + +[[package]] +name = "selenium" +version = "4.28.1" +description = "Official Python bindings for Selenium WebDriver" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "selenium-4.28.1-py3-none-any.whl", hash = "sha256:4238847e45e24e4472cfcf3554427512c7aab9443396435b1623ef406fff1cc1"}, + {file = "selenium-4.28.1.tar.gz", hash = "sha256:0072d08670d7ec32db901bd0107695a330cecac9f196e3afb3fa8163026e022a"}, +] + +[package.dependencies] +certifi = ">=2021.10.8" +trio = ">=0.17,<1.0" +trio-websocket = ">=0.9,<1.0" +typing_extensions = ">=4.9,<5.0" +urllib3 = {version = ">=1.26,<3", extras = ["socks"]} +websocket-client = ">=1.8,<2.0" + +[[package]] +name = "setuptools" +version = "75.8.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.9" +groups = ["worker"] +files = [ + {file = "setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3"}, + {file = "setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"] +core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"] + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev", "web"] +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, +] + +[[package]] +name = "soupsieve" +version = "2.6" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, + {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.38" +description = "Database Abstraction Library" +optional = false +python-versions = ">=3.7" +groups = ["main", "web"] +files = [ + {file = "SQLAlchemy-2.0.38-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5e1d9e429028ce04f187a9f522818386c8b076723cdbe9345708384f49ebcec6"}, + {file = "SQLAlchemy-2.0.38-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b87a90f14c68c925817423b0424381f0e16d80fc9a1a1046ef202ab25b19a444"}, + {file = "SQLAlchemy-2.0.38-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:402c2316d95ed90d3d3c25ad0390afa52f4d2c56b348f212aa9c8d072a40eee5"}, + {file = "SQLAlchemy-2.0.38-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6493bc0eacdbb2c0f0d260d8988e943fee06089cd239bd7f3d0c45d1657a70e2"}, + {file = "SQLAlchemy-2.0.38-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0561832b04c6071bac3aad45b0d3bb6d2c4f46a8409f0a7a9c9fa6673b41bc03"}, + {file = "SQLAlchemy-2.0.38-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:49aa2cdd1e88adb1617c672a09bf4ebf2f05c9448c6dbeba096a3aeeb9d4d443"}, + {file = "SQLAlchemy-2.0.38-cp310-cp310-win32.whl", hash = "sha256:64aa8934200e222f72fcfd82ee71c0130a9c07d5725af6fe6e919017d095b297"}, + {file = "SQLAlchemy-2.0.38-cp310-cp310-win_amd64.whl", hash = "sha256:c57b8e0841f3fce7b703530ed70c7c36269c6d180ea2e02e36b34cb7288c50c7"}, + {file = "SQLAlchemy-2.0.38-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bf89e0e4a30714b357f5d46b6f20e0099d38b30d45fa68ea48589faf5f12f62d"}, + {file = "SQLAlchemy-2.0.38-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8455aa60da49cb112df62b4721bd8ad3654a3a02b9452c783e651637a1f21fa2"}, + {file = "SQLAlchemy-2.0.38-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f53c0d6a859b2db58332e0e6a921582a02c1677cc93d4cbb36fdf49709b327b2"}, + {file = "SQLAlchemy-2.0.38-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3c4817dff8cef5697f5afe5fec6bc1783994d55a68391be24cb7d80d2dbc3a6"}, + {file = "SQLAlchemy-2.0.38-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9cea5b756173bb86e2235f2f871b406a9b9d722417ae31e5391ccaef5348f2c"}, + {file = "SQLAlchemy-2.0.38-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40e9cdbd18c1f84631312b64993f7d755d85a3930252f6276a77432a2b25a2f3"}, + {file = "SQLAlchemy-2.0.38-cp311-cp311-win32.whl", hash = "sha256:cb39ed598aaf102251483f3e4675c5dd6b289c8142210ef76ba24aae0a8f8aba"}, + {file = "SQLAlchemy-2.0.38-cp311-cp311-win_amd64.whl", hash = "sha256:f9d57f1b3061b3e21476b0ad5f0397b112b94ace21d1f439f2db472e568178ae"}, + {file = "SQLAlchemy-2.0.38-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12d5b06a1f3aeccf295a5843c86835033797fea292c60e72b07bcb5d820e6dd3"}, + {file = "SQLAlchemy-2.0.38-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e036549ad14f2b414c725349cce0772ea34a7ab008e9cd67f9084e4f371d1f32"}, + {file = "SQLAlchemy-2.0.38-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee3bee874cb1fadee2ff2b79fc9fc808aa638670f28b2145074538d4a6a5028e"}, + {file = "SQLAlchemy-2.0.38-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e185ea07a99ce8b8edfc788c586c538c4b1351007e614ceb708fd01b095ef33e"}, + {file = "SQLAlchemy-2.0.38-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b79ee64d01d05a5476d5cceb3c27b5535e6bb84ee0f872ba60d9a8cd4d0e6579"}, + {file = "SQLAlchemy-2.0.38-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:afd776cf1ebfc7f9aa42a09cf19feadb40a26366802d86c1fba080d8e5e74bdd"}, + {file = "SQLAlchemy-2.0.38-cp312-cp312-win32.whl", hash = "sha256:a5645cd45f56895cfe3ca3459aed9ff2d3f9aaa29ff7edf557fa7a23515a3725"}, + {file = "SQLAlchemy-2.0.38-cp312-cp312-win_amd64.whl", hash = "sha256:1052723e6cd95312f6a6eff9a279fd41bbae67633415373fdac3c430eca3425d"}, + {file = "SQLAlchemy-2.0.38-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ecef029b69843b82048c5b347d8e6049356aa24ed644006c9a9d7098c3bd3bfd"}, + {file = "SQLAlchemy-2.0.38-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c8bcad7fc12f0cc5896d8e10fdf703c45bd487294a986903fe032c72201596b"}, + {file = "SQLAlchemy-2.0.38-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a0ef3f98175d77180ffdc623d38e9f1736e8d86b6ba70bff182a7e68bed7727"}, + {file = "SQLAlchemy-2.0.38-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b0ac78898c50e2574e9f938d2e5caa8fe187d7a5b69b65faa1ea4648925b096"}, + {file = "SQLAlchemy-2.0.38-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9eb4fa13c8c7a2404b6a8e3772c17a55b1ba18bc711e25e4d6c0c9f5f541b02a"}, + {file = "SQLAlchemy-2.0.38-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5dba1cdb8f319084f5b00d41207b2079822aa8d6a4667c0f369fce85e34b0c86"}, + {file = "SQLAlchemy-2.0.38-cp313-cp313-win32.whl", hash = "sha256:eae27ad7580529a427cfdd52c87abb2dfb15ce2b7a3e0fc29fbb63e2ed6f8120"}, + {file = "SQLAlchemy-2.0.38-cp313-cp313-win_amd64.whl", hash = "sha256:b335a7c958bc945e10c522c069cd6e5804f4ff20f9a744dd38e748eb602cbbda"}, + {file = "SQLAlchemy-2.0.38-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:40310db77a55512a18827488e592965d3dec6a3f1e3d8af3f8243134029daca3"}, + {file = "SQLAlchemy-2.0.38-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d3043375dd5bbcb2282894cbb12e6c559654c67b5fffb462fda815a55bf93f7"}, + {file = "SQLAlchemy-2.0.38-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70065dfabf023b155a9c2a18f573e47e6ca709b9e8619b2e04c54d5bcf193178"}, + {file = "SQLAlchemy-2.0.38-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:c058b84c3b24812c859300f3b5abf300daa34df20d4d4f42e9652a4d1c48c8a4"}, + {file = "SQLAlchemy-2.0.38-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0398361acebb42975deb747a824b5188817d32b5c8f8aba767d51ad0cc7bb08d"}, + {file = "SQLAlchemy-2.0.38-cp37-cp37m-win32.whl", hash = "sha256:a2bc4e49e8329f3283d99840c136ff2cd1a29e49b5624a46a290f04dff48e079"}, + {file = "SQLAlchemy-2.0.38-cp37-cp37m-win_amd64.whl", hash = "sha256:9cd136184dd5f58892f24001cdce986f5d7e96059d004118d5410671579834a4"}, + {file = "SQLAlchemy-2.0.38-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:665255e7aae5f38237b3a6eae49d2358d83a59f39ac21036413fab5d1e810578"}, + {file = "SQLAlchemy-2.0.38-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:92f99f2623ff16bd4aaf786ccde759c1f676d39c7bf2855eb0b540e1ac4530c8"}, + {file = "SQLAlchemy-2.0.38-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa498d1392216fae47eaf10c593e06c34476ced9549657fca713d0d1ba5f7248"}, + {file = "SQLAlchemy-2.0.38-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9afbc3909d0274d6ac8ec891e30210563b2c8bdd52ebbda14146354e7a69373"}, + {file = "SQLAlchemy-2.0.38-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:57dd41ba32430cbcc812041d4de8d2ca4651aeefad2626921ae2a23deb8cd6ff"}, + {file = "SQLAlchemy-2.0.38-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3e35d5565b35b66905b79ca4ae85840a8d40d31e0b3e2990f2e7692071b179ca"}, + {file = "SQLAlchemy-2.0.38-cp38-cp38-win32.whl", hash = "sha256:f0d3de936b192980209d7b5149e3c98977c3810d401482d05fb6d668d53c1c63"}, + {file = "SQLAlchemy-2.0.38-cp38-cp38-win_amd64.whl", hash = "sha256:3868acb639c136d98107c9096303d2d8e5da2880f7706f9f8c06a7f961961149"}, + {file = "SQLAlchemy-2.0.38-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07258341402a718f166618470cde0c34e4cec85a39767dce4e24f61ba5e667ea"}, + {file = "SQLAlchemy-2.0.38-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a826f21848632add58bef4f755a33d45105d25656a0c849f2dc2df1c71f6f50"}, + {file = "SQLAlchemy-2.0.38-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:386b7d136919bb66ced64d2228b92d66140de5fefb3c7df6bd79069a269a7b06"}, + {file = "SQLAlchemy-2.0.38-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f2951dc4b4f990a4b394d6b382accb33141d4d3bd3ef4e2b27287135d6bdd68"}, + {file = "SQLAlchemy-2.0.38-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8bf312ed8ac096d674c6aa9131b249093c1b37c35db6a967daa4c84746bc1bc9"}, + {file = "SQLAlchemy-2.0.38-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6db316d6e340f862ec059dc12e395d71f39746a20503b124edc255973977b728"}, + {file = "SQLAlchemy-2.0.38-cp39-cp39-win32.whl", hash = "sha256:c09a6ea87658695e527104cf857c70f79f14e9484605e205217aae0ec27b45fc"}, + {file = "SQLAlchemy-2.0.38-cp39-cp39-win_amd64.whl", hash = "sha256:12f5c9ed53334c3ce719155424dc5407aaa4f6cadeb09c5b627e06abb93933a1"}, + {file = "SQLAlchemy-2.0.38-py3-none-any.whl", hash = "sha256:63178c675d4c80def39f1febd625a6333f44c0ba269edd8a468b156394b27753"}, + {file = "sqlalchemy-2.0.38.tar.gz", hash = "sha256:e5a4d82bdb4bf1ac1285a68eab02d253ab73355d9f0fe725a97e1e0fa689decb"}, +] + +[package.dependencies] +greenlet = {version = "!=0.4.17", markers = "python_version < \"3.14\" 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\")"} +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.45.3" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.9" +groups = ["web"] +files = [ + {file = "starlette-0.45.3-py3-none-any.whl", hash = "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d"}, + {file = "starlette-0.45.3.tar.gz", hash = "sha256:2cbcba2a75806f8a41c722141486f37c28e30a0921c5f6fe4346cb0dcee1302f"}, +] + +[package.dependencies] +anyio = ">=3.6.2,<5" + +[package.extras] +full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] + +[[package]] +name = "telethon" +version = "1.38.1" +description = "Full-featured Telegram client library for Python 3" +optional = false +python-versions = ">=3.5" +groups = ["main"] +files = [ + {file = "Telethon-1.38.1-py3-none-any.whl", hash = "sha256:30c187017501bfb982b8af5659f864dda4108f77ea49cfce61e8f6fdb8a18d6e"}, + {file = "Telethon-1.38.1.tar.gz", hash = "sha256:f9866c1e37197a0894e0c02aa56a6359bffb14a585e88e18e3e819df4fda399a"}, +] + +[package.dependencies] +pyaes = "*" +rsa = "*" + +[package.extras] +cryptg = ["cryptg"] + +[[package]] +name = "text-unidecode" +version = "1.3" +description = "The most basic Text::Unidecode port" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, + {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, +] + +[[package]] +name = "tomli" +version = "2.2.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version < \"3.11\"" +files = [ + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +description = "Fast, Extensible Progress Meter" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, + {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] +discord = ["requests"] +notebook = ["ipywidgets (>=6)"] +slack = ["slack-sdk"] +telegram = ["requests"] + +[[package]] +name = "trio" +version = "0.29.0" +description = "A friendly Python library for async concurrency and I/O" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "trio-0.29.0-py3-none-any.whl", hash = "sha256:d8c463f1a9cc776ff63e331aba44c125f423a5a13c684307e828d930e625ba66"}, + {file = "trio-0.29.0.tar.gz", hash = "sha256:ea0d3967159fc130acb6939a0be0e558e364fee26b5deeecc893a6b08c361bdf"}, +] + +[package.dependencies] +attrs = ">=23.2.0" +cffi = {version = ">=1.14", markers = "os_name == \"nt\" and implementation_name != \"pypy\""} +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +idna = "*" +outcome = "*" +sniffio = ">=1.3.0" +sortedcontainers = "*" + +[[package]] +name = "trio-websocket" +version = "0.11.1" +description = "WebSocket library for Trio" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "trio-websocket-0.11.1.tar.gz", hash = "sha256:18c11793647703c158b1f6e62de638acada927344d534e3c7628eedcb746839f"}, + {file = "trio_websocket-0.11.1-py3-none-any.whl", hash = "sha256:520d046b0d030cf970b8b2b2e00c4c2245b3807853ecd44214acd33d74581638"}, +] + +[package.dependencies] +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +trio = ">=0.11" +wsproto = ">=0.14" + +[[package]] +name = "tsp-client" +version = "0.2.0" +description = "An IETF Time-Stamp Protocol (TSP) (RFC 3161) client" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "tsp-client-0.2.0.tar.gz", hash = "sha256:6e66148dd116322eb44a7484e5ad33bbe640b997343c443de9cc70fc5eb19987"}, + {file = "tsp_client-0.2.0-py3-none-any.whl", hash = "sha256:0b790d10a68d66782c13f1d7cc7f5206df26b49826c1da80944b7c05b1731784"}, +] + +[package.dependencies] +asn1crypto = ">=0.24.0" +pyOpenSSL = ">=20.0.0" +requests = ">=2.18.4" + +[package.extras] +tests = ["build", "coverage", "mypy", "ruff", "wheel"] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev", "web"] +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "typing-inspect" +version = "0.9.0" +description = "Runtime inspection utilities for typing module." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f"}, + {file = "typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78"}, +] + +[package.dependencies] +mypy-extensions = ">=0.3.0" +typing-extensions = ">=3.7.4" + +[[package]] +name = "tzdata" +version = "2025.1" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +groups = ["main"] +files = [ + {file = "tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639"}, + {file = "tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694"}, +] + +[[package]] +name = "tzlocal" +version = "5.3" +description = "tzinfo object for the local timezone" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "tzlocal-5.3-py3-none-any.whl", hash = "sha256:3814135a1bb29763c6e4f08fd6e41dbb435c7a60bfbb03270211bcc537187d8c"}, + {file = "tzlocal-5.3.tar.gz", hash = "sha256:2fafbfc07e9d8b49ade18f898d6bcd37ae88ce3ad6486842a2e4f03af68323d2"}, +] + +[package.dependencies] +tzdata = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] + +[[package]] +name = "uritemplate" +version = "4.1.1" +description = "Implementation of RFC 6570 URI Templates" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"}, + {file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"}, +] + +[[package]] +name = "urllib3" +version = "2.3.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["main", "web"] +files = [ + {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, + {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, +] + +[package.dependencies] +pysocks = {version = ">=1.5.6,<1.5.7 || >1.5.7,<2.0", optional = true, markers = "extra == \"socks\""} + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "uvicorn" +version = "0.34.0" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.9" +groups = ["web"] +files = [ + {file = "uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4"}, + {file = "uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" +typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} + +[package.extras] +standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[[package]] +name = "vine" +version = "5.1.0" +description = "Python promises." +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc"}, + {file = "vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0"}, +] + +[[package]] +name = "vk-api" +version = "11.9.9" +description = "Python модуль для создания скриптов для социальной сети Вконтакте (vk.com API wrapper)" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "vk_api-11.9.9-py3-none-any.whl", hash = "sha256:c71021506449afe5b9bbb1c4acb0d86b35a007ddc21678478e46fbbeabd1f3ef"}, + {file = "vk_api-11.9.9.tar.gz", hash = "sha256:c7741e40bc05980c91ed94c84542e1e7e7370e101b5eaa74222958d4130fe3c2"}, +] + +[package.dependencies] +requests = "*" + +[package.extras] +vkaudio = ["beautifulsoup4"] +vkstreaming = ["websocket-client"] + +[[package]] +name = "vk-url-scraper" +version = "0.3.27" +description = "Scrape VK URLs to fetch info and media - python API or command line tool." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "vk-url-scraper-0.3.27.tar.gz", hash = "sha256:133d252ee94ceb1ee9515fb448d410ba471cbccc19e303b548076cd44cc81f30"}, + {file = "vk_url_scraper-0.3.27-py3-none-any.whl", hash = "sha256:c1c001b66b80343a991628080398d8a923e8753183b952f99f40ecafe1087070"}, +] + +[package.dependencies] +brotli = {version = ">=1.0.9", markers = "platform_python_implementation >= \"CPython\""} +certifi = {version = ">=2022.12.7", markers = "python_version >= \"3.6\""} +charset-normalizer = {version = ">=3.0.1", markers = "python_version >= \"3.6\""} +idna = {version = ">=3.4", markers = "python_version >= \"3.5\""} +mutagen = {version = ">=1.46.0", markers = "python_version >= \"3.7\""} +pycryptodomex = {version = ">=3.17", markers = "python_version >= \"2.7\" and python_version not in \"3.0, 3.1, 3.2, 3.3, 3.4\""} +requests = {version = ">=2.28.2", markers = "python_version >= \"3.7\" and python_version < \"4\""} +urllib3 = {version = ">=1.26.14", markers = "python_version >= \"2.7\" and python_version not in \"3.0, 3.1, 3.2, 3.3, 3.4, 3.5\""} +vk-api = ">=11.9.9" +websockets = {version = ">=10.4", markers = "python_version >= \"3.7\""} +yt-dlp = ">=2023.2.17" + +[package.extras] +dev = ["Sphinx (>=4.3.0,<5.1.0)", "black (>=22.3.0)", "flake8", "furo (>=2022.6.4.1)", "isort (>=5.10.1)", "mypy (>=0.961)", "myst-parser (>=0.15.2,<0.19.0)", "packaging", "pytest", "pytest-cov", "pytest-sphinx", "python-dotenv (>=0.21.1)", "setuptools", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints", "sphinx-copybutton (>=0.5.0)", "twine (>=1.11.0)", "wheel"] + +[[package]] +name = "warcio" +version = "1.7.5" +description = "Streaming WARC (and ARC) IO library" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "warcio-1.7.5-py2.py3-none-any.whl", hash = "sha256:ca96130bde7747e49da714097d144c6ff939458d4f93e1beb1e42455db4326d4"}, + {file = "warcio-1.7.5.tar.gz", hash = "sha256:7247b57e68074cfd9433cb6dc226f8567d6777052abec2d3c78346cffa4d19b9"}, +] + +[package.dependencies] +six = "*" + +[package.extras] +all = ["brotlipy"] +testing = ["hookdns", "httpbin (>=0.10.2)", "pytest", "pytest-cov", "requests", "urllib3 (>=1.26.5,<1.26.16)", "wsgiprox"] + +[[package]] +name = "watchdog" +version = "6.0.0" +description = "Filesystem events monitoring" +optional = false +python-versions = ">=3.9" +groups = ["worker"] +files = [ + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, + {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, + {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, + {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, + {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, +] + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + +[[package]] +name = "websocket-client" +version = "1.8.0" +description = "WebSocket client for Python with low level API options" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"}, + {file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"}, +] + +[package.extras] +docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx-rtd-theme (>=1.1.0)"] +optional = ["python-socks", "wsaccel"] +test = ["websockets"] + +[[package]] +name = "websockets" +version = "14.2" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "websockets-14.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e8179f95323b9ab1c11723e5d91a89403903f7b001828161b480a7810b334885"}, + {file = "websockets-14.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d8c3e2cdb38f31d8bd7d9d28908005f6fa9def3324edb9bf336d7e4266fd397"}, + {file = "websockets-14.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:714a9b682deb4339d39ffa674f7b674230227d981a37d5d174a4a83e3978a610"}, + {file = "websockets-14.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2e53c72052f2596fb792a7acd9704cbc549bf70fcde8a99e899311455974ca3"}, + {file = "websockets-14.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3fbd68850c837e57373d95c8fe352203a512b6e49eaae4c2f4088ef8cf21980"}, + {file = "websockets-14.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b27ece32f63150c268593d5fdb82819584831a83a3f5809b7521df0685cd5d8"}, + {file = "websockets-14.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4daa0faea5424d8713142b33825fff03c736f781690d90652d2c8b053345b0e7"}, + {file = "websockets-14.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:bc63cee8596a6ec84d9753fd0fcfa0452ee12f317afe4beae6b157f0070c6c7f"}, + {file = "websockets-14.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a570862c325af2111343cc9b0257b7119b904823c675b22d4ac547163088d0d"}, + {file = "websockets-14.2-cp310-cp310-win32.whl", hash = "sha256:75862126b3d2d505e895893e3deac0a9339ce750bd27b4ba515f008b5acf832d"}, + {file = "websockets-14.2-cp310-cp310-win_amd64.whl", hash = "sha256:cc45afb9c9b2dc0852d5c8b5321759cf825f82a31bfaf506b65bf4668c96f8b2"}, + {file = "websockets-14.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3bdc8c692c866ce5fefcaf07d2b55c91d6922ac397e031ef9b774e5b9ea42166"}, + {file = "websockets-14.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c93215fac5dadc63e51bcc6dceca72e72267c11def401d6668622b47675b097f"}, + {file = "websockets-14.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1c9b6535c0e2cf8a6bf938064fb754aaceb1e6a4a51a80d884cd5db569886910"}, + {file = "websockets-14.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a52a6d7cf6938e04e9dceb949d35fbdf58ac14deea26e685ab6368e73744e4c"}, + {file = "websockets-14.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9f05702e93203a6ff5226e21d9b40c037761b2cfb637187c9802c10f58e40473"}, + {file = "websockets-14.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22441c81a6748a53bfcb98951d58d1af0661ab47a536af08920d129b4d1c3473"}, + {file = "websockets-14.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd9b868d78b194790e6236d9cbc46d68aba4b75b22497eb4ab64fa640c3af56"}, + {file = "websockets-14.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1a5a20d5843886d34ff8c57424cc65a1deda4375729cbca4cb6b3353f3ce4142"}, + {file = "websockets-14.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:34277a29f5303d54ec6468fb525d99c99938607bc96b8d72d675dee2b9f5bf1d"}, + {file = "websockets-14.2-cp311-cp311-win32.whl", hash = "sha256:02687db35dbc7d25fd541a602b5f8e451a238ffa033030b172ff86a93cb5dc2a"}, + {file = "websockets-14.2-cp311-cp311-win_amd64.whl", hash = "sha256:862e9967b46c07d4dcd2532e9e8e3c2825e004ffbf91a5ef9dde519ee2effb0b"}, + {file = "websockets-14.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1f20522e624d7ffbdbe259c6b6a65d73c895045f76a93719aa10cd93b3de100c"}, + {file = "websockets-14.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:647b573f7d3ada919fd60e64d533409a79dcf1ea21daeb4542d1d996519ca967"}, + {file = "websockets-14.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6af99a38e49f66be5a64b1e890208ad026cda49355661549c507152113049990"}, + {file = "websockets-14.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:091ab63dfc8cea748cc22c1db2814eadb77ccbf82829bac6b2fbe3401d548eda"}, + {file = "websockets-14.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b374e8953ad477d17e4851cdc66d83fdc2db88d9e73abf755c94510ebddceb95"}, + {file = "websockets-14.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a39d7eceeea35db85b85e1169011bb4321c32e673920ae9c1b6e0978590012a3"}, + {file = "websockets-14.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0a6f3efd47ffd0d12080594f434faf1cd2549b31e54870b8470b28cc1d3817d9"}, + {file = "websockets-14.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:065ce275e7c4ffb42cb738dd6b20726ac26ac9ad0a2a48e33ca632351a737267"}, + {file = "websockets-14.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e9d0e53530ba7b8b5e389c02282f9d2aa47581514bd6049d3a7cffe1385cf5fe"}, + {file = "websockets-14.2-cp312-cp312-win32.whl", hash = "sha256:20e6dd0984d7ca3037afcb4494e48c74ffb51e8013cac71cf607fffe11df7205"}, + {file = "websockets-14.2-cp312-cp312-win_amd64.whl", hash = "sha256:44bba1a956c2c9d268bdcdf234d5e5ff4c9b6dc3e300545cbe99af59dda9dcce"}, + {file = "websockets-14.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f1372e511c7409a542291bce92d6c83320e02c9cf392223272287ce55bc224e"}, + {file = "websockets-14.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4da98b72009836179bb596a92297b1a61bb5a830c0e483a7d0766d45070a08ad"}, + {file = "websockets-14.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8a86a269759026d2bde227652b87be79f8a734e582debf64c9d302faa1e9f03"}, + {file = "websockets-14.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86cf1aaeca909bf6815ea714d5c5736c8d6dd3a13770e885aafe062ecbd04f1f"}, + {file = "websockets-14.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b0f6c3ba3b1240f602ebb3971d45b02cc12bd1845466dd783496b3b05783a5"}, + {file = "websockets-14.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:669c3e101c246aa85bc8534e495952e2ca208bd87994650b90a23d745902db9a"}, + {file = "websockets-14.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eabdb28b972f3729348e632ab08f2a7b616c7e53d5414c12108c29972e655b20"}, + {file = "websockets-14.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2066dc4cbcc19f32c12a5a0e8cc1b7ac734e5b64ac0a325ff8353451c4b15ef2"}, + {file = "websockets-14.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ab95d357cd471df61873dadf66dd05dd4709cae001dd6342edafc8dc6382f307"}, + {file = "websockets-14.2-cp313-cp313-win32.whl", hash = "sha256:a9e72fb63e5f3feacdcf5b4ff53199ec8c18d66e325c34ee4c551ca748623bbc"}, + {file = "websockets-14.2-cp313-cp313-win_amd64.whl", hash = "sha256:b439ea828c4ba99bb3176dc8d9b933392a2413c0f6b149fdcba48393f573377f"}, + {file = "websockets-14.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7cd5706caec1686c5d233bc76243ff64b1c0dc445339bd538f30547e787c11fe"}, + {file = "websockets-14.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ec607328ce95a2f12b595f7ae4c5d71bf502212bddcea528290b35c286932b12"}, + {file = "websockets-14.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da85651270c6bfb630136423037dd4975199e5d4114cae6d3066641adcc9d1c7"}, + {file = "websockets-14.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3ecadc7ce90accf39903815697917643f5b7cfb73c96702318a096c00aa71f5"}, + {file = "websockets-14.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1979bee04af6a78608024bad6dfcc0cc930ce819f9e10342a29a05b5320355d0"}, + {file = "websockets-14.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dddacad58e2614a24938a50b85969d56f88e620e3f897b7d80ac0d8a5800258"}, + {file = "websockets-14.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:89a71173caaf75fa71a09a5f614f450ba3ec84ad9fca47cb2422a860676716f0"}, + {file = "websockets-14.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6af6a4b26eea4fc06c6818a6b962a952441e0e39548b44773502761ded8cc1d4"}, + {file = "websockets-14.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:80c8efa38957f20bba0117b48737993643204645e9ec45512579132508477cfc"}, + {file = "websockets-14.2-cp39-cp39-win32.whl", hash = "sha256:2e20c5f517e2163d76e2729104abc42639c41cf91f7b1839295be43302713661"}, + {file = "websockets-14.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4c8cef610e8d7c70dea92e62b6814a8cd24fbd01d7103cc89308d2bfe1659ef"}, + {file = "websockets-14.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d7d9cafbccba46e768be8a8ad4635fa3eae1ffac4c6e7cb4eb276ba41297ed29"}, + {file = "websockets-14.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c76193c1c044bd1e9b3316dcc34b174bbf9664598791e6fb606d8d29000e070c"}, + {file = "websockets-14.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd475a974d5352390baf865309fe37dec6831aafc3014ffac1eea99e84e83fc2"}, + {file = "websockets-14.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c6c0097a41968b2e2b54ed3424739aab0b762ca92af2379f152c1aef0187e1c"}, + {file = "websockets-14.2-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d7ff794c8b36bc402f2e07c0b2ceb4a2424147ed4785ff03e2a7af03711d60a"}, + {file = "websockets-14.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dec254fcabc7bd488dab64846f588fc5b6fe0d78f641180030f8ea27b76d72c3"}, + {file = "websockets-14.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:bbe03eb853e17fd5b15448328b4ec7fb2407d45fb0245036d06a3af251f8e48f"}, + {file = "websockets-14.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a3c4aa3428b904d5404a0ed85f3644d37e2cb25996b7f096d77caeb0e96a3b42"}, + {file = "websockets-14.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:577a4cebf1ceaf0b65ffc42c54856214165fb8ceeba3935852fc33f6b0c55e7f"}, + {file = "websockets-14.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad1c1d02357b7665e700eca43a31d52814ad9ad9b89b58118bdabc365454b574"}, + {file = "websockets-14.2-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f390024a47d904613577df83ba700bd189eedc09c57af0a904e5c39624621270"}, + {file = "websockets-14.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3c1426c021c38cf92b453cdf371228d3430acd775edee6bac5a4d577efc72365"}, + {file = "websockets-14.2-py3-none-any.whl", hash = "sha256:7a6ceec4ea84469f15cf15807a747e9efe57e369c384fa86e022b3bea679b79b"}, + {file = "websockets-14.2.tar.gz", hash = "sha256:5059ed9c54945efb321f097084b4c7e52c246f2c869815876a69d1efc4ad6eb5"}, +] + +[[package]] +name = "win32-setctime" +version = "1.2.0" +description = "A small Python utility to set file creation time on Windows" +optional = false +python-versions = ">=3.5" +groups = ["main"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390"}, + {file = "win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0"}, +] + +[package.extras] +dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] + +[[package]] +name = "wsproto" +version = "1.2.0" +description = "WebSockets state-machine based protocol implementation" +optional = false +python-versions = ">=3.7.0" +groups = ["main"] +files = [ + {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, + {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, +] + +[package.dependencies] +h11 = ">=0.9.0,<1" + +[[package]] +name = "yt-dlp" +version = "2025.1.26" +description = "A feature-rich command-line audio/video downloader" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "yt_dlp-2025.1.26-py3-none-any.whl", hash = "sha256:3e76bd896b9f96601021ca192ca0fbdd195e3c3dcc28302a3a34c9bc4979da7b"}, + {file = "yt_dlp-2025.1.26.tar.gz", hash = "sha256:1c9738266921ad43c568ad01ac3362fb7c7af549276fbec92bd72f140da16240"}, +] + +[package.extras] +build = ["build", "hatchling", "pip", "setuptools (>=71.0.2)", "wheel"] +curl-cffi = ["curl-cffi (==0.5.10)", "curl-cffi (>=0.5.10,!=0.6.*,<0.7.2)"] +default = ["brotli", "brotlicffi", "certifi", "mutagen", "pycryptodomex", "requests (>=2.32.2,<3)", "urllib3 (>=1.26.17,<3)", "websockets (>=13.0)"] +dev = ["autopep8 (>=2.0,<3.0)", "pre-commit", "pytest (>=8.1,<9.0)", "pytest-rerunfailures (>=14.0,<15.0)", "ruff (>=0.9.0,<0.10.0)"] +pyinstaller = ["pyinstaller (>=6.11.1)"] +secretstorage = ["cffi", "secretstorage"] +static-analysis = ["autopep8 (>=2.0,<3.0)", "ruff (>=0.9.0,<0.10.0)"] +test = ["pytest (>=8.1,<9.0)", "pytest-rerunfailures (>=14.0,<15.0)"] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.10,<3.13" +content-hash = "11d734f2ee32206214a7ecb8dc3ec8d19a7b6281ee98b509a5bb8bdb647c674a" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ea1c87d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,55 @@ +[tool.poetry] +package-mode = false + +[project] +name = "auto-archiver-api" +description = "API wrapper for Bellingcat's Auto Archiver, supports users, groups, sheet and url archives." +authors = [ + { name = "Bellingcat", email = "contact-tech@bellingcat.com" }, +] +license = {text = "MIT"} +readme = "README.md" +keywords = ["archive", "oosi", "osint", "scraping"] +classifiers = [ + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3" +] + +requires-python = ">=3.10,<3.13" + + +dependencies = [ + "auto-archiver (>=0.13.1)", + "oscrypto @ git+https://github.com/wbond/oscrypto.git@d5f3437ed24257895ae1edd9e503cfb352e635a8", + "celery (>=5.0)", + "redis (==3.5.3)", + "loguru (>=0.7.3,<0.8.0)", + "pydantic-settings (>=2.7.1,<3.0.0)", + "sqlalchemy (>=2.0.38,<3.0.0)", + "requests (>=2.25.1)", + "pyopenssl (>=23.3.0)", +] +[tool.poetry.group.worker.dependencies] +watchdog = ">=6.0.0,<7.0.0" +setuptools = "^75.8.0" + +[tool.poetry.group.web.dependencies] +fastapi = ">=0.115.8,<0.116.0" +requests = ">=2.32.3,<3.0.0" +aiosqlite = ">=0.21.0,<0.22.0" +alembic = ">=1.14.1,<2.0.0" +fastapi-utils = ">=0.8.0,<0.9.0" +prometheus-fastapi-instrumentator = ">=7.0.2,<8.0.0" +fastapi-mail = ">=1.4.2,<2.0.0" +uvicorn = ">=0.13.4" +pyyaml = "^6.0.2" + + +[tool.poetry.group.dev.dependencies] +pytest = ">=8.3.4,<9.0.0" +httpx = ">=0.28.1,<0.29.0" +coverage = ">=7.6.11,<8.0.0" +pytest-asyncio = ">=0.25.3,<0.26.0" + diff --git a/src/.example.env b/src/.example.env deleted file mode 100644 index a6eedd4..0000000 --- a/src/.example.env +++ /dev/null @@ -1,7 +0,0 @@ -DATABASE_PATH="sqlite:///./auto-archiver.db" -USER_GROUPS_FILENAME=user-groups.yaml -CHROME_APP_IDS=000000000000000000000000000000000000000000000.apps.googleusercontent.com,000000000000000000000000000000000000000000001.apps.googleusercontent.com -#ALLOWED_ORIGINS="http://localhost:8004" # dev only - - -API_BEARER_TOKEN=TODO \ No newline at end of file diff --git a/src/Dockerfile b/src/Dockerfile deleted file mode 100644 index 5bab89e..0000000 --- a/src/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -# From python:3.10 -FROM bellingcat/auto-archiver - -# set work directory -WORKDIR /app - -RUN curl -fsSL https://get.docker.com -o get-docker.sh && \ - sh get-docker.sh -# set environment variables -ENV PYTHONUNBUFFERED=1 -ENV PYTHONDONTWRITEBYTECODE=1 - -# install dependencies -RUN pip install --upgrade pip && \ - apt-get update -COPY Pipfile* ./ -RUN pipenv install - -# copy src code over -COPY . . - -ENTRYPOINT ["pipenv", "run"] \ No newline at end of file diff --git a/src/Pipfile b/src/Pipfile deleted file mode 100644 index dabe443..0000000 --- a/src/Pipfile +++ /dev/null @@ -1,32 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -aiofiles = "==0.6.0" -celery = ">=5.0" -fastapi = "*" -jinja2 = "*" -redis = "==3.5.3" -requests = ">=2.25.1" -uvicorn = ">=0.13.4" -aiosqlite = "*" -python-dotenv = "*" -loguru = "*" -sqlalchemy = "*" -alembic = "*" -fastapi-utils = "*" -prometheus-fastapi-instrumentator = "*" -auto-archiver = "*" -pydantic-settings = "*" - -[dev-packages] -watchdog = "*" -pytest = "*" -httpx = "*" -coverage = "*" -pytest-asyncio = "*" - -[requires] -python_version = "3.10" diff --git a/src/Pipfile.lock b/src/Pipfile.lock deleted file mode 100644 index 840330c..0000000 --- a/src/Pipfile.lock +++ /dev/null @@ -1,3517 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "da25332a2152541157c6873ec43ac771c5491bff7d60bb2714c26c4e6b40577f" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.10" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "aiofiles": { - "hashes": [ - "sha256:bd3019af67f83b739f8e4053c6c0512a7f545b9a8d91aaeab55e6e0f9d123c27", - "sha256:e0281b157d3d5d59d803e3f4557dcc9a3dff28a4dd4829a9ff478adae50ca092" - ], - "index": "pypi", - "version": "==0.6.0" - }, - "aiohttp": { - "hashes": [ - "sha256:02ab6006ec3c3463b528374c4cdce86434e7b89ad355e7bf29e2f16b46c7dd6f", - "sha256:04fa38875e53eb7e354ece1607b1d2fdee2d175ea4e4d745f6ec9f751fe20c7c", - "sha256:0b0a6a36ed7e164c6df1e18ee47afbd1990ce47cb428739d6c99aaabfaf1b3af", - "sha256:0d406b01a9f5a7e232d1b0d161b40c05275ffbcbd772dc18c1d5a570961a1ca4", - "sha256:0e49b08eafa4f5707ecfb321ab9592717a319e37938e301d462f79b4e860c32a", - "sha256:0e7ba7ff228c0d9a2cd66194e90f2bca6e0abca810b786901a569c0de082f489", - "sha256:11cb254e397a82efb1805d12561e80124928e04e9c4483587ce7390b3866d213", - "sha256:11ff168d752cb41e8492817e10fb4f85828f6a0142b9726a30c27c35a1835f01", - "sha256:176df045597e674fa950bf5ae536be85699e04cea68fa3a616cf75e413737eb5", - "sha256:219a16763dc0294842188ac8a12262b5671817042b35d45e44fd0a697d8c8361", - "sha256:22698f01ff5653fe66d16ffb7658f582a0ac084d7da1323e39fd9eab326a1f26", - "sha256:237533179d9747080bcaad4d02083ce295c0d2eab3e9e8ce103411a4312991a0", - "sha256:289ba9ae8e88d0ba16062ecf02dd730b34186ea3b1e7489046fc338bdc3361c4", - "sha256:2c59e0076ea31c08553e868cec02d22191c086f00b44610f8ab7363a11a5d9d8", - "sha256:2c9376e2b09895c8ca8b95362283365eb5c03bdc8428ade80a864160605715f1", - "sha256:3135713c5562731ee18f58d3ad1bf41e1d8883eb68b363f2ffde5b2ea4b84cc7", - "sha256:3b9c7426923bb7bd66d409da46c41e3fb40f5caf679da624439b9eba92043fa6", - "sha256:3c0266cd6f005e99f3f51e583012de2778e65af6b73860038b968a0a8888487a", - "sha256:41473de252e1797c2d2293804e389a6d6986ef37cbb4a25208de537ae32141dd", - "sha256:4831df72b053b1eed31eb00a2e1aff6896fb4485301d4ccb208cac264b648db4", - "sha256:49f0c1b3c2842556e5de35f122fc0f0b721334ceb6e78c3719693364d4af8499", - "sha256:4b4c452d0190c5a820d3f5c0f3cd8a28ace48c54053e24da9d6041bf81113183", - "sha256:4ee8caa925aebc1e64e98432d78ea8de67b2272252b0a931d2ac3bd876ad5544", - "sha256:500f1c59906cd142d452074f3811614be04819a38ae2b3239a48b82649c08821", - "sha256:5216b6082c624b55cfe79af5d538e499cd5f5b976820eac31951fb4325974501", - "sha256:54311eb54f3a0c45efb9ed0d0a8f43d1bc6060d773f6973efd90037a51cd0a3f", - "sha256:54631fb69a6e44b2ba522f7c22a6fb2667a02fd97d636048478db2fd8c4e98fe", - "sha256:565760d6812b8d78d416c3c7cfdf5362fbe0d0d25b82fed75d0d29e18d7fc30f", - "sha256:598db66eaf2e04aa0c8900a63b0101fdc5e6b8a7ddd805c56d86efb54eb66672", - "sha256:5c4fa235d534b3547184831c624c0b7c1e262cd1de847d95085ec94c16fddcd5", - "sha256:69985d50a2b6f709412d944ffb2e97d0be154ea90600b7a921f95a87d6f108a2", - "sha256:69da0f3ed3496808e8cbc5123a866c41c12c15baaaead96d256477edf168eb57", - "sha256:6c93b7c2e52061f0925c3382d5cb8980e40f91c989563d3d32ca280069fd6a87", - "sha256:70907533db712f7aa791effb38efa96f044ce3d4e850e2d7691abd759f4f0ae0", - "sha256:81b77f868814346662c96ab36b875d7814ebf82340d3284a31681085c051320f", - "sha256:82eefaf1a996060602f3cc1112d93ba8b201dbf5d8fd9611227de2003dddb3b7", - "sha256:85c3e3c9cb1d480e0b9a64c658cd66b3cfb8e721636ab8b0e746e2d79a7a9eed", - "sha256:8a22a34bc594d9d24621091d1b91511001a7eea91d6652ea495ce06e27381f70", - "sha256:8cef8710fb849d97c533f259103f09bac167a008d7131d7b2b0e3a33269185c0", - "sha256:8d44e7bf06b0c0a70a20f9100af9fcfd7f6d9d3913e37754c12d424179b4e48f", - "sha256:8d7f98fde213f74561be1d6d3fa353656197f75d4edfbb3d94c9eb9b0fc47f5d", - "sha256:8d8e4450e7fe24d86e86b23cc209e0023177b6d59502e33807b732d2deb6975f", - "sha256:8fc49a87ac269d4529da45871e2ffb6874e87779c3d0e2ccd813c0899221239d", - "sha256:90ec72d231169b4b8d6085be13023ece8fa9b1bb495e4398d847e25218e0f431", - "sha256:91c742ca59045dce7ba76cab6e223e41d2c70d79e82c284a96411f8645e2afff", - "sha256:9b05d33ff8e6b269e30a7957bd3244ffbce2a7a35a81b81c382629b80af1a8bf", - "sha256:9b05d5cbe9dafcdc733262c3a99ccf63d2f7ce02543620d2bd8db4d4f7a22f83", - "sha256:9c5857612c9813796960c00767645cb5da815af16dafb32d70c72a8390bbf690", - "sha256:a34086c5cc285be878622e0a6ab897a986a6e8bf5b67ecb377015f06ed316587", - "sha256:ab221850108a4a063c5b8a70f00dd7a1975e5a1713f87f4ab26a46e5feac5a0e", - "sha256:b796b44111f0cab6bbf66214186e44734b5baab949cb5fb56154142a92989aeb", - "sha256:b8c3a67eb87394386847d188996920f33b01b32155f0a94f36ca0e0c635bf3e3", - "sha256:bcb6532b9814ea7c5a6a3299747c49de30e84472fa72821b07f5a9818bce0f66", - "sha256:bcc0ea8d5b74a41b621ad4a13d96c36079c81628ccc0b30cfb1603e3dfa3a014", - "sha256:bea94403a21eb94c93386d559bce297381609153e418a3ffc7d6bf772f59cc35", - "sha256:bff7e2811814fa2271be95ab6e84c9436d027a0e59665de60edf44e529a42c1f", - "sha256:c72444d17777865734aa1a4d167794c34b63e5883abb90356a0364a28904e6c0", - "sha256:c7b5d5d64e2a14e35a9240b33b89389e0035e6de8dbb7ffa50d10d8b65c57449", - "sha256:c7e939f1ae428a86e4abbb9a7c4732bf4706048818dfd979e5e2839ce0159f23", - "sha256:c88a15f272a0ad3d7773cf3a37cc7b7d077cbfc8e331675cf1346e849d97a4e5", - "sha256:c9110c06eaaac7e1f5562caf481f18ccf8f6fdf4c3323feab28a93d34cc646bd", - "sha256:ca7ca5abfbfe8d39e653870fbe8d7710be7a857f8a8386fc9de1aae2e02ce7e4", - "sha256:cae4c0c2ca800c793cae07ef3d40794625471040a87e1ba392039639ad61ab5b", - "sha256:cdefe289681507187e375a5064c7599f52c40343a8701761c802c1853a504558", - "sha256:cf2a0ac0615842b849f40c4d7f304986a242f1e68286dbf3bd7a835e4f83acfd", - "sha256:cfeadf42840c1e870dc2042a232a8748e75a36b52d78968cda6736de55582766", - "sha256:d737e69d193dac7296365a6dcb73bbbf53bb760ab25a3727716bbd42022e8d7a", - "sha256:d7481f581251bb5558ba9f635db70908819caa221fc79ee52a7f58392778c636", - "sha256:df9cf74b9bc03d586fc53ba470828d7b77ce51b0582d1d0b5b2fb673c0baa32d", - "sha256:e1f80197f8b0b846a8d5cf7b7ec6084493950d0882cc5537fb7b96a69e3c8590", - "sha256:ecca113f19d5e74048c001934045a2b9368d77b0b17691d905af18bd1c21275e", - "sha256:ee2527134f95e106cc1653e9ac78846f3a2ec1004cf20ef4e02038035a74544d", - "sha256:f27fdaadce22f2ef950fc10dcdf8048407c3b42b73779e48a4e76b3c35bca26c", - "sha256:f694dc8a6a3112059258a725a4ebe9acac5fe62f11c77ac4dcf896edfa78ca28", - "sha256:f800164276eec54e0af5c99feb9494c295118fc10a11b997bbb1348ba1a52065", - "sha256:ffcd828e37dc219a72c9012ec44ad2e7e3066bec6ff3aaa19e7d435dbf4032ca" - ], - "markers": "python_version >= '3.8'", - "version": "==3.9.1" - }, - "aiosignal": { - "hashes": [ - "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc", - "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17" - ], - "markers": "python_version >= '3.7'", - "version": "==1.3.1" - }, - "aiosqlite": { - "hashes": [ - "sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6", - "sha256:6d35c8c256637f4672f843c31021464090805bf925385ac39473fb16eaaca3d7" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==0.20.0" - }, - "alabaster": { - "hashes": [ - "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", - "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92" - ], - "markers": "python_version >= '3.9'", - "version": "==0.7.16" - }, - "alembic": { - "hashes": [ - "sha256:203503117415561e203aa14541740643a611f641517f0209fcae63e9fa09f1a2", - "sha256:908e905976d15235fae59c9ac42c4c5b75cfcefe3d27c0fbf7ae15a37715d80e" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==1.13.3" - }, - "amqp": { - "hashes": [ - "sha256:827cb12fb0baa892aad844fd95258143bce4027fdac4fccddbc43330fd281637", - "sha256:a1ecff425ad063ad42a486c902807d1482311481c8ad95a72694b2975e75f7fd" - ], - "markers": "python_version >= '3.6'", - "version": "==5.2.0" - }, - "annotated-types": { - "hashes": [ - "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", - "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89" - ], - "markers": "python_version >= '3.8'", - "version": "==0.7.0" - }, - "anyio": { - "hashes": [ - "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94", - "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7" - ], - "markers": "python_version >= '3.8'", - "version": "==4.4.0" - }, - "argparse": { - "hashes": [ - "sha256:62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4", - "sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314" - ], - "version": "==1.4.0" - }, - "asn1crypto": { - "hashes": [ - "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c", - "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67" - ], - "version": "==1.5.1" - }, - "async-timeout": { - "hashes": [ - "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", - "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028" - ], - "markers": "python_version >= '3.7'", - "version": "==4.0.3" - }, - "attrs": { - "hashes": [ - "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", - "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" - ], - "markers": "python_version >= '3.7'", - "version": "==23.2.0" - }, - "authlib": { - "hashes": [ - "sha256:4b16130117f9eb82aa6eec97f6dd4673c3f960ac0283ccdae2897ee4bc030ba2", - "sha256:ede026a95e9f5cdc2d4364a52103f5405e75aa156357e831ef2bfd0bc5094dfc" - ], - "markers": "python_version >= '3.8'", - "version": "==1.3.2" - }, - "auto-archiver": { - "hashes": [ - "sha256:3cee45b9a17feba214503eb1be4e8552e40cadbba128964585e0f53a45966fc8", - "sha256:b9f1fb490fc268462325ec3f3c97c425a9c62dd0a2b4e58c771b64e8d29f0a87" - ], - "index": "pypi", - "markers": "python_version >= '3.10'", - "version": "==0.12.0" - }, - "babel": { - "hashes": [ - "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb", - "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413" - ], - "markers": "python_version >= '3.8'", - "version": "==2.15.0" - }, - "backports.tarfile": { - "hashes": [ - "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", - "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991" - ], - "markers": "python_version >= '3.8'", - "version": "==1.2.0" - }, - "beautifulsoup4": { - "hashes": [ - "sha256:7e05ad0b6c26108d9990e2235e8a9b4e2c03ead6f391ceb60347f8ebea6b80ba", - "sha256:c684ddec071aa120819889aa9e8940f85c3f3cdaa08e23b9fa26510387897bd5" - ], - "markers": "python_full_version >= '3.6.0'", - "version": "==4.13.0b2" - }, - "billiard": { - "hashes": [ - "sha256:12b641b0c539073fc8d3f5b8b7be998956665c4233c7c1fcd66a7e677c4fb36f", - "sha256:40b59a4ac8806ba2c2369ea98d876bc6108b051c227baffd928c644d15d8f3cb" - ], - "markers": "python_version >= '3.7'", - "version": "==4.2.1" - }, - "black": { - "hashes": [ - "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474", - "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1", - "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0", - "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8", - "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96", - "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1", - "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04", - "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021", - "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94", - "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d", - "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c", - "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7", - "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c", - "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc", - "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7", - "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d", - "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c", - "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741", - "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce", - "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb", - "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063", - "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e" - ], - "markers": "python_version >= '3.8'", - "version": "==24.4.2" - }, - "bleach": { - "hashes": [ - "sha256:1a1a85c1595e07d8db14c5f09f09e6433502c51c595970edc090551f0db99414", - "sha256:33c16e3353dbd13028ab4799a0f89a83f113405c766e9c122df8a06f5b85b3f4" - ], - "markers": "python_version >= '3.7'", - "version": "==6.0.0" - }, - "blinker": { - "hashes": [ - "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01", - "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83" - ], - "markers": "python_version >= '3.8'", - "version": "==1.8.2" - }, - "boto3": { - "hashes": [ - "sha256:18416d07b41e6094101a44f8b881047dcec6b846dad0b9f83b9bbf2f0cd93d07", - "sha256:7f8e8a252458d584d8cf7877c372c4f74ec103356eedf43d2dd9e479f47f3639" - ], - "markers": "python_version >= '3.8'", - "version": "==1.35.44" - }, - "botocore": { - "hashes": [ - "sha256:1fcd97b966ad8a88de4106fe1bd3bbd6d8dadabe99bbd4a6aadcf11cb6c66b39", - "sha256:55388e80624401d017a9a2b8109afd94814f7e666b53e28fce51375cfa8d9326" - ], - "markers": "python_version >= '3.8'", - "version": "==1.35.44" - }, - "brotli": { - "hashes": [ - "sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208", - "sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48", - "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354", - "sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419", - "sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a", - "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128", - "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c", - "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088", - "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9", - "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a", - "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3", - "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757", - "sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2", - "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438", - "sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578", - "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b", - "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b", - "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68", - "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0", - "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d", - "sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943", - "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd", - "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409", - "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28", - "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da", - "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50", - "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f", - "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0", - "sha256:3ee8a80d67a4334482d9712b8e83ca6b1d9bc7e351931252ebef5d8f7335a547", - "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180", - "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0", - "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d", - "sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a", - "sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb", - "sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112", - "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc", - "sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2", - "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265", - "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327", - "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95", - "sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec", - "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd", - "sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c", - "sha256:5e55da2c8724191e5b557f8e18943b1b4839b8efc3ef60d65985bcf6f587dd38", - "sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914", - "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0", - "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a", - "sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7", - "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368", - "sha256:6974f52a02321b36847cd19d1b8e381bf39939c21efd6ee2fc13a28b0d99348c", - "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0", - "sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f", - "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451", - "sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f", - "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8", - "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e", - "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248", - "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c", - "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91", - "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724", - "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7", - "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966", - "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9", - "sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97", - "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d", - "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5", - "sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf", - "sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac", - "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b", - "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951", - "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74", - "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648", - "sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60", - "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c", - "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1", - "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8", - "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d", - "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc", - "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61", - "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460", - "sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751", - "sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9", - "sha256:a7e53012d2853a07a4a79c00643832161a910674a893d296c9f1259859a289d2", - "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0", - "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1", - "sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474", - "sha256:aea440a510e14e818e67bfc4027880e2fb500c2ccb20ab21c7a7c8b5b4703d75", - "sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5", - "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f", - "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2", - "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f", - "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb", - "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6", - "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9", - "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111", - "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2", - "sha256:cb1dac1770878ade83f2ccdf7d25e494f05c9165f5246b46a621cc849341dc01", - "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467", - "sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619", - "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf", - "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408", - "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579", - "sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84", - "sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7", - "sha256:d342778ef319e1026af243ed0a07c97acf3bad33b9f29e7ae6a1f68fd083e90c", - "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284", - "sha256:d7702622a8b40c49bffb46e1e3ba2e81268d5c04a34f460978c6b5517a34dd52", - "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b", - "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59", - "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752", - "sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1", - "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80", - "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839", - "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0", - "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2", - "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3", - "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64", - "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089", - "sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643", - "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b", - "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e", - "sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985", - "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596", - "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2", - "sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064" - ], - "markers": "platform_python_implementation >= 'CPython'", - "version": "==1.1.0" - }, - "bs4": { - "hashes": [ - "sha256:a48685c58f50fe127722417bae83fe6badf500d54b55f7e39ffe43b798653925", - "sha256:abf8742c0805ef7f662dce4b51cca104cffe52b835238afc169142ab9b3fbccc" - ], - "version": "==0.0.2" - }, - "cachetools": { - "hashes": [ - "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292", - "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a" - ], - "markers": "python_version >= '3.7'", - "version": "==5.5.0" - }, - "celery": { - "hashes": [ - "sha256:369631eb580cf8c51a82721ec538684994f8277637edde2dfc0dacd73ed97f64", - "sha256:504a19140e8d3029d5acad88330c541d4c3f64c789d85f94756762d8bca7e706" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==5.4.0" - }, - "certifi": { - "hashes": [ - "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", - "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90" - ], - "markers": "python_version >= '3.6'", - "version": "==2024.7.4" - }, - "certvalidator": { - "hashes": [ - "sha256:77520b269f516d4fb0902998d5bd0eb3727fe153b659aa1cb828dcf12ea6b8de", - "sha256:922d141c94393ab285ca34338e18dd4093e3ae330b1f278e96c837cb62cffaad" - ], - "version": "==0.11.1" - }, - "cffi": { - "hashes": [ - "sha256:157cfe06e48356a7552e68cb73976a710f2620a5f9eb25a5fe7066cf71601b68", - "sha256:1c9f4df436f3780f2dbea2ff066cea0bb2f74425883bc5b098812768da2b34f7", - "sha256:1da24a9bf6fd9ab987a915887f0d3577d0a0b3946d582b776b380294dc5fce18", - "sha256:1db9f6fcf79e92ee2d193cd989dde4e1419193ff11eef4bcc00cb06293e22f4b", - "sha256:1fee79745f50734490d3358f9cb6578f57850bb61287256115dda2a513abe3c6", - "sha256:205051765f126c1480d1eaf6268c644262bae3ed610423f0783349f04e7f5a6b", - "sha256:22eac8f9c77df0899a6cd373d6a62da40644573a5e27982f7713bd2a9f0b0edf", - "sha256:2e5562c744d495f838dc0fbe9cd76cff27ebea0a2e747dd84dd8a7e47bcd3c8f", - "sha256:3113951a250b021d2092e870fe86cd4292a633a786f7ece67200663406409659", - "sha256:319ec248b55d34a49d7a43c48f2cf922b526e5ad2a3988887cc822a0c226b983", - "sha256:35bd512b1a16723b8c50665c3fe83c80789f7e5599c8f0721ef145552b6853e7", - "sha256:3745df375d5e66261295840fa219797251ff6a30afedfae650576ab2b10f43db", - "sha256:39b9131ed6c28f63132dee75d1fa5653436cb46fc7e6a097af29f32c7f5f8eca", - "sha256:3c4b0e03d0d9f3a31110994bf657076f3821ad1a88e2cdb7c3e43b4e4f96e7b0", - "sha256:3ea7190f834a5979e30bc4af334c031303a4f16f38216599645034751d683171", - "sha256:3f60cc0a65ac412887ba284c946242ed4e07065003b358a4d288334f6c2a54ed", - "sha256:475d2832950f9a65740aeb20d5baf6d84cf0d08a7063c8c6c407ec24cac41881", - "sha256:494abc4dc78792d210249127a75021049c7832468f9daa6e81ec0dfc1f55d9d0", - "sha256:4f17c3cfc4a7a53693bda38ac1631f30ceb2430f4a038550f5515728592ccd6f", - "sha256:58463f9a28f4357f4a07a94fbb0dca91486f6948f19a4971e0bedd6292ef0394", - "sha256:614afb2f32d5ea64a946643d798f3391d53bba868290e7433f4eaae7d1692e06", - "sha256:625eb8d8d377438cfbf64899e09969d20cd139019838a60644f05216f7c7767d", - "sha256:6a891c9e564527b4e65d65f87e3e989c3369329d04b39c49f279a91266287b85", - "sha256:6aff0256e080afb8964e091f94222c2808cdf7c5f13d58f88e799e2fbde53a9d", - "sha256:6bce1aa64c52c3cb0c7326dd81d1dc5a4831946b29721592983eb4ae80beb2ac", - "sha256:6df680dccdb5fcd257343532d5354c0059a6e5e4bc27b24a6a310cc51ba35a31", - "sha256:7249add87681d15f1a291e096f49350b28c44be958c5ca650d8c4dfbce3a458f", - "sha256:730a92dd144eb89f69c7b61ba4e6ac48ee6a01ba92f70c17e277c3e2c49b253d", - "sha256:752c6a06036a24b54936f488ad13b0a83b7d1e0f9fefbe3a4fc237676b1091cf", - "sha256:7953cd1968a8ea99482d7bfcf5bb9c56d56e91660b97ee940923394c8194d921", - "sha256:7e12962a21ba417611c7f9ae3e7f42d5354b68bf3c894af7796c171f6a965acf", - "sha256:84269088c987aa34045ee808b8a3c5f44397403f1afeff65429cd7c9e123dc01", - "sha256:85b997ce260a93010a72767c0f2f7c405524cada076792a9baad75cef435f293", - "sha256:8b77f45d5b938f8fa6d3087892458c57458f55a90410ce15c61585627930838b", - "sha256:8e7b261c3ea000b9a7c4fd40dd54ec3749d4592808025261d82e82f6457e8b7f", - "sha256:8fe736c2666e20090ae52af3b0297fb9273830f9d31f6041d7a8c7172fb6a566", - "sha256:94af5cfe8eb0d2742435458b8c8708aeb88f17fb48372bc4dacb87671e1ba867", - "sha256:98c7f31f55c4d0f9dba7da07bab8cd822cff6ac8dbea28ea8385e3a1e7074ac6", - "sha256:98e89b4eabb3f98c7882016cb4c498bded7882ad655f80d7a9d23043a1d12d43", - "sha256:98eaba1ed99a0a219cabe7d8bb716d9d87aeeb1b6f33792bcf84cc222c1a37b1", - "sha256:9b5cb07680e7d3c522733d14fbc0cac0660b597a2e33d8bbd305537b65eb6a51", - "sha256:9e39b8008534eedae1bde35d7cd5b71069f8aa7e6c079ae549a0de68299af43c", - "sha256:a23431415147e0c711742b4e273b362758e632bd11a1e676c58011f0ed96da42", - "sha256:a33648455eefb348b265bd10764833ab7d5f3811912a90dcefc00328d548da0d", - "sha256:a4b7e94db6e6bc2582fa540175384070edbd63c61103b182f57be3a958c0b7ad", - "sha256:a72748e56cd5edfc808c508da6e4b903225d1ed4c45463c28edf188ffea6d442", - "sha256:b3245d8073632f958cf239a198c0c3bed112a59d6ee2202e85367955b92794c6", - "sha256:b57fa5d8a1a2cc960613e0e578867d21a018f4405e9bad31c7b0af2b14004f2b", - "sha256:b6f35a638639298d4f9dca59db1f7568860ea179ace42318d658698850f2f540", - "sha256:b7cb4755dc605ac5f2cf0b00e4063fdc2ca474da7bdc473877f8b5cba133b43e", - "sha256:ba993bea9f3195dc2f8dd9e3739f97f41eac5d71f5804d1ef87ee1283a13a280", - "sha256:bf62263af2a3fadaf992775e0e555d657546dee30d3ca8a2ed1559c90006d46e", - "sha256:c207ccc9f2e459eab7952401dc9237e36d6b630b5020890736e6b18002a750f3", - "sha256:c82e1f580f3dd473a9d8b213071dfd8da07f7a433b04ba6be4773ada211d3fdb", - "sha256:ca0dd9cfb6a3fd91d6f1de5a2e2ee7a0f4b5b753309ec4edce32d5505dbc9149", - "sha256:cfc1d8a64c44544a01b06b1688dca70433dc47e2d46f462c9ee6dc02ab233ba8", - "sha256:d1089e9654cbbeb4e3ba84caa5eb0a92371fcac6ba43b14514680d339068abed", - "sha256:d50cef1600b59ec5669a28050286a456682443f20be9b0226c0fe5502860216e", - "sha256:e27ceb498d5a93f7fe833c5a3a85f8b9f0a4f1a182f1d37936e9ed31dda6926b", - "sha256:e3ae055e90ea13480185a1ef5325ebd9ac092e03f5f473be3e93eac62bfd43df", - "sha256:e547a347a983bda467ae8d8b607d278cdf8a37bea735399d655c82cba3f5d725", - "sha256:e6c686d93378b18a7b26bbb376dab75716a72bd95c04b7f2cff9094ac66a4582", - "sha256:ec95c379f5ebd92cd09e3e8183da9afee8c2da2544593fe091421ed2d757f3c1", - "sha256:f6e933e0118a97df454139ca84a28473a024429c7c1eb82619a56ef886b07583", - "sha256:f9155a5b35097cbe7a2e31611daf681b7119d895090bb101bf94805fb6bc7834", - "sha256:fa76f23281fd49c305002f510c773ecf6216118f2e7083b34ffa06983d6db96a", - "sha256:ffe885231b8b58f18149e9eaece2d556602aeb233161c069618bda31f3a30d04" - ], - "markers": "platform_python_implementation != 'PyPy'", - "version": "==1.17.0rc1" - }, - "charset-normalizer": { - "hashes": [ - "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", - "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", - "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", - "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", - "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", - "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", - "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", - "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", - "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", - "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", - "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", - "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", - "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", - "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", - "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", - "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", - "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", - "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", - "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", - "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", - "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", - "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", - "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", - "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", - "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", - "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", - "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", - "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", - "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", - "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", - "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", - "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", - "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", - "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", - "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", - "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", - "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", - "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", - "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", - "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", - "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", - "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", - "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", - "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", - "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", - "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", - "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", - "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", - "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", - "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", - "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", - "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", - "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", - "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", - "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", - "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", - "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", - "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", - "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", - "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", - "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", - "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", - "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", - "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", - "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", - "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", - "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", - "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", - "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", - "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", - "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", - "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", - "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", - "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", - "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", - "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", - "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", - "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", - "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", - "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", - "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", - "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", - "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", - "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", - "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", - "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", - "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", - "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", - "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", - "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" - ], - "markers": "python_full_version >= '3.7.0'", - "version": "==3.3.2" - }, - "click": { - "hashes": [ - "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", - "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" - ], - "markers": "python_version >= '3.7'", - "version": "==8.1.7" - }, - "click-didyoumean": { - "hashes": [ - "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463", - "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c" - ], - "markers": "python_full_version >= '3.6.2'", - "version": "==0.3.1" - }, - "click-plugins": { - "hashes": [ - "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b", - "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8" - ], - "version": "==1.1.1" - }, - "click-repl": { - "hashes": [ - "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9", - "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812" - ], - "markers": "python_version >= '3.6'", - "version": "==0.3.0" - }, - "cloudscraper": { - "hashes": [ - "sha256:429c6e8aa6916d5bad5c8a5eac50f3ea53c9ac22616f6cb21b18dcc71517d0d3", - "sha256:76f50ca529ed2279e220837befdec892626f9511708e200d48d5bb76ded679b0" - ], - "version": "==1.2.71" - }, - "colorama": { - "hashes": [ - "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", - "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", - "version": "==0.4.6" - }, - "commonmark": { - "hashes": [ - "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60", - "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9" - ], - "version": "==0.9.1" - }, - "coverage": { - "extras": [ - "toml" - ], - "hashes": [ - "sha256:0086cd4fc71b7d485ac93ca4239c8f75732c2ae3ba83f6be1c9be59d9e2c6382", - "sha256:01c322ef2bbe15057bc4bf132b525b7e3f7206f071799eb8aa6ad1940bcf5fb1", - "sha256:03cafe82c1b32b770a29fd6de923625ccac3185a54a5e66606da26d105f37dac", - "sha256:044a0985a4f25b335882b0966625270a8d9db3d3409ddc49a4eb00b0ef5e8cee", - "sha256:07ed352205574aad067482e53dd606926afebcb5590653121063fbf4e2175166", - "sha256:0d1b923fc4a40c5832be4f35a5dab0e5ff89cddf83bb4174499e02ea089daf57", - "sha256:0e7b27d04131c46e6894f23a4ae186a6a2207209a05df5b6ad4caee6d54a222c", - "sha256:1fad32ee9b27350687035cb5fdf9145bc9cf0a094a9577d43e909948ebcfa27b", - "sha256:289cc803fa1dc901f84701ac10c9ee873619320f2f9aff38794db4a4a0268d51", - "sha256:3c59105f8d58ce500f348c5b56163a4113a440dad6daa2294b5052a10db866da", - "sha256:46c3d091059ad0b9c59d1034de74a7f36dcfa7f6d3bde782c49deb42438f2450", - "sha256:482855914928c8175735a2a59c8dc5806cf7d8f032e4820d52e845d1f731dca2", - "sha256:49c76cdfa13015c4560702574bad67f0e15ca5a2872c6a125f6327ead2b731dd", - "sha256:4b03741e70fb811d1a9a1d75355cf391f274ed85847f4b78e35459899f57af4d", - "sha256:4bea27c4269234e06f621f3fac3925f56ff34bc14521484b8f66a580aacc2e7d", - "sha256:4d5fae0a22dc86259dee66f2cc6c1d3e490c4a1214d7daa2a93d07491c5c04b6", - "sha256:543ef9179bc55edfd895154a51792b01c017c87af0ebaae092720152e19e42ca", - "sha256:54dece71673b3187c86226c3ca793c5f891f9fc3d8aa183f2e3653da18566169", - "sha256:6379688fb4cfa921ae349c76eb1a9ab26b65f32b03d46bb0eed841fd4cb6afb1", - "sha256:65fa405b837060db569a61ec368b74688f429b32fa47a8929a7a2f9b47183713", - "sha256:6616d1c9bf1e3faea78711ee42a8b972367d82ceae233ec0ac61cc7fec09fa6b", - "sha256:6fe885135c8a479d3e37a7aae61cbd3a0fb2deccb4dda3c25f92a49189f766d6", - "sha256:7221f9ac9dad9492cecab6f676b3eaf9185141539d5c9689d13fd6b0d7de840c", - "sha256:76d5f82213aa78098b9b964ea89de4617e70e0d43e97900c2778a50856dac605", - "sha256:7792f0ab20df8071d669d929c75c97fecfa6bcab82c10ee4adb91c7a54055463", - "sha256:831b476d79408ab6ccfadaaf199906c833f02fdb32c9ab907b1d4aa0713cfa3b", - "sha256:9146579352d7b5f6412735d0f203bbd8d00113a680b66565e205bc605ef81bc6", - "sha256:9cc44bf0315268e253bf563f3560e6c004efe38f76db03a1558274a6e04bf5d5", - "sha256:a73d18625f6a8a1cbb11eadc1d03929f9510f4131879288e3f7922097a429f63", - "sha256:a8659fd33ee9e6ca03950cfdcdf271d645cf681609153f218826dd9805ab585c", - "sha256:a94925102c89247530ae1dab7dc02c690942566f22e189cbd53579b0693c0783", - "sha256:ad4567d6c334c46046d1c4c20024de2a1c3abc626817ae21ae3da600f5779b44", - "sha256:b2e16f4cd2bc4d88ba30ca2d3bbf2f21f00f382cf4e1ce3b1ddc96c634bc48ca", - "sha256:bbdf9a72403110a3bdae77948b8011f644571311c2fb35ee15f0f10a8fc082e8", - "sha256:beb08e8508e53a568811016e59f3234d29c2583f6b6e28572f0954a6b4f7e03d", - "sha256:c4cbe651f3904e28f3a55d6f371203049034b4ddbce65a54527a3f189ca3b390", - "sha256:c7b525ab52ce18c57ae232ba6f7010297a87ced82a2383b1afd238849c1ff933", - "sha256:ca5d79cfdae420a1d52bf177de4bc2289c321d6c961ae321503b2ca59c17ae67", - "sha256:cdab02a0a941af190df8782aafc591ef3ad08824f97850b015c8c6a8b3877b0b", - "sha256:d17c6a415d68cfe1091d3296ba5749d3d8696e42c37fca5d4860c5bf7b729f03", - "sha256:d39bd10f0ae453554798b125d2f39884290c480f56e8a02ba7a6ed552005243b", - "sha256:d4b3cd1ca7cd73d229487fa5caca9e4bc1f0bca96526b922d61053ea751fe791", - "sha256:d50a252b23b9b4dfeefc1f663c568a221092cbaded20a05a11665d0dbec9b8fb", - "sha256:da8549d17489cd52f85a9829d0e1d91059359b3c54a26f28bec2c5d369524807", - "sha256:dcd070b5b585b50e6617e8972f3fbbee786afca71b1936ac06257f7e178f00f6", - "sha256:ddaaa91bfc4477d2871442bbf30a125e8fe6b05da8a0015507bfbf4718228ab2", - "sha256:df423f351b162a702c053d5dddc0fc0ef9a9e27ea3f449781ace5f906b664428", - "sha256:dff044f661f59dace805eedb4a7404c573b6ff0cdba4a524141bc63d7be5c7fd", - "sha256:e7e128f85c0b419907d1f38e616c4f1e9f1d1b37a7949f44df9a73d5da5cd53c", - "sha256:ed8d1d1821ba5fc88d4a4f45387b65de52382fa3ef1f0115a4f7a20cdfab0e94", - "sha256:f2501d60d7497fd55e391f423f965bbe9e650e9ffc3c627d5f0ac516026000b8", - "sha256:f7db0b6ae1f96ae41afe626095149ecd1b212b424626175a6633c2999eaad45b" - ], - "markers": "python_version >= '3.8'", - "version": "==7.6.0" - }, - "cryptography": { - "hashes": [ - "sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad", - "sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583", - "sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b", - "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c", - "sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1", - "sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648", - "sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949", - "sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba", - "sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c", - "sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9", - "sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d", - "sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c", - "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e", - "sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2", - "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d", - "sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7", - "sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70", - "sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2", - "sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7", - "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14", - "sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe", - "sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e", - "sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71", - "sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961", - "sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7", - "sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c", - "sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28", - "sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842", - "sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902", - "sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801", - "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a", - "sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e" - ], - "markers": "python_version >= '3.7'", - "version": "==42.0.8" - }, - "dataclasses-json": { - "hashes": [ - "sha256:5ec6fed642adb1dbdb4182badb01e0861badfd8fda82e3b67f44b2d1e9d10d21", - "sha256:d82896a94c992ffaf689cd1fafc180164e2abdd415b8f94a7f78586af5886236" - ], - "markers": "python_version < '3.13' and python_version >= '3.7'", - "version": "==0.5.14" - }, - "dateparser": { - "hashes": [ - "sha256:0b21ad96534e562920a0083e97fd45fa959882d4162acc358705144520a35830", - "sha256:7975b43a4222283e0ae15be7b4999d08c9a70e2d378ac87385b1ccf2cffbbb30" - ], - "markers": "python_version >= '3.7'", - "version": "==1.2.0" - }, - "docutils": { - "hashes": [ - "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c", - "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==0.18.1" - }, - "exceptiongroup": { - "hashes": [ - "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", - "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc" - ], - "markers": "python_version >= '3.7'", - "version": "==1.2.2" - }, - "fastapi": { - "hashes": [ - "sha256:3995739e0b09fa12f984bce8fa9ae197b35d433750d3d312422d846e283697ee", - "sha256:61704c71286579cc5a598763905928f24ee98bfcc07aabe84cfefb98812bbc86" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==0.115.2" - }, - "fastapi-utils": { - "hashes": [ - "sha256:074509405b02e2651dfe2d11862dd760bacc1a64508f3d8cc44e52a6dc1ed342", - "sha256:4fc4d6a10b5c5c3f2ec564d360fc1188507b911e4b06ee4d4c111906d7ddeef1" - ], - "index": "pypi", - "markers": "python_version >= '3.7' and python_version < '4.0'", - "version": "==0.7.0" - }, - "ffmpeg-python": { - "hashes": [ - "sha256:65225db34627c578ef0e11c8b1eb528bb35e024752f6f10b78c011f6f64c4127", - "sha256:ac441a0404e053f8b6a1113a77c0f452f1cfc62f6344a769475ffdc0f56c23c5" - ], - "version": "==0.2.0" - }, - "filelock": { - "hashes": [ - "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", - "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435" - ], - "markers": "python_version >= '3.8'", - "version": "==3.16.1" - }, - "flake8": { - "hashes": [ - "sha256:2e416edcc62471a64cea09353f4e7bdba32aeb079b6e360554c659a122b1bc6a", - "sha256:48a07b626b55236e0fb4784ee69a465fbf59d79eec1f5b4785c3d3bc57d17aa5" - ], - "markers": "python_full_version >= '3.8.1'", - "version": "==7.1.0" - }, - "flask": { - "hashes": [ - "sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3", - "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842" - ], - "markers": "python_version >= '3.8'", - "version": "==3.0.3" - }, - "frozenlist": { - "hashes": [ - "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7", - "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98", - "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad", - "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5", - "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae", - "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e", - "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a", - "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701", - "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d", - "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6", - "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6", - "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106", - "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75", - "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868", - "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a", - "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0", - "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1", - "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826", - "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec", - "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6", - "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950", - "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19", - "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0", - "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8", - "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a", - "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09", - "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86", - "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c", - "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5", - "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b", - "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b", - "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d", - "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0", - "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea", - "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776", - "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a", - "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897", - "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7", - "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09", - "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9", - "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe", - "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd", - "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742", - "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09", - "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0", - "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932", - "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1", - "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a", - "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49", - "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d", - "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7", - "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480", - "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89", - "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e", - "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b", - "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82", - "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb", - "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068", - "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8", - "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b", - "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb", - "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2", - "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11", - "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b", - "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc", - "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0", - "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497", - "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17", - "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0", - "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2", - "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439", - "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5", - "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac", - "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825", - "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887", - "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced", - "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74" - ], - "markers": "python_version >= '3.8'", - "version": "==1.4.1" - }, - "furo": { - "hashes": [ - "sha256:4ab2be254a2d5e52792d0ca793a12c35582dd09897228a6dd47885dabd5c9521", - "sha256:b99e7867a5cc833b2b34d7230631dd6558c7a29f93071fdbb5709634bb33c5a5" - ], - "markers": "python_version >= '3.7'", - "version": "==2023.3.27" - }, - "future": { - "hashes": [ - "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216", - "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.0.0" - }, - "google-api-core": { - "hashes": [ - "sha256:4a152fd11a9f774ea606388d423b68aa7e6d6a0ffe4c8266f74979613ec09f81", - "sha256:6869eacb2a37720380ba5898312af79a4d30b8bca1548fb4093e0697dc4bdf5d" - ], - "markers": "python_version >= '3.7'", - "version": "==2.21.0" - }, - "google-api-python-client": { - "hashes": [ - "sha256:1a5232e9cfed8c201799d9327e4d44dc7ea7daa3c6e1627fca41aa201539c0da", - "sha256:b9d68c6b14ec72580d66001bd33c5816b78e2134b93ccc5cf8f624516b561750" - ], - "markers": "python_version >= '3.7'", - "version": "==2.149.0" - }, - "google-auth": { - "hashes": [ - "sha256:25df55f327ef021de8be50bad0dfd4a916ad0de96da86cd05661c9297723ad3f", - "sha256:f4c64ed4e01e8e8b646ef34c018f8bf3338df0c8e37d8b3bba40e7f574a3278a" - ], - "markers": "python_version >= '3.7'", - "version": "==2.35.0" - }, - "google-auth-httplib2": { - "hashes": [ - "sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05", - "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d" - ], - "version": "==0.2.0" - }, - "google-auth-oauthlib": { - "hashes": [ - "sha256:2d58a27262d55aa1b87678c3ba7142a080098cbc2024f903c62355deb235d91f", - "sha256:afd0cad092a2eaa53cd8e8298557d6de1034c6cb4a740500b5357b648af97263" - ], - "markers": "python_version >= '3.6'", - "version": "==1.2.1" - }, - "googleapis-common-protos": { - "hashes": [ - "sha256:2972e6c496f435b92590fd54045060867f3fe9be2c82ab148fc8885035479a63", - "sha256:334a29d07cddc3aa01dee4988f9afd9b2916ee2ff49d6b757155dc0d197852c0" - ], - "markers": "python_version >= '3.7'", - "version": "==1.65.0" - }, - "greenlet": { - "hashes": [ - "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", - "sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7", - "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", - "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", - "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", - "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563", - "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83", - "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", - "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", - "sha256:1d3755bcb2e02de341c55b4fca7a745a24a9e7212ac953f6b3a48d117d7257aa", - "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", - "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", - "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", - "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22", - "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9", - "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0", - "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba", - "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3", - "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", - "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", - "sha256:47da355d8687fd65240c364c90a31569a133b7b60de111c255ef5b606f2ae291", - "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", - "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", - "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", - "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", - "sha256:54558ea205654b50c438029505def3834e80f0869a70fb15b871c29b4575ddef", - "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c", - "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", - "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c", - "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", - "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", - "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8", - "sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d", - "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", - "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145", - "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80", - "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", - "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e", - "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", - "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1", - "sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef", - "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc", - "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", - "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120", - "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437", - "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd", - "sha256:98884ecf2ffb7d7fe6bd517e8eb99d31ff7855a840fa6d0d63cd07c037f6a981", - "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", - "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", - "sha256:a0dfc6c143b519113354e780a50381508139b07d2177cb6ad6a08278ec655798", - "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7", - "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", - "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", - "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", - "sha256:b8da394b34370874b4572676f36acabac172602abf054cbc4ac910219f3340af", - "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", - "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", - "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42", - "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e", - "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81", - "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", - "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617", - "sha256:db32b5348615a04b82240cc67983cb315309e88d444a288934ee6ceaebcad6cc", - "sha256:dcc62f31eae24de7f8dce72134c8651c58000d3b1868e01392baea7c32c247de", - "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111", - "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", - "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", - "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6", - "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", - "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", - "sha256:f1d4aeb8891338e60d1ab6127af1fe45def5259def8094b9c7e34690c8858803", - "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", - "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f" - ], - "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')))))", - "version": "==3.1.1" - }, - "gspread": { - "hashes": [ - "sha256:cf03627f44e9e03a0a3de241d1748709db22af4fc8c11a13aa389d0bce6053fd", - "sha256:d3b45ea70db9723ac04259bf2650881b0568b943fd04a7f161e88d97ab21bd29" - ], - "markers": "python_version >= '3.8'", - "version": "==6.1.3" - }, - "h11": { - "hashes": [ - "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", - "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761" - ], - "markers": "python_version >= '3.7'", - "version": "==0.14.0" - }, - "httpcore": { - "hashes": [ - "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f", - "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f" - ], - "markers": "python_version >= '3.8'", - "version": "==1.0.6" - }, - "httplib2": { - "hashes": [ - "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc", - "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.22.0" - }, - "httpx": { - "hashes": [ - "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", - "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2" - ], - "markers": "python_version >= '3.8'", - "version": "==0.27.2" - }, - "idna": { - "hashes": [ - "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", - "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" - ], - "markers": "python_version >= '3.5'", - "version": "==3.7" - }, - "imagesize": { - "hashes": [ - "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", - "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.4.1" - }, - "importlib-metadata": { - "hashes": [ - "sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f", - "sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812" - ], - "markers": "python_version >= '3.8'", - "version": "==8.0.0" - }, - "iniconfig": { - "hashes": [ - "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", - "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" - ], - "markers": "python_version >= '3.7'", - "version": "==2.0.0" - }, - "instaloader": { - "hashes": [ - "sha256:36774ea1076eeb236f8782d221e3737f71ddc023042f0b13761429ef137f1133" - ], - "markers": "python_version >= '3.8'", - "version": "==4.13.1" - }, - "isort": { - "hashes": [ - "sha256:0ec8b74806e80fec33e6e7ba89d35e17b3eb1c4c74316ea44cf877cc26e8b118", - "sha256:cde11e804641edbe1b6b95d56582eb541f27eebc77864c6015545944bb0e9c76" - ], - "markers": "python_full_version >= '3.7.0'", - "version": "==6.0.0b2" - }, - "itsdangerous": { - "hashes": [ - "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", - "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173" - ], - "markers": "python_version >= '3.8'", - "version": "==2.2.0" - }, - "jaraco.classes": { - "hashes": [ - "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", - "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790" - ], - "markers": "python_version >= '3.8'", - "version": "==3.4.0" - }, - "jaraco.context": { - "hashes": [ - "sha256:3e16388f7da43d384a1a7cd3452e72e14732ac9fe459678773a3608a812bf266", - "sha256:c2f67165ce1f9be20f32f650f25d8edfc1646a8aeee48ae06fb35f90763576d2" - ], - "markers": "python_version >= '3.8'", - "version": "==5.3.0" - }, - "jaraco.functools": { - "hashes": [ - "sha256:3b24ccb921d6b593bdceb56ce14799204f473976e2a9d4b15b04d0f2c2326664", - "sha256:d33fa765374c0611b52f8b3a795f8900869aa88c84769d4d1746cd68fb28c3e8" - ], - "markers": "python_version >= '3.8'", - "version": "==4.0.1" - }, - "jeepney": { - "hashes": [ - "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806", - "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755" - ], - "markers": "python_version >= '3.7'", - "version": "==0.8.0" - }, - "jinja2": { - "hashes": [ - "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", - "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d" - ], - "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==3.1.4" - }, - "jmespath": { - "hashes": [ - "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", - "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" - ], - "markers": "python_version >= '3.7'", - "version": "==1.0.1" - }, - "jsonlines": { - "hashes": [ - "sha256:0c6d2c09117550c089995247f605ae4cf77dd1533041d366351f6f298822ea74", - "sha256:185b334ff2ca5a91362993f42e83588a360cf95ce4b71a73548502bda52a7c55" - ], - "markers": "python_version >= '3.8'", - "version": "==4.0.0" - }, - "keyring": { - "hashes": [ - "sha256:2458681cdefc0dbc0b7eb6cf75d0b98e59f9ad9b2d4edd319d18f68bdca95e50", - "sha256:daaffd42dbda25ddafb1ad5fec4024e5bbcfe424597ca1ca452b299861e49f1b" - ], - "markers": "python_version >= '3.8'", - "version": "==25.2.1" - }, - "kombu": { - "hashes": [ - "sha256:14212f5ccf022fc0a70453bb025a1dcc32782a588c49ea866884047d66e14763", - "sha256:eef572dd2fd9fc614b37580e3caeafdd5af46c1eff31e7fba89138cdb406f2cf" - ], - "markers": "python_version >= '3.8'", - "version": "==5.4.2" - }, - "livereload": { - "hashes": [ - "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869", - "sha256:ad4ac6f53b2d62bb6ce1a5e6e96f1f00976a32348afedcb4b6d68df2a1d346e4" - ], - "version": "==2.6.3" - }, - "loguru": { - "hashes": [ - "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb", - "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac" - ], - "index": "pypi", - "markers": "python_version >= '3.5'", - "version": "==0.7.2" - }, - "lxml": { - "hashes": [ - "sha256:01220dca0d066d1349bd6a1726856a78f7929f3878f7e2ee83c296c69495309e", - "sha256:02ced472497b8362c8e902ade23e3300479f4f43e45f4105c85ef43b8db85229", - "sha256:052d99051e77a4f3e8482c65014cf6372e61b0a6f4fe9edb98503bb5364cfee3", - "sha256:07da23d7ee08577760f0a71d67a861019103e4812c87e2fab26b039054594cc5", - "sha256:094cb601ba9f55296774c2d57ad68730daa0b13dc260e1f941b4d13678239e70", - "sha256:0a7056921edbdd7560746f4221dca89bb7a3fe457d3d74267995253f46343f15", - "sha256:0c120f43553ec759f8de1fee2f4794452b0946773299d44c36bfe18e83caf002", - "sha256:0d7b36afa46c97875303a94e8f3ad932bf78bace9e18e603f2085b652422edcd", - "sha256:0fdf3a3059611f7585a78ee10399a15566356116a4288380921a4b598d807a22", - "sha256:109fa6fede314cc50eed29e6e56c540075e63d922455346f11e4d7a036d2b8cf", - "sha256:146173654d79eb1fc97498b4280c1d3e1e5d58c398fa530905c9ea50ea849b22", - "sha256:1473427aff3d66a3fa2199004c3e601e6c4500ab86696edffdbc84954c72d832", - "sha256:1483fd3358963cc5c1c9b122c80606a3a79ee0875bcac0204149fa09d6ff2727", - "sha256:168f2dfcfdedf611eb285efac1516c8454c8c99caf271dccda8943576b67552e", - "sha256:17e8d968d04a37c50ad9c456a286b525d78c4a1c15dd53aa46c1d8e06bf6fa30", - "sha256:18feb4b93302091b1541221196a2155aa296c363fd233814fa11e181adebc52f", - "sha256:1afe0a8c353746e610bd9031a630a95bcfb1a720684c3f2b36c4710a0a96528f", - "sha256:1d04f064bebdfef9240478f7a779e8c5dc32b8b7b0b2fc6a62e39b928d428e51", - "sha256:1fdc9fae8dd4c763e8a31e7630afef517eab9f5d5d31a278df087f307bf601f4", - "sha256:1ffc23010330c2ab67fac02781df60998ca8fe759e8efde6f8b756a20599c5de", - "sha256:20094fc3f21ea0a8669dc4c61ed7fa8263bd37d97d93b90f28fc613371e7a875", - "sha256:213261f168c5e1d9b7535a67e68b1f59f92398dd17a56d934550837143f79c42", - "sha256:218c1b2e17a710e363855594230f44060e2025b05c80d1f0661258142b2add2e", - "sha256:23e0553b8055600b3bf4a00b255ec5c92e1e4aebf8c2c09334f8368e8bd174d6", - "sha256:25f1b69d41656b05885aa185f5fdf822cb01a586d1b32739633679699f220391", - "sha256:2b3778cb38212f52fac9fe913017deea2fdf4eb1a4f8e4cfc6b009a13a6d3fcc", - "sha256:2bc9fd5ca4729af796f9f59cd8ff160fe06a474da40aca03fcc79655ddee1a8b", - "sha256:2c226a06ecb8cdef28845ae976da407917542c5e6e75dcac7cc33eb04aaeb237", - "sha256:2c3406b63232fc7e9b8783ab0b765d7c59e7c59ff96759d8ef9632fca27c7ee4", - "sha256:2c86bf781b12ba417f64f3422cfc302523ac9cd1d8ae8c0f92a1c66e56ef2e86", - "sha256:2d9b8d9177afaef80c53c0a9e30fa252ff3036fb1c6494d427c066a4ce6a282f", - "sha256:2dec2d1130a9cda5b904696cec33b2cfb451304ba9081eeda7f90f724097300a", - "sha256:2dfab5fa6a28a0b60a20638dc48e6343c02ea9933e3279ccb132f555a62323d8", - "sha256:2ecdd78ab768f844c7a1d4a03595038c166b609f6395e25af9b0f3f26ae1230f", - "sha256:315f9542011b2c4e1d280e4a20ddcca1761993dda3afc7a73b01235f8641e903", - "sha256:36aef61a1678cb778097b4a6eeae96a69875d51d1e8f4d4b491ab3cfb54b5a03", - "sha256:384aacddf2e5813a36495233b64cb96b1949da72bef933918ba5c84e06af8f0e", - "sha256:3879cc6ce938ff4eb4900d901ed63555c778731a96365e53fadb36437a131a99", - "sha256:3c174dc350d3ec52deb77f2faf05c439331d6ed5e702fc247ccb4e6b62d884b7", - "sha256:3eb44520c4724c2e1a57c0af33a379eee41792595023f367ba3952a2d96c2aab", - "sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d", - "sha256:41ce1f1e2c7755abfc7e759dc34d7d05fd221723ff822947132dc934d122fe22", - "sha256:423b121f7e6fa514ba0c7918e56955a1d4470ed35faa03e3d9f0e3baa4c7e492", - "sha256:44264ecae91b30e5633013fb66f6ddd05c006d3e0e884f75ce0b4755b3e3847b", - "sha256:482c2f67761868f0108b1743098640fbb2a28a8e15bf3f47ada9fa59d9fe08c3", - "sha256:4b0c7a688944891086ba192e21c5229dea54382f4836a209ff8d0a660fac06be", - "sha256:4c1fefd7e3d00921c44dc9ca80a775af49698bbfd92ea84498e56acffd4c5469", - "sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f", - "sha256:501d0d7e26b4d261fca8132854d845e4988097611ba2531408ec91cf3fd9d20a", - "sha256:516f491c834eb320d6c843156440fe7fc0d50b33e44387fcec5b02f0bc118a4c", - "sha256:51806cfe0279e06ed8500ce19479d757db42a30fd509940b1701be9c86a5ff9a", - "sha256:562e7494778a69086f0312ec9689f6b6ac1c6b65670ed7d0267e49f57ffa08c4", - "sha256:56b9861a71575f5795bde89256e7467ece3d339c9b43141dbdd54544566b3b94", - "sha256:5b8f5db71b28b8c404956ddf79575ea77aa8b1538e8b2ef9ec877945b3f46442", - "sha256:5c2fb570d7823c2bbaf8b419ba6e5662137f8166e364a8b2b91051a1fb40ab8b", - "sha256:5c54afdcbb0182d06836cc3d1be921e540be3ebdf8b8a51ee3ef987537455f84", - "sha256:5d6a6972b93c426ace71e0be9a6f4b2cfae9b1baed2eed2006076a746692288c", - "sha256:609251a0ca4770e5a8768ff902aa02bf636339c5a93f9349b48eb1f606f7f3e9", - "sha256:62d172f358f33a26d6b41b28c170c63886742f5b6772a42b59b4f0fa10526cb1", - "sha256:62f7fdb0d1ed2065451f086519865b4c90aa19aed51081979ecd05a21eb4d1be", - "sha256:658f2aa69d31e09699705949b5fc4719cbecbd4a97f9656a232e7d6c7be1a367", - "sha256:65ab5685d56914b9a2a34d67dd5488b83213d680b0c5d10b47f81da5a16b0b0e", - "sha256:68934b242c51eb02907c5b81d138cb977b2129a0a75a8f8b60b01cb8586c7b21", - "sha256:68b87753c784d6acb8a25b05cb526c3406913c9d988d51f80adecc2b0775d6aa", - "sha256:69959bd3167b993e6e710b99051265654133a98f20cec1d9b493b931942e9c16", - "sha256:6a7095eeec6f89111d03dabfe5883a1fd54da319c94e0fb104ee8f23616b572d", - "sha256:6b038cc86b285e4f9fea2ba5ee76e89f21ed1ea898e287dc277a25884f3a7dfe", - "sha256:6ba0d3dcac281aad8a0e5b14c7ed6f9fa89c8612b47939fc94f80b16e2e9bc83", - "sha256:6e91cf736959057f7aac7adfc83481e03615a8e8dd5758aa1d95ea69e8931dba", - "sha256:6ee8c39582d2652dcd516d1b879451500f8db3fe3607ce45d7c5957ab2596040", - "sha256:6f651ebd0b21ec65dfca93aa629610a0dbc13dbc13554f19b0113da2e61a4763", - "sha256:71a8dd38fbd2f2319136d4ae855a7078c69c9a38ae06e0c17c73fd70fc6caad8", - "sha256:74068c601baff6ff021c70f0935b0c7bc528baa8ea210c202e03757c68c5a4ff", - "sha256:7437237c6a66b7ca341e868cda48be24b8701862757426852c9b3186de1da8a2", - "sha256:747a3d3e98e24597981ca0be0fd922aebd471fa99d0043a3842d00cdcad7ad6a", - "sha256:74bcb423462233bc5d6066e4e98b0264e7c1bed7541fff2f4e34fe6b21563c8b", - "sha256:78d9b952e07aed35fe2e1a7ad26e929595412db48535921c5013edc8aa4a35ce", - "sha256:7b1cd427cb0d5f7393c31b7496419da594fe600e6fdc4b105a54f82405e6626c", - "sha256:7d3d1ca42870cdb6d0d29939630dbe48fa511c203724820fc0fd507b2fb46577", - "sha256:7e2f58095acc211eb9d8b5771bf04df9ff37d6b87618d1cbf85f92399c98dae8", - "sha256:7f41026c1d64043a36fda21d64c5026762d53a77043e73e94b71f0521939cc71", - "sha256:81b4e48da4c69313192d8c8d4311e5d818b8be1afe68ee20f6385d0e96fc9512", - "sha256:86a6b24b19eaebc448dc56b87c4865527855145d851f9fc3891673ff97950540", - "sha256:874a216bf6afaf97c263b56371434e47e2c652d215788396f60477540298218f", - "sha256:89e043f1d9d341c52bf2af6d02e6adde62e0a46e6755d5eb60dc6e4f0b8aeca2", - "sha256:8c72e9563347c7395910de6a3100a4840a75a6f60e05af5e58566868d5eb2d6a", - "sha256:8dc2c0395bea8254d8daebc76dcf8eb3a95ec2a46fa6fae5eaccee366bfe02ce", - "sha256:8f0de2d390af441fe8b2c12626d103540b5d850d585b18fcada58d972b74a74e", - "sha256:92e67a0be1639c251d21e35fe74df6bcc40cba445c2cda7c4a967656733249e2", - "sha256:94d6c3782907b5e40e21cadf94b13b0842ac421192f26b84c45f13f3c9d5dc27", - "sha256:97acf1e1fd66ab53dacd2c35b319d7e548380c2e9e8c54525c6e76d21b1ae3b1", - "sha256:9ada35dd21dc6c039259596b358caab6b13f4db4d4a7f8665764d616daf9cc1d", - "sha256:9c52100e2c2dbb0649b90467935c4b0de5528833c76a35ea1a2691ec9f1ee7a1", - "sha256:9e41506fec7a7f9405b14aa2d5c8abbb4dbbd09d88f9496958b6d00cb4d45330", - "sha256:9e4b47ac0f5e749cfc618efdf4726269441014ae1d5583e047b452a32e221920", - "sha256:9fb81d2824dff4f2e297a276297e9031f46d2682cafc484f49de182aa5e5df99", - "sha256:a0eabd0a81625049c5df745209dc7fcef6e2aea7793e5f003ba363610aa0a3ff", - "sha256:a3d819eb6f9b8677f57f9664265d0a10dd6551d227afb4af2b9cd7bdc2ccbf18", - "sha256:a87de7dd873bf9a792bf1e58b1c3887b9264036629a5bf2d2e6579fe8e73edff", - "sha256:aa617107a410245b8660028a7483b68e7914304a6d4882b5ff3d2d3eb5948d8c", - "sha256:aac0bbd3e8dd2d9c45ceb82249e8bdd3ac99131a32b4d35c8af3cc9db1657179", - "sha256:ab6dd83b970dc97c2d10bc71aa925b84788c7c05de30241b9e96f9b6d9ea3080", - "sha256:ace2c2326a319a0bb8a8b0e5b570c764962e95818de9f259ce814ee666603f19", - "sha256:ae5fe5c4b525aa82b8076c1a59d642c17b6e8739ecf852522c6321852178119d", - "sha256:b11a5d918a6216e521c715b02749240fb07ae5a1fefd4b7bf12f833bc8b4fe70", - "sha256:b1c8c20847b9f34e98080da785bb2336ea982e7f913eed5809e5a3c872900f32", - "sha256:b369d3db3c22ed14c75ccd5af429086f166a19627e84a8fdade3f8f31426e52a", - "sha256:b710bc2b8292966b23a6a0121f7a6c51d45d2347edcc75f016ac123b8054d3f2", - "sha256:bd96517ef76c8654446fc3db9242d019a1bb5fe8b751ba414765d59f99210b79", - "sha256:c00f323cc00576df6165cc9d21a4c21285fa6b9989c5c39830c3903dc4303ef3", - "sha256:c162b216070f280fa7da844531169be0baf9ccb17263cf5a8bf876fcd3117fa5", - "sha256:c1a69e58a6bb2de65902051d57fde951febad631a20a64572677a1052690482f", - "sha256:c1f794c02903c2824fccce5b20c339a1a14b114e83b306ff11b597c5f71a1c8d", - "sha256:c24037349665434f375645fa9d1f5304800cec574d0310f618490c871fd902b3", - "sha256:c300306673aa0f3ed5ed9372b21867690a17dba38c68c44b287437c362ce486b", - "sha256:c56a1d43b2f9ee4786e4658c7903f05da35b923fb53c11025712562d5cc02753", - "sha256:c6379f35350b655fd817cd0d6cbeef7f265f3ae5fedb1caae2eb442bbeae9ab9", - "sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957", - "sha256:cb83f8a875b3d9b458cada4f880fa498646874ba4011dc974e071a0a84a1b033", - "sha256:cf120cce539453ae086eacc0130a324e7026113510efa83ab42ef3fcfccac7fb", - "sha256:dd36439be765e2dde7660212b5275641edbc813e7b24668831a5c8ac91180656", - "sha256:dd5350b55f9fecddc51385463a4f67a5da829bc741e38cf689f38ec9023f54ab", - "sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b", - "sha256:e63601ad5cd8f860aa99d109889b5ac34de571c7ee902d6812d5d9ddcc77fa7d", - "sha256:e92ce66cd919d18d14b3856906a61d3f6b6a8500e0794142338da644260595cd", - "sha256:e99f5507401436fdcc85036a2e7dc2e28d962550afe1cbfc07c40e454256a859", - "sha256:ea2e2f6f801696ad7de8aec061044d6c8c0dd4037608c7cab38a9a4d316bfb11", - "sha256:eafa2c8658f4e560b098fe9fc54539f86528651f61849b22111a9b107d18910c", - "sha256:ecd4ad8453ac17bc7ba3868371bffb46f628161ad0eefbd0a855d2c8c32dd81a", - "sha256:ee70d08fd60c9565ba8190f41a46a54096afa0eeb8f76bd66f2c25d3b1b83005", - "sha256:eec1bb8cdbba2925bedc887bc0609a80e599c75b12d87ae42ac23fd199445654", - "sha256:ef0c1fe22171dd7c7c27147f2e9c3e86f8bdf473fed75f16b0c2e84a5030ce80", - "sha256:f2901429da1e645ce548bf9171784c0f74f0718c3f6150ce166be39e4dd66c3e", - "sha256:f422a209d2455c56849442ae42f25dbaaba1c6c3f501d58761c619c7836642ec", - "sha256:f65e5120863c2b266dbcc927b306c5b78e502c71edf3295dfcb9501ec96e5fc7", - "sha256:f7d4a670107d75dfe5ad080bed6c341d18c4442f9378c9f58e5851e86eb79965", - "sha256:f914c03e6a31deb632e2daa881fe198461f4d06e57ac3d0e05bbcab8eae01945", - "sha256:fb66442c2546446944437df74379e9cf9e9db353e61301d1a0e26482f43f0dd8" - ], - "markers": "python_version >= '3.6'", - "version": "==5.3.0" - }, - "mako": { - "hashes": [ - "sha256:260f1dbc3a519453a9c856dedfe4beb4e50bd5a26d96386cb6c80856556bb91a", - "sha256:48dbc20568c1d276a2698b36d968fa76161bf127194907ea6fc594fa81f943bc" - ], - "markers": "python_version >= '3.8'", - "version": "==1.3.5" - }, - "markdown-it-py": { - "hashes": [ - "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30", - "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1" - ], - "markers": "python_version >= '3.7'", - "version": "==2.2.0" - }, - "markupsafe": { - "hashes": [ - "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", - "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", - "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", - "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", - "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", - "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", - "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", - "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df", - "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", - "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", - "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", - "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", - "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", - "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371", - "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2", - "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", - "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52", - "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", - "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", - "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", - "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", - "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", - "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", - "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", - "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", - "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", - "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", - "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", - "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", - "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9", - "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", - "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", - "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", - "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", - "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", - "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", - "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a", - "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", - "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", - "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", - "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", - "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", - "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", - "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", - "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", - "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f", - "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50", - "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", - "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", - "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", - "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", - "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", - "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", - "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", - "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf", - "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", - "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", - "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", - "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", - "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68" - ], - "markers": "python_version >= '3.7'", - "version": "==2.1.5" - }, - "marshmallow": { - "hashes": [ - "sha256:82f20a2397834fe6d9611b241f2f7e7b680ed89c49f84728a1ad937be6b4bdf4", - "sha256:98d8827a9f10c03d44ead298d2e99c6aea8197df18ccfad360dae7f89a50da2e" - ], - "markers": "python_version >= '3.9'", - "version": "==3.23.0" - }, - "mccabe": { - "hashes": [ - "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", - "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" - ], - "markers": "python_version >= '3.6'", - "version": "==0.7.0" - }, - "mdit-py-plugins": { - "hashes": [ - "sha256:ca9a0714ea59a24b2b044a1831f48d817dd0c817e84339f20e7889f392d77c4e", - "sha256:eee0adc7195e5827e17e02d2a258a2ba159944a0748f59c5099a4a27f78fcf6a" - ], - "markers": "python_version >= '3.7'", - "version": "==0.3.5" - }, - "mdurl": { - "hashes": [ - "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", - "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" - ], - "markers": "python_version >= '3.7'", - "version": "==0.1.2" - }, - "minify-html": { - "hashes": [ - "sha256:01ea40dc5ae073c47024f02758d5e18e55d853265eb9c099040a6c00ab0abb99", - "sha256:1056819ea46e9080db6fed678d03511c7e94c2a615e72df82190ea898dc82609", - "sha256:2a9aef71b24c3d38c6bece2db3bf707443894958b01f1c27d3a6459ba4200e59", - "sha256:3b38ea5b446cc69e691a0bf64d1160332ffc220bb5b411775983c87311cab2c7", - "sha256:40f38ddfefbb63beb28df20c2c81c12e6af6838387520506b4eceec807d794a3", - "sha256:597c86f9792437eee0698118fb38dff42b5b4be6d437b6d577453c2f91524ccc", - "sha256:5f707b233b9c163a546b15ce9af433ddd456bd113f0326e5ffb382b8ee5c1a2d", - "sha256:70251bd7174b62c91333110301b27000b547aa2cc06d4fe6ba6c3f11612eecc9", - "sha256:7a5eb7e830277762da69498ee0f15d4a9fa6e91887a93567d388e4f5aee01ec3", - "sha256:7af72438d3ae6ea8b0a94c038d35c9c22c5f8540967f5fa2487f77b2cdb12605", - "sha256:7b071ded7aacbb140a7e751d49e246052f204b896d69663a4a5c3a27203d27f6", - "sha256:7b2aadba6987e6c15a916a4627b94b1db3cbac65e6ae3613b61b3ab0d2bb4c96", - "sha256:7e6d4f97cebb725bc1075f225bdfcd824e0f5c20a37d9ea798d900f96e1b80c0", - "sha256:92375f0cb3b4074e45005e1b4708b5b4c0781b335659d52918671c083c19c71e", - "sha256:a23a8055e65fa01175ddd7d18d101c05e267410fa5956c65597dcc332c7f91dd", - "sha256:afd76ca2dc9afa53b66973a3a66eff9a64692811ead44102aa8044a37872e6e2", - "sha256:b6356541799951c5e8205aabf5970dda687f4ffa736479ce8df031919861e51d", - "sha256:bd682207673246c78fb895e7065425cc94cb712d94cff816dd9752ce014f23e8", - "sha256:cda674cc68ec3b9ebf61f2986f3ef62de60ce837a58860c6f16b011862b5d533", - "sha256:cf4c36b6f9af3b0901bd2a0a29db3b09c0cdf0c38d3dde28e6835bce0f605d37", - "sha256:d4c4ae3909e2896c865ebaa3a96939191f904dd337a87d7594130f3dfca55510", - "sha256:dc2df1e5203d89197f530d14c9a82067f3d04b9cb0118abc8f2ef8f88efce109", - "sha256:e47197849a1c09a95892d32df3c9e15f6d0902c9ae215e73249b9f5bca9aeb97", - "sha256:ea315ad6ac33d7463fac3f313bba8c8d9a55f4811971c203eed931203047e5c8", - "sha256:ef6dc1950e04b7566c1ece72712674416f86fef8966ca026f6c5580d840cd354", - "sha256:f37ce536305500914fd4ee2bbaa4dd05a039f39eeceae45560c39767d99aede0" - ], - "version": "==0.15.0" - }, - "more-itertools": { - "hashes": [ - "sha256:e5d93ef411224fbcef366a6e8ddc4c5781bc6359d43412a65dd5964e46111463", - "sha256:ea6a02e24a9161e51faad17a8782b92a0df82c12c1c8886fec7f0c3fa1a1b320" - ], - "markers": "python_version >= '3.8'", - "version": "==10.3.0" - }, - "multidict": { - "hashes": [ - "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9", - "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8", - "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03", - "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710", - "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161", - "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664", - "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569", - "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067", - "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313", - "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706", - "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2", - "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636", - "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49", - "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93", - "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603", - "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0", - "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60", - "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4", - "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e", - "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1", - "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60", - "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951", - "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc", - "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe", - "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95", - "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d", - "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8", - "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed", - "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2", - "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775", - "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87", - "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c", - "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2", - "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98", - "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3", - "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe", - "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78", - "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660", - "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176", - "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e", - "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988", - "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c", - "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c", - "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0", - "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449", - "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f", - "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde", - "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5", - "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d", - "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac", - "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a", - "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9", - "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca", - "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11", - "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35", - "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063", - "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b", - "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982", - "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258", - "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1", - "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52", - "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480", - "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7", - "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461", - "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d", - "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc", - "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779", - "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a", - "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547", - "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0", - "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171", - "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf", - "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d", - "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba" - ], - "markers": "python_version >= '3.7'", - "version": "==6.0.4" - }, - "mutagen": { - "hashes": [ - "sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99", - "sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719" - ], - "markers": "python_version >= '3.7'", - "version": "==1.47.0" - }, - "mypy": { - "hashes": [ - "sha256:0cd62192a4a32b77ceb31272d9e74d23cd88c8060c34d1d3622db3267679a5d9", - "sha256:1b3a2ffce52cc4dbaeee4df762f20a2905aa171ef157b82192f2e2f368eec05d", - "sha256:1f8f492d7db9e3593ef42d4f115f04e556130f2819ad33ab84551403e97dd4c0", - "sha256:2189ff1e39db399f08205e22a797383613ce1cb0cb3b13d8bcf0170e45b96cc3", - "sha256:378c03f53f10bbdd55ca94e46ec3ba255279706a6aacaecac52ad248f98205d3", - "sha256:37fd87cab83f09842653f08de066ee68f1182b9b5282e4634cdb4b407266bade", - "sha256:3c4c2992f6ea46ff7fce0072642cfb62af7a2484efe69017ed8b095f7b39ef31", - "sha256:51a46974340baaa4145363b9e051812a2446cf583dfaeba124af966fa44593f7", - "sha256:5bb9cd11c01c8606a9d0b83ffa91d0b236a0e91bc4126d9ba9ce62906ada868e", - "sha256:5cc3ca0a244eb9a5249c7c583ad9a7e881aa5d7b73c35652296ddcdb33b2b9c7", - "sha256:604282c886497645ffb87b8f35a57ec773a4a2721161e709a4422c1636ddde5c", - "sha256:6166a88b15f1759f94a46fa474c7b1b05d134b1b61fca627dd7335454cc9aa6b", - "sha256:6bacf8f3a3d7d849f40ca6caea5c055122efe70e81480c8328ad29c55c69e93e", - "sha256:6be84c06e6abd72f960ba9a71561c14137a583093ffcf9bbfaf5e613d63fa531", - "sha256:701b5f71413f1e9855566a34d6e9d12624e9e0a8818a5704d74d6b0402e66c04", - "sha256:71d8ac0b906354ebda8ef1673e5fde785936ac1f29ff6987c7483cfbd5a4235a", - "sha256:8addf6313777dbb92e9564c5d32ec122bf2c6c39d683ea64de6a1fd98b90fe37", - "sha256:901c89c2d67bba57aaaca91ccdb659aa3a312de67f23b9dfb059727cce2e2e0a", - "sha256:97a131ee36ac37ce9581f4220311247ab6cba896b4395b9c87af0675a13a755f", - "sha256:a1bbb3a6f5ff319d2b9d40b4080d46cd639abe3516d5a62c070cf0114a457d84", - "sha256:a2cbc68cb9e943ac0814c13e2452d2046c2f2b23ff0278e26599224cf164e78d", - "sha256:b8edd4e9bbbc9d7b79502eb9592cab808585516ae1bcc1446eb9122656c6066f", - "sha256:bd6f629b67bb43dc0d9211ee98b96d8dabc97b1ad38b9b25f5e4c4d7569a0c6a", - "sha256:c2ae450d60d7d020d67ab440c6e3fae375809988119817214440033f26ddf7bf", - "sha256:d8681909f7b44d0b7b86e653ca152d6dff0eb5eb41694e163c6092124f8246d7", - "sha256:e36f229acfe250dc660790840916eb49726c928e8ce10fbdf90715090fe4ae02", - "sha256:fe85ed6836165d52ae8b88f99527d3d1b2362e0cb90b005409b8bed90e9059b3" - ], - "markers": "python_version >= '3.8'", - "version": "==1.10.1" - }, - "mypy-extensions": { - "hashes": [ - "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", - "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" - ], - "markers": "python_version >= '3.5'", - "version": "==1.0.0" - }, - "myst-parser": { - "hashes": [ - "sha256:61b275b85d9f58aa327f370913ae1bec26ebad372cc99f3ab85c8ec3ee8d9fb8", - "sha256:79317f4bb2c13053dd6e64f9da1ba1da6cd9c40c8a430c447a7b146a594c246d" - ], - "markers": "python_version >= '3.7'", - "version": "==0.18.1" - }, - "nh3": { - "hashes": [ - "sha256:0411beb0589eacb6734f28d5497ca2ed379eafab8ad8c84b31bb5c34072b7164", - "sha256:14c5a72e9fe82aea5fe3072116ad4661af5cf8e8ff8fc5ad3450f123e4925e86", - "sha256:19aaba96e0f795bd0a6c56291495ff59364f4300d4a39b29a0abc9cb3774a84b", - "sha256:34c03fa78e328c691f982b7c03d4423bdfd7da69cd707fe572f544cf74ac23ad", - "sha256:36c95d4b70530b320b365659bb5034341316e6a9b30f0b25fa9c9eff4c27a204", - "sha256:3a157ab149e591bb638a55c8c6bcb8cdb559c8b12c13a8affaba6cedfe51713a", - "sha256:42c64511469005058cd17cc1537578eac40ae9f7200bedcfd1fc1a05f4f8c200", - "sha256:5f36b271dae35c465ef5e9090e1fdaba4a60a56f0bb0ba03e0932a66f28b9189", - "sha256:6955369e4d9f48f41e3f238a9e60f9410645db7e07435e62c6a9ea6135a4907f", - "sha256:7b7c2a3c9eb1a827d42539aa64091640bd275b81e097cd1d8d82ef91ffa2e811", - "sha256:8ce0f819d2f1933953fca255db2471ad58184a60508f03e6285e5114b6254844", - "sha256:94a166927e53972a9698af9542ace4e38b9de50c34352b962f4d9a7d4c927af4", - "sha256:a7f1b5b2c15866f2db413a3649a8fe4fd7b428ae58be2c0f6bca5eefd53ca2be", - "sha256:c8b3a1cebcba9b3669ed1a84cc65bf005728d2f0bc1ed2a6594a992e817f3a50", - "sha256:de3ceed6e661954871d6cd78b410213bdcb136f79aafe22aa7182e028b8c7307", - "sha256:f0eca9ca8628dbb4e916ae2491d72957fdd35f7a5d326b7032a345f111ac07fe" - ], - "version": "==0.2.18" - }, - "numpy": { - "hashes": [ - "sha256:05b2d4e667895cc55e3ff2b56077e4c8a5604361fc21a042845ea3ad67465aa8", - "sha256:12edb90831ff481f7ef5f6bc6431a9d74dc0e5ff401559a71e5e4611d4f2d466", - "sha256:13311c2db4c5f7609b462bc0f43d3c465424d25c626d95040f073e30f7570e35", - "sha256:13532a088217fa624c99b843eeb54640de23b3414b14aa66d023805eb731066c", - "sha256:13602b3174432a35b16c4cfb5de9a12d229727c3dd47a6ce35111f2ebdf66ff4", - "sha256:1600068c262af1ca9580a527d43dc9d959b0b1d8e56f8a05d830eea39b7c8af6", - "sha256:1b8cde4f11f0a975d1fd59373b32e2f5a562ade7cde4f85b7137f3de8fbb29a0", - "sha256:1c193d0b0238638e6fc5f10f1b074a6993cb13b0b431f64079a509d63d3aa8b7", - "sha256:1ebec5fd716c5a5b3d8dfcc439be82a8407b7b24b230d0ad28a81b61c2f4659a", - "sha256:242b39d00e4944431a3cd2db2f5377e15b5785920421993770cddb89992c3f3a", - "sha256:259ec80d54999cc34cd1eb8ded513cb053c3bf4829152a2e00de2371bd406f5e", - "sha256:2abbf905a0b568706391ec6fa15161fad0fb5d8b68d73c461b3c1bab6064dd62", - "sha256:2cbba4b30bf31ddbe97f1c7205ef976909a93a66bb1583e983adbd155ba72ac2", - "sha256:2ffef621c14ebb0188a8633348504a35c13680d6da93ab5cb86f4e54b7e922b5", - "sha256:30d53720b726ec36a7f88dc873f0eec8447fbc93d93a8f079dfac2629598d6ee", - "sha256:32e16a03138cabe0cb28e1007ee82264296ac0983714094380b408097a418cfe", - "sha256:43cca367bf94a14aca50b89e9bc2061683116cfe864e56740e083392f533ce7a", - "sha256:456e3b11cb79ac9946c822a56346ec80275eaf2950314b249b512896c0d2505e", - "sha256:4d6ec0d4222e8ffdab1744da2560f07856421b367928026fb540e1945f2eeeaf", - "sha256:5006b13a06e0b38d561fab5ccc37581f23c9511879be7693bd33c7cd15ca227c", - "sha256:675c741d4739af2dc20cd6c6a5c4b7355c728167845e3c6b0e824e4e5d36a6c3", - "sha256:6cdb606a7478f9ad91c6283e238544451e3a95f30fb5467fbf715964341a8a86", - "sha256:6d95f286b8244b3649b477ac066c6906fbb2905f8ac19b170e2175d3d799f4df", - "sha256:76322dcdb16fccf2ac56f99048af32259dcc488d9b7e25b51e5eca5147a3fb98", - "sha256:7c1c60328bd964b53f8b835df69ae8198659e2b9302ff9ebb7de4e5a5994db3d", - "sha256:860ec6e63e2c5c2ee5e9121808145c7bf86c96cca9ad396c0bd3e0f2798ccbe2", - "sha256:8e00ea6fc82e8a804433d3e9cedaa1051a1422cb6e443011590c14d2dea59146", - "sha256:9c6c754df29ce6a89ed23afb25550d1c2d5fdb9901d9c67a16e0b16eaf7e2550", - "sha256:a26ae94658d3ba3781d5e103ac07a876b3e9b29db53f68ed7df432fd033358a8", - "sha256:a65acfdb9c6ebb8368490dbafe83c03c7e277b37e6857f0caeadbbc56e12f4fb", - "sha256:a7d80b2e904faa63068ead63107189164ca443b42dd1930299e0d1cb041cec2e", - "sha256:a84498e0d0a1174f2b3ed769b67b656aa5460c92c9554039e11f20a05650f00d", - "sha256:ab4754d432e3ac42d33a269c8567413bdb541689b02d93788af4131018cbf366", - "sha256:ad369ed238b1959dfbade9018a740fb9392c5ac4f9b5173f420bd4f37ba1f7a0", - "sha256:b1d0fcae4f0949f215d4632be684a539859b295e2d0cb14f78ec231915d644db", - "sha256:b42a1a511c81cc78cbc4539675713bbcf9d9c3913386243ceff0e9429ca892fe", - "sha256:bd33f82e95ba7ad632bc57837ee99dba3d7e006536200c4e9124089e1bf42426", - "sha256:bdd407c40483463898b84490770199d5714dcc9dd9b792f6c6caccc523c00952", - "sha256:c6eef7a2dbd0abfb0d9eaf78b73017dbfd0b54051102ff4e6a7b2980d5ac1a03", - "sha256:c82af4b2ddd2ee72d1fc0c6695048d457e00b3582ccde72d8a1c991b808bb20f", - "sha256:d666cb72687559689e9906197e3bec7b736764df6a2e58ee265e360663e9baf7", - "sha256:d7bf0a4f9f15b32b5ba53147369e94296f5fffb783db5aacc1be15b4bf72f43b", - "sha256:d82075752f40c0ddf57e6e02673a17f6cb0f8eb3f587f63ca1eaab5594da5b17", - "sha256:da65fb46d4cbb75cb417cddf6ba5e7582eb7bb0b47db4b99c9fe5787ce5d91f5", - "sha256:e2b49c3c0804e8ecb05d59af8386ec2f74877f7ca8fd9c1e00be2672e4d399b1", - "sha256:e585c8ae871fd38ac50598f4763d73ec5497b0de9a0ab4ef5b69f01c6a046142", - "sha256:e8d3ca0a72dd8846eb6f7dfe8f19088060fcb76931ed592d29128e0219652884", - "sha256:ef444c57d664d35cac4e18c298c47d7b504c66b17c2ea91312e979fcfbdfb08a", - "sha256:f1eb068ead09f4994dec71c24b2844f1e4e4e013b9629f812f292f04bd1510d9", - "sha256:f2ded8d9b6f68cc26f8425eda5d3877b47343e68ca23d0d0846f4d312ecaa445", - "sha256:f751ed0a2f250541e19dfca9f1eafa31a392c71c832b6bb9e113b10d050cb0f1", - "sha256:faa88bc527d0f097abdc2c663cddf37c05a1c2f113716601555249805cf573f1", - "sha256:fc44e3c68ff00fd991b59092a54350e6e4911152682b4782f68070985aa9e648" - ], - "markers": "python_version >= '3.10'", - "version": "==2.1.2" - }, - "oauth2client": { - "hashes": [ - "sha256:b8a81cc5d60e2d364f0b1b98f958dbd472887acaf1a5b05e21c28c31a2d6d3ac", - "sha256:d486741e451287f69568a4d26d70d9acd73a2bbfa275746c535b4209891cccc6" - ], - "version": "==4.1.3" - }, - "oauthlib": { - "hashes": [ - "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", - "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918" - ], - "markers": "python_version >= '3.6'", - "version": "==3.2.2" - }, - "oscrypto": { - "hashes": [ - "sha256:2b2f1d2d42ec152ca90ccb5682f3e051fb55986e1b170ebde472b133713e7085", - "sha256:6f5fef59cb5b3708321db7cca56aed8ad7e662853351e7991fcf60ec606d47a4" - ], - "version": "==1.3.0" - }, - "outcome": { - "hashes": [ - "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", - "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b" - ], - "markers": "python_version >= '3.7'", - "version": "==1.3.0.post0" - }, - "packaging": { - "hashes": [ - "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", - "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" - ], - "markers": "python_version >= '3.8'", - "version": "==24.1" - }, - "pathspec": { - "hashes": [ - "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", - "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" - ], - "markers": "python_version >= '3.8'", - "version": "==0.12.1" - }, - "pdqhash": { - "hashes": [ - "sha256:262ec2881b80877a4005f408000f27e492d08b0d4e84269e1cfcc8a31e96c3bf", - "sha256:33bc5d22c458e5245c2649c0e54968d13e7b5940f6ace268f4a05016481b2253", - "sha256:68549e2071499a5a19748505eb00f52384f24830fe14a437f982d046a8edb498", - "sha256:6eab8a3112853f18adfaf1482b48bff8003d822de927b7397a421c5e7f0f76d7", - "sha256:7efd5b4e1ded44ec2a32ea2d32c29fef37d1adca03ce0867975526aea0ffe7fe", - "sha256:91652e70d017c8fd60003ea0bfcf4eefeceb3896b93deb2dec43a2ae896da6cd", - "sha256:9648abdfdbccb5edfc55fa2a61183766e51d140080fe08213f5daa885c3d5c66", - "sha256:a7d943874df8b2ca8c97755f60d1b6ae66e654fe8b2bb6ac8e8be216cae7d130", - "sha256:d63886b1edea4134eaa9862987393391e8958f35569918b91804b528f23c5e6c", - "sha256:df6375fef513089191cadcbf07c90715d40e882867141e04360a39d8a0861cb5", - "sha256:fc6c53cdc395f5421c857e4e30a92862cc918e18f91d7e4452bb5eb746e454f2" - ], - "version": "==0.2.7" - }, - "pillow": { - "hashes": [ - "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7", - "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5", - "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903", - "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2", - "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38", - "sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2", - "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9", - "sha256:1a61b54f87ab5786b8479f81c4b11f4d61702830354520837f8cc791ebba0f5f", - "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc", - "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8", - "sha256:20ec184af98a121fb2da42642dea8a29ec80fc3efbaefb86d8fdd2606619045d", - "sha256:21a0d3b115009ebb8ac3d2ebec5c2982cc693da935f4ab7bb5c8ebe2f47d36f2", - "sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316", - "sha256:2679d2258b7f1192b378e2893a8a0a0ca472234d4c2c0e6bdd3380e8dfa21b6a", - "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25", - "sha256:290f2cc809f9da7d6d622550bbf4c1e57518212da51b6a30fe8e0a270a5b78bd", - "sha256:2e46773dc9f35a1dd28bd6981332fd7f27bec001a918a72a79b4133cf5291dba", - "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc", - "sha256:375b8dd15a1f5d2feafff536d47e22f69625c1aa92f12b339ec0b2ca40263273", - "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa", - "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a", - "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b", - "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a", - "sha256:5178952973e588b3f1360868847334e9e3bf49d19e169bbbdfaf8398002419ae", - "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291", - "sha256:598b4e238f13276e0008299bd2482003f48158e2b11826862b1eb2ad7c768b97", - "sha256:5bd2d3bdb846d757055910f0a59792d33b555800813c3b39ada1829c372ccb06", - "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904", - "sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b", - "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b", - "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8", - "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527", - "sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947", - "sha256:674629ff60030d144b7bca2b8330225a9b11c482ed408813924619c6f302fdbb", - "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003", - "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5", - "sha256:70fbbdacd1d271b77b7721fe3cdd2d537bbbd75d29e6300c672ec6bb38d9672f", - "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739", - "sha256:7326a1787e3c7b0429659e0a944725e1b03eeaa10edd945a86dead1913383944", - "sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830", - "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f", - "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3", - "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4", - "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84", - "sha256:8594f42df584e5b4bb9281799698403f7af489fba84c34d53d1c4bfb71b7c4e7", - "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6", - "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6", - "sha256:88a58d8ac0cc0e7f3a014509f0455248a76629ca9b604eca7dc5927cc593c5e9", - "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de", - "sha256:8c676b587da5673d3c75bd67dd2a8cdfeb282ca38a30f37950511766b26858c4", - "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47", - "sha256:94f3e1780abb45062287b4614a5bc0874519c86a777d4a7ad34978e86428b8dd", - "sha256:9a0f748eaa434a41fccf8e1ee7a3eed68af1b690e75328fd7a60af123c193b50", - "sha256:a5629742881bcbc1f42e840af185fd4d83a5edeb96475a575f4da50d6ede337c", - "sha256:a65149d8ada1055029fcb665452b2814fe7d7082fcb0c5bed6db851cb69b2086", - "sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba", - "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306", - "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699", - "sha256:c12b5ae868897c7338519c03049a806af85b9b8c237b7d675b8c5e089e4a618e", - "sha256:c26845094b1af3c91852745ae78e3ea47abf3dbcd1cf962f16b9a5fbe3ee8488", - "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa", - "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2", - "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3", - "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9", - "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923", - "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2", - "sha256:daffdf51ee5db69a82dd127eabecce20729e21f7a3680cf7cbb23f0829189790", - "sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734", - "sha256:eda2616eb2313cbb3eebbe51f19362eb434b18e3bb599466a1ffa76a033fb916", - "sha256:ee217c198f2e41f184f3869f3e485557296d505b5195c513b2bfe0062dc537f1", - "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f", - "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798", - "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb", - "sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2", - "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9" - ], - "markers": "python_version >= '3.9'", - "version": "==11.0.0" - }, - "pkginfo": { - "hashes": [ - "sha256:5df73835398d10db79f8eecd5cd86b1f6d29317589ea70796994d49399af6297", - "sha256:889a6da2ed7ffc58ab5b900d888ddce90bce912f2d2de1dc1c26f4cb9fe65097" - ], - "markers": "python_version >= '3.6'", - "version": "==1.10.0" - }, - "platformdirs": { - "hashes": [ - "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee", - "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3" - ], - "markers": "python_version >= '3.8'", - "version": "==4.2.2" - }, - "pluggy": { - "hashes": [ - "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", - "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669" - ], - "markers": "python_version >= '3.8'", - "version": "==1.5.0" - }, - "prometheus-client": { - "hashes": [ - "sha256:4fa6b4dd0ac16d58bb587c04b1caae65b8c5043e85f778f42f5f632f6af2e166", - "sha256:96c83c606b71ff2b0a433c98889d275f51ffec6c5e267de37c7a2b5c9aa9233e" - ], - "markers": "python_version >= '3.8'", - "version": "==0.21.0" - }, - "prometheus-fastapi-instrumentator": { - "hashes": [ - "sha256:5ba67c9212719f244ad7942d75ded80693b26331ee5dfc1e7571e4794a9ccbed", - "sha256:96030c43c776ee938a3dae58485ec24caed7e05bfc60fe067161e0d5b5757052" - ], - "index": "pypi", - "markers": "python_full_version >= '3.8.1' and python_full_version < '4.0.0'", - "version": "==7.0.0" - }, - "prompt-toolkit": { - "hashes": [ - "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90", - "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e" - ], - "markers": "python_full_version >= '3.7.0'", - "version": "==3.0.48" - }, - "proto-plus": { - "hashes": [ - "sha256:30b72a5ecafe4406b0d339db35b56c4059064e69227b8c3bda7462397f966445", - "sha256:402576830425e5f6ce4c2a6702400ac79897dab0b4343821aa5188b0fab81a12" - ], - "markers": "python_version >= '3.7'", - "version": "==1.24.0" - }, - "protobuf": { - "hashes": [ - "sha256:2c69461a7fcc8e24be697624c09a839976d82ae75062b11a0972e41fd2cd9132", - "sha256:35cfcb15f213449af7ff6198d6eb5f739c37d7e4f1c09b5d0641babf2cc0c68f", - "sha256:52235802093bd8a2811abbe8bf0ab9c5f54cca0a751fdd3f6ac2a21438bffece", - "sha256:59379674ff119717404f7454647913787034f03fe7049cbef1d74a97bb4593f0", - "sha256:5e8a95246d581eef20471b5d5ba010d55f66740942b95ba9b872d918c459452f", - "sha256:87317e9bcda04a32f2ee82089a204d3a2f0d3c8aeed16568c7daf4756e4f1fe0", - "sha256:8ddc60bf374785fb7cb12510b267f59067fa10087325b8e1855b898a0d81d276", - "sha256:a8b9403fc70764b08d2f593ce44f1d2920c5077bf7d311fefec999f8c40f78b7", - "sha256:c0ea0123dac3399a2eeb1a1443d82b7afc9ff40241433296769f7da42d142ec3", - "sha256:ca53faf29896c526863366a52a8f4d88e69cd04ec9571ed6082fa117fac3ab36", - "sha256:eeea10f3dc0ac7e6b4933d32db20662902b4ab81bf28df12218aa389e9c2102d" - ], - "markers": "python_version >= '3.8'", - "version": "==5.28.2" - }, - "psutil": { - "hashes": [ - "sha256:02615ed8c5ea222323408ceba16c60e99c3f91639b07da6373fb7e6539abc56d", - "sha256:05806de88103b25903dff19bb6692bd2e714ccf9e668d050d144012055cbca73", - "sha256:26bd09967ae00920df88e0352a91cff1a78f8d69b3ecabbfe733610c0af486c8", - "sha256:27cc40c3493bb10de1be4b3f07cae4c010ce715290a5be22b98493509c6299e2", - "sha256:36f435891adb138ed3c9e58c6af3e2e6ca9ac2f365efe1f9cfef2794e6c93b4e", - "sha256:50187900d73c1381ba1454cf40308c2bf6f34268518b3f36a9b663ca87e65e36", - "sha256:611052c4bc70432ec770d5d54f64206aa7203a101ec273a0cd82418c86503bb7", - "sha256:6be126e3225486dff286a8fb9a06246a5253f4c7c53b475ea5f5ac934e64194c", - "sha256:7d79560ad97af658a0f6adfef8b834b53f64746d45b403f225b85c5c2c140eee", - "sha256:8cb6403ce6d8e047495a701dc7c5bd788add903f8986d523e3e20b98b733e421", - "sha256:8db4c1b57507eef143a15a6884ca10f7c73876cdf5d51e713151c1236a0e68cf", - "sha256:aee678c8720623dc456fa20659af736241f575d79429a0e5e9cf88ae0605cc81", - "sha256:bc56c2a1b0d15aa3eaa5a60c9f3f8e3e565303b465dbf57a1b730e7a2b9844e0", - "sha256:bd1184ceb3f87651a67b2708d4c3338e9b10c5df903f2e3776b62303b26cb631", - "sha256:d06016f7f8625a1825ba3732081d77c94589dca78b7a3fc072194851e88461a4", - "sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==5.9.8" - }, - "py": { - "hashes": [ - "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", - "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.11.0" - }, - "pyaes": { - "hashes": [ - "sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f" - ], - "version": "==1.6.1" - }, - "pyasn1": { - "hashes": [ - "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", - "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034" - ], - "markers": "python_version >= '3.8'", - "version": "==0.6.1" - }, - "pyasn1-modules": { - "hashes": [ - "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd", - "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c" - ], - "markers": "python_version >= '3.8'", - "version": "==0.4.1" - }, - "pycodestyle": { - "hashes": [ - "sha256:442f950141b4f43df752dd303511ffded3a04c2b6fb7f65980574f0c31e6e79c", - "sha256:949a39f6b86c3e1515ba1787c2022131d165a8ad271b11370a8819aa070269e4" - ], - "markers": "python_version >= '3.8'", - "version": "==2.12.0" - }, - "pycparser": { - "hashes": [ - "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", - "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc" - ], - "markers": "python_version >= '3.8'", - "version": "==2.22" - }, - "pycryptodomex": { - "hashes": [ - "sha256:0daad007b685db36d977f9de73f61f8da2a7104e20aca3effd30752fd56f73e1", - "sha256:108e5f1c1cd70ffce0b68739c75734437c919d2eaec8e85bffc2c8b4d2794305", - "sha256:19764605feea0df966445d46533729b645033f134baeb3ea26ad518c9fdf212c", - "sha256:1be97461c439a6af4fe1cf8bf6ca5936d3db252737d2f379cc6b2e394e12a458", - "sha256:25cd61e846aaab76d5791d006497134602a9e451e954833018161befc3b5b9ed", - "sha256:2a47bcc478741b71273b917232f521fd5704ab4b25d301669879e7273d3586cc", - "sha256:59af01efb011b0e8b686ba7758d59cf4a8263f9ad35911bfe3f416cee4f5c08c", - "sha256:5dcac11031a71348faaed1f403a0debd56bf5404232284cf8c761ff918886ebc", - "sha256:62a5ec91388984909bb5398ea49ee61b68ecb579123694bffa172c3b0a107079", - "sha256:645bd4ca6f543685d643dadf6a856cc382b654cc923460e3a10a49c1b3832aeb", - "sha256:653b29b0819605fe0898829c8ad6400a6ccde096146730c2da54eede9b7b8baa", - "sha256:69138068268127cd605e03438312d8f271135a33140e2742b417d027a0539427", - "sha256:6e186342cfcc3aafaad565cbd496060e5a614b441cacc3995ef0091115c1f6c5", - "sha256:76bd15bb65c14900d98835fcd10f59e5e0435077431d3a394b60b15864fddd64", - "sha256:7805830e0c56d88f4d491fa5ac640dfc894c5ec570d1ece6ed1546e9df2e98d6", - "sha256:7a710b79baddd65b806402e14766c721aee8fb83381769c27920f26476276c1e", - "sha256:7a7a8f33a1f1fb762ede6cc9cbab8f2a9ba13b196bfaf7bc6f0b39d2ba315a43", - "sha256:82ee7696ed8eb9a82c7037f32ba9b7c59e51dda6f105b39f043b6ef293989cb3", - "sha256:88afd7a3af7ddddd42c2deda43d53d3dfc016c11327d0915f90ca34ebda91499", - "sha256:8af1a451ff9e123d0d8bd5d5e60f8e3315c3a64f3cdd6bc853e26090e195cdc8", - "sha256:8ee606964553c1a0bc74057dd8782a37d1c2bc0f01b83193b6f8bb14523b877b", - "sha256:91852d4480a4537d169c29a9d104dda44094c78f1f5b67bca76c29a91042b623", - "sha256:9c682436c359b5ada67e882fec34689726a09c461efd75b6ea77b2403d5665b7", - "sha256:bc3ee1b4d97081260d92ae813a83de4d2653206967c4a0a017580f8b9548ddbc", - "sha256:bca649483d5ed251d06daf25957f802e44e6bb6df2e8f218ae71968ff8f8edc4", - "sha256:c39778fd0548d78917b61f03c1fa8bfda6cfcf98c767decf360945fe6f97461e", - "sha256:cbe71b6712429650e3883dc81286edb94c328ffcd24849accac0a4dbcc76958a", - "sha256:d00fe8596e1cc46b44bf3907354e9377aa030ec4cd04afbbf6e899fc1e2a7781", - "sha256:d3584623e68a5064a04748fb6d76117a21a7cb5eaba20608a41c7d0c61721794", - "sha256:e48217c7901edd95f9f097feaa0388da215ed14ce2ece803d3f300b4e694abea", - "sha256:f2e497413560e03421484189a6b65e33fe800d3bd75590e6d78d4dfdb7accf3b", - "sha256:ff5c9a67f8a4fba4aed887216e32cbc48f2a6fb2673bb10a99e43be463e15913" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==3.20.0" - }, - "pydantic": { - "hashes": [ - "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f", - "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12" - ], - "markers": "python_version >= '3.8'", - "version": "==2.9.2" - }, - "pydantic-core": { - "hashes": [ - "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36", - "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05", - "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071", - "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327", - "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c", - "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36", - "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29", - "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744", - "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d", - "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec", - "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e", - "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e", - "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577", - "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232", - "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863", - "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6", - "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368", - "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480", - "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2", - "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2", - "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6", - "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769", - "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d", - "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2", - "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84", - "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166", - "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271", - "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5", - "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb", - "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13", - "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323", - "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556", - "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665", - "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef", - "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb", - "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119", - "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126", - "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510", - "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b", - "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87", - "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f", - "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc", - "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8", - "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21", - "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f", - "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6", - "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658", - "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b", - "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3", - "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb", - "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59", - "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24", - "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9", - "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3", - "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd", - "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753", - "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55", - "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad", - "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a", - "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605", - "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e", - "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b", - "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433", - "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8", - "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07", - "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728", - "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0", - "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327", - "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555", - "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64", - "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6", - "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea", - "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b", - "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df", - "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e", - "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd", - "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068", - "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3", - "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040", - "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12", - "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916", - "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f", - "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f", - "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801", - "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231", - "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5", - "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8", - "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee", - "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607" - ], - "markers": "python_version >= '3.8'", - "version": "==2.23.4" - }, - "pydantic-settings": { - "hashes": [ - "sha256:44a1804abffac9e6a30372bb45f6cafab945ef5af25e66b1c634c01dd39e0188", - "sha256:4a819166f119b74d7f8c765196b165f95cc7487ce58ea27dec8a5a26be0970e0" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==2.6.0" - }, - "pyflakes": { - "hashes": [ - "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", - "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a" - ], - "markers": "python_version >= '3.8'", - "version": "==3.2.0" - }, - "pygments": { - "hashes": [ - "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", - "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a" - ], - "markers": "python_version >= '3.8'", - "version": "==2.18.0" - }, - "pyopenssl": { - "hashes": [ - "sha256:4247f0dbe3748d560dcbb2ff3ea01af0f9a1a001ef5f7c4c647956ed8cbf0e95", - "sha256:967d5719b12b243588573f39b0c677637145c7a1ffedcd495a487e58177fbb8d" - ], - "markers": "python_version >= '3.7'", - "version": "==24.2.1" - }, - "pyparsing": { - "hashes": [ - "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", - "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" - ], - "markers": "python_full_version >= '3.6.8'", - "version": "==3.0.9" - }, - "pysocks": { - "hashes": [ - "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299", - "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", - "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0" - ], - "version": "==1.7.1" - }, - "pysubs2": { - "hashes": [ - "sha256:b0130f373390736754531be4e68a0fa521e825fa15cc8ff506e4f8ca2c17459a", - "sha256:de438c868d2c656781c4a78f220ec3a6fd6d52be49266c81fe912d2527002d44" - ], - "markers": "python_version >= '3.8'", - "version": "==1.7.3" - }, - "pytest": { - "hashes": [ - "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343", - "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977" - ], - "markers": "python_version >= '3.8'", - "version": "==8.2.2" - }, - "pytest-cov": { - "hashes": [ - "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", - "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857" - ], - "markers": "python_version >= '3.8'", - "version": "==5.0.0" - }, - "pytest-sphinx": { - "hashes": [ - "sha256:3b63c8181b9de6a5e5c9826d1b4dc0c827245bec8e64c9f16f269be08be5ecd5", - "sha256:856e760e64dfbfc89e362e187d641140a267b97881d3ef8aeefb72cc8438ac40" - ], - "markers": "python_version >= '3.8'", - "version": "==0.6.3" - }, - "python-dateutil": { - "hashes": [ - "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", - "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.9.0.post0" - }, - "python-dotenv": { - "hashes": [ - "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", - "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==1.0.1" - }, - "python-slugify": { - "hashes": [ - "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", - "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856" - ], - "markers": "python_version >= '3.7'", - "version": "==8.0.4" - }, - "python-twitter-v2": { - "hashes": [ - "sha256:45374228d02c2bed150ff59ca93feafeb9cbe3b3cd3319223906ac52caf98a46", - "sha256:4fc88c5d3fc593ada134b16604ebf7a896379fee7694b5956cc460af3435f247" - ], - "markers": "python_version >= '3.7' and python_version < '4.0'", - "version": "==0.9.1" - }, - "pytz": { - "hashes": [ - "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7", - "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c" - ], - "version": "==2022.1" - }, - "pyyaml": { - "hashes": [ - "sha256:0101357af42f5c9fc7e9acc5c5ab8c3049f50db7425de175b6c7a5959cb6023d", - "sha256:0ae563b7e3ed5e918cd0184060e28b48b7e672b975bf7c6f4a892cee9d886ada", - "sha256:0fe2c1c5401a3a98f06337fed48f57340cf652a685484834b44f5ceeadb772ba", - "sha256:1eb00dd3344da80264261ab126c95481824669ed9e5ecc82fb2d88b1fce668ee", - "sha256:2086b30215c433c1e480c08c1db8b43c1edd36c59cf43d36b424e6f35fcaf1ad", - "sha256:29b4a67915232f79506211e69943e3102e211c616181ceff0adf34e21b469357", - "sha256:2e9bc8a34797f0621f56160b961d47a088644370f79d34bedc934fb89e3f47dd", - "sha256:30ec6b9afc17353a9abcff109880edf6e8d5b924eb1eeed7fe9376febc1f9800", - "sha256:31573d7e161d2f905311f036b12e65c058389b474dbd35740f4880b91e2ca2be", - "sha256:36d7bf63558843ea2a81de9d0c3e9c56c353b1df8e6c1faaec86df5adedf2e02", - "sha256:3af6b36bc195d741cd5b511810246cad143b99c953b4591e679e194a820d7b7c", - "sha256:414629800a1ddccd7303471650843fc801801cc579a195d2fe617b5b455409e3", - "sha256:459113f2b9cd68881201a3bd1a858ece3281dc0e92ece6e917d23b128f0fcb31", - "sha256:46e4fae38d00b40a62d32d60f1baa1b9ef33aff28c2aafd96b05d5cc770f1583", - "sha256:4bf821ccd51e8d5bc1a4021b8bd85a92b498832ac1cd1a53b399f0eb7c1c4258", - "sha256:50bd6560a6df3de59336b9a9086cbdea5aa9eee5361661448ee45c21eeb0da68", - "sha256:53056b51f111223e603bed1db5367f54596d44cacfa50f07e082a11929612957", - "sha256:53c5f0749a93e3296078262c9acf632de246241ff2f22bbedfe49d4b55e9bbdd", - "sha256:54c754cee6937bb9b72d6a16163160dec80b93a43020ac6fc9f13729c030c30b", - "sha256:58cc18ccbade0c48fb55102aa971a5b4e571e2b22187d083dda33f8708fa4ee7", - "sha256:5921fd128fbf27ab7c7ad1a566d2cd9557b84ade130743a7c110a55e7dec3b3c", - "sha256:5c758cc29713c9166750a30156ca3d90ac2515d5dea3c874377ae8829cf03087", - "sha256:60bf91e73354c96754220a9c04a9502c2ad063231cd754b59f8e4511157e32e2", - "sha256:6f0f728a88c6eb58a3b762726b965bb6acf12d97f8ea2cb4fecf856a727f9bdc", - "sha256:6f31c5935310da69ea0efe996a962d488f080312f0eb43beff1717acb5fe9bed", - "sha256:728b447d0cedec409ea1a3f0ad1a6cc3cec0a8d086611b45f038a9230a2242f3", - "sha256:72ffbc5c0cc71877104387548a450f2b7b7c4926b40dc9443e7598fe92aa13d9", - "sha256:73d8b233309ecd45c33c51cd55aa1be1dcab1799a9e54f6c753d8cab054b8c34", - "sha256:765029d1cf96e9e761329ee1c20f1ca2de8644e7350a151b198260698b96e30f", - "sha256:7ee3d180d886a3bc50f753b76340f1c314f9e8c507f5b107212112214c3a66fd", - "sha256:826fb4d5ac2c48b9d6e71423def2669d4646c93b6c13612a71b3ac7bb345304b", - "sha256:84c39ceec517cd8f01cb144efb08904a32050be51c55b7a59bc7958c8091568d", - "sha256:88bfe675bb19ae12a9c77c52322a28a8e2a8d3d213fbcfcded5c3f5ca3ead352", - "sha256:8e0a1ebd5c5842595365bf90db3ef7e9a8d6a79c9aedb1d05b675c81c7267fd3", - "sha256:9426067a10b369474396bf57fdf895b899045a25d1848798844693780b147436", - "sha256:9c5c0de7ec50d4df88b62f4b019ab7b3bb2883c826a1044268e9afb344c57b17", - "sha256:ad0c172fe15beffc32e3a8260f18e6708eb0e15ae82c9b3f80fbe04de0ef5729", - "sha256:ad206c7f5f08d393b872d3399f597246fdc6ebebff09c5ae5268ac45aebf4f8d", - "sha256:b0a163f4f84d1e0fe6a07ccad3b02e9b243790b8370ff0408ae5932c50c4d96d", - "sha256:b0dd9c7497d60126445e79e542ff01351c6b6dc121299d89787f5685b382c626", - "sha256:b1de10c488d6f02e498eb6956b89081bea31abf3133223c17749e7137734da75", - "sha256:b408f36eeb4e2be6f802f1be82daf1b578f3de5a51917c6e467aedb46187d827", - "sha256:bae077a01367e4bf5fddf00fd6c8b743e676385911c7c615e29e1c45ace8813b", - "sha256:bc3c3600fec6c2a719106381d6282061d8c108369cdec58b6f280610eba41e09", - "sha256:c16522bf91daa4ea9dedc1243b56b5a226357ab98b3133089ca627ef99baae6f", - "sha256:ca5136a77e2d64b4cf5106fb940376650ae232c74c09a8ff29dbb1e262495b31", - "sha256:d6e0f7ee5f8d851b1d91149a3e5074dbf5aacbb63e4b771fcce16508339a856f", - "sha256:e7930a0612e74fcca37019ca851b50d73b5f0c3dab7f3085a7c15d2026118315", - "sha256:e8e6dd230a158a836cda3cc521fcbedea16f22b16b8cfa8054d0c6cea5d0a531", - "sha256:eee36bf4bc11e39e3f17c171f25cdedff3d7c73b148aedc8820257ce2aa56d3b", - "sha256:f07adc282d51aaa528f3141ac1922d16d32fe89413ee59bfb8a73ed689ad3d23", - "sha256:f09816c047fdb588dddba53d321f1cb8081e38ad2a40ea6a7560a88b7a2f0ea8", - "sha256:fea4c4310061cd70ef73b39801231b9dc3dc638bb8858e38364b144fbd335a1a" - ], - "markers": "python_version >= '3.6'", - "version": "==6.0.2rc1" - }, - "readme-renderer": { - "hashes": [ - "sha256:1818dd28140813509eeed8d62687f7cd4f7bad90d4db586001c5dc09d4fde311", - "sha256:19db308d86ecd60e5affa3b2a98f017af384678c63c88e5d4556a380e674f3f9" - ], - "markers": "python_version >= '3.8'", - "version": "==43.0" - }, - "redis": { - "hashes": [ - "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2", - "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24" - ], - "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==3.5.3" - }, - "regex": { - "hashes": [ - "sha256:01c2acb51f8a7d6494c8c5eafe3d8e06d76563d8a8a4643b37e9b2dd8a2ff623", - "sha256:02087ea0a03b4af1ed6ebab2c54d7118127fee8d71b26398e8e4b05b78963199", - "sha256:040562757795eeea356394a7fb13076ad4f99d3c62ab0f8bdfb21f99a1f85664", - "sha256:042c55879cfeb21a8adacc84ea347721d3d83a159da6acdf1116859e2427c43f", - "sha256:079400a8269544b955ffa9e31f186f01d96829110a3bf79dc338e9910f794fca", - "sha256:07f45f287469039ffc2c53caf6803cd506eb5f5f637f1d4acb37a738f71dd066", - "sha256:09d77559e80dcc9d24570da3745ab859a9cf91953062e4ab126ba9d5993688ca", - "sha256:0cbff728659ce4bbf4c30b2a1be040faafaa9eca6ecde40aaff86f7889f4ab39", - "sha256:0e12c481ad92d129c78f13a2a3662317e46ee7ef96c94fd332e1c29131875b7d", - "sha256:0ea51dcc0835eea2ea31d66456210a4e01a076d820e9039b04ae8d17ac11dee6", - "sha256:0ffbcf9221e04502fc35e54d1ce9567541979c3fdfb93d2c554f0ca583a19b35", - "sha256:1494fa8725c285a81d01dc8c06b55287a1ee5e0e382d8413adc0a9197aac6408", - "sha256:16e13a7929791ac1216afde26f712802e3df7bf0360b32e4914dca3ab8baeea5", - "sha256:18406efb2f5a0e57e3a5881cd9354c1512d3bb4f5c45d96d110a66114d84d23a", - "sha256:18e707ce6c92d7282dfce370cd205098384b8ee21544e7cb29b8aab955b66fa9", - "sha256:220e92a30b426daf23bb67a7962900ed4613589bab80382be09b48896d211e92", - "sha256:23b30c62d0f16827f2ae9f2bb87619bc4fba2044911e2e6c2eb1af0161cdb766", - "sha256:23f9985c8784e544d53fc2930fc1ac1a7319f5d5332d228437acc9f418f2f168", - "sha256:297f54910247508e6e5cae669f2bc308985c60540a4edd1c77203ef19bfa63ca", - "sha256:2b08fce89fbd45664d3df6ad93e554b6c16933ffa9d55cb7e01182baaf971508", - "sha256:2cce2449e5927a0bf084d346da6cd5eb016b2beca10d0013ab50e3c226ffc0df", - "sha256:313ea15e5ff2a8cbbad96ccef6be638393041b0a7863183c2d31e0c6116688cf", - "sha256:323c1f04be6b2968944d730e5c2091c8c89767903ecaa135203eec4565ed2b2b", - "sha256:35f4a6f96aa6cb3f2f7247027b07b15a374f0d5b912c0001418d1d55024d5cb4", - "sha256:3b37fa423beefa44919e009745ccbf353d8c981516e807995b2bd11c2c77d268", - "sha256:3ce4f1185db3fbde8ed8aa223fc9620f276c58de8b0d4f8cc86fd1360829edb6", - "sha256:46989629904bad940bbec2106528140a218b4a36bb3042d8406980be1941429c", - "sha256:4838e24ee015101d9f901988001038f7f0d90dc0c3b115541a1365fb439add62", - "sha256:49b0e06786ea663f933f3710a51e9385ce0cba0ea56b67107fd841a55d56a231", - "sha256:4db21ece84dfeefc5d8a3863f101995de646c6cb0536952c321a2650aa202c36", - "sha256:54c4a097b8bc5bb0dfc83ae498061d53ad7b5762e00f4adaa23bee22b012e6ba", - "sha256:54d9ff35d4515debf14bc27f1e3b38bfc453eff3220f5bce159642fa762fe5d4", - "sha256:55b96e7ce3a69a8449a66984c268062fbaa0d8ae437b285428e12797baefce7e", - "sha256:57fdd2e0b2694ce6fc2e5ccf189789c3e2962916fb38779d3e3521ff8fe7a822", - "sha256:587d4af3979376652010e400accc30404e6c16b7df574048ab1f581af82065e4", - "sha256:5b513b6997a0b2f10e4fd3a1313568e373926e8c252bd76c960f96fd039cd28d", - "sha256:5ddcd9a179c0a6fa8add279a4444015acddcd7f232a49071ae57fa6e278f1f71", - "sha256:6113c008a7780792efc80f9dfe10ba0cd043cbf8dc9a76ef757850f51b4edc50", - "sha256:635a1d96665f84b292e401c3d62775851aedc31d4f8784117b3c68c4fcd4118d", - "sha256:64ce2799bd75039b480cc0360907c4fb2f50022f030bf9e7a8705b636e408fad", - "sha256:69dee6a020693d12a3cf892aba4808fe168d2a4cef368eb9bf74f5398bfd4ee8", - "sha256:6a2644a93da36c784e546de579ec1806bfd2763ef47babc1b03d765fe560c9f8", - "sha256:6b41e1adc61fa347662b09398e31ad446afadff932a24807d3ceb955ed865cc8", - "sha256:6c188c307e8433bcb63dc1915022deb553b4203a70722fc542c363bf120a01fd", - "sha256:6edd623bae6a737f10ce853ea076f56f507fd7726bee96a41ee3d68d347e4d16", - "sha256:73d6d2f64f4d894c96626a75578b0bf7d9e56dcda8c3d037a2118fdfe9b1c664", - "sha256:7a22ccefd4db3f12b526eccb129390942fe874a3a9fdbdd24cf55773a1faab1a", - "sha256:7fb89ee5d106e4a7a51bce305ac4efb981536301895f7bdcf93ec92ae0d91c7f", - "sha256:846bc79ee753acf93aef4184c040d709940c9d001029ceb7b7a52747b80ed2dd", - "sha256:85ab7824093d8f10d44330fe1e6493f756f252d145323dd17ab6b48733ff6c0a", - "sha256:8dee5b4810a89447151999428fe096977346cf2f29f4d5e29609d2e19e0199c9", - "sha256:8e5fb5f77c8745a60105403a774fe2c1759b71d3e7b4ca237a5e67ad066c7199", - "sha256:98eeee2f2e63edae2181c886d7911ce502e1292794f4c5ee71e60e23e8d26b5d", - "sha256:9d4a76b96f398697fe01117093613166e6aa8195d63f1b4ec3f21ab637632963", - "sha256:9e8719792ca63c6b8340380352c24dcb8cd7ec49dae36e963742a275dfae6009", - "sha256:a0b2b80321c2ed3fcf0385ec9e51a12253c50f146fddb2abbb10f033fe3d049a", - "sha256:a4cc92bb6db56ab0c1cbd17294e14f5e9224f0cc6521167ef388332604e92679", - "sha256:a738b937d512b30bf75995c0159c0ddf9eec0775c9d72ac0202076c72f24aa96", - "sha256:a8f877c89719d759e52783f7fe6e1c67121076b87b40542966c02de5503ace42", - "sha256:a906ed5e47a0ce5f04b2c981af1c9acf9e8696066900bf03b9d7879a6f679fc8", - "sha256:ae2941333154baff9838e88aa71c1d84f4438189ecc6021a12c7573728b5838e", - "sha256:b0d0a6c64fcc4ef9c69bd5b3b3626cc3776520a1637d8abaa62b9edc147a58f7", - "sha256:b5b029322e6e7b94fff16cd120ab35a253236a5f99a79fb04fda7ae71ca20ae8", - "sha256:b7aaa315101c6567a9a45d2839322c51c8d6e81f67683d529512f5bcfb99c802", - "sha256:be1c8ed48c4c4065ecb19d882a0ce1afe0745dfad8ce48c49586b90a55f02366", - "sha256:c0256beda696edcf7d97ef16b2a33a8e5a875affd6fa6567b54f7c577b30a137", - "sha256:c157bb447303070f256e084668b702073db99bbb61d44f85d811025fcf38f784", - "sha256:c57d08ad67aba97af57a7263c2d9006d5c404d721c5f7542f077f109ec2a4a29", - "sha256:c69ada171c2d0e97a4b5aa78fbb835e0ffbb6b13fc5da968c09811346564f0d3", - "sha256:c94bb0a9f1db10a1d16c00880bdebd5f9faf267273b8f5bd1878126e0fbde771", - "sha256:cb130fccd1a37ed894824b8c046321540263013da72745d755f2d35114b81a60", - "sha256:ced479f601cd2f8ca1fd7b23925a7e0ad512a56d6e9476f79b8f381d9d37090a", - "sha256:d05ac6fa06959c4172eccd99a222e1fbf17b5670c4d596cb1e5cde99600674c4", - "sha256:d552c78411f60b1fdaafd117a1fca2f02e562e309223b9d44b7de8be451ec5e0", - "sha256:dd4490a33eb909ef5078ab20f5f000087afa2a4daa27b4c072ccb3cb3050ad84", - "sha256:df5cbb1fbc74a8305b6065d4ade43b993be03dbe0f8b30032cced0d7740994bd", - "sha256:e28f9faeb14b6f23ac55bfbbfd3643f5c7c18ede093977f1df249f73fd22c7b1", - "sha256:e464b467f1588e2c42d26814231edecbcfe77f5ac414d92cbf4e7b55b2c2a776", - "sha256:e4c22e1ac1f1ec1e09f72e6c44d8f2244173db7eb9629cc3a346a8d7ccc31142", - "sha256:e53b5fbab5d675aec9f0c501274c467c0f9a5d23696cfc94247e1fb56501ed89", - "sha256:e93f1c331ca8e86fe877a48ad64e77882c0c4da0097f2212873a69bbfea95d0c", - "sha256:e997fd30430c57138adc06bba4c7c2968fb13d101e57dd5bb9355bf8ce3fa7e8", - "sha256:e9a091b0550b3b0207784a7d6d0f1a00d1d1c8a11699c1a4d93db3fbefc3ad35", - "sha256:eab4bb380f15e189d1313195b062a6aa908f5bd687a0ceccd47c8211e9cf0d4a", - "sha256:eb1ae19e64c14c7ec1995f40bd932448713d3c73509e82d8cd7744dc00e29e86", - "sha256:ecea58b43a67b1b79805f1a0255730edaf5191ecef84dbc4cc85eb30bc8b63b9", - "sha256:ee439691d8c23e76f9802c42a95cfeebf9d47cf4ffd06f18489122dbb0a7ad64", - "sha256:eee9130eaad130649fd73e5cd92f60e55708952260ede70da64de420cdcad554", - "sha256:f47cd43a5bfa48f86925fe26fbdd0a488ff15b62468abb5d2a1e092a4fb10e85", - "sha256:f6fff13ef6b5f29221d6904aa816c34701462956aa72a77f1f151a8ec4f56aeb", - "sha256:f745ec09bc1b0bd15cfc73df6fa4f726dcc26bb16c23a03f9e3367d357eeedd0", - "sha256:f8404bf61298bb6f8224bb9176c1424548ee1181130818fcd2cbffddc768bed8", - "sha256:f9268774428ec173654985ce55fc6caf4c6d11ade0f6f914d48ef4719eb05ebb", - "sha256:faa3c142464efec496967359ca99696c896c591c56c53506bac1ad465f66e919" - ], - "markers": "python_version >= '3.8'", - "version": "==2024.9.11" - }, - "requests": { - "extras": [ - "socks" - ], - "hashes": [ - "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", - "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==2.32.3" - }, - "requests-oauthlib": { - "hashes": [ - "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", - "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9" - ], - "markers": "python_version >= '3.4'", - "version": "==2.0.0" - }, - "requests-toolbelt": { - "hashes": [ - "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", - "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.0.0" - }, - "retrying": { - "hashes": [ - "sha256:345da8c5765bd982b1d1915deb9102fd3d1f7ad16bd84a9700b85f64d24e8f3e", - "sha256:8cc4d43cb8e1125e0ff3344e9de678fefd85db3b750b81b2240dc0183af37b35" - ], - "version": "==1.3.4" - }, - "rfc3986": { - "hashes": [ - "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", - "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c" - ], - "markers": "python_version >= '3.7'", - "version": "==2.0.0" - }, - "rich": { - "hashes": [ - "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222", - "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432" - ], - "markers": "python_full_version >= '3.7.0'", - "version": "==13.7.1" - }, - "rsa": { - "hashes": [ - "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7", - "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21" - ], - "markers": "python_version >= '3.6' and python_version < '4'", - "version": "==4.9" - }, - "s3transfer": { - "hashes": [ - "sha256:263ed587a5803c6c708d3ce44dc4dfedaab4c1a32e8329bab818933d79ddcf5d", - "sha256:4f50ed74ab84d474ce614475e0b8d5047ff080810aac5d01ea25231cfc944b0c" - ], - "markers": "python_version >= '3.8'", - "version": "==0.10.3" - }, - "secretstorage": { - "hashes": [ - "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", - "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99" - ], - "markers": "python_version >= '3.6'", - "version": "==3.3.3" - }, - "selenium": { - "hashes": [ - "sha256:3798d2d12b4a570bc5790163ba57fef10b2afee958bf1d80f2a3cf07c4141f33", - "sha256:95d08d3b82fb353f3c474895154516604c7f0e6a9a565ae6498ef36c9bac6921" - ], - "markers": "python_version >= '3.8'", - "version": "==4.25.0" - }, - "six": { - "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" - }, - "sniffio": { - "hashes": [ - "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", - "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" - ], - "markers": "python_version >= '3.7'", - "version": "==1.3.1" - }, - "snowballstemmer": { - "hashes": [ - "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", - "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a" - ], - "version": "==2.2.0" - }, - "snscrape": { - "hashes": [ - "sha256:6eedb85c7e79f35361dde1949e1e7e2dee44e9f8469668438c9f8e72980f482f", - "sha256:71da8aec489a3b1139caaab699ca489c708d117828a2d5bcdf1ce2c9e76f3708" - ], - "markers": "python_version ~= '3.8'", - "version": "==0.7.0.20230622" - }, - "sortedcontainers": { - "hashes": [ - "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", - "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0" - ], - "version": "==2.4.0" - }, - "soupsieve": { - "hashes": [ - "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690", - "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7" - ], - "markers": "python_version >= '3.8'", - "version": "==2.5" - }, - "sphinx": { - "hashes": [ - "sha256:b18e978ea7565720f26019c702cd85c84376e948370f1cd43d60265010e1c7b0", - "sha256:d3e57663eed1d7c5c50895d191fdeda0b54ded6f44d5621b50709466c338d1e8" - ], - "markers": "python_version >= '3.6'", - "version": "==5.0.2" - }, - "sphinx-autobuild": { - "hashes": [ - "sha256:1c0ed37a1970eed197f9c5a66d65759e7c4e4cba7b5a5d77940752bf1a59f2c7", - "sha256:f2522779d30fcbf0253e09714f274ce8c608cb6ebcd67922b1c54de59faba702" - ], - "markers": "python_version >= '3.9'", - "version": "==2024.4.16" - }, - "sphinx-autodoc-typehints": { - "hashes": [ - "sha256:6c841db55e0e9be0483ff3962a2152b60e79306f4288d8c4e7e86ac84486a5ea", - "sha256:9be46aeeb1b315eb5df1f3a7cb262149895d16c7d7dcd77b92513c3c3a1e85e6" - ], - "markers": "python_version >= '3.7'", - "version": "==1.19.1" - }, - "sphinx-basic-ng": { - "hashes": [ - "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9", - "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b" - ], - "markers": "python_version >= '3.7'", - "version": "==1.0.0b2" - }, - "sphinx-copybutton": { - "hashes": [ - "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd", - "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e" - ], - "markers": "python_version >= '3.7'", - "version": "==0.5.2" - }, - "sphinxcontrib-applehelp": { - "hashes": [ - "sha256:c40a4f96f3776c4393d933412053962fac2b84f4c99a7982ba42e09576a70619", - "sha256:cb61eb0ec1b61f349e5cc36b2028e9e7ca765be05e49641c97241274753067b4" - ], - "markers": "python_version >= '3.9'", - "version": "==1.0.8" - }, - "sphinxcontrib-devhelp": { - "hashes": [ - "sha256:6485d09629944511c893fa11355bda18b742b83a2b181f9a009f7e500595c90f", - "sha256:9893fd3f90506bc4b97bdb977ceb8fbd823989f4316b28c3841ec128544372d3" - ], - "markers": "python_version >= '3.9'", - "version": "==1.0.6" - }, - "sphinxcontrib-htmlhelp": { - "hashes": [ - "sha256:0dc87637d5de53dd5eec3a6a01753b1ccf99494bd756aafecd74b4fa9e729015", - "sha256:393f04f112b4d2f53d93448d4bce35842f62b307ccdc549ec1585e950bc35e04" - ], - "markers": "python_version >= '3.9'", - "version": "==2.0.5" - }, - "sphinxcontrib-jsmath": { - "hashes": [ - "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", - "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" - ], - "markers": "python_version >= '3.5'", - "version": "==1.0.1" - }, - "sphinxcontrib-qthelp": { - "hashes": [ - "sha256:053dedc38823a80a7209a80860b16b722e9e0209e32fea98c90e4e6624588ed6", - "sha256:e2ae3b5c492d58fcbd73281fbd27e34b8393ec34a073c792642cd8e529288182" - ], - "markers": "python_version >= '3.9'", - "version": "==1.0.7" - }, - "sphinxcontrib-serializinghtml": { - "hashes": [ - "sha256:326369b8df80a7d2d8d7f99aa5ac577f51ea51556ed974e7716cfd4fca3f6cb7", - "sha256:93f3f5dc458b91b192fe10c397e324f262cf163d79f3282c158e8436a2c4511f" - ], - "markers": "python_version >= '3.9'", - "version": "==1.1.10" - }, - "sqlalchemy": { - "hashes": [ - "sha256:03e08af7a5f9386a43919eda9de33ffda16b44eb11f3b313e6822243770e9763", - "sha256:0572f4bd6f94752167adfd7c1bed84f4b240ee6203a95e05d1e208d488d0d436", - "sha256:07b441f7d03b9a66299ce7ccf3ef2900abc81c0db434f42a5694a37bd73870f2", - "sha256:1bc330d9d29c7f06f003ab10e1eaced295e87940405afe1b110f2eb93a233588", - "sha256:1e0d612a17581b6616ff03c8e3d5eff7452f34655c901f75d62bd86449d9750e", - "sha256:23623166bfefe1487d81b698c423f8678e80df8b54614c2bf4b4cfcd7c711959", - "sha256:2519f3a5d0517fc159afab1015e54bb81b4406c278749779be57a569d8d1bb0d", - "sha256:28120ef39c92c2dd60f2721af9328479516844c6b550b077ca450c7d7dc68575", - "sha256:37350015056a553e442ff672c2d20e6f4b6d0b2495691fa239d8aa18bb3bc908", - "sha256:39769a115f730d683b0eb7b694db9789267bcd027326cccc3125e862eb03bfd8", - "sha256:3c01117dd36800f2ecaa238c65365b7b16497adc1522bf84906e5710ee9ba0e8", - "sha256:3d6718667da04294d7df1670d70eeddd414f313738d20a6f1d1f379e3139a545", - "sha256:3dbb986bad3ed5ceaf090200eba750b5245150bd97d3e67343a3cfed06feecf7", - "sha256:4557e1f11c5f653ebfdd924f3f9d5ebfc718283b0b9beebaa5dd6b77ec290971", - "sha256:46331b00096a6db1fdc052d55b101dbbfc99155a548e20a0e4a8e5e4d1362855", - "sha256:4a121d62ebe7d26fec9155f83f8be5189ef1405f5973ea4874a26fab9f1e262c", - "sha256:4f5e9cd989b45b73bd359f693b935364f7e1f79486e29015813c338450aa5a71", - "sha256:50aae840ebbd6cdd41af1c14590e5741665e5272d2fee999306673a1bb1fdb4d", - "sha256:59b1ee96617135f6e1d6f275bbe988f419c5178016f3d41d3c0abb0c819f75bb", - "sha256:59b8f3adb3971929a3e660337f5dacc5942c2cdb760afcabb2614ffbda9f9f72", - "sha256:66bffbad8d6271bb1cc2f9a4ea4f86f80fe5e2e3e501a5ae2a3dc6a76e604e6f", - "sha256:69f93723edbca7342624d09f6704e7126b152eaed3cdbb634cb657a54332a3c5", - "sha256:6a440293d802d3011028e14e4226da1434b373cbaf4a4bbb63f845761a708346", - "sha256:72c28b84b174ce8af8504ca28ae9347d317f9dba3999e5981a3cd441f3712e24", - "sha256:79d2e78abc26d871875b419e1fd3c0bca31a1cb0043277d0d850014599626c2e", - "sha256:7f2767680b6d2398aea7082e45a774b2b0767b5c8d8ffb9c8b683088ea9b29c5", - "sha256:8318f4776c85abc3f40ab185e388bee7a6ea99e7fa3a30686580b209eaa35c08", - "sha256:8958b10490125124463095bbdadda5aa22ec799f91958e410438ad6c97a7b793", - "sha256:8c78ac40bde930c60e0f78b3cd184c580f89456dd87fc08f9e3ee3ce8765ce88", - "sha256:90812a8933df713fdf748b355527e3af257a11e415b613dd794512461eb8a686", - "sha256:9bc633f4ee4b4c46e7adcb3a9b5ec083bf1d9a97c1d3854b92749d935de40b9b", - "sha256:9e46ed38affdfc95d2c958de328d037d87801cfcbea6d421000859e9789e61c2", - "sha256:9fe53b404f24789b5ea9003fc25b9a3988feddebd7e7b369c8fac27ad6f52f28", - "sha256:a4e46a888b54be23d03a89be510f24a7652fe6ff660787b96cd0e57a4ebcb46d", - "sha256:a86bfab2ef46d63300c0f06936bd6e6c0105faa11d509083ba8f2f9d237fb5b5", - "sha256:ac9dfa18ff2a67b09b372d5db8743c27966abf0e5344c555d86cc7199f7ad83a", - "sha256:af148a33ff0349f53512a049c6406923e4e02bf2f26c5fb285f143faf4f0e46a", - "sha256:b11d0cfdd2b095e7b0686cf5fabeb9c67fae5b06d265d8180715b8cfa86522e3", - "sha256:b2985c0b06e989c043f1dc09d4fe89e1616aadd35392aea2844f0458a989eacf", - "sha256:b544ad1935a8541d177cb402948b94e871067656b3a0b9e91dbec136b06a2ff5", - "sha256:b5cc79df7f4bc3d11e4b542596c03826063092611e481fcf1c9dfee3c94355ef", - "sha256:b817d41d692bf286abc181f8af476c4fbef3fd05e798777492618378448ee689", - "sha256:b81ee3d84803fd42d0b154cb6892ae57ea6b7c55d8359a02379965706c7efe6c", - "sha256:be9812b766cad94a25bc63bec11f88c4ad3629a0cec1cd5d4ba48dc23860486b", - "sha256:c245b1fbade9c35e5bd3b64270ab49ce990369018289ecfde3f9c318411aaa07", - "sha256:c3f3631693003d8e585d4200730616b78fafd5a01ef8b698f6967da5c605b3fa", - "sha256:c4ae3005ed83f5967f961fd091f2f8c5329161f69ce8480aa8168b2d7fe37f06", - "sha256:c54a1e53a0c308a8e8a7dffb59097bff7facda27c70c286f005327f21b2bd6b1", - "sha256:d0ddd9db6e59c44875211bc4c7953a9f6638b937b0a88ae6d09eb46cced54eff", - "sha256:dc022184d3e5cacc9579e41805a681187650e170eb2fd70e28b86192a479dcaa", - "sha256:e32092c47011d113dc01ab3e1d3ce9f006a47223b18422c5c0d150af13a00687", - "sha256:f7b64e6ec3f02c35647be6b4851008b26cff592a95ecb13b6788a54ef80bbdd4", - "sha256:f942a799516184c855e1a32fbc7b29d7e571b52612647866d4ec1c3242578fcb", - "sha256:f9511d8dd4a6e9271d07d150fb2f81874a3c8c95e11ff9af3a2dfc35fe42ee44", - "sha256:fd3a55deef00f689ce931d4d1b23fa9f04c880a48ee97af488fd215cf24e2a6c", - "sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e", - "sha256:fdf3386a801ea5aba17c6410dd1dc8d39cf454ca2565541b5ac42a84e1e28f53" - ], - "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==2.0.36" - }, - "starlette": { - "hashes": [ - "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee", - "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823" - ], - "markers": "python_version >= '3.8'", - "version": "==0.37.2" - }, - "telethon": { - "hashes": [ - "sha256:e5e43cff1c1b34e2f9c2b395215beb6e9bda706b69def7efff4f55b23c9c4374" - ], - "markers": "python_version >= '3.5'", - "version": "==1.37.0" - }, - "text-unidecode": { - "hashes": [ - "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", - "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93" - ], - "version": "==1.3" - }, - "tiktok-downloader": { - "hashes": [ - "sha256:f376ba0d2517fbab87b3185784d6e19481543326121427ae0986b9fdef6f4f75" - ], - "version": "==0.3.5" - }, - "tomli": { - "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" - ], - "markers": "python_version >= '3.7'", - "version": "==2.0.1" - }, - "tornado": { - "hashes": [ - "sha256:02ccefc7d8211e5a7f9e8bc3f9e5b0ad6262ba2fbb683a6443ecc804e5224ce0", - "sha256:10aeaa8006333433da48dec9fe417877f8bcc21f48dda8d661ae79da357b2a63", - "sha256:27787de946a9cffd63ce5814c33f734c627a87072ec7eed71f7fc4417bb16263", - "sha256:6f8a6c77900f5ae93d8b4ae1196472d0ccc2775cc1dfdc9e7727889145c45052", - "sha256:71ddfc23a0e03ef2df1c1397d859868d158c8276a0603b96cf86892bff58149f", - "sha256:72291fa6e6bc84e626589f1c29d90a5a6d593ef5ae68052ee2ef000dfd273dee", - "sha256:88b84956273fbd73420e6d4b8d5ccbe913c65d31351b4c004ae362eba06e1f78", - "sha256:e43bc2e5370a6a8e413e1e1cd0c91bedc5bd62a74a532371042a18ef19e10579", - "sha256:f0251554cdd50b4b44362f73ad5ba7126fc5b2c2895cc62b14a1c2d7ea32f212", - "sha256:f7894c581ecdcf91666a0912f18ce5e757213999e183ebfc2c3fdbf4d5bd764e", - "sha256:fd03192e287fbd0899dd8f81c6fb9cbbc69194d2074b38f384cb6fa72b80e9c2" - ], - "markers": "python_version >= '3.8'", - "version": "==6.4" - }, - "tqdm": { - "hashes": [ - "sha256:90279a3770753eafc9194a0364852159802111925aa30eb3f9d85b0e805ac7cd", - "sha256:e1020aef2e5096702d8a025ac7d16b1577279c9d63f8375b63083e9a5f0fcbad" - ], - "markers": "python_version >= '3.7'", - "version": "==4.66.5" - }, - "trio": { - "hashes": [ - "sha256:1dcc95ab1726b2da054afea8fd761af74bad79bd52381b84eae408e983c76831", - "sha256:68eabbcf8f457d925df62da780eff15ff5dc68fd6b367e2dde59f7aaf2a0b884" - ], - "markers": "python_version >= '3.8'", - "version": "==0.27.0" - }, - "trio-websocket": { - "hashes": [ - "sha256:18c11793647703c158b1f6e62de638acada927344d534e3c7628eedcb746839f", - "sha256:520d046b0d030cf970b8b2b2e00c4c2245b3807853ecd44214acd33d74581638" - ], - "markers": "python_version >= '3.7'", - "version": "==0.11.1" - }, - "tsp-client": { - "hashes": [ - "sha256:0b790d10a68d66782c13f1d7cc7f5206df26b49826c1da80944b7c05b1731784", - "sha256:6e66148dd116322eb44a7484e5ad33bbe640b997343c443de9cc70fc5eb19987" - ], - "version": "==0.2.0" - }, - "twine": { - "hashes": [ - "sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997", - "sha256:9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db" - ], - "markers": "python_version >= '3.8'", - "version": "==5.1.1" - }, - "typing-extensions": { - "hashes": [ - "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", - "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" - ], - "markers": "python_version >= '3.8'", - "version": "==4.12.2" - }, - "typing-inspect": { - "hashes": [ - "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", - "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78" - ], - "version": "==0.9.0" - }, - "tzdata": { - "hashes": [ - "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc", - "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd" - ], - "markers": "python_version >= '2'", - "version": "==2024.2" - }, - "tzlocal": { - "hashes": [ - "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8", - "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e" - ], - "markers": "python_version >= '3.8'", - "version": "==5.2" - }, - "uritemplate": { - "hashes": [ - "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0", - "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e" - ], - "markers": "python_version >= '3.6'", - "version": "==4.1.1" - }, - "urllib3": { - "extras": [ - "socks" - ], - "hashes": [ - "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", - "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" - ], - "markers": "python_version >= '3.8'", - "version": "==2.2.2" - }, - "uvicorn": { - "hashes": [ - "sha256:cd17daa7f3b9d7a24de3617820e634d0933b69eed8e33a516071174427238c81", - "sha256:d46cd8e0fd80240baffbcd9ec1012a712938754afcf81bce56c024c1656aece8" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==0.30.1" - }, - "vine": { - "hashes": [ - "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", - "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0" - ], - "markers": "python_version >= '3.6'", - "version": "==5.1.0" - }, - "vk-api": { - "hashes": [ - "sha256:c71021506449afe5b9bbb1c4acb0d86b35a007ddc21678478e46fbbeabd1f3ef", - "sha256:c7741e40bc05980c91ed94c84542e1e7e7370e101b5eaa74222958d4130fe3c2" - ], - "version": "==11.9.9" - }, - "vk-url-scraper": { - "hashes": [ - "sha256:2e83e690844bb9b04772fae56bed2d9654780ca23132155e63de4ed9bde70c23", - "sha256:6616c8fbe6ea6f8cbe4605898a89d1173a579ed6a9da5410dba80269d708fcb1" - ], - "markers": "python_version >= '3.10'", - "version": "==0.3.30" - }, - "warcio": { - "hashes": [ - "sha256:ced1a162d76434d56abd81b37ac152821d1a11e1db835ead5d649f58068c2203", - "sha256:e1889dad9ecac654de5b0973247f335a55827b1b14a8203772d18c749143ea51" - ], - "version": "==1.7.4" - }, - "watchfiles": { - "hashes": [ - "sha256:00095dd368f73f8f1c3a7982a9801190cc88a2f3582dd395b289294f8975172b", - "sha256:00ad0bcd399503a84cc688590cdffbe7a991691314dde5b57b3ed50a41319a31", - "sha256:00f39592cdd124b4ec5ed0b1edfae091567c72c7da1487ae645426d1b0ffcad1", - "sha256:030bc4e68d14bcad2294ff68c1ed87215fbd9a10d9dea74e7cfe8a17869785ab", - "sha256:052d668a167e9fc345c24203b104c313c86654dd6c0feb4b8a6dfc2462239249", - "sha256:067dea90c43bf837d41e72e546196e674f68c23702d3ef80e4e816937b0a3ffd", - "sha256:0b04a2cbc30e110303baa6d3ddce8ca3664bc3403be0f0ad513d1843a41c97d1", - "sha256:0bc3b2f93a140df6806c8467c7f51ed5e55a931b031b5c2d7ff6132292e803d6", - "sha256:0c8e0aa0e8cc2a43561e0184c0513e291ca891db13a269d8d47cb9841ced7c71", - "sha256:103622865599f8082f03af4214eaff90e2426edff5e8522c8f9e93dc17caee13", - "sha256:1235c11510ea557fe21be5d0e354bae2c655a8ee6519c94617fe63e05bca4171", - "sha256:1cc0cba54f47c660d9fa3218158b8963c517ed23bd9f45fe463f08262a4adae1", - "sha256:1d9188979a58a096b6f8090e816ccc3f255f137a009dd4bbec628e27696d67c1", - "sha256:213792c2cd3150b903e6e7884d40660e0bcec4465e00563a5fc03f30ea9c166c", - "sha256:25c817ff2a86bc3de3ed2df1703e3d24ce03479b27bb4527c57e722f8554d971", - "sha256:2627a91e8110b8de2406d8b2474427c86f5a62bf7d9ab3654f541f319ef22bcb", - "sha256:280a4afbc607cdfc9571b9904b03a478fc9f08bbeec382d648181c695648202f", - "sha256:28324d6b28bcb8d7c1041648d7b63be07a16db5510bea923fc80b91a2a6cbed6", - "sha256:28585744c931576e535860eaf3f2c0ec7deb68e3b9c5a85ca566d69d36d8dd27", - "sha256:28f393c1194b6eaadcdd8f941307fc9bbd7eb567995232c830f6aef38e8a6e88", - "sha256:2abeb79209630da981f8ebca30a2c84b4c3516a214451bfc5f106723c5f45843", - "sha256:2bdadf6b90c099ca079d468f976fd50062905d61fae183f769637cb0f68ba59a", - "sha256:2f350cbaa4bb812314af5dab0eb8d538481e2e2279472890864547f3fe2281ed", - "sha256:3218a6f908f6a276941422b035b511b6d0d8328edd89a53ae8c65be139073f84", - "sha256:3973145235a38f73c61474d56ad6199124e7488822f3a4fc97c72009751ae3b0", - "sha256:3a0d883351a34c01bd53cfa75cd0292e3f7e268bacf2f9e33af4ecede7e21d1d", - "sha256:425440e55cd735386ec7925f64d5dde392e69979d4c8459f6bb4e920210407f2", - "sha256:4b9f2a128a32a2c273d63eb1fdbf49ad64852fc38d15b34eaa3f7ca2f0d2b797", - "sha256:4cc382083afba7918e32d5ef12321421ef43d685b9a67cc452a6e6e18920890e", - "sha256:52fc9b0dbf54d43301a19b236b4a4614e610605f95e8c3f0f65c3a456ffd7d35", - "sha256:55b7cc10261c2786c41d9207193a85c1db1b725cf87936df40972aab466179b6", - "sha256:581f0a051ba7bafd03e17127735d92f4d286af941dacf94bcf823b101366249e", - "sha256:5834e1f8b71476a26df97d121c0c0ed3549d869124ed2433e02491553cb468c2", - "sha256:5e45fb0d70dda1623a7045bd00c9e036e6f1f6a85e4ef2c8ae602b1dfadf7550", - "sha256:61af9efa0733dc4ca462347becb82e8ef4945aba5135b1638bfc20fad64d4f0e", - "sha256:68fe0c4d22332d7ce53ad094622b27e67440dacefbaedd29e0794d26e247280c", - "sha256:72a44e9481afc7a5ee3291b09c419abab93b7e9c306c9ef9108cb76728ca58d2", - "sha256:7a74436c415843af2a769b36bf043b6ccbc0f8d784814ba3d42fc961cdb0a9dc", - "sha256:8597b6f9dc410bdafc8bb362dac1cbc9b4684a8310e16b1ff5eee8725d13dcd6", - "sha256:8c39987a1397a877217be1ac0fb1d8b9f662c6077b90ff3de2c05f235e6a8f96", - "sha256:8c3e3675e6e39dc59b8fe5c914a19d30029e36e9f99468dddffd432d8a7b1c93", - "sha256:8dc1fc25a1dedf2dd952909c8e5cb210791e5f2d9bc5e0e8ebc28dd42fed7562", - "sha256:8fdebb655bb1ba0122402352b0a4254812717a017d2dc49372a1d47e24073795", - "sha256:9165bcab15f2b6d90eedc5c20a7f8a03156b3773e5fb06a790b54ccecdb73385", - "sha256:94ebe84a035993bb7668f58a0ebf998174fb723a39e4ef9fce95baabb42b787f", - "sha256:9624a68b96c878c10437199d9a8b7d7e542feddda8d5ecff58fdc8e67b460848", - "sha256:96eec15e5ea7c0b6eb5bfffe990fc7c6bd833acf7e26704eb18387fb2f5fd087", - "sha256:97b94e14b88409c58cdf4a8eaf0e67dfd3ece7e9ce7140ea6ff48b0407a593ec", - "sha256:988e981aaab4f3955209e7e28c7794acdb690be1efa7f16f8ea5aba7ffdadacb", - "sha256:a8a31bfd98f846c3c284ba694c6365620b637debdd36e46e1859c897123aa232", - "sha256:a927b3034d0672f62fb2ef7ea3c9fc76d063c4b15ea852d1db2dc75fe2c09696", - "sha256:ace7d060432acde5532e26863e897ee684780337afb775107c0a90ae8dbccfd2", - "sha256:aec83c3ba24c723eac14225194b862af176d52292d271c98820199110e31141e", - "sha256:b44b70850f0073b5fcc0b31ede8b4e736860d70e2dbf55701e05d3227a154a67", - "sha256:b610fb5e27825b570554d01cec427b6620ce9bd21ff8ab775fc3a32f28bba63e", - "sha256:b810a2c7878cbdecca12feae2c2ae8af59bea016a78bc353c184fa1e09f76b68", - "sha256:bbf8a20266136507abf88b0df2328e6a9a7c7309e8daff124dda3803306a9fdb", - "sha256:bd4c06100bce70a20c4b81e599e5886cf504c9532951df65ad1133e508bf20be", - "sha256:c2444dc7cb9d8cc5ab88ebe792a8d75709d96eeef47f4c8fccb6df7c7bc5be71", - "sha256:c49b76a78c156979759d759339fb62eb0549515acfe4fd18bb151cc07366629c", - "sha256:c4a65474fd2b4c63e2c18ac67a0c6c66b82f4e73e2e4d940f837ed3d2fd9d4da", - "sha256:c5af2347d17ab0bd59366db8752d9e037982e259cacb2ba06f2c41c08af02c39", - "sha256:c668228833c5619f6618699a2c12be057711b0ea6396aeaece4ded94184304ea", - "sha256:c7b978c384e29d6c7372209cbf421d82286a807bbcdeb315427687f8371c340a", - "sha256:d048ad5d25b363ba1d19f92dcf29023988524bee6f9d952130b316c5802069cb", - "sha256:d3e1f3cf81f1f823e7874ae563457828e940d75573c8fbf0ee66818c8b6a9099", - "sha256:d47e9ef1a94cc7a536039e46738e17cce058ac1593b2eccdede8bf72e45f372a", - "sha256:da1e0a8caebf17976e2ffd00fa15f258e14749db5e014660f53114b676e68538", - "sha256:dc1b9b56f051209be458b87edb6856a449ad3f803315d87b2da4c93b43a6fe72", - "sha256:dc2e8fe41f3cac0660197d95216c42910c2b7e9c70d48e6d84e22f577d106fc1", - "sha256:dc92d2d2706d2b862ce0568b24987eba51e17e14b79a1abcd2edc39e48e743c8", - "sha256:dd64f3a4db121bc161644c9e10a9acdb836853155a108c2446db2f5ae1778c3d", - "sha256:e0f0a874231e2839abbf473256efffe577d6ee2e3bfa5b540479e892e47c172d", - "sha256:f7e1f9c5d1160d03b93fc4b68a0aeb82fe25563e12fbcdc8507f8434ab6f823c", - "sha256:fe82d13461418ca5e5a808a9e40f79c1879351fcaeddbede094028e74d836e86" - ], - "markers": "python_version >= '3.8'", - "version": "==0.22.0" - }, - "wcwidth": { - "hashes": [ - "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", - "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5" - ], - "version": "==0.2.13" - }, - "webencodings": { - "hashes": [ - "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", - "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" - ], - "version": "==0.5.1" - }, - "websocket-client": { - "hashes": [ - "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", - "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da" - ], - "markers": "python_version >= '3.8'", - "version": "==1.8.0" - }, - "websockets": { - "hashes": [ - "sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b", - "sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6", - "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df", - "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b", - "sha256:1a9d160fd080c6285e202327aba140fc9a0d910b09e423afff4ae5cbbf1c7205", - "sha256:1bf386089178ea69d720f8db6199a0504a406209a0fc23e603b27b300fdd6892", - "sha256:1df2fbd2c8a98d38a66f5238484405b8d1d16f929bb7a33ed73e4801222a6f53", - "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2", - "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed", - "sha256:23509452b3bc38e3a057382c2e941d5ac2e01e251acce7adc74011d7d8de434c", - "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd", - "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b", - "sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931", - "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30", - "sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370", - "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be", - "sha256:2e5fc14ec6ea568200ea4ef46545073da81900a2b67b3e666f04adf53ad452ec", - "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf", - "sha256:3c6cc1360c10c17463aadd29dd3af332d4a1adaa8796f6b0e9f9df1fdb0bad62", - "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b", - "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402", - "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f", - "sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123", - "sha256:46e71dbbd12850224243f5d2aeec90f0aaa0f2dde5aeeb8fc8df21e04d99eff9", - "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603", - "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45", - "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558", - "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4", - "sha256:5f6ffe2c6598f7f7207eef9a1228b6f5c818f9f4d53ee920aacd35cec8110438", - "sha256:604428d1b87edbf02b233e2c207d7d528460fa978f9e391bd8aaf9c8311de137", - "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480", - "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447", - "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8", - "sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04", - "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c", - "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb", - "sha256:7fa3d25e81bfe6a89718e9791128398a50dec6d57faf23770787ff441d851967", - "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b", - "sha256:8572132c7be52632201a35f5e08348137f658e5ffd21f51f94572ca6c05ea81d", - "sha256:87b4aafed34653e465eb77b7c93ef058516cb5acf3eb21e42f33928616172def", - "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c", - "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92", - "sha256:9edf3fc590cc2ec20dc9d7a45108b5bbaf21c0d89f9fd3fd1685e223771dc0b2", - "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113", - "sha256:a02413bc474feda2849c59ed2dfb2cddb4cd3d2f03a2fedec51d6e959d9b608b", - "sha256:a1d9697f3337a89691e3bd8dc56dea45a6f6d975f92e7d5f773bc715c15dde28", - "sha256:a571f035a47212288e3b3519944f6bf4ac7bc7553243e41eac50dd48552b6df7", - "sha256:ab3d732ad50a4fbd04a4490ef08acd0517b6ae6b77eb967251f4c263011a990d", - "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f", - "sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468", - "sha256:b2ee7288b85959797970114deae81ab41b731f19ebcd3bd499ae9ca0e3f1d2c8", - "sha256:b81f90dcc6c85a9b7f29873beb56c94c85d6f0dac2ea8b60d995bd18bf3e2aae", - "sha256:ba0cab91b3956dfa9f512147860783a1829a8d905ee218a9837c18f683239611", - "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d", - "sha256:bbe6013f9f791944ed31ca08b077e26249309639313fff132bfbf3ba105673b9", - "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca", - "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f", - "sha256:c3181df4583c4d3994d31fb235dc681d2aaad744fbdbf94c4802485ececdecf2", - "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077", - "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2", - "sha256:cbe83a6bbdf207ff0541de01e11904827540aa069293696dd528a6640bd6a5f6", - "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374", - "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc", - "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e", - "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53", - "sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399", - "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547", - "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3", - "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870", - "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5", - "sha256:fc4e7fa5414512b481a2483775a8e8be7803a35b30ca805afa4998a84f9fd9e8", - "sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7" - ], - "markers": "python_version >= '3.8'", - "version": "==12.0" - }, - "werkzeug": { - "hashes": [ - "sha256:02c9eb92b7d6c06f31a782811505d2157837cea66aaede3e217c7c27c039476c", - "sha256:34f2371506b250df4d4f84bfe7b0921e4762525762bbd936614909fe25cd7306" - ], - "markers": "python_version >= '3.8'", - "version": "==3.0.4" - }, - "wsproto": { - "hashes": [ - "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", - "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736" - ], - "markers": "python_full_version >= '3.7.0'", - "version": "==1.2.0" - }, - "yarl": { - "hashes": [ - "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51", - "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce", - "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559", - "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0", - "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81", - "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc", - "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4", - "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c", - "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130", - "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136", - "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e", - "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec", - "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7", - "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1", - "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455", - "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099", - "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129", - "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10", - "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142", - "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98", - "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa", - "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7", - "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525", - "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c", - "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9", - "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c", - "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8", - "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b", - "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf", - "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23", - "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd", - "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27", - "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f", - "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece", - "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434", - "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec", - "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff", - "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78", - "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d", - "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863", - "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53", - "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31", - "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15", - "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5", - "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b", - "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57", - "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3", - "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1", - "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f", - "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad", - "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c", - "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7", - "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2", - "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b", - "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2", - "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b", - "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9", - "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be", - "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e", - "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984", - "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4", - "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074", - "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2", - "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392", - "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91", - "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541", - "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf", - "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572", - "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66", - "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575", - "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14", - "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5", - "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1", - "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e", - "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551", - "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17", - "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead", - "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0", - "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe", - "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234", - "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0", - "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7", - "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34", - "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42", - "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385", - "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78", - "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be", - "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958", - "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749", - "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec" - ], - "markers": "python_version >= '3.7'", - "version": "==1.9.4" - }, - "yt-dlp": { - "hashes": [ - "sha256:2a59d9e65ef6dadb1ff318346d04403664c3fa395e098fcd0d7ad626ef9f8a89", - "sha256:f4614e1c710fcb387bf152d2162868c565ed3f675647ecaa19dab54e581780eb" - ], - "markers": "python_version >= '3.8'", - "version": "==2024.7.15.232803.dev0" - }, - "zipp": { - "hashes": [ - "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19", - "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c" - ], - "markers": "python_version >= '3.8'", - "version": "==3.19.2" - } - }, - "develop": { - "anyio": { - "hashes": [ - "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94", - "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7" - ], - "markers": "python_version >= '3.8'", - "version": "==4.4.0" - }, - "certifi": { - "hashes": [ - "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", - "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90" - ], - "markers": "python_version >= '3.6'", - "version": "==2024.7.4" - }, - "coverage": { - "extras": [ - "toml" - ], - "hashes": [ - "sha256:0086cd4fc71b7d485ac93ca4239c8f75732c2ae3ba83f6be1c9be59d9e2c6382", - "sha256:01c322ef2bbe15057bc4bf132b525b7e3f7206f071799eb8aa6ad1940bcf5fb1", - "sha256:03cafe82c1b32b770a29fd6de923625ccac3185a54a5e66606da26d105f37dac", - "sha256:044a0985a4f25b335882b0966625270a8d9db3d3409ddc49a4eb00b0ef5e8cee", - "sha256:07ed352205574aad067482e53dd606926afebcb5590653121063fbf4e2175166", - "sha256:0d1b923fc4a40c5832be4f35a5dab0e5ff89cddf83bb4174499e02ea089daf57", - "sha256:0e7b27d04131c46e6894f23a4ae186a6a2207209a05df5b6ad4caee6d54a222c", - "sha256:1fad32ee9b27350687035cb5fdf9145bc9cf0a094a9577d43e909948ebcfa27b", - "sha256:289cc803fa1dc901f84701ac10c9ee873619320f2f9aff38794db4a4a0268d51", - "sha256:3c59105f8d58ce500f348c5b56163a4113a440dad6daa2294b5052a10db866da", - "sha256:46c3d091059ad0b9c59d1034de74a7f36dcfa7f6d3bde782c49deb42438f2450", - "sha256:482855914928c8175735a2a59c8dc5806cf7d8f032e4820d52e845d1f731dca2", - "sha256:49c76cdfa13015c4560702574bad67f0e15ca5a2872c6a125f6327ead2b731dd", - "sha256:4b03741e70fb811d1a9a1d75355cf391f274ed85847f4b78e35459899f57af4d", - "sha256:4bea27c4269234e06f621f3fac3925f56ff34bc14521484b8f66a580aacc2e7d", - "sha256:4d5fae0a22dc86259dee66f2cc6c1d3e490c4a1214d7daa2a93d07491c5c04b6", - "sha256:543ef9179bc55edfd895154a51792b01c017c87af0ebaae092720152e19e42ca", - "sha256:54dece71673b3187c86226c3ca793c5f891f9fc3d8aa183f2e3653da18566169", - "sha256:6379688fb4cfa921ae349c76eb1a9ab26b65f32b03d46bb0eed841fd4cb6afb1", - "sha256:65fa405b837060db569a61ec368b74688f429b32fa47a8929a7a2f9b47183713", - "sha256:6616d1c9bf1e3faea78711ee42a8b972367d82ceae233ec0ac61cc7fec09fa6b", - "sha256:6fe885135c8a479d3e37a7aae61cbd3a0fb2deccb4dda3c25f92a49189f766d6", - "sha256:7221f9ac9dad9492cecab6f676b3eaf9185141539d5c9689d13fd6b0d7de840c", - "sha256:76d5f82213aa78098b9b964ea89de4617e70e0d43e97900c2778a50856dac605", - "sha256:7792f0ab20df8071d669d929c75c97fecfa6bcab82c10ee4adb91c7a54055463", - "sha256:831b476d79408ab6ccfadaaf199906c833f02fdb32c9ab907b1d4aa0713cfa3b", - "sha256:9146579352d7b5f6412735d0f203bbd8d00113a680b66565e205bc605ef81bc6", - "sha256:9cc44bf0315268e253bf563f3560e6c004efe38f76db03a1558274a6e04bf5d5", - "sha256:a73d18625f6a8a1cbb11eadc1d03929f9510f4131879288e3f7922097a429f63", - "sha256:a8659fd33ee9e6ca03950cfdcdf271d645cf681609153f218826dd9805ab585c", - "sha256:a94925102c89247530ae1dab7dc02c690942566f22e189cbd53579b0693c0783", - "sha256:ad4567d6c334c46046d1c4c20024de2a1c3abc626817ae21ae3da600f5779b44", - "sha256:b2e16f4cd2bc4d88ba30ca2d3bbf2f21f00f382cf4e1ce3b1ddc96c634bc48ca", - "sha256:bbdf9a72403110a3bdae77948b8011f644571311c2fb35ee15f0f10a8fc082e8", - "sha256:beb08e8508e53a568811016e59f3234d29c2583f6b6e28572f0954a6b4f7e03d", - "sha256:c4cbe651f3904e28f3a55d6f371203049034b4ddbce65a54527a3f189ca3b390", - "sha256:c7b525ab52ce18c57ae232ba6f7010297a87ced82a2383b1afd238849c1ff933", - "sha256:ca5d79cfdae420a1d52bf177de4bc2289c321d6c961ae321503b2ca59c17ae67", - "sha256:cdab02a0a941af190df8782aafc591ef3ad08824f97850b015c8c6a8b3877b0b", - "sha256:d17c6a415d68cfe1091d3296ba5749d3d8696e42c37fca5d4860c5bf7b729f03", - "sha256:d39bd10f0ae453554798b125d2f39884290c480f56e8a02ba7a6ed552005243b", - "sha256:d4b3cd1ca7cd73d229487fa5caca9e4bc1f0bca96526b922d61053ea751fe791", - "sha256:d50a252b23b9b4dfeefc1f663c568a221092cbaded20a05a11665d0dbec9b8fb", - "sha256:da8549d17489cd52f85a9829d0e1d91059359b3c54a26f28bec2c5d369524807", - "sha256:dcd070b5b585b50e6617e8972f3fbbee786afca71b1936ac06257f7e178f00f6", - "sha256:ddaaa91bfc4477d2871442bbf30a125e8fe6b05da8a0015507bfbf4718228ab2", - "sha256:df423f351b162a702c053d5dddc0fc0ef9a9e27ea3f449781ace5f906b664428", - "sha256:dff044f661f59dace805eedb4a7404c573b6ff0cdba4a524141bc63d7be5c7fd", - "sha256:e7e128f85c0b419907d1f38e616c4f1e9f1d1b37a7949f44df9a73d5da5cd53c", - "sha256:ed8d1d1821ba5fc88d4a4f45387b65de52382fa3ef1f0115a4f7a20cdfab0e94", - "sha256:f2501d60d7497fd55e391f423f965bbe9e650e9ffc3c627d5f0ac516026000b8", - "sha256:f7db0b6ae1f96ae41afe626095149ecd1b212b424626175a6633c2999eaad45b" - ], - "markers": "python_version >= '3.8'", - "version": "==7.6.0" - }, - "exceptiongroup": { - "hashes": [ - "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", - "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc" - ], - "markers": "python_version >= '3.7'", - "version": "==1.2.2" - }, - "h11": { - "hashes": [ - "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", - "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761" - ], - "markers": "python_version >= '3.7'", - "version": "==0.14.0" - }, - "httpcore": { - "hashes": [ - "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f", - "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f" - ], - "markers": "python_version >= '3.8'", - "version": "==1.0.6" - }, - "httpx": { - "hashes": [ - "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", - "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2" - ], - "markers": "python_version >= '3.8'", - "version": "==0.27.2" - }, - "idna": { - "hashes": [ - "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", - "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" - ], - "markers": "python_version >= '3.5'", - "version": "==3.7" - }, - "iniconfig": { - "hashes": [ - "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", - "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" - ], - "markers": "python_version >= '3.7'", - "version": "==2.0.0" - }, - "packaging": { - "hashes": [ - "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", - "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" - ], - "markers": "python_version >= '3.8'", - "version": "==24.1" - }, - "pluggy": { - "hashes": [ - "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", - "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669" - ], - "markers": "python_version >= '3.8'", - "version": "==1.5.0" - }, - "pytest": { - "hashes": [ - "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343", - "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977" - ], - "markers": "python_version >= '3.8'", - "version": "==8.2.2" - }, - "pytest-asyncio": { - "hashes": [ - "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b", - "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==0.24.0" - }, - "sniffio": { - "hashes": [ - "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", - "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" - ], - "markers": "python_version >= '3.7'", - "version": "==1.3.1" - }, - "tomli": { - "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" - ], - "markers": "python_version >= '3.7'", - "version": "==2.0.1" - }, - "typing-extensions": { - "hashes": [ - "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", - "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" - ], - "markers": "python_version >= '3.8'", - "version": "==4.12.2" - }, - "watchdog": { - "hashes": [ - "sha256:0f9332243355643d567697c3e3fa07330a1d1abf981611654a1f2bf2175612b7", - "sha256:1021223c08ba8d2d38d71ec1704496471ffd7be42cfb26b87cd5059323a389a1", - "sha256:108f42a7f0345042a854d4d0ad0834b741d421330d5f575b81cb27b883500176", - "sha256:1e9679245e3ea6498494b3028b90c7b25dbb2abe65c7d07423ecfc2d6218ff7c", - "sha256:223160bb359281bb8e31c8f1068bf71a6b16a8ad3d9524ca6f523ac666bb6a1e", - "sha256:26dd201857d702bdf9d78c273cafcab5871dd29343748524695cecffa44a8d97", - "sha256:294b7a598974b8e2c6123d19ef15de9abcd282b0fbbdbc4d23dfa812959a9e05", - "sha256:349c9488e1d85d0a58e8cb14222d2c51cbc801ce11ac3936ab4c3af986536926", - "sha256:49f4d36cb315c25ea0d946e018c01bb028048023b9e103d3d3943f58e109dd45", - "sha256:53a3f10b62c2d569e260f96e8d966463dec1a50fa4f1b22aec69e3f91025060e", - "sha256:53adf73dcdc0ef04f7735066b4a57a4cd3e49ef135daae41d77395f0b5b692cb", - "sha256:560135542c91eaa74247a2e8430cf83c4342b29e8ad4f520ae14f0c8a19cfb5b", - "sha256:720ef9d3a4f9ca575a780af283c8fd3a0674b307651c1976714745090da5a9e8", - "sha256:752fb40efc7cc8d88ebc332b8f4bcbe2b5cc7e881bccfeb8e25054c00c994ee3", - "sha256:78864cc8f23dbee55be34cc1494632a7ba30263951b5b2e8fc8286b95845f82c", - "sha256:85527b882f3facda0579bce9d743ff7f10c3e1e0db0a0d0e28170a7d0e5ce2ea", - "sha256:90a67d7857adb1d985aca232cc9905dd5bc4803ed85cfcdcfcf707e52049eda7", - "sha256:91b522adc25614cdeaf91f7897800b82c13b4b8ac68a42ca959f992f6990c490", - "sha256:9413384f26b5d050b6978e6fcd0c1e7f0539be7a4f1a885061473c5deaa57221", - "sha256:94d11b07c64f63f49876e0ab8042ae034674c8653bfcdaa8c4b32e71cfff87e8", - "sha256:950f531ec6e03696a2414b6308f5c6ff9dab7821a768c9d5788b1314e9a46ca7", - "sha256:a2e8f3f955d68471fa37b0e3add18500790d129cc7efe89971b8a4cc6fdeb0b2", - "sha256:ae6deb336cba5d71476caa029ceb6e88047fc1dc74b62b7c4012639c0b563906", - "sha256:b8ca4d854adcf480bdfd80f46fdd6fb49f91dd020ae11c89b3a79e19454ec627", - "sha256:c66f80ee5b602a9c7ab66e3c9f36026590a0902db3aea414d59a2f55188c1f49", - "sha256:d52db5beb5e476e6853da2e2d24dbbbed6797b449c8bf7ea118a4ee0d2c9040e", - "sha256:dd021efa85970bd4824acacbb922066159d0f9e546389a4743d56919b6758b91", - "sha256:e25adddab85f674acac303cf1f5835951345a56c5f7f582987d266679979c75b", - "sha256:f00b4cf737f568be9665563347a910f8bdc76f88c2970121c86243c8cfdf90e9", - "sha256:f01f4a3565a387080dc49bdd1fefe4ecc77f894991b88ef927edbfa45eb10818" - ], - "index": "pypi", - "markers": "python_version >= '3.9'", - "version": "==5.0.3" - } - } -} diff --git a/src/core/events.py b/src/core/events.py deleted file mode 100644 index e9bbfbc..0000000 --- a/src/core/events.py +++ /dev/null @@ -1,41 +0,0 @@ -import asyncio -import logging -import alembic.config -from fastapi import FastAPI -from contextlib import asynccontextmanager -from fastapi_utils.tasks import repeat_every -from loguru import logger - -from db import crud, models -from db.database import get_db, make_engine -from shared.settings import get_settings -from utils.metrics import measure_regular_metrics, redis_subscribe_worker_exceptions - - -@asynccontextmanager -async def lifespan(app: FastAPI): - # see https://fastapi.tiangolo.com/advanced/events/#lifespan - - # STARTUP - engine = make_engine(get_settings().DATABASE_PATH) - models.Base.metadata.create_all(bind=engine) - alembic.config.main(argv=['--raiseerr', 'upgrade', 'head']) - # disabling uvicorn logger since we use loguru in logging_middleware - logging.getLogger("uvicorn.access").disabled = True - asyncio.create_task(redis_subscribe_worker_exceptions(get_settings().REDIS_EXCEPTIONS_CHANNEL, get_settings().CELERY_BROKER_URL)) - asyncio.create_task(repeat_measure_regular_metrics()) - with get_db() as db: - crud.upsert_user_groups(db) - - yield # separates startup from shutdown instructions - - # SHUTDOWN - logger.info("shutting down") - - -# CRON JOBS - - -@repeat_every(seconds=get_settings().REPEAT_COUNT_METRICS_SECONDS) -async def repeat_measure_regular_metrics(): - await measure_regular_metrics(get_settings().DATABASE_PATH, get_settings().REPEAT_COUNT_METRICS_SECONDS) diff --git a/src/core/logging.py b/src/core/logging.py deleted file mode 100644 index 5ff03db..0000000 --- a/src/core/logging.py +++ /dev/null @@ -1,26 +0,0 @@ -import traceback -from loguru import logger -from fastapi import Request - - -# logging configurations -logger.add("logs/api_logs.log", retention="30 days", rotation="3 days") -logger.add("logs/error_logs.log", retention="30 days", level="ERROR") - - -def log_error(e: Exception, traceback_str: str = None, extra:str = ""): - # EXCEPTION_COUNTER.labels(type(e).__name__).inc() - if not traceback_str: traceback_str = traceback.format_exc() - if extra: extra = f"{extra}\n" - logger.error(f"{extra}{e.__class__.__name__}: {e}\n{traceback_str}") - -async def logging_middleware(request: Request, call_next): - try: - response = await call_next(request) - logger.info(f"{request.client.host}:{request.client.port} {request.method} {request.url._url} - HTTP {response.status_code}") - return response - except Exception as e: - from utils.metrics import EXCEPTION_COUNTER - EXCEPTION_COUNTER.labels(type(e).__name__).inc() - log_error(e) - raise e \ No newline at end of file diff --git a/src/db/README.md b/src/db/README.md deleted file mode 100644 index c92c465..0000000 --- a/src/db/README.md +++ /dev/null @@ -1 +0,0 @@ -based on https://fastapi-users.github.io/fastapi-users/10.4/configuration/oauth/ \ No newline at end of file diff --git a/src/db/__init__.py b/src/db/__init__.py deleted file mode 100644 index 2901713..0000000 --- a/src/db/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# https://fastapi.tiangolo.com/tutorial/sql-databases/#review-all-the-files \ No newline at end of file diff --git a/src/db/crud.py b/src/db/crud.py deleted file mode 100644 index 7f9c67f..0000000 --- a/src/db/crud.py +++ /dev/null @@ -1,259 +0,0 @@ -from collections import defaultdict -from functools import cache -from sqlalchemy.orm import Session, load_only -from sqlalchemy import Column, or_, func -from loguru import logger -from datetime import datetime, timedelta - -from core.config import ALLOW_ANY_EMAIL -from shared.settings import get_settings -from . import models, schemas -import yaml - -DATABASE_QUERY_LIMIT = get_settings().DATABASE_QUERY_LIMIT - -# --------------- TASK = Archive - - -def get_limit(user_limit: int): - return max(1, min(user_limit, DATABASE_QUERY_LIMIT)) - - -def get_archive(db: Session, id: str, email: str): - email = email.lower() - query = base_query(db).filter(models.Archive.id == id) - if email != ALLOW_ANY_EMAIL: - groups = get_user_groups(db, email) - query = query.filter(or_(models.Archive.public == True, models.Archive.author_id == email, models.Archive.group_id.in_(groups))) - return query.first() - - -def search_archives_by_url(db: Session, url: str, email: str, skip: int = 0, limit: int = 100, archived_after: datetime = None, archived_before: datetime = None, absolute_search: bool = False): - # searches for partial URLs, if email is * no ownership filtering happens - query = base_query(db) - if email != ALLOW_ANY_EMAIL: - email = email.lower() - groups = get_user_groups(db, email) - query = query.filter(or_(models.Archive.public == True, models.Archive.author_id == email, models.Archive.group_id.in_(groups))) - if absolute_search: - query = query.filter(models.Archive.url == url) - else: - query = query.filter(models.Archive.url.like(f'%{url}%')) - if archived_after: - query = query.filter(models.Archive.created_at > archived_after) - if archived_before: - query = query.filter(models.Archive.created_at < archived_before) - return query.order_by(models.Archive.created_at.desc()).offset(skip).limit(get_limit(limit)).all() - - -def search_archives_by_email(db: Session, email: str, skip: int = 0, limit: int = 100): - email = email.lower() - return base_query(db).filter(models.Archive.author_id == email).order_by(models.Archive.created_at.desc()).offset(skip).limit(get_limit(limit)).all() - - -def create_task(db: Session, task: schemas.ArchiveCreate, tags: list[models.Tag], urls: list[models.ArchiveUrl]): - db_task = models.Archive(id=task.id, url=task.url, result=task.result, public=task.public, author_id=task.author_id, group_id=task.group_id) - db_task.tags = tags - db_task.urls = urls - db.add(db_task) - db.commit() - db.refresh(db_task) - return db_task - - -def soft_delete_task(db: Session, task_id: str, email: str) -> bool: - # TODO: implement hard-delete with cronjob that deletes from S3 - db_task = db.query(models.Archive).filter(models.Archive.id == task_id, models.Archive.author_id == email, models.Archive.deleted == False).first() - if db_task: - db_task.deleted = True - db.commit() - return db_task is not None - - -def count_archives(db: Session): - return db.query(func.count(models.Archive.id)).scalar() - - -def count_archive_urls(db: Session): - return db.query(func.count(models.ArchiveUrl.url)).scalar() - - -def count_users(db: Session): - return db.query(func.count(models.User.email)).scalar() - - -def count_by_user_since(db: Session, seconds_delta: int = 15): - time_threshold = datetime.now() - timedelta(seconds=seconds_delta) - return db.query(models.Archive.author_id, func.count().label('total'))\ - .filter(models.Archive.created_at >= time_threshold)\ - .group_by(models.Archive.author_id)\ - .order_by(func.count().desc())\ - .limit(500).all() - - -def base_query(db: Session): - # NOTE: load_only is for optimization and not obfuscation, use .with_entities() if needed - return db.query(models.Archive)\ - .filter(models.Archive.deleted == False)\ - .options(load_only(models.Archive.id, models.Archive.created_at, models.Archive.url, models.Archive.result)) - -# --------------- TAG - - -def create_tag(db: Session, tag: str): - db_tag = db.query(models.Tag).filter(models.Tag.id == tag).first() - if not db_tag: - db_tag = models.Tag(id=tag) - db.add(db_tag) - db.commit() - db.refresh(db_tag) - return db_tag - - -def is_active_user(db: Session, email: str) -> bool: - email = email.lower() - if not email or not len(email) or "@" not in email: return False - domain = email.split('@')[1] - - explicitly_active = db.query(models.User).filter(models.User.email == email, models.User.is_active == True).first() is not None - if explicitly_active: return True - - return db.query(models.Group).filter(models.Group.domains.contains(domain)).first() is not None - - -def is_user_in_group(db: Session, group_name: str, email: str) -> models.Group: - if email == ALLOW_ANY_EMAIL: return True - return len(group_name) and len(email) and group_name in get_user_groups(db, email) - - -def get_user_groups(db: Session, email: str): - """ - given an email retrieves the user groups from the DB and then the email-domain groups from a global variable, the email does not need to belong to an existing user. User does not need to be active. - """ - if not email or not len(email) or "@" not in email: return [] - email = email.lower() - - # get user groups - user_groups = db.query(models.association_table_user_groups).filter_by(user_id=email).with_entities(Column("group_id")).all() - user_level_groups = [g[0] for g in user_groups] - - # get domain groups - domain = email.split('@')[1] - domain_level_groups = db.query(models.Group.id).filter(models.Group.domains.contains(domain)).with_entities(Column("id")).all() - domain_level_groups = [g[0] for g in domain_level_groups] - - # combine and return - return list(set(user_level_groups + domain_level_groups)) - - -# --------------- INIT User-Groups - - -def create_or_get_user(db: Session, author_id: str, is_active: bool = models.User.is_active.default.arg) -> models.User: - if type(author_id) == str: author_id = author_id.lower() - db_user = db.query(models.User).filter(models.User.email == author_id).first() - if not db_user: - db_user = models.User(email=author_id, is_active=is_active) - db.add(db_user) - db.commit() - db.refresh(db_user) - return db_user - - -def upsert_group(db: Session, group_name: str, description: str, orchestrator: str, orchestrator_sheet: str, permissions: dict, domains: list) -> models.Group: - db_group = db.query(models.Group).filter(models.Group.id == group_name).first() - if db_group is None: - db_group = models.Group(id=group_name, description=description, orchestrator=orchestrator, orchestrator_sheet=orchestrator_sheet, permissions=permissions, domains=domains) - db.add(db_group) - else: - db_group.description = description - db_group.orchestrator = orchestrator - db_group.orchestrator_sheet = orchestrator_sheet - db_group.permissions = permissions - db_group.domains = domains - db.commit() - db.refresh(db_group) - return db_group - - -def upsert_user(db: Session, email: str, active: bool): - db_user = db.query(models.User).filter(models.User.email == email).first() - if db_user is None: - db_user = models.User(email=email, is_active=active) - db.add(db_user) - else: - db_user.is_active = active - db.commit() - return db_user - - -def upsert_user_groups(db: Session): - def display_email_pii(email: str): - return f"'{email[0:3]}...@{email.split('@')[1]}'" - """ - reads the user_groups yaml file and inserts any new users, groups, - along with new participation of users in groups - """ - logger.debug("Updating user-groups configuration.") - filename = get_settings().USER_GROUPS_FILENAME - - # read yaml safely - try: - with open(filename) as inf: - user_groups_yaml = yaml.safe_load(inf) - except Exception as e: - logger.error(f"could not open user groups filename {filename}: {e}") - raise e - - # delete all user-groups relationships - db.query(models.association_table_user_groups).delete() - - # set all users to inactive - db.query(models.User).update({models.User.is_active: False}) - - # create a map of group_id -> domains and another of domain -> groups - group_domains = defaultdict(set) - domain_groups = defaultdict(list) - for domain, explicit_groups in user_groups_yaml.get("domains", {}).items(): - domain_groups[domain] = list(set(explicit_groups)) - for group in explicit_groups: - group_domains[group].add(domain) - - # upsert groups and save a map of groupid -> dbobject - for group_id, g in user_groups_yaml.get("groups", {}).items(): - upsert_group(db, group_id, g["description"], g["orchestrator"], g["orchestrator_sheet"], g["permissions"], list(group_domains.get(group_id, []))) - db_groups: dict[str, models.Group] = {g.id: g for g in db.query(models.Group).all()} - - # integrity checks - for group_in_domains in group_domains: - if group_in_domains not in db_groups: - logger.error(f"[CONFIG] Group '{group_in_domains}' does not exist in the database: domains setting will not work.") - if group_in_domains not in user_groups_yaml.get("groups", {}): - logger.error(f"[CONFIG] Group '{group_in_domains}' does not exist in the config file: domains setting will not work.") - - # reinsert users in their EXPLICITLY DEFINED groups - # domain groups are check live, as there may be new users that are not explicitly registered but belong to a domain - for email, explicit_groups in user_groups_yaml.get("users", {}).items(): - explicit_groups = explicit_groups or [] - email = email.lower().strip() - if '@' not in email: - logger.error(f'[CONFIG] Invalid user email {email}, skipping.') - continue - - logger.info(f"{display_email_pii(email)} => {explicit_groups}") - - # upsert active user - db_user = upsert_user(db, email, active=True) - - # connect users to groups - for group_id in explicit_groups: - if group_id not in db_groups: - logger.error(f"[CONFIG] Group {group_id} does not exist in config file, skipping for email={display_email_pii(email)}.") - continue - db_groups[group_id].users.append(db_user) - - db.commit() - count_user_groups = db.query(models.association_table_user_groups).count() - count_groups = db.query(func.count(models.Group.id)).scalar() - - logger.success(f"[CONFIG] DONE: [users={count_users(db)}, groups={count_groups}, explicit user groups={count_user_groups}].") diff --git a/src/db/database.py b/src/db/database.py deleted file mode 100644 index d42466a..0000000 --- a/src/db/database.py +++ /dev/null @@ -1,36 +0,0 @@ -from functools import lru_cache -from sqlalchemy import Engine, create_engine, event -from sqlalchemy.orm import sessionmaker -from shared.settings import get_settings -from contextlib import contextmanager - - -@lru_cache -def make_engine(database_url: str): - engine = create_engine(database_url, connect_args={"check_same_thread": False}) - - @event.listens_for(engine, "connect") - def set_sqlite_pragma(conn, _) -> None: - cursor = conn.cursor() - cursor.execute("PRAGMA journal_mode=WAL") - cursor.close() - - return engine - - -def make_session_local(engine: Engine): - session_local = sessionmaker(autocommit=False, autoflush=False, bind=engine) - return session_local - - -@contextmanager -def get_db(): - session = make_session_local(make_engine(get_settings().DATABASE_PATH))() - try: yield session - finally: session.close() - - -def get_db_dependency(): - # to use with Depends and ensure proper session closing - with get_db() as db: - yield db \ No newline at end of file diff --git a/src/db/schemas.py b/src/db/schemas.py deleted file mode 100644 index 538609a..0000000 --- a/src/db/schemas.py +++ /dev/null @@ -1,66 +0,0 @@ -from pydantic import BaseModel -from datetime import datetime - - -class Tag(BaseModel): - id: str - created_at: datetime - - model_config = { "from_attributes": True } - __hash__ = object.__hash__ - -class ArchiveCreate(BaseModel): - id: str | None = None - url: str - result: dict | None = None - public: bool = True - author_id: str | None = None - group_id: str | None = None - tags: set[Tag] | None = set() - rearchive: bool = True - # urls: list = [] - - -class Archive(ArchiveCreate): - created_at: datetime - updated_at: datetime | None - deleted: bool - - model_config = { "from_attributes": True } - -class SubmitSheet(BaseModel): - sheet_name: str | None = None - sheet_id: str | None = None - header: int = 1 - public: bool = False - author_id: str | None = None - group_id: str | None = None - tags: set[str] | None = set() - columns: dict | None = {} # TODO: implement - -class SubmitManual(BaseModel): - result: str # should be a Metadata.to_json() - public: bool = False - author_id: str | None = None - group_id: str | None = None - tags: set[str] | None = set() - -# API RESPONSES BELOW -class ArchiveResult(BaseModel): - id: str - url: str - result: dict - created_at: datetime - -class Task(BaseModel): - id: str - -class TaskResult(Task): - status: str - result: str - -class TaskDelete(Task): - deleted: bool - -class ActiveUser(BaseModel): - active: bool \ No newline at end of file diff --git a/src/endpoints/__init__.py b/src/endpoints/__init__.py deleted file mode 100644 index 1551fae..0000000 --- a/src/endpoints/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from endpoints.default import default_router -from endpoints.url import url_router -from endpoints.task import task_router -from endpoints.interoperability import interoperability_router -from endpoints.sheet import sheet_router \ No newline at end of file diff --git a/src/endpoints/default.py b/src/endpoints/default.py deleted file mode 100644 index 4569bd9..0000000 --- a/src/endpoints/default.py +++ /dev/null @@ -1,45 +0,0 @@ - -from fastapi import APIRouter, Depends, Request, HTTPException -from fastapi.responses import FileResponse, JSONResponse -from sqlalchemy.orm import Session - -from core.config import VERSION, BREAKING_CHANGES -from core.logging import log_error -from db import crud, schemas -from db.database import get_db_dependency, get_db -from web.security import get_user_auth, bearer_security - -default_router = APIRouter() - - -@default_router.get("/") -async def home(request: Request): - # TODO: maybe split into 2 routes: one non authenticated and one authenticated for the groups info only - status = {"version": VERSION, "breakingChanges": BREAKING_CHANGES} - try: - email = await get_user_auth(await bearer_security(request)) - with get_db() as db: - status["groups"] = crud.get_user_groups(db, email) - except HTTPException: pass # not authenticated is fine - except Exception as e: log_error(e) - return JSONResponse(status) - - -@default_router.get("/health") -async def health(): - return JSONResponse({"status": "ok"}) - - -@default_router.get("/user/active", summary="Check if the user is active and can use the tool.") -async def active(db: Session = Depends(get_db_dependency), email=Depends(get_user_auth)) -> schemas.ActiveUser: - return {"active": crud.is_active_user(db, email)} - - -@default_router.get("/groups") -def get_user_groups(db: Session = Depends(get_db_dependency), email=Depends(get_user_auth)) -> list[str]: - return crud.get_user_groups(db, email) - - -@default_router.get('/favicon.ico', include_in_schema=False) -async def favicon(): - return FileResponse("static/favicon.ico") diff --git a/src/endpoints/interoperability.py b/src/endpoints/interoperability.py deleted file mode 100644 index 5758dc1..0000000 --- a/src/endpoints/interoperability.py +++ /dev/null @@ -1,27 +0,0 @@ -from fastapi import APIRouter, Depends, HTTPException -from fastapi.responses import JSONResponse -from auto_archiver import Metadata -from loguru import logger -import sqlalchemy - -from web.security import token_api_key_auth -from db import models, schemas -from worker.main import insert_result_into_db -from core.logging import log_error - - -interoperability_router = APIRouter(prefix="/interop", tags=["Interoperability endpoints."]) - - -# ----- endpoint to submit data archived elsewhere -@interoperability_router.post("/submit-archive", status_code=201, summary="Submit a manual archive entry, for data that was archived elsewhere.") -def submit_manual_archive(manual: schemas.SubmitManual, auth=Depends(token_api_key_auth)): - result = Metadata.from_json(manual.result) - logger.info(f"MANUAL SUBMIT {result.get_url()} {manual.author_id}") - manual.tags.add("manual") - try: - archive_id = insert_result_into_db(result, manual.tags, manual.public, manual.group_id, manual.author_id, models.generate_uuid()) - except sqlalchemy.exc.IntegrityError as e: - log_error(e) - raise HTTPException(status_code=422, detail=f"Cannot insert into DB due to integrity error") - return JSONResponse({"id": archive_id}, status_code=201) diff --git a/src/endpoints/sheet.py b/src/endpoints/sheet.py deleted file mode 100644 index 5a32d4b..0000000 --- a/src/endpoints/sheet.py +++ /dev/null @@ -1,24 +0,0 @@ - -from fastapi import APIRouter, Depends, HTTPException -from fastapi.responses import JSONResponse - -from loguru import logger - -from core.config import ALLOW_ANY_EMAIL -from web.security import get_token_or_user_auth -from db import schemas -from worker.main import create_sheet_task - -sheet_router = APIRouter(prefix="/sheet", tags=["Google Spreadsheet operations"]) - - -@sheet_router.post("/archive", status_code=201, summary="Submit a Google Sheet archive request, starts a sheet archiving task.", response_description="task_id for the archiving task.") -def archive_sheet(sheet:schemas.SubmitSheet, email = Depends(get_token_or_user_auth)) -> schemas.Task: - logger.info(f"SHEET TASK for {sheet=}") - if email == ALLOW_ANY_EMAIL: - email = sheet.author_id or "api-endpoint" - sheet.author_id = email - if not sheet.sheet_name and not sheet.sheet_id: - raise HTTPException(status_code=422, detail=f"sheet name or id is required") - task = create_sheet_task.delay(sheet.model_dump_json()) - return JSONResponse({"id": task.id}, status_code=201) \ No newline at end of file diff --git a/src/endpoints/url.py b/src/endpoints/url.py deleted file mode 100644 index b17b082..0000000 --- a/src/endpoints/url.py +++ /dev/null @@ -1,60 +0,0 @@ - -from fastapi import APIRouter, Depends, HTTPException -from fastapi.responses import JSONResponse -from datetime import datetime - -from loguru import logger -from web.security import get_user_auth, get_token_or_user_auth -from sqlalchemy.orm import Session - -from db import crud, schemas -from db.database import get_db_dependency - -from worker.main import create_archive_task - -url_router = APIRouter(prefix="/url", tags=["Single URL operations"]) - - -@url_router.post("/archive", status_code=201, summary="Submit a single URL archive request, starts an archiving task.", response_description="task_id for the archiving task, will match the archive id.") -def archive_url(archive: schemas.ArchiveCreate, email=Depends(get_token_or_user_auth)) -> schemas.Task: - archive.author_id = email - url = archive.url - logger.info(f"new {archive.public=} task for {email=} and {archive.group_id=}: {url}") - if type(url) != str or len(url) <= 5: - raise HTTPException(status_code=422, detail=f"Invalid URL received: {url}") - logger.info("creating task") - task = create_archive_task.delay(archive.model_dump_json()) - task_response = schemas.Task(id=task.id) - return JSONResponse(task_response.model_dump(), status_code=201) - - -@url_router.get("/search", summary="Search for archive entries by URL.") -def search_by_url( - url: str, skip: int = 0, limit: int = 25, - archived_after: datetime = None, archived_before: datetime = None, - db: Session = Depends(get_db_dependency), - email=Depends(get_token_or_user_auth) -) -> list[schemas.ArchiveResult]: - return crud.search_archives_by_url(db, url.strip(), email, skip=skip, limit=limit, archived_after=archived_after, archived_before=archived_before) - - -@url_router.get("/latest", summary="Fetch latest URL archives for the authenticated user.") -def latest(skip: int = 0, limit: int = 25, db: Session = Depends(get_db_dependency), email=Depends(get_user_auth)) -> list[schemas.ArchiveResult]: - return crud.search_archives_by_email(db, email, skip=skip, limit=limit) - - -@url_router.get("/{id}", summary="Fetch a single URL archive by the associated id.") -def lookup(id, db: Session = Depends(get_db_dependency), email=Depends(get_token_or_user_auth)) -> schemas.ArchiveResult: - archive = crud.get_archive(db, id, email) - if archive is None: - raise HTTPException(status_code=404, detail="Archive not found") - return archive - - -@url_router.delete("/{id}", summary="Delete a single URL archive by id.") -def delete_task(id, db: Session = Depends(get_db_dependency), email=Depends(get_user_auth)) -> schemas.TaskDelete: - logger.info(f"deleting url archive task {id} request by {email}") - return JSONResponse({ - "id": id, - "deleted": crud.soft_delete_task(db, id, email) - }) diff --git a/src/example.user-groups.yaml b/src/example.user-groups.yaml deleted file mode 100644 index 707e39f..0000000 --- a/src/example.user-groups.yaml +++ /dev/null @@ -1,18 +0,0 @@ -# email-level group access -users: - email1@example.com: - - group1 - - group2 - email2@example.com: - - group2 - email3@example-no-group.com: - -# domain-level group access (taken from the emails) -domains: - example.com: - - group3 - -orchestrators: - group1: secrets/orchestration-group1.yaml - group2: secrets/orchestration-group2.yaml - default: secrets/orchestration-default.yaml \ No newline at end of file diff --git a/src/migrations/README b/src/migrations/README deleted file mode 100644 index 98e4f9c..0000000 --- a/src/migrations/README +++ /dev/null @@ -1 +0,0 @@ -Generic single-database configuration. \ No newline at end of file diff --git a/src/shared/settings.py b/src/shared/settings.py deleted file mode 100644 index 8fa5ae5..0000000 --- a/src/shared/settings.py +++ /dev/null @@ -1,38 +0,0 @@ - -from functools import lru_cache -from pydantic_settings import BaseSettings -from pydantic import ConfigDict -from typing import Annotated, Set -from annotated_types import Len - - -class Settings(BaseSettings): - model_config = ConfigDict(extra='ignore', str_strip_whitespace=True) - - # general - SERVE_LOCAL_ARCHIVE: str = "" - USER_GROUPS_FILENAME: str = "user-groups.yaml" - SHEET_ORCHESTRATION_YAML : str = "secrets/orchestration-sheet.yaml" - - # database - DATABASE_PATH: str - DATABASE_QUERY_LIMIT: int = 100 - - # redis - CELERY_BROKER_URL: str = "redis://localhost:6379" - CELERY_RESULT_BACKEND: str = "redis://localhost:6379" - REDIS_EXCEPTIONS_CHANNEL: str = "exceptions-channel" - - # observability - REPEAT_COUNT_METRICS_SECONDS: int = 30 - - # security - API_BEARER_TOKEN: Annotated[str, Len(min_length=20)] - ALLOWED_ORIGINS: Annotated[set[str], Len(min_length=1)] - CHROME_APP_IDS: Annotated[set[Annotated[str, Len(min_length=10)]], Len(min_length=1)] - #TODO: deprecate blocklist - BLOCKED_EMAILS: Annotated[Set[str], Len(min_length=0)] = set() - -@lru_cache -def get_settings(): - return Settings() \ No newline at end of file diff --git a/src/tests/conftest.py b/src/tests/conftest.py deleted file mode 100644 index 97c18e8..0000000 --- a/src/tests/conftest.py +++ /dev/null @@ -1,95 +0,0 @@ -import os -from fastapi.testclient import TestClient -import pytest -from unittest.mock import patch -from shared.settings import Settings - - -@pytest.fixture(autouse=True) -def mock_logger_add(): - """Fixture to mock loguru.logger.add for all tests.""" - with patch('loguru.logger.add') as mock_add: - yield mock_add # This makes the mock available to tests - - -@pytest.fixture() -def get_settings(): - return Settings(_env_file=".env.test") - -@pytest.fixture(autouse=True) -def mock_settings(): - with patch('shared.settings.Settings', return_value=Settings(_env_file=".env.test")) as mock_settings: - yield mock_settings - - -@pytest.fixture() -def test_db(get_settings: Settings): - from db.database import make_engine - from db import models - - make_engine.cache_clear() - engine = make_engine(get_settings.DATABASE_PATH) - - fs = get_settings.DATABASE_PATH.replace("sqlite:///", "") - if not os.path.exists(fs): - open(fs, 'w').close() - - models.Base.metadata.create_all(engine) - - connection = engine.connect() - yield connection - connection.close() - - models.Base.metadata.drop_all(bind=engine) - for suffix in ["", "-wal", "-shm"]: - new_fs = fs + suffix - if os.path.exists(new_fs): - os.remove(new_fs) - - -@pytest.fixture() -def db_session(test_db): - from db.database import make_session_local - session_local = make_session_local(test_db) - with session_local() as session: - yield session - - -@pytest.fixture() -def app(db_session): - from web.main import app_factory - from db import crud - app = app_factory() - crud.upsert_user_groups(db_session) - return app - - -@pytest.fixture() -def client(app): - client = TestClient(app) - return client - - -@pytest.fixture() -def app_with_auth(app): - from web.security import get_token_or_user_auth, get_user_auth, token_api_key_auth - app.dependency_overrides[get_token_or_user_auth] = lambda: "rick@example.com" - app.dependency_overrides[get_user_auth] = lambda: "morty@example.com" - app.dependency_overrides[token_api_key_auth] = lambda: "jerry@example.com" - return app - - -@pytest.fixture() -def client_with_auth(app_with_auth): - client = TestClient(app_with_auth) - return client - - -@pytest.fixture() -def test_no_auth(): - # reusable code to ensure a method/endpoint combination is unauthorized - def no_auth(http_method, endpoint): - response = http_method(endpoint) - assert response.status_code == 403 - assert response.json() == {"detail": "Not authenticated"} - return no_auth \ No newline at end of file diff --git a/src/tests/db/test_crud.py b/src/tests/db/test_crud.py deleted file mode 100644 index 6f838df..0000000 --- a/src/tests/db/test_crud.py +++ /dev/null @@ -1,423 +0,0 @@ -from datetime import datetime -from unittest.mock import patch - -import pytest -import yaml -from db import models -from shared.settings import Settings - -authors = ["rick@example.com", "morty@example.com", "jerry@example.com"] - - -@pytest.fixture() -def test_data(db_session): - - # creates 3 users - for email in authors: - db_session.add(models.User(email=email)) - db_session.commit() - assert db_session.query(models.User).count() == 3 - - # creates 100 archives for 3 users over 2 months with repeating URLs - for i in range(100): - author = authors[i % 3] - archive = models.Archive( - id=f"archive-id-456-{i}", - url=f"https://example-{i%3}.com", - result={}, - public=author == "jerry@example.com", - author_id=author, - group_id="spaceship" if author == "morty@example.com" and i % 2 == 0 else None, - created_at=datetime(2021, (i % 2) + 1, (i % 25) + 1) - ) - if i % 5 == 0: - archive.tags.append(models.Tag(id=f"tag-{i}")) - if i % 10 == 0: - archive.tags.append(models.Tag(id=f"tag-second-{i}")) - if i % 4 == 0: - archive.tags.append(models.Tag(id=f"tag-third-{i}")) - for j in range(10): - archive.urls.append(models.ArchiveUrl(url=f"https://example-{i}.com/{j}", key=f"media_{j}")) - db_session.add(archive) - - db_session.commit() - - assert db_session.query(models.Archive).count() == 100 - assert db_session.query(models.Tag).count() == 20 + 10 + 25 - assert db_session.query(models.ArchiveUrl).count() == 1000 - assert db_session.query(models.ArchiveUrl).filter(models.ArchiveUrl.archive_id == "archive-id-456-0").count() == 10 - - # setup groups - assert db_session.query(models.Group).count() == 0 - from db import crud - crud.upsert_user_groups(db_session) - assert db_session.query(models.Group).count() == 3 - assert db_session.query(models.User).count() == 4 - - -def test_get_archive(test_data, db_session): - from db import crud - from core.config import ALLOW_ANY_EMAIL - - print(db_session.query(models.Group).all()) - - # each author's archives work - assert (a0 := crud.get_archive(db_session, "archive-id-456-0", authors[0])) is not None - assert a0.id == "archive-id-456-0" - assert a0.url == "https://example-0.com" - assert a0.author_id == authors[0] - assert a0.public == False - - assert crud.get_archive(db_session, "archive-id-456-1", authors[1]) is not None - assert crud.get_archive(db_session, "archive-id-456-2", authors[2]) is not None - - # ALLOW_ANY_EMAIL - assert crud.get_archive(db_session, "archive-id-456-0", ALLOW_ANY_EMAIL) is not None - assert crud.get_archive(db_session, "archive-id-456-1", ALLOW_ANY_EMAIL) is not None - - # not found - assert crud.get_archive(db_session, "archive-missing", authors[0]) is None - - # public - assert (a_public := crud.get_archive(db_session, "archive-id-456-2", authors[0])) is not None - assert a_public.public == True - - # not public - rick's - assert crud.get_archive(db_session, "archive-id-456-0", authors[1]) is None - - -def test_search_archives_by_url(test_data, db_session): - from db import crud - from core.config import ALLOW_ANY_EMAIL - - # rick's archives are private - assert len(crud.search_archives_by_url(db_session, "https://example-0.com", "rick@example.com")) == 34 - assert len(crud.search_archives_by_url(db_session, "https://example-0.com", ALLOW_ANY_EMAIL)) == 34 - assert len(crud.search_archives_by_url(db_session, "https://example-0.com", "morty@example.com")) == 0 - - # morty's archives are public but half are in spaceship group - assert len(crud.search_archives_by_url(db_session, "https://example-1.com", "rick@example.com")) == 16 - - # jerry's archives are public - assert len(crud.search_archives_by_url(db_session, "https://example-2.com", "jerry@example.com")) == 33 - assert len(crud.search_archives_by_url(db_session, "https://example-2.com", "rick@example.com")) == 33 - - # fuzzy search - assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL)) == 100 - assert len(crud.search_archives_by_url(db_session, "https://EXAMPLE", ALLOW_ANY_EMAIL)) == 100 - assert len(crud.search_archives_by_url(db_session, "2.com", ALLOW_ANY_EMAIL)) == 33 - - # absolute search - assert len(crud.search_archives_by_url(db_session, "example-2.com", ALLOW_ANY_EMAIL, absolute_search=True)) == 0 - assert len(crud.search_archives_by_url(db_session, "https://example-2.com", ALLOW_ANY_EMAIL, absolute_search=True)) == 33 - - # archived_after - assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, archived_after=datetime(2010, 1, 1))) == 100 - assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, archived_after=datetime(2021, 1, 15))) == 70 - assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, archived_after=datetime(2031, 1, 1))) == 0 - - # archived before - assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, archived_before=datetime(2010, 1, 1))) == 0 - assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, archived_before=datetime(2021, 1, 15))) == 28 - assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, archived_before=datetime(2031, 1, 1))) == 100 - - # archived before and after - assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, archived_after=datetime(2001, 1, 1), archived_before=datetime(2031, 1, 11))) == 100 - assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, archived_after=datetime(2021, 1, 14), archived_before=datetime(2021, 1, 16))) == 2 - - # limit - assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, limit=10)) == 10 - assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, limit=-1)) == 1 - - # skip - assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, skip=10)) == 90 - - -def test_search_archives_by_email(test_data, db_session): - from core.config import ALLOW_ANY_EMAIL - from db import crud - - # lower/upper case - assert len(crud.search_archives_by_email(db_session, "rick@example.com")) == 34 - assert len(crud.search_archives_by_email(db_session, "RICK@example.com")) == 34 - - # ALLOW_ANY_EMAIL is not a user - assert len(crud.search_archives_by_email(db_session, ALLOW_ANY_EMAIL)) == 0 - - # most recent first - a1 = crud.search_archives_by_email(db_session, "rick@example.com", limit=1) - assert len(a1) == 1 - assert a1[0].created_at == datetime(2021, 2, 25) - - # earliest is the last - a2 = crud.search_archives_by_email(db_session, "rick@example.com", skip=33) - assert len(a2) == 1 - assert a2[0].created_at == datetime(2021, 1, 1) - - -@patch("db.crud.DATABASE_QUERY_LIMIT", new=25) -def test_max_query_limit(test_data, db_session): - from db import crud - from core.config import ALLOW_ANY_EMAIL - - assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL)) == 25 - assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, limit=1000)) == 25 - - assert len(crud.search_archives_by_email(db_session, "rick@example.com")) == 25 - assert len(crud.search_archives_by_email(db_session, "rick@example.com", limit=1000)) == 25 - - -def test_create_task(db_session): - from db import crud - from db import schemas - - task = schemas.ArchiveCreate( - id="archive-id-456-101", - url="https://example-0.com", - result={}, - public=False, - author_id="rick@example.com", - group_id="spaceship", - tags=[], - urls=[] - ) - - # with tags and urls - nt = crud.create_task(db_session, task, [models.Tag(id="tag-101")], [models.ArchiveUrl(url="https://example-0.com/0", key="media_0")]) - - assert nt is not None - assert nt.id == "archive-id-456-101" - assert nt.url == "https://example-0.com" - assert nt.author_id == "rick@example.com" - assert nt.public == False - assert nt.group_id == "spaceship" - assert len(nt.tags) == 1 - assert nt.tags[0].id == "tag-101" - assert len(nt.urls) == 1 - assert nt.urls[0].url == "https://example-0.com/0" - assert nt.urls[0].key == "media_0" - assert nt.created_at is not None - - # without tags and urls - task.id = "archive-id-456-102" - nt = crud.create_task(db_session, task, [], []) - assert nt is not None - assert nt.id == "archive-id-456-102" - assert nt.url == "https://example-0.com" - assert nt.author_id == "rick@example.com" - assert nt.public == False - assert nt.group_id == "spaceship" - assert len(nt.tags) == 0 - assert len(nt.urls) == 0 - assert nt.created_at is not None - - -def test_soft_delete(test_data, db_session): - from db import crud - - # none deleted yet - assert crud.get_archive(db_session, "archive-id-456-0", "rick@example.com") is not None - assert db_session.query(models.Archive).filter(models.Archive.deleted == True).count() == 0 - - # delete - assert crud.soft_delete_task(db_session, "archive-id-456-0", "rick@example.com") == True - - # ensure soft delete - assert db_session.query(models.Archive).filter(models.Archive.deleted == True).count() == 1 - assert crud.get_archive(db_session, "archive-id-456-0", "rick@example.com") is None - - # already deleted - assert crud.soft_delete_task(db_session, "archive-id-456-0", "rick@example.com") == False - - -def test_count_archives(test_data, db_session): - from db import crud - - assert crud.count_archives(db_session) == 100 - db_session.query(models.Archive).filter(models.Archive.id == "archive-id-456-0").delete() - db_session.commit() - assert crud.count_archives(db_session) == 99 - - -def test_count_archive_urls(test_data, db_session): - from db import crud - - assert crud.count_archive_urls(db_session) == 1000 - db_session.query(models.ArchiveUrl).filter(models.ArchiveUrl.url == "https://example-0.com/0").delete() - db_session.commit() - assert crud.count_archive_urls(db_session) == 999 - - db_session.query(models.Archive).filter(models.Archive.id == "archive-id-456-0").delete() - db_session.commit() - # no Cascade is enabled - assert crud.count_archives(db_session) == 99 - assert crud.count_archive_urls(db_session) == 999 - -def test_count_users(test_data, db_session): - from db import crud - - assert crud.count_users(db_session) == 4 - db_session.query(models.User).filter(models.User.email == "rick@example.com").delete() - db_session.commit() - assert crud.count_users(db_session) == 3 - -def test_count_by_users_since(test_data, db_session): - from db import crud - - # 100y window - assert len(cu := crud.count_by_user_since(db_session, 60 * 60 * 24 * 31 * 12 * 100)) == 3 - assert cu[0].total == 34 - assert cu[1].total == 33 - assert cu[2].total == 33 - - -def test_create_tag(db_session): - from db import crud - - assert db_session.query(models.Tag).count() == 0 - - # create first - create_tag = crud.create_tag(db_session, "tag-101") - assert create_tag is not None - assert create_tag.id == "tag-101" - assert db_session.query(models.Tag).count() == 1 - assert db_session.query(models.Tag).filter(models.Tag.id == "tag-101").first() == create_tag - - # same id does not add new db entry - existing_tag = crud.create_tag(db_session, "tag-101") - assert existing_tag == create_tag - assert db_session.query(models.Tag).count() == 1 - - # create second - second_tag = crud.create_tag(db_session, "tag-102") - assert second_tag is not None - assert second_tag.id == "tag-102" - assert db_session.query(models.Tag).count() == 2 - -def test_is_active_user(test_data, db_session): - from db import crud - - assert crud.is_active_user(db_session, "") == False - assert crud.is_active_user(db_session, "example.com") == False - assert crud.is_active_user(db_session, "unknown@example.com") == True - assert crud.is_active_user(db_session, "ANYONE@example.com") == True - assert crud.is_active_user(db_session, "ANYONE@birdy.com") == True - assert crud.is_active_user(db_session, "rick@example.com") == True - assert crud.is_active_user(db_session, "RICK@example.com") == True - assert crud.is_active_user(db_session, "summer@herself.com") == True - assert crud.is_active_user(db_session, "rick@not-in-groups.com") == False - - -def test_is_user_in_group(test_data, db_session): - from db import crud - from core.config import ALLOW_ANY_EMAIL - - # see user-groups.test.yaml - test_pairs = [ - (ALLOW_ANY_EMAIL, "spaceship", True), - (ALLOW_ANY_EMAIL, "non-existant!@#!%!", True), - - ("rick@example.com", "spaceship", True), - ("rick@example.com", "SPACESHIP", False), - ("RICK@example.com", "interdimensional", True), - ("rick@example.com", "animated-characters", True), - ("rick@example.com", "the-jerrys-club", False), - - ("morty@example.com", "spaceship", True), - ("morty@example.com", "interdimensional", False), - ("morty@example.com", "the-jerrys-club", False), - - ("jerry@example.com", "spaceship", False), - ("jerry@example.com", "interdimensional", False), - ("jerry@example.com", "the-jerrys-club", False), # group not in 'groups' - - ("rick@example.com", "animated-characters", True), - ("morty@example.com", "animated-characters", True), - ("jerry@example.com", "animated-characters", True), - ("ANYONE@example.com", "animated-characters", True), - ("ANYONE@birdy.com", "animated-characters", True), - - ("summer@herself.com", "animated-characters", False), - - ("rick@example.com", "", False), - ("", "spaceship", False), - ("BADEMAILexample.com", "spaceship", False), - ] - for email, group, expected in test_pairs: - print(f"{email} in {group} == {expected}") - assert crud.is_user_in_group(db_session, group, email) == expected - - -def test_create_or_get_user(test_data, db_session): - from db import crud - - assert db_session.query(models.User).count() == 4 - - # already exists - assert (u1 := crud.create_or_get_user(db_session, "rick@example.com")) is not None - assert u1.email == "rick@example.com" - assert u1.is_active == True - - # new active - assert (u2 := crud.create_or_get_user(db_session, "beth@example.com", is_active=True)) is not None - assert u2.email == "beth@example.com" - assert u2.is_active == True - - # new not active - assert (u3 := crud.create_or_get_user(db_session, "not-active@example.com")) is not None - assert u3.email == "not-active@example.com" - assert u3.is_active == False - - assert db_session.query(models.User).count() == 6 - - -def test_upsert_group(test_data, db_session): - from db import crud - - assert db_session.query(models.Group).count() == 3 - - repeatable_params = ["desc 1", "orch.yaml", "sheet.yaml", {"read": ["all"]}, ["example.com"]] - - assert (g1 := crud.upsert_group(db_session, "spaceship", *repeatable_params)) is not None - assert g1.id == "spaceship" - assert g1.description == "desc 1" - assert g1.orchestrator == "orch.yaml" - assert g1.orchestrator_sheet == "sheet.yaml" - assert g1.permissions == {"read": ["all"]} - assert g1.domains == ["example.com"] - assert len(g1.users) == 2 - assert [u.email for u in g1.users] == ["rick@example.com", "morty@example.com"] - - assert (g2 := crud.upsert_group(db_session, "interdimensional", *repeatable_params)) is not None - assert g2.id == "interdimensional" - assert len(g2.users) == 1 - assert [u.email for u in g2.users] == ["rick@example.com"] - - assert (g3 := crud.upsert_group(db_session, "this-is-a-new-group", *repeatable_params)) is not None - assert g3.id == "this-is-a-new-group" - assert len(g3.users) == 0 - - assert db_session.query(models.Group).count() == 4 - - -def test_upsert_user_groups(db_session): - from db import crud - - @patch('db.crud.get_settings', new = lambda: bad_setings) - def test_missing_yaml(db_session): - with pytest.raises(FileNotFoundError): - crud.upsert_user_groups(db_session) - - - @patch('db.crud.get_settings', new = lambda: bad_setings) - def test_broken_yaml(db_session): - with pytest.raises(yaml.YAMLError): - crud.upsert_user_groups(db_session) - - bad_setings = Settings(_env_file=".env.test") - - bad_setings.USER_GROUPS_FILENAME = "tests/user-groups.test.missing.yaml" - test_missing_yaml(db_session) - - bad_setings.USER_GROUPS_FILENAME = "tests/user-groups.test.broken.yaml" - test_broken_yaml(db_session) \ No newline at end of file diff --git a/src/tests/endpoints/test_default.py b/src/tests/endpoints/test_default.py deleted file mode 100644 index 6a585e6..0000000 --- a/src/tests/endpoints/test_default.py +++ /dev/null @@ -1,139 +0,0 @@ -from unittest.mock import AsyncMock, patch -from fastapi.testclient import TestClient -import pytest -from core.config import VERSION -from tests.db.test_crud import test_data - - - -def test_endpoint_home(client_with_auth): - r = client_with_auth.get("/") - assert r.status_code == 200 - j = r.json() - assert "version" in j and j["version"] == VERSION - assert "breakingChanges" in j - assert "groups" not in j - - -@patch("endpoints.default.bearer_security", new_callable=AsyncMock) -@patch("endpoints.default.get_user_auth", new_callable=AsyncMock, return_value="test@example.com") -@patch("endpoints.default.crud.get_user_groups", return_value=["group1", "group2"]) -def test_endpoint_home_with_groups(m1, m2, m3, client_with_auth): - r = client_with_auth.get("/") - assert r.status_code == 200 - j = r.json() - assert "version" in j and j["version"] == VERSION - assert "breakingChanges" in j - assert "groups" in j - assert j["groups"] == ["group1", "group2"] - - -@patch("endpoints.default.bearer_security", new_callable=AsyncMock) -@patch("endpoints.default.get_user_auth", new_callable=AsyncMock, return_value="test@example.com") -@patch("endpoints.default.crud.get_user_groups", side_effect=Exception('mocked error')) -def test_endpoint_home_with_groups_exception(m1, m2, m3, client_with_auth): # mocks call that triggers an internal error - r = client_with_auth.get("/") - assert r.status_code == 200 - j = r.json() - assert "version" in j and j["version"] == VERSION - assert "breakingChanges" in j - assert "groups" not in j - - -def test_endpoint_health(client_with_auth): - r = client_with_auth.get("/health") - assert r.status_code == 200 - assert r.json() == {"status": "ok"} - - -def test_endpoint_active_no_auth(client, test_no_auth): - test_no_auth(client.get, "/user/active") - - -def test_endpoint_active_true_user(client_with_auth): - r = client_with_auth.get("/user/active") - assert r.status_code == 200 - assert r.json() == {"active": True} - -def test_endpoint_active_false_user(app): - from web.security import get_user_auth - - app.dependency_overrides[get_user_auth] = lambda: "morty@not-recognized-group.com" - client = TestClient(app) - r = client.get("/user/active") - - assert r.status_code == 200 - assert r.json() == {"active": False} - - -def test_endpoint_groups_no_auth(client, test_no_auth): - test_no_auth(client.get, "/groups") - - -def test_endpoint_groups_rick_and_morty(client_with_auth): - r = client_with_auth.get("/groups") - assert r.status_code == 200 - assert len(j := r.json()) == 2 - assert 'animated-characters' in j - assert 'spaceship' in j - - -@patch("endpoints.default.crud.get_user_groups", return_value=["group1", "group2"]) -def test_endpoint_groups(m1, app): - from web.security import get_user_auth - app.dependency_overrides[get_user_auth] = lambda: True - client = TestClient(app) - - r = client.get("/groups") - - assert r.status_code == 200 - assert r.json() == ["group1", "group2"] - - -def test_no_serve_local_archive_by_default(client_with_auth): - r = client_with_auth.get("/app/local_archive_test/temp.txt") - assert r.status_code == 404 - - -def test_favicon(client_with_auth): - r = client_with_auth.get("/favicon.ico") - assert r.status_code == 200 - assert r.headers["content-type"] == "image/vnd.microsoft.icon" - - -@pytest.mark.asyncio -async def test_prometheus_metrics(test_data, client_with_auth, get_settings): - # before metrics calculation - r = client_with_auth.get("/metrics") - assert r.status_code == 200 - assert r.headers["content-type"] == "text/plain; version=0.0.4; charset=utf-8" - assert "disk_utilization" in r.text - assert "database_metrics" in r.text - assert "exceptions" in r.text - assert "worker_exceptions_total" in r.text - assert 'disk_utilization{type="used"}' not in r.text - - # after metrics calculation - from utils.metrics import measure_regular_metrics - await measure_regular_metrics(get_settings.DATABASE_PATH, 60 * 60 * 24 * 31 * 12 * 100) - r2 = client_with_auth.get("/metrics") - assert 'disk_utilization{type="used"}' in r2.text - assert 'disk_utilization{type="free"}' in r2.text - assert 'disk_utilization{type="database"}' in r2.text - assert 'database_metrics{query="count_archives"} 100.0' in r2.text - assert 'database_metrics{query="count_archive_urls"} 1000.0' in r2.text - assert 'database_metrics{query="count_users"} 4.0' in r2.text - assert 'database_metrics_counter_total{query="count_by_user",user="rick@example.com"} 34.0' in r2.text - assert 'database_metrics_counter_total{query="count_by_user",user="morty@example.com"} 33.0' in r2.text - assert 'database_metrics_counter_total{query="count_by_user",user="jerry@example.com"} 33.0' in r2.text - - # 30s window, should not change the gauges nor the total in the counters - from utils.metrics import measure_regular_metrics - await measure_regular_metrics(get_settings.DATABASE_PATH, 30) - r3 = client_with_auth.get("/metrics") - assert 'database_metrics{query="count_archives"} 100.0' in r3.text - assert 'database_metrics{query="count_archive_urls"} 1000.0' in r3.text - assert 'database_metrics{query="count_users"} 4.0' in r3.text - assert 'database_metrics_counter_total{query="count_by_user",user="rick@example.com"} 34.0' in r3.text - assert 'database_metrics_counter_total{query="count_by_user",user="morty@example.com"} 33.0' in r3.text - assert 'database_metrics_counter_total{query="count_by_user",user="jerry@example.com"} 33.0' in r3.text diff --git a/src/tests/endpoints/test_interopreability.py b/src/tests/endpoints/test_interopreability.py deleted file mode 100644 index 82136f0..0000000 --- a/src/tests/endpoints/test_interopreability.py +++ /dev/null @@ -1,19 +0,0 @@ -import json - - -def test_submit_manual_archive_unauthenticated(client, test_no_auth): - test_no_auth(client.post, "/interop/submit-archive") - - -def test_submit_manual_archive(client_with_auth): - aa_metadata = json.dumps({"status": "test: success", "metadata": {"url": "http://example.com"}, "media": []}) - - r = client_with_auth.post("/interop/submit-archive", json={"result": aa_metadata, "public": False, "author_id": "jerry@gmail.com", "group_id": None, "tags": ["test"]}) - assert r.status_code == 201 - assert "id" in r.json() - - # cannot have the same URL twice - aa_metadata = json.dumps({"status": "test: success", "metadata": {"url": "http://example.com"}, "media": [{"filename": "fn1", "urls": ["http://example.com", "http://example.com"]}]}) - r = client_with_auth.post("/interop/submit-archive", json={"result": aa_metadata, "public": False, "author_id": "jerry@gmail.com", "group_id": None, "tags": ["test"]}) - assert r.status_code == 422 - assert r.json() == {"detail": "Cannot insert into DB due to integrity error"} diff --git a/src/tests/endpoints/test_sheet.py b/src/tests/endpoints/test_sheet.py deleted file mode 100644 index f3e2559..0000000 --- a/src/tests/endpoints/test_sheet.py +++ /dev/null @@ -1,46 +0,0 @@ -import json -from unittest.mock import patch - -from db.schemas import TaskResult - - -def test_sheet_no_auth(client, test_no_auth): - test_no_auth(client.post, "/sheet/archive") - - -@patch("worker.main.create_sheet_task.delay", return_value=TaskResult(id="123-456-789", status="PENDING", result="")) -def test_sheet_rick(m1, client_with_auth): - - response = client_with_auth.post("/sheet/archive", json={"sheet_id": "123-sheet-id"}) - assert response.status_code == 201 - assert response.json() == {'id': '123-456-789'} - - m1.assert_called_once() - called_val = m1.call_args.args[0] - assert json.loads(called_val) == {"sheet_id": "123-sheet-id", "sheet_name": None, "public": False, "author_id": "rick@example.com", "group_id": None, "tags": [], "columns": {}, "header": 1} - - -def test_sheet_missing_sheet_data(client_with_auth): - r = client_with_auth.post("/sheet/archive", json={}) - assert r.status_code == 422 - assert r.json() == {"detail": "sheet name or id is required"} - - -@patch("worker.main.create_sheet_task.delay", return_value=TaskResult(id="123-API-789", status="PENDING", result="")) -def test_sheet_api(m1, client): - - response = client.post("/sheet/archive", json={"sheet_name": "456-sheet_name-id"}, headers={"Authorization": "Bearer this_is_the_test_api_token"}) - assert response.status_code == 201 - assert response.json() == {'id': '123-API-789'} - - m1.assert_called_once() - called_val = m1.call_args.args[0] - assert json.loads(called_val) == {"sheet_name": "456-sheet_name-id", "sheet_id": None, "public": False, "author_id": "api-endpoint", "group_id": None, "tags": [], "columns": {}, "header": 1} - - response = client.post("/sheet/archive", json={"sheet_id": "456-sheet-id", "author_id": "custom-author"}, headers={"Authorization": "Bearer this_is_the_test_api_token"}) - assert response.status_code == 201 - assert response.json() == {'id': '123-API-789'} - - assert m1.call_count == 2 - called_val = m1.call_args.args[0] - assert json.loads(called_val) == {"sheet_id": "456-sheet-id", "sheet_name": None, "public": False, "author_id": "custom-author", "group_id": None, "tags": [], "columns": {}, "header": 1} diff --git a/src/tests/endpoints/test_url.py b/src/tests/endpoints/test_url.py deleted file mode 100644 index 4506c1c..0000000 --- a/src/tests/endpoints/test_url.py +++ /dev/null @@ -1,140 +0,0 @@ -import json -from unittest.mock import patch - -from db.schemas import ArchiveCreate, TaskResult - -def test_archive_url_unauthenticated(client, test_no_auth): - test_no_auth(client.post, "/url/archive") - test_no_auth(client.get, "/url/archive") - - -@patch("worker.main.create_archive_task.delay", return_value=TaskResult(id="123-456-789", status="PENDING", result="")) -def test_archive_url(m1, client_with_auth): - response = client_with_auth.post("/url/archive", json={"url": "bad"}) - assert response.status_code == 422 - assert response.json() == {'detail': 'Invalid URL received: bad'} - m1.assert_not_called() - - response = client_with_auth.post("/url/archive", json={"url": "https://example.com"}) - assert response.status_code == 201 - assert response.json() == {'id': '123-456-789'} - - m1.assert_called_once() - called_val = m1.call_args.args[0] - assert json.loads(called_val) == {"id": None, "url": "https://example.com", "result": None, "public": True, "author_id": "rick@example.com", "group_id": None, "tags": [], "rearchive": True} - - -def test_search_by_url_unauthenticated(client, test_no_auth): - test_no_auth(client.get, "/url/search") - - -def test_search_by_url(client_with_auth, db_session): - # tests the search endpoint, including through some db data for the endpoint params - response = client_with_auth.get("/url/search") - assert response.status_code == 422 - assert response.json()["detail"][0]["msg"] == "Field required" - - response = client_with_auth.get("/url/search?url=https://example.com") - assert response.status_code == 200 - assert response.json() == [] - - from db import crud, schemas - for i in range(11): - crud.create_task(db_session, ArchiveCreate(id=f"url-456-{i}", url="https://example.com" if i < 10 else "https://something-else.com", result={}, public=True, author_id="rick@example.com", group_id=None), [], []) - #NB: this insertion is too fast for the ordering to be correct as they are within the same second - - response = client_with_auth.get("/url/search?url=https://example.com") - assert response.status_code == 200 - assert len(j := response.json()) == 10 - assert "url-456-0" in [i["id"] for i in j] - assert "url-456-9" in [i["id"] for i in j] - assert "url-456-10" not in [i["id"] for i in j] - assert j[0].keys() == schemas.ArchiveResult.model_fields.keys() - - response = client_with_auth.get("/url/search?url=https://example.com&limit=5") - assert response.status_code == 200 - assert len(response.json()) == 5 - - response = client_with_auth.get("/url/search?url=https://example.com&skip=5&limit=2") - assert response.status_code == 200 - assert len(response.json()) == 2 - - response = client_with_auth.get("/url/search?url=https://example.com&archived_before=2010-01-01") - assert response.status_code == 200 - assert len(response.json()) == 0 - - response = client_with_auth.get("/url/search?url=https://example.com&archived_after=2010-01-01") - assert response.status_code == 200 - assert len(response.json()) == 10 - - -def test_latest_unauthenticated(client, test_no_auth): - test_no_auth(client.get, "/url/latest") - - -def test_latest(client_with_auth, db_session): - response = client_with_auth.get("/url/latest") - assert response.status_code == 200 - assert response.json() == [] - - from db import crud, schemas - for i in range(11): - crud.create_task(db_session, ArchiveCreate(id=f"latest-456-{i}", url="https://example.com", result={}, public=True, author_id="morty@example.com" if i < 10 else "rick@example.com", group_id=None), [], []) - #NB: this insertion is too fast for the ordering to be correct as they are within the same second - - # user must exist for /latest to work - crud.create_or_get_user(db_session, "morty@example.com") - - response = client_with_auth.get("/url/latest") - assert response.status_code == 200 - assert len(j := response.json()) == 10 - assert "latest-456-0" in [i["id"] for i in j] - assert "latest-456-9" in [i["id"] for i in j] - assert "latest-456-10" not in [i["id"] for i in j] - assert j[0].keys() == schemas.ArchiveResult.model_fields.keys() - - response = client_with_auth.get("/url/latest?limit=5") - assert response.status_code == 200 - assert len(response.json()) == 5 - - response = client_with_auth.get("/url/latest?skip=5&limit=2") - assert response.status_code == 200 - assert len(response.json()) == 2 - - -def test_lookup_unauthenticated(client, test_no_auth): - test_no_auth(client.get, "/url/123-456-789") - - -def test_lookup(client_with_auth, db_session): - response = client_with_auth.get("/url/lookup-123-456-789") - assert response.status_code == 404 - assert response.json() == {"detail": "Archive not found"} - - from db import crud, schemas - crud.create_task(db_session, ArchiveCreate(id="lookup-123-456-789", url="https://example.com", result={}, public=True, author_id="rick@example.com", group_id=None), [], []) - - response = client_with_auth.get("/url/lookup-123-456-789") - assert response.status_code == 200 - j = response.json() - assert j.keys() == schemas.ArchiveResult.model_fields.keys() - assert j["id"] == "lookup-123-456-789" - assert j["url"] == "https://example.com" - assert j["result"] == {} - - -def test_delete_task_unauthenticated(client, test_no_auth): - test_no_auth(client.delete, "/url/123-456-789") - - -def test_delete_task(client_with_auth, db_session): - response = client_with_auth.delete("/url/delete-123-456-789") - assert response.status_code == 200 - assert response.json() == {"id": "delete-123-456-789", "deleted": False} - - from db import crud - crud.create_task(db_session, ArchiveCreate(id="delete-123-456-789", url="https://example.com", result={}, public=True, author_id="morty@example.com", group_id=None), [], []) - - response = client_with_auth.delete("/url/delete-123-456-789") - assert response.status_code == 200 - assert response.json() == {"id": "delete-123-456-789", "deleted": True} diff --git a/src/tests/user-groups.test.yaml b/src/tests/user-groups.test.yaml deleted file mode 100644 index 612f09d..0000000 --- a/src/tests/user-groups.test.yaml +++ /dev/null @@ -1,53 +0,0 @@ -# NOTE: all emails should be lower-cased -users: - rick@example.com: - - spaceship - - interdimensional - morty@example.com: - - spaceship - jerry@example.com: - - the-jerrys-club - summer@herself.com: - badyemail.com: - -domains: - example.com: - - animated-characters - birdy.com: - - animated-characters - - this-does-not-exist - - -orchestrators: - spaceship: tests/orchestration.test.yaml - interdimensional: tests/orchestration.test.yaml - default: tests/orchestration.test.yaml - -groups: - spaceship: - description: "The spaceship crew" - orchestrator: tests/orchestration.test.yaml - orchestrator_sheet: tests/orchestration.test.yaml - permissions: - read: ["all"] - active_sheets: -1 - monthly_urls: all - monthly_mbs: all - interdimensional: - description: "Interdimensional travelers" - orchestrator: tests/orchestration.test.yaml - orchestrator_sheet: tests/orchestration.test.yaml - permissions: - read: ["interdimensional", "animated-characters"] - active_sheets: 5 - monthly_urls: 1000 - monthly_mbs: 1000 - animated-characters: - description: "Animated characters" - orchestrator: tests/orchestration.test.yaml - orchestrator_sheet: tests/orchestration.test.yaml - permissions: - read: ["animated-characters"] - active_sheets: -1 - monthly_urls: all - monthly_mbs: all \ No newline at end of file diff --git a/src/tests/worker/test_worker_main.py b/src/tests/worker/test_worker_main.py deleted file mode 100644 index 5818443..0000000 --- a/src/tests/worker/test_worker_main.py +++ /dev/null @@ -1,196 +0,0 @@ -from unittest import mock - -from unittest.mock import MagicMock, patch - -import pytest - -from db import models, schemas -from auto_archiver import Metadata -from auto_archiver.core import Media - - -@pytest.fixture() -def worker_init(): - from worker.main import at_start - at_start(None) - - -class Test_create_archive_task(): - URL = "https://example-live.com" - archive = schemas.ArchiveCreate(url=URL, tags=[], public=True, group_id=None, author_id="rick@example.com") - - @patch("worker.main.insert_result_into_db") - @patch("worker.main.is_group_invalid_for_user", return_value=None) - @patch("worker.main.choose_orchestrator") - @patch("celery.app.task.Task.request") - def test_success(self, m_req, m_choose, m_is_group, m_insert, worker_init, db_session): - from worker.main import create_archive_task - - m_req.id = "this-just-in" - mock_orchestrator = self.mock_orchestrator_choice(m_choose) - - task = create_archive_task(self.archive.model_dump_json()) - - m_choose.assert_called_once() - mock_orchestrator.feed_item.assert_called_once() - - assert task["status"] == "success" - assert task["metadata"]["url"] == self.URL - assert len(task["media"]) == 0 - - @patch("worker.main.is_group_invalid_for_user", return_value=True) - def test_raise_invalid(self, m_is_group, worker_init): - from worker.main import create_archive_task - with pytest.raises(Exception): - create_archive_task(self.archive.model_dump_json()) - - @patch("worker.main.insert_result_into_db", side_effect=Exception) - @patch("worker.main.is_group_invalid_for_user", return_value=False) - @patch("worker.main.choose_orchestrator") - def test_raise_db_error(self, m_choose, m_is_group, m_insert, worker_init): - from worker.main import create_archive_task - mock_orchestrator = self.mock_orchestrator_choice(m_choose) - - with pytest.raises(Exception): - create_archive_task(self.archive.model_dump_json()) - mock_orchestrator.feed_item.assert_called_once() - - def mock_orchestrator_choice(self, m_choose): - mock_orchestrator = mock.MagicMock() - mock_orchestrator.configure_mock(feed_item=mock.MagicMock(return_value=Metadata().set_url(self.URL).success())) - m_choose.return_value = mock_orchestrator - return mock_orchestrator - - -class Test_create_sheet_task(): - URL = "https://example-live.com" - sheet = schemas.SubmitSheet(sheet_name="Sheet", sheet_id="123", author_id="rick@example.com", group_id=None) - - # @patch("worker.main.insert_result_into_db") - @patch("worker.main.models.generate_uuid", return_value="constant-uuid") - @patch("worker.main.is_group_invalid_for_user", return_value=False) - @patch("worker.main.ArchivingOrchestrator") - def test_success(self, m_orch_generator, m_is_group, m_uuid, worker_init, db_session): - from worker.main import create_sheet_task - - assert db_session.query(models.Archive).filter(models.Archive.url == self.URL).count() == 0 - - mock_metadata = Metadata().set_url(self.URL).success() - mock_metadata.add_media(Media("fn1.txt", urls=["outcome1.com"])) - m_orch = MagicMock() - m_orch.feed.return_value = iter([False, mock_metadata, mock_metadata]) - m_orch_generator.return_value = m_orch - - res = create_sheet_task(self.sheet.model_dump_json()) - print(res) - assert res["archived"] == 1 - assert res["failed"] == 0 - assert len(res["errors"]) == 0 - assert res["sheet"] == "Sheet" - assert res["sheet_id"] == "123" - assert res["success"] == True - assert len(res["time"]) > 0 - - # query created archive entry - inserted = db_session.query(models.Archive).filter(models.Archive.url == self.URL).one() - assert inserted is not None - assert inserted.url == self.URL - assert inserted.tags[0].id == "gsheet" - - @patch("worker.main.insert_result_into_db", side_effect=Exception("some-error")) - @patch("worker.main.models.generate_uuid", return_value="constant-uuid") - @patch("worker.main.is_group_invalid_for_user", return_value=False) - @patch("worker.main.ArchivingOrchestrator") - def test_has_exception(self, m_orch_generator, m_is_group, m_uuid, worker_init, db_session): - from worker.main import create_sheet_task - - assert db_session.query(models.Archive).filter(models.Archive.url == self.URL).count() == 0 - - mock_metadata = Metadata().set_url(self.URL).success() - mock_metadata.add_media(Media("fn1.txt", urls=["outcome1.com"])) - m_orch = MagicMock() - m_orch.feed.return_value = iter([mock_metadata]) - m_orch_generator.return_value = m_orch - - res = create_sheet_task(self.sheet.model_dump_json()) - print(res) - assert res["archived"] == 0 - assert res["failed"] == 1 - assert res["errors"] == ["some-error"] - assert res["sheet_id"] == "123" - assert res["success"] == True - - assert db_session.query(models.Archive).filter(models.Archive.url == self.URL).count() == 0 - - @patch("worker.main.is_group_invalid_for_user", return_value="Access denied") - def test_error_access(self, m_insert, worker_init, db_session): - from worker.main import create_sheet_task - - res = create_sheet_task(self.sheet.model_dump_json()) - assert "error" in res - assert res["error"] == "Access denied" - - -def test_choose_orchestrator(worker_init): - from worker.main import choose_orchestrator - - assert choose_orchestrator(None, "rick@example.com").__class__.__name__ == "ArchivingOrchestrator" - - -@patch("worker.main.get_user_first_group", return_value="does-not-exist") -def test_choose_orchestrator_assertion(worker_init): - from worker.main import choose_orchestrator - - with pytest.raises(Exception): - choose_orchestrator(None, "rick@example.com") - - -@patch("worker.main.read_user_groups") -def test_get_user_first_group(m_read_user_groups, worker_init): - from worker.main import get_user_first_group - - m_read_user_groups.return_value = {"users": {}} - assert get_user_first_group("email1") == "default" - m_read_user_groups.return_value = {"users": {"email1": []}} - assert get_user_first_group("email1") == "default" - m_read_user_groups.return_value = {"users": {"email1": ["group1", "group2"]}} - assert get_user_first_group("email1") == "group1" - - -def test_is_group_invalid_for_user(worker_init, db_session): - from worker.main import is_group_invalid_for_user - from db.crud import upsert_user_groups - - upsert_user_groups(db_session) - - assert is_group_invalid_for_user(True, "", "") == False - assert is_group_invalid_for_user(False, "", "") == False - - assert is_group_invalid_for_user(False, "default", "") == "User is not part of default, no permission" - assert is_group_invalid_for_user(False, "spaceship", "jerry@example.com") == "User jerry@example.com is not part of spaceship, no permission" - - assert is_group_invalid_for_user(False, "spaceship", "rick@example.com") == False - - -def test_get_all_urls(worker_init, db_session): - from worker.main import get_all_urls - from auto_archiver import Metadata - - meta = Metadata().set_url("https://example.com") - m1 = meta.add_media(Media("fn1.txt", urls=["outcome1.com"])) - m2 = meta.add_media(Media("fn2.txt", urls=["outcome2.com"])) - m3 = meta.add_media(Media("fn3.txt", urls=["outcome3.com"])) - m1.set("screenshot", Media("screenshot.png", urls=["screenshot.com"])) - m2.set("thumbnails", [Media("thumb1.png", urls=["thumb1.com"]), Media("thumb2.png", urls=["thumb2.com"])]) - m3.set("ssl_data", Media("ssl_data.txt", urls=["ssl_data.com"]).to_dict()) - m3.set("bad_data", {"bad": "dict is ignored"}) - - urls = [u.url for u in get_all_urls(meta)] - assert len(urls) == 7 - assert "outcome1.com" in urls - assert "outcome2.com" in urls - assert "outcome3.com" in urls - assert "screenshot.com" in urls - assert "thumb1.com" in urls - assert "thumb2.com" in urls - assert "ssl_data.com" in urls diff --git a/src/utils/mics.py b/src/utils/mics.py deleted file mode 100644 index f3a9803..0000000 --- a/src/utils/mics.py +++ /dev/null @@ -1,7 +0,0 @@ -import base64 -from fastapi.encoders import jsonable_encoder - -def custom_jsonable_encoder(obj): - if isinstance(obj, bytes): - return base64.b64encode(obj).decode('utf-8') - return jsonable_encoder(obj) \ No newline at end of file diff --git a/src/web/__init__.py b/src/web/__init__.py deleted file mode 100644 index cdcd353..0000000 --- a/src/web/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from web.main import app_factory - - -app = app_factory \ No newline at end of file diff --git a/src/web/main.py b/src/web/main.py deleted file mode 100644 index 826cc03..0000000 --- a/src/web/main.py +++ /dev/null @@ -1,167 +0,0 @@ -import os -from celery.result import AsyncResult -from fastapi import FastAPI, Depends, HTTPException -from fastapi.encoders import jsonable_encoder -from fastapi.responses import JSONResponse -from fastapi.staticfiles import StaticFiles -from fastapi.middleware.cors import CORSMiddleware -from prometheus_fastapi_instrumentator import Instrumentator -from datetime import datetime -import sqlalchemy -from sqlalchemy.orm import Session -from loguru import logger - -from core.logging import logging_middleware, log_error -from worker.main import create_archive_task, create_sheet_task, celery, insert_result_into_db - -from db import crud, models, schemas -from web.security import get_user_auth, token_api_key_auth, get_token_or_user_auth -from core.config import VERSION, API_DESCRIPTION -from db.database import get_db_dependency -from core.events import lifespan -from shared.settings import get_settings - -from auto_archiver import Metadata - -from endpoints import default_router, url_router, sheet_router, task_router, interoperability_router - - -def app_factory(settings = get_settings()): - app = FastAPI( - title="Auto-Archiver API", - description=API_DESCRIPTION, - version=VERSION, - contact={"name": "GitHub", "url": "https://github.com/bellingcat/auto-archiver-api"}, - lifespan=lifespan - ) - - app.add_middleware( - CORSMiddleware, - allow_origins=settings.ALLOWED_ORIGINS, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], - ) - app.middleware("http")(logging_middleware) - - app.include_router(default_router) - app.include_router(url_router) - app.include_router(sheet_router) - app.include_router(task_router) - app.include_router(interoperability_router) - - # prometheus exposed in /metrics with authentication - Instrumentator(should_group_status_codes=False, excluded_handlers=["/metrics", "/health", "/openapi.json", "/favicon.ico"]).instrument(app).expose(app, dependencies=[Depends(token_api_key_auth)]) - - local_dir = settings.SERVE_LOCAL_ARCHIVE - if not os.path.isdir(local_dir) and os.path.isdir(local_dir.replace("/app", ".")): - local_dir = local_dir.replace("/app", ".") - if len(settings.SERVE_LOCAL_ARCHIVE) > 1 and os.path.isdir(local_dir): - logger.warning(f"MOUNTing local archive {settings.SERVE_LOCAL_ARCHIVE}") - app.mount(settings.SERVE_LOCAL_ARCHIVE, StaticFiles(directory=local_dir), name=settings.SERVE_LOCAL_ARCHIVE) - - - - # -----Submit URL and manipulate tasks. Bearer protected below - - - @app.get("/tasks/search-url", response_model=list[schemas.Archive], deprecated=True) # DEPRECATED - def search_by_url(url: str, skip: int = 0, limit: int = 100, archived_after: datetime = None, archived_before: datetime = None, db: Session = Depends(get_db_dependency), email=Depends(get_token_or_user_auth)): - return crud.search_archives_by_url(db, url.strip(), email, skip=skip, limit=limit, archived_after=archived_after, archived_before=archived_before) - - - @app.get("/tasks/sync", response_model=list[schemas.Archive], deprecated=True) # DEPRECATED - def search(skip: int = 0, limit: int = 100, db: Session = Depends(get_db_dependency), email=Depends(get_user_auth)): - return crud.search_archives_by_email(db, email, skip=skip, limit=limit) - - - @app.post("/tasks", status_code=201, deprecated=True) # DEPRECATED - def archive_tasks(archive: schemas.ArchiveCreate, email=Depends(get_token_or_user_auth)): - archive.author_id = email - url = archive.url - logger.info(f"new {archive.public=} task for {email=} and {archive.group_id=}: {url}") - if type(url) != str or len(url) <= 5: - raise HTTPException(status_code=422, detail=f"Invalid URL received: {url}") - logger.info("creating task") - task = create_archive_task.delay(archive.model_dump_json()) - return JSONResponse({"id": task.id}) - - - @app.get("/archive/{task_id}", deprecated=True) # DEPRECATED - def lookup(task_id, db: Session = Depends(get_db_dependency), email=Depends(get_token_or_user_auth)): - return crud.get_archive(db, task_id, email) - - - @app.get("/tasks/{task_id}", deprecated=True) # DEPRECATED - def get_status(task_id, email=Depends(get_token_or_user_auth)): - logger.info(f"status check for user {email} task {task_id}") - task = AsyncResult(task_id, app=celery) - try: - if task.status == "FAILURE": - # *FAILURE* The task raised an exception, or has exceeded the retry limit. - # The :attr:`result` attribute then contains the exception raised by the task. - # https://docs.celeryq.dev/en/stable/_modules/celery/result.html#AsyncResult - raise task.result - - response = { - "id": task_id, - "status": task.status, - "result": task.result - } - return JSONResponse(jsonable_encoder(response, exclude_unset=True)) - - except Exception as e: - log_error(e) - return JSONResponse({ - "id": task_id, - "status": "FAILURE", - "result": {"error": str(e)} - }) - - - @app.delete("/tasks/{task_id}", deprecated=True) # DEPRECATED - def delete_task(task_id, db: Session = Depends(get_db_dependency), email=Depends(get_user_auth)): - logger.info(f"deleting task {task_id} request by {email}") - return JSONResponse({ - "id": task_id, - "deleted": crud.soft_delete_task(db, task_id, email) - }) - - # ----- Google Sheets Logic - - - @app.post("/sheet", status_code=201, deprecated=True) # DEPRECATED - def archive_sheet(sheet: schemas.SubmitSheet, email=Depends(get_user_auth)): - logger.info(f"SHEET TASK for {sheet=}") - sheet.author_id = email - if not sheet.sheet_name and not sheet.sheet_id: - raise HTTPException(status_code=422, detail=f"sheet name or id is required") - task = create_sheet_task.delay(sheet.model_dump_json()) - return JSONResponse({"id": task.id}) - - - @app.post("/sheet_service", status_code=201, deprecated=True) # DEPRECATED - def archive_sheet_service(sheet: schemas.SubmitSheet, auth=Depends(token_api_key_auth)): - logger.info(f"SHEET TASK for {sheet=}") - sheet.author_id = sheet.author_id or "api-endpoint" - if not sheet.sheet_name and not sheet.sheet_id: - raise HTTPException(status_code=422, detail=f"sheet name or id is required") - task = create_sheet_task.delay(sheet.model_dump_json()) - return JSONResponse({"id": task.id}) - - # ----- endpoint to submit data archived elsewhere - - - @app.post("/submit-archive", status_code=201, deprecated=True) # DEPRECATED - def submit_manual_archive(manual: schemas.SubmitManual, auth=Depends(token_api_key_auth)): - result = Metadata.from_json(manual.result) - logger.info(f"MANUAL SUBMIT {result.get_url()} {manual.author_id}") - manual.tags.add("manual") - try: - archive_id = insert_result_into_db(result, manual.tags, manual.public, manual.group_id, manual.author_id, models.generate_uuid()) - except sqlalchemy.exc.IntegrityError as e: - log_error(e) - raise HTTPException(status_code=422, detail=f"Cannot insert into DB due to integrity error") - return JSONResponse({"id": archive_id}) - - return app \ No newline at end of file diff --git a/src/worker/main.py b/src/worker/main.py deleted file mode 100644 index bde1073..0000000 --- a/src/worker/main.py +++ /dev/null @@ -1,219 +0,0 @@ - -import traceback, yaml, datetime -from typing import List, Set - -from celery import Celery -from celery.signals import task_failure, worker_init -from auto_archiver import Config, ArchivingOrchestrator, Metadata -from auto_archiver.core import Media -from loguru import logger - -from db import crud, schemas, models -from db.database import get_db -from shared.settings import get_settings -import json -import redis -from sqlalchemy import exc -from core.logging import log_error - -settings = get_settings() - -celery = Celery(__name__) -celery.conf.broker_url = settings.CELERY_BROKER_URL -celery.conf.result_backend = settings.CELERY_RESULT_BACKEND -USER_GROUPS_FILENAME = settings.USER_GROUPS_FILENAME - -Rdis = redis.Redis.from_url(celery.conf.broker_url) - - -@celery.task(name="create_archive_task", bind=True, autoretry_for=(Exception,), retry_backoff=True, retry_kwargs={'max_retries': 3}) -def create_archive_task(self, archive_json: str): - archive = schemas.ArchiveCreate.model_validate_json(archive_json) - logger.info(f"Archiving {archive.url=} {archive.tags=} {archive.public=} {archive.group_id=} {archive.author_id=}") - invalid = is_group_invalid_for_user(archive.public, archive.group_id, archive.author_id) - if invalid: - raise Exception(invalid) # marks task FAILED, saves the Exception as result - - url = archive.url - logger.info(f"{url=} {archive=}") - - # TODO: re-evaluate if this logic is to be used - if not archive.rearchive: - with get_db() as session: - archives = crud.search_archives_by_url(session, url, archive.author_id, absolute_search=True) - if len(archives): - logger.info(f"Skipping {url=} as it was already archived") - return Metadata.choose_most_complete([a.result for a in archives]) - - orchestrator = choose_orchestrator(archive.group_id, archive.author_id) - result = orchestrator.feed_item(Metadata().set_url(url)) - - try: - insert_result_into_db(result, archive.tags, archive.public, archive.group_id, archive.author_id, self.request.id) - except Exception as e: - # Log it, then raise again to store the error as the task result - log_error(e) - redis_publish_exception(e, self.name, traceback.format_exc()) - raise e - return result.to_dict() - - -@celery.task(name="create_sheet_task", bind=True, autoretry_for=(Exception,), retry_backoff=True, retry_kwargs={'max_retries': 0}) -def create_sheet_task(self, sheet_json: str): - sheet = schemas.SubmitSheet.model_validate_json(sheet_json) - sheet.tags.add("gsheet") - logger.info(f"SHEET START {sheet=}") - - if (em := is_group_invalid_for_user(sheet.public, sheet.group_id, sheet.author_id)): - return {"error": em} - - config = Config() - # TODO: use choose_orchestrator and overwrite the feeder - config.parse(use_cli=False, yaml_config_filename=get_settings().SHEET_ORCHESTRATION_YAML, overwrite_configs={"configurations": {"gsheet_feeder": {"sheet": sheet.sheet_name, "sheet_id": sheet.sheet_id, "header": sheet.header}}}) - orchestrator = ArchivingOrchestrator(config) - - stats = {"archived": 0, "failed": 0, "errors": []} - for result in orchestrator.feed(): - if not result: - logger.error("Got empty result from feeder, an internal error must have occurred.") - continue - try: - insert_result_into_db(result, sheet.tags, sheet.public, sheet.group_id, sheet.author_id, models.generate_uuid()) - stats["archived"] += 1 - except exc.IntegrityError as e: - logger.warning(f"cached result detected: {e}") - except Exception as e: - log_error(e, extra=f"{self.name}: {sheet_json}") - redis_publish_exception(e, self.name, traceback.format_exc()) - stats["failed"] += 1 - stats["errors"].append(str(e)) - - logger.info(f"SHEET DONE {sheet=}") - return {"success": True, "sheet": sheet.sheet_name, "sheet_id": sheet.sheet_id, "time": datetime.datetime.now().isoformat(), **stats} - - -@task_failure.connect(sender=create_sheet_task) -@task_failure.connect(sender=create_archive_task) -def task_failure_notifier(sender, **kwargs): - traceback_msg = "\n".join(traceback.format_list(traceback.extract_tb(kwargs['traceback']))) - logger.warning("😅 From task_failure_notifier ==> Task failed successfully!") - log_error(kwargs['exception'], traceback_msg, f"task_failure: {sender.name}") - redis_publish_exception(kwargs['exception'], sender.name, traceback_msg) - - -def choose_orchestrator(group, email): - global ORCHESTRATORS - if group not in ORCHESTRATORS: group = get_user_first_group(email) - assert group in ORCHESTRATORS, f"{group=} not in configurations" - logger.info(f"CHOOSE Orchestrator for {group=}, {email=}") - return ArchivingOrchestrator(ORCHESTRATORS.get(group)) - - -def read_user_groups(): - # read yaml safely - with open(USER_GROUPS_FILENAME) as inf: - try: - return yaml.safe_load(inf) - except yaml.YAMLError as e: - logger.error(f"could not open user groups filename {USER_GROUPS_FILENAME}: {e}") - raise e - - -def get_user_first_group(email): - user_groups_yaml = read_user_groups() - groups = user_groups_yaml.get("users", {}).get(email, []) - if groups != None and len(groups): - return groups[0] - return "default" - - -def load_orchestrators(): - global ORCHESTRATORS - ORCHESTRATORS = {} - """ - reads the orchestrators key in the config file to load different orchestrators for different groups - """ - user_groups_yaml = read_user_groups() - - orchestrators_config = user_groups_yaml.get("orchestrators", {}) - assert len(orchestrators_config), f"No orchestrators key found in {USER_GROUPS_FILENAME}. please see the example file" - assert "default" in orchestrators_config, "please include a 'default' orchestrator to be used when the user has no group" - logger.debug(f"Found {len(orchestrators_config)} group orchestrators.") - - for group, config_filename in orchestrators_config.items(): - config = Config() - config.parse(use_cli=False, yaml_config_filename=config_filename) - ORCHESTRATORS[group] = config - return ORCHESTRATORS - - -def is_group_invalid_for_user(public: bool, group_id: str, author_id: str): - """ - ensures that, if a group is specified, the user belongs to it. - if public is true the requirement is not needed - returns an error message if invalid, or False if all is good. - """ - if public: return False - if not group_id or len(group_id) == 0: return False - - # otherwise group must match - with get_db() as session: - if not crud.is_user_in_group(session, group_id, author_id): - logger.error(em := f"User {author_id} is not part of {group_id}, no permission") - return em - return False - - -def insert_result_into_db(result: Metadata, tags: Set[str], public: bool, group_id: str, author_id: str, task_id: str) -> str: - logger.info(f"INSERTING {public=} {group_id=} {author_id=} {tags=} into {task_id}") - assert result, f"UNABLE TO archive: {result.get_url() if result else result}" - with get_db() as session: - # urls are created by get_all_urls - # create author_id if needed - crud.create_or_get_user(session, author_id) - # create DB TAGs if needed - db_tags = [crud.create_tag(session, tag) for tag in tags] - # insert archive - db_task = crud.create_task(session, task=schemas.ArchiveCreate(id=task_id, url=result.get_url(), result=json.loads(result.to_json()), public=public, author_id=author_id, group_id=group_id), tags=db_tags, urls=get_all_urls(result)) - logger.debug(f"Added {db_task.id=} to database on {db_task.created_at} ({db_task.author_id})") - return db_task.id - - -def get_all_urls(result: Metadata) -> List[models.ArchiveUrl]: - db_urls = [] - for m in result.media: - for i, url in enumerate(m.urls): db_urls.append(models.ArchiveUrl(url=url, key=m.get("id", f"media_{i}"))) - for k, prop in m.properties.items(): - if prop_converted := convert_if_media(prop): - for i, url in enumerate(prop_converted.urls): db_urls.append(models.ArchiveUrl(url=url, key=prop_converted.get("id", f"{k}_{i}"))) - if isinstance(prop, list): - for i, prop_media in enumerate(prop): - if prop_media := convert_if_media(prop_media): - for j, url in enumerate(prop_media.urls): - db_urls.append(models.ArchiveUrl(url=url, key=prop_media.get("id", f"{k}{prop_media.key}_{i}.{j}"))) - return db_urls - - -def convert_if_media(media): - if isinstance(media, Media): return media - elif isinstance(media, dict): - try: return Media.from_dict(media) - except Exception as e: - logger.debug(f"error parsing {media} : {e}") - return False - - -def redis_publish_exception(exception, task_name, traceback: str = ""): - REDIS_EXCEPTIONS_CHANNEL = settings.REDIS_EXCEPTIONS_CHANNEL - try: - Rdis.publish(REDIS_EXCEPTIONS_CHANNEL, json.dumps({"exception": exception, "task": task_name, "traceback": traceback}, default=str)) - except Exception as e: - log_error(e, f"[CRITICAL] Could not publish to {REDIS_EXCEPTIONS_CHANNEL}") - - -@worker_init.connect -def at_start(sender, **kwargs): - global ORCHESTRATORS - ORCHESTRATORS = {} - load_orchestrators() - logger.info("Orchestrators loaded successfully.") diff --git a/user-groups.example.yaml b/user-groups.example.yaml new file mode 100644 index 0000000..ec67f86 --- /dev/null +++ b/user-groups.example.yaml @@ -0,0 +1,62 @@ +# NOTE: all emails should be lower-cased +users: + user01@example.com: + - group1 + user02@example.com: + - group2 + user03@example.com: + - group1 + - group2 + +domains: + example.com: + - group-for-friends + gmail-example.com: + - group1 + + +groups: + group1: + description: "Group 1 which can do everything, no limits" + orchestrator: secrets/orchestration.group1.yaml + orchestrator_sheet: secrets/orchestration.group1-sheet.yaml + permissions: + read: ["all"] + archive_url: true + archive_sheet: true + sheet_frequency: ["hourly", "daily"] + max_sheets: -1 + max_archive_lifespan_months: -1 + max_monthly_urls: -1 + max_monthly_mbs: -1 + manually_trigger_sheet: true + group2: + description: "Group that can only archive URLs, not sheets, they can search their own group and group-for-friends archives." + orchestrator: secrets/orchestration.group2.yaml + orchestrator_sheet: secrets/orchestration-group2-sheet.yaml + permissions: + read: ["group2", "group-for-friends"] + archive_url: true + max_archive_lifespan_months: 12 + max_monthly_urls: 100 + max_monthly_mbs: 1000 + group-for-friends: + description: "Friends can have one sheet only which archives once a day" + orchestrator: secrets/orchestration.friends.yaml + orchestrator_sheet: secrets/orchestration.friends-sheet.yaml + permissions: + read: ["friends-1"] + archive_sheet: true + sheet_frequency: ["daily"] + max_sheets: 1 + max_archive_lifespan_months: 12 + max_monthly_urls: 1000 + max_monthly_mbs: 1000 + default: + description: "Public access, can only search public archives" + orchestrator: secrets/orchestration-default.yaml + orchestrator_sheet: secrets/orchestration-default.yaml + permissions: + read: ["default"] + read_public: true + \ No newline at end of file diff --git a/web.Dockerfile b/web.Dockerfile new file mode 100644 index 0000000..d142877 --- /dev/null +++ b/web.Dockerfile @@ -0,0 +1,22 @@ +# Stage 1: install dependencies +FROM python:3.10-slim AS build + +WORKDIR /aa-api +# TODO: multistage build +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + gcc \ + g++ \ + && rm -rf /var/lib/apt/lists/* + +RUN pip install --no-cache-dir poetry +COPY pyproject.toml poetry.lock README.md . +RUN poetry install --with web --no-interaction --no-ansi --no-cache + +# Copy the application code and configurations +COPY alembic.ini ./ +COPY ./app/ ./app/ +COPY user-groups.* ./app/ + +# Run the FastAPI app with Uvicorn +ENTRYPOINT ["poetry", "run"] diff --git a/worker.Dockerfile b/worker.Dockerfile new file mode 100644 index 0000000..4e24f87 --- /dev/null +++ b/worker.Dockerfile @@ -0,0 +1,33 @@ +# From python:3.10 +FROM bellingcat/auto-archiver:v0.13.4 + +# set work directory +WORKDIR /aa-api + +RUN curl -fsSL https://get.docker.com -o get-docker.sh && \ + sh get-docker.sh +# set environment variables +ENV LANG=C.UTF-8 \ + PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + POETRY_NO_INTERACTION=1 \ + POETRY_VIRTUALENVS_IN_PROJECT=1 \ + POETRY_VIRTUALENVS_CREATE=1 + +# install dependencies +RUN apt update -y && \ + apt install -y python3-venv && \ + python3 -m venv ./poetry-venv && \ + ./poetry-venv/bin/python -m pip install --upgrade pip && \ + ./poetry-venv/bin/python -m pip install "poetry>=2.0.0,<3.0.0" +COPY pyproject.toml poetry.lock ./ +RUN ./poetry-venv/bin/poetry install --without dev --no-root --no-cache + +# install dependencies + +# copy source code and .env files over +COPY alembic.ini ./ +COPY ./app/ ./app/ +COPY user-groups.* ./app/ + +ENTRYPOINT ["./poetry-venv/bin/poetry", "run"] \ No newline at end of file