From 59c1be597c57d4b5dc91be0f6c6bae4924f3cd63 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Tue, 5 Nov 2024 10:33:44 +0000
Subject: [PATCH 01/75] introduces Sheet models and auth flow
---
src/db/crud.py | 51 +++-
src/db/models.py | 33 ++-
src/db/schemas.py | 53 ++++-
src/endpoints/sheet.py | 83 +++++--
...21d2c96d8_add_sheet_id_to_archive_table.py | 42 ++++
...a012ec405b8_add_columns_to_groups_table.py | 13 +-
src/tests/conftest.py | 22 +-
src/tests/endpoints/test_default.py | 14 +-
src/tests/endpoints/test_interopreability.py | 10 +-
src/tests/endpoints/test_sheet.py | 218 +++++++++++++++---
src/tests/user-groups.test.yaml | 9 +-
src/web/security.py | 16 +-
src/worker/main.py | 3 +
13 files changed, 493 insertions(+), 74 deletions(-)
create mode 100644 src/migrations/versions/89121d2c96d8_add_sheet_id_to_archive_table.py
diff --git a/src/db/crud.py b/src/db/crud.py
index 7f9c67f..aae2cd8 100644
--- a/src/db/crud.py
+++ b/src/db/crud.py
@@ -126,7 +126,8 @@ def is_user_in_group(db: Session, group_name: str, email: str) -> models.Group:
return len(group_name) and len(email) and group_name in get_user_groups(db, email)
-def get_user_groups(db: Session, email: str):
+#TODO: maybe this can be cached? what about the db session?
+def get_user_groups(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. User does not need to be active.
"""
@@ -135,15 +136,53 @@ def get_user_groups(db: Session, email: str):
# 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]
+ 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 = [g[0] for g in domain_level_groups]
+ domain_level_groups_names = [g[0] for g in domain_level_groups]
- # combine and return
- return list(set(user_level_groups + domain_level_groups))
+ return list(set(user_level_groups_names + domain_level_groups_names))
+
+
+# --------------- SHEET
+
+def has_quota_sheet(db: Session, email: str, user_groups_names: list[str]) -> bool:
+ """
+ checks if a user has reached their sheet quota
+ """
+ user_sheets = db.query(models.Sheet).filter(models.Sheet.author_id == email).count()
+
+ user_groups = db.query(models.Group).filter(models.Group.id.in_(user_groups_names)).all()
+
+ quota = 0
+ for group in user_groups:
+ active_sheets = group.permissions.get("active_sheets", 0)
+ if active_sheets == -1: return True
+ quota = max(quota, active_sheets)
+ return user_sheets < quota
+
+
+def create_sheet(db: Session, sheet_id: str, sheet_name: str, email: str, group_id: str, frequency: str):
+ db_sheet = models.Sheet(id=sheet_id, name=sheet_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_sheets(db: Session, email: str) -> list[models.Sheet]:
+ return db.query(models.Sheet).filter(models.Sheet.author_id == email).order_by(models.Sheet.last_archived_at.desc()).all()
+
+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 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
# --------------- INIT User-Groups
@@ -255,5 +294,5 @@ def upsert_user_groups(db: Session):
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/models.py b/src/db/models.py
index 193adba..f782588 100644
--- a/src/db/models.py
+++ b/src/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",
@@ -24,24 +26,29 @@ association_table_user_groups = Table(
)
# data model tables
+
+
class Archive(Base):
__tablename__ = "archives"
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 to 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())
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,6 +68,7 @@ class Tag(Base):
archives = relationship("Archive", back_populates="tags", secondary=association_table_archive_tags)
+
class User(Base):
__tablename__ = "users"
@@ -68,8 +76,10 @@ class User(Base):
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"
@@ -81,4 +91,23 @@ class Group(Base):
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.")
+ stats = Column(JSON, default={}, doc="Sheet statistics like total links, total rows, ...")
+ last_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/src/db/schemas.py b/src/db/schemas.py
index 538609a..a67dac6 100644
--- a/src/db/schemas.py
+++ b/src/db/schemas.py
@@ -1,4 +1,4 @@
-from pydantic import BaseModel
+from pydantic import BaseModel, field_validator
from datetime import datetime
@@ -6,9 +6,10 @@ class Tag(BaseModel):
id: str
created_at: datetime
- model_config = { "from_attributes": True }
+ model_config = {"from_attributes": True}
__hash__ = object.__hash__
+
class ArchiveCreate(BaseModel):
id: str | None = None
url: str
@@ -26,7 +27,8 @@ class Archive(ArchiveCreate):
updated_at: datetime | None
deleted: bool
- model_config = { "from_attributes": True }
+ model_config = {"from_attributes": True}
+
class SubmitSheet(BaseModel):
sheet_name: str | None = None
@@ -36,31 +38,70 @@ class SubmitSheet(BaseModel):
author_id: str | None = None
group_id: str | None = None
tags: set[str] | None = set()
- columns: dict | None = {} # TODO: implement
+ columns: dict | None = {} # TODO: implement
+
class SubmitManual(BaseModel):
- result: str # should be a Metadata.to_json()
+ 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 REQUESTS BELOW
+# TODO: replace existing schemas with these
+
+
+class ArchiveUrl(BaseModel):
+ url: str
+ public: bool = False
+ author_id: str | None
+ group_id: str | 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
+ active: bool
+
+
+class SheetAdd(BaseModel):
+ id: str
+ name: str
+ group_id: str
+ frequency: str
+
+ @field_validator('frequency')
+ def validate_frequency(cls, v):
+ valid_frequencies = {"hourly", "daily"}
+ if v not in {"hourly", "daily"}:
+ raise ValueError(f"Invalid frequency: {v}. Must be one of {valid_frequencies}.")
+ return v
+
+
+class SheetResponse(SheetAdd):
+ author_id: str
+ stats: dict | None
+ last_archived_at: datetime | None
+ created_at: datetime
diff --git a/src/endpoints/sheet.py b/src/endpoints/sheet.py
index 5a32d4b..257ed92 100644
--- a/src/endpoints/sheet.py
+++ b/src/endpoints/sheet.py
@@ -2,23 +2,80 @@
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import JSONResponse
-from loguru import logger
+from sqlalchemy import exc
+from sqlalchemy.orm import Session
-from core.config import ALLOW_ANY_EMAIL
-from web.security import get_token_or_user_auth
-from db import schemas
+from web.security import token_api_key_auth, get_active_user_auth
+from db import schemas, crud
+from db.database import get_db_dependency
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")
+@sheet_router.post("/create", status_code=201, summary="Store a new Google Sheet for regular archiving.")
+def create_sheet(
+ sheet: schemas.SheetAdd,
+ email=Depends(get_active_user_auth),
+ db: Session = Depends(get_db_dependency),
+) -> schemas.SheetResponse:
+ user_groups_names = crud.get_user_groups(db, email)
+
+ if sheet.group_id not in user_groups_names:
+ raise HTTPException(status_code=403, detail="User does not have access to this group.")
+
+ if not crud.has_quota_sheet(db, email, user_groups_names):
+ raise HTTPException(status_code=429, detail="User has reached their sheet quota.")
+
+ try:
+ return crud.create_sheet(db, sheet.id, sheet.name, email, sheet.group_id, sheet.frequency)
+ except exc.IntegrityError as e:
+ raise HTTPException(status_code=400, detail="Sheet with this ID already exists.") from e
+
+
+@sheet_router.get("/mine", status_code=200, summary="Get the authenticated user's Google Sheets.")
+def get_user_sheets(
+ email=Depends(get_active_user_auth),
+ db: Session = Depends(get_db_dependency)
+) -> list[schemas.SheetResponse]:
+ return crud.get_user_sheets(db, email)
+
+
+@sheet_router.delete("/{id}", summary="Delete a Google Sheet by ID.")
+def delete_sheet(
+ id: str,
+ email=Depends(get_active_user_auth),
+ db: Session = Depends(get_db_dependency),
+) -> schemas.TaskDelete:
+ return JSONResponse({
+ "id": id,
+ "deleted": crud.delete_sheet(db, id, 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,
+ email=Depends(get_active_user_auth),
+ db: Session = Depends(get_db_dependency),
+) -> schemas.Task:
+
+ sheet = crud.get_user_sheet(db, email, sheet_id=id)
+ if not sheet:
+ raise HTTPException(status_code=403, detail="No access to this sheet.")
+
+ task = create_sheet_task.delay(schemas.SubmitSheet(sheet_id=id, author_id=email, group=sheet.group_id).model_dump_json())
+
+ return JSONResponse({"id": task.id}, status_code=201)
+
+
+@sheet_router.post("/archive", status_code=201, summary="Trigger an archiving task for any GSheet with an API token.", response_description="task_id for the archiving task.")
+def archive_sheet(
+ sheet: schemas.SubmitSheet, #TODO: replace with simpler model
+ auth=Depends(token_api_key_auth)
+) -> schemas.Task:
+ sheet.author_id = sheet.author_id or "api-endpoint"
+ if not sheet.sheet_id:
+ raise HTTPException(status_code=422, detail=f"sheet 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
+ return JSONResponse({"id": task.id}, status_code=201)
diff --git a/src/migrations/versions/89121d2c96d8_add_sheet_id_to_archive_table.py b/src/migrations/versions/89121d2c96d8_add_sheet_id_to_archive_table.py
new file mode 100644
index 0000000..bbdbee1
--- /dev/null
+++ b/src/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 = Inspector.from_engine(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 = Inspector.from_engine(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/fa012ec405b8_add_columns_to_groups_table.py b/src/migrations/versions/fa012ec405b8_add_columns_to_groups_table.py
index da77d41..be94c98 100644
--- a/src/migrations/versions/fa012ec405b8_add_columns_to_groups_table.py
+++ b/src/migrations/versions/fa012ec405b8_add_columns_to_groups_table.py
@@ -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 = Inspector.from_engine(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/tests/conftest.py b/src/tests/conftest.py
index 97c18e8..6188d9f 100644
--- a/src/tests/conftest.py
+++ b/src/tests/conftest.py
@@ -16,6 +16,7 @@ def mock_logger_add():
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:
@@ -26,7 +27,7 @@ def mock_settings():
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)
@@ -72,10 +73,10 @@ def client(app):
@pytest.fixture()
def app_with_auth(app):
- from web.security import get_token_or_user_auth, get_user_auth, token_api_key_auth
+ from web.security import get_token_or_user_auth, get_user_auth, get_active_user_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"
+ app.dependency_overrides[get_active_user_auth] = lambda: "morty@example.com"
return app
@@ -85,6 +86,19 @@ def client_with_auth(app_with_auth):
return client
+@pytest.fixture()
+def app_with_token(app):
+ from web.security import token_api_key_auth
+ app.dependency_overrides[token_api_key_auth] = lambda: "jerry@example.com"
+ 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
@@ -92,4 +106,4 @@ def test_no_auth():
response = http_method(endpoint)
assert response.status_code == 403
assert response.json() == {"detail": "Not authenticated"}
- return no_auth
\ No newline at end of file
+ return no_auth
diff --git a/src/tests/endpoints/test_default.py b/src/tests/endpoints/test_default.py
index 6a585e6..9455bcb 100644
--- a/src/tests/endpoints/test_default.py
+++ b/src/tests/endpoints/test_default.py
@@ -101,10 +101,16 @@ def test_favicon(client_with_auth):
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_auth, get_settings):
+async def test_prometheus_metrics(test_data, client_with_token, get_settings):
# before metrics calculation
- r = client_with_auth.get("/metrics")
+ 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
@@ -116,7 +122,7 @@ async def test_prometheus_metrics(test_data, client_with_auth, get_settings):
# 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")
+ 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
@@ -130,7 +136,7 @@ async def test_prometheus_metrics(test_data, client_with_auth, get_settings):
# 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")
+ 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"} 4.0' in r3.text
diff --git a/src/tests/endpoints/test_interopreability.py b/src/tests/endpoints/test_interopreability.py
index 82136f0..8021bfe 100644
--- a/src/tests/endpoints/test_interopreability.py
+++ b/src/tests/endpoints/test_interopreability.py
@@ -5,15 +5,19 @@ 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):
+def test_submit_manual_archive_not_user_auth(client_with_auth, test_no_auth):
+ test_no_auth(client_with_auth.post, "/interop/submit-archive")
+
+
+def test_submit_manual_archive(client_with_token):
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"]})
+ r = client_with_token.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"]})
+ r = client_with_token.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
index f3e2559..ef56361 100644
--- a/src/tests/endpoints/test_sheet.py
+++ b/src/tests/endpoints/test_sheet.py
@@ -1,46 +1,210 @@
+from datetime import datetime
import json
from unittest.mock import patch
+from fastapi.testclient import TestClient
+
from db.schemas import TaskResult
-def test_sheet_no_auth(client, test_no_auth):
+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")
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):
+def test_create_sheet_endpoint(app_with_auth):
+ client_with_auth = TestClient(app_with_auth)
+ good_data = {
+ "id": "123-sheet-id",
+ "name": "Test Sheet",
+ "group_id": "spaceship",
+ "frequency": "daily"
+ }
- response = client_with_auth.post("/sheet/archive", json={"sheet_id": "123-sheet-id"})
+ # with good data
+ response = client_with_auth.post("/sheet/create", json=good_data)
assert response.status_code == 201
- assert response.json() == {'id': '123-456-789'}
+ j = response.json()
+ assert datetime.fromisoformat(j.pop("created_at"))
+ assert datetime.fromisoformat(j.pop("last_archived_at"))
+ assert j.pop("stats") == {}
+ assert j.pop("author_id") == 'morty@example.com'
+ assert j == good_data
- 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}
+ # 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 already exists."}
+ # bad frequency
+ bad_data = good_data.copy()
+ bad_data["frequency"] = "every hour"
+ response = client_with_auth.post("/sheet/create", json=bad_data)
+ assert response.status_code == 422
+ assert "Value error, Invalid frequency: every hour. Must be one of" in response.json()["detail"][0]["msg"]
-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"}
+ # 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."}
-
-@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"})
+ # bad quota
+ jerry_data = good_data.copy()
+ jerry_data["group_id"] = "animated-characters"
+ jerry_data["id"] = "jerry-sheet-id"
+ from web.security import get_active_user_auth
+ app_with_auth.dependency_overrides[get_active_user_auth] = lambda: "jerry@example.com"
+ client_jerry = TestClient(app_with_auth)
+ response = client_jerry.post("/sheet/create", json=jerry_data)
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_jerry.post("/sheet/create", json=jerry_data)
+ assert response.status_code == 429
+ assert response.json() == {"detail": "User has reached their sheet quota."}
- 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}
+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 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_archived_at"))
+ assert datetime.fromisoformat(r[1].pop("created_at"))
+ assert datetime.fromisoformat(r[1].pop("last_archived_at"))
+ assert r[0] == {
+ 'id': '123',
+ 'author_id': 'morty@example.com',
+ 'frequency': 'hourly',
+ 'group_id': 'spaceship',
+ 'name': 'Test Sheet 1',
+ 'stats': {},
+ }
+ assert r[1] == {
+ 'id': '456',
+ 'author_id': 'morty@example.com',
+ 'frequency': 'daily',
+ 'group_id': 'interdimensional',
+ 'name': 'Test Sheet 2',
+ 'stats': {},
+ }
+
+
+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 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}
+
+
+# def test_archive_user_sheet_endpoint(client_with_auth):
+# response = client_with_auth.post("/sheet/123-sheet-id/archive")
+# assert response.status_code == 201
+# assert "id" in response.json()
+
+
+class TestArchiveUserSheetEndpoint:
+ 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 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."}
+
+ @patch("worker.main.create_sheet_task.delay", return_value=TaskResult(id="123-taskid", status="PENDING", result=""))
+ def test_normal_flow(self, m1, client_with_auth, db_session):
+ from 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()
+ r = client_with_auth.post("/sheet/123-sheet-id/archive")
+ assert r.status_code == 201
+ assert r.json() == {"id": "123-taskid"}
+ m1.assert_called_once()
+
+
+class TestTokenArchiveEndpoint:
+
+ def test_user_auth(self, client_with_auth, test_no_auth):
+ test_no_auth(client_with_auth.post, "/sheet/archive")
+
+ def test_missing_data(self, client_with_token):
+ r = client_with_token.post("/sheet/archive", json={})
+ assert r.status_code == 422
+ assert r.json() == {"detail": "sheet id is required"}
+
+ @patch("worker.main.create_sheet_task.delay", return_value=TaskResult(id="123-456-789", status="PENDING", result=""))
+ def test_normal_flow(self, m1, client_with_token):
+
+ # minimum data
+ response = client_with_token.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": "api-endpoint", "group_id": None, "tags": [], "columns": {}, "header": 1}
+
+ # maximum data
+ response = client_with_token.post("/sheet/archive", json={"sheet_id": "123-sheet-id", "sheet_name": "768-sheet-name", "author_id": "birdman@example.com", "header": 2, "public": True, "group_id": "456-group-id", "tags": ["tag1"], "columns": {"col1": "type1"}})
+ assert response.status_code == 201
+ assert response.json() == {'id': '123-456-789'}
+
+ m1.call_count == 2
+ called_val = m1.call_args.args[0]
+ assert json.loads(called_val) == {"sheet_id": "123-sheet-id", "sheet_name": "768-sheet-name", "public": True, "author_id": "birdman@example.com", "group_id": "456-group-id", "tags": ["tag1"], "columns": {"col1": "type1"}, "header": 2}
diff --git a/src/tests/user-groups.test.yaml b/src/tests/user-groups.test.yaml
index 612f09d..aa18c76 100644
--- a/src/tests/user-groups.test.yaml
+++ b/src/tests/user-groups.test.yaml
@@ -33,6 +33,7 @@ groups:
active_sheets: -1
monthly_urls: all
monthly_mbs: all
+ alowed_frequency: "hourly"
interdimensional:
description: "Interdimensional travelers"
orchestrator: tests/orchestration.test.yaml
@@ -42,12 +43,14 @@ groups:
active_sheets: 5
monthly_urls: 1000
monthly_mbs: 1000
+ alowed_frequency: "hourly"
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
+ active_sheets: 1
+ monthly_urls: 2
+ monthly_mbs: 10
+ alowed_frequency: "daily"
\ No newline at end of file
diff --git a/src/web/security.py b/src/web/security.py
index bfa678a..141cd1b 100644
--- a/src/web/security.py
+++ b/src/web/security.py
@@ -4,6 +4,8 @@ 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 db.database import get_db
+from db import crud
settings = get_settings()
bearer_security = HTTPBearer()
@@ -54,6 +56,18 @@ async def get_user_auth(credentials: HTTPAuthorizationCredentials = Depends(bear
)
+async def get_active_user_auth(credentials: HTTPAuthorizationCredentials = Depends(bearer_security)):
+ # validates Bearer token and Active User status
+ try:
+ email = await get_user_auth(credentials)
+ with get_db() as db:
+ if crud.is_active_user(db, email):
+ return email
+ raise HTTPException(status_code=403, detail="User is not active")
+ except HTTPException as e:
+ raise e
+
+
def authenticate_user(access_token):
# https://cloud.google.com/docs/authentication/token-types#access
if type(access_token) != str or len(access_token) < 10: return False, "invalid access_token"
@@ -69,7 +83,7 @@ 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"
diff --git a/src/worker/main.py b/src/worker/main.py
index bde1073..8fe97d5 100644
--- a/src/worker/main.py
+++ b/src/worker/main.py
@@ -64,11 +64,13 @@ def create_sheet_task(self, sheet_json: str):
sheet.tags.add("gsheet")
logger.info(f"SHEET START {sheet=}")
+ #TODO: should this check live here?
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
+ # TODO: drop sheet_name and use only sheet_id (new endpoints/models)
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)
@@ -78,6 +80,7 @@ def create_sheet_task(self, sheet_json: str):
logger.error("Got empty result from feeder, an internal error must have occurred.")
continue
try:
+ #TODO: remove public from sheet in new refactor
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:
From 66dd35db0c346ac181670ad1944cf55eb40fffdf Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Tue, 5 Nov 2024 10:34:12 +0000
Subject: [PATCH 02/75] renaming interop file
---
.../{test_interopreability.py => test_interoperability.py} | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename src/tests/endpoints/{test_interopreability.py => test_interoperability.py} (100%)
diff --git a/src/tests/endpoints/test_interopreability.py b/src/tests/endpoints/test_interoperability.py
similarity index 100%
rename from src/tests/endpoints/test_interopreability.py
rename to src/tests/endpoints/test_interoperability.py
From 46a3cbc0033ef724f5bb23fa4157c85af23dd7a4 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Tue, 5 Nov 2024 10:35:31 +0000
Subject: [PATCH 03/75] renames mics -> misc
---
src/endpoints/task.py | 2 +-
src/utils/{mics.py => misc.py} | 0
2 files changed, 1 insertion(+), 1 deletion(-)
rename src/utils/{mics.py => misc.py} (100%)
diff --git a/src/endpoints/task.py b/src/endpoints/task.py
index f446d12..a2250fd 100644
--- a/src/endpoints/task.py
+++ b/src/endpoints/task.py
@@ -9,7 +9,7 @@ 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 utils.misc import custom_jsonable_encoder
task_router = APIRouter(prefix="/task", tags=["Async task operations"])
diff --git a/src/utils/mics.py b/src/utils/misc.py
similarity index 100%
rename from src/utils/mics.py
rename to src/utils/misc.py
From 2209b09a9a41bfcdacee222bc994ba2f83e01bdf Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Tue, 5 Nov 2024 11:41:07 +0000
Subject: [PATCH 04/75] missing tests for security
---
src/tests/web/test_security.py | 22 ++++++++++++++++++++--
src/web/security.py | 13 +++++--------
2 files changed, 25 insertions(+), 10 deletions(-)
diff --git a/src/tests/web/test_security.py b/src/tests/web/test_security.py
index 64fe4d4..f82874c 100644
--- a/src/tests/web/test_security.py
+++ b/src/tests/web/test_security.py
@@ -36,8 +36,26 @@ async def test_get_token_or_user_auth_with_user():
@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"
+ good_user = HTTPAuthorizationCredentials(scheme="ipsum", credentials="valid-and-good")
+ assert await get_user_auth(good_user) == "summer@example.com"
+
+
+@patch("web.security.authenticate_user", return_value=(True, "summer@example.com"))
+@pytest.mark.asyncio
+async def test_get_active_user_auth_inactive(m1, db_session):
+ from web.security import get_active_user_auth
+
+ # inactive at first
+ creds = HTTPAuthorizationCredentials(scheme="ipsum", credentials="valid-and-good")
+ with pytest.raises(HTTPException):
+ await get_active_user_auth(creds)
+
+ from db import models
+ db_session.add(models.User(email="summer@example.com", is_active=True))
+ db_session.commit()
+ assert await get_active_user_auth(creds) == "summer@example.com"
+
+
@patch("web.security.secure_compare", return_value=False)
diff --git a/src/web/security.py b/src/web/security.py
index 141cd1b..224b86a 100644
--- a/src/web/security.py
+++ b/src/web/security.py
@@ -58,14 +58,11 @@ async def get_user_auth(credentials: HTTPAuthorizationCredentials = Depends(bear
async def get_active_user_auth(credentials: HTTPAuthorizationCredentials = Depends(bearer_security)):
# validates Bearer token and Active User status
- try:
- email = await get_user_auth(credentials)
- with get_db() as db:
- if crud.is_active_user(db, email):
- return email
- raise HTTPException(status_code=403, detail="User is not active")
- except HTTPException as e:
- raise e
+ email = await get_user_auth(credentials)
+ with get_db() as db:
+ if crud.is_active_user(db, email):
+ return email
+ raise HTTPException(status_code=403, detail="User is not active")
def authenticate_user(access_token):
From 9f9bbc93446a56fa235d67d7dbe6bd02dd873d11 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Wed, 22 Jan 2025 13:21:16 +0000
Subject: [PATCH 05/75] pushing bulk of changes
---
README.md | 2 +
src/Pipfile | 1 +
src/Pipfile.lock | 3759 +++++++++++---------------
src/db/README.md | 1 -
src/db/crud.py | 99 +-
src/db/database.py | 2 +-
src/db/models.py | 2 +-
src/db/schemas.py | 9 +
src/db/user_state.py | 142 +
src/endpoints/default.py | 27 +-
src/endpoints/sheet.py | 31 +-
src/endpoints/url.py | 32 +-
src/tests/conftest.py | 8 +-
src/tests/db/test_crud.py | 81 +-
src/tests/endpoints/test_sheet.py | 34 +-
src/tests/endpoints/test_url.py | 18 +-
src/tests/user-groups.test.yaml | 12 +-
src/tests/worker/test_worker_main.py | 8 -
src/web/main.py | 6 +-
src/web/security.py | 6 +
src/worker/main.py | 18 +-
21 files changed, 2048 insertions(+), 2250 deletions(-)
delete mode 100644 src/db/README.md
create mode 100644 src/db/user_state.py
diff --git a/README.md b/README.md
index 5cd1ee6..8582798 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,8 @@ orchestration must be from the console(?)
* turn off VPNs if connection to docker is not working
## User management
+TODO: update description and example
+- users/domains/groups
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.
diff --git a/src/Pipfile b/src/Pipfile
index dabe443..a8a1936 100644
--- a/src/Pipfile
+++ b/src/Pipfile
@@ -4,6 +4,7 @@ verify_ssl = true
name = "pypi"
[packages]
+oscrypto = {git = "https://github.com/wbond/oscrypto.git", ref = "d5f3437ed24257895ae1edd9e503cfb352e635a8"}
aiofiles = "==0.6.0"
celery = ">=5.0"
fastapi = "*"
diff --git a/src/Pipfile.lock b/src/Pipfile.lock
index 840330c..7ba5bf6 100644
--- a/src/Pipfile.lock
+++ b/src/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "da25332a2152541157c6873ec43ac771c5491bff7d60bb2714c26c4e6b40577f"
+ "sha256": "601b24d82095b8b58f1376755d9a1c5c3bbf144aa622e1eef9fc034835f097fe"
},
"pipfile-spec": 6,
"requires": {
@@ -24,95 +24,103 @@
"index": "pypi",
"version": "==0.6.0"
},
- "aiohttp": {
+ "aiohappyeyeballs": {
"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"
+ "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745",
+ "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8"
],
"markers": "python_version >= '3.8'",
- "version": "==3.9.1"
+ "version": "==2.4.4"
+ },
+ "aiohttp": {
+ "hashes": [
+ "sha256:0882c2820fd0132240edbb4a51eb8ceb6eef8181db9ad5291ab3332e0d71df5f",
+ "sha256:0a6d3fbf2232e3a08c41eca81ae4f1dff3d8f1a30bae415ebe0af2d2458b8a33",
+ "sha256:0b7fb429ab1aafa1f48578eb315ca45bd46e9c37de11fe45c7f5f4138091e2f1",
+ "sha256:0eb98d90b6690827dcc84c246811feeb4e1eea683c0eac6caed7549be9c84665",
+ "sha256:0fd82b8e9c383af11d2b26f27a478640b6b83d669440c0a71481f7c865a51da9",
+ "sha256:10b4ff0ad793d98605958089fabfa350e8e62bd5d40aa65cdc69d6785859f94e",
+ "sha256:1642eceeaa5ab6c9b6dfeaaa626ae314d808188ab23ae196a34c9d97efb68350",
+ "sha256:1dac54e8ce2ed83b1f6b1a54005c87dfed139cf3f777fdc8afc76e7841101226",
+ "sha256:1e69966ea6ef0c14ee53ef7a3d68b564cc408121ea56c0caa2dc918c1b2f553d",
+ "sha256:1f21bb8d0235fc10c09ce1d11ffbd40fc50d3f08a89e4cf3a0c503dc2562247a",
+ "sha256:2170816e34e10f2fd120f603e951630f8a112e1be3b60963a1f159f5699059a6",
+ "sha256:21fef42317cf02e05d3b09c028712e1d73a9606f02467fd803f7c1f39cc59add",
+ "sha256:249cc6912405917344192b9f9ea5cd5b139d49e0d2f5c7f70bdfaf6b4dbf3a2e",
+ "sha256:3499c7ffbfd9c6a3d8d6a2b01c26639da7e43d47c7b4f788016226b1e711caa8",
+ "sha256:3af41686ccec6a0f2bdc66686dc0f403c41ac2089f80e2214a0f82d001052c03",
+ "sha256:3e23419d832d969f659c208557de4a123e30a10d26e1e14b73431d3c13444c2e",
+ "sha256:3ea1b59dc06396b0b424740a10a0a63974c725b1c64736ff788a3689d36c02d2",
+ "sha256:44167fc6a763d534a6908bdb2592269b4bf30a03239bcb1654781adf5e49caf1",
+ "sha256:479b8c6ebd12aedfe64563b85920525d05d394b85f166b7873c8bde6da612f9c",
+ "sha256:4af57160800b7a815f3fe0eba9b46bf28aafc195555f1824555fa2cfab6c1538",
+ "sha256:4b4fa1cb5f270fb3eab079536b764ad740bb749ce69a94d4ec30ceee1b5940d5",
+ "sha256:4eed954b161e6b9b65f6be446ed448ed3921763cc432053ceb606f89d793927e",
+ "sha256:541d823548ab69d13d23730a06f97460f4238ad2e5ed966aaf850d7c369782d9",
+ "sha256:568c1236b2fde93b7720f95a890741854c1200fba4a3471ff48b2934d2d93fd3",
+ "sha256:5854be2f3e5a729800bac57a8d76af464e160f19676ab6aea74bde18ad19d438",
+ "sha256:620598717fce1b3bd14dd09947ea53e1ad510317c85dda2c9c65b622edc96b12",
+ "sha256:6526e5fb4e14f4bbf30411216780c9967c20c5a55f2f51d3abd6de68320cc2f3",
+ "sha256:6fba278063559acc730abf49845d0e9a9e1ba74f85f0ee6efd5803f08b285853",
+ "sha256:70d1f9dde0e5dd9e292a6d4d00058737052b01f3532f69c0c65818dac26dc287",
+ "sha256:731468f555656767cda219ab42e033355fe48c85fbe3ba83a349631541715ba2",
+ "sha256:81b8fe282183e4a3c7a1b72f5ade1094ed1c6345a8f153506d114af5bf8accd9",
+ "sha256:84a585799c58b795573c7fa9b84c455adf3e1d72f19a2bf498b54a95ae0d194c",
+ "sha256:85992ee30a31835fc482468637b3e5bd085fa8fe9392ba0bdcbdc1ef5e9e3c55",
+ "sha256:8811f3f098a78ffa16e0ea36dffd577eb031aea797cbdba81be039a4169e242c",
+ "sha256:88a12ad8ccf325a8a5ed80e6d7c3bdc247d66175afedbe104ee2aaca72960d8e",
+ "sha256:8be8508d110d93061197fd2d6a74f7401f73b6d12f8822bbcd6d74f2b55d71b1",
+ "sha256:8e2bf8029dbf0810c7bfbc3e594b51c4cc9101fbffb583a3923aea184724203c",
+ "sha256:929f3ed33743a49ab127c58c3e0a827de0664bfcda566108989a14068f820194",
+ "sha256:92cde43018a2e17d48bb09c79e4d4cb0e236de5063ce897a5e40ac7cb4878773",
+ "sha256:92fc484e34b733704ad77210c7957679c5c3877bd1e6b6d74b185e9320cc716e",
+ "sha256:943a8b052e54dfd6439fd7989f67fc6a7f2138d0a2cf0a7de5f18aa4fe7eb3b1",
+ "sha256:9d73ee3725b7a737ad86c2eac5c57a4a97793d9f442599bea5ec67ac9f4bdc3d",
+ "sha256:9f5b3c1ed63c8fa937a920b6c1bec78b74ee09593b3f5b979ab2ae5ef60d7600",
+ "sha256:9fd46ce0845cfe28f108888b3ab17abff84ff695e01e73657eec3f96d72eef34",
+ "sha256:a344d5dc18074e3872777b62f5f7d584ae4344cd6006c17ba12103759d407af3",
+ "sha256:a60804bff28662cbcf340a4d61598891f12eea3a66af48ecfdc975ceec21e3c8",
+ "sha256:a8f5f7515f3552d899c61202d99dcb17d6e3b0de777900405611cd747cecd1b8",
+ "sha256:a9b7371665d4f00deb8f32208c7c5e652059b0fda41cf6dbcac6114a041f1cc2",
+ "sha256:aa54f8ef31d23c506910c21163f22b124facb573bff73930735cf9fe38bf7dff",
+ "sha256:aba807f9569455cba566882c8938f1a549f205ee43c27b126e5450dc9f83cc62",
+ "sha256:ae545f31489548c87b0cced5755cfe5a5308d00407000e72c4fa30b19c3220ac",
+ "sha256:af01e42ad87ae24932138f154105e88da13ce7d202a6de93fafdafb2883a00ef",
+ "sha256:b540bd67cfb54e6f0865ceccd9979687210d7ed1a1cc8c01f8e67e2f1e883d28",
+ "sha256:b6212a60e5c482ef90f2d788835387070a88d52cf6241d3916733c9176d39eab",
+ "sha256:b63de12e44935d5aca7ed7ed98a255a11e5cb47f83a9fded7a5e41c40277d104",
+ "sha256:ba74ec819177af1ef7f59063c6d35a214a8fde6f987f7661f4f0eecc468a8f76",
+ "sha256:bb49c7f1e6ebf3821a42d81d494f538107610c3a705987f53068546b0e90303e",
+ "sha256:bd176afcf8f5d2aed50c3647d4925d0db0579d96f75a31e77cbaf67d8a87742d",
+ "sha256:bd7227b87a355ce1f4bf83bfae4399b1f5bb42e0259cb9405824bd03d2f4336a",
+ "sha256:bf8d9bfee991d8acc72d060d53860f356e07a50f0e0d09a8dfedea1c554dd0d5",
+ "sha256:bfde76a8f430cf5c5584553adf9926534352251d379dcb266ad2b93c54a29745",
+ "sha256:c341c7d868750e31961d6d8e60ff040fb9d3d3a46d77fd85e1ab8e76c3e9a5c4",
+ "sha256:c7a06301c2fb096bdb0bd25fe2011531c1453b9f2c163c8031600ec73af1cc99",
+ "sha256:cb23d8bb86282b342481cad4370ea0853a39e4a32a0042bb52ca6bdde132df43",
+ "sha256:d119fafe7b634dbfa25a8c597718e69a930e4847f0b88e172744be24515140da",
+ "sha256:d40f9da8cabbf295d3a9dae1295c69975b86d941bc20f0a087f0477fa0a66231",
+ "sha256:d6c9af134da4bc9b3bd3e6a70072509f295d10ee60c697826225b60b9959acdd",
+ "sha256:dd7659baae9ccf94ae5fe8bfaa2c7bc2e94d24611528395ce88d009107e00c6d",
+ "sha256:de8d38f1c2810fa2a4f1d995a2e9c70bb8737b18da04ac2afbf3971f65781d87",
+ "sha256:e595c591a48bbc295ebf47cb91aebf9bd32f3ff76749ecf282ea7f9f6bb73886",
+ "sha256:ec2aa89305006fba9ffb98970db6c8221541be7bee4c1d027421d6f6df7d1ce2",
+ "sha256:ec82bf1fda6cecce7f7b915f9196601a1bd1a3079796b76d16ae4cce6d0ef89b",
+ "sha256:ed9ee95614a71e87f1a70bc81603f6c6760128b140bc4030abe6abaa988f1c3d",
+ "sha256:f047569d655f81cb70ea5be942ee5d4421b6219c3f05d131f64088c73bb0917f",
+ "sha256:ffa336210cf9cd8ed117011085817d00abe4c08f99968deef0013ea283547204",
+ "sha256:ffb3dc385f6bb1568aa974fe65da84723210e5d9707e360e9ecb51f59406cd2e"
+ ],
+ "markers": "python_version >= '3.9'",
+ "version": "==3.11.11"
},
"aiosignal": {
"hashes": [
- "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc",
- "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"
+ "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5",
+ "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"
],
- "markers": "python_version >= '3.7'",
- "version": "==1.3.1"
+ "markers": "python_version >= '3.9'",
+ "version": "==1.3.2"
},
"aiosqlite": {
"hashes": [
@@ -123,30 +131,22 @@
"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"
+ "sha256:1acdd7a3a478e208b0503cd73614d5e4c6efafa4e73518bb60e4f2846a37b1c5",
+ "sha256:496e888245a53adf1498fcab31713a469c65836f8de76e01399aa1c3e90dd213"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
- "version": "==1.13.3"
+ "version": "==1.14.1"
},
"amqp": {
"hashes": [
- "sha256:827cb12fb0baa892aad844fd95258143bce4027fdac4fccddbc43330fd281637",
- "sha256:a1ecff425ad063ad42a486c902807d1482311481c8ad95a72694b2975e75f7fd"
+ "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2",
+ "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432"
],
"markers": "python_version >= '3.6'",
- "version": "==5.2.0"
+ "version": "==5.3.1"
},
"annotated-types": {
"hashes": [
@@ -158,11 +158,11 @@
},
"anyio": {
"hashes": [
- "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94",
- "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"
+ "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a",
+ "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"
],
- "markers": "python_version >= '3.8'",
- "version": "==4.4.0"
+ "markers": "python_version >= '3.9'",
+ "version": "==4.8.0"
},
"argparse": {
"hashes": [
@@ -180,27 +180,27 @@
},
"async-timeout": {
"hashes": [
- "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f",
- "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"
+ "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c",
+ "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"
],
- "markers": "python_version >= '3.7'",
- "version": "==4.0.3"
+ "markers": "python_version >= '3.8'",
+ "version": "==5.0.1"
},
"attrs": {
"hashes": [
- "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30",
- "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"
+ "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff",
+ "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"
],
- "markers": "python_version >= '3.7'",
- "version": "==23.2.0"
+ "markers": "python_version >= '3.8'",
+ "version": "==24.3.0"
},
"authlib": {
"hashes": [
- "sha256:4b16130117f9eb82aa6eec97f6dd4673c3f960ac0283ccdae2897ee4bc030ba2",
- "sha256:ede026a95e9f5cdc2d4364a52103f5405e75aa156357e831ef2bfd0bc5094dfc"
+ "sha256:1c1e6608b5ed3624aeeee136ca7f8c120d6f51f731aa152b153d54741840e1f2",
+ "sha256:4bb20b978c8b636222b549317c1815e1fe62234fc1c5efe8855d84aebf3a74e3"
],
- "markers": "python_version >= '3.8'",
- "version": "==1.3.2"
+ "markers": "python_version >= '3.9'",
+ "version": "==1.4.0"
},
"auto-archiver": {
"hashes": [
@@ -211,29 +211,13 @@
"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"
+ "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051",
+ "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"
],
"markers": "python_full_version >= '3.6.0'",
- "version": "==4.13.0b2"
+ "version": "==4.12.3"
},
"billiard": {
"hashes": [
@@ -243,65 +227,29 @@
"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"
+ "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf",
+ "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"
],
- "markers": "python_version >= '3.8'",
- "version": "==1.8.2"
+ "markers": "python_version >= '3.9'",
+ "version": "==1.9.0"
},
"boto3": {
"hashes": [
- "sha256:18416d07b41e6094101a44f8b881047dcec6b846dad0b9f83b9bbf2f0cd93d07",
- "sha256:7f8e8a252458d584d8cf7877c372c4f74ec103356eedf43d2dd9e479f47f3639"
+ "sha256:53a5307f6a3526ee2f8590e3c45efa504a3ea4532c1bfe4926c0c19bf188d141",
+ "sha256:f9843a5d06f501d66ada06f5a5417f671823af2cf319e36ceefa1bafaaaaa953"
],
"markers": "python_version >= '3.8'",
- "version": "==1.35.44"
+ "version": "==1.36.3"
},
"botocore": {
"hashes": [
- "sha256:1fcd97b966ad8a88de4106fe1bd3bbd6d8dadabe99bbd4a6aadcf11cb6c66b39",
- "sha256:55388e80624401d017a9a2b8109afd94814f7e666b53e28fce51375cfa8d9326"
+ "sha256:536ab828e6f90dbb000e3702ac45fd76642113ae2db1b7b1373ad24104e89255",
+ "sha256:775b835e979da5c96548ed1a0b798101a145aec3cd46541d62e27dda5a94d7f8"
],
"markers": "python_version >= '3.8'",
- "version": "==1.35.44"
+ "version": "==1.36.3"
},
"brotli": {
"hashes": [
@@ -431,7 +379,6 @@
"sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2",
"sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064"
],
- "markers": "platform_python_implementation >= 'CPython'",
"version": "==1.1.0"
},
"bs4": {
@@ -443,11 +390,11 @@
},
"cachetools": {
"hashes": [
- "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292",
- "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"
+ "sha256:70f238fbba50383ef62e55c6aff6d9673175fe59f7c6782c7a0b9e38f4a9df95",
+ "sha256:b76651fdc3b24ead3c648bbdeeb940c1b04d365b38b4af66788f9ec4a81d42bb"
],
"markers": "python_version >= '3.7'",
- "version": "==5.5.0"
+ "version": "==5.5.1"
},
"celery": {
"hashes": [
@@ -460,11 +407,11 @@
},
"certifi": {
"hashes": [
- "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b",
- "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"
+ "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56",
+ "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"
],
"markers": "python_version >= '3.6'",
- "version": "==2024.7.4"
+ "version": "==2024.12.14"
},
"certvalidator": {
"hashes": [
@@ -475,180 +422,182 @@
},
"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"
+ "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8",
+ "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2",
+ "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1",
+ "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15",
+ "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36",
+ "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824",
+ "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8",
+ "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36",
+ "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17",
+ "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf",
+ "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc",
+ "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3",
+ "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed",
+ "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702",
+ "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1",
+ "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8",
+ "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903",
+ "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6",
+ "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d",
+ "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b",
+ "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e",
+ "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be",
+ "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c",
+ "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683",
+ "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9",
+ "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c",
+ "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8",
+ "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1",
+ "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4",
+ "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655",
+ "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67",
+ "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595",
+ "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0",
+ "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65",
+ "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41",
+ "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6",
+ "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401",
+ "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6",
+ "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3",
+ "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16",
+ "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93",
+ "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e",
+ "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4",
+ "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964",
+ "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c",
+ "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576",
+ "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0",
+ "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3",
+ "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662",
+ "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3",
+ "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff",
+ "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5",
+ "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd",
+ "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f",
+ "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5",
+ "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14",
+ "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d",
+ "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9",
+ "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7",
+ "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382",
+ "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a",
+ "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e",
+ "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a",
+ "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4",
+ "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99",
+ "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87",
+ "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"
],
- "markers": "platform_python_implementation != 'PyPy'",
- "version": "==1.17.0rc1"
+ "markers": "python_version >= '3.8'",
+ "version": "==1.17.1"
},
"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"
+ "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537",
+ "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa",
+ "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a",
+ "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294",
+ "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b",
+ "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd",
+ "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601",
+ "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd",
+ "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4",
+ "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d",
+ "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2",
+ "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313",
+ "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd",
+ "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa",
+ "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8",
+ "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1",
+ "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2",
+ "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496",
+ "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d",
+ "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b",
+ "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e",
+ "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a",
+ "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4",
+ "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca",
+ "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78",
+ "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408",
+ "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5",
+ "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3",
+ "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f",
+ "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a",
+ "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765",
+ "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6",
+ "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146",
+ "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6",
+ "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9",
+ "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd",
+ "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c",
+ "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f",
+ "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545",
+ "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176",
+ "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770",
+ "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824",
+ "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f",
+ "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf",
+ "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487",
+ "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d",
+ "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd",
+ "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b",
+ "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534",
+ "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f",
+ "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b",
+ "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9",
+ "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd",
+ "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125",
+ "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9",
+ "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de",
+ "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11",
+ "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d",
+ "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35",
+ "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f",
+ "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda",
+ "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7",
+ "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a",
+ "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971",
+ "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8",
+ "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41",
+ "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d",
+ "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f",
+ "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757",
+ "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a",
+ "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886",
+ "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77",
+ "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76",
+ "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247",
+ "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85",
+ "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb",
+ "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7",
+ "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e",
+ "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6",
+ "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037",
+ "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1",
+ "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e",
+ "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807",
+ "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407",
+ "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c",
+ "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12",
+ "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3",
+ "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089",
+ "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd",
+ "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e",
+ "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00",
+ "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"
],
- "markers": "python_full_version >= '3.7.0'",
- "version": "==3.3.2"
+ "markers": "python_version >= '3.7'",
+ "version": "==3.4.1"
},
"click": {
"hashes": [
- "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28",
- "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"
+ "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2",
+ "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"
],
"markers": "python_version >= '3.7'",
- "version": "==8.1.7"
+ "version": "==8.1.8"
},
"click-didyoumean": {
"hashes": [
@@ -680,127 +629,42 @@
],
"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"
+ "sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960",
+ "sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a",
+ "sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc",
+ "sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a",
+ "sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf",
+ "sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1",
+ "sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39",
+ "sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406",
+ "sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a",
+ "sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a",
+ "sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c",
+ "sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be",
+ "sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15",
+ "sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2",
+ "sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d",
+ "sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157",
+ "sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003",
+ "sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248",
+ "sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a",
+ "sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec",
+ "sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309",
+ "sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7",
+ "sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d"
],
"markers": "python_version >= '3.7'",
- "version": "==42.0.8"
+ "version": "==41.0.7"
},
"dataclasses-json": {
"hashes": [
- "sha256:5ec6fed642adb1dbdb4182badb01e0861badfd8fda82e3b67f44b2d1e9d10d21",
- "sha256:d82896a94c992ffaf689cd1fafc180164e2abdd415b8f94a7f78586af5886236"
+ "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a",
+ "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0"
],
- "markers": "python_version < '3.13' and python_version >= '3.7'",
- "version": "==0.5.14"
+ "markers": "python_version >= '3.7' and python_version < '4.0'",
+ "version": "==0.6.7"
},
"dateparser": {
"hashes": [
@@ -810,14 +674,6 @@
"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",
@@ -828,21 +684,21 @@
},
"fastapi": {
"hashes": [
- "sha256:3995739e0b09fa12f984bce8fa9ae197b35d433750d3d312422d846e283697ee",
- "sha256:61704c71286579cc5a598763905928f24ee98bfcc07aabe84cfefb98812bbc86"
+ "sha256:9ec46f7addc14ea472958a96aae5b5de65f39721a46aaf5705c480d9a8b76654",
+ "sha256:e9240b29e36fa8f4bb7290316988e90c381e5092e0cbe84e7818cc3713bcf305"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
- "version": "==0.115.2"
+ "version": "==0.115.6"
},
"fastapi-utils": {
"hashes": [
- "sha256:074509405b02e2651dfe2d11862dd760bacc1a64508f3d8cc44e52a6dc1ed342",
- "sha256:4fc4d6a10b5c5c3f2ec564d360fc1188507b911e4b06ee4d4c111906d7ddeef1"
+ "sha256:6c4d507a76bab9a016cee0c4fa3a4638c636b2b2689e39c62254b1b2e4e81825",
+ "sha256:eca834e80c09f85df30004fe5e861981262b296f60c93d5a1a1416fe4c784140"
],
"index": "pypi",
- "markers": "python_version >= '3.7' and python_version < '4.0'",
- "version": "==0.7.0"
+ "markers": "python_version >= '3.8' and python_version < '4.0'",
+ "version": "==0.8.0"
},
"ffmpeg-python": {
"hashes": [
@@ -853,150 +709,149 @@
},
"filelock": {
"hashes": [
- "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0",
- "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"
+ "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338",
+ "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e"
],
- "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"
+ "markers": "python_version >= '3.9'",
+ "version": "==3.17.0"
},
"flask": {
"hashes": [
- "sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3",
- "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842"
+ "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac",
+ "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136"
],
- "markers": "python_version >= '3.8'",
- "version": "==3.0.3"
+ "markers": "python_version >= '3.9'",
+ "version": "==3.1.0"
},
"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"
+ "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e",
+ "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf",
+ "sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6",
+ "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a",
+ "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d",
+ "sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f",
+ "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28",
+ "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b",
+ "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9",
+ "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2",
+ "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec",
+ "sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2",
+ "sha256:17dcc32fc7bda7ce5875435003220a457bcfa34ab7924a49a1c19f55b6ee185c",
+ "sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336",
+ "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4",
+ "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d",
+ "sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b",
+ "sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c",
+ "sha256:2150cc6305a2c2ab33299453e2968611dacb970d2283a14955923062c8d00b10",
+ "sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08",
+ "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942",
+ "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8",
+ "sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f",
+ "sha256:2d0da8bbec082bf6bf18345b180958775363588678f64998c2b7609e34719b10",
+ "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5",
+ "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6",
+ "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21",
+ "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c",
+ "sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d",
+ "sha256:374ca2dabdccad8e2a76d40b1d037f5bd16824933bf7bcea3e59c891fd4a0923",
+ "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608",
+ "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de",
+ "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17",
+ "sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0",
+ "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f",
+ "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641",
+ "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c",
+ "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a",
+ "sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0",
+ "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9",
+ "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab",
+ "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f",
+ "sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3",
+ "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a",
+ "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784",
+ "sha256:73f2e31ea8dd7df61a359b731716018c2be196e5bb3b74ddba107f694fbd7604",
+ "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d",
+ "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5",
+ "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03",
+ "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e",
+ "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953",
+ "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee",
+ "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d",
+ "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817",
+ "sha256:828afae9f17e6de596825cf4228ff28fbdf6065974e5ac1410cecc22f699d2b3",
+ "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039",
+ "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f",
+ "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9",
+ "sha256:91d6c171862df0a6c61479d9724f22efb6109111017c87567cfeb7b5d1449fdf",
+ "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76",
+ "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba",
+ "sha256:97160e245ea33d8609cd2b8fd997c850b56db147a304a262abc2b3be021a9171",
+ "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb",
+ "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439",
+ "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631",
+ "sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972",
+ "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d",
+ "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869",
+ "sha256:a72b7a6e3cd2725eff67cd64c8f13335ee18fc3c7befc05aed043d24c7b9ccb9",
+ "sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411",
+ "sha256:b97f7b575ab4a8af9b7bc1d2ef7f29d3afee2226bd03ca3875c16451ad5a7723",
+ "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2",
+ "sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b",
+ "sha256:c16d2fa63e0800723139137d667e1056bee1a1cf7965153d2d104b62855e9b99",
+ "sha256:c1fac3e2ace2eb1052e9f7c7db480818371134410e1f5c55d65e8f3ac6d1407e",
+ "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840",
+ "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3",
+ "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb",
+ "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3",
+ "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0",
+ "sha256:dd94994fc91a6177bfaafd7d9fd951bc8689b0a98168aa26b5f543868548d3ca",
+ "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45",
+ "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e",
+ "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f",
+ "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5",
+ "sha256:f1577515d35ed5649d52ab4319db757bb881ce3b2b796d7283e6634d99ace307",
+ "sha256:f1e6540b7fa044eee0bb5111ada694cf3dc15f2b0347ca125ee9ca984d5e9e6e",
+ "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2",
+ "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778",
+ "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a",
+ "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30",
+ "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a"
],
"markers": "python_version >= '3.8'",
- "version": "==1.4.1"
- },
- "furo": {
- "hashes": [
- "sha256:4ab2be254a2d5e52792d0ca793a12c35582dd09897228a6dd47885dabd5c9521",
- "sha256:b99e7867a5cc833b2b34d7230631dd6558c7a29f93071fdbb5709634bb33c5a5"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==2023.3.27"
+ "version": "==1.5.0"
},
"future": {
"hashes": [
"sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216",
"sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05"
],
- "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'",
"version": "==1.0.0"
},
"google-api-core": {
"hashes": [
- "sha256:4a152fd11a9f774ea606388d423b68aa7e6d6a0ffe4c8266f74979613ec09f81",
- "sha256:6869eacb2a37720380ba5898312af79a4d30b8bca1548fb4093e0697dc4bdf5d"
+ "sha256:10d82ac0fca69c82a25b3efdeefccf6f28e02ebb97925a8cce8edbfe379929d9",
+ "sha256:e255640547a597a4da010876d333208ddac417d60add22b6851a0c66a831fcaf"
],
"markers": "python_version >= '3.7'",
- "version": "==2.21.0"
+ "version": "==2.24.0"
},
"google-api-python-client": {
"hashes": [
- "sha256:1a5232e9cfed8c201799d9327e4d44dc7ea7daa3c6e1627fca41aa201539c0da",
- "sha256:b9d68c6b14ec72580d66001bd33c5816b78e2134b93ccc5cf8f624516b561750"
+ "sha256:55197f430f25c907394b44fa078545ffef89d33fd4dca501b7db9f0d8e224bd6",
+ "sha256:baef0bb631a60a0bd7c0bf12a5499e3a40cd4388484de7ee55c1950bf820a0cf"
],
"markers": "python_version >= '3.7'",
- "version": "==2.149.0"
+ "version": "==2.159.0"
},
"google-auth": {
"hashes": [
- "sha256:25df55f327ef021de8be50bad0dfd4a916ad0de96da86cd05661c9297723ad3f",
- "sha256:f4c64ed4e01e8e8b646ef34c018f8bf3338df0c8e37d8b3bba40e7f574a3278a"
+ "sha256:0054623abf1f9c83492c63d3f47e77f0a544caa3d40b2d98e099a611c2dd5d00",
+ "sha256:42664f18290a6be591be5329a96fe30184be1a1badb7292a7f686a9659de9ca0"
],
"markers": "python_version >= '3.7'",
- "version": "==2.35.0"
+ "version": "==2.37.0"
},
"google-auth-httplib2": {
"hashes": [
@@ -1015,11 +870,11 @@
},
"googleapis-common-protos": {
"hashes": [
- "sha256:2972e6c496f435b92590fd54045060867f3fe9be2c82ab148fc8885035479a63",
- "sha256:334a29d07cddc3aa01dee4988f9afd9b2916ee2ff49d6b757155dc0d197852c0"
+ "sha256:c3e7b33d15fdca5374cc0a7346dd92ffa847425cc4ea941d970f13680052ec8c",
+ "sha256:d7abcd75fabb2e0ec9f74466401f6c119a0b498e27370e9be4c94cb7e382b8ed"
],
"markers": "python_version >= '3.7'",
- "version": "==1.65.0"
+ "version": "==1.66.0"
},
"greenlet": {
"hashes": [
@@ -1097,16 +952,16 @@
"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')))))",
+ "markers": "python_version >= '3.7'",
"version": "==3.1.1"
},
"gspread": {
"hashes": [
- "sha256:cf03627f44e9e03a0a3de241d1748709db22af4fc8c11a13aa389d0bce6053fd",
- "sha256:d3b45ea70db9723ac04259bf2650881b0568b943fd04a7f161e88d97ab21bd29"
+ "sha256:b8eec27de7cadb338bb1b9f14a9be168372dee8965c0da32121816b5050ac1de",
+ "sha256:c34781c426031a243ad154952b16f21ac56a5af90687885fbee3d1fba5280dcd"
],
"markers": "python_version >= '3.8'",
- "version": "==6.1.3"
+ "version": "==6.1.4"
},
"h11": {
"hashes": [
@@ -1118,11 +973,11 @@
},
"httpcore": {
"hashes": [
- "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f",
- "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f"
+ "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c",
+ "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"
],
"markers": "python_version >= '3.8'",
- "version": "==1.0.6"
+ "version": "==1.0.7"
},
"httplib2": {
"hashes": [
@@ -1134,58 +989,26 @@
},
"httpx": {
"hashes": [
- "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0",
- "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"
+ "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc",
+ "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"
],
"markers": "python_version >= '3.8'",
- "version": "==0.27.2"
+ "version": "==0.28.1"
},
"idna": {
"hashes": [
- "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc",
- "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"
+ "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9",
+ "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"
],
- "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"
+ "markers": "python_version >= '3.6'",
+ "version": "==3.10"
},
"instaloader": {
"hashes": [
- "sha256:36774ea1076eeb236f8782d221e3737f71ddc023042f0b13761429ef137f1133"
+ "sha256:754425eb17af44ce4bb6056e4eacd044a518d13b5efc11b9d80eb229bb96c652"
],
- "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"
+ "markers": "python_version >= '3.9'",
+ "version": "==4.14"
},
"itsdangerous": {
"hashes": [
@@ -1195,46 +1018,14 @@
"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"
+ "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb",
+ "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
- "version": "==3.1.4"
+ "version": "==3.1.5"
},
"jmespath": {
"hashes": [
@@ -1252,14 +1043,6 @@
"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",
@@ -1268,21 +1051,14 @@
"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"
+ "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6",
+ "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c"
],
"index": "pypi",
- "markers": "python_version >= '3.5'",
- "version": "==0.7.2"
+ "markers": "python_version >= '3.5' and python_version < '4.0'",
+ "version": "==0.7.3"
},
"lxml": {
"hashes": [
@@ -1430,109 +1206,94 @@
},
"mako": {
"hashes": [
- "sha256:260f1dbc3a519453a9c856dedfe4beb4e50bd5a26d96386cb6c80856556bb91a",
- "sha256:48dbc20568c1d276a2698b36d968fa76161bf127194907ea6fc594fa81f943bc"
+ "sha256:42f48953c7eb91332040ff567eb7eea69b22e7a4affbc5ba8e845e8f730f6627",
+ "sha256:577b97e414580d3e088d47c2dbbe9594aa7a5146ed2875d4dfa9075af2dd3cc8"
],
"markers": "python_version >= '3.8'",
- "version": "==1.3.5"
+ "version": "==1.3.8"
},
"markdown-it-py": {
"hashes": [
- "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30",
- "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"
+ "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1",
+ "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"
],
- "markers": "python_version >= '3.7'",
- "version": "==2.2.0"
+ "markers": "python_version >= '3.8'",
+ "version": "==3.0.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"
+ "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4",
+ "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30",
+ "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0",
+ "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9",
+ "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396",
+ "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13",
+ "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028",
+ "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca",
+ "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557",
+ "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832",
+ "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0",
+ "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b",
+ "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579",
+ "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a",
+ "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c",
+ "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff",
+ "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c",
+ "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22",
+ "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094",
+ "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb",
+ "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e",
+ "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5",
+ "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a",
+ "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d",
+ "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a",
+ "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b",
+ "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8",
+ "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225",
+ "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c",
+ "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144",
+ "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f",
+ "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87",
+ "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d",
+ "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93",
+ "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf",
+ "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158",
+ "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84",
+ "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb",
+ "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48",
+ "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171",
+ "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c",
+ "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6",
+ "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd",
+ "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d",
+ "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1",
+ "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d",
+ "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca",
+ "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a",
+ "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29",
+ "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe",
+ "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798",
+ "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c",
+ "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8",
+ "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f",
+ "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f",
+ "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a",
+ "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178",
+ "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0",
+ "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79",
+ "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430",
+ "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"
],
- "markers": "python_version >= '3.7'",
- "version": "==2.1.5"
+ "markers": "python_version >= '3.9'",
+ "version": "==3.0.2"
},
"marshmallow": {
"hashes": [
- "sha256:82f20a2397834fe6d9611b241f2f7e7b680ed89c49f84728a1ad937be6b4bdf4",
- "sha256:98d8827a9f10c03d44ead298d2e99c6aea8197df18ccfad360dae7f89a50da2e"
+ "sha256:ec5d00d873ce473b7f2ffcb7104286a376c354cab0c2fa12f5573dab03e87210",
+ "sha256:f4debda3bb11153d81ac34b0d582bf23053055ee11e791b54b4b35493468040a"
],
"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"
+ "version": "==3.25.1"
},
"mdurl": {
"hashes": [
@@ -1573,93 +1334,103 @@
],
"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"
+ "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f",
+ "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056",
+ "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761",
+ "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3",
+ "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b",
+ "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6",
+ "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748",
+ "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966",
+ "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f",
+ "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1",
+ "sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6",
+ "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada",
+ "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305",
+ "sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2",
+ "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d",
+ "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a",
+ "sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef",
+ "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c",
+ "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb",
+ "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60",
+ "sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6",
+ "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4",
+ "sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478",
+ "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81",
+ "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7",
+ "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56",
+ "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3",
+ "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6",
+ "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30",
+ "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb",
+ "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506",
+ "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0",
+ "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925",
+ "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c",
+ "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6",
+ "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e",
+ "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95",
+ "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2",
+ "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133",
+ "sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2",
+ "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa",
+ "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3",
+ "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3",
+ "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436",
+ "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657",
+ "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581",
+ "sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492",
+ "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43",
+ "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2",
+ "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2",
+ "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926",
+ "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057",
+ "sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc",
+ "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80",
+ "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255",
+ "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1",
+ "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972",
+ "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53",
+ "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1",
+ "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423",
+ "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a",
+ "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160",
+ "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c",
+ "sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd",
+ "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa",
+ "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5",
+ "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b",
+ "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa",
+ "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef",
+ "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44",
+ "sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4",
+ "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156",
+ "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753",
+ "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28",
+ "sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d",
+ "sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a",
+ "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304",
+ "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008",
+ "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429",
+ "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72",
+ "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399",
+ "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3",
+ "sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392",
+ "sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167",
+ "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c",
+ "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774",
+ "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351",
+ "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76",
+ "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875",
+ "sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd",
+ "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28",
+ "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db"
],
- "markers": "python_version >= '3.7'",
- "version": "==6.0.4"
+ "markers": "python_version >= '3.8'",
+ "version": "==6.1.0"
},
"mutagen": {
"hashes": [
@@ -1669,39 +1440,6 @@
"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",
@@ -1710,93 +1448,66 @@
"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"
+ "sha256:02935e2c3c0c6cbe9c7955a8efa8908dd4221d7755644c59d1bba28b94fd334f",
+ "sha256:0349b025e15ea9d05c3d63f9657707a4e1d471128a3b1d876c095f328f8ff7f0",
+ "sha256:09d6a2032faf25e8d0cadde7fd6145118ac55d2740132c1d845f98721b5ebcfd",
+ "sha256:0bc61b307655d1a7f9f4b043628b9f2b721e80839914ede634e3d485913e1fb2",
+ "sha256:0eec19f8af947a61e968d5429f0bd92fec46d92b0008d0a6685b40d6adf8a4f4",
+ "sha256:106397dbbb1896f99e044efc90360d098b3335060375c26aa89c0d8a97c5f648",
+ "sha256:128c41c085cab8a85dc29e66ed88c05613dccf6bc28b3866cd16050a2f5448be",
+ "sha256:149d1113ac15005652e8d0d3f6fd599360e1a708a4f98e43c9c77834a28238cb",
+ "sha256:159ff6ee4c4a36a23fe01b7c3d07bd8c14cc433d9720f977fcd52c13c0098160",
+ "sha256:22ea3bb552ade325530e72a0c557cdf2dea8914d3a5e1fecf58fa5dbcc6f43cd",
+ "sha256:23ae9f0c2d889b7b2d88a3791f6c09e2ef827c2446f1c4a3e3e76328ee4afd9a",
+ "sha256:250c16b277e3b809ac20d1f590716597481061b514223c7badb7a0f9993c7f84",
+ "sha256:2ec6c689c61df613b783aeb21f945c4cbe6c51c28cb70aae8430577ab39f163e",
+ "sha256:2ffbb1acd69fdf8e89dd60ef6182ca90a743620957afb7066385a7bbe88dc748",
+ "sha256:3074634ea4d6df66be04f6728ee1d173cfded75d002c75fac79503a880bf3825",
+ "sha256:356ca982c188acbfa6af0d694284d8cf20e95b1c3d0aefa8929376fea9146f60",
+ "sha256:3fbe72d347fbc59f94124125e73fc4976a06927ebc503ec5afbfb35f193cd957",
+ "sha256:40c7ff5da22cd391944a28c6a9c638a5eef77fcf71d6e3a79e1d9d9e82752715",
+ "sha256:41184c416143defa34cc8eb9d070b0a5ba4f13a0fa96a709e20584638254b317",
+ "sha256:451e854cfae0febe723077bd0cf0a4302a5d84ff25f0bfece8f29206c7bed02e",
+ "sha256:4525b88c11906d5ab1b0ec1f290996c0020dd318af8b49acaa46f198b1ffc283",
+ "sha256:463247edcee4a5537841d5350bc87fe8e92d7dd0e8c71c995d2c6eecb8208278",
+ "sha256:4dbd80e453bd34bd003b16bd802fac70ad76bd463f81f0c518d1245b1c55e3d9",
+ "sha256:57b4012e04cc12b78590a334907e01b3a85efb2107df2b8733ff1ed05fce71de",
+ "sha256:5a8c863ceacae696aff37d1fd636121f1a512117652e5dfb86031c8d84836369",
+ "sha256:5acea83b801e98541619af398cc0109ff48016955cc0818f478ee9ef1c5c3dcb",
+ "sha256:642199e98af1bd2b6aeb8ecf726972d238c9877b0f6e8221ee5ab945ec8a2189",
+ "sha256:64bd6e1762cd7f0986a740fee4dff927b9ec2c5e4d9a28d056eb17d332158014",
+ "sha256:6d9fc9d812c81e6168b6d405bf00b8d6739a7f72ef22a9214c4241e0dc70b323",
+ "sha256:7079129b64cb78bdc8d611d1fd7e8002c0a2565da6a47c4df8062349fee90e3e",
+ "sha256:7dca87ca328f5ea7dafc907c5ec100d187911f94825f8700caac0b3f4c384b49",
+ "sha256:860fd59990c37c3ef913c3ae390b3929d005243acca1a86facb0773e2d8d9e50",
+ "sha256:8e6da5cffbbe571f93588f562ed130ea63ee206d12851b60819512dd3e1ba50d",
+ "sha256:8ec0636d3f7d68520afc6ac2dc4b8341ddb725039de042faf0e311599f54eb37",
+ "sha256:9491100aba630910489c1d0158034e1c9a6546f0b1340f716d522dc103788e39",
+ "sha256:97b974d3ba0fb4612b77ed35d7627490e8e3dff56ab41454d9e8b23448940576",
+ "sha256:995f9e8181723852ca458e22de5d9b7d3ba4da3f11cc1cb113f093b271d7965a",
+ "sha256:9dd47ff0cb2a656ad69c38da850df3454da88ee9a6fde0ba79acceee0e79daba",
+ "sha256:9fad446ad0bc886855ddf5909cbf8cb5d0faa637aaa6277fb4b19ade134ab3c7",
+ "sha256:a972cec723e0563aa0823ee2ab1df0cb196ed0778f173b381c871a03719d4826",
+ "sha256:ac9bea18d6d58a995fac1b2cb4488e17eceeac413af014b1dd26170b766d8467",
+ "sha256:b0531f0b0e07643eb089df4c509d30d72c9ef40defa53e41363eca8a8cc61495",
+ "sha256:b208cfd4f5fe34e1535c08983a1a6803fdbc7a1e86cf13dd0c61de0b51a0aadc",
+ "sha256:b3482cb7b3325faa5f6bc179649406058253d91ceda359c104dac0ad320e1391",
+ "sha256:b6fb9c32a91ec32a689ec6410def76443e3c750e7cfc3fb2206b985ffb2b85f0",
+ "sha256:b78ea78450fd96a498f50ee096f69c75379af5138f7881a51355ab0e11286c97",
+ "sha256:bd249bc894af67cbd8bad2c22e7cbcd46cf87ddfca1f1289d1e7e54868cc785c",
+ "sha256:c7d1fd447e33ee20c1f33f2c8e6634211124a9aabde3c617687d8b739aa69eac",
+ "sha256:d0bbe7dd86dca64854f4b6ce2ea5c60b51e36dfd597300057cf473d3615f2369",
+ "sha256:d6d6a0910c3b4368d89dde073e630882cdb266755565155bc33520283b2d9df8",
+ "sha256:da1eeb460ecce8d5b8608826595c777728cdf28ce7b5a5a8c8ac8d949beadcf2",
+ "sha256:e0c8854b09bc4de7b041148d8550d3bd712b5c21ff6a8ed308085f190235d7ff",
+ "sha256:e0d4142eb40ca6f94539e4db929410f2a46052a0fe7a2c1c59f6179c39938d2a",
+ "sha256:e9e82dcb3f2ebbc8cb5ce1102d5f1c5ed236bf8a11730fb45ba82e2841ec21df",
+ "sha256:ed6906f61834d687738d25988ae117683705636936cc605be0bb208b23df4d8f"
],
"markers": "python_version >= '3.10'",
- "version": "==2.1.2"
+ "version": "==2.2.2"
},
"oauth2client": {
"hashes": [
@@ -1814,11 +1525,8 @@
"version": "==3.2.2"
},
"oscrypto": {
- "hashes": [
- "sha256:2b2f1d2d42ec152ca90ccb5682f3e051fb55986e1b170ebde472b133713e7085",
- "sha256:6f5fef59cb5b3708321db7cca56aed8ad7e662853351e7991fcf60ec606d47a4"
- ],
- "version": "==1.3.0"
+ "git": "https://github.com/wbond/oscrypto.git",
+ "ref": "d5f3437ed24257895ae1edd9e503cfb352e635a8"
},
"outcome": {
"hashes": [
@@ -1830,19 +1538,11 @@
},
"packaging": {
"hashes": [
- "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002",
- "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"
+ "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759",
+ "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"
],
"markers": "python_version >= '3.8'",
- "version": "==24.1"
- },
- "pathspec": {
- "hashes": [
- "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08",
- "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==0.12.1"
+ "version": "==24.2"
},
"pdqhash": {
"hashes": [
@@ -1862,158 +1562,218 @@
},
"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"
+ "sha256:015c6e863faa4779251436db398ae75051469f7c903b043a48f078e437656f83",
+ "sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96",
+ "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65",
+ "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a",
+ "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352",
+ "sha256:3362c6ca227e65c54bf71a5f88b3d4565ff1bcbc63ae72c34b07bbb1cc59a43f",
+ "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20",
+ "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c",
+ "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114",
+ "sha256:3a5fe20a7b66e8135d7fd617b13272626a28278d0e578c98720d9ba4b2439d49",
+ "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91",
+ "sha256:4637b88343166249fe8aa94e7c4a62a180c4b3898283bb5d3d2fd5fe10d8e4e0",
+ "sha256:4db853948ce4e718f2fc775b75c37ba2efb6aaea41a1a5fc57f0af59eee774b2",
+ "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5",
+ "sha256:54251ef02a2309b5eec99d151ebf5c9904b77976c8abdcbce7891ed22df53884",
+ "sha256:54ce1c9a16a9561b6d6d8cb30089ab1e5eb66918cb47d457bd996ef34182922e",
+ "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c",
+ "sha256:5bb94705aea800051a743aa4874bb1397d4695fb0583ba5e425ee0328757f196",
+ "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756",
+ "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861",
+ "sha256:73ddde795ee9b06257dac5ad42fcb07f3b9b813f8c1f7f870f402f4dc54b5269",
+ "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1",
+ "sha256:7d33d2fae0e8b170b6a6c57400e077412240f6f5bb2a342cf1ee512a787942bb",
+ "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a",
+ "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081",
+ "sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1",
+ "sha256:89dbdb3e6e9594d512780a5a1c42801879628b38e3efc7038094430844e271d8",
+ "sha256:8c730dc3a83e5ac137fbc92dfcfe1511ce3b2b5d7578315b63dbbb76f7f51d90",
+ "sha256:8e275ee4cb11c262bd108ab2081f750db2a1c0b8c12c1897f27b160c8bd57bbc",
+ "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5",
+ "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1",
+ "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3",
+ "sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35",
+ "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f",
+ "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c",
+ "sha256:a07dba04c5e22824816b2615ad7a7484432d7f540e6fa86af60d2de57b0fcee2",
+ "sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2",
+ "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf",
+ "sha256:a76da0a31da6fcae4210aa94fd779c65c75786bc9af06289cd1c184451ef7a65",
+ "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b",
+ "sha256:a8d65b38173085f24bc07f8b6c505cbb7418009fa1a1fcb111b1f4961814a442",
+ "sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2",
+ "sha256:ab8a209b8485d3db694fa97a896d96dd6533d63c22829043fd9de627060beade",
+ "sha256:abc56501c3fd148d60659aae0af6ddc149660469082859fa7b066a298bde9482",
+ "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe",
+ "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc",
+ "sha256:b20be51b37a75cc54c2c55def3fa2c65bb94ba859dde241cd0a4fd302de5ae0a",
+ "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec",
+ "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3",
+ "sha256:b6123aa4a59d75f06e9dd3dac5bf8bc9aa383121bb3dd9a7a612e05eabc9961a",
+ "sha256:bd165131fd51697e22421d0e467997ad31621b74bfc0b75956608cb2906dda07",
+ "sha256:bf902d7413c82a1bfa08b06a070876132a5ae6b2388e2712aab3a7cbc02205c6",
+ "sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f",
+ "sha256:c1eec9d950b6fe688edee07138993e54ee4ae634c51443cfb7c1e7613322718e",
+ "sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192",
+ "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0",
+ "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6",
+ "sha256:d3d8da4a631471dfaf94c10c85f5277b1f8e42ac42bade1ac67da4b4a7359b73",
+ "sha256:d44ff19eea13ae4acdaaab0179fa68c0c6f2f45d66a4d8ec1eda7d6cecbcc15f",
+ "sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6",
+ "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547",
+ "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9",
+ "sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457",
+ "sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8",
+ "sha256:e267b0ed063341f3e60acd25c05200df4193e15a4a5807075cd71225a2386e26",
+ "sha256:e5449ca63da169a2e6068dd0e2fcc8d91f9558aba89ff6d02121ca8ab11e79e5",
+ "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab",
+ "sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070",
+ "sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71",
+ "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9",
+ "sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761"
],
"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"
+ "version": "==11.1.0"
},
"prometheus-client": {
"hashes": [
- "sha256:4fa6b4dd0ac16d58bb587c04b1caae65b8c5043e85f778f42f5f632f6af2e166",
- "sha256:96c83c606b71ff2b0a433c98889d275f51ffec6c5e267de37c7a2b5c9aa9233e"
+ "sha256:252505a722ac04b0456be05c05f75f45d760c2911ffc45f2a06bcaed9f3ae3fb",
+ "sha256:594b45c410d6f4f8888940fe80b5cc2521b305a1fafe1c58609ef715a001f301"
],
"markers": "python_version >= '3.8'",
- "version": "==0.21.0"
+ "version": "==0.21.1"
},
"prometheus-fastapi-instrumentator": {
"hashes": [
- "sha256:5ba67c9212719f244ad7942d75ded80693b26331ee5dfc1e7571e4794a9ccbed",
- "sha256:96030c43c776ee938a3dae58485ec24caed7e05bfc60fe067161e0d5b5757052"
+ "sha256:8a4d8fb13dbe19d2882ac6af9ce236e4e1f98dc48e3fa44fe88d8e23ac3c953f",
+ "sha256:975e39992acb7a112758ff13ba95317e6c54d1bbf605f9156f31ac9f2800c32d"
],
"index": "pypi",
- "markers": "python_full_version >= '3.8.1' and python_full_version < '4.0.0'",
- "version": "==7.0.0"
+ "markers": "python_version >= '3.8'",
+ "version": "==7.0.2"
},
"prompt-toolkit": {
"hashes": [
- "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90",
- "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e"
+ "sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab",
+ "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198"
],
- "markers": "python_full_version >= '3.7.0'",
- "version": "==3.0.48"
+ "markers": "python_full_version >= '3.8.0'",
+ "version": "==3.0.50"
+ },
+ "propcache": {
+ "hashes": [
+ "sha256:03ff9d3f665769b2a85e6157ac8b439644f2d7fd17615a82fa55739bc97863f4",
+ "sha256:049324ee97bb67285b49632132db351b41e77833678432be52bdd0289c0e05e4",
+ "sha256:081a430aa8d5e8876c6909b67bd2d937bfd531b0382d3fdedb82612c618bc41a",
+ "sha256:0f022d381747f0dfe27e99d928e31bc51a18b65bb9e481ae0af1380a6725dd1f",
+ "sha256:12d1083f001ace206fe34b6bdc2cb94be66d57a850866f0b908972f90996b3e9",
+ "sha256:14d86fe14b7e04fa306e0c43cdbeebe6b2c2156a0c9ce56b815faacc193e320d",
+ "sha256:160291c60081f23ee43d44b08a7e5fb76681221a8e10b3139618c5a9a291b84e",
+ "sha256:1672137af7c46662a1c2be1e8dc78cb6d224319aaa40271c9257d886be4363a6",
+ "sha256:19a0f89a7bb9d8048d9c4370c9c543c396e894c76be5525f5e1ad287f1750ddf",
+ "sha256:1ac2f5fe02fa75f56e1ad473f1175e11f475606ec9bd0be2e78e4734ad575034",
+ "sha256:1cd9a1d071158de1cc1c71a26014dcdfa7dd3d5f4f88c298c7f90ad6f27bb46d",
+ "sha256:1ffc3cca89bb438fb9c95c13fc874012f7b9466b89328c3c8b1aa93cdcfadd16",
+ "sha256:297878dc9d0a334358f9b608b56d02e72899f3b8499fc6044133f0d319e2ec30",
+ "sha256:2d3af2e79991102678f53e0dbf4c35de99b6b8b58f29a27ca0325816364caaba",
+ "sha256:30b43e74f1359353341a7adb783c8f1b1c676367b011709f466f42fda2045e95",
+ "sha256:3156628250f46a0895f1f36e1d4fbe062a1af8718ec3ebeb746f1d23f0c5dc4d",
+ "sha256:31f5af773530fd3c658b32b6bdc2d0838543de70eb9a2156c03e410f7b0d3aae",
+ "sha256:3935bfa5fede35fb202c4b569bb9c042f337ca4ff7bd540a0aa5e37131659348",
+ "sha256:39d51fbe4285d5db5d92a929e3e21536ea3dd43732c5b177c7ef03f918dff9f2",
+ "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64",
+ "sha256:4160d9283bd382fa6c0c2b5e017acc95bc183570cd70968b9202ad6d8fc48dce",
+ "sha256:4a571d97dbe66ef38e472703067021b1467025ec85707d57e78711c085984e54",
+ "sha256:4e6281aedfca15301c41f74d7005e6e3f4ca143584ba696ac69df4f02f40d629",
+ "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54",
+ "sha256:556fc6c10989f19a179e4321e5d678db8eb2924131e64652a51fe83e4c3db0e1",
+ "sha256:574faa3b79e8ebac7cb1d7930f51184ba1ccf69adfdec53a12f319a06030a68b",
+ "sha256:58791550b27d5488b1bb52bc96328456095d96206a250d28d874fafe11b3dfaf",
+ "sha256:5b750a8e5a1262434fb1517ddf64b5de58327f1adc3524a5e44c2ca43305eb0b",
+ "sha256:5d97151bc92d2b2578ff7ce779cdb9174337390a535953cbb9452fb65164c587",
+ "sha256:5eee736daafa7af6d0a2dc15cc75e05c64f37fc37bafef2e00d77c14171c2097",
+ "sha256:6445804cf4ec763dc70de65a3b0d9954e868609e83850a47ca4f0cb64bd79fea",
+ "sha256:647894f5ae99c4cf6bb82a1bb3a796f6e06af3caa3d32e26d2350d0e3e3faf24",
+ "sha256:66d4cfda1d8ed687daa4bc0274fcfd5267873db9a5bc0418c2da19273040eeb7",
+ "sha256:6a9a8c34fb7bb609419a211e59da8887eeca40d300b5ea8e56af98f6fbbb1541",
+ "sha256:6b3f39a85d671436ee3d12c017f8fdea38509e4f25b28eb25877293c98c243f6",
+ "sha256:6b6fb63ae352e13748289f04f37868099e69dba4c2b3e271c46061e82c745634",
+ "sha256:70693319e0b8fd35dd863e3e29513875eb15c51945bf32519ef52927ca883bc3",
+ "sha256:781e65134efaf88feb447e8c97a51772aa75e48b794352f94cb7ea717dedda0d",
+ "sha256:819ce3b883b7576ca28da3861c7e1a88afd08cc8c96908e08a3f4dd64a228034",
+ "sha256:857112b22acd417c40fa4595db2fe28ab900c8c5fe4670c7989b1c0230955465",
+ "sha256:887d9b0a65404929641a9fabb6452b07fe4572b269d901d622d8a34a4e9043b2",
+ "sha256:8b3489ff1ed1e8315674d0775dc7d2195fb13ca17b3808721b54dbe9fd020faf",
+ "sha256:92fc4500fcb33899b05ba73276dfb684a20d31caa567b7cb5252d48f896a91b1",
+ "sha256:9403db39be1393618dd80c746cb22ccda168efce239c73af13c3763ef56ffc04",
+ "sha256:98110aa363f1bb4c073e8dcfaefd3a5cea0f0834c2aab23dda657e4dab2f53b5",
+ "sha256:999779addc413181912e984b942fbcc951be1f5b3663cd80b2687758f434c583",
+ "sha256:9caac6b54914bdf41bcc91e7eb9147d331d29235a7c967c150ef5df6464fd1bb",
+ "sha256:a7a078f5d37bee6690959c813977da5291b24286e7b962e62a94cec31aa5188b",
+ "sha256:a7e65eb5c003a303b94aa2c3852ef130230ec79e349632d030e9571b87c4698c",
+ "sha256:a96dc1fa45bd8c407a0af03b2d5218392729e1822b0c32e62c5bf7eeb5fb3958",
+ "sha256:aca405706e0b0a44cc6bfd41fbe89919a6a56999157f6de7e182a990c36e37bc",
+ "sha256:accb6150ce61c9c4b7738d45550806aa2b71c7668c6942f17b0ac182b6142fd4",
+ "sha256:ad1af54a62ffe39cf34db1aa6ed1a1873bd548f6401db39d8e7cd060b9211f82",
+ "sha256:ae1aa1cd222c6d205853b3013c69cd04515f9d6ab6de4b0603e2e1c33221303e",
+ "sha256:b2d0a12018b04f4cb820781ec0dffb5f7c7c1d2a5cd22bff7fb055a2cb19ebce",
+ "sha256:b480c6a4e1138e1aa137c0079b9b6305ec6dcc1098a8ca5196283e8a49df95a9",
+ "sha256:b74c261802d3d2b85c9df2dfb2fa81b6f90deeef63c2db9f0e029a3cac50b518",
+ "sha256:ba278acf14471d36316159c94a802933d10b6a1e117b8554fe0d0d9b75c9d536",
+ "sha256:bb6178c241278d5fe853b3de743087be7f5f4c6f7d6d22a3b524d323eecec505",
+ "sha256:bf72af5e0fb40e9babf594308911436c8efde3cb5e75b6f206c34ad18be5c052",
+ "sha256:bfd3223c15bebe26518d58ccf9a39b93948d3dcb3e57a20480dfdd315356baff",
+ "sha256:c214999039d4f2a5b2073ac506bba279945233da8c786e490d411dfc30f855c1",
+ "sha256:c2f992c07c0fca81655066705beae35fc95a2fa7366467366db627d9f2ee097f",
+ "sha256:cba4cfa1052819d16699e1d55d18c92b6e094d4517c41dd231a8b9f87b6fa681",
+ "sha256:cea7daf9fc7ae6687cf1e2c049752f19f146fdc37c2cc376e7d0032cf4f25347",
+ "sha256:cf6c4150f8c0e32d241436526f3c3f9cbd34429492abddbada2ffcff506c51af",
+ "sha256:d09c333d36c1409d56a9d29b3a1b800a42c76a57a5a8907eacdbce3f18768246",
+ "sha256:d27b84d5880f6d8aa9ae3edb253c59d9f6642ffbb2c889b78b60361eed449787",
+ "sha256:d2ccec9ac47cf4e04897619c0e0c1a48c54a71bdf045117d3a26f80d38ab1fb0",
+ "sha256:d71264a80f3fcf512eb4f18f59423fe82d6e346ee97b90625f283df56aee103f",
+ "sha256:d93f3307ad32a27bda2e88ec81134b823c240aa3abb55821a8da553eed8d9439",
+ "sha256:d9631c5e8b5b3a0fda99cb0d29c18133bca1e18aea9effe55adb3da1adef80d3",
+ "sha256:ddfab44e4489bd79bda09d84c430677fc7f0a4939a73d2bba3073036f487a0a6",
+ "sha256:e7048abd75fe40712005bcfc06bb44b9dfcd8e101dda2ecf2f5aa46115ad07ca",
+ "sha256:e73091191e4280403bde6c9a52a6999d69cdfde498f1fdf629105247599b57ec",
+ "sha256:e800776a79a5aabdb17dcc2346a7d66d0777e942e4cd251defeb084762ecd17d",
+ "sha256:edc9fc7051e3350643ad929df55c451899bb9ae6d24998a949d2e4c87fb596d3",
+ "sha256:f089118d584e859c62b3da0892b88a83d611c2033ac410e929cb6754eec0ed16",
+ "sha256:f174bbd484294ed9fdf09437f889f95807e5f229d5d93588d34e92106fbf6717",
+ "sha256:f508b0491767bb1f2b87fdfacaba5f7eddc2f867740ec69ece6d1946d29029a6",
+ "sha256:f7a31fc1e1bd362874863fdeed71aed92d348f5336fd84f2197ba40c59f061bd",
+ "sha256:f9479aa06a793c5aeba49ce5c5692ffb51fcd9a7016e017d555d5e2b0045d212"
+ ],
+ "markers": "python_version >= '3.9'",
+ "version": "==0.2.1"
},
"proto-plus": {
"hashes": [
- "sha256:30b72a5ecafe4406b0d339db35b56c4059064e69227b8c3bda7462397f966445",
- "sha256:402576830425e5f6ce4c2a6702400ac79897dab0b4343821aa5188b0fab81a12"
+ "sha256:c91fc4a65074ade8e458e95ef8bac34d4008daa7cce4a12d6707066fca648961",
+ "sha256:fbb17f57f7bd05a68b7707e745e26528b0b3c34e378db91eef93912c54982d91"
],
"markers": "python_version >= '3.7'",
- "version": "==1.24.0"
+ "version": "==1.25.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"
+ "sha256:0a18ed4a24198528f2333802eb075e59dea9d679ab7a6c5efb017a59004d849f",
+ "sha256:0eb32bfa5219fc8d4111803e9a690658aa2e6366384fd0851064b963b6d1f2a7",
+ "sha256:3ea51771449e1035f26069c4c7fd51fba990d07bc55ba80701c78f886bf9c888",
+ "sha256:5da0f41edaf117bde316404bad1a486cb4ededf8e4a54891296f648e8e076620",
+ "sha256:6ce8cc3389a20693bfde6c6562e03474c40851b44975c9b2bf6df7d8c4f864da",
+ "sha256:84a57163a0ccef3f96e4b6a20516cedcf5bb3a95a657131c5c3ac62200d23252",
+ "sha256:a4fa6f80816a9a0678429e84973f2f98cbc218cca434abe8db2ad0bffc98503a",
+ "sha256:a8434404bbf139aa9e1300dbf989667a83d42ddda9153d8ab76e0d5dcaca484e",
+ "sha256:b89c115d877892a512f79a8114564fb435943b59067615894c3b13cd3e1fa107",
+ "sha256:c027e08a08be10b67c06bf2370b99c811c466398c357e615ca88c91c07f0910f",
+ "sha256:daaf63f70f25e8689c072cfad4334ca0ac1d1e05a92fc15c54eb9cf23c3efd84"
],
"markers": "python_version >= '3.8'",
- "version": "==5.28.2"
+ "version": "==5.29.3"
},
"psutil": {
"hashes": [
@@ -2037,14 +1797,6 @@
"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"
@@ -2067,14 +1819,6 @@
"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",
@@ -2085,185 +1829,188 @@
},
"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"
+ "sha256:0df2608682db8279a9ebbaf05a72f62a321433522ed0e499bc486a6889b96bf3",
+ "sha256:103c133d6cd832ae7266feb0a65b69e3a5e4dbbd6f3a3ae3211a557fd653f516",
+ "sha256:1233443f19d278c72c4daae749872a4af3787a813e05c3561c73ab0c153c7b0f",
+ "sha256:222d0bd05381dd25c32dd6065c071ebf084212ab79bab4599ba9e6a3e0009e6c",
+ "sha256:27e84eeff24250ffec32722334749ac2a57a5fd60332cd6a0680090e7c42877e",
+ "sha256:34325b84c8b380675fd2320d0649cdcbc9cf1e0d1526edbe8fce43ed858cdc7e",
+ "sha256:365aa5a66d52fd1f9e0530ea97f392c48c409c2f01ff8b9a39c73ed6f527d36c",
+ "sha256:3efddfc50ac0ca143364042324046800c126a1d63816d532f2e19e6f2d8c0c31",
+ "sha256:46eb1f0c8d309da63a2064c28de54e5e614ad17b7e2f88df0faef58ce192fc7b",
+ "sha256:5241bdb53bcf32a9568770a6584774b1b8109342bd033398e4ff2da052123832",
+ "sha256:52e23a0a6e61691134aa8c8beba89de420602541afaae70f66e16060fdcd677e",
+ "sha256:56435c7124dd0ce0c8bdd99c52e5d183a0ca7fdcd06c5d5509423843f487dd0b",
+ "sha256:5823d03e904ea3e53aebd6799d6b8ec63b7675b5d2f4a4bd5e3adcb512d03b37",
+ "sha256:65d275e3f866cf6fe891411be9c1454fb58809ccc5de6d3770654c47197acd65",
+ "sha256:770d630a5c46605ec83393feaa73a9635a60e55b112e1fb0c3cea84c2897aa0a",
+ "sha256:77ac2ea80bcb4b4e1c6a596734c775a1615d23e31794967416afc14852a639d3",
+ "sha256:7a1058e6dfe827f4209c5cae466e67610bcd0d66f2f037465daa2a29d92d952b",
+ "sha256:8a9d8342cf22b74a746e3c6c9453cb0cfbb55943410e3a2619bd9164b48dc9d9",
+ "sha256:8ef436cdeea794015263853311f84c1ff0341b98fc7908e8a70595a68cefd971",
+ "sha256:9aa0cf13a1a1128b3e964dc667e5fe5c6235f7d7cfb0277213f0e2a783837cc2",
+ "sha256:9ba09a5b407cbb3bcb325221e346a140605714b5e880741dc9a1e9ecf1688d42",
+ "sha256:a192fb46c95489beba9c3f002ed7d93979423d1b2a53eab8771dbb1339eb3ddd",
+ "sha256:a3d77919e6ff56d89aada1bd009b727b874d464cb0e2e3f00a49f7d2e709d76e",
+ "sha256:b0e9765f93fe4890f39875e6c90c96cb341767833cfa767f41b490b506fa9ec0",
+ "sha256:bbb07f88e277162b8bfca7134b34f18b400d84eac7375ce73117f865e3c80d4c",
+ "sha256:c07e64867a54f7e93186a55bec08a18b7302e7bee1b02fd84c6089ec215e723a",
+ "sha256:cc7e111e66c274b0df5f4efa679eb31e23c7545d702333dfd2df10ab02c2a2ce",
+ "sha256:da76ebf6650323eae7236b54b1b1f0e57c16483be6e3c1ebf901d4ada47563b6",
+ "sha256:dbeb84a399373df84a69e0919c1d733b89e049752426041deeb30d68e9867822",
+ "sha256:e859e53d983b7fe18cb8f1b0e29d991a5c93be2c8dd25db7db1fe3bd3617f6f9",
+ "sha256:ef046b2e6c425647971b51424f0f88d8a2e0a2a63d3531817968c42078895c00",
+ "sha256:feaecdce4e5c0045e7a287de0c4351284391fe170729aa9182f6bd967631b3a8"
],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
- "version": "==3.20.0"
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
+ "version": "==3.21.0"
},
"pydantic": {
"hashes": [
- "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f",
- "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"
+ "sha256:278b38dbbaec562011d659ee05f63346951b3a248a6f3642e1bc68894ea2b4ff",
+ "sha256:4dd4e322dbe55472cb7ca7e73f4b63574eecccf2835ffa2af9021ce113c83c53"
],
"markers": "python_version >= '3.8'",
- "version": "==2.9.2"
+ "version": "==2.10.5"
},
"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"
+ "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278",
+ "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50",
+ "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9",
+ "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f",
+ "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6",
+ "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc",
+ "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54",
+ "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630",
+ "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9",
+ "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236",
+ "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7",
+ "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee",
+ "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b",
+ "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048",
+ "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc",
+ "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130",
+ "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4",
+ "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd",
+ "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4",
+ "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7",
+ "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7",
+ "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4",
+ "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e",
+ "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa",
+ "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6",
+ "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962",
+ "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b",
+ "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f",
+ "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474",
+ "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5",
+ "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459",
+ "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf",
+ "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a",
+ "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c",
+ "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76",
+ "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362",
+ "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4",
+ "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934",
+ "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320",
+ "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118",
+ "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96",
+ "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306",
+ "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046",
+ "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3",
+ "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2",
+ "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af",
+ "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9",
+ "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67",
+ "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a",
+ "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27",
+ "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35",
+ "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b",
+ "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151",
+ "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b",
+ "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154",
+ "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133",
+ "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef",
+ "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145",
+ "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15",
+ "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4",
+ "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc",
+ "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee",
+ "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c",
+ "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0",
+ "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5",
+ "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57",
+ "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b",
+ "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8",
+ "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1",
+ "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da",
+ "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e",
+ "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc",
+ "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993",
+ "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656",
+ "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4",
+ "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c",
+ "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb",
+ "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d",
+ "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9",
+ "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e",
+ "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1",
+ "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc",
+ "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a",
+ "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9",
+ "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506",
+ "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b",
+ "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1",
+ "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d",
+ "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99",
+ "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3",
+ "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31",
+ "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c",
+ "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39",
+ "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a",
+ "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308",
+ "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2",
+ "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228",
+ "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b",
+ "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9",
+ "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"
],
"markers": "python_version >= '3.8'",
- "version": "==2.23.4"
+ "version": "==2.27.2"
},
"pydantic-settings": {
"hashes": [
- "sha256:44a1804abffac9e6a30372bb45f6cafab945ef5af25e66b1c634c01dd39e0188",
- "sha256:4a819166f119b74d7f8c765196b165f95cc7487ce58ea27dec8a5a26be0970e0"
+ "sha256:10c9caad35e64bfb3c2fbf70a078c0e25cc92499782e5200747f942a065dec93",
+ "sha256:590be9e6e24d06db33a4262829edef682500ef008565a969c73d39d5f8bfb3fd"
],
"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"
+ "version": "==2.7.1"
},
"pygments": {
"hashes": [
- "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199",
- "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"
+ "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f",
+ "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"
],
"markers": "python_version >= '3.8'",
- "version": "==2.18.0"
+ "version": "==2.19.1"
},
"pyopenssl": {
"hashes": [
- "sha256:4247f0dbe3748d560dcbb2ff3ea01af0f9a1a001ef5f7c4c647956ed8cbf0e95",
- "sha256:967d5719b12b243588573f39b0c677637145c7a1ffedcd495a487e58177fbb8d"
+ "sha256:6756834481d9ed5470f4a9393455154bc92fe7a64b7bc6ee2c804e78c52099b2",
+ "sha256:6b2cba5cc46e822750ec3e5a81ee12819850b11303630d575e98108a079c2b12"
],
"markers": "python_version >= '3.7'",
- "version": "==24.2.1"
+ "version": "==23.3.0"
},
"pyparsing": {
"hashes": [
- "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb",
- "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"
+ "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1",
+ "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a"
],
- "markers": "python_full_version >= '3.6.8'",
- "version": "==3.0.9"
+ "markers": "python_version >= '3.9'",
+ "version": "==3.2.1"
},
"pysocks": {
"hashes": [
@@ -2271,46 +2018,23 @@
"sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5",
"sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.7.1"
},
"pysubs2": {
"hashes": [
- "sha256:b0130f373390736754531be4e68a0fa521e825fa15cc8ff506e4f8ca2c17459a",
- "sha256:de438c868d2c656781c4a78f220ec3a6fd6d52be49266c81fe912d2527002d44"
+ "sha256:05716f5039a9ebe32cd4d7673f923cf36204f3a3e99987f823ab83610b7035a0",
+ "sha256:3397bb58a4a15b1325ba2ae3fd4d7c214e2c0ddb9f33190d6280d783bb433b20"
],
- "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"
+ "markers": "python_version >= '3.9'",
+ "version": "==1.8.0"
},
"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'",
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
"version": "==2.9.0.post0"
},
"python-dotenv": {
@@ -2332,85 +2056,77 @@
},
"python-twitter-v2": {
"hashes": [
- "sha256:45374228d02c2bed150ff59ca93feafeb9cbe3b3cd3319223906ac52caf98a46",
- "sha256:4fc88c5d3fc593ada134b16604ebf7a896379fee7694b5956cc460af3435f247"
+ "sha256:c032c0b90e824ccd605620eb67cc59601f48a100fe7424090aaf37f243239e82",
+ "sha256:dcd41ebfbc1b0ca6a1212870b0ff68b85e2111655e09027a0e42829fe3a63460"
],
"markers": "python_version >= '3.7' and python_version < '4.0'",
- "version": "==0.9.1"
+ "version": "==0.9.2"
},
"pytz": {
"hashes": [
- "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7",
- "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"
+ "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a",
+ "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"
],
- "version": "==2022.1"
+ "version": "==2024.2"
},
"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"
+ "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff",
+ "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48",
+ "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086",
+ "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e",
+ "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133",
+ "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5",
+ "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484",
+ "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee",
+ "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5",
+ "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68",
+ "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a",
+ "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf",
+ "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99",
+ "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8",
+ "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85",
+ "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19",
+ "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc",
+ "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a",
+ "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1",
+ "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317",
+ "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c",
+ "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631",
+ "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d",
+ "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652",
+ "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5",
+ "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e",
+ "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b",
+ "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8",
+ "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476",
+ "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706",
+ "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563",
+ "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237",
+ "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b",
+ "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083",
+ "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180",
+ "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425",
+ "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e",
+ "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f",
+ "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725",
+ "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183",
+ "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab",
+ "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774",
+ "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725",
+ "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e",
+ "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5",
+ "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d",
+ "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290",
+ "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44",
+ "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed",
+ "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4",
+ "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba",
+ "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12",
+ "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"
],
"markers": "python_version >= '3.8'",
- "version": "==43.0"
+ "version": "==6.0.2"
},
"redis": {
"hashes": [
@@ -2423,103 +2139,103 @@
},
"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"
+ "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c",
+ "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60",
+ "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d",
+ "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d",
+ "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67",
+ "sha256:072623554418a9911446278f16ecb398fb3b540147a7828c06e2011fa531e773",
+ "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0",
+ "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef",
+ "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad",
+ "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe",
+ "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3",
+ "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114",
+ "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4",
+ "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39",
+ "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e",
+ "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3",
+ "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7",
+ "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d",
+ "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e",
+ "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a",
+ "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7",
+ "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f",
+ "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0",
+ "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54",
+ "sha256:3a51ccc315653ba012774efca4f23d1d2a8a8f278a6072e29c7147eee7da446b",
+ "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c",
+ "sha256:40291b1b89ca6ad8d3f2b82782cc33807f1406cf68c8d440861da6304d8ffbbd",
+ "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57",
+ "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34",
+ "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d",
+ "sha256:50153825ee016b91549962f970d6a4442fa106832e14c918acd1c8e479916c4f",
+ "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b",
+ "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519",
+ "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4",
+ "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a",
+ "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638",
+ "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b",
+ "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839",
+ "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07",
+ "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf",
+ "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff",
+ "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0",
+ "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f",
+ "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95",
+ "sha256:6f44ec28b1f858c98d3036ad5d7d0bfc568bdd7a74f9c24e25f41ef1ebfd81a4",
+ "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e",
+ "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13",
+ "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519",
+ "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2",
+ "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008",
+ "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9",
+ "sha256:89d75e7293d2b3e674db7d4d9b1bee7f8f3d1609428e293771d1a962617150cc",
+ "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48",
+ "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20",
+ "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89",
+ "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e",
+ "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf",
+ "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b",
+ "sha256:a36fdf2af13c2b14738f6e973aba563623cb77d753bbbd8d414d18bfaa3105dd",
+ "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84",
+ "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29",
+ "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b",
+ "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3",
+ "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45",
+ "sha256:ad182d02e40de7459b73155deb8996bbd8e96852267879396fb274e8700190e3",
+ "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983",
+ "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e",
+ "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7",
+ "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4",
+ "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e",
+ "sha256:ba9b72e5643641b7d41fa1f6d5abda2c9a263ae835b917348fc3c928182ad467",
+ "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577",
+ "sha256:bb8f74f2f10dbf13a0be8de623ba4f9491faf58c24064f32b65679b021ed0001",
+ "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0",
+ "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55",
+ "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9",
+ "sha256:cdf58d0e516ee426a48f7b2c03a332a4114420716d55769ff7108c37a09951bf",
+ "sha256:d1cee317bfc014c2419a76bcc87f071405e3966da434e03e13beb45f8aced1a6",
+ "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e",
+ "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde",
+ "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62",
+ "sha256:df951c5f4a1b1910f1a99ff42c473ff60f8225baa1cdd3539fe2819d9543e9df",
+ "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51",
+ "sha256:ea1bfda2f7162605f6e8178223576856b3d791109f15ea99a9f95c16a7636fb5",
+ "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86",
+ "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2",
+ "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2",
+ "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0",
+ "sha256:f654882311409afb1d780b940234208a252322c24a93b442ca714d119e68086c",
+ "sha256:f65557897fc977a44ab205ea871b690adaef6b9da6afda4790a2484b04293a5f",
+ "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6",
+ "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2",
+ "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9",
+ "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"
],
"markers": "python_version >= '3.8'",
- "version": "==2024.9.11"
+ "version": "==2024.11.6"
},
"requests": {
"extras": [
@@ -2556,21 +2272,13 @@
],
"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"
+ "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098",
+ "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"
],
- "markers": "python_full_version >= '3.7.0'",
- "version": "==13.7.1"
+ "markers": "python_full_version >= '3.8.0'",
+ "version": "==13.9.4"
},
"rsa": {
"hashes": [
@@ -2582,35 +2290,27 @@
},
"s3transfer": {
"hashes": [
- "sha256:263ed587a5803c6c708d3ce44dc4dfedaab4c1a32e8329bab818933d79ddcf5d",
- "sha256:4f50ed74ab84d474ce614475e0b8d5047ff080810aac5d01ea25231cfc944b0c"
+ "sha256:3f25c900a367c8b7f7d8f9c34edc87e300bde424f779dc9f0a8ae4f9df9264f6",
+ "sha256:8fa0aa48177be1f3425176dfe1ab85dcd3d962df603c3dbfc585e6bf857ef0ff"
],
"markers": "python_version >= '3.8'",
- "version": "==0.10.3"
- },
- "secretstorage": {
- "hashes": [
- "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77",
- "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"
- ],
- "markers": "python_version >= '3.6'",
- "version": "==3.3.3"
+ "version": "==0.11.1"
},
"selenium": {
"hashes": [
- "sha256:3798d2d12b4a570bc5790163ba57fef10b2afee958bf1d80f2a3cf07c4141f33",
- "sha256:95d08d3b82fb353f3c474895154516604c7f0e6a9a565ae6498ef36c9bac6921"
+ "sha256:3d6a2e8e1b850a1078884ea19f4e011ecdc12263434d87a0b78769836fb82dd8",
+ "sha256:a9fae6eef48d470a1b0c6e45185d96f0dafb025e8da4b346cc41e4da3ac54fa0"
],
- "markers": "python_version >= '3.8'",
- "version": "==4.25.0"
+ "markers": "python_version >= '3.9'",
+ "version": "==4.28.0"
},
"six": {
"hashes": [
- "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
- "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
+ "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274",
+ "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"
],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==1.16.0"
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
+ "version": "==1.17.0"
},
"sniffio": {
"hashes": [
@@ -2620,13 +2320,6 @@
"markers": "python_version >= '3.7'",
"version": "==1.3.1"
},
- "snowballstemmer": {
- "hashes": [
- "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1",
- "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"
- ],
- "version": "==2.2.0"
- },
"snscrape": {
"hashes": [
"sha256:6eedb85c7e79f35361dde1949e1e7e2dee44e9f8469668438c9f8e72980f482f",
@@ -2644,178 +2337,91 @@
},
"soupsieve": {
"hashes": [
- "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690",
- "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"
+ "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb",
+ "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"
],
"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"
+ "version": "==2.6"
},
"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"
+ "sha256:03f0528c53ca0b67094c4764523c1451ea15959bbf0a8a8a3096900014db0278",
+ "sha256:12b0f1ec623cccf058cf21cb544f0e74656618165b083d78145cafde156ea7b6",
+ "sha256:12b28d99a9c14eaf4055810df1001557176716de0167b91026e648e65229bffb",
+ "sha256:1b2690456528a87234a75d1a1644cdb330a6926f455403c8e4f6cad6921f9098",
+ "sha256:1cdba1f73b64530c47b27118b7053b8447e6d6f3c8104e3ac59f3d40c33aa9fd",
+ "sha256:293f9ade06b2e68dd03cfb14d49202fac47b7bb94bffcff174568c951fbc7af2",
+ "sha256:2952748ecd67ed3b56773c185e85fc084f6bdcdec10e5032a7c25a6bc7d682ef",
+ "sha256:2f95fc8e3f34b5f6b3effb49d10ac97c569ec8e32f985612d9b25dd12d0d2e94",
+ "sha256:2fa2c0913f02341d25fb858e4fb2031e6b0813494cca1ba07d417674128ce11b",
+ "sha256:3151822aa1db0eb5afd65ccfafebe0ef5cda3a7701a279c8d0bf17781a793bb4",
+ "sha256:35bd2df269de082065d4b23ae08502a47255832cc3f17619a5cea92ce478b02b",
+ "sha256:41296bbcaa55ef5fdd32389a35c710133b097f7b2609d8218c0eabded43a1d84",
+ "sha256:44f569d0b1eb82301b92b72085583277316e7367e038d97c3a1a899d9a05e342",
+ "sha256:46954173612617a99a64aee103bcd3f078901b9a8dcfc6ae80cbf34ba23df989",
+ "sha256:4b12885dc85a2ab2b7d00995bac6d967bffa8594123b02ed21e8eb2205a7584b",
+ "sha256:4f581d365af9373a738c49e0c51e8b18e08d8a6b1b15cc556773bcd8a192fa8b",
+ "sha256:51bc9cfef83e0ac84f86bf2b10eaccb27c5a3e66a1212bef676f5bee6ef33ebb",
+ "sha256:521ef85c04c33009166777c77e76c8a676e2d8528dc83a57836b63ca9c69dcd1",
+ "sha256:5bc3339db84c5fb9130ac0e2f20347ee77b5dd2596ba327ce0d399752f4fce39",
+ "sha256:635d8a21577341dfe4f7fa59ec394b346da12420b86624a69e466d446de16aff",
+ "sha256:648ec5acf95ad59255452ef759054f2176849662af4521db6cb245263ae4aa33",
+ "sha256:650dcb70739957a492ad8acff65d099a9586b9b8920e3507ca61ec3ce650bb72",
+ "sha256:6b788f14c5bb91db7f468dcf76f8b64423660a05e57fe277d3f4fad7b9dcb7ce",
+ "sha256:6c67415258f9f3c69867ec02fea1bf6508153709ecbd731a982442a590f2b7e4",
+ "sha256:74bbd1d0a9bacf34266a7907d43260c8d65d31d691bb2356f41b17c2dca5b1d0",
+ "sha256:75311559f5c9881a9808eadbeb20ed8d8ba3f7225bef3afed2000c2a9f4d49b9",
+ "sha256:78361be6dc9073ed17ab380985d1e45e48a642313ab68ab6afa2457354ff692c",
+ "sha256:7b7e772dc4bc507fdec4ee20182f15bd60d2a84f1e087a8accf5b5b7a0dcf2ba",
+ "sha256:82df02816c14f8dc9f4d74aea4cb84a92f4b0620235daa76dde002409a3fbb5a",
+ "sha256:84b9f23b0fa98a6a4b99d73989350a94e4a4ec476b9a7dfe9b79ba5939f5e80b",
+ "sha256:8c4096727193762e72ce9437e2a86a110cf081241919ce3fab8e89c02f6b6658",
+ "sha256:8e47f1af09444f87c67b4f1bb6231e12ba6d4d9f03050d7fc88df6d075231a49",
+ "sha256:93d1543cd8359040c02b6614421c8e10cd7a788c40047dbc507ed46c29ae5636",
+ "sha256:94b564e38b344d3e67d2e224f0aec6ba09a77e4582ced41e7bfd0f757d926ec9",
+ "sha256:955a2a765aa1bd81aafa69ffda179d4fe3e2a3ad462a736ae5b6f387f78bfeb8",
+ "sha256:9d087663b7e1feabea8c578d6887d59bb00388158e8bff3a76be11aa3f748ca2",
+ "sha256:9df21b8d9e5c136ea6cde1c50d2b1c29a2b5ff2b1d610165c23ff250e0704087",
+ "sha256:a8998bf9f8658bd3839cbc44ddbe982955641863da0c1efe5b00c1ab4f5c16b1",
+ "sha256:b2eae3423e538c10d93ae3e87788c6a84658c3ed6db62e6a61bb9495b0ad16bb",
+ "sha256:b661b49d0cb0ab311a189b31e25576b7ac3e20783beb1e1817d72d9d02508bf5",
+ "sha256:bedee60385c1c0411378cbd4dc486362f5ee88deceea50002772912d798bb00f",
+ "sha256:c505edd429abdfe3643fa3b2e83efb3445a34a9dc49d5f692dd087be966020e0",
+ "sha256:cce918ada64c956b62ca2c2af59b125767097ec1dca89650a6221e887521bfd7",
+ "sha256:cf5ae8a9dcf657fd72144a7fd01f243236ea39e7344e579a121c4205aedf07bb",
+ "sha256:cf95a60b36997dad99692314c4713f141b61c5b0b4cc5c3426faad570b31ca01",
+ "sha256:d57bafbab289e147d064ffbd5cca2d7b1394b63417c0636cea1f2e93d16eb9e8",
+ "sha256:d70f53a0646cc418ca4853da57cf3ddddbccb8c98406791f24426f2dd77fd0e2",
+ "sha256:d75ead7dd4d255068ea0f21492ee67937bd7c90964c8f3c2bea83c7b7f81b95f",
+ "sha256:da36c3b0e891808a7542c5c89f224520b9a16c7f5e4d6a1156955605e54aef0e",
+ "sha256:db18ff6b8c0f1917f8b20f8eca35c28bbccb9f83afa94743e03d40203ed83de9",
+ "sha256:dfff7be361048244c3aa0f60b5e63221c5e0f0e509f4e47b8910e22b57d10ae7",
+ "sha256:e4fb5ac86d8fe8151966814f6720996430462e633d225497566b3996966b9bdb",
+ "sha256:e56a139bfe136a22c438478a86f8204c1eb5eed36f4e15c4224e4b9db01cb3e4",
+ "sha256:e6f5d254a22394847245f411a2956976401e84da4288aa70cbcd5190744062c1",
+ "sha256:e7402ff96e2b073a98ef6d6142796426d705addd27b9d26c3b32dbaa06d7d069",
+ "sha256:ea308cec940905ba008291d93619d92edaf83232ec85fbd514dcb329f3192761",
+ "sha256:eaa8039b6d20137a4e02603aba37d12cd2dde7887500b8855356682fc33933f4"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
- "version": "==2.0.36"
+ "version": "==2.0.37"
},
"starlette": {
"hashes": [
- "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee",
- "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823"
+ "sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835",
+ "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7"
],
"markers": "python_version >= '3.8'",
- "version": "==0.37.2"
+ "version": "==0.41.3"
},
"telethon": {
"hashes": [
- "sha256:e5e43cff1c1b34e2f9c2b395215beb6e9bda706b69def7efff4f55b23c9c4374"
+ "sha256:30c187017501bfb982b8af5659f864dda4108f77ea49cfce61e8f6fdb8a18d6e",
+ "sha256:f9866c1e37197a0894e0c02aa56a6359bffb14a585e88e18e3e819df4fda399a"
],
"markers": "python_version >= '3.5'",
- "version": "==1.37.0"
+ "version": "==1.38.1"
},
"text-unidecode": {
"hashes": [
@@ -2830,46 +2436,21 @@
],
"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"
+ "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2",
+ "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"
],
"markers": "python_version >= '3.7'",
- "version": "==4.66.5"
+ "version": "==4.67.1"
},
"trio": {
"hashes": [
- "sha256:1dcc95ab1726b2da054afea8fd761af74bad79bd52381b84eae408e983c76831",
- "sha256:68eabbcf8f457d925df62da780eff15ff5dc68fd6b367e2dde59f7aaf2a0b884"
+ "sha256:4e547896fe9e8a5658e54e4c7c5fa1db748cbbbaa7c965e7d40505b928c73c05",
+ "sha256:56d58977acc1635735a96581ec70513cc781b8b6decd299c487d3be2a721cd94"
],
- "markers": "python_version >= '3.8'",
- "version": "==0.27.0"
+ "markers": "python_version >= '3.9'",
+ "version": "==0.28.0"
},
"trio-websocket": {
"hashes": [
@@ -2881,18 +2462,10 @@
},
"tsp-client": {
"hashes": [
- "sha256:0b790d10a68d66782c13f1d7cc7f5206df26b49826c1da80944b7c05b1731784",
- "sha256:6e66148dd116322eb44a7484e5ad33bbe640b997343c443de9cc70fc5eb19987"
+ "sha256:415ff89aa15775533801bb18bd6b287f30a293d976b8fbb4d30f48873af41ba4",
+ "sha256:db7f98e26ac370f5aab0055f74e7b3e4fd5245ef2f57cc56db3caa2694b82fd6"
],
- "version": "==0.2.0"
- },
- "twine": {
- "hashes": [
- "sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997",
- "sha256:9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==5.1.1"
+ "version": "==0.2.1"
},
"typing-extensions": {
"hashes": [
@@ -2911,11 +2484,11 @@
},
"tzdata": {
"hashes": [
- "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc",
- "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"
+ "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694",
+ "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639"
],
"markers": "python_version >= '2'",
- "version": "==2024.2"
+ "version": "==2025.1"
},
"tzlocal": {
"hashes": [
@@ -2938,20 +2511,20 @@
"socks"
],
"hashes": [
- "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472",
- "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"
+ "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df",
+ "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"
],
- "markers": "python_version >= '3.8'",
- "version": "==2.2.2"
+ "markers": "python_version >= '3.9'",
+ "version": "==2.3.0"
},
"uvicorn": {
"hashes": [
- "sha256:cd17daa7f3b9d7a24de3617820e634d0933b69eed8e33a516071174427238c81",
- "sha256:d46cd8e0fd80240baffbcd9ec1012a712938754afcf81bce56c024c1656aece8"
+ "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4",
+ "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"
],
"index": "pypi",
- "markers": "python_version >= '3.8'",
- "version": "==0.30.1"
+ "markers": "python_version >= '3.9'",
+ "version": "==0.34.0"
},
"vine": {
"hashes": [
@@ -2970,99 +2543,18 @@
},
"vk-url-scraper": {
"hashes": [
- "sha256:2e83e690844bb9b04772fae56bed2d9654780ca23132155e63de4ed9bde70c23",
- "sha256:6616c8fbe6ea6f8cbe4605898a89d1173a579ed6a9da5410dba80269d708fcb1"
+ "sha256:133d252ee94ceb1ee9515fb448d410ba471cbccc19e303b548076cd44cc81f30",
+ "sha256:c1c001b66b80343a991628080398d8a923e8753183b952f99f40ecafe1087070"
],
- "markers": "python_version >= '3.10'",
- "version": "==0.3.30"
+ "markers": "python_version >= '3.7'",
+ "version": "==0.3.27"
},
"warcio": {
"hashes": [
- "sha256:ced1a162d76434d56abd81b37ac152821d1a11e1db835ead5d649f58068c2203",
- "sha256:e1889dad9ecac654de5b0973247f335a55827b1b14a8203772d18c749143ea51"
+ "sha256:7247b57e68074cfd9433cb6dc226f8567d6777052abec2d3c78346cffa4d19b9",
+ "sha256:ca96130bde7747e49da714097d144c6ff939458d4f93e1beb1e42455db4326d4"
],
- "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"
+ "version": "==1.7.5"
},
"wcwidth": {
"hashes": [
@@ -3071,13 +2563,6 @@
],
"version": "==0.2.13"
},
- "webencodings": {
- "hashes": [
- "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78",
- "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"
- ],
- "version": "==0.5.1"
- },
"websocket-client": {
"hashes": [
"sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526",
@@ -3088,89 +2573,86 @@
},
"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"
+ "sha256:02687db35dbc7d25fd541a602b5f8e451a238ffa033030b172ff86a93cb5dc2a",
+ "sha256:065ce275e7c4ffb42cb738dd6b20726ac26ac9ad0a2a48e33ca632351a737267",
+ "sha256:091ab63dfc8cea748cc22c1db2814eadb77ccbf82829bac6b2fbe3401d548eda",
+ "sha256:0a52a6d7cf6938e04e9dceb949d35fbdf58ac14deea26e685ab6368e73744e4c",
+ "sha256:0a6f3efd47ffd0d12080594f434faf1cd2549b31e54870b8470b28cc1d3817d9",
+ "sha256:0d8c3e2cdb38f31d8bd7d9d28908005f6fa9def3324edb9bf336d7e4266fd397",
+ "sha256:1979bee04af6a78608024bad6dfcc0cc930ce819f9e10342a29a05b5320355d0",
+ "sha256:1a5a20d5843886d34ff8c57424cc65a1deda4375729cbca4cb6b3353f3ce4142",
+ "sha256:1c9b6535c0e2cf8a6bf938064fb754aaceb1e6a4a51a80d884cd5db569886910",
+ "sha256:1f20522e624d7ffbdbe259c6b6a65d73c895045f76a93719aa10cd93b3de100c",
+ "sha256:2066dc4cbcc19f32c12a5a0e8cc1b7ac734e5b64ac0a325ff8353451c4b15ef2",
+ "sha256:20e6dd0984d7ca3037afcb4494e48c74ffb51e8013cac71cf607fffe11df7205",
+ "sha256:22441c81a6748a53bfcb98951d58d1af0661ab47a536af08920d129b4d1c3473",
+ "sha256:2c6c0097a41968b2e2b54ed3424739aab0b762ca92af2379f152c1aef0187e1c",
+ "sha256:2dddacad58e2614a24938a50b85969d56f88e620e3f897b7d80ac0d8a5800258",
+ "sha256:2e20c5f517e2163d76e2729104abc42639c41cf91f7b1839295be43302713661",
+ "sha256:34277a29f5303d54ec6468fb525d99c99938607bc96b8d72d675dee2b9f5bf1d",
+ "sha256:3bdc8c692c866ce5fefcaf07d2b55c91d6922ac397e031ef9b774e5b9ea42166",
+ "sha256:3c1426c021c38cf92b453cdf371228d3430acd775edee6bac5a4d577efc72365",
+ "sha256:44bba1a956c2c9d268bdcdf234d5e5ff4c9b6dc3e300545cbe99af59dda9dcce",
+ "sha256:4b27ece32f63150c268593d5fdb82819584831a83a3f5809b7521df0685cd5d8",
+ "sha256:4da98b72009836179bb596a92297b1a61bb5a830c0e483a7d0766d45070a08ad",
+ "sha256:4daa0faea5424d8713142b33825fff03c736f781690d90652d2c8b053345b0e7",
+ "sha256:5059ed9c54945efb321f097084b4c7e52c246f2c869815876a69d1efc4ad6eb5",
+ "sha256:577a4cebf1ceaf0b65ffc42c54856214165fb8ceeba3935852fc33f6b0c55e7f",
+ "sha256:647b573f7d3ada919fd60e64d533409a79dcf1ea21daeb4542d1d996519ca967",
+ "sha256:669c3e101c246aa85bc8534e495952e2ca208bd87994650b90a23d745902db9a",
+ "sha256:6af6a4b26eea4fc06c6818a6b962a952441e0e39548b44773502761ded8cc1d4",
+ "sha256:6af99a38e49f66be5a64b1e890208ad026cda49355661549c507152113049990",
+ "sha256:6d7ff794c8b36bc402f2e07c0b2ceb4a2424147ed4785ff03e2a7af03711d60a",
+ "sha256:6f1372e511c7409a542291bce92d6c83320e02c9cf392223272287ce55bc224e",
+ "sha256:714a9b682deb4339d39ffa674f7b674230227d981a37d5d174a4a83e3978a610",
+ "sha256:75862126b3d2d505e895893e3deac0a9339ce750bd27b4ba515f008b5acf832d",
+ "sha256:7a570862c325af2111343cc9b0257b7119b904823c675b22d4ac547163088d0d",
+ "sha256:7a6ceec4ea84469f15cf15807a747e9efe57e369c384fa86e022b3bea679b79b",
+ "sha256:7cd5706caec1686c5d233bc76243ff64b1c0dc445339bd538f30547e787c11fe",
+ "sha256:80c8efa38957f20bba0117b48737993643204645e9ec45512579132508477cfc",
+ "sha256:862e9967b46c07d4dcd2532e9e8e3c2825e004ffbf91a5ef9dde519ee2effb0b",
+ "sha256:86cf1aaeca909bf6815ea714d5c5736c8d6dd3a13770e885aafe062ecbd04f1f",
+ "sha256:89a71173caaf75fa71a09a5f614f450ba3ec84ad9fca47cb2422a860676716f0",
+ "sha256:9f05702e93203a6ff5226e21d9b40c037761b2cfb637187c9802c10f58e40473",
+ "sha256:a39d7eceeea35db85b85e1169011bb4321c32e673920ae9c1b6e0978590012a3",
+ "sha256:a3c4aa3428b904d5404a0ed85f3644d37e2cb25996b7f096d77caeb0e96a3b42",
+ "sha256:a9b0f6c3ba3b1240f602ebb3971d45b02cc12bd1845466dd783496b3b05783a5",
+ "sha256:a9e72fb63e5f3feacdcf5b4ff53199ec8c18d66e325c34ee4c551ca748623bbc",
+ "sha256:ab95d357cd471df61873dadf66dd05dd4709cae001dd6342edafc8dc6382f307",
+ "sha256:ad1c1d02357b7665e700eca43a31d52814ad9ad9b89b58118bdabc365454b574",
+ "sha256:b374e8953ad477d17e4851cdc66d83fdc2db88d9e73abf755c94510ebddceb95",
+ "sha256:b439ea828c4ba99bb3176dc8d9b933392a2413c0f6b149fdcba48393f573377f",
+ "sha256:b4c8cef610e8d7c70dea92e62b6814a8cd24fbd01d7103cc89308d2bfe1659ef",
+ "sha256:bbe03eb853e17fd5b15448328b4ec7fb2407d45fb0245036d06a3af251f8e48f",
+ "sha256:bc63cee8596a6ec84d9753fd0fcfa0452ee12f317afe4beae6b157f0070c6c7f",
+ "sha256:c3ecadc7ce90accf39903815697917643f5b7cfb73c96702318a096c00aa71f5",
+ "sha256:c76193c1c044bd1e9b3316dcc34b174bbf9664598791e6fb606d8d29000e070c",
+ "sha256:c93215fac5dadc63e51bcc6dceca72e72267c11def401d6668622b47675b097f",
+ "sha256:cc45afb9c9b2dc0852d5c8b5321759cf825f82a31bfaf506b65bf4668c96f8b2",
+ "sha256:d7d9cafbccba46e768be8a8ad4635fa3eae1ffac4c6e7cb4eb276ba41297ed29",
+ "sha256:da85651270c6bfb630136423037dd4975199e5d4114cae6d3066641adcc9d1c7",
+ "sha256:dec254fcabc7bd488dab64846f588fc5b6fe0d78f641180030f8ea27b76d72c3",
+ "sha256:e3fbd68850c837e57373d95c8fe352203a512b6e49eaae4c2f4088ef8cf21980",
+ "sha256:e8179f95323b9ab1c11723e5d91a89403903f7b001828161b480a7810b334885",
+ "sha256:e9d0e53530ba7b8b5e389c02282f9d2aa47581514bd6049d3a7cffe1385cf5fe",
+ "sha256:eabdb28b972f3729348e632ab08f2a7b616c7e53d5414c12108c29972e655b20",
+ "sha256:ec607328ce95a2f12b595f7ae4c5d71bf502212bddcea528290b35c286932b12",
+ "sha256:efd9b868d78b194790e6236d9cbc46d68aba4b75b22497eb4ab64fa640c3af56",
+ "sha256:f2e53c72052f2596fb792a7acd9704cbc549bf70fcde8a99e899311455974ca3",
+ "sha256:f390024a47d904613577df83ba700bd189eedc09c57af0a904e5c39624621270",
+ "sha256:f8a86a269759026d2bde227652b87be79f8a734e582debf64c9d302faa1e9f03",
+ "sha256:fd475a974d5352390baf865309fe37dec6831aafc3014ffac1eea99e84e83fc2"
],
- "markers": "python_version >= '3.8'",
- "version": "==12.0"
+ "markers": "python_version >= '3.9'",
+ "version": "==14.2"
},
"werkzeug": {
"hashes": [
- "sha256:02c9eb92b7d6c06f31a782811505d2157837cea66aaede3e217c7c27c039476c",
- "sha256:34f2371506b250df4d4f84bfe7b0921e4762525762bbd936614909fe25cd7306"
+ "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e",
+ "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"
],
- "markers": "python_version >= '3.8'",
- "version": "==3.0.4"
+ "markers": "python_version >= '3.9'",
+ "version": "==3.1.3"
},
"wsproto": {
"hashes": [
@@ -3182,194 +2664,186 @@
},
"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"
+ "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba",
+ "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193",
+ "sha256:045b8482ce9483ada4f3f23b3774f4e1bf4f23a2d5c912ed5170f68efb053318",
+ "sha256:09c7907c8548bcd6ab860e5f513e727c53b4a714f459b084f6580b49fa1b9cee",
+ "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e",
+ "sha256:0b3c92fa08759dbf12b3a59579a4096ba9af8dd344d9a813fc7f5070d86bbab1",
+ "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a",
+ "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186",
+ "sha256:1d407181cfa6e70077df3377938c08012d18893f9f20e92f7d2f314a437c30b1",
+ "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50",
+ "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640",
+ "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb",
+ "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8",
+ "sha256:3236da9272872443f81fedc389bace88408f64f89f75d1bdb2256069a8730ccc",
+ "sha256:35098b24e0327fc4ebdc8ffe336cee0a87a700c24ffed13161af80124b7dc8e5",
+ "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58",
+ "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2",
+ "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393",
+ "sha256:4ac515b860c36becb81bb84b667466885096b5fc85596948548b667da3bf9f24",
+ "sha256:5094d9206c64181d0f6e76ebd8fb2f8fe274950a63890ee9e0ebfd58bf9d787b",
+ "sha256:54d6921f07555713b9300bee9c50fb46e57e2e639027089b1d795ecd9f7fa910",
+ "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c",
+ "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272",
+ "sha256:602d98f2c2d929f8e697ed274fbadc09902c4025c5a9963bf4e9edfc3ab6f7ed",
+ "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1",
+ "sha256:61e5e68cb65ac8f547f6b5ef933f510134a6bf31bb178be428994b0cb46c2a04",
+ "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d",
+ "sha256:6333c5a377c8e2f5fae35e7b8f145c617b02c939d04110c76f29ee3676b5f9a5",
+ "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d",
+ "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889",
+ "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae",
+ "sha256:757e81cae69244257d125ff31663249b3013b5dc0a8520d73694aed497fb195b",
+ "sha256:77a6e85b90a7641d2e07184df5557132a337f136250caafc9ccaa4a2a998ca2c",
+ "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576",
+ "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34",
+ "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477",
+ "sha256:80316a8bd5109320d38eef8833ccf5f89608c9107d02d2a7f985f98ed6876990",
+ "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2",
+ "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512",
+ "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069",
+ "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a",
+ "sha256:8874027a53e3aea659a6d62751800cf6e63314c160fd607489ba5c2edd753cf6",
+ "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0",
+ "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8",
+ "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb",
+ "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa",
+ "sha256:93b2e109287f93db79210f86deb6b9bbb81ac32fc97236b16f7433db7fc437d8",
+ "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e",
+ "sha256:a440a2a624683108a1b454705ecd7afc1c3438a08e890a1513d468671d90a04e",
+ "sha256:a4bb030cf46a434ec0225bddbebd4b89e6471814ca851abb8696170adb163985",
+ "sha256:a9ca04806f3be0ac6d558fffc2fdf8fcef767e0489d2684a21912cc4ed0cd1b8",
+ "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1",
+ "sha256:ac36703a585e0929b032fbaab0707b75dc12703766d0b53486eabd5139ebadd5",
+ "sha256:b1771de9944d875f1b98a745bc547e684b863abf8f8287da8466cf470ef52690",
+ "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10",
+ "sha256:b4f6450109834af88cb4cc5ecddfc5380ebb9c228695afc11915a0bf82116789",
+ "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b",
+ "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca",
+ "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e",
+ "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5",
+ "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59",
+ "sha256:ba87babd629f8af77f557b61e49e7c7cac36f22f871156b91e10a6e9d4f829e9",
+ "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8",
+ "sha256:c1e1cc06da1491e6734f0ea1e6294ce00792193c463350626571c287c9a704db",
+ "sha256:c654d5207c78e0bd6d749f6dae1dcbbfde3403ad3a4b11f3c5544d9906969dde",
+ "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7",
+ "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb",
+ "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3",
+ "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6",
+ "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285",
+ "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb",
+ "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8",
+ "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482",
+ "sha256:e2c08cc9b16f4f4bc522771d96734c7901e7ebef70c6c5c35dd0f10845270bcd",
+ "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75",
+ "sha256:e3b9fd71836999aad54084906f8663dffcd2a7fb5cdafd6c37713b2e72be1760",
+ "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782",
+ "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53",
+ "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2",
+ "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1",
+ "sha256:fe57328fbc1bfd0bd0514470ac692630f3901c0ee39052ae47acd1d90a436719",
+ "sha256:fea09ca13323376a2fdfb353a5fa2e59f90cd18d7ca4eaa1fd31f0a8b4f91e62"
],
- "markers": "python_version >= '3.7'",
- "version": "==1.9.4"
+ "markers": "python_version >= '3.9'",
+ "version": "==1.18.3"
},
"yt-dlp": {
"hashes": [
- "sha256:2a59d9e65ef6dadb1ff318346d04403664c3fa395e098fcd0d7ad626ef9f8a89",
- "sha256:f4614e1c710fcb387bf152d2162868c565ed3f675647ecaa19dab54e581780eb"
+ "sha256:b8666b88e23c3fa5ee1e80920f4a9dfac7c405504a447214c0cf3d0c386edcfc",
+ "sha256:e8ec515d49bb62704915d13a22ee6fe03a5658d651e4e64574e3a17ee01f6e3b"
],
- "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"
+ "markers": "python_version >= '3.9'",
+ "version": "==2025.1.15"
}
},
"develop": {
"anyio": {
"hashes": [
- "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94",
- "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"
+ "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a",
+ "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"
],
- "markers": "python_version >= '3.8'",
- "version": "==4.4.0"
+ "markers": "python_version >= '3.9'",
+ "version": "==4.8.0"
},
"certifi": {
"hashes": [
- "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b",
- "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"
+ "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56",
+ "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"
],
"markers": "python_version >= '3.6'",
- "version": "==2024.7.4"
+ "version": "==2024.12.14"
},
"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"
+ "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9",
+ "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f",
+ "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273",
+ "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994",
+ "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e",
+ "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50",
+ "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e",
+ "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e",
+ "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c",
+ "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853",
+ "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8",
+ "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8",
+ "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe",
+ "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165",
+ "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb",
+ "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59",
+ "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609",
+ "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18",
+ "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098",
+ "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd",
+ "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3",
+ "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43",
+ "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d",
+ "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359",
+ "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90",
+ "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78",
+ "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a",
+ "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99",
+ "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988",
+ "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2",
+ "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0",
+ "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694",
+ "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377",
+ "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d",
+ "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23",
+ "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312",
+ "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf",
+ "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6",
+ "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b",
+ "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c",
+ "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690",
+ "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a",
+ "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f",
+ "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4",
+ "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25",
+ "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd",
+ "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852",
+ "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0",
+ "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244",
+ "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315",
+ "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078",
+ "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0",
+ "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27",
+ "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132",
+ "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5",
+ "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247",
+ "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022",
+ "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b",
+ "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3",
+ "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18",
+ "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5",
+ "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"
],
- "markers": "python_version >= '3.8'",
- "version": "==7.6.0"
+ "index": "pypi",
+ "markers": "python_version >= '3.9'",
+ "version": "==7.6.10"
},
"exceptiongroup": {
"hashes": [
@@ -3389,27 +2863,27 @@
},
"httpcore": {
"hashes": [
- "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f",
- "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f"
+ "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c",
+ "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"
],
"markers": "python_version >= '3.8'",
- "version": "==1.0.6"
+ "version": "==1.0.7"
},
"httpx": {
"hashes": [
- "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0",
- "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"
+ "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc",
+ "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"
],
"markers": "python_version >= '3.8'",
- "version": "==0.27.2"
+ "version": "==0.28.1"
},
"idna": {
"hashes": [
- "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc",
- "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"
+ "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9",
+ "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"
],
- "markers": "python_version >= '3.5'",
- "version": "==3.7"
+ "markers": "python_version >= '3.6'",
+ "version": "==3.10"
},
"iniconfig": {
"hashes": [
@@ -3421,11 +2895,11 @@
},
"packaging": {
"hashes": [
- "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002",
- "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"
+ "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759",
+ "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"
],
"markers": "python_version >= '3.8'",
- "version": "==24.1"
+ "version": "==24.2"
},
"pluggy": {
"hashes": [
@@ -3437,20 +2911,21 @@
},
"pytest": {
"hashes": [
- "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343",
- "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==8.2.2"
- },
- "pytest-asyncio": {
- "hashes": [
- "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b",
- "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"
+ "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6",
+ "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
- "version": "==0.24.0"
+ "version": "==8.3.4"
+ },
+ "pytest-asyncio": {
+ "hashes": [
+ "sha256:0d0bb693f7b99da304a0634afc0a4b19e49d5e0de2d670f38dc4bfa5727c5075",
+ "sha256:3f8ef9a98f45948ea91a0ed3dc4268b5326c0e7bce73892acc654df4262ad45f"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.9'",
+ "version": "==0.25.2"
},
"sniffio": {
"hashes": [
@@ -3462,11 +2937,41 @@
},
"tomli": {
"hashes": [
- "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
- "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
+ "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6",
+ "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd",
+ "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c",
+ "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b",
+ "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8",
+ "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6",
+ "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77",
+ "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff",
+ "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea",
+ "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192",
+ "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249",
+ "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee",
+ "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4",
+ "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98",
+ "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8",
+ "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4",
+ "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281",
+ "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744",
+ "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69",
+ "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13",
+ "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140",
+ "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e",
+ "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e",
+ "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc",
+ "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff",
+ "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec",
+ "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2",
+ "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222",
+ "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106",
+ "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272",
+ "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a",
+ "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"
],
- "markers": "python_version >= '3.7'",
- "version": "==2.0.1"
+ "markers": "python_version >= '3.8'",
+ "version": "==2.2.1"
},
"typing-extensions": {
"hashes": [
@@ -3478,40 +2983,40 @@
},
"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"
+ "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a",
+ "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2",
+ "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f",
+ "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c",
+ "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c",
+ "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c",
+ "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0",
+ "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13",
+ "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134",
+ "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa",
+ "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e",
+ "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379",
+ "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a",
+ "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11",
+ "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282",
+ "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b",
+ "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f",
+ "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c",
+ "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112",
+ "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948",
+ "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881",
+ "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860",
+ "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3",
+ "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680",
+ "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26",
+ "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26",
+ "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e",
+ "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8",
+ "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c",
+ "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"
],
"index": "pypi",
"markers": "python_version >= '3.9'",
- "version": "==5.0.3"
+ "version": "==6.0.0"
}
}
}
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/crud.py b/src/db/crud.py
index aae2cd8..c48f6ae 100644
--- a/src/db/crud.py
+++ b/src/db/crud.py
@@ -1,11 +1,12 @@
from collections import defaultdict
-from functools import cache
+from functools import lru_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 db.database import get_db
from shared.settings import get_settings
from . import models, schemas
import yaml
@@ -23,7 +24,7 @@ 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)
+ groups = get_user_groups(email)
query = query.filter(or_(models.Archive.public == True, models.Archive.author_id == email, models.Archive.group_id.in_(groups)))
return query.first()
@@ -33,7 +34,7 @@ def search_archives_by_url(db: Session, url: str, email: str, skip: int = 0, lim
query = base_query(db)
if email != ALLOW_ANY_EMAIL:
email = email.lower()
- groups = get_user_groups(db, email)
+ groups = get_user_groups(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)
@@ -121,72 +122,37 @@ def is_active_user(db: Session, email: str) -> bool:
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:
+def is_user_in_group(db: Session, email: str, group_name: 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)
+ return len(group_name) and len(email) and group_name in get_user_groups(email)
-#TODO: maybe this can be cached? what about the db session?
-def get_user_groups(db: Session, email: str) -> list[str]:
+@lru_cache
+def get_user_groups(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. 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_names = [g[0] for g in user_groups]
+ with get_db() as db:
+ # 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]
+ # 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))
-
-
-# --------------- SHEET
-
-def has_quota_sheet(db: Session, email: str, user_groups_names: list[str]) -> bool:
- """
- checks if a user has reached their sheet quota
- """
- user_sheets = db.query(models.Sheet).filter(models.Sheet.author_id == email).count()
-
- user_groups = db.query(models.Group).filter(models.Group.id.in_(user_groups_names)).all()
-
- quota = 0
- for group in user_groups:
- active_sheets = group.permissions.get("active_sheets", 0)
- if active_sheets == -1: return True
- quota = max(quota, active_sheets)
- return user_sheets < quota
-
-
-def create_sheet(db: Session, sheet_id: str, sheet_name: str, email: str, group_id: str, frequency: str):
- db_sheet = models.Sheet(id=sheet_id, name=sheet_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_sheets(db: Session, email: str) -> list[models.Sheet]:
- return db.query(models.Sheet).filter(models.Sheet.author_id == email).order_by(models.Sheet.last_archived_at.desc()).all()
-
-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 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
+ return list(set(user_level_groups_names + domain_level_groups_names))
# --------------- INIT User-Groups
+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, is_active: bool = models.User.is_active.default.arg) -> models.User:
if type(author_id) == str: author_id = author_id.lower()
@@ -296,3 +262,28 @@ def upsert_user_groups(db: Session):
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, sheet_name: str, email: str, group_id: str, frequency: str):
+ db_sheet = models.Sheet(id=sheet_id, name=sheet_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_archived_at.desc()).all()
+
+
+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
\ No newline at end of file
diff --git a/src/db/database.py b/src/db/database.py
index d42466a..1166099 100644
--- a/src/db/database.py
+++ b/src/db/database.py
@@ -33,4 +33,4 @@ def get_db():
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
+ yield db
diff --git a/src/db/models.py b/src/db/models.py
index f782588..afadec6 100644
--- a/src/db/models.py
+++ b/src/db/models.py
@@ -87,7 +87,7 @@ 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={})
domains = Column(JSON, default=[])
archives = relationship("Archive", back_populates="group")
diff --git a/src/db/schemas.py b/src/db/schemas.py
index a67dac6..a03cb5a 100644
--- a/src/db/schemas.py
+++ b/src/db/schemas.py
@@ -1,3 +1,5 @@
+from typing import Annotated
+from annotated_types import Len
from pydantic import BaseModel, field_validator
from datetime import datetime
@@ -105,3 +107,10 @@ class SheetResponse(SheetAdd):
stats: dict | None
last_archived_at: datetime | None
created_at: datetime
+
+
+class ArchiveTrigger(BaseModel):
+ url: Annotated[str, Len(min_length=5)]
+ public: bool = True
+ group_id: Annotated[str, Len(min_length=1)] | None = None
+ tags: set[Tag] | None = set()
diff --git a/src/db/user_state.py b/src/db/user_state.py
new file mode 100644
index 0000000..afcb042
--- /dev/null
+++ b/src/db/user_state.py
@@ -0,0 +1,142 @@
+
+import sqlalchemy
+from sqlalchemy.orm import Session
+from sqlalchemy import func
+from db import crud, models
+from datetime import datetime
+
+
+class UserState:
+ """
+ Manage a user's state and permissions
+ """
+
+ def __init__(self, db: Session, email: str, active=False):
+ self.db = db
+ self.email = email
+ self.active = active
+
+ @property
+ def user_groups_names(self):
+ if not hasattr(self, '_user_groups_names'):
+ self._user_groups_names = crud.get_user_groups(self.email)
+ return self._user_groups_names
+
+ @property
+ def user_groups(self):
+ if not hasattr(self, '_user_groups'):
+ self._user_groups = self.db.query(models.Group).filter(
+ models.Group.id.in_(self.user_groups_names)
+ ).all()
+ return self._user_groups
+
+ @property
+ def allowed_frequencies(self):
+ if not hasattr(self, '_allowed_frequencies'):
+ self._allowed_frequencies = set()
+ for group in self.user_groups:
+ if not group.permissions: continue
+ self._allowed_frequencies.add(group.permissions.get("allowed_frequency", None))
+ if "hourly" in self._allowed_frequencies:
+ self._allowed_frequencies.add("daily")
+ return self._allowed_frequencies
+
+ @property
+ def sheet_quota(self):
+ """
+ infer the user's sheet quota from the groups
+ -1 means unlimited
+ """
+ if not hasattr(self, '_sheet_quota'):
+ self._sheet_quota = 0
+ for group in self.user_groups:
+ if not group.permissions: continue
+ active_sheets = group.permissions.get("active_sheets", 0)
+ if active_sheets == -1:
+ self._sheet_quota = -1
+ return self._sheet_quota
+ self._sheet_quota = max(self._sheet_quota, active_sheets)
+
+ return self._sheet_quota
+
+ def in_group(self, group_id: str) -> bool:
+ return group_id in self.user_groups_names
+
+ def has_quota_sheet(self) -> bool:
+ """
+ checks if a user has reached their sheet quota
+ """
+ if self.sheet_quota == -1: return True
+
+ user_sheets = self.db.query(models.Sheet).filter(models.Sheet.author_id == self.email).count()
+
+ return user_sheets < self.sheet_quota
+
+ def has_quota_monthly_urls(self) -> bool:
+ """
+ checks if a user has reached their monthly url quota
+ """
+ quota = 0
+ for group in self.user_groups:
+ if not group.permissions: continue
+ monthly_urls = group.permissions.get("monthly_urls", 0)
+ if monthly_urls == -1: return True
+ quota = max(quota, monthly_urls)
+
+ current_month = datetime.now().month
+ current_year = datetime.now().year
+ user_urls = self.db.query(models.Archive).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
+ ).count()
+
+ return user_urls < quota
+
+ def has_quota_monthly_mbs(self) -> bool:
+ """
+ checks if a user has reached their monthly mb quota
+ """
+ quota = 0
+ for group in self.user_groups:
+ if not group.permissions: continue
+ monthly_mbs = group.permissions.get("monthly_mbs", 0)
+ if monthly_mbs == -1: return True
+ quota = max(quota, monthly_mbs)
+
+ 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,
+ 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) -> bool:
+ # """
+ # checks if a user is allowed to manually trigger a sheet
+ # """
+ # for group in self.user_groups:
+ # if not group.permissions: continue
+ # if group.permissions.get("manual_trigger", False):
+ # return True
+ # return False
+
+ def is_sheet_frequency_allowed(self, frequency: str) -> bool:
+ """
+ checks if a user is allowed to create a sheet with this frequency
+ """
+ return frequency in self.allowed_frequencies
diff --git a/src/endpoints/default.py b/src/endpoints/default.py
index 4569bd9..efb6cf6 100644
--- a/src/endpoints/default.py
+++ b/src/endpoints/default.py
@@ -6,8 +6,9 @@ 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
+from db.database import get_db_dependency
+from db.user_state import UserState
+from web.security import get_user_auth, bearer_security, get_active_user_state
default_router = APIRouter()
@@ -18,8 +19,7 @@ async def home(request: Request):
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)
+ status["groups"] = crud.get_user_groups(email)
except HTTPException: pass # not authenticated is fine
except Exception as e: log_error(e)
return JSONResponse(status)
@@ -31,13 +31,28 @@ async def health():
@default_router.get("/user/active", summary="Check if the user is active and can use the tool.")
+# TODO: reorder db dependencies to after auth
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)
+def get_user_groups(email=Depends(get_user_auth)) -> list[str]:
+ return crud.get_user_groups(email)
+
+
+@default_router.get("/permissions")
+def get_user_groups(
+ user: UserState = Depends(get_active_user_state),
+) -> list[str]:
+ return JSONResponse({
+ "groups": user.user_groups_names,
+ "allowedFrequencies": list(user.allowed_frequencies),
+ "sheet_quota": user.sheet_quota,
+ "monthly_urls": user.monthly_urls,
+ "monthly_mbs": user.monthly_mbs,
+ #TODO: should this return
+ })
@default_router.get('/favicon.ico', include_in_schema=False)
diff --git a/src/endpoints/sheet.py b/src/endpoints/sheet.py
index 257ed92..a37ccb2 100644
--- a/src/endpoints/sheet.py
+++ b/src/endpoints/sheet.py
@@ -5,7 +5,8 @@ from fastapi.responses import JSONResponse
from sqlalchemy import exc
from sqlalchemy.orm import Session
-from web.security import token_api_key_auth, get_active_user_auth
+from db.user_state import UserState
+from web.security import token_api_key_auth, get_active_user_auth, get_active_user_state
from db import schemas, crud
from db.database import get_db_dependency
from worker.main import create_sheet_task
@@ -16,19 +17,21 @@ sheet_router = APIRouter(prefix="/sheet", tags=["Google Spreadsheet operations"]
@sheet_router.post("/create", status_code=201, summary="Store a new Google Sheet for regular archiving.")
def create_sheet(
sheet: schemas.SheetAdd,
- email=Depends(get_active_user_auth),
+ user: UserState = Depends(get_active_user_state),
db: Session = Depends(get_db_dependency),
) -> schemas.SheetResponse:
- user_groups_names = crud.get_user_groups(db, email)
- if sheet.group_id not in user_groups_names:
+ if not user.in_group(sheet.group_id):
raise HTTPException(status_code=403, detail="User does not have access to this group.")
- if not crud.has_quota_sheet(db, email, user_groups_names):
+ if not user.has_quota_sheet():
raise HTTPException(status_code=429, detail="User has reached their sheet quota.")
+
+ if not user.is_sheet_frequency_allowed(sheet.frequency):
+ raise HTTPException(status_code=422, detail=f"Invalid frequency: {sheet.frequency}. Must be one of {user.allowed_frequencies}")
try:
- return crud.create_sheet(db, sheet.id, sheet.name, email, sheet.group_id, sheet.frequency)
+ 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 already exists.") from e
@@ -56,22 +59,30 @@ def delete_sheet(
@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,
- email=Depends(get_active_user_auth),
+ user: UserState = Depends(get_active_user_state),
db: Session = Depends(get_db_dependency),
) -> schemas.Task:
+
+ #TODO: are we enabling manual triggers?
+ # if not user.can_manually_trigger():
+ # raise HTTPException(status_code=429, detail="User cannot manually trigger archiving tasks.")
- sheet = crud.get_user_sheet(db, email, sheet_id=id)
+ 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.")
- task = create_sheet_task.delay(schemas.SubmitSheet(sheet_id=id, author_id=email, group=sheet.group_id).model_dump_json())
+ # TODO: what happens if user is taken out of group after sheet is created? this should be checked in a cronjob that notifies the user
+ if not user.in_group(sheet.group_id):
+ raise HTTPException(status_code=403, detail="User does not have access to this group.")
+
+ task = create_sheet_task.delay(schemas.SubmitSheet(sheet_id=id, author_id=user.email, group=sheet.group_id).model_dump_json())
return JSONResponse({"id": task.id}, status_code=201)
@sheet_router.post("/archive", status_code=201, summary="Trigger an archiving task for any GSheet with an API token.", response_description="task_id for the archiving task.")
def archive_sheet(
- sheet: schemas.SubmitSheet, #TODO: replace with simpler model
+ sheet: schemas.SubmitSheet, # TODO: replace with simpler model
auth=Depends(token_api_key_auth)
) -> schemas.Task:
sheet.author_id = sheet.author_id or "api-endpoint"
diff --git a/src/endpoints/url.py b/src/endpoints/url.py
index b17b082..f8e7154 100644
--- a/src/endpoints/url.py
+++ b/src/endpoints/url.py
@@ -8,7 +8,7 @@ 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 db.database import get_db, get_db_dependency
from worker.main import create_archive_task
@@ -16,14 +16,28 @@ 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())
+def archive_url(
+ archive: schemas.ArchiveTrigger,
+ email=Depends(get_token_or_user_auth)
+) -> schemas.Task:
+ logger.info(f"new {archive.public=} task for {email=} and {archive.group_id=}: {archive.url}")
+
+ # TODO: implement quota
+
+ if archive.group_id:
+ with get_db() as db:
+ if not crud.is_user_in_group(db, email, archive.group_id):
+ raise HTTPException(status_code=403, detail="User does not have access to this group.")
+
+ # TODO: deprecate ArchiveCreate
+ backwards_compatible_archive = schemas.ArchiveCreate(
+ url=archive.url,
+ author_id=email,
+ group_id=archive.group_id,
+ public=archive.public,
+ )
+
+ task = create_archive_task.delay(backwards_compatible_archive.model_dump_json())
task_response = schemas.Task(id=task.id)
return JSONResponse(task_response.model_dump(), status_code=201)
diff --git a/src/tests/conftest.py b/src/tests/conftest.py
index 6188d9f..8091062 100644
--- a/src/tests/conftest.py
+++ b/src/tests/conftest.py
@@ -2,6 +2,7 @@ import os
from fastapi.testclient import TestClient
import pytest
from unittest.mock import patch
+from db.user_state import UserState
from shared.settings import Settings
@@ -27,7 +28,9 @@ def mock_settings():
def test_db(get_settings: Settings):
from db.database import make_engine
from db import models
+ from db.crud import get_user_groups
+ get_user_groups.cache_clear()
make_engine.cache_clear()
engine = make_engine(get_settings.DATABASE_PATH)
@@ -72,11 +75,12 @@ def client(app):
@pytest.fixture()
-def app_with_auth(app):
- from web.security import get_token_or_user_auth, get_user_auth, get_active_user_auth
+def app_with_auth(app, db_session):
+ from web.security import get_token_or_user_auth, get_user_auth, get_active_user_auth, get_active_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_active_user_auth] = lambda: "morty@example.com"
+ app.dependency_overrides[get_active_user_state] = lambda: UserState(db_session, "morty@example.com", active=True)
return app
diff --git a/src/tests/db/test_crud.py b/src/tests/db/test_crud.py
index 6f838df..a465ee2 100644
--- a/src/tests/db/test_crud.py
+++ b/src/tests/db/test_crud.py
@@ -40,6 +40,12 @@ def test_data(db_session):
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
@@ -253,6 +259,7 @@ def test_count_archive_urls(test_data, db_session):
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
@@ -261,6 +268,7 @@ def test_count_users(test_data, db_session):
db_session.commit()
assert crud.count_users(db_session) == 3
+
def test_count_by_users_since(test_data, db_session):
from db import crud
@@ -294,6 +302,7 @@ def test_create_tag(db_session):
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
@@ -329,7 +338,7 @@ def test_is_user_in_group(test_data, db_session):
("jerry@example.com", "spaceship", False),
("jerry@example.com", "interdimensional", False),
- ("jerry@example.com", "the-jerrys-club", False), # group not in 'groups'
+ ("jerry@example.com", "the-jerrys-club", False), # group not in 'groups'
("rick@example.com", "animated-characters", True),
("morty@example.com", "animated-characters", True),
@@ -337,7 +346,7 @@ def test_is_user_in_group(test_data, db_session):
("ANYONE@example.com", "animated-characters", True),
("ANYONE@birdy.com", "animated-characters", True),
- ("summer@herself.com", "animated-characters", False),
+ ("summer@herself.com", "animated-characters", False),
("rick@example.com", "", False),
("", "spaceship", False),
@@ -345,7 +354,16 @@ def test_is_user_in_group(test_data, db_session):
]
for email, group, expected in test_pairs:
print(f"{email} in {group} == {expected}")
- assert crud.is_user_in_group(db_session, group, email) == expected
+ assert crud.is_user_in_group(db_session, email, group) == expected
+
+
+def test_get_group(test_data, db_session):
+ from db import crud
+
+ assert crud.get_group(db_session, "spaceship") is not None
+ assert crud.get_group(db_session, "interdimensional") is not None
+ assert crud.get_group(db_session, "animated-characters") is not None
+ assert crud.get_group(db_session, "non-existant!@#!%!") is None
def test_create_or_get_user(test_data, db_session):
@@ -403,13 +421,12 @@ def test_upsert_group(test_data, db_session):
def test_upsert_user_groups(db_session):
from db import crud
- @patch('db.crud.get_settings', new = lambda: bad_setings)
+ @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)
+ @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)
@@ -420,4 +437,54 @@ def test_upsert_user_groups(db_session):
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
+ test_broken_yaml(db_session)
+
+
+def test_create_sheet(db_session):
+ from db import crud
+
+ 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):
+ from db import crud
+
+ 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):
+ from db import crud
+
+ 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):
+ from db import crud
+
+ 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
+
diff --git a/src/tests/endpoints/test_sheet.py b/src/tests/endpoints/test_sheet.py
index ef56361..e9949a2 100644
--- a/src/tests/endpoints/test_sheet.py
+++ b/src/tests/endpoints/test_sheet.py
@@ -15,7 +15,7 @@ def test_endpoints_no_auth(client, test_no_auth):
test_no_auth(client.post, "/sheet/archive")
-def test_create_sheet_endpoint(app_with_auth):
+def test_create_sheet_endpoint(app_with_auth, db_session):
client_with_auth = TestClient(app_with_auth)
good_data = {
"id": "123-sheet-id",
@@ -53,13 +53,23 @@ def test_create_sheet_endpoint(app_with_auth):
assert response.status_code == 403
assert response.json() == {"detail": "User does not have access to this group."}
- # bad quota
+ # switch to jerry who's got less quota/permissions
+ from web.security import get_active_user_state
+ from db.user_state import UserState
+ app_with_auth.dependency_overrides[get_active_user_state] = lambda: UserState(db_session, "jerry@example.com", active=True)
+ 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"
- from web.security import get_active_user_auth
- app_with_auth.dependency_overrides[get_active_user_auth] = lambda: "jerry@example.com"
- client_jerry = TestClient(app_with_auth)
+ response = client_jerry.post("/sheet/create", json=jerry_data)
+ assert response.status_code == 422
+ assert "Invalid frequency: hourly" in response.json()["detail"]
+ 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
@@ -144,12 +154,6 @@ def test_delete_sheet_endpoint(client_with_auth, db_session):
assert response.json() == {"id": "456-sheet-id", "deleted": False}
-# def test_archive_user_sheet_endpoint(client_with_auth):
-# response = client_with_auth.post("/sheet/123-sheet-id/archive")
-# assert response.status_code == 201
-# assert "id" in response.json()
-
-
class TestArchiveUserSheetEndpoint:
def test_token_auth(self, client_with_token, test_no_auth):
test_no_auth(client_with_token.post, "/sheet/123-sheet-id/archive")
@@ -177,6 +181,14 @@ class TestArchiveUserSheetEndpoint:
assert r.json() == {"id": "123-taskid"}
m1.assert_called_once()
+ def test_user_not_in_group(self, client_with_auth, db_session):
+ from 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."}
+
class TestTokenArchiveEndpoint:
diff --git a/src/tests/endpoints/test_url.py b/src/tests/endpoints/test_url.py
index 4506c1c..8b0f549 100644
--- a/src/tests/endpoints/test_url.py
+++ b/src/tests/endpoints/test_url.py
@@ -10,11 +10,13 @@ def test_archive_url_unauthenticated(client, test_no_auth):
@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):
+ # url is too short
response = client_with_auth.post("/url/archive", json={"url": "bad"})
assert response.status_code == 422
- assert response.json() == {'detail': 'Invalid URL received: bad'}
+ assert response.json()["detail"][0]["msg"] == 'String should have at least 5 characters'
m1.assert_not_called()
+ # valid request
response = client_with_auth.post("/url/archive", json={"url": "https://example.com"})
assert response.status_code == 201
assert response.json() == {'id': '123-456-789'}
@@ -23,6 +25,20 @@ def test_archive_url(m1, client_with_auth):
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}
+ # user is not in group
+ 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."
+
+ # user is in group
+ 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 m1.call_count == 2
+ called_val = m1.call_args.args[0]
+ assert json.loads(called_val)["group_id"] == "spaceship"
+
def test_search_by_url_unauthenticated(client, test_no_auth):
test_no_auth(client.get, "/url/search")
diff --git a/src/tests/user-groups.test.yaml b/src/tests/user-groups.test.yaml
index aa18c76..c1160c2 100644
--- a/src/tests/user-groups.test.yaml
+++ b/src/tests/user-groups.test.yaml
@@ -23,6 +23,8 @@ orchestrators:
interdimensional: tests/orchestration.test.yaml
default: tests/orchestration.test.yaml
+default_orchestrator: tests/orchestration.test.yaml
+
groups:
spaceship:
description: "The spaceship crew"
@@ -31,9 +33,9 @@ groups:
permissions:
read: ["all"]
active_sheets: -1
- monthly_urls: all
- monthly_mbs: all
- alowed_frequency: "hourly"
+ monthly_urls: -1
+ monthly_mbs: -1
+ allowed_frequency: "hourly"
interdimensional:
description: "Interdimensional travelers"
orchestrator: tests/orchestration.test.yaml
@@ -43,7 +45,7 @@ groups:
active_sheets: 5
monthly_urls: 1000
monthly_mbs: 1000
- alowed_frequency: "hourly"
+ allowed_frequency: "hourly"
animated-characters:
description: "Animated characters"
orchestrator: tests/orchestration.test.yaml
@@ -53,4 +55,4 @@ groups:
active_sheets: 1
monthly_urls: 2
monthly_mbs: 10
- alowed_frequency: "daily"
\ No newline at end of file
+ allowed_frequency: "daily"
\ No newline at end of file
diff --git a/src/tests/worker/test_worker_main.py b/src/tests/worker/test_worker_main.py
index 5818443..ffef233 100644
--- a/src/tests/worker/test_worker_main.py
+++ b/src/tests/worker/test_worker_main.py
@@ -122,14 +122,6 @@ class Test_create_sheet_task():
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
diff --git a/src/web/main.py b/src/web/main.py
index 826cc03..37f6839 100644
--- a/src/web/main.py
+++ b/src/web/main.py
@@ -131,17 +131,19 @@ def app_factory(settings = get_settings()):
@app.post("/sheet", status_code=201, deprecated=True) # DEPRECATED
- def archive_sheet(sheet: schemas.SubmitSheet, email=Depends(get_user_auth)):
+ def archive_sheet(sheet: schemas.SubmitSheet, email=Depends(get_user_auth), db: Session = Depends(get_db_dependency)):
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")
+ if not crud.is_user_in_group(db, email, sheet.group_id):
+ raise HTTPException(status_code=403, detail="User does not have access to this group.")
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)):
+ def archive_sheet_service(sheet: schemas.SubmitSheet, auth=Depends(token_api_key_auth), db: Session = Depends(get_db_dependency)):
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:
diff --git a/src/web/security.py b/src/web/security.py
index 224b86a..cbb4cae 100644
--- a/src/web/security.py
+++ b/src/web/security.py
@@ -6,6 +6,7 @@ from core.config import ALLOW_ANY_EMAIL
from shared.settings import get_settings
from db.database import get_db
from db import crud
+from db.user_state import UserState
settings = get_settings()
bearer_security = HTTPBearer()
@@ -84,3 +85,8 @@ def authenticate_user(access_token):
except Exception as e:
logger.warning(f"AUTH EXCEPTION occurred: {e}")
return False, "exception occurred"
+
+
+def get_active_user_state(email=Depends(get_active_user_auth)):
+ with get_db() as db:
+ return UserState(db, email, active=True)
\ No newline at end of file
diff --git a/src/worker/main.py b/src/worker/main.py
index 8fe97d5..ac83a54 100644
--- a/src/worker/main.py
+++ b/src/worker/main.py
@@ -1,4 +1,5 @@
+from functools import lru_cache
import traceback, yaml, datetime
from typing import List, Set
@@ -30,6 +31,7 @@ Rdis = redis.Redis.from_url(celery.conf.broker_url)
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=}")
+ #TODO: move group checks out of here
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
@@ -64,10 +66,6 @@ def create_sheet_task(self, sheet_json: str):
sheet.tags.add("gsheet")
logger.info(f"SHEET START {sheet=}")
- #TODO: should this check live here?
- 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
# TODO: drop sheet_name and use only sheet_id (new endpoints/models)
@@ -161,7 +159,7 @@ def is_group_invalid_for_user(public: bool, group_id: str, author_id: str):
# otherwise group must match
with get_db() as session:
- if not crud.is_user_in_group(session, group_id, author_id):
+ if not crud.is_user_in_group(session, author_id, group_id):
logger.error(em := f"User {author_id} is not part of {group_id}, no permission")
return em
return False
@@ -220,3 +218,13 @@ def at_start(sender, **kwargs):
ORCHESTRATORS = {}
load_orchestrators()
logger.info("Orchestrators loaded successfully.")
+
+@lru_cache
+def get_url_orchestrator(group_name):
+ with get_db() as db:
+ group = crud.get_group(db, group_name)
+ assert group, f"Group {group_name} not found"
+
+ # config = Config()
+ # config.parse(use_cli=False, yaml_config_filename=group.orchestrator_sheet)
+ # return ArchivingOrchestrator(config)
\ No newline at end of file
From f8383bc6a314ce52c29227391fc0146728df7b7b Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Wed, 22 Jan 2025 13:59:44 +0000
Subject: [PATCH 06/75] method renaming
---
src/db/user_state.py | 2 +-
src/endpoints/sheet.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/db/user_state.py b/src/db/user_state.py
index afcb042..691574a 100644
--- a/src/db/user_state.py
+++ b/src/db/user_state.py
@@ -62,7 +62,7 @@ class UserState:
def in_group(self, group_id: str) -> bool:
return group_id in self.user_groups_names
- def has_quota_sheet(self) -> bool:
+ def has_quota_monthly_sheets(self) -> bool:
"""
checks if a user has reached their sheet quota
"""
diff --git a/src/endpoints/sheet.py b/src/endpoints/sheet.py
index a37ccb2..263966a 100644
--- a/src/endpoints/sheet.py
+++ b/src/endpoints/sheet.py
@@ -24,7 +24,7 @@ def create_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.has_quota_sheet():
+ if not user.has_quota_monthly_sheets():
raise HTTPException(status_code=429, detail="User has reached their sheet quota.")
if not user.is_sheet_frequency_allowed(sheet.frequency):
From c737368f41a2899b5b76726094f1379fc31096a7 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Thu, 23 Jan 2025 14:12:12 +0000
Subject: [PATCH 07/75] disable endpoint until we find whether it is used or
not
---
src/endpoints/url.py | 14 +++++++-------
src/tests/endpoints/test_url.py | 27 ++++++++++++++-------------
2 files changed, 21 insertions(+), 20 deletions(-)
diff --git a/src/endpoints/url.py b/src/endpoints/url.py
index f8e7154..58cf3c4 100644
--- a/src/endpoints/url.py
+++ b/src/endpoints/url.py
@@ -56,13 +56,13 @@ def search_by_url(
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
+# TODO: find out where/if this is used, tests are also disabled
+# @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.")
diff --git a/src/tests/endpoints/test_url.py b/src/tests/endpoints/test_url.py
index 8b0f549..5cccbf2 100644
--- a/src/tests/endpoints/test_url.py
+++ b/src/tests/endpoints/test_url.py
@@ -122,21 +122,22 @@ 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"}
+# # TODO: find out where/if this is used, tests are also disabled
+# 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), [], [])
+# 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"] == {}
+# 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):
From 9f8d7b31f3ac0c2b0f0708386f624b9363678714 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Mon, 3 Feb 2025 12:33:21 +0000
Subject: [PATCH 08/75] refactors user-groups definition and fixes tests
---
.gitignore | 3 +-
README.md | 49 +++++++++++
src/db/crud.py | 28 ++-----
src/db/user_state.py | 32 ++++---
src/endpoints/default.py | 4 +-
src/shared/user_groups.py | 124 ++++++++++++++++++++++++++++
src/tests/db/test_crud.py | 18 ++--
src/tests/endpoints/test_default.py | 4 +-
src/tests/endpoints/test_url.py | 9 +-
src/tests/user-groups.test.yaml | 54 ++++++++----
10 files changed, 255 insertions(+), 70 deletions(-)
create mode 100644 src/shared/user_groups.py
diff --git a/.gitignore b/.gitignore
index 1df91d8..ec92013 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,4 +23,5 @@ local_archive
local_archive_test
*db-wal
*db-shm
-copy-files.sh
\ No newline at end of file
+copy-files.sh
+.pytest_cache
\ No newline at end of file
diff --git a/README.md b/README.md
index 8582798..4a6e688 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,55 @@
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).
+## 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)
+ - domains are 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
+ - 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
+ - `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
+ - `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
+ - `monthly_urls` how many total URLs someone can archive per month (`-1` means no limit)
+ - `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
+
+
+To figure out:
+- workshop participants should be able to test this. `public`
+- how can people bring their own storage/api keys?
+- how to implement lifespan of archives? 6 months lifespan example. they should expect a way to download all archives locally.
+- how to deactivate unused sheets and notify?
+- how to mark URLs for deletion, and then do a hard delete?
+- what actions can people take:
+ - URL (P=needs permission, O=open)
+ - P archive
+ - P search
+ - O find own links
+ - DISABLED find by id
+ - P delete archive (soft)
+ - Sheets
+ - P create a new sheet
+ - O get my sheets
+ - O delete a sheet
+ - P archive a sheet now
+
+
## Development
http://localhost:8004
diff --git a/src/db/crud.py b/src/db/crud.py
index c48f6ae..1368bc0 100644
--- a/src/db/crud.py
+++ b/src/db/crud.py
@@ -8,6 +8,7 @@ from datetime import datetime, timedelta
from core.config import ALLOW_ANY_EMAIL
from db.database import get_db
from shared.settings import get_settings
+from shared.user_groups import UserGroups
from . import models, schemas
import yaml
@@ -202,13 +203,7 @@ def upsert_user_groups(db: Session):
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
+ ug = UserGroups(filename)
# delete all user-groups relationships
db.query(models.association_table_user_groups).delete()
@@ -219,33 +214,26 @@ def upsert_user_groups(db: Session):
# 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():
+ 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 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, [])))
+ for group_id, g in ug.groups.items():
+ upsert_group(db, group_id, g.description, g.orchestrator, g.orchestrator_sheet, 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.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():
+ for email, explicit_groups in ug.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}")
+ logger.info(f"EXPLICIT {display_email_pii(email)} => {explicit_groups}")
# upsert active user
db_user = upsert_user(db, email, active=True)
diff --git a/src/db/user_state.py b/src/db/user_state.py
index 691574a..99c68ac 100644
--- a/src/db/user_state.py
+++ b/src/db/user_state.py
@@ -32,14 +32,12 @@ class UserState:
@property
def allowed_frequencies(self):
- if not hasattr(self, '_allowed_frequencies'):
- self._allowed_frequencies = set()
+ if not hasattr(self, '_sheet_frequency'):
+ self._sheet_frequency = set()
for group in self.user_groups:
if not group.permissions: continue
- self._allowed_frequencies.add(group.permissions.get("allowed_frequency", None))
- if "hourly" in self._allowed_frequencies:
- self._allowed_frequencies.add("daily")
- return self._allowed_frequencies
+ self._sheet_frequency.update(group.permissions.get("sheet_frequency", None))
+ return self._sheet_frequency
@property
def sheet_quota(self):
@@ -51,11 +49,11 @@ class UserState:
self._sheet_quota = 0
for group in self.user_groups:
if not group.permissions: continue
- active_sheets = group.permissions.get("active_sheets", 0)
- if active_sheets == -1:
+ max_sheets = group.permissions.get("max_sheets", 0)
+ if max_sheets == -1:
self._sheet_quota = -1
return self._sheet_quota
- self._sheet_quota = max(self._sheet_quota, active_sheets)
+ self._sheet_quota = max(self._sheet_quota, max_sheets)
return self._sheet_quota
@@ -72,16 +70,16 @@ class UserState:
return user_sheets < self.sheet_quota
- def has_quota_monthly_urls(self) -> bool:
+ def has_quota_max_monthly_urls(self) -> bool:
"""
checks if a user has reached their monthly url quota
"""
quota = 0
for group in self.user_groups:
if not group.permissions: continue
- monthly_urls = group.permissions.get("monthly_urls", 0)
- if monthly_urls == -1: return True
- quota = max(quota, monthly_urls)
+ max_monthly_urls = group.permissions.get("max_monthly_urls", 0)
+ if max_monthly_urls == -1: return True
+ quota = max(quota, max_monthly_urls)
current_month = datetime.now().month
current_year = datetime.now().year
@@ -93,16 +91,16 @@ class UserState:
return user_urls < quota
- def has_quota_monthly_mbs(self) -> bool:
+ def has_quota_max_monthly_mbs(self) -> bool:
"""
checks if a user has reached their monthly mb quota
"""
quota = 0
for group in self.user_groups:
if not group.permissions: continue
- monthly_mbs = group.permissions.get("monthly_mbs", 0)
- if monthly_mbs == -1: return True
- quota = max(quota, monthly_mbs)
+ max_monthly_mbs = group.permissions.get("max_monthly_mbs", 0)
+ if max_monthly_mbs == -1: return True
+ quota = max(quota, max_monthly_mbs)
current_month = datetime.now().month
current_year = datetime.now().year
diff --git a/src/endpoints/default.py b/src/endpoints/default.py
index efb6cf6..62d1174 100644
--- a/src/endpoints/default.py
+++ b/src/endpoints/default.py
@@ -49,8 +49,8 @@ def get_user_groups(
"groups": user.user_groups_names,
"allowedFrequencies": list(user.allowed_frequencies),
"sheet_quota": user.sheet_quota,
- "monthly_urls": user.monthly_urls,
- "monthly_mbs": user.monthly_mbs,
+ "max_monthly_urls": user.max_monthly_urls, #TODO
+ "max_monthly_mbs": user.max_monthly_mbs, # TODO
#TODO: should this return
})
diff --git a/src/shared/user_groups.py b/src/shared/user_groups.py
new file mode 100644
index 0000000..d4ee02f
--- /dev/null
+++ b/src/shared/user_groups.py
@@ -0,0 +1,124 @@
+import yaml
+from loguru import logger
+from pydantic import BaseModel, 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] = Field(default_factory=list)
+ archive_url: bool = False
+ archive_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:
+ raise ValueError("sheet_frequency should have at least one value.")
+ 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
+
+
+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.")
+ return {k.lower().strip(): list(set([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
diff --git a/src/tests/db/test_crud.py b/src/tests/db/test_crud.py
index a465ee2..79c6894 100644
--- a/src/tests/db/test_crud.py
+++ b/src/tests/db/test_crud.py
@@ -57,8 +57,8 @@ def test_data(db_session):
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
+ assert db_session.query(models.Group).count() == 4
+ assert db_session.query(models.User).count() == 3
def test_get_archive(test_data, db_session):
@@ -263,10 +263,10 @@ def test_count_archive_urls(test_data, db_session):
def test_count_users(test_data, db_session):
from db import crud
- assert crud.count_users(db_session) == 4
+ 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) == 3
+ assert crud.count_users(db_session) == 2
def test_count_by_users_since(test_data, db_session):
@@ -313,7 +313,7 @@ def test_is_active_user(test_data, db_session):
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, "summer@herself.com") == False
assert crud.is_active_user(db_session, "rick@not-in-groups.com") == False
@@ -369,7 +369,7 @@ def test_get_group(test_data, db_session):
def test_create_or_get_user(test_data, db_session):
from db import crud
- assert db_session.query(models.User).count() == 4
+ assert db_session.query(models.User).count() == 3
# already exists
assert (u1 := crud.create_or_get_user(db_session, "rick@example.com")) is not None
@@ -386,13 +386,13 @@ def test_create_or_get_user(test_data, db_session):
assert u3.email == "not-active@example.com"
assert u3.is_active == False
- assert db_session.query(models.User).count() == 6
+ assert db_session.query(models.User).count() == 5
def test_upsert_group(test_data, db_session):
from db import crud
- assert db_session.query(models.Group).count() == 3
+ assert db_session.query(models.Group).count() == 4
repeatable_params = ["desc 1", "orch.yaml", "sheet.yaml", {"read": ["all"]}, ["example.com"]]
@@ -415,7 +415,7 @@ def test_upsert_group(test_data, db_session):
assert g3.id == "this-is-a-new-group"
assert len(g3.users) == 0
- assert db_session.query(models.Group).count() == 4
+ assert db_session.query(models.Group).count() == 5
def test_upsert_user_groups(db_session):
diff --git a/src/tests/endpoints/test_default.py b/src/tests/endpoints/test_default.py
index 9455bcb..fa371d1 100644
--- a/src/tests/endpoints/test_default.py
+++ b/src/tests/endpoints/test_default.py
@@ -128,7 +128,7 @@ async def test_prometheus_metrics(test_data, client_with_token, get_settings):
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{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
@@ -139,7 +139,7 @@ async def test_prometheus_metrics(test_data, client_with_token, get_settings):
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"} 4.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
diff --git a/src/tests/endpoints/test_url.py b/src/tests/endpoints/test_url.py
index 5cccbf2..b23b07e 100644
--- a/src/tests/endpoints/test_url.py
+++ b/src/tests/endpoints/test_url.py
@@ -5,7 +5,6 @@ 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=""))
@@ -118,11 +117,11 @@ def test_latest(client_with_auth, db_session):
assert len(response.json()) == 2
-def test_lookup_unauthenticated(client, test_no_auth):
- test_no_auth(client.get, "/url/123-456-789")
-
-
# # TODO: find out where/if this is used, tests are also disabled
+
+# 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
diff --git a/src/tests/user-groups.test.yaml b/src/tests/user-groups.test.yaml
index c1160c2..b2abf43 100644
--- a/src/tests/user-groups.test.yaml
+++ b/src/tests/user-groups.test.yaml
@@ -7,8 +7,8 @@ users:
- spaceship
jerry@example.com:
- the-jerrys-club
- summer@herself.com:
- badyemail.com:
+ # summer@herself.com:
+ # badyemail.com:
domains:
example.com:
@@ -32,27 +32,53 @@ groups:
orchestrator_sheet: tests/orchestration.test.yaml
permissions:
read: ["all"]
- active_sheets: -1
- monthly_urls: -1
- monthly_mbs: -1
- allowed_frequency: "hourly"
+ 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"
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
- allowed_frequency: "hourly"
+ archive_url: true
+ archive_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: tests/orchestration.test.yaml
orchestrator_sheet: tests/orchestration.test.yaml
permissions:
read: ["animated-characters"]
- active_sheets: 1
- monthly_urls: 2
- monthly_mbs: 10
- allowed_frequency: "daily"
\ No newline at end of file
+ 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: tests/orchestration.test.yaml
+ orchestrator_sheet: tests/orchestration.test.yaml
+ permissions:
+ read: []
+ archive_url: true
+ archive_sheet: true
+ sheet_frequency: ["daily"]
+ max_sheets: 1
+ max_archive_lifespan_months: 12
+ max_monthly_urls: 1
+ max_monthly_mbs: 1
+ priority: "low"
\ No newline at end of file
From 370ebd0c8cb9fa023d6245307fcaee1be30dacce Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Mon, 3 Feb 2025 12:33:29 +0000
Subject: [PATCH 09/75] sqlalchemy deprecation fixed
---
.../versions/89121d2c96d8_add_sheet_id_to_archive_table.py | 4 ++--
.../versions/fa012ec405b8_add_columns_to_groups_table.py | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/migrations/versions/89121d2c96d8_add_sheet_id_to_archive_table.py b/src/migrations/versions/89121d2c96d8_add_sheet_id_to_archive_table.py
index bbdbee1..3011cf6 100644
--- a/src/migrations/versions/89121d2c96d8_add_sheet_id_to_archive_table.py
+++ b/src/migrations/versions/89121d2c96d8_add_sheet_id_to_archive_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('archives')]
if 'sheet_id' not in columns:
@@ -30,7 +30,7 @@ def upgrade() -> None:
def downgrade() -> None:
conn = op.get_bind()
- inspector = Inspector.from_engine(conn)
+ 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')]
diff --git a/src/migrations/versions/fa012ec405b8_add_columns_to_groups_table.py b/src/migrations/versions/fa012ec405b8_add_columns_to_groups_table.py
index be94c98..f0577ea 100644
--- a/src/migrations/versions/fa012ec405b8_add_columns_to_groups_table.py
+++ b/src/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:
@@ -36,7 +36,7 @@ def upgrade() -> None:
def downgrade() -> None:
conn = op.get_bind()
- inspector = Inspector.from_engine(conn)
+ inspector = sa.inspect(conn)
columns = [col['name'] for col in inspector.get_columns('groups')]
column_names = ['description', 'orchestrator', 'orchestrator_sheet', 'permissions', 'domains']
From 1e872e82252028bea10c0bdadad5561c8d6d7a81 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Tue, 4 Feb 2025 14:01:39 +0000
Subject: [PATCH 10/75] deprecates user.active in favor of user.permissions
---
src/db/crud.py | 35 ++-----
src/db/models.py | 1 -
src/db/user_state.py | 98 ++++++++++++++++---
src/endpoints/default.py | 25 +++--
src/endpoints/sheet.py | 12 +--
.../a23aaf3ae930_drop_active_column.py | 34 +++++++
src/shared/user_groups.py | 2 +-
src/tests/conftest.py | 5 +-
src/tests/db/test_crud.py | 29 +-----
src/tests/endpoints/test_sheet.py | 4 +-
src/tests/web/test_security.py | 18 ----
src/web/security.py | 13 +--
12 files changed, 156 insertions(+), 120 deletions(-)
create mode 100644 src/migrations/versions/a23aaf3ae930_drop_active_column.py
diff --git a/src/db/crud.py b/src/db/crud.py
index 1368bc0..d09a4c8 100644
--- a/src/db/crud.py
+++ b/src/db/crud.py
@@ -112,17 +112,6 @@ def create_tag(db: Session, tag: str):
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, email: str, group_name: str) -> models.Group:
if email == ALLOW_ANY_EMAIL: return True
return len(group_name) and len(email) and group_name in get_user_groups(email)
@@ -131,7 +120,7 @@ def is_user_in_group(db: Session, email: str, group_name: str) -> models.Group:
@lru_cache
def get_user_groups(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. User does not need to be active.
+ 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.
"""
if not email or not len(email) or "@" not in email: return []
email = email.lower()
@@ -155,11 +144,11 @@ 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, is_active: bool = models.User.is_active.default.arg) -> models.User:
+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, is_active=is_active)
+ db_user = models.User(email=author_id)
db.add(db_user)
db.commit()
db.refresh(db_user)
@@ -182,14 +171,12 @@ def upsert_group(db: Session, group_name: str, description: str, orchestrator: s
return db_group
-def upsert_user(db: Session, email: str, active: bool):
+def upsert_user(db: Session, email: str):
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_user = models.User(email=email)
db.add(db_user)
- else:
- db_user.is_active = active
- db.commit()
+ db.commit()
return db_user
@@ -208,9 +195,6 @@ def upsert_user_groups(db: Session):
# 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)
@@ -227,7 +211,7 @@ def upsert_user_groups(db: Session):
# 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.")
+ 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
@@ -235,13 +219,12 @@ def upsert_user_groups(db: Session):
explicit_groups = explicit_groups or []
logger.info(f"EXPLICIT {display_email_pii(email)} => {explicit_groups}")
- # upsert active user
- db_user = upsert_user(db, email, active=True)
+ db_user = upsert_user(db, email)
# 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)}.")
+ 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)
diff --git a/src/db/models.py b/src/db/models.py
index afadec6..d8b12c8 100644
--- a/src/db/models.py
+++ b/src/db/models.py
@@ -73,7 +73,6 @@ 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")
diff --git a/src/db/user_state.py b/src/db/user_state.py
index 99c68ac..28074aa 100644
--- a/src/db/user_state.py
+++ b/src/db/user_state.py
@@ -1,9 +1,11 @@
+from typing import Dict, Set
import sqlalchemy
from sqlalchemy.orm import Session
from sqlalchemy import func
from db import crud, models
from datetime import datetime
+from shared.user_groups import GroupPermissions
class UserState:
@@ -11,10 +13,26 @@ class UserState:
Manage a user's state and permissions
"""
- def __init__(self, db: Session, email: str, active=False):
+ def __init__(self, db: Session, email: str):
self.db = db
self.email = email
- self.active = active
+
+ @property
+ def permissions(self) -> Dict[str, GroupPermissions]:
+ """
+ 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"] = GroupPermissions(
+ read=self.read,
+ archive_url=self.archive_url,
+ archive_sheet=self.archive_sheet,
+ )
+ for group in self.user_groups:
+ if not group.permissions: continue
+ self._permissions[group.id] = GroupPermissions(**group.permissions)
+ return self._permissions
@property
def user_groups_names(self):
@@ -31,7 +49,52 @@ class UserState:
return self._user_groups
@property
- def allowed_frequencies(self):
+ 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 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:
@@ -40,22 +103,31 @@ class UserState:
return self._sheet_frequency
@property
- def sheet_quota(self):
+ def max_sheets(self):
"""
infer the user's sheet quota from the groups
-1 means unlimited
"""
- if not hasattr(self, '_sheet_quota'):
- self._sheet_quota = 0
+ if not hasattr(self, '_max_sheets'):
+ self._max_sheets = 0
for group in self.user_groups:
if not group.permissions: continue
max_sheets = group.permissions.get("max_sheets", 0)
if max_sheets == -1:
- self._sheet_quota = -1
- return self._sheet_quota
- self._sheet_quota = max(self._sheet_quota, max_sheets)
+ self._max_sheets = -1
+ return self._max_sheets
+ self._max_sheets = max(self._max_sheets, max_sheets)
- return self._sheet_quota
+ return self._max_sheets
+
+ @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.archive_url or self.archive_sheet)
+ return self._active
def in_group(self, group_id: str) -> bool:
return group_id in self.user_groups_names
@@ -64,11 +136,11 @@ class UserState:
"""
checks if a user has reached their sheet quota
"""
- if self.sheet_quota == -1: return True
+ if self.max_sheets == -1: return True
user_sheets = self.db.query(models.Sheet).filter(models.Sheet.author_id == self.email).count()
- return user_sheets < self.sheet_quota
+ return user_sheets < self.max_sheets
def has_quota_max_monthly_urls(self) -> bool:
"""
@@ -137,4 +209,4 @@ class UserState:
"""
checks if a user is allowed to create a sheet with this frequency
"""
- return frequency in self.allowed_frequencies
+ return frequency in self.sheet_frequency
diff --git a/src/endpoints/default.py b/src/endpoints/default.py
index 62d1174..616ea8c 100644
--- a/src/endpoints/default.py
+++ b/src/endpoints/default.py
@@ -1,4 +1,5 @@
+from typing import Dict
from fastapi import APIRouter, Depends, Request, HTTPException
from fastapi.responses import FileResponse, JSONResponse
from sqlalchemy.orm import Session
@@ -8,7 +9,8 @@ from core.logging import log_error
from db import crud, schemas
from db.database import get_db_dependency
from db.user_state import UserState
-from web.security import get_user_auth, bearer_security, get_active_user_state
+from web.security import get_user_auth, bearer_security, get_user_state
+from shared.user_groups import GroupPermissions
default_router = APIRouter()
@@ -32,27 +34,22 @@ async def health():
@default_router.get("/user/active", summary="Check if the user is active and can use the tool.")
# TODO: reorder db dependencies to after auth
-async def active(db: Session = Depends(get_db_dependency), email=Depends(get_user_auth)) -> schemas.ActiveUser:
- return {"active": crud.is_active_user(db, email)}
+async def active(
+ user: UserState = Depends(get_user_state),
+) -> schemas.ActiveUser:
+ return {"active": user.active}
-@default_router.get("/groups")
+@default_router.get("/groups", deprecated=True) # DEPRECATED, only used by extension
def get_user_groups(email=Depends(get_user_auth)) -> list[str]:
return crud.get_user_groups(email)
@default_router.get("/permissions")
def get_user_groups(
- user: UserState = Depends(get_active_user_state),
-) -> list[str]:
- return JSONResponse({
- "groups": user.user_groups_names,
- "allowedFrequencies": list(user.allowed_frequencies),
- "sheet_quota": user.sheet_quota,
- "max_monthly_urls": user.max_monthly_urls, #TODO
- "max_monthly_mbs": user.max_monthly_mbs, # TODO
- #TODO: should this return
- })
+ user: UserState = Depends(get_user_state),
+) -> Dict[str, GroupPermissions]:
+ return user.permissions
@default_router.get('/favicon.ico', include_in_schema=False)
diff --git a/src/endpoints/sheet.py b/src/endpoints/sheet.py
index 263966a..07d3ee3 100644
--- a/src/endpoints/sheet.py
+++ b/src/endpoints/sheet.py
@@ -6,7 +6,7 @@ from sqlalchemy import exc
from sqlalchemy.orm import Session
from db.user_state import UserState
-from web.security import token_api_key_auth, get_active_user_auth, get_active_user_state
+from web.security import token_api_key_auth, get_user_auth, get_user_state
from db import schemas, crud
from db.database import get_db_dependency
from worker.main import create_sheet_task
@@ -17,7 +17,7 @@ sheet_router = APIRouter(prefix="/sheet", tags=["Google Spreadsheet operations"]
@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_active_user_state),
+ user: UserState = Depends(get_user_state),
db: Session = Depends(get_db_dependency),
) -> schemas.SheetResponse:
@@ -28,7 +28,7 @@ def create_sheet(
raise HTTPException(status_code=429, detail="User has reached their sheet quota.")
if not user.is_sheet_frequency_allowed(sheet.frequency):
- raise HTTPException(status_code=422, detail=f"Invalid frequency: {sheet.frequency}. Must be one of {user.allowed_frequencies}")
+ raise HTTPException(status_code=422, detail=f"Invalid frequency: {sheet.frequency}. Must be one of {user.sheet_frequency}")
try:
return crud.create_sheet(db, sheet.id, sheet.name, user.email, sheet.group_id, sheet.frequency)
@@ -38,7 +38,7 @@ def create_sheet(
@sheet_router.get("/mine", status_code=200, summary="Get the authenticated user's Google Sheets.")
def get_user_sheets(
- email=Depends(get_active_user_auth),
+ email=Depends(get_user_auth),
db: Session = Depends(get_db_dependency)
) -> list[schemas.SheetResponse]:
return crud.get_user_sheets(db, email)
@@ -47,7 +47,7 @@ def get_user_sheets(
@sheet_router.delete("/{id}", summary="Delete a Google Sheet by ID.")
def delete_sheet(
id: str,
- email=Depends(get_active_user_auth),
+ email=Depends(get_user_auth),
db: Session = Depends(get_db_dependency),
) -> schemas.TaskDelete:
return JSONResponse({
@@ -59,7 +59,7 @@ def delete_sheet(
@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_active_user_state),
+ user: UserState = Depends(get_user_state),
db: Session = Depends(get_db_dependency),
) -> schemas.Task:
diff --git a/src/migrations/versions/a23aaf3ae930_drop_active_column.py b/src/migrations/versions/a23aaf3ae930_drop_active_column.py
new file mode 100644
index 0000000..912f408
--- /dev/null
+++ b/src/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/shared/user_groups.py b/src/shared/user_groups.py
index d4ee02f..12e4836 100644
--- a/src/shared/user_groups.py
+++ b/src/shared/user_groups.py
@@ -31,7 +31,7 @@ class UserGroups:
class GroupPermissions(BaseModel):
- read: Set[str] = Field(default_factory=list)
+ read: Set[str] | bool = Field(default_factory=list)
archive_url: bool = False
archive_sheet: bool = False
sheet_frequency: Set[str] = Field(default_factory=list)
diff --git a/src/tests/conftest.py b/src/tests/conftest.py
index 8091062..58ce781 100644
--- a/src/tests/conftest.py
+++ b/src/tests/conftest.py
@@ -76,11 +76,10 @@ def client(app):
@pytest.fixture()
def app_with_auth(app, db_session):
- from web.security import get_token_or_user_auth, get_user_auth, get_active_user_auth, get_active_user_state
+ from 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_active_user_auth] = lambda: "morty@example.com"
- app.dependency_overrides[get_active_user_state] = lambda: UserState(db_session, "morty@example.com", active=True)
+ app.dependency_overrides[get_user_state] = lambda: UserState(db_session, "morty@example.com")
return app
diff --git a/src/tests/db/test_crud.py b/src/tests/db/test_crud.py
index 79c6894..59b76d8 100644
--- a/src/tests/db/test_crud.py
+++ b/src/tests/db/test_crud.py
@@ -303,20 +303,6 @@ def test_create_tag(db_session):
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") == False
- 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
@@ -363,7 +349,7 @@ def test_get_group(test_data, db_session):
assert crud.get_group(db_session, "spaceship") is not None
assert crud.get_group(db_session, "interdimensional") is not None
assert crud.get_group(db_session, "animated-characters") is not None
- assert crud.get_group(db_session, "non-existant!@#!%!") is None
+ assert crud.get_group(db_session, "non-existent!@#!%!") is None
def test_create_or_get_user(test_data, db_session):
@@ -374,19 +360,12 @@ def test_create_or_get_user(test_data, db_session):
# 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
+ # new user
+ assert (u2 := crud.create_or_get_user(db_session, "beth@example.com")) 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() == 5
+ assert db_session.query(models.User).count() == 4
def test_upsert_group(test_data, db_session):
diff --git a/src/tests/endpoints/test_sheet.py b/src/tests/endpoints/test_sheet.py
index e9949a2..71df69d 100644
--- a/src/tests/endpoints/test_sheet.py
+++ b/src/tests/endpoints/test_sheet.py
@@ -54,9 +54,9 @@ def test_create_sheet_endpoint(app_with_auth, db_session):
assert response.json() == {"detail": "User does not have access to this group."}
# switch to jerry who's got less quota/permissions
- from web.security import get_active_user_state
+ from web.security import get_user_state
from db.user_state import UserState
- app_with_auth.dependency_overrides[get_active_user_state] = lambda: UserState(db_session, "jerry@example.com", active=True)
+ app_with_auth.dependency_overrides[get_user_state] = lambda: UserState(db_session, "jerry@example.com")
client_jerry = TestClient(app_with_auth)
# frequency not allowed
diff --git a/src/tests/web/test_security.py b/src/tests/web/test_security.py
index f82874c..c7427d1 100644
--- a/src/tests/web/test_security.py
+++ b/src/tests/web/test_security.py
@@ -40,24 +40,6 @@ async def test_get_user_auth(m1):
assert await get_user_auth(good_user) == "summer@example.com"
-@patch("web.security.authenticate_user", return_value=(True, "summer@example.com"))
-@pytest.mark.asyncio
-async def test_get_active_user_auth_inactive(m1, db_session):
- from web.security import get_active_user_auth
-
- # inactive at first
- creds = HTTPAuthorizationCredentials(scheme="ipsum", credentials="valid-and-good")
- with pytest.raises(HTTPException):
- await get_active_user_auth(creds)
-
- from db import models
- db_session.add(models.User(email="summer@example.com", is_active=True))
- db_session.commit()
- assert await get_active_user_auth(creds) == "summer@example.com"
-
-
-
-
@patch("web.security.secure_compare", return_value=False)
@pytest.mark.asyncio
async def test_token_api_key_auth_exception(m1):
diff --git a/src/web/security.py b/src/web/security.py
index cbb4cae..772fcbd 100644
--- a/src/web/security.py
+++ b/src/web/security.py
@@ -57,15 +57,6 @@ async def get_user_auth(credentials: HTTPAuthorizationCredentials = Depends(bear
)
-async def get_active_user_auth(credentials: HTTPAuthorizationCredentials = Depends(bearer_security)):
- # validates Bearer token and Active User status
- email = await get_user_auth(credentials)
- with get_db() as db:
- if crud.is_active_user(db, email):
- return email
- raise HTTPException(status_code=403, detail="User is not active")
-
-
def authenticate_user(access_token):
# https://cloud.google.com/docs/authentication/token-types#access
if type(access_token) != str or len(access_token) < 10: return False, "invalid access_token"
@@ -87,6 +78,6 @@ def authenticate_user(access_token):
return False, "exception occurred"
-def get_active_user_state(email=Depends(get_active_user_auth)):
+def get_user_state(email=Depends(get_user_auth)):
with get_db() as db:
- return UserState(db, email, active=True)
\ No newline at end of file
+ return UserState(db, email)
\ No newline at end of file
From 8bd7e5e590fc32d9866ee68e1ddf206428ac8ae0 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Tue, 4 Feb 2025 14:05:59 +0000
Subject: [PATCH 11/75] drops /groups endpoint, no longer used
---
src/endpoints/default.py | 7 +------
src/tests/endpoints/test_default.py | 28 +++-------------------------
2 files changed, 4 insertions(+), 31 deletions(-)
diff --git a/src/endpoints/default.py b/src/endpoints/default.py
index 616ea8c..1b8dc61 100644
--- a/src/endpoints/default.py
+++ b/src/endpoints/default.py
@@ -40,13 +40,8 @@ async def active(
return {"active": user.active}
-@default_router.get("/groups", deprecated=True) # DEPRECATED, only used by extension
-def get_user_groups(email=Depends(get_user_auth)) -> list[str]:
- return crud.get_user_groups(email)
-
-
@default_router.get("/permissions")
-def get_user_groups(
+def get_user_permissions(
user: UserState = Depends(get_user_state),
) -> Dict[str, GroupPermissions]:
return user.permissions
diff --git a/src/tests/endpoints/test_default.py b/src/tests/endpoints/test_default.py
index fa371d1..da0385a 100644
--- a/src/tests/endpoints/test_default.py
+++ b/src/tests/endpoints/test_default.py
@@ -5,7 +5,6 @@ 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
@@ -55,6 +54,7 @@ def test_endpoint_active_true_user(client_with_auth):
assert r.status_code == 200
assert r.json() == {"active": True}
+
def test_endpoint_active_false_user(app):
from web.security import get_user_auth
@@ -66,30 +66,6 @@ def test_endpoint_active_false_user(app):
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
@@ -104,9 +80,11 @@ def test_favicon(client_with_auth):
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
From 7f5211f0ca8439a7210c565408940d5f86ad8b69 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Tue, 4 Feb 2025 14:13:58 +0000
Subject: [PATCH 12/75] improves documentation
---
src/endpoints/default.py | 4 ++--
src/web/main.py | 1 +
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/endpoints/default.py b/src/endpoints/default.py
index 1b8dc61..6a70a0d 100644
--- a/src/endpoints/default.py
+++ b/src/endpoints/default.py
@@ -33,14 +33,14 @@ async def health():
@default_router.get("/user/active", summary="Check if the user is active and can use the tool.")
-# TODO: reorder db dependencies to after auth
async def active(
user: UserState = Depends(get_user_state),
) -> schemas.ActiveUser:
return {"active": user.active}
-@default_router.get("/permissions")
+# TODO: test
+@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, GroupPermissions]:
diff --git a/src/web/main.py b/src/web/main.py
index 37f6839..f2020f0 100644
--- a/src/web/main.py
+++ b/src/web/main.py
@@ -53,6 +53,7 @@ def app_factory(settings = get_settings()):
# 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)])
+ # TODO: recheck this for security, currently only needed for when local_storage is used
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", ".")
From 809438fbb9f4156b7b879505dbe8849f6fe6c705 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Tue, 4 Feb 2025 15:40:20 +0000
Subject: [PATCH 13/75] introduces user.read_public drops unused endpoints
---
src/db/crud.py | 5 +--
src/db/user_state.py | 19 +++++++++--
src/endpoints/url.py | 54 ++++++++++++++++---------------
src/shared/user_groups.py | 4 +--
src/tests/conftest.py | 2 +-
src/tests/db/test_crud.py | 9 +++---
src/tests/endpoints/test_url.py | 56 ---------------------------------
src/tests/user-groups.test.yaml | 10 +++---
src/tests/web/test_main.py | 4 +--
src/web/security.py | 2 +-
10 files changed, 61 insertions(+), 104 deletions(-)
diff --git a/src/db/crud.py b/src/db/crud.py
index d09a4c8..d9e27d9 100644
--- a/src/db/crud.py
+++ b/src/db/crud.py
@@ -22,7 +22,6 @@ def get_limit(user_limit: int):
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(email)
@@ -34,7 +33,6 @@ def search_archives_by_url(db: Session, url: str, email: str, skip: int = 0, lim
# 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(email)
query = query.filter(or_(models.Archive.public == True, models.Archive.author_id == email, models.Archive.group_id.in_(groups)))
if absolute_search:
@@ -49,7 +47,6 @@ def search_archives_by_url(db: Session, url: str, email: str, skip: int = 0, lim
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()
@@ -123,7 +120,6 @@ def get_user_groups(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.
"""
if not email or not len(email) or "@" not in email: return []
- email = email.lower()
with get_db() as db:
# get user groups
@@ -172,6 +168,7 @@ def upsert_group(db: Session, group_name: str, description: str, orchestrator: s
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)
diff --git a/src/db/user_state.py b/src/db/user_state.py
index 28074aa..e3af199 100644
--- a/src/db/user_state.py
+++ b/src/db/user_state.py
@@ -15,7 +15,7 @@ class UserState:
def __init__(self, db: Session, email: str):
self.db = db
- self.email = email
+ self.email = email.lower()
@property
def permissions(self) -> Dict[str, GroupPermissions]:
@@ -26,6 +26,7 @@ class UserState:
self._permissions = {}
self._permissions["all"] = GroupPermissions(
read=self.read,
+ read_public=self.read_public,
archive_url=self.archive_url,
archive_sheet=self.archive_sheet,
)
@@ -65,6 +66,20 @@ class UserState:
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:
"""
@@ -126,7 +141,7 @@ class UserState:
A user is active if they can read/archive anything
"""
if not hasattr(self, '_active'):
- self._active = bool(self.read or self.archive_url or self.archive_sheet)
+ self._active = bool(self.read or self.read_public or self.archive_url or self.archive_sheet)
return self._active
def in_group(self, group_id: str) -> bool:
diff --git a/src/endpoints/url.py b/src/endpoints/url.py
index 58cf3c4..d32e6f6 100644
--- a/src/endpoints/url.py
+++ b/src/endpoints/url.py
@@ -4,11 +4,13 @@ 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 core.config import ALLOW_ANY_EMAIL
+from db.user_state import UserState
+from web.security import get_token_or_user_auth, get_user_state
from sqlalchemy.orm import Session
from db import crud, schemas
-from db.database import get_db, get_db_dependency
+from db.database import get_db_dependency
from worker.main import create_archive_task
@@ -18,16 +20,19 @@ 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.ArchiveTrigger,
- email=Depends(get_token_or_user_auth)
+ email=Depends(get_token_or_user_auth),
+ db: Session = Depends(get_db_dependency)
) -> schemas.Task:
logger.info(f"new {archive.public=} task for {email=} and {archive.group_id=}: {archive.url}")
- # TODO: implement quota
-
- if archive.group_id:
- with get_db() as db:
- if not crud.is_user_in_group(db, email, archive.group_id):
- raise HTTPException(status_code=403, detail="User does not have access to this group.")
+ if email != ALLOW_ANY_EMAIL:
+ user = UserState(db, email)
+ if not user.has_quota_max_monthly_urls():
+ raise HTTPException(status_code=429, detail="User has reached their monthly URL quota.")
+ if not user.has_quota_max_monthly_mbs():
+ raise HTTPException(status_code=429, detail="User has reached their monthly MB quota.")
+ 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.")
# TODO: deprecate ArchiveCreate
backwards_compatible_archive = schemas.ArchiveCreate(
@@ -47,28 +52,25 @@ 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)
+ email: str = Depends(get_token_or_user_auth)
) -> list[schemas.ArchiveResult]:
+
+ 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.")
+
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)
-
-# TODO: find out where/if this is used, tests are also disabled
-# @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}")
+def delete_task(
+ id:str,
+ user: UserState = Depends(get_user_state),
+ db: Session = Depends(get_db_dependency)
+) -> schemas.TaskDelete:
+ logger.info(f"deleting url archive task {id} request by {user.email}")
return JSONResponse({
"id": id,
- "deleted": crud.soft_delete_task(db, id, email)
+ "deleted": crud.soft_delete_task(db, id, user.email)
})
diff --git a/src/shared/user_groups.py b/src/shared/user_groups.py
index 12e4836..71a9216 100644
--- a/src/shared/user_groups.py
+++ b/src/shared/user_groups.py
@@ -32,6 +32,7 @@ class UserGroups:
class GroupPermissions(BaseModel):
read: Set[str] | bool = Field(default_factory=list)
+ read_public: bool = False
archive_url: bool = False
archive_sheet: bool = False
sheet_frequency: Set[str] = Field(default_factory=list)
@@ -49,8 +50,7 @@ class GroupPermissions(BaseModel):
@field_validator('sheet_frequency', mode='before')
def validate_sheet_frequency(cls, v):
- if not v:
- raise ValueError("sheet_frequency should have at least one value.")
+ if not v: return []
allowed = ["daily", "hourly"]
for k in v:
if k not in allowed:
diff --git a/src/tests/conftest.py b/src/tests/conftest.py
index 58ce781..854bd20 100644
--- a/src/tests/conftest.py
+++ b/src/tests/conftest.py
@@ -79,7 +79,7 @@ def app_with_auth(app, db_session):
from 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")
+ app.dependency_overrides[get_user_state] = lambda: UserState(db_session, "MORTY@example.com")
return app
diff --git a/src/tests/db/test_crud.py b/src/tests/db/test_crud.py
index 59b76d8..9517bd2 100644
--- a/src/tests/db/test_crud.py
+++ b/src/tests/db/test_crud.py
@@ -145,7 +145,6 @@ def test_search_archives_by_email(test_data, db_session):
# 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
@@ -314,7 +313,7 @@ def test_is_user_in_group(test_data, db_session):
("rick@example.com", "spaceship", True),
("rick@example.com", "SPACESHIP", False),
- ("RICK@example.com", "interdimensional", True),
+ ("rick@example.com", "interdimensional", True),
("rick@example.com", "animated-characters", True),
("rick@example.com", "the-jerrys-club", False),
@@ -329,14 +328,14 @@ def test_is_user_in_group(test_data, db_session):
("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),
+ ("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),
+ ("bademailexample.com", "spaceship", False),
]
for email, group, expected in test_pairs:
print(f"{email} in {group} == {expected}")
diff --git a/src/tests/endpoints/test_url.py b/src/tests/endpoints/test_url.py
index b23b07e..282769d 100644
--- a/src/tests/endpoints/test_url.py
+++ b/src/tests/endpoints/test_url.py
@@ -83,62 +83,6 @@ def test_search_by_url(client_with_auth, db_session):
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
-
-
-# # TODO: find out where/if this is used, tests are also disabled
-
-# 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")
diff --git a/src/tests/user-groups.test.yaml b/src/tests/user-groups.test.yaml
index b2abf43..4e33cbd 100644
--- a/src/tests/user-groups.test.yaml
+++ b/src/tests/user-groups.test.yaml
@@ -75,10 +75,10 @@ groups:
permissions:
read: []
archive_url: true
- archive_sheet: true
- sheet_frequency: ["daily"]
- max_sheets: 1
+ archive_sheet: false
+ sheet_frequency: []
+ max_sheets: 0
max_archive_lifespan_months: 12
- max_monthly_urls: 1
- max_monthly_mbs: 1
+ max_monthly_urls: 10
+ max_monthly_mbs: 50
priority: "low"
\ No newline at end of file
diff --git a/src/tests/web/test_main.py b/src/tests/web/test_main.py
index e880311..7e3b77e 100644
--- a/src/tests/web/test_main.py
+++ b/src/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("endpoints.default.crud.soft_delete_task", side_effect=Exception('mocked error'))
def test_logging_middleware(m1, client_with_auth):
from 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
diff --git a/src/web/security.py b/src/web/security.py
index 772fcbd..85ceae4 100644
--- a/src/web/security.py
+++ b/src/web/security.py
@@ -48,7 +48,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,
From a9aae77726c0aaa45873c3d957ca11ef72d13254 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Tue, 4 Feb 2025 16:18:36 +0000
Subject: [PATCH 14/75] adds tests to url/archive endpoint
---
src/endpoints/url.py | 2 +-
src/migrations/README | 1 -
src/tests/conftest.py | 6 ++++--
src/tests/endpoints/test_url.py | 27 ++++++++++++++++++++++++++-
src/tests/user-groups.test.yaml | 14 +++++++-------
5 files changed, 38 insertions(+), 12 deletions(-)
delete mode 100644 src/migrations/README
diff --git a/src/endpoints/url.py b/src/endpoints/url.py
index d32e6f6..2578b5e 100644
--- a/src/endpoints/url.py
+++ b/src/endpoints/url.py
@@ -33,7 +33,7 @@ def archive_url(
raise HTTPException(status_code=429, detail="User has reached their monthly MB quota.")
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.")
-
+
# TODO: deprecate ArchiveCreate
backwards_compatible_archive = schemas.ArchiveCreate(
url=archive.url,
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/tests/conftest.py b/src/tests/conftest.py
index 854bd20..89160c1 100644
--- a/src/tests/conftest.py
+++ b/src/tests/conftest.py
@@ -2,6 +2,7 @@ import os
from fastapi.testclient import TestClient
import pytest
from unittest.mock import patch
+from core.config import ALLOW_ANY_EMAIL
from db.user_state import UserState
from shared.settings import Settings
@@ -91,8 +92,9 @@ def client_with_auth(app_with_auth):
@pytest.fixture()
def app_with_token(app):
- from web.security import token_api_key_auth
- app.dependency_overrides[token_api_key_auth] = lambda: "jerry@example.com"
+ from 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
diff --git a/src/tests/endpoints/test_url.py b/src/tests/endpoints/test_url.py
index 282769d..ee4f134 100644
--- a/src/tests/endpoints/test_url.py
+++ b/src/tests/endpoints/test_url.py
@@ -1,5 +1,5 @@
import json
-from unittest.mock import patch
+from unittest.mock import MagicMock, patch
from db.schemas import ArchiveCreate, TaskResult
@@ -38,6 +38,31 @@ def test_archive_url(m1, client_with_auth):
called_val = m1.call_args.args[0]
assert json.loads(called_val)["group_id"] == "spaceship"
+def test_archive_url_quotas(client_with_auth):
+ m_user_state = MagicMock()
+
+ # misses on monthly URLs quota
+ m_user_state.has_quota_max_monthly_urls.return_value = False
+ with patch("endpoints.url.UserState", return_value=m_user_state):
+ 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
+ with patch("endpoints.url.UserState", return_value=m_user_state):
+ 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("worker.main.create_archive_task.delay", return_value=TaskResult(id="123-456-789", status="PENDING", result=""))
+def test_archive_url_with_api_token(m1, client_with_token):
+ response = client_with_token.post("/url/archive", json={"url": "https://example.com"})
+ assert response.status_code == 201
+ assert response.json() == {'id': '123-456-789'}
def test_search_by_url_unauthenticated(client, test_no_auth):
test_no_auth(client.get, "/url/search")
diff --git a/src/tests/user-groups.test.yaml b/src/tests/user-groups.test.yaml
index 4e33cbd..80c96ac 100644
--- a/src/tests/user-groups.test.yaml
+++ b/src/tests/user-groups.test.yaml
@@ -73,12 +73,12 @@ groups:
orchestrator: tests/orchestration.test.yaml
orchestrator_sheet: tests/orchestration.test.yaml
permissions:
- read: []
+ # read: []
archive_url: true
- archive_sheet: false
- sheet_frequency: []
- max_sheets: 0
- max_archive_lifespan_months: 12
- max_monthly_urls: 10
- max_monthly_mbs: 50
+ # 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
From 73968eafc43d947163346d493c570e237296beca Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Tue, 4 Feb 2025 16:31:24 +0000
Subject: [PATCH 15/75] adds final tests for endpoints.url
---
src/tests/endpoints/test_url.py | 35 ++++++++++++++++++++++-----------
1 file changed, 24 insertions(+), 11 deletions(-)
diff --git a/src/tests/endpoints/test_url.py b/src/tests/endpoints/test_url.py
index ee4f134..ee777f5 100644
--- a/src/tests/endpoints/test_url.py
+++ b/src/tests/endpoints/test_url.py
@@ -38,24 +38,24 @@ def test_archive_url(m1, client_with_auth):
called_val = m1.call_args.args[0]
assert json.loads(called_val)["group_id"] == "spaceship"
-def test_archive_url_quotas(client_with_auth):
+@patch("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
- with patch("endpoints.url.UserState", return_value=m_user_state):
- 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."
+ 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
- with patch("endpoints.url.UserState", return_value=m_user_state):
- 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."
+ 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("worker.main.create_archive_task.delay", return_value=TaskResult(id="123-456-789", status="PENDING", result=""))
@@ -67,8 +67,7 @@ def test_archive_url_with_api_token(m1, client_with_token):
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):
+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
@@ -107,6 +106,20 @@ def test_search_by_url(client_with_auth, db_session):
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("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")
From 2b8c48af1bd134a966a7144792caae1132dfe64b Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Tue, 4 Feb 2025 19:08:08 +0000
Subject: [PATCH 16/75] introduces user.manually_trigger_sheet and implements
quotas for sheets
---
src/db/schemas.py | 9 +----
src/db/user_state.py | 60 +++++++++++++------------------
src/endpoints/sheet.py | 31 ++++++++--------
src/shared/user_groups.py | 4 ++-
src/tests/endpoints/test_sheet.py | 43 +++++++++++-----------
src/tests/user-groups.test.yaml | 3 ++
6 files changed, 68 insertions(+), 82 deletions(-)
diff --git a/src/db/schemas.py b/src/db/schemas.py
index a03cb5a..9c26a78 100644
--- a/src/db/schemas.py
+++ b/src/db/schemas.py
@@ -84,7 +84,7 @@ class TaskDelete(Task):
deleted: bool
-class ActiveUser(BaseModel):
+class ActiveUser(BaseModel):
active: bool
@@ -94,13 +94,6 @@ class SheetAdd(BaseModel):
group_id: str
frequency: str
- @field_validator('frequency')
- def validate_frequency(cls, v):
- valid_frequencies = {"hourly", "daily"}
- if v not in {"hourly", "daily"}:
- raise ValueError(f"Invalid frequency: {v}. Must be one of {valid_frequencies}.")
- return v
-
class SheetResponse(SheetAdd):
author_id: str
diff --git a/src/db/user_state.py b/src/db/user_state.py
index e3af199..2563877 100644
--- a/src/db/user_state.py
+++ b/src/db/user_state.py
@@ -117,24 +117,6 @@ class UserState:
self._sheet_frequency.update(group.permissions.get("sheet_frequency", None))
return self._sheet_frequency
- @property
- def max_sheets(self):
- """
- infer the user's sheet quota from the groups
- -1 means unlimited
- """
- if not hasattr(self, '_max_sheets'):
- self._max_sheets = 0
- for group in self.user_groups:
- if not group.permissions: continue
- max_sheets = group.permissions.get("max_sheets", 0)
- if max_sheets == -1:
- self._max_sheets = -1
- return self._max_sheets
- self._max_sheets = max(self._max_sheets, max_sheets)
-
- return self._max_sheets
-
@property
def active(self) -> bool:
"""
@@ -147,15 +129,19 @@ class UserState:
def in_group(self, group_id: str) -> bool:
return group_id in self.user_groups_names
- def has_quota_monthly_sheets(self) -> bool:
+ def has_quota_monthly_sheets(self, group_id: str) -> bool:
"""
- checks if a user has reached their sheet quota
+ checks if a user has reached their sheet quota for a given group
"""
- if self.max_sheets == -1: return True
+ if group_id not in self.permissions:
+ return False
- user_sheets = self.db.query(models.Sheet).filter(models.Sheet.author_id == self.email).count()
-
- return user_sheets < self.max_sheets
+ 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) -> bool:
"""
@@ -210,18 +196,20 @@ class UserState:
user_mbs = int(user_bytes / 1024 / 1024)
return user_mbs < quota
- # def can_manually_trigger(self) -> bool:
- # """
- # checks if a user is allowed to manually trigger a sheet
- # """
- # for group in self.user_groups:
- # if not group.permissions: continue
- # if group.permissions.get("manual_trigger", False):
- # return True
- # return False
+ 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, frequency: str) -> bool:
+ 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
+ checks if a user is allowed to create a sheet with this frequency for this group
"""
- return frequency in self.sheet_frequency
+ if group_id not in self.permissions:
+ return False
+
+ return frequency in self.permissions[group_id].sheet_frequency
diff --git a/src/endpoints/sheet.py b/src/endpoints/sheet.py
index 07d3ee3..72f37a5 100644
--- a/src/endpoints/sheet.py
+++ b/src/endpoints/sheet.py
@@ -6,7 +6,7 @@ from sqlalchemy import exc
from sqlalchemy.orm import Session
from db.user_state import UserState
-from web.security import token_api_key_auth, get_user_auth, get_user_state
+from web.security import token_api_key_auth, get_user_state
from db import schemas, crud
from db.database import get_db_dependency
from worker.main import create_sheet_task
@@ -24,35 +24,35 @@ def create_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.has_quota_monthly_sheets():
- raise HTTPException(status_code=429, detail="User has reached their sheet quota.")
-
- if not user.is_sheet_frequency_allowed(sheet.frequency):
- raise HTTPException(status_code=422, detail=f"Invalid frequency: {sheet.frequency}. Must be one of {user.sheet_frequency}")
+ 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 already exists.") from 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(
- email=Depends(get_user_auth),
+ user: UserState = Depends(get_user_state),
db: Session = Depends(get_db_dependency)
) -> list[schemas.SheetResponse]:
- return crud.get_user_sheets(db, email)
+ return crud.get_user_sheets(db, user.email)
@sheet_router.delete("/{id}", summary="Delete a Google Sheet by ID.")
def delete_sheet(
id: str,
- email=Depends(get_user_auth),
+ user: UserState = Depends(get_user_state),
db: Session = Depends(get_db_dependency),
) -> schemas.TaskDelete:
return JSONResponse({
"id": id,
- "deleted": crud.delete_sheet(db, id, email)
+ "deleted": crud.delete_sheet(db, id, user.email)
})
@@ -62,10 +62,6 @@ def archive_user_sheet(
user: UserState = Depends(get_user_state),
db: Session = Depends(get_db_dependency),
) -> schemas.Task:
-
- #TODO: are we enabling manual triggers?
- # if not user.can_manually_trigger():
- # raise HTTPException(status_code=429, detail="User cannot manually trigger archiving tasks.")
sheet = crud.get_user_sheet(db, user.email, sheet_id=id)
if not sheet:
@@ -75,6 +71,9 @@ def archive_user_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.")
+
task = create_sheet_task.delay(schemas.SubmitSheet(sheet_id=id, author_id=user.email, group=sheet.group_id).model_dump_json())
return JSONResponse({"id": task.id}, status_code=201)
@@ -82,7 +81,7 @@ def archive_user_sheet(
@sheet_router.post("/archive", status_code=201, summary="Trigger an archiving task for any GSheet with an API token.", response_description="task_id for the archiving task.")
def archive_sheet(
- sheet: schemas.SubmitSheet, # TODO: replace with simpler model
+ sheet: schemas.SubmitSheet,
auth=Depends(token_api_key_auth)
) -> schemas.Task:
sheet.author_id = sheet.author_id or "api-endpoint"
diff --git a/src/shared/user_groups.py b/src/shared/user_groups.py
index 71a9216..a846439 100644
--- a/src/shared/user_groups.py
+++ b/src/shared/user_groups.py
@@ -35,6 +35,7 @@ class GroupPermissions(BaseModel):
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
@@ -85,7 +86,8 @@ class UserGroupModel(BaseModel):
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.")
- return {k.lower().strip(): list(set([g.lower().strip() for g in v])) for k, v in v.items()}
+ # 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
diff --git a/src/tests/endpoints/test_sheet.py b/src/tests/endpoints/test_sheet.py
index 71df69d..908b9a8 100644
--- a/src/tests/endpoints/test_sheet.py
+++ b/src/tests/endpoints/test_sheet.py
@@ -37,14 +37,7 @@ def test_create_sheet_endpoint(app_with_auth, db_session):
# 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 already exists."}
-
- # bad frequency
- bad_data = good_data.copy()
- bad_data["frequency"] = "every hour"
- response = client_with_auth.post("/sheet/create", json=bad_data)
- assert response.status_code == 422
- assert "Value error, Invalid frequency: every hour. Must be one of" in response.json()["detail"][0]["msg"]
+ assert response.json() == {"detail": "Sheet with this ID is already being archived."}
# bad group
bad_data = good_data.copy()
@@ -66,16 +59,16 @@ def test_create_sheet_endpoint(app_with_auth, db_session):
jerry_data["id"] = "jerry-sheet-id"
response = client_jerry.post("/sheet/create", json=jerry_data)
assert response.status_code == 422
- assert "Invalid frequency: hourly" in response.json()["detail"]
- jerry_data["frequency"] = "daily"
+ 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."}
+ assert response.json() == {"detail": "User has reached their sheet quota for this group."}
def test_get_user_sheets_endpoint(client_with_auth, db_session):
@@ -155,6 +148,16 @@ def test_delete_sheet_endpoint(client_with_auth, db_session):
class TestArchiveUserSheetEndpoint:
+ @patch("worker.main.create_sheet_task.delay", return_value=TaskResult(id="123-taskid", status="PENDING", result=""))
+ def test_normal_flow(self, m1, client_with_auth, db_session):
+ from 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()
+ r = client_with_auth.post("/sheet/123-sheet-id/archive")
+ assert r.status_code == 201
+ assert r.json() == {"id": "123-taskid"}
+ m1.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")
@@ -171,16 +174,6 @@ class TestArchiveUserSheetEndpoint:
assert r.status_code == 403
assert r.json() == {"detail": "No access to this sheet."}
- @patch("worker.main.create_sheet_task.delay", return_value=TaskResult(id="123-taskid", status="PENDING", result=""))
- def test_normal_flow(self, m1, client_with_auth, db_session):
- from 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()
- r = client_with_auth.post("/sheet/123-sheet-id/archive")
- assert r.status_code == 201
- assert r.json() == {"id": "123-taskid"}
- m1.assert_called_once()
-
def test_user_not_in_group(self, client_with_auth, db_session):
from 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"))
@@ -189,6 +182,14 @@ class TestArchiveUserSheetEndpoint:
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 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."}
+
class TestTokenArchiveEndpoint:
diff --git a/src/tests/user-groups.test.yaml b/src/tests/user-groups.test.yaml
index 80c96ac..ccbbfec 100644
--- a/src/tests/user-groups.test.yaml
+++ b/src/tests/user-groups.test.yaml
@@ -34,6 +34,7 @@ groups:
read: ["all"]
archive_url: true
archive_sheet: true
+ manually_trigger_sheet: true
sheet_frequency: ["hourly", "daily"]
max_sheets: -1
max_archive_lifespan_months: -1
@@ -48,6 +49,7 @@ groups:
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
@@ -75,6 +77,7 @@ groups:
permissions:
# read: []
archive_url: true
+ # manually_trigger_sheet: false
# archive_sheet: false
# sheet_frequency: []
# max_sheets: 0
From 5344cc56e7503abb77f44f93188c8c0fdd9b1595 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Thu, 6 Feb 2025 18:41:12 +0000
Subject: [PATCH 17/75] introduces group/global usage & permissions, validates
in endpoints and tests endpoints
---
src/db/crud.py | 13 +-
src/db/models.py | 8 +-
src/db/schemas.py | 6 +-
src/db/user_state.py | 159 +++++++++++++++---
src/endpoints/default.py | 10 +-
src/endpoints/url.py | 13 +-
...24ec4b1_rename_sheets_last_archived_col.py | 32 ++++
src/tests/endpoints/test_sheet.py | 9 +-
src/tests/endpoints/test_url.py | 39 ++++-
src/worker/main.py | 15 +-
10 files changed, 252 insertions(+), 52 deletions(-)
create mode 100644 src/migrations/versions/1636724ec4b1_rename_sheets_last_archived_col.py
diff --git a/src/db/crud.py b/src/db/crud.py
index d9e27d9..0ede4f0 100644
--- a/src/db/crud.py
+++ b/src/db/crud.py
@@ -3,7 +3,7 @@ from functools import lru_cache
from sqlalchemy.orm import Session, load_only
from sqlalchemy import Column, or_, func
from loguru import logger
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
from core.config import ALLOW_ANY_EMAIL
from db.database import get_db
@@ -51,7 +51,7 @@ def search_archives_by_email(db: Session, email: str, skip: int = 0, limit: int
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 = models.Archive(id=task.id, url=task.url, result=task.result, public=task.public, author_id=task.author_id, group_id=task.group_id, sheet_id=task.sheet_id)
db_task.tags = tags
db_task.urls = urls
db.add(db_task)
@@ -246,8 +246,15 @@ def get_user_sheet(db: Session, email: str, sheet_id: str) -> models.Sheet:
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_archived_at.desc()).all()
+ return db.query(models.Sheet).filter(models.Sheet.author_id == email).order_by(models.Sheet.last_url_archived_at.desc()).all()
+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
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()
diff --git a/src/db/models.py b/src/db/models.py
index d8b12c8..41d2c3c 100644
--- a/src/db/models.py
+++ b/src/db/models.py
@@ -25,16 +25,15 @@ association_table_user_groups = Table(
Column("group_id", ForeignKey("groups.id")),
)
+
# data model tables
-
-
class Archive(Base):
__tablename__ = "archives"
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())
@@ -102,8 +101,9 @@ class Sheet(Base):
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 needed, is it?
stats = Column(JSON, default={}, doc="Sheet statistics like total links, total rows, ...")
- last_archived_at = Column(DateTime(timezone=True), server_default=func.now(), doc="Last time a new link was archived.")
+ 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())
diff --git a/src/db/schemas.py b/src/db/schemas.py
index 9c26a78..072d4c3 100644
--- a/src/db/schemas.py
+++ b/src/db/schemas.py
@@ -1,6 +1,6 @@
from typing import Annotated
from annotated_types import Len
-from pydantic import BaseModel, field_validator
+from pydantic import BaseModel
from datetime import datetime
@@ -21,6 +21,7 @@ class ArchiveCreate(BaseModel):
group_id: str | None = None
tags: set[Tag] | None = set()
rearchive: bool = True
+ sheet_id: str | None = None
# urls: list = []
@@ -97,9 +98,8 @@ class SheetAdd(BaseModel):
class SheetResponse(SheetAdd):
author_id: str
- stats: dict | None
- last_archived_at: datetime | None
created_at: datetime
+ last_url_archived_at: datetime | None
class ArchiveTrigger(BaseModel):
diff --git a/src/db/user_state.py b/src/db/user_state.py
index 2563877..8b3ad54 100644
--- a/src/db/user_state.py
+++ b/src/db/user_state.py
@@ -29,6 +29,11 @@ class UserState:
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
@@ -117,6 +122,34 @@ class UserState:
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", "low") == "high":
+ self._priority = "high"
+ return self._priority
+
@property
def active(self) -> bool:
"""
@@ -125,34 +158,114 @@ class UserState:
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 = {
+ (url.group_id or ""): {
+ "monthly_urls": url.url_count,
+ "monthly_mbs": int(url.total_bytes / 1024 / 1024),
+ "total_sheets": 0
+ }
+ 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] = {
+ "monthly_urls": 0,
+ "monthly_mbs": 0,
+ "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 {
+ "total_sheets": total_sheets,
+ "monthly_urls": total_urls,
+ "monthly_mbs": int(total_bytes / 1024 / 1024),
+ "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:
+ 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:
+ if sheet_quota == -1:
return True
return user_sheets < sheet_quota
- def has_quota_max_monthly_urls(self) -> bool:
+ def has_quota_max_monthly_urls(self, group_id:str) -> bool:
"""
- checks if a user has reached their monthly url quota
+ checks if a user has reached their monthly url quota for a group, if global then group should be empty string
"""
quota = 0
- for group in self.user_groups:
- if not group.permissions: continue
- max_monthly_urls = group.permissions.get("max_monthly_urls", 0)
- if max_monthly_urls == -1: return True
- quota = max(quota, max_monthly_urls)
+ 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
current_month = datetime.now().month
current_year = datetime.now().year
@@ -164,16 +277,16 @@ class UserState:
return user_urls < quota
- def has_quota_max_monthly_mbs(self) -> bool:
+ def has_quota_max_monthly_mbs(self, group_id:str) -> bool:
"""
- checks if a user has reached their monthly mb quota
+ checks if a user has reached their monthly MBs quota for a group, if global then group should be empty string
"""
quota = 0
- for group in self.user_groups:
- if not group.permissions: continue
- max_monthly_mbs = group.permissions.get("max_monthly_mbs", 0)
- if max_monthly_mbs == -1: return True
- quota = max(quota, max_monthly_mbs)
+ 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
current_month = datetime.now().month
current_year = datetime.now().year
@@ -196,20 +309,20 @@ class UserState:
user_mbs = int(user_bytes / 1024 / 1024)
return user_mbs < quota
- def can_manually_trigger(self, group_id:str) -> bool:
+ 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:
+ 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:
+ 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:
+ if group_id not in self.permissions:
return False
-
+
return frequency in self.permissions[group_id].sheet_frequency
diff --git a/src/endpoints/default.py b/src/endpoints/default.py
index 6a70a0d..d5f712f 100644
--- a/src/endpoints/default.py
+++ b/src/endpoints/default.py
@@ -39,13 +39,21 @@ async def active(
return {"active": user.active}
-# TODO: test
@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, GroupPermissions]:
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),
+):
+ 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():
diff --git a/src/endpoints/url.py b/src/endpoints/url.py
index 2578b5e..3d0aae1 100644
--- a/src/endpoints/url.py
+++ b/src/endpoints/url.py
@@ -13,6 +13,7 @@ from db import crud, schemas
from db.database import get_db_dependency
from worker.main import create_archive_task
+from urllib.parse import urlparse
url_router = APIRouter(prefix="/url", tags=["Single URL operations"])
@@ -25,14 +26,18 @@ def archive_url(
) -> schemas.Task:
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.")
+
if email != ALLOW_ANY_EMAIL:
user = UserState(db, email)
- if not user.has_quota_max_monthly_urls():
- raise HTTPException(status_code=429, detail="User has reached their monthly URL quota.")
- if not user.has_quota_max_monthly_mbs():
- raise HTTPException(status_code=429, detail="User has reached their monthly MB quota.")
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.")
# TODO: deprecate ArchiveCreate
backwards_compatible_archive = schemas.ArchiveCreate(
diff --git a/src/migrations/versions/1636724ec4b1_rename_sheets_last_archived_col.py b/src/migrations/versions/1636724ec4b1_rename_sheets_last_archived_col.py
new file mode 100644
index 0000000..6c109f3
--- /dev/null
+++ b/src/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/src/tests/endpoints/test_sheet.py b/src/tests/endpoints/test_sheet.py
index 908b9a8..129cd2a 100644
--- a/src/tests/endpoints/test_sheet.py
+++ b/src/tests/endpoints/test_sheet.py
@@ -29,8 +29,7 @@ def test_create_sheet_endpoint(app_with_auth, db_session):
assert response.status_code == 201
j = response.json()
assert datetime.fromisoformat(j.pop("created_at"))
- assert datetime.fromisoformat(j.pop("last_archived_at"))
- assert j.pop("stats") == {}
+ assert datetime.fromisoformat(j.pop("last_url_archived_at"))
assert j.pop("author_id") == 'morty@example.com'
assert j == good_data
@@ -95,16 +94,15 @@ def test_get_user_sheets_endpoint(client_with_auth, db_session):
assert isinstance(r, list)
assert len(r) == 2
assert datetime.fromisoformat(r[0].pop("created_at"))
- assert datetime.fromisoformat(r[0].pop("last_archived_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_archived_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',
- 'stats': {},
}
assert r[1] == {
'id': '456',
@@ -112,7 +110,6 @@ def test_get_user_sheets_endpoint(client_with_auth, db_session):
'frequency': 'daily',
'group_id': 'interdimensional',
'name': 'Test Sheet 2',
- 'stats': {},
}
diff --git a/src/tests/endpoints/test_url.py b/src/tests/endpoints/test_url.py
index ee777f5..af198e3 100644
--- a/src/tests/endpoints/test_url.py
+++ b/src/tests/endpoints/test_url.py
@@ -7,36 +7,67 @@ def test_archive_url_unauthenticated(client, test_no_auth):
test_no_auth(client.post, "/url/archive")
+@patch("endpoints.url.UserState")
@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):
+def test_archive_url(m1, m2, client_with_auth):
+ 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'
m1.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'}
-
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}
+ 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, "sheet_id":None}
+ m_user_state.has_quota_max_monthly_urls.assert_called_once()
+ m_user_state.has_quota_max_monthly_mbs.assert_called_once()
# 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_once_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 m1.call_count == 2
called_val = m1.call_args.args[0]
assert json.loads(called_val)["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")
@patch("endpoints.url.UserState")
def test_archive_url_quotas(m1, client_with_auth):
diff --git a/src/worker/main.py b/src/worker/main.py
index ac83a54..2896a23 100644
--- a/src/worker/main.py
+++ b/src/worker/main.py
@@ -19,6 +19,7 @@ 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
@@ -48,6 +49,7 @@ def create_archive_task(self, archive_json: str):
return Metadata.choose_most_complete([a.result for a in archives])
orchestrator = choose_orchestrator(archive.group_id, archive.author_id)
+ logger.info(f"Using orchestrator {orchestrator=}")
result = orchestrator.feed_item(Metadata().set_url(url))
try:
@@ -59,7 +61,7 @@ def create_archive_task(self, archive_json: str):
raise e
return result.to_dict()
-
+#TODO: refactor how user-groups are loaded and orchestrators chosen
@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)
@@ -79,7 +81,8 @@ def create_sheet_task(self, sheet_json: str):
continue
try:
#TODO: remove public from sheet in new refactor
- insert_result_into_db(result, sheet.tags, sheet.public, sheet.group_id, sheet.author_id, models.generate_uuid())
+ #TODO: update the sheets table with the current date if any new archive was done
+ insert_result_into_db(result, sheet.tags, sheet.public, sheet.group_id, sheet.author_id, models.generate_uuid(), sheet.sheet_id)
stats["archived"] += 1
except exc.IntegrityError as e:
logger.warning(f"cached result detected: {e}")
@@ -89,6 +92,10 @@ def create_sheet_task(self, sheet_json: str):
stats["failed"] += 1
stats["errors"].append(str(e))
+ if stats["archived"] > 0:
+ with get_db() as session:
+ crud.update_sheet_last_url_archived_at(session, sheet.sheet_id)
+
logger.info(f"SHEET DONE {sheet=}")
return {"success": True, "sheet": sheet.sheet_name, "sheet_id": sheet.sheet_id, "time": datetime.datetime.now().isoformat(), **stats}
@@ -165,7 +172,7 @@ def is_group_invalid_for_user(public: bool, group_id: str, author_id: str):
return False
-def insert_result_into_db(result: Metadata, tags: Set[str], public: bool, group_id: str, author_id: str, task_id: str) -> str:
+def insert_result_into_db(result: Metadata, tags: Set[str], public: bool, group_id: str, author_id: str, task_id: str, sheet_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:
@@ -175,7 +182,7 @@ def insert_result_into_db(result: Metadata, tags: Set[str], public: bool, group_
# 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))
+ 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, sheet_id=sheet_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
From 6471b08a4bf4434b8549b40c8d49fe13d4bdfb27 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Thu, 6 Feb 2025 18:58:35 +0000
Subject: [PATCH 18/75] adds user schemas
---
src/db/schemas.py | 8 ++++++++
src/db/user_state.py | 38 ++++++++++++++++----------------------
src/endpoints/default.py | 9 +++++----
3 files changed, 29 insertions(+), 26 deletions(-)
diff --git a/src/db/schemas.py b/src/db/schemas.py
index 072d4c3..424b9de 100644
--- a/src/db/schemas.py
+++ b/src/db/schemas.py
@@ -107,3 +107,11 @@ class ArchiveTrigger(BaseModel):
public: bool = True
group_id: Annotated[str, Len(min_length=1)] | None = None
tags: set[Tag] | None = set()
+
+class Usage(BaseModel):
+ monthly_urls: int = 0
+ monthly_mbs: int = 0
+ total_sheets: int = 0
+
+class UsageResponse(Usage):
+ groups: dict[str, Usage]
\ No newline at end of file
diff --git a/src/db/user_state.py b/src/db/user_state.py
index 8b3ad54..5b81a15 100644
--- a/src/db/user_state.py
+++ b/src/db/user_state.py
@@ -6,6 +6,7 @@ from sqlalchemy import func
from db import crud, models
from datetime import datetime
from shared.user_groups import GroupPermissions
+from db.schemas import Usage, UsageResponse
class UserState:
@@ -127,7 +128,7 @@ class UserState:
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'):
@@ -158,7 +159,7 @@ class UserState:
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
@@ -211,36 +212,29 @@ class UserState:
).group_by(models.Archive.group_id).all()
# merge the two queries
- usage_by_group = {
- (url.group_id or ""): {
- "monthly_urls": url.url_count,
- "monthly_mbs": int(url.total_bytes / 1024 / 1024),
- "total_sheets": 0
- }
+ 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
+ usage_by_group[group_id].total_sheets = sheet_count
else:
- usage_by_group[group_id] = {
- "monthly_urls": 0,
- "monthly_mbs": 0,
- "total_sheets": sheet_count
- }
+ 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 {
- "total_sheets": total_sheets,
- "monthly_urls": total_urls,
- "monthly_mbs": int(total_bytes / 1024 / 1024),
- "groups": usage_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:
"""
@@ -256,7 +250,7 @@ class UserState:
return True
return user_sheets < sheet_quota
- def has_quota_max_monthly_urls(self, group_id:str) -> bool:
+ 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
"""
@@ -277,7 +271,7 @@ class UserState:
return user_urls < quota
- def has_quota_max_monthly_mbs(self, group_id:str) -> bool:
+ 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
"""
diff --git a/src/endpoints/default.py b/src/endpoints/default.py
index d5f712f..557d7de 100644
--- a/src/endpoints/default.py
+++ b/src/endpoints/default.py
@@ -6,7 +6,8 @@ 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 import crud
+from db.schemas import ActiveUser, UsageResponse
from db.database import get_db_dependency
from db.user_state import UserState
from web.security import get_user_auth, bearer_security, get_user_state
@@ -35,7 +36,7 @@ async def health():
@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),
-) -> schemas.ActiveUser:
+) -> ActiveUser:
return {"active": user.active}
@@ -48,7 +49,7 @@ def get_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()
@@ -56,5 +57,5 @@ def get_user_usage(
@default_router.get('/favicon.ico', include_in_schema=False)
-async def favicon():
+async def favicon() -> FileResponse:
return FileResponse("static/favicon.ico")
From 90bcd44e0ac19c826e5b7a91fff5687ccd822ee5 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Thu, 6 Feb 2025 20:11:58 +0000
Subject: [PATCH 19/75] default group needs to be considered explicitly
---
src/db/user_state.py | 2 +-
src/shared/user_groups.py | 7 +++++++
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/src/db/user_state.py b/src/db/user_state.py
index 5b81a15..9ae3135 100644
--- a/src/db/user_state.py
+++ b/src/db/user_state.py
@@ -44,7 +44,7 @@ class UserState:
@property
def user_groups_names(self):
if not hasattr(self, '_user_groups_names'):
- self._user_groups_names = crud.get_user_groups(self.email)
+ self._user_groups_names = crud.get_user_groups(self.email) + ["default"]
return self._user_groups_names
@property
diff --git a/src/shared/user_groups.py b/src/shared/user_groups.py
index a846439..5d8a1ce 100644
--- a/src/shared/user_groups.py
+++ b/src/shared/user_groups.py
@@ -65,6 +65,13 @@ class GroupPermissions(BaseModel):
raise ValueError("priority must be either 'low' or 'high'.")
return v
+ @field_validator('read', mode='before')
+ def validate_priority(cls, v):
+ if type(v) == list:
+ if "default" in v:
+ raise ValueError("The 'default' group is not used for archive permissions, please remove it.")
+ return v
+
class GroupModel(BaseModel):
description: str
From 9af48efe22ad8a8a1ab2fa7edceeab6e71b6a3ed Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Thu, 6 Feb 2025 20:26:32 +0000
Subject: [PATCH 20/75] default group should actually exist
---
src/shared/user_groups.py | 8 --------
1 file changed, 8 deletions(-)
diff --git a/src/shared/user_groups.py b/src/shared/user_groups.py
index 5d8a1ce..d36ab4b 100644
--- a/src/shared/user_groups.py
+++ b/src/shared/user_groups.py
@@ -65,14 +65,6 @@ class GroupPermissions(BaseModel):
raise ValueError("priority must be either 'low' or 'high'.")
return v
- @field_validator('read', mode='before')
- def validate_priority(cls, v):
- if type(v) == list:
- if "default" in v:
- raise ValueError("The 'default' group is not used for archive permissions, please remove it.")
- return v
-
-
class GroupModel(BaseModel):
description: str
orchestrator: str
From fed8543c3072d7374aa565db1cfaa04017f4a578 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Thu, 6 Feb 2025 20:40:40 +0000
Subject: [PATCH 21/75] fix /user/active tests
---
src/endpoints/default.py | 1 -
src/tests/endpoints/test_default.py | 27 ++++++++++++++++-----------
2 files changed, 16 insertions(+), 12 deletions(-)
diff --git a/src/endpoints/default.py b/src/endpoints/default.py
index 557d7de..8838d2a 100644
--- a/src/endpoints/default.py
+++ b/src/endpoints/default.py
@@ -8,7 +8,6 @@ from core.config import VERSION, BREAKING_CHANGES
from core.logging import log_error
from db import crud
from db.schemas import ActiveUser, UsageResponse
-from db.database import get_db_dependency
from db.user_state import UserState
from web.security import get_user_auth, bearer_security, get_user_state
from shared.user_groups import GroupPermissions
diff --git a/src/tests/endpoints/test_default.py b/src/tests/endpoints/test_default.py
index da0385a..d93f35d 100644
--- a/src/tests/endpoints/test_default.py
+++ b/src/tests/endpoints/test_default.py
@@ -1,4 +1,4 @@
-from unittest.mock import AsyncMock, patch
+from unittest.mock import AsyncMock, MagicMock, patch
from fastapi.testclient import TestClient
import pytest
from core.config import VERSION
@@ -49,22 +49,27 @@ 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(app):
+ m_user_state = MagicMock()
-
-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"
+ from 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")
From eccd71d168cd90aab3f6bff27dfa8aa7911acc52 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Fri, 7 Feb 2025 16:53:33 +0000
Subject: [PATCH 22/75] implments cronjob to do the sheet archiving
---
src/core/events.py | 41 +++++++++++++++++++++++++++++++++-------
src/db/crud.py | 22 +++++++++++++++++----
src/db/database.py | 23 +++++++++++++++++++++-
src/endpoints/default.py | 2 +-
src/endpoints/sheet.py | 1 -
src/shared/settings.py | 8 +++++++-
src/utils/misc.py | 12 +++++++++++-
7 files changed, 93 insertions(+), 16 deletions(-)
diff --git a/src/core/events.py b/src/core/events.py
index e9bbfbc..03865d9 100644
--- a/src/core/events.py
+++ b/src/core/events.py
@@ -1,4 +1,5 @@
import asyncio
+import datetime
import logging
import alembic.config
from fastapi import FastAPI
@@ -6,10 +7,11 @@ 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 db import crud, models, schemas
+from db.database import get_db, get_db_async, make_engine
from shared.settings import get_settings
from utils.metrics import measure_regular_metrics, redis_subscribe_worker_exceptions
+from worker.main import create_sheet_task
@asynccontextmanager
@@ -20,13 +22,19 @@ async def lifespan(app: FastAPI):
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
+ logging.getLogger("uvicorn.access").disabled = True # loguru
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)
+ # 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.")
+
yield # separates startup from shutdown instructions
# SHUTDOWN
@@ -34,8 +42,27 @@ async def lifespan(app: FastAPI):
# CRON JOBS
-
-
-@repeat_every(seconds=get_settings().REPEAT_COUNT_METRICS_SECONDS)
+@repeat_every(seconds=get_settings().REPEAT_COUNT_METRICS_SECONDS, on_exception=logger.error)
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=logger.error)
+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=logger.error)
+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:
+ task = create_sheet_task.apply_async(args=[schemas.SubmitSheet(sheet_id=s.id, author_id=s.author_id, group=s.group_id).model_dump_json()])
+ triggered_jobs.append({"sheet_id": s.id, "task_id": task.id})
+ logger.info(f"[CRON {frequency.upper()}:{current_time_unit}] Triggered {len(triggered_jobs)} sheet tasks: {triggered_jobs}")
diff --git a/src/db/crud.py b/src/db/crud.py
index 0ede4f0..069277d 100644
--- a/src/db/crud.py
+++ b/src/db/crud.py
@@ -1,16 +1,17 @@
from collections import defaultdict
from functools import lru_cache
from sqlalchemy.orm import Session, load_only
-from sqlalchemy import Column, or_, func
+from sqlalchemy import Column, or_, func, select
from loguru import logger
-from datetime import datetime, timedelta, timezone
+from datetime import datetime, timedelta
from core.config import ALLOW_ANY_EMAIL
from db.database import get_db
from shared.settings import get_settings
from shared.user_groups import UserGroups
+from utils.misc import fnv1a_hash_mod
from . import models, schemas
-import yaml
+from sqlalchemy.ext.asyncio import AsyncSession
DATABASE_QUERY_LIMIT = get_settings().DATABASE_QUERY_LIMIT
@@ -248,6 +249,18 @@ def get_user_sheet(db: Session, email: str, sheet_id: str) -> models.Sheet:
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: str) -> 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
+
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:
@@ -256,9 +269,10 @@ def update_sheet_last_url_archived_at(db: Session, sheet_id: str):
return True
return False
+
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
\ No newline at end of file
+ return db_sheet is not None
diff --git a/src/db/database.py b/src/db/database.py
index 1166099..e72dc15 100644
--- a/src/db/database.py
+++ b/src/db/database.py
@@ -2,7 +2,8 @@ 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
+from contextlib import asynccontextmanager, contextmanager
+from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, AsyncEngine, async_sessionmaker
@lru_cache
@@ -34,3 +35,23 @@ def get_db_dependency():
# to use with Depends and ensure proper session closing
with get_db() as db:
yield db
+
+# ASYNC connections
+
+
+async def make_async_engine(database_url: str) -> AsyncEngine:
+ engine = create_async_engine(database_url, connect_args={"check_same_thread": False})
+ return engine
+
+
+async def make_async_session_local(engine: AsyncEngine) -> AsyncSession:
+ return async_sessionmaker(engine, expire_on_commit=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/endpoints/default.py b/src/endpoints/default.py
index 8838d2a..ceef172 100644
--- a/src/endpoints/default.py
+++ b/src/endpoints/default.py
@@ -17,7 +17,7 @@ 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
+ # TODO: maybe split into 2 routes: one non authenticated and one authenticated for the groups info only, necessary only for the extension
status = {"version": VERSION, "breakingChanges": BREAKING_CHANGES}
try:
email = await get_user_auth(await bearer_security(request))
diff --git a/src/endpoints/sheet.py b/src/endpoints/sheet.py
index 72f37a5..b1a4230 100644
--- a/src/endpoints/sheet.py
+++ b/src/endpoints/sheet.py
@@ -67,7 +67,6 @@ def archive_user_sheet(
if not sheet:
raise HTTPException(status_code=403, detail="No access to this sheet.")
- # TODO: what happens if user is taken out of group after sheet is created? this should be checked in a cronjob that notifies the user
if not user.in_group(sheet.group_id):
raise HTTPException(status_code=403, detail="User does not have access to this group.")
diff --git a/src/shared/settings.py b/src/shared/settings.py
index 8fa5ae5..0a2aec5 100644
--- a/src/shared/settings.py
+++ b/src/shared/settings.py
@@ -14,9 +14,15 @@ class Settings(BaseSettings):
USER_GROUPS_FILENAME: str = "user-groups.yaml"
SHEET_ORCHESTRATION_YAML : str = "secrets/orchestration-sheet.yaml"
+ # cronjobs
+ CRON_ARCHIVE_SHEETS: bool = False
+
# database
DATABASE_PATH: str
DATABASE_QUERY_LIMIT: int = 100
+ @property
+ def ASYNC_DATABASE_PATH(self) -> str:
+ return self.DATABASE_PATH.replace("sqlite://", "sqlite+aiosqlite://")
# redis
CELERY_BROKER_URL: str = "redis://localhost:6379"
@@ -30,7 +36,7 @@ class Settings(BaseSettings):
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
+ #TODO: deprecate blocklist?
BLOCKED_EMAILS: Annotated[Set[str], Len(min_length=0)] = set()
@lru_cache
diff --git a/src/utils/misc.py b/src/utils/misc.py
index f3a9803..4f94a63 100644
--- a/src/utils/misc.py
+++ b/src/utils/misc.py
@@ -4,4 +4,14 @@ 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
+ return jsonable_encoder(obj)
+
+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
From 83ba9884f62784c6dc83067d235ad5c7e84aead2 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Fri, 7 Feb 2025 17:57:02 +0000
Subject: [PATCH 23/75] auto remove stale sheets and email users
---
src/Pipfile | 1 +
src/Pipfile.lock | 49 +++++++++++++++++++++++++++++++++++-------
src/core/events.py | 42 ++++++++++++++++++++++++++++++++++++
src/db/crud.py | 12 +++++++++++
src/db/database.py | 6 +++---
src/shared/settings.py | 26 ++++++++++++++++++++++
6 files changed, 125 insertions(+), 11 deletions(-)
diff --git a/src/Pipfile b/src/Pipfile
index a8a1936..811903f 100644
--- a/src/Pipfile
+++ b/src/Pipfile
@@ -21,6 +21,7 @@ fastapi-utils = "*"
prometheus-fastapi-instrumentator = "*"
auto-archiver = "*"
pydantic-settings = "*"
+fastapi-mail = "*"
[dev-packages]
watchdog = "*"
diff --git a/src/Pipfile.lock b/src/Pipfile.lock
index 7ba5bf6..0d430ab 100644
--- a/src/Pipfile.lock
+++ b/src/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "601b24d82095b8b58f1376755d9a1c5c3bbf144aa622e1eef9fc034835f097fe"
+ "sha256": "14a7ead66a74419eebfa72478bbb7e3efe378df9f41e401738faa2871f5c4344"
},
"pipfile-spec": 6,
"requires": {
@@ -122,6 +122,14 @@
"markers": "python_version >= '3.9'",
"version": "==1.3.2"
},
+ "aiosmtplib": {
+ "hashes": [
+ "sha256:08fd840f9dbc23258025dca229e8a8f04d2ccf3ecb1319585615bfc7933f7f47",
+ "sha256:8783059603a34834c7c90ca51103c3aa129d5922003b5ce98dbaa6d4440f10fc"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==3.0.2"
+ },
"aiosqlite": {
"hashes": [
"sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6",
@@ -674,6 +682,22 @@
"markers": "python_version >= '3.7'",
"version": "==1.2.0"
},
+ "dnspython": {
+ "hashes": [
+ "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86",
+ "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1"
+ ],
+ "markers": "python_version >= '3.9'",
+ "version": "==2.7.0"
+ },
+ "email-validator": {
+ "hashes": [
+ "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631",
+ "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==2.2.0"
+ },
"exceptiongroup": {
"hashes": [
"sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b",
@@ -691,6 +715,15 @@
"markers": "python_version >= '3.8'",
"version": "==0.115.6"
},
+ "fastapi-mail": {
+ "hashes": [
+ "sha256:04bde1005c624f42dfc0a9c1e313fcc544499fdd6b3531e606c500d80ac2ffcb",
+ "sha256:3525cf342ff91f6bcb3298570d1783498082e586957f668ee4164a0aab6ec743"
+ ],
+ "index": "pypi",
+ "markers": "python_full_version >= '3.8.1' and python_version < '4.0'",
+ "version": "==1.4.2"
+ },
"fastapi-utils": {
"hashes": [
"sha256:6c4d507a76bab9a016cee0c4fa3a4638c636b2b2689e39c62254b1b2e4e81825",
@@ -1867,11 +1900,11 @@
},
"pydantic": {
"hashes": [
- "sha256:278b38dbbaec562011d659ee05f63346951b3a248a6f3642e1bc68894ea2b4ff",
- "sha256:4dd4e322dbe55472cb7ca7e73f4b63574eecccf2835ffa2af9021ce113c83c53"
+ "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584",
+ "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"
],
"markers": "python_version >= '3.8'",
- "version": "==2.10.5"
+ "version": "==2.10.6"
},
"pydantic-core": {
"hashes": [
@@ -2409,11 +2442,11 @@
},
"starlette": {
"hashes": [
- "sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835",
- "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7"
+ "sha256:2cbcba2a75806f8a41c722141486f37c28e30a0921c5f6fe4346cb0dcee1302f",
+ "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d"
],
- "markers": "python_version >= '3.8'",
- "version": "==0.41.3"
+ "markers": "python_version >= '3.9'",
+ "version": "==0.45.3"
},
"telethon": {
"hashes": [
diff --git a/src/core/events.py b/src/core/events.py
index 03865d9..d423b16 100644
--- a/src/core/events.py
+++ b/src/core/events.py
@@ -12,6 +12,7 @@ from db.database import get_db, get_db_async, make_engine
from shared.settings import get_settings
from utils.metrics import measure_regular_metrics, redis_subscribe_worker_exceptions
from worker.main import create_sheet_task
+from fastapi_mail import FastMail, MessageSchema, MessageType
@asynccontextmanager
@@ -35,6 +36,11 @@ async def lifespan(app: FastAPI):
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.")
+
yield # separates startup from shutdown instructions
# SHUTDOWN
@@ -66,3 +72,39 @@ async def archive_sheets_cronjob(frequency: str, interval: int, current_time_uni
task = create_sheet_task.apply_async(args=[schemas.SubmitSheet(sheet_id=s.id, author_id=s.author_id, group=s.group_id).model_dump_json()])
triggered_jobs.append({"sheet_id": s.id, "task_id": task.id})
logger.info(f"[CRON {frequency.upper()}:{current_time_unit}] Triggered {len(triggered_jobs)} sheet tasks: {triggered_jobs}")
+
+
+@repeat_every(seconds=86400, wait_first=150, on_exception=logger.error)
+async def delete_stale_sheets():
+ STALE_DAYS = get_settings().DELETE_STALE_SHEETS_DAYS
+ logger.info(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:
+
+ 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.info(f"[CRON] Email sent to {email} about stale sheets deletion.")
+
diff --git a/src/db/crud.py b/src/db/crud.py
index 069277d..8e3fb1d 100644
--- a/src/db/crud.py
+++ b/src/db/crud.py
@@ -261,6 +261,18 @@ async def get_sheets_by_id_hash(db: AsyncSession, frequency: str, modulo: str, i
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 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:
diff --git a/src/db/database.py b/src/db/database.py
index e72dc15..7e4046b 100644
--- a/src/db/database.py
+++ b/src/db/database.py
@@ -36,16 +36,16 @@ def get_db_dependency():
with get_db() as db:
yield db
+
+
# ASYNC connections
-
-
async def make_async_engine(database_url: str) -> AsyncEngine:
engine = create_async_engine(database_url, connect_args={"check_same_thread": False})
return engine
async def make_async_session_local(engine: AsyncEngine) -> AsyncSession:
- return async_sessionmaker(engine, expire_on_commit=False)
+ return async_sessionmaker(engine, expire_on_commit=False, autoflush=False, autocommit=False)
@asynccontextmanager
diff --git a/src/shared/settings.py b/src/shared/settings.py
index 0a2aec5..5d4b843 100644
--- a/src/shared/settings.py
+++ b/src/shared/settings.py
@@ -1,5 +1,6 @@
from functools import lru_cache
+from fastapi_mail import ConnectionConfig
from pydantic_settings import BaseSettings
from pydantic import ConfigDict
from typing import Annotated, Set
@@ -16,6 +17,8 @@ class Settings(BaseSettings):
# cronjobs
CRON_ARCHIVE_SHEETS: bool = False
+ CRON_DELETE_STALE_SHEETS: bool = True
+ DELETE_STALE_SHEETS_DAYS: int = 14
# database
DATABASE_PATH: str
@@ -39,6 +42,29 @@ class Settings(BaseSettings):
#TODO: deprecate blocklist?
BLOCKED_EMAILS: Annotated[Set[str], Len(min_length=0)] = set()
+ # 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
From 46a5c1a2606aa18f910a620cb49e99a20562653a Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Fri, 7 Feb 2025 18:21:25 +0000
Subject: [PATCH 24/75] WAL checkpoint at startup
---
src/core/events.py | 5 ++++-
src/db/database.py | 11 ++++++++++-
2 files changed, 14 insertions(+), 2 deletions(-)
diff --git a/src/core/events.py b/src/core/events.py
index d423b16..76dc973 100644
--- a/src/core/events.py
+++ b/src/core/events.py
@@ -6,9 +6,10 @@ from fastapi import FastAPI
from contextlib import asynccontextmanager
from fastapi_utils.tasks import repeat_every
from loguru import logger
+from sqlalchemy import text
from db import crud, models, schemas
-from db.database import get_db, get_db_async, make_engine
+from db.database import get_db, get_db_async, make_engine, wal_checkpoint
from shared.settings import get_settings
from utils.metrics import measure_regular_metrics, redis_subscribe_worker_exceptions
from worker.main import create_sheet_task
@@ -41,6 +42,8 @@ async def lifespan(app: FastAPI):
else:
logger.warning("[CRON] Delete stale sheets cronjob is disabled.")
+ wal_checkpoint()
+
yield # separates startup from shutdown instructions
# SHUTDOWN
diff --git a/src/db/database.py b/src/db/database.py
index 7e4046b..4555b61 100644
--- a/src/db/database.py
+++ b/src/db/database.py
@@ -1,5 +1,5 @@
from functools import lru_cache
-from sqlalchemy import Engine, create_engine, event
+from sqlalchemy import Engine, create_engine, event, text
from sqlalchemy.orm import sessionmaker
from shared.settings import get_settings
from contextlib import asynccontextmanager, contextmanager
@@ -36,11 +36,20 @@ def get_db_dependency():
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("PRAGMA journal_mode=WAL;"))
+
return engine
From 9a62f3ff59b683c9603fafa03a04412d03c1aa0b Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Sat, 8 Feb 2025 00:40:35 +0000
Subject: [PATCH 25/75] WIP decoupling worker/web, cleaning worker code
---
src/core/events.py | 10 +-
src/core/logging.py | 3 +-
src/db/crud.py | 2 +-
src/db/database.py | 2 +-
src/db/schemas.py | 45 ++++----
src/db/user_state.py | 6 +
src/endpoints/__init__.py | 5 -
src/endpoints/sheet.py | 7 +-
src/endpoints/task.py | 3 +-
src/endpoints/url.py | 14 +--
src/shared/settings.py | 1 +
src/shared/task_messaging.py | 18 +++
src/tests/conftest.py | 2 +-
src/tests/endpoints/test_sheet.py | 37 ++++--
src/tests/endpoints/test_url.py | 46 ++++++--
src/tests/worker/test_worker_main.py | 45 +-------
src/utils/metrics.py | 13 ++-
src/web/main.py | 18 ++-
src/worker/main.py | 164 ++++++++-------------------
19 files changed, 194 insertions(+), 247 deletions(-)
create mode 100644 src/shared/task_messaging.py
diff --git a/src/core/events.py b/src/core/events.py
index 76dc973..8336f7b 100644
--- a/src/core/events.py
+++ b/src/core/events.py
@@ -6,15 +6,15 @@ from fastapi import FastAPI
from contextlib import asynccontextmanager
from fastapi_utils.tasks import repeat_every
from loguru import logger
-from sqlalchemy import text
from db import crud, models, schemas
from db.database import get_db, get_db_async, make_engine, wal_checkpoint
from shared.settings import get_settings
+from shared.task_messaging import get_celery
from utils.metrics import measure_regular_metrics, redis_subscribe_worker_exceptions
-from worker.main import create_sheet_task
from fastapi_mail import FastMail, MessageSchema, MessageType
+celery = get_celery()
@asynccontextmanager
async def lifespan(app: FastAPI):
@@ -25,7 +25,7 @@ async def lifespan(app: FastAPI):
models.Base.metadata.create_all(bind=engine)
alembic.config.main(argv=['--raiseerr', 'upgrade', 'head'])
logging.getLogger("uvicorn.access").disabled = True # loguru
- asyncio.create_task(redis_subscribe_worker_exceptions(get_settings().REDIS_EXCEPTIONS_CHANNEL, get_settings().CELERY_BROKER_URL))
+ 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)
@@ -72,7 +72,9 @@ async def archive_sheets_cronjob(frequency: str, interval: int, current_time_uni
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:
- task = create_sheet_task.apply_async(args=[schemas.SubmitSheet(sheet_id=s.id, author_id=s.author_id, group=s.group_id).model_dump_json()])
+
+ task = celery.signature("create_sheet_task", args=[schemas.SubmitSheet(sheet_id=s.id, author_id=s.author_id, group=s.group_id).model_dump_json()]).apply_async()
+
triggered_jobs.append({"sheet_id": s.id, "task_id": task.id})
logger.info(f"[CRON {frequency.upper()}:{current_time_unit}] Triggered {len(triggered_jobs)} sheet tasks: {triggered_jobs}")
diff --git a/src/core/logging.py b/src/core/logging.py
index 5ff03db..fe9f905 100644
--- a/src/core/logging.py
+++ b/src/core/logging.py
@@ -9,7 +9,6 @@ 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}")
@@ -21,6 +20,6 @@ async def logging_middleware(request: Request, call_next):
return response
except Exception as e:
from utils.metrics import EXCEPTION_COUNTER
- EXCEPTION_COUNTER.labels(type(e).__name__).inc()
+ EXCEPTION_COUNTER.labels(type=e.__class__.__name__).inc()
log_error(e)
raise e
\ No newline at end of file
diff --git a/src/db/crud.py b/src/db/crud.py
index 8e3fb1d..68989b3 100644
--- a/src/db/crud.py
+++ b/src/db/crud.py
@@ -100,7 +100,7 @@ def base_query(db: Session):
# --------------- TAG
-def create_tag(db: Session, tag: str):
+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)
diff --git a/src/db/database.py b/src/db/database.py
index 4555b61..f672b87 100644
--- a/src/db/database.py
+++ b/src/db/database.py
@@ -48,7 +48,7 @@ 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("PRAGMA journal_mode=WAL;"))
+ await conn.run_sync(lambda sync_conn: sync_conn.execute(text("PRAGMA journal_mode=WAL;")))
return engine
diff --git a/src/db/schemas.py b/src/db/schemas.py
index 424b9de..6600c63 100644
--- a/src/db/schemas.py
+++ b/src/db/schemas.py
@@ -11,35 +11,13 @@ class Tag(BaseModel):
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
- sheet_id: str | None = None
- # 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
+ group_id: str | None
tags: set[str] | None = set()
columns: dict | None = {} # TODO: implement
@@ -103,10 +81,25 @@ class SheetResponse(SheetAdd):
class ArchiveTrigger(BaseModel):
+ author_id: str | None = None
url: Annotated[str, Len(min_length=5)]
- public: bool = True
- group_id: Annotated[str, Len(min_length=1)] | None = None
- tags: set[Tag] | None = set()
+ public: bool = False
+ group_id: Annotated[str, Len(min_length=1)] = "default"
+ tags: set[Tag] | None = None
+
+class ArchiveCreate(ArchiveTrigger):
+ id: str | None = None
+ result: dict | None = None
+ sheet_id: str | None = None
+ urls: list | 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
diff --git a/src/db/user_state.py b/src/db/user_state.py
index 9ae3135..0091398 100644
--- a/src/db/user_state.py
+++ b/src/db/user_state.py
@@ -260,6 +260,9 @@ class UserState:
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
@@ -282,6 +285,9 @@ class UserState:
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
diff --git a/src/endpoints/__init__.py b/src/endpoints/__init__.py
index 1551fae..e69de29 100644
--- a/src/endpoints/__init__.py
+++ b/src/endpoints/__init__.py
@@ -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/sheet.py b/src/endpoints/sheet.py
index b1a4230..95731f0 100644
--- a/src/endpoints/sheet.py
+++ b/src/endpoints/sheet.py
@@ -6,13 +6,14 @@ from sqlalchemy import exc
from sqlalchemy.orm import Session
from db.user_state import UserState
+from shared.task_messaging import get_celery
from web.security import token_api_key_auth, get_user_state
from db import schemas, crud
from db.database import get_db_dependency
-from worker.main import create_sheet_task
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(
@@ -73,7 +74,7 @@ def archive_user_sheet(
if not user.can_manually_trigger(sheet.group_id):
raise HTTPException(status_code=429, detail="User cannot manually trigger sheet archiving in this group.")
- task = create_sheet_task.delay(schemas.SubmitSheet(sheet_id=id, author_id=user.email, group=sheet.group_id).model_dump_json())
+ task = celery.signature("create_sheet_task", args=[schemas.SubmitSheet(sheet_id=id, author_id=user.email, group=sheet.group_id).model_dump_json()]).delay()
return JSONResponse({"id": task.id}, status_code=201)
@@ -86,5 +87,5 @@ def archive_sheet(
sheet.author_id = sheet.author_id or "api-endpoint"
if not sheet.sheet_id:
raise HTTPException(status_code=422, detail=f"sheet id is required")
- task = create_sheet_task.delay(sheet.model_dump_json())
+ task = celery.signature("create_sheet_task", args=[sheet.model_dump_json()]).delay()
return JSONResponse({"id": task.id}, status_code=201)
diff --git a/src/endpoints/task.py b/src/endpoints/task.py
index a2250fd..0c7f1e3 100644
--- a/src/endpoints/task.py
+++ b/src/endpoints/task.py
@@ -4,16 +4,17 @@ from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from loguru import logger
+from shared.task_messaging import get_celery
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.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:
diff --git a/src/endpoints/url.py b/src/endpoints/url.py
index 3d0aae1..0c35238 100644
--- a/src/endpoints/url.py
+++ b/src/endpoints/url.py
@@ -6,17 +6,18 @@ from datetime import datetime
from loguru import logger
from core.config import ALLOW_ANY_EMAIL
from db.user_state import UserState
+from shared.task_messaging import get_celery
from web.security import get_token_or_user_auth, get_user_state
from sqlalchemy.orm import Session
from db import crud, schemas
from db.database import get_db_dependency
-from worker.main import create_archive_task
from urllib.parse import urlparse
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(
@@ -24,6 +25,7 @@ def archive_url(
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)
@@ -39,15 +41,9 @@ def archive_url(
if not user.has_quota_max_monthly_mbs(archive.group_id):
raise HTTPException(status_code=429, detail="User has reached their monthly MB quota.")
- # TODO: deprecate ArchiveCreate
- backwards_compatible_archive = schemas.ArchiveCreate(
- url=archive.url,
- author_id=email,
- group_id=archive.group_id,
- public=archive.public,
- )
+ archive_create = schemas.ArchiveCreate(**archive.model_dump())
- task = create_archive_task.delay(backwards_compatible_archive.model_dump_json())
+ task = celery.signature("create_archive_task", args=[archive_create.model_dump_json()]).delay()
task_response = schemas.Task(id=task.id)
return JSONResponse(task_response.model_dump(), status_code=201)
diff --git a/src/shared/settings.py b/src/shared/settings.py
index 5d4b843..f7383dd 100644
--- a/src/shared/settings.py
+++ b/src/shared/settings.py
@@ -16,6 +16,7 @@ class Settings(BaseSettings):
SHEET_ORCHESTRATION_YAML : str = "secrets/orchestration-sheet.yaml"
# cronjobs
+ #TODO: disable by default?
CRON_ARCHIVE_SHEETS: bool = False
CRON_DELETE_STALE_SHEETS: bool = True
DELETE_STALE_SHEETS_DAYS: int = 14
diff --git a/src/shared/task_messaging.py b/src/shared/task_messaging.py
new file mode 100644
index 0000000..4b2e000
--- /dev/null
+++ b/src/shared/task_messaging.py
@@ -0,0 +1,18 @@
+
+from functools import lru_cache
+from celery import Celery
+import redis
+
+from 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_RESULT_BACKEND,
+ )
+
+
+def get_redis() -> redis.Redis:
+ return redis.Redis.from_url(get_settings().CELERY_BROKER_URL)
diff --git a/src/tests/conftest.py b/src/tests/conftest.py
index 89160c1..33c2886 100644
--- a/src/tests/conftest.py
+++ b/src/tests/conftest.py
@@ -92,7 +92,7 @@ def client_with_auth(app_with_auth):
@pytest.fixture()
def app_with_token(app):
- from web.security import token_api_key_auth,get_token_or_user_auth
+ from 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
diff --git a/src/tests/endpoints/test_sheet.py b/src/tests/endpoints/test_sheet.py
index 129cd2a..81d2cc3 100644
--- a/src/tests/endpoints/test_sheet.py
+++ b/src/tests/endpoints/test_sheet.py
@@ -1,6 +1,6 @@
from datetime import datetime
import json
-from unittest.mock import patch
+from unittest.mock import MagicMock, patch
from fastapi.testclient import TestClient
@@ -145,15 +145,21 @@ def test_delete_sheet_endpoint(client_with_auth, db_session):
class TestArchiveUserSheetEndpoint:
- @patch("worker.main.create_sheet_task.delay", return_value=TaskResult(id="123-taskid", status="PENDING", result=""))
- def test_normal_flow(self, m1, client_with_auth, db_session):
+ @patch("endpoints.sheet.celery", return_value=MagicMock())
+ def test_normal_flow(self, m_celery, client_with_auth, db_session):
from 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.delay.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"}
- m1.assert_called_once()
+ m_celery.signature.assert_called_once()
+ m_signature.delay.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")
@@ -198,23 +204,30 @@ class TestTokenArchiveEndpoint:
assert r.status_code == 422
assert r.json() == {"detail": "sheet id is required"}
- @patch("worker.main.create_sheet_task.delay", return_value=TaskResult(id="123-456-789", status="PENDING", result=""))
- def test_normal_flow(self, m1, client_with_token):
+ @patch("endpoints.sheet.celery", return_value=MagicMock())
+ def test_normal_flow(self, m_celery, client_with_token):
+ m_signature = MagicMock()
+ m_signature.delay.return_value = TaskResult(id="123-456-789", status="PENDING", result="")
+ m_celery.signature.return_value = m_signature
# minimum data
response = client_with_token.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": "api-endpoint", "group_id": None, "tags": [], "columns": {}, "header": 1}
+ m_celery.signature.assert_called_once()
+ m_signature.delay.assert_called_once()
+ called_val = m_celery.signature.call_args
+ assert called_val[0][0] == "create_sheet_task"
+ assert json.loads(called_val[1]['args'][0]) == {"sheet_id": "123-sheet-id", "sheet_name": None, "public": False, "author_id": "api-endpoint", "group_id": None, "tags": [], "columns": {}, "header": 1}
# maximum data
response = client_with_token.post("/sheet/archive", json={"sheet_id": "123-sheet-id", "sheet_name": "768-sheet-name", "author_id": "birdman@example.com", "header": 2, "public": True, "group_id": "456-group-id", "tags": ["tag1"], "columns": {"col1": "type1"}})
assert response.status_code == 201
assert response.json() == {'id': '123-456-789'}
- m1.call_count == 2
- called_val = m1.call_args.args[0]
- assert json.loads(called_val) == {"sheet_id": "123-sheet-id", "sheet_name": "768-sheet-name", "public": True, "author_id": "birdman@example.com", "group_id": "456-group-id", "tags": ["tag1"], "columns": {"col1": "type1"}, "header": 2}
+ m_celery.signature.call_count == 2
+ m_signature.delay.call_count == 2
+ called_val = m_celery.signature.call_args
+ assert called_val[0][0] == "create_sheet_task"
+ assert json.loads(called_val[1]['args'][0]) == {"sheet_id": "123-sheet-id", "sheet_name": "768-sheet-name", "public": True, "author_id": "birdman@example.com", "group_id": "456-group-id", "tags": ["tag1"], "columns": {"col1": "type1"}, "header": 2}
diff --git a/src/tests/endpoints/test_url.py b/src/tests/endpoints/test_url.py
index af198e3..3d85e71 100644
--- a/src/tests/endpoints/test_url.py
+++ b/src/tests/endpoints/test_url.py
@@ -3,13 +3,18 @@ from unittest.mock import MagicMock, patch
from db.schemas import ArchiveCreate, TaskResult
+
def test_archive_url_unauthenticated(client, test_no_auth):
test_no_auth(client.post, "/url/archive")
@patch("endpoints.url.UserState")
-@patch("worker.main.create_archive_task.delay", return_value=TaskResult(id="123-456-789", status="PENDING", result=""))
-def test_archive_url(m1, m2, client_with_auth):
+@patch("endpoints.url.celery", return_value=MagicMock())
+def test_archive_url(m_celery, m2, client_with_auth):
+ m_signature = MagicMock()
+ m_signature.delay.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
@@ -17,7 +22,7 @@ def test_archive_url(m1, m2, client_with_auth):
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'
- m1.assert_not_called()
+ m_celery.signature.assert_not_called()
# url is invalid
response = client_with_auth.post("/url/archive", json={"url": "example.com"})
@@ -30,9 +35,11 @@ def test_archive_url(m1, m2, client_with_auth):
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, "sheet_id":None}
+ m_celery.signature.assert_called_once()
+ m_signature.delay.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": True, "author_id": "rick@example.com", "group_id": None, "tags": [], "sheet_id": None}
m_user_state.has_quota_max_monthly_urls.assert_called_once()
m_user_state.has_quota_max_monthly_mbs.assert_called_once()
@@ -48,9 +55,10 @@ def test_archive_url(m1, m2, client_with_auth):
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 m1.call_count == 2
- called_val = m1.call_args.args[0]
- assert json.loads(called_val)["group_id"] == "spaceship"
+ assert m_celery.signature.call_count == 2
+ assert m_signature.delay.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
@@ -68,6 +76,9 @@ def test_archive_url(m1, m2, client_with_auth):
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.delay.call_count == 2
+
@patch("endpoints.url.UserState")
def test_archive_url_quotas(m1, client_with_auth):
@@ -89,15 +100,25 @@ def test_archive_url_quotas(m1, client_with_auth):
assert response.json()["detail"] == "User has reached their monthly MB quota."
m_user_state.has_quota_max_monthly_mbs.assert_called_once()
-@patch("worker.main.create_archive_task.delay", return_value=TaskResult(id="123-456-789", status="PENDING", result=""))
-def test_archive_url_with_api_token(m1, client_with_token):
+
+@patch("endpoints.url.celery", return_value=MagicMock())
+def test_archive_url_with_api_token(m_celery, client_with_token):
+ m_signature = MagicMock()
+ m_signature.delay.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.delay.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")
@@ -111,7 +132,7 @@ def test_search_by_url(client_with_auth, client_with_token, db_session):
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
+ # 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
@@ -142,6 +163,7 @@ def test_search_by_url(client_with_auth, client_with_token, db_session):
assert response.status_code == 200
assert len(response.json()) == 10
+
@patch("endpoints.url.UserState")
def test_search_no_read_access(mock_user_state, client_with_auth):
mock_user_state.return_value.read = False
diff --git a/src/tests/worker/test_worker_main.py b/src/tests/worker/test_worker_main.py
index ffef233..3550173 100644
--- a/src/tests/worker/test_worker_main.py
+++ b/src/tests/worker/test_worker_main.py
@@ -21,7 +21,7 @@ class Test_create_archive_task():
@patch("worker.main.insert_result_into_db")
@patch("worker.main.is_group_invalid_for_user", return_value=None)
- @patch("worker.main.choose_orchestrator")
+ # @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
@@ -46,7 +46,7 @@ class Test_create_archive_task():
@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")
+ # @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)
@@ -123,47 +123,6 @@ class Test_create_sheet_task():
assert db_session.query(models.Archive).filter(models.Archive.url == self.URL).count() == 0
-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
diff --git a/src/utils/metrics.py b/src/utils/metrics.py
index 8d513e6..05c0bb0 100644
--- a/src/utils/metrics.py
+++ b/src/utils/metrics.py
@@ -8,18 +8,19 @@ import redis
from db import crud
from db.database import get_db
from core.logging import log_error
+from shared.task_messaging import get_redis
# Custom metrics
EXCEPTION_COUNTER = Counter(
"exceptions",
"Number of times a certain exception has occurred.",
- labelnames=["types"]
+ labelnames=["type"]
)
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 +39,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):
# 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/src/web/main.py b/src/web/main.py
index f2020f0..8b721f9 100644
--- a/src/web/main.py
+++ b/src/web/main.py
@@ -12,7 +12,8 @@ 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 shared.task_messaging import get_celery
+from worker.main import 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
@@ -23,8 +24,13 @@ from shared.settings import get_settings
from auto_archiver import Metadata
-from endpoints import default_router, url_router, sheet_router, task_router, interoperability_router
+from endpoints.default import default_router
+from endpoints.url import url_router
+from endpoints.sheet import sheet_router
+from endpoints.task import task_router
+from endpoints.interoperability import interoperability_router
+celery = get_celery()
def app_factory(settings = get_settings()):
app = FastAPI(
@@ -84,7 +90,8 @@ def app_factory(settings = get_settings()):
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 = celery.signature("create_archive_task", args=[archive.model_dump_json()]).delay()
return JSONResponse({"id": task.id})
@@ -139,7 +146,7 @@ def app_factory(settings = get_settings()):
raise HTTPException(status_code=422, detail=f"sheet name or id is required")
if not crud.is_user_in_group(db, email, sheet.group_id):
raise HTTPException(status_code=403, detail="User does not have access to this group.")
- task = create_sheet_task.delay(sheet.model_dump_json())
+ task = celery.signature("create_sheet_task", args=[sheet.model_dump_json()]).delay()
return JSONResponse({"id": task.id})
@@ -149,7 +156,8 @@ def app_factory(settings = get_settings()):
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())
+
+ task = celery.signature("create_sheet_task", args=[sheet.model_dump_json()]).delay()
return JSONResponse({"id": task.id})
# ----- endpoint to submit data archived elsewhere
diff --git a/src/worker/main.py b/src/worker/main.py
index 2896a23..27b261a 100644
--- a/src/worker/main.py
+++ b/src/worker/main.py
@@ -1,78 +1,56 @@
-from functools import lru_cache
import traceback, yaml, datetime
from typing import List, Set
-from celery import Celery
-from celery.signals import task_failure, worker_init
+from celery.signals import task_failure
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.task_messaging import get_celery, get_redis
from shared.settings import get_settings
import json
-import redis
from sqlalchemy import exc
from core.logging import log_error
+
settings = get_settings()
+celery = get_celery("worker")
+Redis = get_redis()
-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})
+@celery.task(name="create_archive_task", bind=True, autoretry_for=(Exception,), retry_backoff=True, retry_kwargs={'max_retries': 0})
def create_archive_task(self, archive_json: str):
+ logger.info(archive_json)
archive = schemas.ArchiveCreate.model_validate_json(archive_json)
- logger.info(f"Archiving {archive.url=} {archive.tags=} {archive.public=} {archive.group_id=} {archive.author_id=}")
- #TODO: move group checks out of here
- 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=}")
+ # call auto-archiver
+ orchestrator = load_orchestrator(archive.group_id)
+ result = orchestrator.feed_item(Metadata().set_url(archive.url))
- # 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])
+ # prepare for DB
+ assert result, f"UNABLE TO archive: {archive.url}"
+ archive.id = self.request.id
+ archive.urls = get_all_urls(result)
+ archive.result = json.loads(result.to_json())
- orchestrator = choose_orchestrator(archive.group_id, archive.author_id)
- logger.info(f"Using orchestrator {orchestrator=}")
- result = orchestrator.feed_item(Metadata().set_url(url))
+ insert_result_into_db(archive)
+ return archive.result.to_dict() # TODO: is return used?
- 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()
-#TODO: refactor how user-groups are loaded and orchestrators chosen
-@celery.task(name="create_sheet_task", bind=True, autoretry_for=(Exception,), retry_backoff=True, retry_kwargs={'max_retries': 0})
+@celery.task(name="create_sheet_task", bind=True)
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=}")
- config = Config()
- # TODO: use choose_orchestrator and overwrite the feeder
# TODO: drop sheet_name and use only sheet_id (new endpoints/models)
- 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)
+ orchestrator = load_orchestrator(sheet.group_id, {"configurations": {"gsheet_feeder": {"sheet": sheet.sheet_name, "sheet_id": sheet.sheet_id, "header": sheet.header}}})
stats = {"archived": 0, "failed": 0, "errors": []}
for result in orchestrator.feed():
@@ -80,8 +58,8 @@ def create_sheet_task(self, sheet_json: str):
logger.error("Got empty result from feeder, an internal error must have occurred.")
continue
try:
- #TODO: remove public from sheet in new refactor
- #TODO: update the sheets table with the current date if any new archive was done
+ # TODO: remove public from sheet in new refactor
+ #TODO: use new insert_result_into_db
insert_result_into_db(result, sheet.tags, sheet.public, sheet.group_id, sheet.author_id, models.generate_uuid(), sheet.sheet_id)
stats["archived"] += 1
except exc.IntegrityError as e:
@@ -97,26 +75,20 @@ def create_sheet_task(self, sheet_json: str):
crud.update_sheet_last_url_archived_at(session, sheet.sheet_id)
logger.info(f"SHEET DONE {sheet=}")
+ # TODO: use data model
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):
+ # 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'])))
- 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:
@@ -127,52 +99,28 @@ def read_user_groups():
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
+def load_orchestrator(group_id: str, overwrite_configs: dict = {}) -> ArchivingOrchestrator:
with get_db() as session:
- if not crud.is_user_in_group(session, author_id, group_id):
- logger.error(em := f"User {author_id} is not part of {group_id}, no permission")
- return em
- return False
+ orchestrator_fn = crud.get_group(session, group_id).orchestrator
+ assert orchestrator_fn, f"no orchestrator found for {group_id}"
+
+ config = Config()
+ config.parse(use_cli=False, yaml_config_filename=orchestrator_fn, overwrite_configs=overwrite_configs)
+ return ArchivingOrchestrator(config)
-def insert_result_into_db(result: Metadata, tags: Set[str], public: bool, group_id: str, author_id: str, task_id: str, sheet_id:str="") -> str:
+def insert_result_into_db(archive: schemas.ArchiveCreate) -> str:
+ with get_db() as session:
+ # create and load user, tags, if needed
+ crud.create_or_get_user(session, archive.author_id)
+ db_tags = [crud.create_tag(session, tag) for tag in archive.tags]
+ # insert everything
+ db_task = crud.create_task(session, task=archive, tags=db_tags, urls=archive.urls)
+ logger.debug(f"Added {db_task.id=} to database on {db_task.created_at} ({db_task.author_id})")
+ return db_task.id
+
+
+def insert_result_into_db(result: Metadata, tags: Set[str], public: bool, group_id: str, author_id: str, task_id: str, sheet_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:
@@ -186,7 +134,7 @@ def insert_result_into_db(result: Metadata, tags: Set[str], public: bool, group_
logger.debug(f"Added {db_task.id=} to database on {db_task.created_at} ({db_task.author_id})")
return db_task.id
-
+# TODO: this should live within the auto-archiver
def get_all_urls(result: Metadata) -> List[models.ArchiveUrl]:
db_urls = []
for m in result.media:
@@ -202,6 +150,7 @@ def get_all_urls(result: Metadata) -> List[models.ArchiveUrl]:
return db_urls
+# TODO: this should live within the auto-archiver??
def convert_if_media(media):
if isinstance(media, Media): return media
elif isinstance(media, dict):
@@ -214,24 +163,7 @@ def convert_if_media(media):
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))
+ 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}")
-
-
-@worker_init.connect
-def at_start(sender, **kwargs):
- global ORCHESTRATORS
- ORCHESTRATORS = {}
- load_orchestrators()
- logger.info("Orchestrators loaded successfully.")
-
-@lru_cache
-def get_url_orchestrator(group_name):
- with get_db() as db:
- group = crud.get_group(db, group_name)
- assert group, f"Group {group_name} not found"
-
- # config = Config()
- # config.parse(use_cli=False, yaml_config_filename=group.orchestrator_sheet)
- # return ArchivingOrchestrator(config)
\ No newline at end of file
+ log_error(e, f"[CRITICAL] Could not publish to {REDIS_EXCEPTIONS_CHANNEL}")
\ No newline at end of file
From 5494825286b0edb5fccf343071cd1e7ff76639b9 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Sat, 8 Feb 2025 15:18:15 +0000
Subject: [PATCH 26/75] minor docker comments
---
docker-compose.dev.yml | 1 +
docker-compose.yml | 1 +
2 files changed, 2 insertions(+)
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index 3782477..2665a87 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -1,5 +1,6 @@
services:
web:
+ command: uvicorn web:app --factory --host 0.0.0.0 --reload
restart: "no"
env_file: src/.env.dev
environment:
diff --git a/docker-compose.yml b/docker-compose.yml
index 1ffd164..4e6b807 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -18,6 +18,7 @@ services:
<<: *base-setup
ports:
- "127.0.0.1:8004:8000"
+ #TODO: should prod have the --reload flag?
command: uvicorn web:app --factory --host 0.0.0.0 --reload
volumes:
- ./src:/app
From a97333c4d6536d28aedc0d0b787e9bdf0864f6ae Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Sat, 8 Feb 2025 15:19:16 +0000
Subject: [PATCH 27/75] drops /sheet/archive API token endpoint
---
src/endpoints/sheet.py | 18 +++-----------
src/tests/endpoints/test_sheet.py | 40 -------------------------------
2 files changed, 3 insertions(+), 55 deletions(-)
diff --git a/src/endpoints/sheet.py b/src/endpoints/sheet.py
index 95731f0..e479ee9 100644
--- a/src/endpoints/sheet.py
+++ b/src/endpoints/sheet.py
@@ -7,7 +7,7 @@ from sqlalchemy.orm import Session
from db.user_state import UserState
from shared.task_messaging import get_celery
-from web.security import token_api_key_auth, get_user_state
+from web.security import get_user_state
from db import schemas, crud
from db.database import get_db_dependency
@@ -74,18 +74,6 @@ def archive_user_sheet(
if not user.can_manually_trigger(sheet.group_id):
raise HTTPException(status_code=429, detail="User cannot manually trigger sheet archiving in this group.")
- task = celery.signature("create_sheet_task", args=[schemas.SubmitSheet(sheet_id=id, author_id=user.email, group=sheet.group_id).model_dump_json()]).delay()
+ task = celery.signature("create_sheet_task", args=[schemas.SubmitSheet(sheet_id=id, author_id=user.email, group_id=sheet.group_id).model_dump_json()]).delay()
- return JSONResponse({"id": task.id}, status_code=201)
-
-
-@sheet_router.post("/archive", status_code=201, summary="Trigger an archiving task for any GSheet with an API token.", response_description="task_id for the archiving task.")
-def archive_sheet(
- sheet: schemas.SubmitSheet,
- auth=Depends(token_api_key_auth)
-) -> schemas.Task:
- sheet.author_id = sheet.author_id or "api-endpoint"
- if not sheet.sheet_id:
- raise HTTPException(status_code=422, detail=f"sheet id is required")
- task = celery.signature("create_sheet_task", args=[sheet.model_dump_json()]).delay()
- return JSONResponse({"id": task.id}, status_code=201)
+ return JSONResponse({"id": task.id}, status_code=201)
\ No newline at end of file
diff --git a/src/tests/endpoints/test_sheet.py b/src/tests/endpoints/test_sheet.py
index 81d2cc3..5d43b65 100644
--- a/src/tests/endpoints/test_sheet.py
+++ b/src/tests/endpoints/test_sheet.py
@@ -12,7 +12,6 @@ def test_endpoints_no_auth(client, test_no_auth):
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")
- test_no_auth(client.post, "/sheet/archive")
def test_create_sheet_endpoint(app_with_auth, db_session):
@@ -192,42 +191,3 @@ class TestArchiveUserSheetEndpoint:
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."}
-
-
-class TestTokenArchiveEndpoint:
-
- def test_user_auth(self, client_with_auth, test_no_auth):
- test_no_auth(client_with_auth.post, "/sheet/archive")
-
- def test_missing_data(self, client_with_token):
- r = client_with_token.post("/sheet/archive", json={})
- assert r.status_code == 422
- assert r.json() == {"detail": "sheet id is required"}
-
- @patch("endpoints.sheet.celery", return_value=MagicMock())
- def test_normal_flow(self, m_celery, client_with_token):
- m_signature = MagicMock()
- m_signature.delay.return_value = TaskResult(id="123-456-789", status="PENDING", result="")
- m_celery.signature.return_value = m_signature
-
- # minimum data
- response = client_with_token.post("/sheet/archive", json={"sheet_id": "123-sheet-id"})
- assert response.status_code == 201
- assert response.json() == {'id': '123-456-789'}
-
- m_celery.signature.assert_called_once()
- m_signature.delay.assert_called_once()
- called_val = m_celery.signature.call_args
- assert called_val[0][0] == "create_sheet_task"
- assert json.loads(called_val[1]['args'][0]) == {"sheet_id": "123-sheet-id", "sheet_name": None, "public": False, "author_id": "api-endpoint", "group_id": None, "tags": [], "columns": {}, "header": 1}
-
- # maximum data
- response = client_with_token.post("/sheet/archive", json={"sheet_id": "123-sheet-id", "sheet_name": "768-sheet-name", "author_id": "birdman@example.com", "header": 2, "public": True, "group_id": "456-group-id", "tags": ["tag1"], "columns": {"col1": "type1"}})
- assert response.status_code == 201
- assert response.json() == {'id': '123-456-789'}
-
- m_celery.signature.call_count == 2
- m_signature.delay.call_count == 2
- called_val = m_celery.signature.call_args
- assert called_val[0][0] == "create_sheet_task"
- assert json.loads(called_val[1]['args'][0]) == {"sheet_id": "123-sheet-id", "sheet_name": "768-sheet-name", "public": True, "author_id": "birdman@example.com", "group_id": "456-group-id", "tags": ["tag1"], "columns": {"col1": "type1"}, "header": 2}
From ad6dc4db43ecc478a234ab36dad92d1d0b1e8fec Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Sat, 8 Feb 2025 15:20:27 +0000
Subject: [PATCH 28/75] bugs, worker cleanup
---
src/core/events.py | 2 +-
src/core/logging.py | 1 +
src/db/crud.py | 21 ++++--
src/db/schemas.py | 24 +++----
src/endpoints/task.py | 1 -
src/shared/__init__.py | 0
src/tests/worker/test_worker_main.py | 3 +-
src/web/main.py | 6 +-
src/worker/main.py | 102 +++++++++++----------------
9 files changed, 72 insertions(+), 88 deletions(-)
create mode 100644 src/shared/__init__.py
diff --git a/src/core/events.py b/src/core/events.py
index 8336f7b..3a700af 100644
--- a/src/core/events.py
+++ b/src/core/events.py
@@ -73,7 +73,7 @@ async def archive_sheets_cronjob(frequency: str, interval: int, current_time_uni
sheets = await crud.get_sheets_by_id_hash(db, frequency, interval, current_time_unit)
for s in sheets:
- task = celery.signature("create_sheet_task", args=[schemas.SubmitSheet(sheet_id=s.id, author_id=s.author_id, group=s.group_id).model_dump_json()]).apply_async()
+ 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()
triggered_jobs.append({"sheet_id": s.id, "task_id": task.id})
logger.info(f"[CRON {frequency.upper()}:{current_time_unit}] Triggered {len(triggered_jobs)} sheet tasks: {triggered_jobs}")
diff --git a/src/core/logging.py b/src/core/logging.py
index fe9f905..4929232 100644
--- a/src/core/logging.py
+++ b/src/core/logging.py
@@ -21,5 +21,6 @@ async def logging_middleware(request: Request, call_next):
except Exception as e:
from utils.metrics import EXCEPTION_COUNTER
EXCEPTION_COUNTER.labels(type=e.__class__.__name__).inc()
+ logger.info(f"{request.client.host}:{request.client.port} {request.method} {request.url._url} - {e.__class__.__name__} {e}")
log_error(e)
raise e
\ No newline at end of file
diff --git a/src/db/crud.py b/src/db/crud.py
index 68989b3..d984a4f 100644
--- a/src/db/crud.py
+++ b/src/db/crud.py
@@ -50,8 +50,8 @@ def search_archives_by_url(db: Session, url: str, email: str, skip: int = 0, lim
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 create_task(db: Session, task: schemas.ArchiveCreate, tags: list[models.Tag], urls: list[models.ArchiveUrl]):
+#TODO: rename task to archive
+def create_task(db: Session, task: schemas.ArchiveCreate, tags: list[models.Tag], urls: list[models.ArchiveUrl]) -> models.Archive:
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, sheet_id=task.sheet_id)
db_task.tags = tags
db_task.urls = urls
@@ -234,8 +234,8 @@ def upsert_user_groups(db: Session):
# --------------- SHEET
-def create_sheet(db: Session, sheet_id: str, sheet_name: str, email: str, group_id: str, frequency: str):
- db_sheet = models.Sheet(id=sheet_id, name=sheet_name, author_id=email, group_id=group_id, frequency=frequency)
+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)
@@ -250,7 +250,6 @@ 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: str) -> list[models.Sheet]:
result = await db.execute(
select(models.Sheet).filter(models.Sheet.frequency == frequency)
@@ -288,3 +287,15 @@ def delete_sheet(db: Session, sheet_id: str, email: str) -> bool:
db.delete(db_sheet)
db.commit()
return db_sheet is not None
+
+
+#--- Celery worker tasks
+
+
+def insert_result_into_db(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]
+ # insert everything
+ db_task = create_task(db, task=archive, tags=db_tags, urls=archive.urls)
+ return db_task
\ No newline at end of file
diff --git a/src/db/schemas.py b/src/db/schemas.py
index 6600c63..850fa3a 100644
--- a/src/db/schemas.py
+++ b/src/db/schemas.py
@@ -4,22 +4,12 @@ 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 SubmitSheet(BaseModel):
- sheet_name: str | None = None
- sheet_id: str | None = None
- header: int = 1
- public: bool = False
+ sheet_id: str | None
author_id: str | None = None
group_id: str | None
tags: set[str] | None = set()
- columns: dict | None = {} # TODO: implement
+ columns: dict | None = {} # TODO: implement/remove
class SubmitManual(BaseModel):
@@ -85,7 +75,7 @@ class ArchiveTrigger(BaseModel):
url: Annotated[str, Len(min_length=5)]
public: bool = False
group_id: Annotated[str, Len(min_length=1)] = "default"
- tags: set[Tag] | None = None
+ tags: set[str] | None = None
class ArchiveCreate(ArchiveTrigger):
id: str | None = None
@@ -107,4 +97,10 @@ class Usage(BaseModel):
total_sheets: int = 0
class UsageResponse(Usage):
- groups: dict[str, Usage]
\ No newline at end of file
+ groups: dict[str, Usage]
+
+class CelerySheetTask(BaseModel):
+ success: bool
+ sheet_id: str
+ time: datetime
+ stats: dict
\ No newline at end of file
diff --git a/src/endpoints/task.py b/src/endpoints/task.py
index 0c7f1e3..bacdb31 100644
--- a/src/endpoints/task.py
+++ b/src/endpoints/task.py
@@ -18,7 +18,6 @@ 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/src/shared/__init__.py b/src/shared/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/tests/worker/test_worker_main.py b/src/tests/worker/test_worker_main.py
index 3550173..f82a80c 100644
--- a/src/tests/worker/test_worker_main.py
+++ b/src/tests/worker/test_worker_main.py
@@ -64,7 +64,7 @@ class Test_create_archive_task():
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)
+ sheet = schemas.SubmitSheet(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")
@@ -82,7 +82,6 @@ class Test_create_sheet_task():
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
diff --git a/src/web/main.py b/src/web/main.py
index 8b721f9..ee6d192 100644
--- a/src/web/main.py
+++ b/src/web/main.py
@@ -142,8 +142,6 @@ def app_factory(settings = get_settings()):
def archive_sheet(sheet: schemas.SubmitSheet, email=Depends(get_user_auth), db: Session = Depends(get_db_dependency)):
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")
if not crud.is_user_in_group(db, email, sheet.group_id):
raise HTTPException(status_code=403, detail="User does not have access to this group.")
task = celery.signature("create_sheet_task", args=[sheet.model_dump_json()]).delay()
@@ -151,11 +149,9 @@ def app_factory(settings = get_settings()):
@app.post("/sheet_service", status_code=201, deprecated=True) # DEPRECATED
- def archive_sheet_service(sheet: schemas.SubmitSheet, auth=Depends(token_api_key_auth), db: Session = Depends(get_db_dependency)):
+ 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 = celery.signature("create_sheet_task", args=[sheet.model_dump_json()]).delay()
return JSONResponse({"id": task.id})
diff --git a/src/worker/main.py b/src/worker/main.py
index 27b261a..940158b 100644
--- a/src/worker/main.py
+++ b/src/worker/main.py
@@ -1,6 +1,6 @@
-import traceback, yaml, datetime
-from typing import List, Set
+import traceback, datetime
+from typing import List
from celery.signals import task_failure
from auto_archiver import Config, ArchivingOrchestrator, Metadata
@@ -23,6 +23,8 @@ Redis = get_redis()
USER_GROUPS_FILENAME = settings.USER_GROUPS_FILENAME
+# 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': 0})
def create_archive_task(self, archive_json: str):
@@ -32,35 +34,39 @@ def create_archive_task(self, archive_json: str):
# call auto-archiver
orchestrator = load_orchestrator(archive.group_id)
result = orchestrator.feed_item(Metadata().set_url(archive.url))
-
- # prepare for DB
assert result, f"UNABLE TO archive: {archive.url}"
+
+ # prepare and insert in DB
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.to_dict() # TODO: is return used?
+
+ 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)
- sheet.tags.add("gsheet")
logger.info(f"SHEET START {sheet=}")
- # TODO: drop sheet_name and use only sheet_id (new endpoints/models)
- orchestrator = load_orchestrator(sheet.group_id, {"configurations": {"gsheet_feeder": {"sheet": sheet.sheet_name, "sheet_id": sheet.sheet_id, "header": sheet.header}}})
+ orchestrator = load_orchestrator(sheet.group_id, True, {"configurations": {"gsheet_feeder": {"sheet_id": sheet.sheet_id}}})
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:
- # TODO: remove public from sheet in new refactor
- #TODO: use new insert_result_into_db
- insert_result_into_db(result, sheet.tags, sheet.public, sheet.group_id, sheet.author_id, models.generate_uuid(), sheet.sheet_id)
+ assert result, f"UNABLE TO archive: {result.get_url()}"
+ 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)
+ )
+ insert_result_into_db(archive)
stats["archived"] += 1
except exc.IntegrityError as e:
logger.warning(f"cached result detected: {e}")
@@ -75,33 +81,17 @@ def create_sheet_task(self, sheet_json: str):
crud.update_sheet_last_url_archived_at(session, sheet.sheet_id)
logger.info(f"SHEET DONE {sheet=}")
- # TODO: use data model
- return {"success": True, "sheet": sheet.sheet_name, "sheet_id": sheet.sheet_id, "time": datetime.datetime.now().isoformat(), **stats}
+ # 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()
-@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)
-
-
-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 load_orchestrator(group_id: str, overwrite_configs: dict = {}) -> ArchivingOrchestrator:
+def load_orchestrator(group_id: str, orchestrator_for_sheet: bool = False, overwrite_configs: dict = {}) -> ArchivingOrchestrator:
with get_db() as session:
- orchestrator_fn = crud.get_group(session, group_id).orchestrator
+ group = crud.get_group(session, group_id)
+ if orchestrator_for_sheet:
+ orchestrator_fn = group.orchestrator_sheet
+ else:
+ orchestrator_fn = crud.get_group(session, group_id).orchestrator
assert orchestrator_fn, f"no orchestrator found for {group_id}"
config = Config()
@@ -111,29 +101,11 @@ def load_orchestrator(group_id: str, overwrite_configs: dict = {}) -> ArchivingO
def insert_result_into_db(archive: schemas.ArchiveCreate) -> str:
with get_db() as session:
- # create and load user, tags, if needed
- crud.create_or_get_user(session, archive.author_id)
- db_tags = [crud.create_tag(session, tag) for tag in archive.tags]
- # insert everything
- db_task = crud.create_task(session, task=archive, tags=db_tags, urls=archive.urls)
- logger.debug(f"Added {db_task.id=} to database on {db_task.created_at} ({db_task.author_id})")
+ db_task = crud.insert_result_into_db(session, archive)
+ logger.debug(f"[ARCHIVE STORED] {db_task.author_id} {db_task.url}")
return db_task.id
-def insert_result_into_db(result: Metadata, tags: Set[str], public: bool, group_id: str, author_id: str, task_id: str, sheet_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, sheet_id=sheet_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
-
# TODO: this should live within the auto-archiver
def get_all_urls(result: Metadata) -> List[models.ArchiveUrl]:
db_urls = []
@@ -166,4 +138,14 @@ def redis_publish_exception(exception, task_name, traceback: str = ""):
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}")
\ No newline at end of file
+ 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)
From 0d51e5cd65827aa4120640356ba53f9aabcd9a1e Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Sat, 8 Feb 2025 17:29:42 +0000
Subject: [PATCH 29/75] implements expiration policy via store_until
---
src/core/events.py | 64 ++++++++++++++++++-
src/db/crud.py | 36 +++++++----
src/db/models.py | 1 +
src/db/schemas.py | 2 +
...7ed0_create_archives_store_until_column.py | 34 ++++++++++
src/shared/settings.py | 2 +
src/worker/main.py | 13 +++-
7 files changed, 137 insertions(+), 15 deletions(-)
create mode 100644 src/migrations/versions/02b2f6d17ed0_create_archives_store_until_column.py
diff --git a/src/core/events.py b/src/core/events.py
index 3a700af..d051e12 100644
--- a/src/core/events.py
+++ b/src/core/events.py
@@ -1,4 +1,5 @@
import asyncio
+from collections import defaultdict
import datetime
import logging
import alembic.config
@@ -16,6 +17,7 @@ from fastapi_mail import FastMail, MessageSchema, MessageType
celery = get_celery()
+
@asynccontextmanager
async def lifespan(app: FastAPI):
# see https://fastapi.tiangolo.com/advanced/events/#lifespan
@@ -42,6 +44,11 @@ async def lifespan(app: FastAPI):
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
@@ -72,13 +79,67 @@ async def archive_sheets_cronjob(frequency: str, interval: int, current_time_uni
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:
-
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()
triggered_jobs.append({"sheet_id": s.id, "task_id": task.id})
logger.info(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_NOTIFY_DAYS * 24 * 60 * 60
+
+@repeat_every(seconds=DELETE_WINDOW, wait_first=180, on_exception=logger.error)
+async def notify_about_expired_archives():
+ notify_from = datetime.datetime.now() + datetime.timedelta(days=get_settings().DELETE_SCHEDULED_ARCHIVES_NOTIFY_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} ' 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_NOTIFY_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
+ {list_of_archives}
+
+ Best, The Auto Archiver team
+
+
+ """,
+ subtype=MessageType.html
+ )
+ await fastmail.send_message(message)
+ logger.info(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=DELETE_WINDOW - (60 * 60), seconds=0, on_exception=logger.error)
+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.info(f"[CRON] Deleted {count_deleted} archives.")
+
+
+
@repeat_every(seconds=86400, wait_first=150, on_exception=logger.error)
async def delete_stale_sheets():
STALE_DAYS = get_settings().DELETE_STALE_SHEETS_DAYS
@@ -112,4 +173,3 @@ async def delete_stale_sheets():
)
await fastmail.send_message(message)
logger.info(f"[CRON] Email sent to {email} about stale sheets deletion.")
-
diff --git a/src/db/crud.py b/src/db/crud.py
index d984a4f..0742ca7 100644
--- a/src/db/crud.py
+++ b/src/db/crud.py
@@ -15,12 +15,17 @@ from sqlalchemy.ext.asyncio import AsyncSession
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))
+# --------------- 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 get_archive(db: Session, id: str, email: str):
query = base_query(db).filter(models.Archive.id == id)
@@ -29,8 +34,7 @@ def get_archive(db: Session, id: str, email: str):
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):
+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)-> list[models.Archive]:
# searches for partial URLs, if email is * no ownership filtering happens
query = base_query(db)
if email != ALLOW_ANY_EMAIL:
@@ -52,7 +56,7 @@ def search_archives_by_email(db: Session, email: str, skip: int = 0, limit: int
#TODO: rename task to archive
def create_task(db: Session, task: schemas.ArchiveCreate, tags: list[models.Tag], urls: list[models.ArchiveUrl]) -> models.Archive:
- 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, sheet_id=task.sheet_id)
+ 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, sheet_id=task.sheet_id, store_until=task.store_until)
db_task.tags = tags
db_task.urls = urls
db.add(db_task)
@@ -90,13 +94,21 @@ def count_by_user_since(db: Session, seconds_delta: int = 15):
.order_by(func.count().desc())\
.limit(500).all()
+async def find_by_store_until(db: AsyncSession, store_until_is_before:datetime) -> dict:
+ res = await db.execute(
+ select(models.Archive)
+ .filter(models.Archive.deleted ==False, models.Archive.store_until < store_until_is_before)
+ )
+ return res.scalars()
-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))
-
+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
@@ -269,7 +281,7 @@ async def delete_stale_sheets(db: AsyncSession, inactivity_days: int) -> dict:
for sheet in result.scalars():
await db.delete(sheet)
deleted[sheet.author_id].append(sheet)
- await db.commit()
+ await db.commit()
return dict(deleted)
def update_sheet_last_url_archived_at(db: Session, sheet_id: str):
diff --git a/src/db/models.py b/src/db/models.py
index 41d2c3c..5447531 100644
--- a/src/db/models.py
+++ b/src/db/models.py
@@ -37,6 +37,7 @@ class Archive(Base):
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"))
diff --git a/src/db/schemas.py b/src/db/schemas.py
index 850fa3a..aff5730 100644
--- a/src/db/schemas.py
+++ b/src/db/schemas.py
@@ -38,6 +38,7 @@ class ArchiveResult(BaseModel):
url: str
result: dict
created_at: datetime
+ store_until: datetime | None
class Task(BaseModel):
@@ -82,6 +83,7 @@ class ArchiveCreate(ArchiveTrigger):
result: dict | None = None
sheet_id: str | None = None
urls: list | None = None
+ store_until: datetime | None = None
class Archive(ArchiveCreate):
created_at: datetime
diff --git a/src/migrations/versions/02b2f6d17ed0_create_archives_store_until_column.py b/src/migrations/versions/02b2f6d17ed0_create_archives_store_until_column.py
new file mode 100644
index 0000000..d00fa2c
--- /dev/null
+++ b/src/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/src/shared/settings.py b/src/shared/settings.py
index f7383dd..8a7cf31 100644
--- a/src/shared/settings.py
+++ b/src/shared/settings.py
@@ -20,6 +20,8 @@ class Settings(BaseSettings):
CRON_ARCHIVE_SHEETS: bool = False
CRON_DELETE_STALE_SHEETS: bool = True
DELETE_STALE_SHEETS_DAYS: int = 14
+ CRON_DELETE_SCHEDULED_ARCHIVES: bool = True
+ DELETE_SCHEDULED_ARCHIVES_NOTIFY_DAYS: int = 14
# database
DATABASE_PATH: str
diff --git a/src/worker/main.py b/src/worker/main.py
index 940158b..df8ffb8 100644
--- a/src/worker/main.py
+++ b/src/worker/main.py
@@ -37,6 +37,8 @@ def create_archive_task(self, archive_json: str):
assert result, f"UNABLE TO archive: {archive.url}"
# prepare and insert in DB
+ store_until = get_store_until(archive.group_id)
+ archive.store_until = store_until
archive.id = self.request.id
archive.urls = get_all_urls(result)
archive.result = json.loads(result.to_json())
@@ -64,7 +66,8 @@ def create_sheet_task(self, sheet_json: str):
id=models.generate_uuid(),
result=json.loads(result.to_json()),
sheet_id=sheet.sheet_id,
- urls=get_all_urls(result)
+ urls=get_all_urls(result),
+ store_until = get_store_until(sheet.group_id)
)
insert_result_into_db(archive)
stats["archived"] += 1
@@ -122,6 +125,14 @@ def get_all_urls(result: Metadata) -> List[models.ArchiveUrl]:
return db_urls
+def get_store_until(group_id: str) -> datetime.datetime:
+ with get_db() as session:
+ group = crud.get_group(session, group_id)
+ 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)
+
# TODO: this should live within the auto-archiver??
def convert_if_media(media):
if isinstance(media, Media): return media
From 937b69ffcc08b8dd2bf8992f31c813ef121f70fd Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Sat, 8 Feb 2025 23:33:02 +0000
Subject: [PATCH 30/75] new schemas
---
src/db/schemas.py | 17 +++++++++++++----
1 file changed, 13 insertions(+), 4 deletions(-)
diff --git a/src/db/schemas.py b/src/db/schemas.py
index aff5730..b76e711 100644
--- a/src/db/schemas.py
+++ b/src/db/schemas.py
@@ -7,12 +7,12 @@ from datetime import datetime
class SubmitSheet(BaseModel):
sheet_id: str | None
author_id: str | None = None
- group_id: str | None
+ group_id: str = "default"
tags: set[str] | None = set()
columns: dict | None = {} # TODO: implement/remove
-class SubmitManual(BaseModel):
+class SubmitManual(BaseModel): # deprecated
result: str # should be a Metadata.to_json()
public: bool = False
author_id: str | None = None
@@ -54,7 +54,7 @@ class TaskDelete(Task):
deleted: bool
-class ActiveUser(BaseModel):
+class ActiveUser(BaseModel):
active: bool
@@ -78,6 +78,7 @@ class ArchiveTrigger(BaseModel):
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
@@ -85,6 +86,7 @@ class ArchiveCreate(ArchiveTrigger):
urls: list | None = None
store_until: datetime | None = None
+
class Archive(ArchiveCreate):
created_at: datetime
updated_at: datetime | None
@@ -98,11 +100,18 @@ class Usage(BaseModel):
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
\ No newline at end of file
+ stats: dict
+
+
+class SubmitManualArchive(ArchiveTrigger):
+ url: None = None
+ result: str # should be a Metadata.to_json()
From cdf99509883402a5d925cd2878e1faf9d1f17c4d Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Sat, 8 Feb 2025 23:33:10 +0000
Subject: [PATCH 31/75] updates interop endpoints
---
src/endpoints/interoperability.py | 30 ++++++++++++++-----
src/tests/endpoints/test_interoperability.py | 31 +++++++++++++++-----
2 files changed, 47 insertions(+), 14 deletions(-)
diff --git a/src/endpoints/interoperability.py b/src/endpoints/interoperability.py
index 5758dc1..752f7e8 100644
--- a/src/endpoints/interoperability.py
+++ b/src/endpoints/interoperability.py
@@ -1,12 +1,13 @@
+import json
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import JSONResponse
from auto_archiver import Metadata
-from loguru import logger
import sqlalchemy
+from core.config import ALLOW_ANY_EMAIL
from web.security import token_api_key_auth
from db import models, schemas
-from worker.main import insert_result_into_db
+from worker.main import insert_result_into_db, get_all_urls, get_store_until
from core.logging import log_error
@@ -15,13 +16,28 @@ interoperability_router = APIRouter(prefix="/interop", tags=["Interoperability e
# ----- 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}")
+def submit_manual_archive(
+ manual: schemas.SubmitManualArchive,
+ auth=Depends(token_api_key_auth)
+):
+ result: Metadata = Metadata.from_json(manual.result)
+ manual.author_id = manual.author_id or ALLOW_ANY_EMAIL
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())
+ 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=get_store_until(manual.group_id),
+ )
+ archive_id = insert_result_into_db(archive)
except sqlalchemy.exc.IntegrityError as e:
log_error(e)
- raise HTTPException(status_code=422, detail=f"Cannot insert into DB due to integrity error")
+ raise HTTPException(status_code=422, detail=f"Cannot insert into DB due to integrity error, likely duplicate urls.")
return JSONResponse({"id": archive_id}, status_code=201)
diff --git a/src/tests/endpoints/test_interoperability.py b/src/tests/endpoints/test_interoperability.py
index 8021bfe..fa97d86 100644
--- a/src/tests/endpoints/test_interoperability.py
+++ b/src/tests/endpoints/test_interoperability.py
@@ -1,4 +1,9 @@
+from datetime import datetime
import json
+from unittest.mock import patch
+
+from core.config import ALLOW_ANY_EMAIL
+from db import crud
def test_submit_manual_archive_unauthenticated(client, test_no_auth):
@@ -9,15 +14,27 @@ def test_submit_manual_archive_not_user_auth(client_with_auth, test_no_auth):
test_no_auth(client_with_auth.post, "/interop/submit-archive")
-def test_submit_manual_archive(client_with_token):
- aa_metadata = json.dumps({"status": "test: success", "metadata": {"url": "http://example.com"}, "media": []})
-
- r = client_with_token.post("/interop/submit-archive", json={"result": aa_metadata, "public": False, "author_id": "jerry@gmail.com", "group_id": None, "tags": ["test"]})
+@patch("endpoints.interoperability.get_store_until", return_value=datetime.now())
+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"]})
assert r.status_code == 201
assert "id" in r.json()
- # cannot have the same URL twice
+ inserted = crud.get_archive(db_session, r.json()["id"], ALLOW_ANY_EMAIL)
+ 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", "group_id": None, "tags": ["test"]})
+ r = client_with_token.post("/interop/submit-archive", json={"result": aa_metadata, "public": False, "author_id": "jerry@gmail.com", "tags": ["test"]})
assert r.status_code == 422
- assert r.json() == {"detail": "Cannot insert into DB due to integrity error"}
+ assert r.json() == {"detail": "Cannot insert into DB due to integrity error, likely duplicate urls."}
From a374c0e662302e71bce6ed092281067513ba9a13 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Sat, 8 Feb 2025 23:33:20 +0000
Subject: [PATCH 32/75] fixing tests
---
src/tests/endpoints/test_url.py | 9 ++-
src/tests/worker/test_worker_main.py | 108 +++++++++++++--------------
2 files changed, 55 insertions(+), 62 deletions(-)
diff --git a/src/tests/endpoints/test_url.py b/src/tests/endpoints/test_url.py
index 3d85e71..6128291 100644
--- a/src/tests/endpoints/test_url.py
+++ b/src/tests/endpoints/test_url.py
@@ -39,16 +39,17 @@ def test_archive_url(m_celery, m2, client_with_auth):
m_signature.delay.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": True, "author_id": "rick@example.com", "group_id": None, "tags": [], "sheet_id": None}
+ 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_once_with("new-group")
+ m_user_state.in_group.assert_called_with("new-group")
# user is in group
m_user_state.in_group.return_value = True
@@ -131,7 +132,7 @@ def test_search_by_url(client_with_auth, client_with_token, db_session):
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), [], [])
+ 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"), [], [])
# 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")
@@ -184,7 +185,7 @@ def test_delete_task(client_with_auth, db_session):
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), [], [])
+ crud.create_task(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
diff --git a/src/tests/worker/test_worker_main.py b/src/tests/worker/test_worker_main.py
index f82a80c..edebd5f 100644
--- a/src/tests/worker/test_worker_main.py
+++ b/src/tests/worker/test_worker_main.py
@@ -1,3 +1,4 @@
+from datetime import datetime
from unittest import mock
from unittest.mock import MagicMock, patch
@@ -9,68 +10,74 @@ 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")
+ archive = schemas.ArchiveCreate(url=URL, tags=["tag-celery"], public=True, author_id="rick@example.com", group_id="interstellar")
@patch("worker.main.insert_result_into_db")
- @patch("worker.main.is_group_invalid_for_user", return_value=None)
- # @patch("worker.main.choose_orchestrator")
+ @patch("worker.main.get_store_until", return_value=datetime.now())
+ @patch("worker.main.load_orchestrator")
@patch("celery.app.task.Task.request")
- def test_success(self, m_req, m_choose, m_is_group, m_insert, worker_init, db_session):
+ def test_success(self, m_req, m_load, m_store, m_insert, db_session):
from worker.main import create_archive_task
m_req.id = "this-just-in"
- mock_orchestrator = self.mock_orchestrator_choice(m_choose)
+ mock_orchestrator = self.mock_orchestrator_choice(m_load)
task = create_archive_task(self.archive.model_dump_json())
- m_choose.assert_called_once()
+ m_load.assert_called_once_with("interstellar")
+ m_store.assert_called_once_with("interstellar")
+ m_insert.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):
+ def test_raise_invalid(self):
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):
+ @patch("worker.main.load_orchestrator")
+ def test_raise_db_error(self, m_load, m_insert):
from worker.main import create_archive_task
- mock_orchestrator = self.mock_orchestrator_choice(m_choose)
+ mock_orchestrator = self.mock_orchestrator_choice(m_load)
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):
+
+ @patch("worker.main.insert_result_into_db", return_value=None)
+ @patch("worker.main.load_orchestrator")
+ def test_raise_empty_result(self, m_load, m_insert):
+ from worker.main import create_archive_task
+ mock_orchestrator = self.mock_orchestrator_choice(m_load)
+
+ with pytest.raises(Exception) as e:
+ create_archive_task(self.archive.model_dump_json())
+ assert "UNABLE TO archive" in str(e)
+ mock_orchestrator.feed_item.assert_called_once()
+
+ def mock_orchestrator_choice(self, m_load):
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
+ m_load.return_value = mock_orchestrator
return mock_orchestrator
class Test_create_sheet_task():
URL = "https://example-live.com"
- sheet = schemas.SubmitSheet(sheet_id="123", author_id="rick@example.com", group_id=None)
+ sheet = schemas.SubmitSheet(sheet_id="123", author_id="rick@example.com", group_id="interstellar", tags=["spaceship"])
- # @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):
+ @patch("worker.main.get_store_until", return_value=datetime.now())
+ @patch("worker.main.load_orchestrator")
+ def test_success(self, m_load, m_store, m_uuid, db_session):
from worker.main import create_sheet_task
assert db_session.query(models.Archive).filter(models.Archive.url == self.URL).count() == 0
@@ -79,50 +86,35 @@ class Test_create_sheet_task():
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
+ m_load.return_value = m_orch
res = create_sheet_task(self.sheet.model_dump_json())
- assert res["archived"] == 1
- assert res["failed"] == 0
- assert len(res["errors"]) == 0
- assert res["sheet"] == "Sheet"
+
+ m_load.assert_called_once_with("interstellar", True, {'configurations': {'gsheet_feeder': {'sheet_id': '123'}}})
+ m_orch.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"] == True
- assert len(res["time"]) > 0
+ 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 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
+ 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(worker_init, db_session):
+def test_get_all_urls(db_session):
from worker.main import get_all_urls
from auto_archiver import Metadata
From a1b730bad43cadef41278c31fff1f65f9651c209 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Sun, 9 Feb 2025 12:48:33 +0000
Subject: [PATCH 33/75] adds group permissions
---
src/db/user_state.py | 8 ++++----
src/endpoints/default.py | 4 ++--
src/shared/user_groups.py | 5 +++++
3 files changed, 11 insertions(+), 6 deletions(-)
diff --git a/src/db/user_state.py b/src/db/user_state.py
index 0091398..06077ce 100644
--- a/src/db/user_state.py
+++ b/src/db/user_state.py
@@ -5,7 +5,7 @@ from sqlalchemy.orm import Session
from sqlalchemy import func
from db import crud, models
from datetime import datetime
-from shared.user_groups import GroupPermissions
+from shared.user_groups import GroupInfo, GroupPermissions
from db.schemas import Usage, UsageResponse
@@ -19,13 +19,13 @@ class UserState:
self.email = email.lower()
@property
- def permissions(self) -> Dict[str, GroupPermissions]:
+ 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"] = GroupPermissions(
+ self._permissions["all"] = GroupInfo(
read=self.read,
read_public=self.read_public,
archive_url=self.archive_url,
@@ -38,7 +38,7 @@ class UserState:
)
for group in self.user_groups:
if not group.permissions: continue
- self._permissions[group.id] = GroupPermissions(**group.permissions)
+ self._permissions[group.id] = GroupInfo(**group.permissions, description=group.description)
return self._permissions
@property
diff --git a/src/endpoints/default.py b/src/endpoints/default.py
index ceef172..80921ce 100644
--- a/src/endpoints/default.py
+++ b/src/endpoints/default.py
@@ -10,7 +10,7 @@ from db import crud
from db.schemas import ActiveUser, UsageResponse
from db.user_state import UserState
from web.security import get_user_auth, bearer_security, get_user_state
-from shared.user_groups import GroupPermissions
+from shared.user_groups import GroupInfo
default_router = APIRouter()
@@ -42,7 +42,7 @@ async def 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, GroupPermissions]:
+) -> 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.")
diff --git a/src/shared/user_groups.py b/src/shared/user_groups.py
index d36ab4b..7c04c89 100644
--- a/src/shared/user_groups.py
+++ b/src/shared/user_groups.py
@@ -123,3 +123,8 @@ class UserGroupModel(BaseModel):
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_emails: list[str] = []
\ No newline at end of file
From f8c45e2d920b586beac1a83437cd9a466d141f99 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Mon, 10 Feb 2025 00:41:50 +0000
Subject: [PATCH 34/75] major refactor of structure for worker V web:
docker/app/secrets/envs/...
---
src/.env.alembic => .env.alembic | 2 +-
src/.env.test => .env.test | 4 +-
.example.env | 10 +-
.github/workflows/ci.yml | 2 +-
.gitignore | 17 +-
Makefile | 5 +
src/Pipfile => Pipfile | 2 -
src/Pipfile.lock => Pipfile.lock | 552 +++++++++---------
src/alembic.ini => alembic.ini | 2 +-
{src => app}/example.user-groups.yaml | 0
{src => app}/logs/.gitkeep | 0
{src => app}/migrations/env.py | 3 +-
{src => app}/migrations/script.py.mako | 0
{src => app}/migrations/versions/.gitkeep | 0
...7ed0_create_archives_store_until_column.py | 0
...24ec4b1_rename_sheets_last_archived_col.py | 0
...21d2c96d8_add_sheet_id_to_archive_table.py | 0
...45b_modify_archive_url_to_have_uuid_id_.py | 0
...vacuum_database_if_there_s_enough_space.py | 0
.../a23aaf3ae930_drop_active_column.py | 0
...a012ec405b8_add_columns_to_groups_table.py | 0
{src/core => app/shared}/__init__.py | 0
app/shared/aa_utils.py | 33 ++
app/shared/business_logic.py | 15 +
{src/core => app/shared}/config.py | 0
{src/endpoints => app/shared/db}/__init__.py | 0
{src => app/shared}/db/crud.py | 17 +-
{src => app/shared}/db/database.py | 3 +-
{src => app/shared}/db/models.py | 0
{src => app/shared}/db/user_state.py | 6 +-
app/shared/log.py | 13 +
{src/db => app/shared}/schemas.py | 0
{src => app}/shared/settings.py | 6 +-
{src => app}/shared/task_messaging.py | 4 +-
{src => app}/shared/user_groups.py | 12 +-
{src => app/shared}/utils/misc.py | 7 -
{src => app}/tests/conftest.py | 2 +-
{src => app}/tests/db/test_crud.py | 10 +-
{src => app}/tests/db/test_models.py | 0
{src => app}/tests/endpoints/test_default.py | 6 +-
.../tests/endpoints/test_interoperability.py | 2 +-
{src => app}/tests/endpoints/test_sheet.py | 2 +-
{src => app}/tests/endpoints/test_task.py | 0
{src => app}/tests/endpoints/test_url.py | 2 +-
{src => app}/tests/orchestration.test.yaml | 0
.../tests/user-groups.test.broken.yaml | 0
{src => app}/tests/user-groups.test.yaml | 0
{src => app}/tests/web/test_main.py | 2 +-
{src => app}/tests/web/test_security.py | 2 +-
{src => app}/tests/worker/test_worker_main.py | 0
app/web/__init__.py | 3 +
{src/shared => app/web/endpoints}/__init__.py | 0
{src => app/web}/endpoints/default.py | 17 +-
.../web}/endpoints/interoperability.py | 28 +-
{src => app/web}/endpoints/sheet.py | 11 +-
{src => app/web}/endpoints/task.py | 12 +-
{src => app/web}/endpoints/url.py | 14 +-
{src/core => app/web}/events.py | 22 +-
{src => app}/web/main.py | 50 +-
src/core/logging.py => app/web/middleware.py | 17 +-
{src => app}/web/security.py | 10 +-
{src => app/web}/static/.gitkeep | 0
{src => app/web}/static/favicon.ico | Bin
{src => app/web}/utils/__init__.py | 0
{src => app/web}/utils/metrics.py | 11 +-
app/web/utils/misc.py | 7 +
{src => app}/worker/__init__.py | 0
{src => app}/worker/main.py | 60 +-
docker-compose.dev.yml | 21 +-
docker-compose.yml | 43 +-
src/.example.env | 7 -
src/db/__init__.py | 1 -
src/web/__init__.py | 4 -
src/Dockerfile => worker.Dockerfile | 11 +-
74 files changed, 567 insertions(+), 525 deletions(-)
rename src/.env.alembic => .env.alembic (71%)
rename src/.env.test => .env.test (72%)
rename src/Pipfile => Pipfile (93%)
rename src/Pipfile.lock => Pipfile.lock (88%)
rename src/alembic.ini => alembic.ini (98%)
rename {src => app}/example.user-groups.yaml (100%)
rename {src => app}/logs/.gitkeep (100%)
rename {src => app}/migrations/env.py (97%)
rename {src => app}/migrations/script.py.mako (100%)
rename {src => app}/migrations/versions/.gitkeep (100%)
rename {src => app}/migrations/versions/02b2f6d17ed0_create_archives_store_until_column.py (100%)
rename {src => app}/migrations/versions/1636724ec4b1_rename_sheets_last_archived_col.py (100%)
rename {src => app}/migrations/versions/89121d2c96d8_add_sheet_id_to_archive_table.py (100%)
rename {src => app}/migrations/versions/9369a264945b_modify_archive_url_to_have_uuid_id_.py (100%)
rename {src => app}/migrations/versions/93a611e4c066_vacuum_database_if_there_s_enough_space.py (100%)
rename {src => app}/migrations/versions/a23aaf3ae930_drop_active_column.py (100%)
rename {src => app}/migrations/versions/fa012ec405b8_add_columns_to_groups_table.py (100%)
rename {src/core => app/shared}/__init__.py (100%)
create mode 100644 app/shared/aa_utils.py
create mode 100644 app/shared/business_logic.py
rename {src/core => app/shared}/config.py (100%)
rename {src/endpoints => app/shared/db}/__init__.py (100%)
rename {src => app/shared}/db/crud.py (97%)
rename {src => app/shared}/db/database.py (97%)
rename {src => app/shared}/db/models.py (100%)
rename {src => app/shared}/db/user_state.py (98%)
create mode 100644 app/shared/log.py
rename {src/db => app/shared}/schemas.py (100%)
rename {src => app}/shared/settings.py (92%)
rename {src => app}/shared/task_messaging.py (75%)
rename {src => app}/shared/user_groups.py (94%)
rename {src => app/shared}/utils/misc.py (66%)
rename {src => app}/tests/conftest.py (98%)
rename {src => app}/tests/db/test_crud.py (98%)
rename {src => app}/tests/db/test_models.py (100%)
rename {src => app}/tests/endpoints/test_default.py (97%)
rename {src => app}/tests/endpoints/test_interoperability.py (97%)
rename {src => app}/tests/endpoints/test_sheet.py (99%)
rename {src => app}/tests/endpoints/test_task.py (100%)
rename {src => app}/tests/endpoints/test_url.py (99%)
rename {src => app}/tests/orchestration.test.yaml (100%)
rename {src => app}/tests/user-groups.test.broken.yaml (100%)
rename {src => app}/tests/user-groups.test.yaml (100%)
rename {src => app}/tests/web/test_main.py (96%)
rename {src => app}/tests/web/test_security.py (99%)
rename {src => app}/tests/worker/test_worker_main.py (100%)
create mode 100644 app/web/__init__.py
rename {src/shared => app/web/endpoints}/__init__.py (100%)
rename {src => app/web}/endpoints/default.py (81%)
rename {src => app/web}/endpoints/interoperability.py (61%)
rename {src => app/web}/endpoints/sheet.py (91%)
rename {src => app/web}/endpoints/task.py (85%)
rename {src => app/web}/endpoints/url.py (90%)
rename {src/core => app/web}/events.py (92%)
rename {src => app}/web/main.py (81%)
rename src/core/logging.py => app/web/middleware.py (53%)
rename {src => app}/web/security.py (94%)
rename {src => app/web}/static/.gitkeep (100%)
rename {src => app/web}/static/favicon.ico (100%)
rename {src => app/web}/utils/__init__.py (100%)
rename {src => app/web}/utils/metrics.py (93%)
create mode 100644 app/web/utils/misc.py
rename {src => app}/worker/__init__.py (100%)
rename {src => app}/worker/main.py (73%)
delete mode 100644 src/.example.env
delete mode 100644 src/db/__init__.py
delete mode 100644 src/web/__init__.py
rename src/Dockerfile => worker.Dockerfile (69%)
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/src/.env.test b/.env.test
similarity index 72%
rename from src/.env.test
rename to .env.test
index 5e5ea24..f7da607 100644
--- a/src/.env.test
+++ b/.env.test
@@ -5,5 +5,5 @@ 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
+SHEET_ORCHESTRATION_YAML=app/tests/orchestration.test.yaml
\ No newline at end of file
diff --git a/.example.env b/.example.env
index e8970b2..b21cf10 100644
--- a/.example.env
+++ b/.example.env
@@ -1 +1,9 @@
-REDIS_PASSWORD=TODO
\ No newline at end of file
+REDIS_PASSWORD=TODO
+
+DATABASE_PATH="sqlite:///./database/auto-archiver.db"
+USER_GROUPS_FILENAME=app/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/.github/workflows/ci.yml b/.github/workflows/ci.yml
index bf9ca3b..dbc4515 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -35,7 +35,7 @@ jobs:
- name: Install dependencies
run: pipenv install --dev
working-directory: src
-
+#TODO: fix working-directories here
- 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
diff --git a/.gitignore b/.gitignore
index ec92013..e937aa2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,26 +2,25 @@ 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
-.pytest_cache
\ No newline at end of file
+copy-files.sh
\ No newline at end of file
diff --git a/Makefile b/Makefile
index e4002d9..57f5e0e 100644
--- a/Makefile
+++ b/Makefile
@@ -6,6 +6,11 @@ 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
+
+dev-redis-only:
+ docker compose -f docker-compose.yml -f docker-compose.dev.yml build redis
+ docker compose -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
diff --git a/src/Pipfile b/Pipfile
similarity index 93%
rename from src/Pipfile
rename to Pipfile
index 811903f..90244d0 100644
--- a/src/Pipfile
+++ b/Pipfile
@@ -5,7 +5,6 @@ name = "pypi"
[packages]
oscrypto = {git = "https://github.com/wbond/oscrypto.git", ref = "d5f3437ed24257895ae1edd9e503cfb352e635a8"}
-aiofiles = "==0.6.0"
celery = ">=5.0"
fastapi = "*"
jinja2 = "*"
@@ -13,7 +12,6 @@ redis = "==3.5.3"
requests = ">=2.25.1"
uvicorn = ">=0.13.4"
aiosqlite = "*"
-python-dotenv = "*"
loguru = "*"
sqlalchemy = "*"
alembic = "*"
diff --git a/src/Pipfile.lock b/Pipfile.lock
similarity index 88%
rename from src/Pipfile.lock
rename to Pipfile.lock
index 0d430ab..e03b705 100644
--- a/src/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "14a7ead66a74419eebfa72478bbb7e3efe378df9f41e401738faa2871f5c4344"
+ "sha256": "f03b75b94f11f10065e9fd4b4f107a78c37f880f4537b4b19fd0aaad7afa9ab7"
},
"pipfile-spec": 6,
"requires": {
@@ -16,103 +16,100 @@
]
},
"default": {
- "aiofiles": {
- "hashes": [
- "sha256:bd3019af67f83b739f8e4053c6c0512a7f545b9a8d91aaeab55e6e0f9d123c27",
- "sha256:e0281b157d3d5d59d803e3f4557dcc9a3dff28a4dd4829a9ff478adae50ca092"
- ],
- "index": "pypi",
- "version": "==0.6.0"
- },
"aiohappyeyeballs": {
"hashes": [
- "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745",
- "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8"
+ "sha256:147ec992cf873d74f5062644332c539fcd42956dc69453fe5204195e560517e1",
+ "sha256:9b05052f9042985d32ecbe4b59a77ae19c006a78f1344d7fdad69d28ded3d0b0"
],
- "markers": "python_version >= '3.8'",
- "version": "==2.4.4"
+ "markers": "python_version >= '3.9'",
+ "version": "==2.4.6"
},
"aiohttp": {
"hashes": [
- "sha256:0882c2820fd0132240edbb4a51eb8ceb6eef8181db9ad5291ab3332e0d71df5f",
- "sha256:0a6d3fbf2232e3a08c41eca81ae4f1dff3d8f1a30bae415ebe0af2d2458b8a33",
- "sha256:0b7fb429ab1aafa1f48578eb315ca45bd46e9c37de11fe45c7f5f4138091e2f1",
- "sha256:0eb98d90b6690827dcc84c246811feeb4e1eea683c0eac6caed7549be9c84665",
- "sha256:0fd82b8e9c383af11d2b26f27a478640b6b83d669440c0a71481f7c865a51da9",
- "sha256:10b4ff0ad793d98605958089fabfa350e8e62bd5d40aa65cdc69d6785859f94e",
- "sha256:1642eceeaa5ab6c9b6dfeaaa626ae314d808188ab23ae196a34c9d97efb68350",
- "sha256:1dac54e8ce2ed83b1f6b1a54005c87dfed139cf3f777fdc8afc76e7841101226",
- "sha256:1e69966ea6ef0c14ee53ef7a3d68b564cc408121ea56c0caa2dc918c1b2f553d",
- "sha256:1f21bb8d0235fc10c09ce1d11ffbd40fc50d3f08a89e4cf3a0c503dc2562247a",
- "sha256:2170816e34e10f2fd120f603e951630f8a112e1be3b60963a1f159f5699059a6",
- "sha256:21fef42317cf02e05d3b09c028712e1d73a9606f02467fd803f7c1f39cc59add",
- "sha256:249cc6912405917344192b9f9ea5cd5b139d49e0d2f5c7f70bdfaf6b4dbf3a2e",
- "sha256:3499c7ffbfd9c6a3d8d6a2b01c26639da7e43d47c7b4f788016226b1e711caa8",
- "sha256:3af41686ccec6a0f2bdc66686dc0f403c41ac2089f80e2214a0f82d001052c03",
- "sha256:3e23419d832d969f659c208557de4a123e30a10d26e1e14b73431d3c13444c2e",
- "sha256:3ea1b59dc06396b0b424740a10a0a63974c725b1c64736ff788a3689d36c02d2",
- "sha256:44167fc6a763d534a6908bdb2592269b4bf30a03239bcb1654781adf5e49caf1",
- "sha256:479b8c6ebd12aedfe64563b85920525d05d394b85f166b7873c8bde6da612f9c",
- "sha256:4af57160800b7a815f3fe0eba9b46bf28aafc195555f1824555fa2cfab6c1538",
- "sha256:4b4fa1cb5f270fb3eab079536b764ad740bb749ce69a94d4ec30ceee1b5940d5",
- "sha256:4eed954b161e6b9b65f6be446ed448ed3921763cc432053ceb606f89d793927e",
- "sha256:541d823548ab69d13d23730a06f97460f4238ad2e5ed966aaf850d7c369782d9",
- "sha256:568c1236b2fde93b7720f95a890741854c1200fba4a3471ff48b2934d2d93fd3",
- "sha256:5854be2f3e5a729800bac57a8d76af464e160f19676ab6aea74bde18ad19d438",
- "sha256:620598717fce1b3bd14dd09947ea53e1ad510317c85dda2c9c65b622edc96b12",
- "sha256:6526e5fb4e14f4bbf30411216780c9967c20c5a55f2f51d3abd6de68320cc2f3",
- "sha256:6fba278063559acc730abf49845d0e9a9e1ba74f85f0ee6efd5803f08b285853",
- "sha256:70d1f9dde0e5dd9e292a6d4d00058737052b01f3532f69c0c65818dac26dc287",
- "sha256:731468f555656767cda219ab42e033355fe48c85fbe3ba83a349631541715ba2",
- "sha256:81b8fe282183e4a3c7a1b72f5ade1094ed1c6345a8f153506d114af5bf8accd9",
- "sha256:84a585799c58b795573c7fa9b84c455adf3e1d72f19a2bf498b54a95ae0d194c",
- "sha256:85992ee30a31835fc482468637b3e5bd085fa8fe9392ba0bdcbdc1ef5e9e3c55",
- "sha256:8811f3f098a78ffa16e0ea36dffd577eb031aea797cbdba81be039a4169e242c",
- "sha256:88a12ad8ccf325a8a5ed80e6d7c3bdc247d66175afedbe104ee2aaca72960d8e",
- "sha256:8be8508d110d93061197fd2d6a74f7401f73b6d12f8822bbcd6d74f2b55d71b1",
- "sha256:8e2bf8029dbf0810c7bfbc3e594b51c4cc9101fbffb583a3923aea184724203c",
- "sha256:929f3ed33743a49ab127c58c3e0a827de0664bfcda566108989a14068f820194",
- "sha256:92cde43018a2e17d48bb09c79e4d4cb0e236de5063ce897a5e40ac7cb4878773",
- "sha256:92fc484e34b733704ad77210c7957679c5c3877bd1e6b6d74b185e9320cc716e",
- "sha256:943a8b052e54dfd6439fd7989f67fc6a7f2138d0a2cf0a7de5f18aa4fe7eb3b1",
- "sha256:9d73ee3725b7a737ad86c2eac5c57a4a97793d9f442599bea5ec67ac9f4bdc3d",
- "sha256:9f5b3c1ed63c8fa937a920b6c1bec78b74ee09593b3f5b979ab2ae5ef60d7600",
- "sha256:9fd46ce0845cfe28f108888b3ab17abff84ff695e01e73657eec3f96d72eef34",
- "sha256:a344d5dc18074e3872777b62f5f7d584ae4344cd6006c17ba12103759d407af3",
- "sha256:a60804bff28662cbcf340a4d61598891f12eea3a66af48ecfdc975ceec21e3c8",
- "sha256:a8f5f7515f3552d899c61202d99dcb17d6e3b0de777900405611cd747cecd1b8",
- "sha256:a9b7371665d4f00deb8f32208c7c5e652059b0fda41cf6dbcac6114a041f1cc2",
- "sha256:aa54f8ef31d23c506910c21163f22b124facb573bff73930735cf9fe38bf7dff",
- "sha256:aba807f9569455cba566882c8938f1a549f205ee43c27b126e5450dc9f83cc62",
- "sha256:ae545f31489548c87b0cced5755cfe5a5308d00407000e72c4fa30b19c3220ac",
- "sha256:af01e42ad87ae24932138f154105e88da13ce7d202a6de93fafdafb2883a00ef",
- "sha256:b540bd67cfb54e6f0865ceccd9979687210d7ed1a1cc8c01f8e67e2f1e883d28",
- "sha256:b6212a60e5c482ef90f2d788835387070a88d52cf6241d3916733c9176d39eab",
- "sha256:b63de12e44935d5aca7ed7ed98a255a11e5cb47f83a9fded7a5e41c40277d104",
- "sha256:ba74ec819177af1ef7f59063c6d35a214a8fde6f987f7661f4f0eecc468a8f76",
- "sha256:bb49c7f1e6ebf3821a42d81d494f538107610c3a705987f53068546b0e90303e",
- "sha256:bd176afcf8f5d2aed50c3647d4925d0db0579d96f75a31e77cbaf67d8a87742d",
- "sha256:bd7227b87a355ce1f4bf83bfae4399b1f5bb42e0259cb9405824bd03d2f4336a",
- "sha256:bf8d9bfee991d8acc72d060d53860f356e07a50f0e0d09a8dfedea1c554dd0d5",
- "sha256:bfde76a8f430cf5c5584553adf9926534352251d379dcb266ad2b93c54a29745",
- "sha256:c341c7d868750e31961d6d8e60ff040fb9d3d3a46d77fd85e1ab8e76c3e9a5c4",
- "sha256:c7a06301c2fb096bdb0bd25fe2011531c1453b9f2c163c8031600ec73af1cc99",
- "sha256:cb23d8bb86282b342481cad4370ea0853a39e4a32a0042bb52ca6bdde132df43",
- "sha256:d119fafe7b634dbfa25a8c597718e69a930e4847f0b88e172744be24515140da",
- "sha256:d40f9da8cabbf295d3a9dae1295c69975b86d941bc20f0a087f0477fa0a66231",
- "sha256:d6c9af134da4bc9b3bd3e6a70072509f295d10ee60c697826225b60b9959acdd",
- "sha256:dd7659baae9ccf94ae5fe8bfaa2c7bc2e94d24611528395ce88d009107e00c6d",
- "sha256:de8d38f1c2810fa2a4f1d995a2e9c70bb8737b18da04ac2afbf3971f65781d87",
- "sha256:e595c591a48bbc295ebf47cb91aebf9bd32f3ff76749ecf282ea7f9f6bb73886",
- "sha256:ec2aa89305006fba9ffb98970db6c8221541be7bee4c1d027421d6f6df7d1ce2",
- "sha256:ec82bf1fda6cecce7f7b915f9196601a1bd1a3079796b76d16ae4cce6d0ef89b",
- "sha256:ed9ee95614a71e87f1a70bc81603f6c6760128b140bc4030abe6abaa988f1c3d",
- "sha256:f047569d655f81cb70ea5be942ee5d4421b6219c3f05d131f64088c73bb0917f",
- "sha256:ffa336210cf9cd8ed117011085817d00abe4c08f99968deef0013ea283547204",
- "sha256:ffb3dc385f6bb1568aa974fe65da84723210e5d9707e360e9ecb51f59406cd2e"
+ "sha256:0450ada317a65383b7cce9576096150fdb97396dcfe559109b403c7242faffef",
+ "sha256:0b5263dcede17b6b0c41ef0c3ccce847d82a7da98709e75cf7efde3e9e3b5cae",
+ "sha256:0d5176f310a7fe6f65608213cc74f4228e4f4ce9fd10bcb2bb6da8fc66991462",
+ "sha256:0ed49efcd0dc1611378beadbd97beb5d9ca8fe48579fc04a6ed0844072261b6a",
+ "sha256:145a73850926018ec1681e734cedcf2716d6a8697d90da11284043b745c286d5",
+ "sha256:1987770fb4887560363b0e1a9b75aa303e447433c41284d3af2840a2f226d6e0",
+ "sha256:246067ba0cf5560cf42e775069c5d80a8989d14a7ded21af529a4e10e3e0f0e6",
+ "sha256:2c311e2f63e42c1bf86361d11e2c4a59f25d9e7aabdbdf53dc38b885c5435cdb",
+ "sha256:2cee3b117a8d13ab98b38d5b6bdcd040cfb4181068d05ce0c474ec9db5f3c5bb",
+ "sha256:2de1378f72def7dfb5dbd73d86c19eda0ea7b0a6873910cc37d57e80f10d64e1",
+ "sha256:30f546358dfa0953db92ba620101fefc81574f87b2346556b90b5f3ef16e55ce",
+ "sha256:34245498eeb9ae54c687a07ad7f160053911b5745e186afe2d0c0f2898a1ab8a",
+ "sha256:392432a2dde22b86f70dd4a0e9671a349446c93965f261dbaecfaf28813e5c42",
+ "sha256:3c0600bcc1adfaaac321422d615939ef300df81e165f6522ad096b73439c0f58",
+ "sha256:4016e383f91f2814e48ed61e6bda7d24c4d7f2402c75dd28f7e1027ae44ea204",
+ "sha256:40cd36749a1035c34ba8d8aaf221b91ca3d111532e5ccb5fa8c3703ab1b967ed",
+ "sha256:413ad794dccb19453e2b97c2375f2ca3cdf34dc50d18cc2693bd5aed7d16f4b9",
+ "sha256:4a93d28ed4b4b39e6f46fd240896c29b686b75e39cc6992692e3922ff6982b4c",
+ "sha256:4ee84c2a22a809c4f868153b178fe59e71423e1f3d6a8cd416134bb231fbf6d3",
+ "sha256:50c5c7b8aa5443304c55c262c5693b108c35a3b61ef961f1e782dd52a2f559c7",
+ "sha256:525410e0790aab036492eeea913858989c4cb070ff373ec3bc322d700bdf47c1",
+ "sha256:526c900397f3bbc2db9cb360ce9c35134c908961cdd0ac25b1ae6ffcaa2507ff",
+ "sha256:54775858c7f2f214476773ce785a19ee81d1294a6bedc5cc17225355aab74802",
+ "sha256:584096938a001378484aa4ee54e05dc79c7b9dd933e271c744a97b3b6f644957",
+ "sha256:6130459189e61baac5a88c10019b21e1f0c6d00ebc770e9ce269475650ff7f73",
+ "sha256:67453e603cea8e85ed566b2700efa1f6916aefbc0c9fcb2e86aaffc08ec38e78",
+ "sha256:68d54234c8d76d8ef74744f9f9fc6324f1508129e23da8883771cdbb5818cbef",
+ "sha256:6dfe7f984f28a8ae94ff3a7953cd9678550dbd2a1f9bda5dd9c5ae627744c78e",
+ "sha256:74bd573dde27e58c760d9ca8615c41a57e719bff315c9adb6f2a4281a28e8798",
+ "sha256:7603ca26d75b1b86160ce1bbe2787a0b706e592af5b2504e12caa88a217767b0",
+ "sha256:76719dd521c20a58a6c256d058547b3a9595d1d885b830013366e27011ffe804",
+ "sha256:7c3623053b85b4296cd3925eeb725e386644fd5bc67250b3bb08b0f144803e7b",
+ "sha256:7e44eba534381dd2687be50cbd5f2daded21575242ecfdaf86bbeecbc38dae8e",
+ "sha256:7fe3d65279bfbee8de0fb4f8c17fc4e893eed2dba21b2f680e930cc2b09075c5",
+ "sha256:8340def6737118f5429a5df4e88f440746b791f8f1c4ce4ad8a595f42c980bd5",
+ "sha256:84ede78acde96ca57f6cf8ccb8a13fbaf569f6011b9a52f870c662d4dc8cd854",
+ "sha256:850ff6155371fd802a280f8d369d4e15d69434651b844bde566ce97ee2277420",
+ "sha256:87a2e00bf17da098d90d4145375f1d985a81605267e7f9377ff94e55c5d769eb",
+ "sha256:88d385b8e7f3a870146bf5ea31786ef7463e99eb59e31db56e2315535d811f55",
+ "sha256:8a2fb742ef378284a50766e985804bd6adb5adb5aa781100b09befdbfa757b65",
+ "sha256:8dc0fba9a74b471c45ca1a3cb6e6913ebfae416678d90529d188886278e7f3f6",
+ "sha256:8fa1510b96c08aaad49303ab11f8803787c99222288f310a62f493faf883ede1",
+ "sha256:8fd12d0f989c6099e7b0f30dc6e0d1e05499f3337461f0b2b0dadea6c64b89df",
+ "sha256:9060addfa4ff753b09392efe41e6af06ea5dd257829199747b9f15bfad819460",
+ "sha256:930ffa1925393381e1e0a9b82137fa7b34c92a019b521cf9f41263976666a0d6",
+ "sha256:936d8a4f0f7081327014742cd51d320296b56aa6d324461a13724ab05f4b2933",
+ "sha256:97fe431f2ed646a3b56142fc81d238abcbaff08548d6912acb0b19a0cadc146b",
+ "sha256:9bd8695be2c80b665ae3f05cb584093a1e59c35ecb7d794d1edd96e8cc9201d7",
+ "sha256:9dec0000d2d8621d8015c293e24589d46fa218637d820894cb7356c77eca3259",
+ "sha256:a478aa11b328983c4444dacb947d4513cb371cd323f3845e53caeda6be5589d5",
+ "sha256:a481a574af914b6e84624412666cbfbe531a05667ca197804ecc19c97b8ab1b0",
+ "sha256:a4ac6a0f0f6402854adca4e3259a623f5c82ec3f0c049374133bcb243132baf9",
+ "sha256:a5e69046f83c0d3cb8f0d5bd9b8838271b1bc898e01562a04398e160953e8eb9",
+ "sha256:a7442662afebbf7b4c6d28cb7aab9e9ce3a5df055fc4116cc7228192ad6cb484",
+ "sha256:aa8a8caca81c0a3e765f19c6953416c58e2f4cc1b84829af01dd1c771bb2f91f",
+ "sha256:ab3247d58b393bda5b1c8f31c9edece7162fc13265334217785518dd770792b8",
+ "sha256:b10a47e5390c4b30a0d58ee12581003be52eedd506862ab7f97da7a66805befb",
+ "sha256:b34508f1cd928ce915ed09682d11307ba4b37d0708d1f28e5774c07a7674cac9",
+ "sha256:b8d3bb96c147b39c02d3db086899679f31958c5d81c494ef0fc9ef5bb1359b3d",
+ "sha256:b9d45dbb3aaec05cf01525ee1a7ac72de46a8c425cb75c003acd29f76b1ffe94",
+ "sha256:bf4480a5438f80e0f1539e15a7eb8b5f97a26fe087e9828e2c0ec2be119a9f72",
+ "sha256:c160a04283c8c6f55b5bf6d4cad59bb9c5b9c9cd08903841b25f1f7109ef1259",
+ "sha256:c96a43822f1f9f69cc5c3706af33239489a6294be486a0447fb71380070d4d5f",
+ "sha256:c9fd9dcf9c91affe71654ef77426f5cf8489305e1c66ed4816f5a21874b094b9",
+ "sha256:cddb31f8474695cd61fc9455c644fc1606c164b93bff2490390d90464b4655df",
+ "sha256:ce1bb21fc7d753b5f8a5d5a4bae99566386b15e716ebdb410154c16c91494d7f",
+ "sha256:d1c031a7572f62f66f1257db37ddab4cb98bfaf9b9434a3b4840bf3560f5e788",
+ "sha256:d589264dbba3b16e8951b6f145d1e6b883094075283dafcab4cdd564a9e353a0",
+ "sha256:dc065a4285307607df3f3686363e7f8bdd0d8ab35f12226362a847731516e42c",
+ "sha256:e10c440d142fa8b32cfdb194caf60ceeceb3e49807072e0dc3a8887ea80e8c16",
+ "sha256:e3552fe98e90fdf5918c04769f338a87fa4f00f3b28830ea9b78b1bdc6140e0d",
+ "sha256:e392804a38353900c3fd8b7cacbea5132888f7129f8e241915e90b85f00e3250",
+ "sha256:e4cecdb52aaa9994fbed6b81d4568427b6002f0a91c322697a4bfcc2b2363f5a",
+ "sha256:e5148ca8955affdfeb864aca158ecae11030e952b25b3ae15d4e2b5ba299bad2",
+ "sha256:e6b2732ef3bafc759f653a98881b5b9cdef0716d98f013d376ee8dfd7285abf1",
+ "sha256:ea756b5a7bac046d202a9a3889b9a92219f885481d78cd318db85b15cc0b7bcf",
+ "sha256:edb69b9589324bdc40961cdf0657815df674f1743a8d5ad9ab56a99e4833cfdd",
+ "sha256:f0203433121484b32646a5f5ea93ae86f3d9559d7243f07e8c0eab5ff8e3f70e",
+ "sha256:f6a19bcab7fbd8f8649d6595624856635159a6527861b9cdc3447af288a00c00",
+ "sha256:f752e80606b132140883bb262a457c475d219d7163d996dc9072434ffb0784c4",
+ "sha256:f7914ab70d2ee8ab91c13e5402122edbc77821c66d2758abb53aabe87f013287"
],
"markers": "python_version >= '3.9'",
- "version": "==3.11.11"
+ "version": "==3.11.12"
},
"aiosignal": {
"hashes": [
@@ -132,12 +129,12 @@
},
"aiosqlite": {
"hashes": [
- "sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6",
- "sha256:6d35c8c256637f4672f843c31021464090805bf925385ac39473fb16eaaca3d7"
+ "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3",
+ "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0"
],
"index": "pypi",
- "markers": "python_version >= '3.8'",
- "version": "==0.20.0"
+ "markers": "python_version >= '3.9'",
+ "version": "==0.21.0"
},
"alembic": {
"hashes": [
@@ -196,19 +193,19 @@
},
"attrs": {
"hashes": [
- "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff",
- "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"
+ "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e",
+ "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a"
],
"markers": "python_version >= '3.8'",
- "version": "==24.3.0"
+ "version": "==25.1.0"
},
"authlib": {
"hashes": [
- "sha256:1c1e6608b5ed3624aeeee136ca7f8c120d6f51f731aa152b153d54741840e1f2",
- "sha256:4bb20b978c8b636222b549317c1815e1fe62234fc1c5efe8855d84aebf3a74e3"
+ "sha256:30ead9ea4993cdbab821dc6e01e818362f92da290c04c7f6a1940f86507a790d",
+ "sha256:edc29c3f6a3e72cd9e9f45fff67fc663a2c364022eb0371c003f22d5405915c1"
],
"markers": "python_version >= '3.9'",
- "version": "==1.4.0"
+ "version": "==1.4.1"
},
"auto-archiver": {
"hashes": [
@@ -221,11 +218,11 @@
},
"beautifulsoup4": {
"hashes": [
- "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051",
- "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"
+ "sha256:1bd32405dacc920b42b83ba01644747ed77456a65760e285fbc47633ceddaf8b",
+ "sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16"
],
- "markers": "python_full_version >= '3.6.0'",
- "version": "==4.12.3"
+ "markers": "python_full_version >= '3.7.0'",
+ "version": "==4.13.3"
},
"billiard": {
"hashes": [
@@ -245,19 +242,19 @@
},
"boto3": {
"hashes": [
- "sha256:53a5307f6a3526ee2f8590e3c45efa504a3ea4532c1bfe4926c0c19bf188d141",
- "sha256:f9843a5d06f501d66ada06f5a5417f671823af2cf319e36ceefa1bafaaaaa953"
+ "sha256:0cf92ca0538ab115447e1c58050d43e1273e88c58ddfea2b6f133fdc508b400a",
+ "sha256:b10583bf8bd35be1b4027ee7e26b7cdf2078c79eab18357fd602cecb6d39400b"
],
"markers": "python_version >= '3.8'",
- "version": "==1.36.3"
+ "version": "==1.36.16"
},
"botocore": {
"hashes": [
- "sha256:536ab828e6f90dbb000e3702ac45fd76642113ae2db1b7b1373ad24104e89255",
- "sha256:775b835e979da5c96548ed1a0b798101a145aec3cd46541d62e27dda5a94d7f8"
+ "sha256:10c6aa386ba1a9a0faef6bb5dbfc58fc2563a3c6b95352e86a583cd5f14b11f3",
+ "sha256:aca0348ccd730332082489b6817fdf89e1526049adcf6e9c8c11c96dd9f42c03"
],
"markers": "python_version >= '3.8'",
- "version": "==1.36.3"
+ "version": "==1.36.16"
},
"brotli": {
"hashes": [
@@ -415,11 +412,11 @@
},
"certifi": {
"hashes": [
- "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56",
- "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"
+ "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651",
+ "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"
],
"markers": "python_version >= '3.6'",
- "version": "==2024.12.14"
+ "version": "==2025.1.31"
},
"certvalidator": {
"hashes": [
@@ -676,11 +673,11 @@
},
"dateparser": {
"hashes": [
- "sha256:0b21ad96534e562920a0083e97fd45fa959882d4162acc358705144520a35830",
- "sha256:7975b43a4222283e0ae15be7b4999d08c9a70e2d378ac87385b1ccf2cffbbb30"
+ "sha256:7e4919aeb48481dbfc01ac9683c8e20bfe95bb715a38c1e9f6af889f4f30ccc3",
+ "sha256:bdcac262a467e6260030040748ad7c10d6bacd4f3b9cdb4cfd2251939174508c"
],
- "markers": "python_version >= '3.7'",
- "version": "==1.2.0"
+ "markers": "python_version >= '3.8'",
+ "version": "==1.2.1"
},
"dnspython": {
"hashes": [
@@ -708,12 +705,12 @@
},
"fastapi": {
"hashes": [
- "sha256:9ec46f7addc14ea472958a96aae5b5de65f39721a46aaf5705c480d9a8b76654",
- "sha256:e9240b29e36fa8f4bb7290316988e90c381e5092e0cbe84e7818cc3713bcf305"
+ "sha256:0ce9111231720190473e222cdf0f07f7206ad7e53ea02beb1d2dc36e2f0741e9",
+ "sha256:753a96dd7e036b34eeef8babdfcfe3f28ff79648f86551eb36bfc1b0bf4a8cbf"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
- "version": "==0.115.6"
+ "version": "==0.115.8"
},
"fastapi-mail": {
"hashes": [
@@ -864,27 +861,27 @@
},
"google-api-core": {
"hashes": [
- "sha256:10d82ac0fca69c82a25b3efdeefccf6f28e02ebb97925a8cce8edbfe379929d9",
- "sha256:e255640547a597a4da010876d333208ddac417d60add22b6851a0c66a831fcaf"
+ "sha256:bc78d608f5a5bf853b80bd70a795f703294de656c096c0968320830a4bc280f1",
+ "sha256:f8b36f5456ab0dd99a1b693a40a31d1e7757beea380ad1b38faaf8941eae9d8a"
],
"markers": "python_version >= '3.7'",
- "version": "==2.24.0"
+ "version": "==2.24.1"
},
"google-api-python-client": {
"hashes": [
- "sha256:55197f430f25c907394b44fa078545ffef89d33fd4dca501b7db9f0d8e224bd6",
- "sha256:baef0bb631a60a0bd7c0bf12a5499e3a40cd4388484de7ee55c1950bf820a0cf"
+ "sha256:63d61fb3e4cf3fb31a70a87f45567c22f6dfe87bbfa27252317e3e2c42900db4",
+ "sha256:a8ccafaecfa42d15d5b5c3134ced8de08380019717fc9fb1ed510ca58eca3b7e"
],
"markers": "python_version >= '3.7'",
- "version": "==2.159.0"
+ "version": "==2.160.0"
},
"google-auth": {
"hashes": [
- "sha256:0054623abf1f9c83492c63d3f47e77f0a544caa3d40b2d98e099a611c2dd5d00",
- "sha256:42664f18290a6be591be5329a96fe30184be1a1badb7292a7f686a9659de9ca0"
+ "sha256:8285113607d3b80a3f1543b75962447ba8a09fe85783432a784fdeef6ac094c4",
+ "sha256:e7dae6694313f434a2727bf2906f27ad259bae090d7aa896590d86feec3d9d4a"
],
"markers": "python_version >= '3.7'",
- "version": "==2.37.0"
+ "version": "==2.38.0"
},
"google-auth-httplib2": {
"hashes": [
@@ -1038,10 +1035,11 @@
},
"instaloader": {
"hashes": [
- "sha256:754425eb17af44ce4bb6056e4eacd044a518d13b5efc11b9d80eb229bb96c652"
+ "sha256:43356f696231621ea5a93354f9a4578124fe131940ee9aa1e83c20f57e18f26d",
+ "sha256:a41a7372a18fb096b3ed545469479884de9cf768e12020c0e0e67c488d9d599c"
],
"markers": "python_version >= '3.9'",
- "version": "==4.14"
+ "version": "==4.14.1"
},
"itsdangerous": {
"hashes": [
@@ -1239,11 +1237,11 @@
},
"mako": {
"hashes": [
- "sha256:42f48953c7eb91332040ff567eb7eea69b22e7a4affbc5ba8e845e8f730f6627",
- "sha256:577b97e414580d3e088d47c2dbbe9594aa7a5146ed2875d4dfa9075af2dd3cc8"
+ "sha256:95920acccb578427a9aa38e37a186b1e43156c87260d7ba18ca63aa4c7cbd3a1",
+ "sha256:b5d65ff3462870feec922dbccf38f6efb44e5714d7b593a656be86663d8600ac"
],
"markers": "python_version >= '3.8'",
- "version": "==1.3.8"
+ "version": "==1.3.9"
},
"markdown-it-py": {
"hashes": [
@@ -1322,11 +1320,11 @@
},
"marshmallow": {
"hashes": [
- "sha256:ec5d00d873ce473b7f2ffcb7104286a376c354cab0c2fa12f5573dab03e87210",
- "sha256:f4debda3bb11153d81ac34b0d582bf23053055ee11e791b54b4b35493468040a"
+ "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c",
+ "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6"
],
"markers": "python_version >= '3.9'",
- "version": "==3.25.1"
+ "version": "==3.26.1"
},
"mdurl": {
"hashes": [
@@ -1785,11 +1783,11 @@
},
"proto-plus": {
"hashes": [
- "sha256:c91fc4a65074ade8e458e95ef8bac34d4008daa7cce4a12d6707066fca648961",
- "sha256:fbb17f57f7bd05a68b7707e745e26528b0b3c34e378db91eef93912c54982d91"
+ "sha256:6e93d5f5ca267b54300880fff156b6a3386b3fa3f43b1da62e680fc0c586ef22",
+ "sha256:bf2dfaa3da281fc3187d12d224c707cb57214fb2c22ba854eb0c105a3fb2d4d7"
],
"markers": "python_version >= '3.7'",
- "version": "==1.25.0"
+ "version": "==1.26.0"
},
"protobuf": {
"hashes": [
@@ -2075,7 +2073,6 @@
"sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca",
"sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"
],
- "index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==1.0.1"
},
@@ -2097,10 +2094,10 @@
},
"pytz": {
"hashes": [
- "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a",
- "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"
+ "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57",
+ "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e"
],
- "version": "==2024.2"
+ "version": "==2025.1"
},
"pyyaml": {
"hashes": [
@@ -2323,19 +2320,19 @@
},
"s3transfer": {
"hashes": [
- "sha256:3f25c900a367c8b7f7d8f9c34edc87e300bde424f779dc9f0a8ae4f9df9264f6",
- "sha256:8fa0aa48177be1f3425176dfe1ab85dcd3d962df603c3dbfc585e6bf857ef0ff"
+ "sha256:3b39185cb72f5acc77db1a58b6e25b977f28d20496b6e58d6813d75f464d632f",
+ "sha256:be6ecb39fadd986ef1701097771f87e4d2f821f27f6071c872143884d2950fbc"
],
"markers": "python_version >= '3.8'",
- "version": "==0.11.1"
+ "version": "==0.11.2"
},
"selenium": {
"hashes": [
- "sha256:3d6a2e8e1b850a1078884ea19f4e011ecdc12263434d87a0b78769836fb82dd8",
- "sha256:a9fae6eef48d470a1b0c6e45185d96f0dafb025e8da4b346cc41e4da3ac54fa0"
+ "sha256:0072d08670d7ec32db901bd0107695a330cecac9f196e3afb3fa8163026e022a",
+ "sha256:4238847e45e24e4472cfcf3554427512c7aab9443396435b1623ef406fff1cc1"
],
"markers": "python_version >= '3.9'",
- "version": "==4.28.0"
+ "version": "==4.28.1"
},
"six": {
"hashes": [
@@ -2378,67 +2375,67 @@
},
"sqlalchemy": {
"hashes": [
- "sha256:03f0528c53ca0b67094c4764523c1451ea15959bbf0a8a8a3096900014db0278",
- "sha256:12b0f1ec623cccf058cf21cb544f0e74656618165b083d78145cafde156ea7b6",
- "sha256:12b28d99a9c14eaf4055810df1001557176716de0167b91026e648e65229bffb",
- "sha256:1b2690456528a87234a75d1a1644cdb330a6926f455403c8e4f6cad6921f9098",
- "sha256:1cdba1f73b64530c47b27118b7053b8447e6d6f3c8104e3ac59f3d40c33aa9fd",
- "sha256:293f9ade06b2e68dd03cfb14d49202fac47b7bb94bffcff174568c951fbc7af2",
- "sha256:2952748ecd67ed3b56773c185e85fc084f6bdcdec10e5032a7c25a6bc7d682ef",
- "sha256:2f95fc8e3f34b5f6b3effb49d10ac97c569ec8e32f985612d9b25dd12d0d2e94",
- "sha256:2fa2c0913f02341d25fb858e4fb2031e6b0813494cca1ba07d417674128ce11b",
- "sha256:3151822aa1db0eb5afd65ccfafebe0ef5cda3a7701a279c8d0bf17781a793bb4",
- "sha256:35bd2df269de082065d4b23ae08502a47255832cc3f17619a5cea92ce478b02b",
- "sha256:41296bbcaa55ef5fdd32389a35c710133b097f7b2609d8218c0eabded43a1d84",
- "sha256:44f569d0b1eb82301b92b72085583277316e7367e038d97c3a1a899d9a05e342",
- "sha256:46954173612617a99a64aee103bcd3f078901b9a8dcfc6ae80cbf34ba23df989",
- "sha256:4b12885dc85a2ab2b7d00995bac6d967bffa8594123b02ed21e8eb2205a7584b",
- "sha256:4f581d365af9373a738c49e0c51e8b18e08d8a6b1b15cc556773bcd8a192fa8b",
- "sha256:51bc9cfef83e0ac84f86bf2b10eaccb27c5a3e66a1212bef676f5bee6ef33ebb",
- "sha256:521ef85c04c33009166777c77e76c8a676e2d8528dc83a57836b63ca9c69dcd1",
- "sha256:5bc3339db84c5fb9130ac0e2f20347ee77b5dd2596ba327ce0d399752f4fce39",
- "sha256:635d8a21577341dfe4f7fa59ec394b346da12420b86624a69e466d446de16aff",
- "sha256:648ec5acf95ad59255452ef759054f2176849662af4521db6cb245263ae4aa33",
- "sha256:650dcb70739957a492ad8acff65d099a9586b9b8920e3507ca61ec3ce650bb72",
- "sha256:6b788f14c5bb91db7f468dcf76f8b64423660a05e57fe277d3f4fad7b9dcb7ce",
- "sha256:6c67415258f9f3c69867ec02fea1bf6508153709ecbd731a982442a590f2b7e4",
- "sha256:74bbd1d0a9bacf34266a7907d43260c8d65d31d691bb2356f41b17c2dca5b1d0",
- "sha256:75311559f5c9881a9808eadbeb20ed8d8ba3f7225bef3afed2000c2a9f4d49b9",
- "sha256:78361be6dc9073ed17ab380985d1e45e48a642313ab68ab6afa2457354ff692c",
- "sha256:7b7e772dc4bc507fdec4ee20182f15bd60d2a84f1e087a8accf5b5b7a0dcf2ba",
- "sha256:82df02816c14f8dc9f4d74aea4cb84a92f4b0620235daa76dde002409a3fbb5a",
- "sha256:84b9f23b0fa98a6a4b99d73989350a94e4a4ec476b9a7dfe9b79ba5939f5e80b",
- "sha256:8c4096727193762e72ce9437e2a86a110cf081241919ce3fab8e89c02f6b6658",
- "sha256:8e47f1af09444f87c67b4f1bb6231e12ba6d4d9f03050d7fc88df6d075231a49",
- "sha256:93d1543cd8359040c02b6614421c8e10cd7a788c40047dbc507ed46c29ae5636",
- "sha256:94b564e38b344d3e67d2e224f0aec6ba09a77e4582ced41e7bfd0f757d926ec9",
- "sha256:955a2a765aa1bd81aafa69ffda179d4fe3e2a3ad462a736ae5b6f387f78bfeb8",
- "sha256:9d087663b7e1feabea8c578d6887d59bb00388158e8bff3a76be11aa3f748ca2",
- "sha256:9df21b8d9e5c136ea6cde1c50d2b1c29a2b5ff2b1d610165c23ff250e0704087",
- "sha256:a8998bf9f8658bd3839cbc44ddbe982955641863da0c1efe5b00c1ab4f5c16b1",
- "sha256:b2eae3423e538c10d93ae3e87788c6a84658c3ed6db62e6a61bb9495b0ad16bb",
- "sha256:b661b49d0cb0ab311a189b31e25576b7ac3e20783beb1e1817d72d9d02508bf5",
- "sha256:bedee60385c1c0411378cbd4dc486362f5ee88deceea50002772912d798bb00f",
- "sha256:c505edd429abdfe3643fa3b2e83efb3445a34a9dc49d5f692dd087be966020e0",
- "sha256:cce918ada64c956b62ca2c2af59b125767097ec1dca89650a6221e887521bfd7",
- "sha256:cf5ae8a9dcf657fd72144a7fd01f243236ea39e7344e579a121c4205aedf07bb",
- "sha256:cf95a60b36997dad99692314c4713f141b61c5b0b4cc5c3426faad570b31ca01",
- "sha256:d57bafbab289e147d064ffbd5cca2d7b1394b63417c0636cea1f2e93d16eb9e8",
- "sha256:d70f53a0646cc418ca4853da57cf3ddddbccb8c98406791f24426f2dd77fd0e2",
- "sha256:d75ead7dd4d255068ea0f21492ee67937bd7c90964c8f3c2bea83c7b7f81b95f",
- "sha256:da36c3b0e891808a7542c5c89f224520b9a16c7f5e4d6a1156955605e54aef0e",
- "sha256:db18ff6b8c0f1917f8b20f8eca35c28bbccb9f83afa94743e03d40203ed83de9",
- "sha256:dfff7be361048244c3aa0f60b5e63221c5e0f0e509f4e47b8910e22b57d10ae7",
- "sha256:e4fb5ac86d8fe8151966814f6720996430462e633d225497566b3996966b9bdb",
- "sha256:e56a139bfe136a22c438478a86f8204c1eb5eed36f4e15c4224e4b9db01cb3e4",
- "sha256:e6f5d254a22394847245f411a2956976401e84da4288aa70cbcd5190744062c1",
- "sha256:e7402ff96e2b073a98ef6d6142796426d705addd27b9d26c3b32dbaa06d7d069",
- "sha256:ea308cec940905ba008291d93619d92edaf83232ec85fbd514dcb329f3192761",
- "sha256:eaa8039b6d20137a4e02603aba37d12cd2dde7887500b8855356682fc33933f4"
+ "sha256:0398361acebb42975deb747a824b5188817d32b5c8f8aba767d51ad0cc7bb08d",
+ "sha256:0561832b04c6071bac3aad45b0d3bb6d2c4f46a8409f0a7a9c9fa6673b41bc03",
+ "sha256:07258341402a718f166618470cde0c34e4cec85a39767dce4e24f61ba5e667ea",
+ "sha256:0a826f21848632add58bef4f755a33d45105d25656a0c849f2dc2df1c71f6f50",
+ "sha256:1052723e6cd95312f6a6eff9a279fd41bbae67633415373fdac3c430eca3425d",
+ "sha256:12d5b06a1f3aeccf295a5843c86835033797fea292c60e72b07bcb5d820e6dd3",
+ "sha256:12f5c9ed53334c3ce719155424dc5407aaa4f6cadeb09c5b627e06abb93933a1",
+ "sha256:2a0ef3f98175d77180ffdc623d38e9f1736e8d86b6ba70bff182a7e68bed7727",
+ "sha256:2f2951dc4b4f990a4b394d6b382accb33141d4d3bd3ef4e2b27287135d6bdd68",
+ "sha256:3868acb639c136d98107c9096303d2d8e5da2880f7706f9f8c06a7f961961149",
+ "sha256:386b7d136919bb66ced64d2228b92d66140de5fefb3c7df6bd79069a269a7b06",
+ "sha256:3d3043375dd5bbcb2282894cbb12e6c559654c67b5fffb462fda815a55bf93f7",
+ "sha256:3e35d5565b35b66905b79ca4ae85840a8d40d31e0b3e2990f2e7692071b179ca",
+ "sha256:402c2316d95ed90d3d3c25ad0390afa52f4d2c56b348f212aa9c8d072a40eee5",
+ "sha256:40310db77a55512a18827488e592965d3dec6a3f1e3d8af3f8243134029daca3",
+ "sha256:40e9cdbd18c1f84631312b64993f7d755d85a3930252f6276a77432a2b25a2f3",
+ "sha256:49aa2cdd1e88adb1617c672a09bf4ebf2f05c9448c6dbeba096a3aeeb9d4d443",
+ "sha256:57dd41ba32430cbcc812041d4de8d2ca4651aeefad2626921ae2a23deb8cd6ff",
+ "sha256:5dba1cdb8f319084f5b00d41207b2079822aa8d6a4667c0f369fce85e34b0c86",
+ "sha256:5e1d9e429028ce04f187a9f522818386c8b076723cdbe9345708384f49ebcec6",
+ "sha256:63178c675d4c80def39f1febd625a6333f44c0ba269edd8a468b156394b27753",
+ "sha256:6493bc0eacdbb2c0f0d260d8988e943fee06089cd239bd7f3d0c45d1657a70e2",
+ "sha256:64aa8934200e222f72fcfd82ee71c0130a9c07d5725af6fe6e919017d095b297",
+ "sha256:665255e7aae5f38237b3a6eae49d2358d83a59f39ac21036413fab5d1e810578",
+ "sha256:6db316d6e340f862ec059dc12e395d71f39746a20503b124edc255973977b728",
+ "sha256:70065dfabf023b155a9c2a18f573e47e6ca709b9e8619b2e04c54d5bcf193178",
+ "sha256:8455aa60da49cb112df62b4721bd8ad3654a3a02b9452c783e651637a1f21fa2",
+ "sha256:8b0ac78898c50e2574e9f938d2e5caa8fe187d7a5b69b65faa1ea4648925b096",
+ "sha256:8bf312ed8ac096d674c6aa9131b249093c1b37c35db6a967daa4c84746bc1bc9",
+ "sha256:92f99f2623ff16bd4aaf786ccde759c1f676d39c7bf2855eb0b540e1ac4530c8",
+ "sha256:9c8bcad7fc12f0cc5896d8e10fdf703c45bd487294a986903fe032c72201596b",
+ "sha256:9cd136184dd5f58892f24001cdce986f5d7e96059d004118d5410671579834a4",
+ "sha256:9eb4fa13c8c7a2404b6a8e3772c17a55b1ba18bc711e25e4d6c0c9f5f541b02a",
+ "sha256:a2bc4e49e8329f3283d99840c136ff2cd1a29e49b5624a46a290f04dff48e079",
+ "sha256:a5645cd45f56895cfe3ca3459aed9ff2d3f9aaa29ff7edf557fa7a23515a3725",
+ "sha256:a9afbc3909d0274d6ac8ec891e30210563b2c8bdd52ebbda14146354e7a69373",
+ "sha256:aa498d1392216fae47eaf10c593e06c34476ced9549657fca713d0d1ba5f7248",
+ "sha256:afd776cf1ebfc7f9aa42a09cf19feadb40a26366802d86c1fba080d8e5e74bdd",
+ "sha256:b335a7c958bc945e10c522c069cd6e5804f4ff20f9a744dd38e748eb602cbbda",
+ "sha256:b3c4817dff8cef5697f5afe5fec6bc1783994d55a68391be24cb7d80d2dbc3a6",
+ "sha256:b79ee64d01d05a5476d5cceb3c27b5535e6bb84ee0f872ba60d9a8cd4d0e6579",
+ "sha256:b87a90f14c68c925817423b0424381f0e16d80fc9a1a1046ef202ab25b19a444",
+ "sha256:bf89e0e4a30714b357f5d46b6f20e0099d38b30d45fa68ea48589faf5f12f62d",
+ "sha256:c058b84c3b24812c859300f3b5abf300daa34df20d4d4f42e9652a4d1c48c8a4",
+ "sha256:c09a6ea87658695e527104cf857c70f79f14e9484605e205217aae0ec27b45fc",
+ "sha256:c57b8e0841f3fce7b703530ed70c7c36269c6d180ea2e02e36b34cb7288c50c7",
+ "sha256:c9cea5b756173bb86e2235f2f871b406a9b9d722417ae31e5391ccaef5348f2c",
+ "sha256:cb39ed598aaf102251483f3e4675c5dd6b289c8142210ef76ba24aae0a8f8aba",
+ "sha256:e036549ad14f2b414c725349cce0772ea34a7ab008e9cd67f9084e4f371d1f32",
+ "sha256:e185ea07a99ce8b8edfc788c586c538c4b1351007e614ceb708fd01b095ef33e",
+ "sha256:e5a4d82bdb4bf1ac1285a68eab02d253ab73355d9f0fe725a97e1e0fa689decb",
+ "sha256:eae27ad7580529a427cfdd52c87abb2dfb15ce2b7a3e0fc29fbb63e2ed6f8120",
+ "sha256:ecef029b69843b82048c5b347d8e6049356aa24ed644006c9a9d7098c3bd3bfd",
+ "sha256:ee3bee874cb1fadee2ff2b79fc9fc808aa638670f28b2145074538d4a6a5028e",
+ "sha256:f0d3de936b192980209d7b5149e3c98977c3810d401482d05fb6d668d53c1c63",
+ "sha256:f53c0d6a859b2db58332e0e6a921582a02c1677cc93d4cbb36fdf49709b327b2",
+ "sha256:f9d57f1b3061b3e21476b0ad5f0397b112b94ace21d1f439f2db472e568178ae"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
- "version": "==2.0.37"
+ "version": "==2.0.38"
},
"starlette": {
"hashes": [
@@ -2785,11 +2782,11 @@
},
"yt-dlp": {
"hashes": [
- "sha256:b8666b88e23c3fa5ee1e80920f4a9dfac7c405504a447214c0cf3d0c386edcfc",
- "sha256:e8ec515d49bb62704915d13a22ee6fe03a5658d651e4e64574e3a17ee01f6e3b"
+ "sha256:1c9738266921ad43c568ad01ac3362fb7c7af549276fbec92bd72f140da16240",
+ "sha256:3e76bd896b9f96601021ca192ca0fbdd195e3c3dcc28302a3a34c9bc4979da7b"
],
"markers": "python_version >= '3.9'",
- "version": "==2025.1.15"
+ "version": "==2025.1.26"
}
},
"develop": {
@@ -2803,80 +2800,75 @@
},
"certifi": {
"hashes": [
- "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56",
- "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"
+ "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651",
+ "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"
],
"markers": "python_version >= '3.6'",
- "version": "==2024.12.14"
+ "version": "==2025.1.31"
},
"coverage": {
"hashes": [
- "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9",
- "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f",
- "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273",
- "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994",
- "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e",
- "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50",
- "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e",
- "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e",
- "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c",
- "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853",
- "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8",
- "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8",
- "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe",
- "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165",
- "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb",
- "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59",
- "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609",
- "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18",
- "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098",
- "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd",
- "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3",
- "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43",
- "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d",
- "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359",
- "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90",
- "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78",
- "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a",
- "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99",
- "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988",
- "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2",
- "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0",
- "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694",
- "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377",
- "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d",
- "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23",
- "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312",
- "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf",
- "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6",
- "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b",
- "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c",
- "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690",
- "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a",
- "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f",
- "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4",
- "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25",
- "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd",
- "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852",
- "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0",
- "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244",
- "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315",
- "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078",
- "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0",
- "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27",
- "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132",
- "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5",
- "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247",
- "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022",
- "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b",
- "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3",
- "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18",
- "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5",
- "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"
+ "sha256:050172741de03525290e67f0161ae5f7f387c88fca50d47fceb4724ceaa591d2",
+ "sha256:08e5fb93576a6b054d3d326242af5ef93daaac9bb52bc25f12ccbc3fa94227cd",
+ "sha256:09d03f48d9025b8a6a116cddcb6c7b8ce80e4fb4c31dd2e124a7c377036ad58e",
+ "sha256:0d03c9452d9d1ccfe5d3a5df0427705022a49b356ac212d529762eaea5ef97b4",
+ "sha256:13100f98497086b359bf56fc035a762c674de8ef526daa389ac8932cb9bff1e0",
+ "sha256:25575cd5a7d2acc46b42711e8aff826027c0e4f80fb38028a74f31ac22aae69d",
+ "sha256:27700d859be68e4fb2e7bf774cf49933dcac6f81a9bc4c13bd41735b8d26a53b",
+ "sha256:2c81e53782043b323bd34c7de711ed9b4673414eb517eaf35af92185b873839c",
+ "sha256:397489c611b76302dfa1d9ea079e138dddc4af80fc6819d5f5119ec8ca6c0e47",
+ "sha256:476f29a258b9cd153f2be5bf5f119d670d2806363595263917bddc167d6e5cce",
+ "sha256:4bda710139ea646890d1c000feb533caff86904a0e0638f85e967c28cb8eec50",
+ "sha256:4cf96beb05d004e4c51cd846fcdf9eee9eb2681518524b66b2e7610507944c2f",
+ "sha256:4f21e3617f48d683f30cf2a6c8b739c838e600cb1454fe6b2eb486ac2bce8fbd",
+ "sha256:5128f3ba694c0a1bde55fc480090392c336236c3e1a10dad40dc1ab17c7675ff",
+ "sha256:532fe139691af134aa8b54ed60dd3c806aa81312d93693bd2883c7b61592c840",
+ "sha256:5a3f7cbbcb4ad95067a6525f83a6fc78d9cbc1e70f8abaeeaeaa72ef34f48fc3",
+ "sha256:5b48db06f53d1864fea6dbd855e6d51d41c0f06c212c3004511c0bdc6847b297",
+ "sha256:5e7ac966ab110bd94ee844f2643f196d78fde1cd2450399116d3efdd706e19f5",
+ "sha256:5edc16712187139ab635a2e644cc41fc239bc6d245b16124045743130455c652",
+ "sha256:60d4ad09dfc8c36c4910685faafcb8044c84e4dae302e86c585b3e2e7778726c",
+ "sha256:61c834cbb80946d6ebfddd9b393a4c46bec92fcc0fa069321fcb8049117f76ea",
+ "sha256:6ba27a0375c5ef4d2a7712f829265102decd5ff78b96d342ac2fa555742c4f4f",
+ "sha256:6c96a142057d83ee993eaf71629ca3fb952cda8afa9a70af4132950c2bd3deb9",
+ "sha256:6d60577673ba48d8ae8e362e61fd4ad1a640293ffe8991d11c86f195479100b7",
+ "sha256:7eb0504bb307401fd08bc5163a351df301438b3beb88a4fa044681295bbefc67",
+ "sha256:8e433b6e3a834a43dae2889adc125f3fa4c66668df420d8e49bc4ee817dd7a70",
+ "sha256:8fa4fffd90ee92f62ff7404b4801b59e8ea8502e19c9bf2d3241ce745b52926c",
+ "sha256:90de4e9ca4489e823138bd13098af9ac8028cc029f33f60098b5c08c675c7bda",
+ "sha256:a165b09e7d5f685bf659063334a9a7b1a2d57b531753d3e04bd442b3cfe5845b",
+ "sha256:a46d56e99a31d858d6912d31ffa4ede6a325c86af13139539beefca10a1234ce",
+ "sha256:ac476e6d0128fb7919b3fae726de72b28b5c9644cb4b579e4a523d693187c551",
+ "sha256:ac5d92e2cc121a13270697e4cb37e1eb4511ac01d23fe1b6c097facc3b46489e",
+ "sha256:adc2d941c0381edfcf3897f94b9f41b1e504902fab78a04b1677f2f72afead4b",
+ "sha256:b6ff5be3b1853e0862da9d349fe87f869f68e63a25f7c37ce1130b321140f963",
+ "sha256:bb35ae9f134fbd9cf7302a9654d5a1e597c974202678082dcc569eb39a8cde03",
+ "sha256:be05bde21d5e6eefbc3a6de6b9bee2b47894b8945342e8663192809c4d1f08ce",
+ "sha256:c27df03730059118b8a923cfc8b84b7e9976742560af528242f201880879c1da",
+ "sha256:c7719a5e1dc93883a6b319bc0374ecd46fb6091ed659f3fbe281ab991634b9b0",
+ "sha256:c86f4c7a6d1a54a24d804d9684d96e36a62d3ef7c0d7745ae2ea39e3e0293251",
+ "sha256:ca95d40900cf614e07f00cee8c2fad0371df03ca4d7a80161d84be2ec132b7a4",
+ "sha256:cd4839813b09ab1dd1be1bbc74f9a7787615f931f83952b6a9af1b2d3f708bf7",
+ "sha256:db4b1a69976b1b02acda15937538a1d3fe10b185f9d99920b17a740a0a102e06",
+ "sha256:dbb1a822fd858d9853333a7c95d4e70dde9a79e65893138ce32c2ec6457d7a36",
+ "sha256:de6b079b39246a7da9a40cfa62d5766bd52b4b7a88cf5a82ec4c45bf6e152306",
+ "sha256:df6ff122a0a10a30121d9f0cb3fbd03a6fe05861e4ec47adb9f25e9245aabc19",
+ "sha256:e0b0f272901a5172090c0802053fbc503cdc3fa2612720d2669a98a7384a7bec",
+ "sha256:e2778be4f574b39ec9dcd9e5e13644f770351ee0990a0ecd27e364aba95af89b",
+ "sha256:e3b746fa0ffc5b6b8856529de487da8b9aeb4fb394bb58de6502ef45f3434f12",
+ "sha256:e642e6a46a04e992ebfdabed79e46f478ec60e2c528e1e1a074d63800eda4286",
+ "sha256:eafea49da254a8289bed3fab960f808b322eda5577cb17a3733014928bbfbebd",
+ "sha256:f0f334ae844675420164175bf32b04e18a81fe57ad8eb7e0cfd4689d681ffed7",
+ "sha256:f382004fa4c93c01016d9226b9d696a08c53f6818b7ad59b4e96cb67e863353a",
+ "sha256:f4679fcc9eb9004fdd1b00231ef1ec7167168071bebc4d66327e28c1979b4449",
+ "sha256:fd2fffc8ce8692ce540103dff26279d2af22d424516ddebe2d7e4d6dbb3816b2",
+ "sha256:ff136607689c1c87f43d24203b6d2055b42030f352d5176f9c8b204d4235ef27",
+ "sha256:ff52b4e2ac0080c96e506819586c4b16cdbf46724bda90d308a7330a73cc8521",
+ "sha256:ff562952f15eff27247a4c4b03e45ce8a82e3fb197de6a7c54080f9d4ba07845"
],
"index": "pypi",
"markers": "python_version >= '3.9'",
- "version": "==7.6.10"
+ "version": "==7.6.11"
},
"exceptiongroup": {
"hashes": [
@@ -2953,12 +2945,12 @@
},
"pytest-asyncio": {
"hashes": [
- "sha256:0d0bb693f7b99da304a0634afc0a4b19e49d5e0de2d670f38dc4bfa5727c5075",
- "sha256:3f8ef9a98f45948ea91a0ed3dc4268b5326c0e7bce73892acc654df4262ad45f"
+ "sha256:9e89518e0f9bd08928f97a3482fdc4e244df17529460bc038291ccaf8f85c7c3",
+ "sha256:fc1da2cf9f125ada7e710b4ddad05518d4cee187ae9412e9ac9271003497f07a"
],
"index": "pypi",
"markers": "python_version >= '3.9'",
- "version": "==0.25.2"
+ "version": "==0.25.3"
},
"sniffio": {
"hashes": [
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/example.user-groups.yaml b/app/example.user-groups.yaml
similarity index 100%
rename from src/example.user-groups.yaml
rename to app/example.user-groups.yaml
diff --git a/src/logs/.gitkeep b/app/logs/.gitkeep
similarity index 100%
rename from src/logs/.gitkeep
rename to app/logs/.gitkeep
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/migrations/versions/.gitkeep b/app/migrations/versions/.gitkeep
similarity index 100%
rename from src/migrations/versions/.gitkeep
rename to app/migrations/versions/.gitkeep
diff --git a/src/migrations/versions/02b2f6d17ed0_create_archives_store_until_column.py b/app/migrations/versions/02b2f6d17ed0_create_archives_store_until_column.py
similarity index 100%
rename from src/migrations/versions/02b2f6d17ed0_create_archives_store_until_column.py
rename to app/migrations/versions/02b2f6d17ed0_create_archives_store_until_column.py
diff --git a/src/migrations/versions/1636724ec4b1_rename_sheets_last_archived_col.py b/app/migrations/versions/1636724ec4b1_rename_sheets_last_archived_col.py
similarity index 100%
rename from src/migrations/versions/1636724ec4b1_rename_sheets_last_archived_col.py
rename to app/migrations/versions/1636724ec4b1_rename_sheets_last_archived_col.py
diff --git a/src/migrations/versions/89121d2c96d8_add_sheet_id_to_archive_table.py b/app/migrations/versions/89121d2c96d8_add_sheet_id_to_archive_table.py
similarity index 100%
rename from src/migrations/versions/89121d2c96d8_add_sheet_id_to_archive_table.py
rename to app/migrations/versions/89121d2c96d8_add_sheet_id_to_archive_table.py
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/src/migrations/versions/a23aaf3ae930_drop_active_column.py b/app/migrations/versions/a23aaf3ae930_drop_active_column.py
similarity index 100%
rename from src/migrations/versions/a23aaf3ae930_drop_active_column.py
rename to app/migrations/versions/a23aaf3ae930_drop_active_column.py
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 100%
rename from src/migrations/versions/fa012ec405b8_add_columns_to_groups_table.py
rename to app/migrations/versions/fa012ec405b8_add_columns_to_groups_table.py
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..b9c376c
--- /dev/null
+++ b/app/shared/aa_utils.py
@@ -0,0 +1,33 @@
+# 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 import Metadata
+from auto_archiver.core import Media
+
+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..2691ffe
--- /dev/null
+++ b/app/shared/business_logic.py
@@ -0,0 +1,15 @@
+# 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 crud
+
+
+def get_store_archive_until(db: Session, group_id: str) -> datetime.datetime:
+ group = crud.get_group(db, group_id)
+ 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/core/config.py b/app/shared/config.py
similarity index 100%
rename from src/core/config.py
rename to app/shared/config.py
diff --git a/src/endpoints/__init__.py b/app/shared/db/__init__.py
similarity index 100%
rename from src/endpoints/__init__.py
rename to app/shared/db/__init__.py
diff --git a/src/db/crud.py b/app/shared/db/crud.py
similarity index 97%
rename from src/db/crud.py
rename to app/shared/db/crud.py
index 0742ca7..c3c2d00 100644
--- a/src/db/crud.py
+++ b/app/shared/db/crud.py
@@ -4,15 +4,16 @@ from sqlalchemy.orm import Session, load_only
from sqlalchemy import Column, or_, func, select
from loguru import logger
from datetime import datetime, timedelta
-
-from core.config import ALLOW_ANY_EMAIL
-from db.database import get_db
-from shared.settings import get_settings
-from shared.user_groups import UserGroups
-from utils.misc import fnv1a_hash_mod
-from . import models, schemas
from sqlalchemy.ext.asyncio import AsyncSession
+from app.shared.config import ALLOW_ANY_EMAIL
+from app.shared.db.database import get_db
+from app.shared.db import models
+from app.shared import schemas
+from app.shared.settings import get_settings
+from app.shared.user_groups import UserGroups
+from app.shared.utils.misc import fnv1a_hash_mod
+
DATABASE_QUERY_LIMIT = get_settings().DATABASE_QUERY_LIMIT
@@ -304,7 +305,7 @@ def delete_sheet(db: Session, sheet_id: str, email: str) -> bool:
#--- Celery worker tasks
-def insert_result_into_db(db: Session, archive: schemas.ArchiveCreate) -> models.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]
diff --git a/src/db/database.py b/app/shared/db/database.py
similarity index 97%
rename from src/db/database.py
rename to app/shared/db/database.py
index f672b87..d404b5c 100644
--- a/src/db/database.py
+++ b/app/shared/db/database.py
@@ -1,10 +1,11 @@
from functools import lru_cache
from sqlalchemy import Engine, create_engine, event, text
from sqlalchemy.orm import sessionmaker
-from shared.settings import get_settings
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):
diff --git a/src/db/models.py b/app/shared/db/models.py
similarity index 100%
rename from src/db/models.py
rename to app/shared/db/models.py
diff --git a/src/db/user_state.py b/app/shared/db/user_state.py
similarity index 98%
rename from src/db/user_state.py
rename to app/shared/db/user_state.py
index 06077ce..d7f5192 100644
--- a/src/db/user_state.py
+++ b/app/shared/db/user_state.py
@@ -3,11 +3,11 @@ from typing import Dict, Set
import sqlalchemy
from sqlalchemy.orm import Session
from sqlalchemy import func
-from db import crud, models
from datetime import datetime
-from shared.user_groups import GroupInfo, GroupPermissions
-from db.schemas import Usage, UsageResponse
+from app.shared.db import crud, models
+from app.shared.user_groups import GroupInfo, GroupPermissions
+from app.shared.schemas import Usage, UsageResponse
class UserState:
"""
diff --git a/app/shared/log.py b/app/shared/log.py
new file mode 100644
index 0000000..b52d136
--- /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", 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 = ""):
+ 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/src/db/schemas.py b/app/shared/schemas.py
similarity index 100%
rename from src/db/schemas.py
rename to app/shared/schemas.py
diff --git a/src/shared/settings.py b/app/shared/settings.py
similarity index 92%
rename from src/shared/settings.py
rename to app/shared/settings.py
index 8a7cf31..e3b4f77 100644
--- a/src/shared/settings.py
+++ b/app/shared/settings.py
@@ -31,8 +31,8 @@ class Settings(BaseSettings):
return self.DATABASE_PATH.replace("sqlite://", "sqlite+aiosqlite://")
# redis
+ REDIS_PASSWORD: str = ""
CELERY_BROKER_URL: str = "redis://localhost:6379"
- CELERY_RESULT_BACKEND: str = "redis://localhost:6379"
REDIS_EXCEPTIONS_CHANNEL: str = "exceptions-channel"
# observability
@@ -40,8 +40,8 @@ class Settings(BaseSettings):
# 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)]
+ 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()
diff --git a/src/shared/task_messaging.py b/app/shared/task_messaging.py
similarity index 75%
rename from src/shared/task_messaging.py
rename to app/shared/task_messaging.py
index 4b2e000..52dcba3 100644
--- a/src/shared/task_messaging.py
+++ b/app/shared/task_messaging.py
@@ -3,14 +3,14 @@ from functools import lru_cache
from celery import Celery
import redis
-from shared.settings import get_settings
+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_RESULT_BACKEND,
+ result_backend=get_settings().CELERY_BROKER_URL,
)
diff --git a/src/shared/user_groups.py b/app/shared/user_groups.py
similarity index 94%
rename from src/shared/user_groups.py
rename to app/shared/user_groups.py
index 7c04c89..8647ed9 100644
--- a/src/shared/user_groups.py
+++ b/app/shared/user_groups.py
@@ -1,3 +1,4 @@
+import os
import yaml
from loguru import logger
from pydantic import BaseModel, field_validator, Field, model_validator
@@ -65,12 +66,19 @@ class GroupPermissions(BaseModel):
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_priority(cls, v):
+ if not os.path.exists(v):
+ raise ValueError(f"Orchestrator file not found with this path: {v}")
+ return v
+
class UserGroupModel(BaseModel):
users: Dict[str, List[str]] = Field(default_factory=dict)
@@ -125,6 +133,8 @@ class UserGroupModel(BaseModel):
return self
# for the API return values
+
+
class GroupInfo(GroupPermissions):
description: str = ""
- service_account_emails: list[str] = []
\ No newline at end of file
+ service_account_emails: list[str] = []
diff --git a/src/utils/misc.py b/app/shared/utils/misc.py
similarity index 66%
rename from src/utils/misc.py
rename to app/shared/utils/misc.py
index 4f94a63..562b2c3 100644
--- a/src/utils/misc.py
+++ b/app/shared/utils/misc.py
@@ -1,10 +1,3 @@
-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 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
diff --git a/src/tests/conftest.py b/app/tests/conftest.py
similarity index 98%
rename from src/tests/conftest.py
rename to app/tests/conftest.py
index 33c2886..dbf1ec5 100644
--- a/src/tests/conftest.py
+++ b/app/tests/conftest.py
@@ -2,7 +2,7 @@ import os
from fastapi.testclient import TestClient
import pytest
from unittest.mock import patch
-from core.config import ALLOW_ANY_EMAIL
+from app.shared.config import ALLOW_ANY_EMAIL
from db.user_state import UserState
from shared.settings import Settings
diff --git a/src/tests/db/test_crud.py b/app/tests/db/test_crud.py
similarity index 98%
rename from src/tests/db/test_crud.py
rename to app/tests/db/test_crud.py
index 9517bd2..bbc8bdd 100644
--- a/src/tests/db/test_crud.py
+++ b/app/tests/db/test_crud.py
@@ -63,7 +63,7 @@ def test_data(db_session):
def test_get_archive(test_data, db_session):
from db import crud
- from core.config import ALLOW_ANY_EMAIL
+ from app.shared.config import ALLOW_ANY_EMAIL
print(db_session.query(models.Group).all())
@@ -94,7 +94,7 @@ def test_get_archive(test_data, db_session):
def test_search_archives_by_url(test_data, db_session):
from db import crud
- from core.config import ALLOW_ANY_EMAIL
+ from app.shared.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
@@ -140,7 +140,7 @@ def test_search_archives_by_url(test_data, db_session):
def test_search_archives_by_email(test_data, db_session):
- from core.config import ALLOW_ANY_EMAIL
+ from app.shared.config import ALLOW_ANY_EMAIL
from db import crud
# lower/upper case
@@ -163,7 +163,7 @@ def test_search_archives_by_email(test_data, db_session):
@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
+ from app.shared.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
@@ -304,7 +304,7 @@ def test_create_tag(db_session):
def test_is_user_in_group(test_data, db_session):
from db import crud
- from core.config import ALLOW_ANY_EMAIL
+ from app.shared.config import ALLOW_ANY_EMAIL
# see user-groups.test.yaml
test_pairs = [
diff --git a/src/tests/db/test_models.py b/app/tests/db/test_models.py
similarity index 100%
rename from src/tests/db/test_models.py
rename to app/tests/db/test_models.py
diff --git a/src/tests/endpoints/test_default.py b/app/tests/endpoints/test_default.py
similarity index 97%
rename from src/tests/endpoints/test_default.py
rename to app/tests/endpoints/test_default.py
index d93f35d..4215ec6 100644
--- a/src/tests/endpoints/test_default.py
+++ b/app/tests/endpoints/test_default.py
@@ -1,7 +1,7 @@
from unittest.mock import AsyncMock, MagicMock, patch
from fastapi.testclient import TestClient
import pytest
-from core.config import VERSION
+from app.shared.config import VERSION
from tests.db.test_crud import test_data
@@ -103,7 +103,7 @@ async def test_prometheus_metrics(test_data, client_with_token, get_settings):
assert 'disk_utilization{type="used"}' not in r.text
# after metrics calculation
- from utils.metrics import measure_regular_metrics
+ from 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
@@ -117,7 +117,7 @@ async def test_prometheus_metrics(test_data, client_with_token, get_settings):
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
+ from 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
diff --git a/src/tests/endpoints/test_interoperability.py b/app/tests/endpoints/test_interoperability.py
similarity index 97%
rename from src/tests/endpoints/test_interoperability.py
rename to app/tests/endpoints/test_interoperability.py
index fa97d86..2dac484 100644
--- a/src/tests/endpoints/test_interoperability.py
+++ b/app/tests/endpoints/test_interoperability.py
@@ -2,7 +2,7 @@ from datetime import datetime
import json
from unittest.mock import patch
-from core.config import ALLOW_ANY_EMAIL
+from app.shared.config import ALLOW_ANY_EMAIL
from db import crud
diff --git a/src/tests/endpoints/test_sheet.py b/app/tests/endpoints/test_sheet.py
similarity index 99%
rename from src/tests/endpoints/test_sheet.py
rename to app/tests/endpoints/test_sheet.py
index 5d43b65..d9c2f31 100644
--- a/src/tests/endpoints/test_sheet.py
+++ b/app/tests/endpoints/test_sheet.py
@@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch
from fastapi.testclient import TestClient
-from db.schemas import TaskResult
+from app.shared.schemas import TaskResult
def test_endpoints_no_auth(client, test_no_auth):
diff --git a/src/tests/endpoints/test_task.py b/app/tests/endpoints/test_task.py
similarity index 100%
rename from src/tests/endpoints/test_task.py
rename to app/tests/endpoints/test_task.py
diff --git a/src/tests/endpoints/test_url.py b/app/tests/endpoints/test_url.py
similarity index 99%
rename from src/tests/endpoints/test_url.py
rename to app/tests/endpoints/test_url.py
index 6128291..c5d2fcc 100644
--- a/src/tests/endpoints/test_url.py
+++ b/app/tests/endpoints/test_url.py
@@ -1,7 +1,7 @@
import json
from unittest.mock import MagicMock, patch
-from db.schemas import ArchiveCreate, TaskResult
+from app.shared.schemas import ArchiveCreate, TaskResult
def test_archive_url_unauthenticated(client, test_no_auth):
diff --git a/src/tests/orchestration.test.yaml b/app/tests/orchestration.test.yaml
similarity index 100%
rename from src/tests/orchestration.test.yaml
rename to app/tests/orchestration.test.yaml
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/src/tests/user-groups.test.yaml b/app/tests/user-groups.test.yaml
similarity index 100%
rename from src/tests/user-groups.test.yaml
rename to app/tests/user-groups.test.yaml
diff --git a/src/tests/web/test_main.py b/app/tests/web/test_main.py
similarity index 96%
rename from src/tests/web/test_main.py
rename to app/tests/web/test_main.py
index 7e3b77e..817125c 100644
--- a/src/tests/web/test_main.py
+++ b/app/tests/web/test_main.py
@@ -19,7 +19,7 @@ def test_alembic(db_session):
@patch("endpoints.default.crud.soft_delete_task", side_effect=Exception('mocked error'))
def test_logging_middleware(m1, client_with_auth):
- from utils.metrics import EXCEPTION_COUNTER
+ from web.utils.metrics import EXCEPTION_COUNTER
assert len(EXCEPTION_COUNTER.collect()[0].samples) == 0
with pytest.raises(Exception, match="mocked error"):
client_with_auth.delete("/url/123")
diff --git a/src/tests/web/test_security.py b/app/tests/web/test_security.py
similarity index 99%
rename from src/tests/web/test_security.py
rename to app/tests/web/test_security.py
index c7427d1..e9cb1e8 100644
--- a/src/tests/web/test_security.py
+++ b/app/tests/web/test_security.py
@@ -4,7 +4,7 @@ from fastapi import HTTPException
from fastapi.security import HTTPAuthorizationCredentials
import pytest
-from core.config import ALLOW_ANY_EMAIL
+from app.shared.config import ALLOW_ANY_EMAIL
def test_secure_compare():
diff --git a/src/tests/worker/test_worker_main.py b/app/tests/worker/test_worker_main.py
similarity index 100%
rename from src/tests/worker/test_worker_main.py
rename to app/tests/worker/test_worker_main.py
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/shared/__init__.py b/app/web/endpoints/__init__.py
similarity index 100%
rename from src/shared/__init__.py
rename to app/web/endpoints/__init__.py
diff --git a/src/endpoints/default.py b/app/web/endpoints/default.py
similarity index 81%
rename from src/endpoints/default.py
rename to app/web/endpoints/default.py
index 80921ce..0568e51 100644
--- a/src/endpoints/default.py
+++ b/app/web/endpoints/default.py
@@ -2,15 +2,14 @@
from typing import Dict
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
-from db.schemas import ActiveUser, UsageResponse
-from db.user_state import UserState
-from web.security import get_user_auth, bearer_security, get_user_state
-from shared.user_groups import GroupInfo
+from app.shared.config import VERSION, BREAKING_CHANGES
+from app.shared.log import log_error
+from app.shared.db import crud
+from app.shared.schemas import ActiveUser, UsageResponse
+from app.shared.db.user_state import UserState
+from app.web.security import get_user_auth, bearer_security, get_user_state
+from app.shared.user_groups import GroupInfo
default_router = APIRouter()
@@ -57,4 +56,4 @@ def get_user_usage(
@default_router.get('/favicon.ico', include_in_schema=False)
async def favicon() -> FileResponse:
- return FileResponse("static/favicon.ico")
+ return FileResponse("web/static/favicon.ico")
diff --git a/src/endpoints/interoperability.py b/app/web/endpoints/interoperability.py
similarity index 61%
rename from src/endpoints/interoperability.py
rename to app/web/endpoints/interoperability.py
index 752f7e8..33eff6e 100644
--- a/src/endpoints/interoperability.py
+++ b/app/web/endpoints/interoperability.py
@@ -1,14 +1,19 @@
import json
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import JSONResponse
-from auto_archiver import Metadata
+from loguru import logger
import sqlalchemy
+from auto_archiver import Metadata
+from sqlalchemy.orm import Session
-from core.config import ALLOW_ANY_EMAIL
-from web.security import token_api_key_auth
-from db import models, schemas
-from worker.main import insert_result_into_db, get_all_urls, get_store_until
-from core.logging import log_error
+from app.shared.aa_utils import get_all_urls
+from app.shared.config import ALLOW_ANY_EMAIL
+from app.shared import business_logic, schemas
+from app.shared.db import 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."])
@@ -18,7 +23,8 @@ interoperability_router = APIRouter(prefix="/interop", tags=["Interoperability e
@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)
+ auth=Depends(token_api_key_auth),
+ db: Session = Depends(get_db_dependency)
):
result: Metadata = Metadata.from_json(manual.result)
manual.author_id = manual.author_id or ALLOW_ANY_EMAIL
@@ -34,10 +40,12 @@ def submit_manual_archive(
id=models.generate_uuid(),
result=json.loads(result.to_json()),
urls=get_all_urls(result),
- store_until=get_store_until(manual.group_id),
+ store_until=business_logic.get_store_archive_until(db, manual.group_id),
)
- archive_id = insert_result_into_db(archive)
+
+ db_archive = 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.")
- return JSONResponse({"id": archive_id}, status_code=201)
diff --git a/src/endpoints/sheet.py b/app/web/endpoints/sheet.py
similarity index 91%
rename from src/endpoints/sheet.py
rename to app/web/endpoints/sheet.py
index e479ee9..643ecac 100644
--- a/src/endpoints/sheet.py
+++ b/app/web/endpoints/sheet.py
@@ -5,11 +5,12 @@ from fastapi.responses import JSONResponse
from sqlalchemy import exc
from sqlalchemy.orm import Session
-from db.user_state import UserState
-from shared.task_messaging import get_celery
-from web.security import get_user_state
-from db import schemas, crud
-from db.database import get_db_dependency
+from app.shared.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.shared.db import crud
+from app.shared.db.database import get_db_dependency
sheet_router = APIRouter(prefix="/sheet", tags=["Google Spreadsheet operations"])
diff --git a/src/endpoints/task.py b/app/web/endpoints/task.py
similarity index 85%
rename from src/endpoints/task.py
rename to app/web/endpoints/task.py
index bacdb31..610c579 100644
--- a/src/endpoints/task.py
+++ b/app/web/endpoints/task.py
@@ -3,13 +3,11 @@ from fastapi import APIRouter, Depends
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
-from loguru import logger
-from shared.task_messaging import get_celery
-from web.security import get_token_or_user_auth
-
-from db import schemas
-from core.logging import log_error
-from utils.misc 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"])
diff --git a/src/endpoints/url.py b/app/web/endpoints/url.py
similarity index 90%
rename from src/endpoints/url.py
rename to app/web/endpoints/url.py
index 0c35238..72e0cc2 100644
--- a/src/endpoints/url.py
+++ b/app/web/endpoints/url.py
@@ -2,16 +2,16 @@
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import JSONResponse
from datetime import datetime
-
from loguru import logger
-from core.config import ALLOW_ANY_EMAIL
-from db.user_state import UserState
-from shared.task_messaging import get_celery
-from web.security import get_token_or_user_auth, get_user_state
from sqlalchemy.orm import Session
-from db import crud, schemas
-from db.database import get_db_dependency
+from app.shared.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.shared.db import crud
+from app.shared.db.user_state import UserState
+from app.shared.db.database import get_db_dependency
from urllib.parse import urlparse
diff --git a/src/core/events.py b/app/web/events.py
similarity index 92%
rename from src/core/events.py
rename to app/web/events.py
index d051e12..65b7347 100644
--- a/src/core/events.py
+++ b/app/web/events.py
@@ -7,14 +7,15 @@ from fastapi import FastAPI
from contextlib import asynccontextmanager
from fastapi_utils.tasks import repeat_every
from loguru import logger
-
-from db import crud, models, schemas
-from db.database import get_db, get_db_async, make_engine, wal_checkpoint
-from shared.settings import get_settings
-from shared.task_messaging import get_celery
-from utils.metrics import measure_regular_metrics, redis_subscribe_worker_exceptions
from fastapi_mail import FastMail, MessageSchema, MessageType
+from app.shared.db import crud, 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.utils.metrics import measure_regular_metrics, redis_subscribe_worker_exceptions
+
celery = get_celery()
@@ -23,9 +24,12 @@ async def lifespan(app: FastAPI):
# see https://fastapi.tiangolo.com/advanced/events/#lifespan
# STARTUP
+ logger.debug("HERE 00")
engine = make_engine(get_settings().DATABASE_PATH)
models.Base.metadata.create_all(bind=engine)
- alembic.config.main(argv=['--raiseerr', 'upgrade', 'head'])
+ logger.debug("HERE 01")
+ alembic.config.main(prog="alembic", argv=['--raiseerr', 'upgrade', 'head'])
+ logger.debug("HERE 02")
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())
@@ -88,6 +92,7 @@ async def archive_sheets_cronjob(frequency: str, interval: int, current_time_uni
# TODO: on exception should logerror but also prometheus counter
DELETE_WINDOW = get_settings().DELETE_SCHEDULED_ARCHIVES_NOTIFY_DAYS * 24 * 60 * 60
+
@repeat_every(seconds=DELETE_WINDOW, wait_first=180, on_exception=logger.error)
async def notify_about_expired_archives():
notify_from = datetime.datetime.now() + datetime.timedelta(days=get_settings().DELETE_SCHEDULED_ARCHIVES_NOTIFY_DAYS)
@@ -103,7 +108,7 @@ async def notify_about_expired_archives():
# notify users
for email in user_archives:
list_of_archives = "\n".join([f'{a.url},{a.id} ' for a in user_archives[email]])
- #TODO: how can users download them in bulk?
+ # TODO: how can users download them in bulk?
message = MessageSchema(
subject="Auto Archiver: Archives Scheduled for Deletion",
recipients=[email],
@@ -139,7 +144,6 @@ async def delete_expired_archives():
logger.info(f"[CRON] Deleted {count_deleted} archives.")
-
@repeat_every(seconds=86400, wait_first=150, on_exception=logger.error)
async def delete_stale_sheets():
STALE_DAYS = get_settings().DELETE_STALE_SHEETS_DAYS
diff --git a/src/web/main.py b/app/web/main.py
similarity index 81%
rename from src/web/main.py
rename to app/web/main.py
index ee6d192..a11908e 100644
--- a/src/web/main.py
+++ b/app/web/main.py
@@ -7,28 +7,27 @@ 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 shared.task_messaging import get_celery
-from worker.main import insert_result_into_db
+from app.shared.log import log_error
+from app.web.middleware import logging_middleware
+from app.shared import schemas
+from app.shared.task_messaging import get_celery
-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 app.shared.db import crud
+from app.web.security import get_user_auth, token_api_key_auth, get_token_or_user_auth
+from app.shared.config import VERSION, API_DESCRIPTION
+from app.shared.db.database import get_db_dependency
+from app.web.events import lifespan
+from app.shared.settings import get_settings
-from auto_archiver import Metadata
-from endpoints.default import default_router
-from endpoints.url import url_router
-from endpoints.sheet import sheet_router
-from endpoints.task import task_router
-from endpoints.interoperability import interoperability_router
+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()
@@ -161,14 +160,15 @@ def app_factory(settings = get_settings()):
@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})
+ raise HTTPException(status_code=410, detail="This endpoint is deprecated. Use /interop/submit-archive instead.")
+ # 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/core/logging.py b/app/web/middleware.py
similarity index 53%
rename from src/core/logging.py
rename to app/web/middleware.py
index 4929232..3663af9 100644
--- a/src/core/logging.py
+++ b/app/web/middleware.py
@@ -1,26 +1,17 @@
-import traceback
+
from loguru import logger
from fastapi import Request
+from app.shared.log import log_error
-# 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 = ""):
- 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
+ from web.utils.metrics import EXCEPTION_COUNTER
EXCEPTION_COUNTER.labels(type=e.__class__.__name__).inc()
logger.info(f"{request.client.host}:{request.client.port} {request.method} {request.url._url} - {e.__class__.__name__} {e}")
log_error(e)
- raise e
\ No newline at end of file
+ raise e
diff --git a/src/web/security.py b/app/web/security.py
similarity index 94%
rename from src/web/security.py
rename to app/web/security.py
index 85ceae4..cefabdc 100644
--- a/src/web/security.py
+++ b/app/web/security.py
@@ -2,11 +2,11 @@ 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 db.database import get_db
-from db import crud
-from db.user_state import UserState
+
+from app.shared.config import ALLOW_ANY_EMAIL
+from app.shared.settings import get_settings
+from app.shared.db.database import get_db
+from app.shared.db.user_state import UserState
settings = get_settings()
bearer_security = HTTPBearer()
diff --git a/src/static/.gitkeep b/app/web/static/.gitkeep
similarity index 100%
rename from src/static/.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/utils/__init__.py b/app/web/utils/__init__.py
similarity index 100%
rename from src/utils/__init__.py
rename to app/web/utils/__init__.py
diff --git a/src/utils/metrics.py b/app/web/utils/metrics.py
similarity index 93%
rename from src/utils/metrics.py
rename to app/web/utils/metrics.py
index 05c0bb0..0a2d793 100644
--- a/src/utils/metrics.py
+++ b/app/web/utils/metrics.py
@@ -3,12 +3,11 @@ 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 shared.task_messaging import get_redis
+from app.shared.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
@@ -39,7 +38,7 @@ DATABASE_METRICS_COUNTER = Counter(
)
-async def redis_subscribe_worker_exceptions(REDIS_EXCEPTIONS_CHANNEL):
+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
Redis = get_redis()
PubSubExceptions = Redis.pubsub()
diff --git a/app/web/utils/misc.py b/app/web/utils/misc.py
new file mode 100644
index 0000000..cfa856b
--- /dev/null
+++ b/app/web/utils/misc.py
@@ -0,0 +1,7 @@
+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)
diff --git a/src/worker/__init__.py b/app/worker/__init__.py
similarity index 100%
rename from src/worker/__init__.py
rename to app/worker/__init__.py
diff --git a/src/worker/main.py b/app/worker/main.py
similarity index 73%
rename from src/worker/main.py
rename to app/worker/main.py
index df8ffb8..966cd5c 100644
--- a/src/worker/main.py
+++ b/app/worker/main.py
@@ -1,19 +1,19 @@
+import json
import traceback, datetime
-from typing import List
-
from celery.signals import task_failure
-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.task_messaging import get_celery, get_redis
-from shared.settings import get_settings
-import json
from sqlalchemy import exc
-from core.logging import log_error
+
+from auto_archiver import Config, ArchivingOrchestrator, Metadata
+
+from app.shared.db import crud, 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
settings = get_settings()
@@ -24,8 +24,6 @@ Redis = get_redis()
USER_GROUPS_FILENAME = settings.USER_GROUPS_FILENAME
# 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': 0})
def create_archive_task(self, archive_json: str):
logger.info(archive_json)
@@ -96,6 +94,7 @@ def load_orchestrator(group_id: str, orchestrator_for_sheet: bool = False, overw
else:
orchestrator_fn = crud.get_group(session, group_id).orchestrator
assert orchestrator_fn, f"no orchestrator found for {group_id}"
+
config = Config()
config.parse(use_cli=False, yaml_config_filename=orchestrator_fn, overwrite_configs=overwrite_configs)
@@ -104,44 +103,13 @@ def load_orchestrator(group_id: str, orchestrator_for_sheet: bool = False, overw
def insert_result_into_db(archive: schemas.ArchiveCreate) -> str:
with get_db() as session:
- db_task = crud.insert_result_into_db(session, archive)
+ db_task = crud.store_archived_url(session, archive)
logger.debug(f"[ARCHIVE STORED] {db_task.author_id} {db_task.url}")
return db_task.id
-
-# TODO: this should live within the auto-archiver
-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 get_store_until(group_id: str) -> datetime.datetime:
with get_db() as session:
- group = crud.get_group(session, group_id)
- 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)
-
-# TODO: this should live within the auto-archiver??
-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
-
+ 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
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index 2665a87..f4f6eaf 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -1,20 +1,25 @@
services:
web:
- command: uvicorn web:app --factory --host 0.0.0.0 --reload
+ 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:/aa-api/app # 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
+ - 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/app/database/auto-archiver.db
+
worker:
+ #TODO: add watchmedo
restart: "no"
- env_file: src/.env.dev
+ env_file: .env.dev
redis:
+ command: redis-server /conf/redis.conf --requirepass ${REDIS_PASSWORD}
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 4e6b807..d59e1c0 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,13 +5,21 @@ volumes:
name: "auto-archiver-api"
services:
web:
- <<: *base-setup
+ build:
+ context: .
+ dockerfile: worker.Dockerfile
+ restart: always
+ env_file: .env.prod
+ environment:
+ CELERY_BROKER_URL: redis://:${REDIS_PASSWORD}@redis:6379/0
ports:
- "127.0.0.1:8004:8000"
#TODO: should prod have the --reload flag?
- 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
+ # - ./app:/app
+ - ./app/logs:/aa-api/app/logs
+ - ./app/database:/aa-api/app/database
depends_on:
- redis
healthcheck:
@@ -31,16 +29,19 @@ services:
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=info --logfile=/aa-api/app/logs/celery.log
volumes:
- - ./src:/app
+ - ./app/logs:/aa-api/app/logs
+ - ./app/database:/aa-api/app/database
- /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
+ CELERY_BROKER_URL: redis://:${REDIS_PASSWORD}@redis:6379/0
WACZ_ENABLE_DOCKER: 1 # Enable calling docker from this container
BROWSERTRIX_HOME_HOST: auto-archiver-api_crawls
BROWSERTRIX_HOME_CONTAINER: /crawls
@@ -58,8 +59,8 @@ services:
restart: always
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/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/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/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/Dockerfile b/worker.Dockerfile
similarity index 69%
rename from src/Dockerfile
rename to worker.Dockerfile
index 5bab89e..5073228 100644
--- a/src/Dockerfile
+++ b/worker.Dockerfile
@@ -2,7 +2,7 @@
FROM bellingcat/auto-archiver
# set work directory
-WORKDIR /app
+WORKDIR /aa-api
RUN curl -fsSL https://get.docker.com -o get-docker.sh && \
sh get-docker.sh
@@ -13,10 +13,13 @@ ENV PYTHONDONTWRITEBYTECODE=1
# install dependencies
RUN pip install --upgrade pip && \
apt-get update
-COPY Pipfile* ./
+COPY ./Pipfile* ./
RUN pipenv install
-# copy src code over
-COPY . .
+# copy source code and .env files over
+COPY alembic.ini ./
+COPY .env* ./app/
+COPY ./secrets/ ./secrets/
+COPY ./app/ ./app/
ENTRYPOINT ["pipenv", "run"]
\ No newline at end of file
From c2dff5c121d0f7037a953d920c743bf968c50d4b Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Mon, 10 Feb 2025 22:39:02 +0000
Subject: [PATCH 35/75] refactors from pipenv to poetry
---
app/shared/config.py | 2 +-
app/shared/log.py | 4 +-
docker-compose.dev.yml | 4 +-
poetry.lock | 4462 ++++++++++++++++++++++++++++++++++++++++
pyproject.toml | 51 +
worker.Dockerfile | 23 +-
6 files changed, 4535 insertions(+), 11 deletions(-)
create mode 100644 poetry.lock
create mode 100644 pyproject.toml
diff --git a/app/shared/config.py b/app/shared/config.py
index dcfd135..6122b1c 100644
--- a/app/shared/config.py
+++ b/app/shared/config.py
@@ -1,4 +1,4 @@
-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.
diff --git a/app/shared/log.py b/app/shared/log.py
index b52d136..d11b7a3 100644
--- a/app/shared/log.py
+++ b/app/shared/log.py
@@ -3,8 +3,8 @@ from loguru import logger
# 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")
+logger.add("app/logs/api_logs.log", retention="30 days", rotation="3 days")
+logger.add("app/logs/error_logs.log", retention="30 days", level="ERROR")
def log_error(e: Exception, traceback_str: str = None, extra:str = ""):
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index f4f6eaf..3f5b264 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -13,9 +13,11 @@ services:
worker:
- #TODO: add watchmedo
+ command: watchmedo auto-restart --patterns="*.py" --recursive --ignore-directories -- celery -- --app=app.worker.main.celery worker --loglevel=info --logfile=/aa-api/app/logs/celery.log
restart: "no"
env_file: .env.dev
+ volumes:
+ - ./app:/aa-api/app # for watchmedo
redis:
command: redis-server /conf/redis.conf --requirepass ${REDIS_PASSWORD}
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..0dba0cf
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,4462 @@
+# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand.
+
+[[package]]
+name = "aiohappyeyeballs"
+version = "2.4.6"
+description = "Happy Eyeballs for asyncio"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "aiohappyeyeballs-2.4.6-py3-none-any.whl", hash = "sha256:147ec992cf873d74f5062644332c539fcd42956dc69453fe5204195e560517e1"},
+ {file = "aiohappyeyeballs-2.4.6.tar.gz", hash = "sha256:9b05052f9042985d32ecbe4b59a77ae19c006a78f1344d7fdad69d28ded3d0b0"},
+]
+
+[[package]]
+name = "aiohttp"
+version = "3.11.12"
+description = "Async http client/server framework (asyncio)"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "aiohttp-3.11.12-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:aa8a8caca81c0a3e765f19c6953416c58e2f4cc1b84829af01dd1c771bb2f91f"},
+ {file = "aiohttp-3.11.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:84ede78acde96ca57f6cf8ccb8a13fbaf569f6011b9a52f870c662d4dc8cd854"},
+ {file = "aiohttp-3.11.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:584096938a001378484aa4ee54e05dc79c7b9dd933e271c744a97b3b6f644957"},
+ {file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:392432a2dde22b86f70dd4a0e9671a349446c93965f261dbaecfaf28813e5c42"},
+ {file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:88d385b8e7f3a870146bf5ea31786ef7463e99eb59e31db56e2315535d811f55"},
+ {file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b10a47e5390c4b30a0d58ee12581003be52eedd506862ab7f97da7a66805befb"},
+ {file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b5263dcede17b6b0c41ef0c3ccce847d82a7da98709e75cf7efde3e9e3b5cae"},
+ {file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50c5c7b8aa5443304c55c262c5693b108c35a3b61ef961f1e782dd52a2f559c7"},
+ {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1c031a7572f62f66f1257db37ddab4cb98bfaf9b9434a3b4840bf3560f5e788"},
+ {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:7e44eba534381dd2687be50cbd5f2daded21575242ecfdaf86bbeecbc38dae8e"},
+ {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:145a73850926018ec1681e734cedcf2716d6a8697d90da11284043b745c286d5"},
+ {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:2c311e2f63e42c1bf86361d11e2c4a59f25d9e7aabdbdf53dc38b885c5435cdb"},
+ {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ea756b5a7bac046d202a9a3889b9a92219f885481d78cd318db85b15cc0b7bcf"},
+ {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:526c900397f3bbc2db9cb360ce9c35134c908961cdd0ac25b1ae6ffcaa2507ff"},
+ {file = "aiohttp-3.11.12-cp310-cp310-win32.whl", hash = "sha256:b8d3bb96c147b39c02d3db086899679f31958c5d81c494ef0fc9ef5bb1359b3d"},
+ {file = "aiohttp-3.11.12-cp310-cp310-win_amd64.whl", hash = "sha256:7fe3d65279bfbee8de0fb4f8c17fc4e893eed2dba21b2f680e930cc2b09075c5"},
+ {file = "aiohttp-3.11.12-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:87a2e00bf17da098d90d4145375f1d985a81605267e7f9377ff94e55c5d769eb"},
+ {file = "aiohttp-3.11.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b34508f1cd928ce915ed09682d11307ba4b37d0708d1f28e5774c07a7674cac9"},
+ {file = "aiohttp-3.11.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:936d8a4f0f7081327014742cd51d320296b56aa6d324461a13724ab05f4b2933"},
+ {file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de1378f72def7dfb5dbd73d86c19eda0ea7b0a6873910cc37d57e80f10d64e1"},
+ {file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9d45dbb3aaec05cf01525ee1a7ac72de46a8c425cb75c003acd29f76b1ffe94"},
+ {file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:930ffa1925393381e1e0a9b82137fa7b34c92a019b521cf9f41263976666a0d6"},
+ {file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8340def6737118f5429a5df4e88f440746b791f8f1c4ce4ad8a595f42c980bd5"},
+ {file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4016e383f91f2814e48ed61e6bda7d24c4d7f2402c75dd28f7e1027ae44ea204"},
+ {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3c0600bcc1adfaaac321422d615939ef300df81e165f6522ad096b73439c0f58"},
+ {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:0450ada317a65383b7cce9576096150fdb97396dcfe559109b403c7242faffef"},
+ {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:850ff6155371fd802a280f8d369d4e15d69434651b844bde566ce97ee2277420"},
+ {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8fd12d0f989c6099e7b0f30dc6e0d1e05499f3337461f0b2b0dadea6c64b89df"},
+ {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:76719dd521c20a58a6c256d058547b3a9595d1d885b830013366e27011ffe804"},
+ {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:97fe431f2ed646a3b56142fc81d238abcbaff08548d6912acb0b19a0cadc146b"},
+ {file = "aiohttp-3.11.12-cp311-cp311-win32.whl", hash = "sha256:e10c440d142fa8b32cfdb194caf60ceeceb3e49807072e0dc3a8887ea80e8c16"},
+ {file = "aiohttp-3.11.12-cp311-cp311-win_amd64.whl", hash = "sha256:246067ba0cf5560cf42e775069c5d80a8989d14a7ded21af529a4e10e3e0f0e6"},
+ {file = "aiohttp-3.11.12-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e392804a38353900c3fd8b7cacbea5132888f7129f8e241915e90b85f00e3250"},
+ {file = "aiohttp-3.11.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8fa1510b96c08aaad49303ab11f8803787c99222288f310a62f493faf883ede1"},
+ {file = "aiohttp-3.11.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dc065a4285307607df3f3686363e7f8bdd0d8ab35f12226362a847731516e42c"},
+ {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddb31f8474695cd61fc9455c644fc1606c164b93bff2490390d90464b4655df"},
+ {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9dec0000d2d8621d8015c293e24589d46fa218637d820894cb7356c77eca3259"},
+ {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3552fe98e90fdf5918c04769f338a87fa4f00f3b28830ea9b78b1bdc6140e0d"},
+ {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dfe7f984f28a8ae94ff3a7953cd9678550dbd2a1f9bda5dd9c5ae627744c78e"},
+ {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a481a574af914b6e84624412666cbfbe531a05667ca197804ecc19c97b8ab1b0"},
+ {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1987770fb4887560363b0e1a9b75aa303e447433c41284d3af2840a2f226d6e0"},
+ {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a4ac6a0f0f6402854adca4e3259a623f5c82ec3f0c049374133bcb243132baf9"},
+ {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c96a43822f1f9f69cc5c3706af33239489a6294be486a0447fb71380070d4d5f"},
+ {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a5e69046f83c0d3cb8f0d5bd9b8838271b1bc898e01562a04398e160953e8eb9"},
+ {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:68d54234c8d76d8ef74744f9f9fc6324f1508129e23da8883771cdbb5818cbef"},
+ {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9fd9dcf9c91affe71654ef77426f5cf8489305e1c66ed4816f5a21874b094b9"},
+ {file = "aiohttp-3.11.12-cp312-cp312-win32.whl", hash = "sha256:0ed49efcd0dc1611378beadbd97beb5d9ca8fe48579fc04a6ed0844072261b6a"},
+ {file = "aiohttp-3.11.12-cp312-cp312-win_amd64.whl", hash = "sha256:54775858c7f2f214476773ce785a19ee81d1294a6bedc5cc17225355aab74802"},
+ {file = "aiohttp-3.11.12-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:413ad794dccb19453e2b97c2375f2ca3cdf34dc50d18cc2693bd5aed7d16f4b9"},
+ {file = "aiohttp-3.11.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a93d28ed4b4b39e6f46fd240896c29b686b75e39cc6992692e3922ff6982b4c"},
+ {file = "aiohttp-3.11.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d589264dbba3b16e8951b6f145d1e6b883094075283dafcab4cdd564a9e353a0"},
+ {file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5148ca8955affdfeb864aca158ecae11030e952b25b3ae15d4e2b5ba299bad2"},
+ {file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:525410e0790aab036492eeea913858989c4cb070ff373ec3bc322d700bdf47c1"},
+ {file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bd8695be2c80b665ae3f05cb584093a1e59c35ecb7d794d1edd96e8cc9201d7"},
+ {file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0203433121484b32646a5f5ea93ae86f3d9559d7243f07e8c0eab5ff8e3f70e"},
+ {file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40cd36749a1035c34ba8d8aaf221b91ca3d111532e5ccb5fa8c3703ab1b967ed"},
+ {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a7442662afebbf7b4c6d28cb7aab9e9ce3a5df055fc4116cc7228192ad6cb484"},
+ {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8a2fb742ef378284a50766e985804bd6adb5adb5aa781100b09befdbfa757b65"},
+ {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2cee3b117a8d13ab98b38d5b6bdcd040cfb4181068d05ce0c474ec9db5f3c5bb"},
+ {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f6a19bcab7fbd8f8649d6595624856635159a6527861b9cdc3447af288a00c00"},
+ {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e4cecdb52aaa9994fbed6b81d4568427b6002f0a91c322697a4bfcc2b2363f5a"},
+ {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:30f546358dfa0953db92ba620101fefc81574f87b2346556b90b5f3ef16e55ce"},
+ {file = "aiohttp-3.11.12-cp313-cp313-win32.whl", hash = "sha256:ce1bb21fc7d753b5f8a5d5a4bae99566386b15e716ebdb410154c16c91494d7f"},
+ {file = "aiohttp-3.11.12-cp313-cp313-win_amd64.whl", hash = "sha256:f7914ab70d2ee8ab91c13e5402122edbc77821c66d2758abb53aabe87f013287"},
+ {file = "aiohttp-3.11.12-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7c3623053b85b4296cd3925eeb725e386644fd5bc67250b3bb08b0f144803e7b"},
+ {file = "aiohttp-3.11.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:67453e603cea8e85ed566b2700efa1f6916aefbc0c9fcb2e86aaffc08ec38e78"},
+ {file = "aiohttp-3.11.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6130459189e61baac5a88c10019b21e1f0c6d00ebc770e9ce269475650ff7f73"},
+ {file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9060addfa4ff753b09392efe41e6af06ea5dd257829199747b9f15bfad819460"},
+ {file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34245498eeb9ae54c687a07ad7f160053911b5745e186afe2d0c0f2898a1ab8a"},
+ {file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8dc0fba9a74b471c45ca1a3cb6e6913ebfae416678d90529d188886278e7f3f6"},
+ {file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a478aa11b328983c4444dacb947d4513cb371cd323f3845e53caeda6be5589d5"},
+ {file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c160a04283c8c6f55b5bf6d4cad59bb9c5b9c9cd08903841b25f1f7109ef1259"},
+ {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:edb69b9589324bdc40961cdf0657815df674f1743a8d5ad9ab56a99e4833cfdd"},
+ {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4ee84c2a22a809c4f868153b178fe59e71423e1f3d6a8cd416134bb231fbf6d3"},
+ {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:bf4480a5438f80e0f1539e15a7eb8b5f97a26fe087e9828e2c0ec2be119a9f72"},
+ {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:e6b2732ef3bafc759f653a98881b5b9cdef0716d98f013d376ee8dfd7285abf1"},
+ {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f752e80606b132140883bb262a457c475d219d7163d996dc9072434ffb0784c4"},
+ {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ab3247d58b393bda5b1c8f31c9edece7162fc13265334217785518dd770792b8"},
+ {file = "aiohttp-3.11.12-cp39-cp39-win32.whl", hash = "sha256:0d5176f310a7fe6f65608213cc74f4228e4f4ce9fd10bcb2bb6da8fc66991462"},
+ {file = "aiohttp-3.11.12-cp39-cp39-win_amd64.whl", hash = "sha256:74bd573dde27e58c760d9ca8615c41a57e719bff315c9adb6f2a4281a28e8798"},
+ {file = "aiohttp-3.11.12.tar.gz", hash = "sha256:7603ca26d75b1b86160ce1bbe2787a0b706e592af5b2504e12caa88a217767b0"},
+]
+
+[package.dependencies]
+aiohappyeyeballs = ">=2.3.0"
+aiosignal = ">=1.1.2"
+async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""}
+attrs = ">=17.3.0"
+frozenlist = ">=1.1.1"
+multidict = ">=4.5,<7.0"
+propcache = ">=0.2.0"
+yarl = ">=1.17.0,<2.0"
+
+[package.extras]
+speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"]
+
+[[package]]
+name = "aiosignal"
+version = "1.3.2"
+description = "aiosignal: a list of registered asynchronous callbacks"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5"},
+ {file = "aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"},
+]
+
+[package.dependencies]
+frozenlist = ">=1.1.0"
+
+[[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 = ["main", "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 = "argparse"
+version = "1.4.0"
+description = "Python command-line parsing library"
+optional = false
+python-versions = "*"
+groups = ["main"]
+files = [
+ {file = "argparse-1.4.0-py2.py3-none-any.whl", hash = "sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314"},
+ {file = "argparse-1.4.0.tar.gz", hash = "sha256:62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4"},
+]
+
+[[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 = "async-timeout"
+version = "5.0.1"
+description = "Timeout context manager for asyncio programs"
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+markers = "python_version < \"3.11\""
+files = [
+ {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"},
+ {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"},
+]
+
+[[package]]
+name = "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.12.0"
+description = "Easily archive online media content"
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+files = [
+ {file = "auto_archiver-0.12.0-py3-none-any.whl", hash = "sha256:3cee45b9a17feba214503eb1be4e8552e40cadbba128964585e0f53a45966fc8"},
+ {file = "auto_archiver-0.12.0.tar.gz", hash = "sha256:b9f1fb490fc268462325ec3f3c97c425a9c62dd0a2b4e58c771b64e8d29f0a87"},
+]
+
+[package.dependencies]
+argparse = "*"
+beautifulsoup4 = "*"
+boto3 = "*"
+bs4 = "*"
+certvalidator = "*"
+cryptography = "*"
+dataclasses-json = "*"
+dateparser = "*"
+ffmpeg-python = "*"
+google-api-python-client = "*"
+google-auth-httplib2 = "*"
+google-auth-oauthlib = "*"
+gspread = "*"
+instaloader = "*"
+jinja2 = "*"
+jsonlines = "*"
+loguru = "*"
+minify-html = "*"
+numpy = "*"
+oauth2client = "*"
+pdqhash = "*"
+pillow = "*"
+pysubs2 = "*"
+python-slugify = "*"
+python-twitter-v2 = "*"
+pyyaml = "*"
+requests = {version = "*", extras = ["socks"]}
+retrying = "*"
+selenium = "*"
+snscrape = "*"
+telethon = "*"
+tiktok-downloader = "*"
+tqdm = "*"
+tsp-client = "*"
+vk-url-scraper = "*"
+warcio = "*"
+yt-dlp = "*"
+
+[[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 = ["main", "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.16"
+description = "The AWS SDK for Python"
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "boto3-1.36.16-py3-none-any.whl", hash = "sha256:b10583bf8bd35be1b4027ee7e26b7cdf2078c79eab18357fd602cecb6d39400b"},
+ {file = "boto3-1.36.16.tar.gz", hash = "sha256:0cf92ca0538ab115447e1c58050d43e1273e88c58ddfea2b6f133fdc508b400a"},
+]
+
+[package.dependencies]
+botocore = ">=1.36.16,<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.16"
+description = "Low-level, data-driven core of boto 3."
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "botocore-1.36.16-py3-none-any.whl", hash = "sha256:aca0348ccd730332082489b6817fdf89e1526049adcf6e9c8c11c96dd9f42c03"},
+ {file = "botocore-1.36.16.tar.gz", hash = "sha256:10c6aa386ba1a9a0faef6bb5dbfc58fc2563a3c6b95352e86a583cd5f14b11f3"},
+]
+
+[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 = "cloudscraper"
+version = "1.2.71"
+description = "A Python module to bypass Cloudflare's anti-bot page."
+optional = false
+python-versions = "*"
+groups = ["main"]
+files = [
+ {file = "cloudscraper-1.2.71-py2.py3-none-any.whl", hash = "sha256:76f50ca529ed2279e220837befdec892626f9511708e200d48d5bb76ded679b0"},
+ {file = "cloudscraper-1.2.71.tar.gz", hash = "sha256:429c6e8aa6916d5bad5c8a5eac50f3ea53c9ac22616f6cb21b18dcc71517d0d3"},
+]
+
+[package.dependencies]
+pyparsing = ">=2.4.7"
+requests = ">=2.9.2"
+requests-toolbelt = ">=0.9.1"
+
+[[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.11"
+description = "Code coverage measurement for Python"
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+files = [
+ {file = "coverage-7.6.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eafea49da254a8289bed3fab960f808b322eda5577cb17a3733014928bbfbebd"},
+ {file = "coverage-7.6.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5a3f7cbbcb4ad95067a6525f83a6fc78d9cbc1e70f8abaeeaeaa72ef34f48fc3"},
+ {file = "coverage-7.6.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de6b079b39246a7da9a40cfa62d5766bd52b4b7a88cf5a82ec4c45bf6e152306"},
+ {file = "coverage-7.6.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:60d4ad09dfc8c36c4910685faafcb8044c84e4dae302e86c585b3e2e7778726c"},
+ {file = "coverage-7.6.11-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e433b6e3a834a43dae2889adc125f3fa4c66668df420d8e49bc4ee817dd7a70"},
+ {file = "coverage-7.6.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ac5d92e2cc121a13270697e4cb37e1eb4511ac01d23fe1b6c097facc3b46489e"},
+ {file = "coverage-7.6.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5128f3ba694c0a1bde55fc480090392c336236c3e1a10dad40dc1ab17c7675ff"},
+ {file = "coverage-7.6.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:397489c611b76302dfa1d9ea079e138dddc4af80fc6819d5f5119ec8ca6c0e47"},
+ {file = "coverage-7.6.11-cp310-cp310-win32.whl", hash = "sha256:c7719a5e1dc93883a6b319bc0374ecd46fb6091ed659f3fbe281ab991634b9b0"},
+ {file = "coverage-7.6.11-cp310-cp310-win_amd64.whl", hash = "sha256:c27df03730059118b8a923cfc8b84b7e9976742560af528242f201880879c1da"},
+ {file = "coverage-7.6.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:532fe139691af134aa8b54ed60dd3c806aa81312d93693bd2883c7b61592c840"},
+ {file = "coverage-7.6.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0b0f272901a5172090c0802053fbc503cdc3fa2612720d2669a98a7384a7bec"},
+ {file = "coverage-7.6.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4bda710139ea646890d1c000feb533caff86904a0e0638f85e967c28cb8eec50"},
+ {file = "coverage-7.6.11-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a165b09e7d5f685bf659063334a9a7b1a2d57b531753d3e04bd442b3cfe5845b"},
+ {file = "coverage-7.6.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ff136607689c1c87f43d24203b6d2055b42030f352d5176f9c8b204d4235ef27"},
+ {file = "coverage-7.6.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:050172741de03525290e67f0161ae5f7f387c88fca50d47fceb4724ceaa591d2"},
+ {file = "coverage-7.6.11-cp311-cp311-win32.whl", hash = "sha256:27700d859be68e4fb2e7bf774cf49933dcac6f81a9bc4c13bd41735b8d26a53b"},
+ {file = "coverage-7.6.11-cp311-cp311-win_amd64.whl", hash = "sha256:cd4839813b09ab1dd1be1bbc74f9a7787615f931f83952b6a9af1b2d3f708bf7"},
+ {file = "coverage-7.6.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dbb1a822fd858d9853333a7c95d4e70dde9a79e65893138ce32c2ec6457d7a36"},
+ {file = "coverage-7.6.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:61c834cbb80946d6ebfddd9b393a4c46bec92fcc0fa069321fcb8049117f76ea"},
+ {file = "coverage-7.6.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a46d56e99a31d858d6912d31ffa4ede6a325c86af13139539beefca10a1234ce"},
+ {file = "coverage-7.6.11-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b48db06f53d1864fea6dbd855e6d51d41c0f06c212c3004511c0bdc6847b297"},
+ {file = "coverage-7.6.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b6ff5be3b1853e0862da9d349fe87f869f68e63a25f7c37ce1130b321140f963"},
+ {file = "coverage-7.6.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be05bde21d5e6eefbc3a6de6b9bee2b47894b8945342e8663192809c4d1f08ce"},
+ {file = "coverage-7.6.11-cp312-cp312-win32.whl", hash = "sha256:e3b746fa0ffc5b6b8856529de487da8b9aeb4fb394bb58de6502ef45f3434f12"},
+ {file = "coverage-7.6.11-cp312-cp312-win_amd64.whl", hash = "sha256:ac476e6d0128fb7919b3fae726de72b28b5c9644cb4b579e4a523d693187c551"},
+ {file = "coverage-7.6.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c86f4c7a6d1a54a24d804d9684d96e36a62d3ef7c0d7745ae2ea39e3e0293251"},
+ {file = "coverage-7.6.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7eb0504bb307401fd08bc5163a351df301438b3beb88a4fa044681295bbefc67"},
+ {file = "coverage-7.6.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca95d40900cf614e07f00cee8c2fad0371df03ca4d7a80161d84be2ec132b7a4"},
+ {file = "coverage-7.6.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db4b1a69976b1b02acda15937538a1d3fe10b185f9d99920b17a740a0a102e06"},
+ {file = "coverage-7.6.11-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf96beb05d004e4c51cd846fcdf9eee9eb2681518524b66b2e7610507944c2f"},
+ {file = "coverage-7.6.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:08e5fb93576a6b054d3d326242af5ef93daaac9bb52bc25f12ccbc3fa94227cd"},
+ {file = "coverage-7.6.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25575cd5a7d2acc46b42711e8aff826027c0e4f80fb38028a74f31ac22aae69d"},
+ {file = "coverage-7.6.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8fa4fffd90ee92f62ff7404b4801b59e8ea8502e19c9bf2d3241ce745b52926c"},
+ {file = "coverage-7.6.11-cp313-cp313-win32.whl", hash = "sha256:0d03c9452d9d1ccfe5d3a5df0427705022a49b356ac212d529762eaea5ef97b4"},
+ {file = "coverage-7.6.11-cp313-cp313-win_amd64.whl", hash = "sha256:fd2fffc8ce8692ce540103dff26279d2af22d424516ddebe2d7e4d6dbb3816b2"},
+ {file = "coverage-7.6.11-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:5e7ac966ab110bd94ee844f2643f196d78fde1cd2450399116d3efdd706e19f5"},
+ {file = "coverage-7.6.11-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ba27a0375c5ef4d2a7712f829265102decd5ff78b96d342ac2fa555742c4f4f"},
+ {file = "coverage-7.6.11-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2778be4f574b39ec9dcd9e5e13644f770351ee0990a0ecd27e364aba95af89b"},
+ {file = "coverage-7.6.11-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5edc16712187139ab635a2e644cc41fc239bc6d245b16124045743130455c652"},
+ {file = "coverage-7.6.11-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df6ff122a0a10a30121d9f0cb3fbd03a6fe05861e4ec47adb9f25e9245aabc19"},
+ {file = "coverage-7.6.11-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ff562952f15eff27247a4c4b03e45ce8a82e3fb197de6a7c54080f9d4ba07845"},
+ {file = "coverage-7.6.11-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4f21e3617f48d683f30cf2a6c8b739c838e600cb1454fe6b2eb486ac2bce8fbd"},
+ {file = "coverage-7.6.11-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6d60577673ba48d8ae8e362e61fd4ad1a640293ffe8991d11c86f195479100b7"},
+ {file = "coverage-7.6.11-cp313-cp313t-win32.whl", hash = "sha256:13100f98497086b359bf56fc035a762c674de8ef526daa389ac8932cb9bff1e0"},
+ {file = "coverage-7.6.11-cp313-cp313t-win_amd64.whl", hash = "sha256:2c81e53782043b323bd34c7de711ed9b4673414eb517eaf35af92185b873839c"},
+ {file = "coverage-7.6.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ff52b4e2ac0080c96e506819586c4b16cdbf46724bda90d308a7330a73cc8521"},
+ {file = "coverage-7.6.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f4679fcc9eb9004fdd1b00231ef1ec7167168071bebc4d66327e28c1979b4449"},
+ {file = "coverage-7.6.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:90de4e9ca4489e823138bd13098af9ac8028cc029f33f60098b5c08c675c7bda"},
+ {file = "coverage-7.6.11-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c96a142057d83ee993eaf71629ca3fb952cda8afa9a70af4132950c2bd3deb9"},
+ {file = "coverage-7.6.11-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:476f29a258b9cd153f2be5bf5f119d670d2806363595263917bddc167d6e5cce"},
+ {file = "coverage-7.6.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:09d03f48d9025b8a6a116cddcb6c7b8ce80e4fb4c31dd2e124a7c377036ad58e"},
+ {file = "coverage-7.6.11-cp39-cp39-win32.whl", hash = "sha256:bb35ae9f134fbd9cf7302a9654d5a1e597c974202678082dcc569eb39a8cde03"},
+ {file = "coverage-7.6.11-cp39-cp39-win_amd64.whl", hash = "sha256:f382004fa4c93c01016d9226b9d696a08c53f6818b7ad59b4e96cb67e863353a"},
+ {file = "coverage-7.6.11-pp39.pp310-none-any.whl", hash = "sha256:adc2d941c0381edfcf3897f94b9f41b1e504902fab78a04b1677f2f72afead4b"},
+ {file = "coverage-7.6.11-py3-none-any.whl", hash = "sha256:f0f334ae844675420164175bf32b04e18a81fe57ad8eb7e0cfd4689d681ffed7"},
+ {file = "coverage-7.6.11.tar.gz", hash = "sha256:e642e6a46a04e992ebfdabed79e46f478ec60e2c528e1e1a074d63800eda4286"},
+]
+
+[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 = "filelock"
+version = "3.17.0"
+description = "A platform independent file lock."
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338"},
+ {file = "filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e"},
+]
+
+[package.extras]
+docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"]
+testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"]
+typing = ["typing-extensions (>=4.12.2)"]
+
+[[package]]
+name = "flask"
+version = "3.1.0"
+description = "A simple framework for building complex web applications."
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "flask-3.1.0-py3-none-any.whl", hash = "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136"},
+ {file = "flask-3.1.0.tar.gz", hash = "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac"},
+]
+
+[package.dependencies]
+blinker = ">=1.9"
+click = ">=8.1.3"
+itsdangerous = ">=2.2"
+Jinja2 = ">=3.1.2"
+Werkzeug = ">=3.1"
+
+[package.extras]
+async = ["asgiref (>=3.2)"]
+dotenv = ["python-dotenv"]
+
+[[package]]
+name = "frozenlist"
+version = "1.5.0"
+description = "A list-like structure which implements collections.abc.MutableSequence"
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a"},
+ {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb"},
+ {file = "frozenlist-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec"},
+ {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5"},
+ {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76"},
+ {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17"},
+ {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba"},
+ {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d"},
+ {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2"},
+ {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f"},
+ {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c"},
+ {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab"},
+ {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5"},
+ {file = "frozenlist-1.5.0-cp310-cp310-win32.whl", hash = "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb"},
+ {file = "frozenlist-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4"},
+ {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30"},
+ {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5"},
+ {file = "frozenlist-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778"},
+ {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a"},
+ {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869"},
+ {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d"},
+ {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45"},
+ {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d"},
+ {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3"},
+ {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a"},
+ {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9"},
+ {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2"},
+ {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf"},
+ {file = "frozenlist-1.5.0-cp311-cp311-win32.whl", hash = "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942"},
+ {file = "frozenlist-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d"},
+ {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21"},
+ {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d"},
+ {file = "frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e"},
+ {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a"},
+ {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a"},
+ {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee"},
+ {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6"},
+ {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e"},
+ {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9"},
+ {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039"},
+ {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784"},
+ {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631"},
+ {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f"},
+ {file = "frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8"},
+ {file = "frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f"},
+ {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953"},
+ {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0"},
+ {file = "frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2"},
+ {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f"},
+ {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608"},
+ {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b"},
+ {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840"},
+ {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439"},
+ {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de"},
+ {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641"},
+ {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e"},
+ {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9"},
+ {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03"},
+ {file = "frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c"},
+ {file = "frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28"},
+ {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:dd94994fc91a6177bfaafd7d9fd951bc8689b0a98168aa26b5f543868548d3ca"},
+ {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0da8bbec082bf6bf18345b180958775363588678f64998c2b7609e34719b10"},
+ {file = "frozenlist-1.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:73f2e31ea8dd7df61a359b731716018c2be196e5bb3b74ddba107f694fbd7604"},
+ {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:828afae9f17e6de596825cf4228ff28fbdf6065974e5ac1410cecc22f699d2b3"},
+ {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1577515d35ed5649d52ab4319db757bb881ce3b2b796d7283e6634d99ace307"},
+ {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2150cc6305a2c2ab33299453e2968611dacb970d2283a14955923062c8d00b10"},
+ {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a72b7a6e3cd2725eff67cd64c8f13335ee18fc3c7befc05aed043d24c7b9ccb9"},
+ {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c16d2fa63e0800723139137d667e1056bee1a1cf7965153d2d104b62855e9b99"},
+ {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:17dcc32fc7bda7ce5875435003220a457bcfa34ab7924a49a1c19f55b6ee185c"},
+ {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:97160e245ea33d8609cd2b8fd997c850b56db147a304a262abc2b3be021a9171"},
+ {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f1e6540b7fa044eee0bb5111ada694cf3dc15f2b0347ca125ee9ca984d5e9e6e"},
+ {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:91d6c171862df0a6c61479d9724f22efb6109111017c87567cfeb7b5d1449fdf"},
+ {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c1fac3e2ace2eb1052e9f7c7db480818371134410e1f5c55d65e8f3ac6d1407e"},
+ {file = "frozenlist-1.5.0-cp38-cp38-win32.whl", hash = "sha256:b97f7b575ab4a8af9b7bc1d2ef7f29d3afee2226bd03ca3875c16451ad5a7723"},
+ {file = "frozenlist-1.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:374ca2dabdccad8e2a76d40b1d037f5bd16824933bf7bcea3e59c891fd4a0923"},
+ {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972"},
+ {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336"},
+ {file = "frozenlist-1.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f"},
+ {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f"},
+ {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6"},
+ {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411"},
+ {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08"},
+ {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2"},
+ {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d"},
+ {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b"},
+ {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b"},
+ {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0"},
+ {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c"},
+ {file = "frozenlist-1.5.0-cp39-cp39-win32.whl", hash = "sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3"},
+ {file = "frozenlist-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0"},
+ {file = "frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3"},
+ {file = "frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817"},
+]
+
+[[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 = [
+ {version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""},
+ {version = ">=1.22.3,<2.0.0dev", markers = "python_version < \"3.13\""},
+]
+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.160.0"
+description = "Google API Client Library for Python"
+optional = false
+python-versions = ">=3.7"
+groups = ["main"]
+files = [
+ {file = "google_api_python_client-2.160.0-py2.py3-none-any.whl", hash = "sha256:63d61fb3e4cf3fb31a70a87f45567c22f6dfe87bbfa27252317e3e2c42900db4"},
+ {file = "google_api_python_client-2.160.0.tar.gz", hash = "sha256:a8ccafaecfa42d15d5b5c3134ced8de08380019717fc9fb1ed510ca58eca3b7e"},
+]
+
+[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.66.0"
+description = "Common protobufs used in Google APIs"
+optional = false
+python-versions = ">=3.7"
+groups = ["main"]
+files = [
+ {file = "googleapis_common_protos-1.66.0-py2.py3-none-any.whl", hash = "sha256:d7abcd75fabb2e0ec9f74466401f6c119a0b498e27370e9be4c94cb7e382b8ed"},
+ {file = "googleapis_common_protos-1.66.0.tar.gz", hash = "sha256:c3e7b33d15fdca5374cc0a7346dd92ffa847425cc4ea941d970f13680052ec8c"},
+]
+
+[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 = "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\")"
+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 = ["main", "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 = ["main", "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 = "itsdangerous"
+version = "2.2.0"
+description = "Safely pass data to untrusted environments and back."
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"},
+ {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"},
+]
+
+[[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 = "lxml"
+version = "5.3.1"
+description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
+optional = false
+python-versions = ">=3.6"
+groups = ["main"]
+files = [
+ {file = "lxml-5.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a4058f16cee694577f7e4dd410263cd0ef75644b43802a689c2b3c2a7e69453b"},
+ {file = "lxml-5.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:364de8f57d6eda0c16dcfb999af902da31396949efa0e583e12675d09709881b"},
+ {file = "lxml-5.3.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:528f3a0498a8edc69af0559bdcf8a9f5a8bf7c00051a6ef3141fdcf27017bbf5"},
+ {file = "lxml-5.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db4743e30d6f5f92b6d2b7c86b3ad250e0bad8dee4b7ad8a0c44bfb276af89a3"},
+ {file = "lxml-5.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17b5d7f8acf809465086d498d62a981fa6a56d2718135bb0e4aa48c502055f5c"},
+ {file = "lxml-5.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:928e75a7200a4c09e6efc7482a1337919cc61fe1ba289f297827a5b76d8969c2"},
+ {file = "lxml-5.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a997b784a639e05b9d4053ef3b20c7e447ea80814a762f25b8ed5a89d261eac"},
+ {file = "lxml-5.3.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:7b82e67c5feb682dbb559c3e6b78355f234943053af61606af126df2183b9ef9"},
+ {file = "lxml-5.3.1-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:f1de541a9893cf8a1b1db9bf0bf670a2decab42e3e82233d36a74eda7822b4c9"},
+ {file = "lxml-5.3.1-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:de1fc314c3ad6bc2f6bd5b5a5b9357b8c6896333d27fdbb7049aea8bd5af2d79"},
+ {file = "lxml-5.3.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:7c0536bd9178f754b277a3e53f90f9c9454a3bd108b1531ffff720e082d824f2"},
+ {file = "lxml-5.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:68018c4c67d7e89951a91fbd371e2e34cd8cfc71f0bb43b5332db38497025d51"},
+ {file = "lxml-5.3.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aa826340a609d0c954ba52fd831f0fba2a4165659ab0ee1a15e4aac21f302406"},
+ {file = "lxml-5.3.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:796520afa499732191e39fc95b56a3b07f95256f2d22b1c26e217fb69a9db5b5"},
+ {file = "lxml-5.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3effe081b3135237da6e4c4530ff2a868d3f80be0bda027e118a5971285d42d0"},
+ {file = "lxml-5.3.1-cp310-cp310-win32.whl", hash = "sha256:a22f66270bd6d0804b02cd49dae2b33d4341015545d17f8426f2c4e22f557a23"},
+ {file = "lxml-5.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:0bcfadea3cdc68e678d2b20cb16a16716887dd00a881e16f7d806c2138b8ff0c"},
+ {file = "lxml-5.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e220f7b3e8656ab063d2eb0cd536fafef396829cafe04cb314e734f87649058f"},
+ {file = "lxml-5.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f2cfae0688fd01f7056a17367e3b84f37c545fb447d7282cf2c242b16262607"},
+ {file = "lxml-5.3.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67d2f8ad9dcc3a9e826bdc7802ed541a44e124c29b7d95a679eeb58c1c14ade8"},
+ {file = "lxml-5.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db0c742aad702fd5d0c6611a73f9602f20aec2007c102630c06d7633d9c8f09a"},
+ {file = "lxml-5.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:198bb4b4dd888e8390afa4f170d4fa28467a7eaf857f1952589f16cfbb67af27"},
+ {file = "lxml-5.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2a3e412ce1849be34b45922bfef03df32d1410a06d1cdeb793a343c2f1fd666"},
+ {file = "lxml-5.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b8969dbc8d09d9cd2ae06362c3bad27d03f433252601ef658a49bd9f2b22d79"},
+ {file = "lxml-5.3.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5be8f5e4044146a69c96077c7e08f0709c13a314aa5315981185c1f00235fe65"},
+ {file = "lxml-5.3.1-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:133f3493253a00db2c870d3740bc458ebb7d937bd0a6a4f9328373e0db305709"},
+ {file = "lxml-5.3.1-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:52d82b0d436edd6a1d22d94a344b9a58abd6c68c357ed44f22d4ba8179b37629"},
+ {file = "lxml-5.3.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b6f92e35e2658a5ed51c6634ceb5ddae32053182851d8cad2a5bc102a359b33"},
+ {file = "lxml-5.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:203b1d3eaebd34277be06a3eb880050f18a4e4d60861efba4fb946e31071a295"},
+ {file = "lxml-5.3.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:155e1a5693cf4b55af652f5c0f78ef36596c7f680ff3ec6eb4d7d85367259b2c"},
+ {file = "lxml-5.3.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22ec2b3c191f43ed21f9545e9df94c37c6b49a5af0a874008ddc9132d49a2d9c"},
+ {file = "lxml-5.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7eda194dd46e40ec745bf76795a7cccb02a6a41f445ad49d3cf66518b0bd9cff"},
+ {file = "lxml-5.3.1-cp311-cp311-win32.whl", hash = "sha256:fb7c61d4be18e930f75948705e9718618862e6fc2ed0d7159b2262be73f167a2"},
+ {file = "lxml-5.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:c809eef167bf4a57af4b03007004896f5c60bd38dc3852fcd97a26eae3d4c9e6"},
+ {file = "lxml-5.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e69add9b6b7b08c60d7ff0152c7c9a6c45b4a71a919be5abde6f98f1ea16421c"},
+ {file = "lxml-5.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4e52e1b148867b01c05e21837586ee307a01e793b94072d7c7b91d2c2da02ffe"},
+ {file = "lxml-5.3.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4b382e0e636ed54cd278791d93fe2c4f370772743f02bcbe431a160089025c9"},
+ {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e49dc23a10a1296b04ca9db200c44d3eb32c8d8ec532e8c1fd24792276522a"},
+ {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4399b4226c4785575fb20998dc571bc48125dc92c367ce2602d0d70e0c455eb0"},
+ {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5412500e0dc5481b1ee9cf6b38bb3b473f6e411eb62b83dc9b62699c3b7b79f7"},
+ {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c93ed3c998ea8472be98fb55aed65b5198740bfceaec07b2eba551e55b7b9ae"},
+ {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:63d57fc94eb0bbb4735e45517afc21ef262991d8758a8f2f05dd6e4174944519"},
+ {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:b450d7cabcd49aa7ab46a3c6aa3ac7e1593600a1a0605ba536ec0f1b99a04322"},
+ {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:4df0ec814b50275ad6a99bc82a38b59f90e10e47714ac9871e1b223895825468"},
+ {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d184f85ad2bb1f261eac55cddfcf62a70dee89982c978e92b9a74a1bfef2e367"},
+ {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b725e70d15906d24615201e650d5b0388b08a5187a55f119f25874d0103f90dd"},
+ {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a31fa7536ec1fb7155a0cd3a4e3d956c835ad0a43e3610ca32384d01f079ea1c"},
+ {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3c3c8b55c7fc7b7e8877b9366568cc73d68b82da7fe33d8b98527b73857a225f"},
+ {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d61ec60945d694df806a9aec88e8f29a27293c6e424f8ff91c80416e3c617645"},
+ {file = "lxml-5.3.1-cp312-cp312-win32.whl", hash = "sha256:f4eac0584cdc3285ef2e74eee1513a6001681fd9753b259e8159421ed28a72e5"},
+ {file = "lxml-5.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:29bfc8d3d88e56ea0a27e7c4897b642706840247f59f4377d81be8f32aa0cfbf"},
+ {file = "lxml-5.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c093c7088b40d8266f57ed71d93112bd64c6724d31f0794c1e52cc4857c28e0e"},
+ {file = "lxml-5.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b0884e3f22d87c30694e625b1e62e6f30d39782c806287450d9dc2fdf07692fd"},
+ {file = "lxml-5.3.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1637fa31ec682cd5760092adfabe86d9b718a75d43e65e211d5931809bc111e7"},
+ {file = "lxml-5.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a364e8e944d92dcbf33b6b494d4e0fb3499dcc3bd9485beb701aa4b4201fa414"},
+ {file = "lxml-5.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:779e851fd0e19795ccc8a9bb4d705d6baa0ef475329fe44a13cf1e962f18ff1e"},
+ {file = "lxml-5.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c4393600915c308e546dc7003d74371744234e8444a28622d76fe19b98fa59d1"},
+ {file = "lxml-5.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:673b9d8e780f455091200bba8534d5f4f465944cbdd61f31dc832d70e29064a5"},
+ {file = "lxml-5.3.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:2e4a570f6a99e96c457f7bec5ad459c9c420ee80b99eb04cbfcfe3fc18ec6423"},
+ {file = "lxml-5.3.1-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:71f31eda4e370f46af42fc9f264fafa1b09f46ba07bdbee98f25689a04b81c20"},
+ {file = "lxml-5.3.1-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:42978a68d3825eaac55399eb37a4d52012a205c0c6262199b8b44fcc6fd686e8"},
+ {file = "lxml-5.3.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8b1942b3e4ed9ed551ed3083a2e6e0772de1e5e3aca872d955e2e86385fb7ff9"},
+ {file = "lxml-5.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85c4f11be9cf08917ac2a5a8b6e1ef63b2f8e3799cec194417e76826e5f1de9c"},
+ {file = "lxml-5.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:231cf4d140b22a923b1d0a0a4e0b4f972e5893efcdec188934cc65888fd0227b"},
+ {file = "lxml-5.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5865b270b420eda7b68928d70bb517ccbe045e53b1a428129bb44372bf3d7dd5"},
+ {file = "lxml-5.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7bebc2275016cddf3c997bf8a0f7044160714c64a9b83975670a04e6d2252"},
+ {file = "lxml-5.3.1-cp313-cp313-win32.whl", hash = "sha256:d0751528b97d2b19a388b302be2a0ee05817097bab46ff0ed76feeec24951f78"},
+ {file = "lxml-5.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:91fb6a43d72b4f8863d21f347a9163eecbf36e76e2f51068d59cd004c506f332"},
+ {file = "lxml-5.3.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:016b96c58e9a4528219bb563acf1aaaa8bc5452e7651004894a973f03b84ba81"},
+ {file = "lxml-5.3.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82a4bb10b0beef1434fb23a09f001ab5ca87895596b4581fd53f1e5145a8934a"},
+ {file = "lxml-5.3.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d68eeef7b4d08a25e51897dac29bcb62aba830e9ac6c4e3297ee7c6a0cf6439"},
+ {file = "lxml-5.3.1-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:f12582b8d3b4c6be1d298c49cb7ae64a3a73efaf4c2ab4e37db182e3545815ac"},
+ {file = "lxml-5.3.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2df7ed5edeb6bd5590914cd61df76eb6cce9d590ed04ec7c183cf5509f73530d"},
+ {file = "lxml-5.3.1-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:585c4dc429deebc4307187d2b71ebe914843185ae16a4d582ee030e6cfbb4d8a"},
+ {file = "lxml-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:06a20d607a86fccab2fc15a77aa445f2bdef7b49ec0520a842c5c5afd8381576"},
+ {file = "lxml-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:057e30d0012439bc54ca427a83d458752ccda725c1c161cc283db07bcad43cf9"},
+ {file = "lxml-5.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4867361c049761a56bd21de507cab2c2a608c55102311d142ade7dab67b34f32"},
+ {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3dddf0fb832486cc1ea71d189cb92eb887826e8deebe128884e15020bb6e3f61"},
+ {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bcc211542f7af6f2dfb705f5f8b74e865592778e6cafdfd19c792c244ccce19"},
+ {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaca5a812f050ab55426c32177091130b1e49329b3f002a32934cd0245571307"},
+ {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:236610b77589faf462337b3305a1be91756c8abc5a45ff7ca8f245a71c5dab70"},
+ {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:aed57b541b589fa05ac248f4cb1c46cbb432ab82cbd467d1c4f6a2bdc18aecf9"},
+ {file = "lxml-5.3.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:75fa3d6946d317ffc7016a6fcc44f42db6d514b7fdb8b4b28cbe058303cb6e53"},
+ {file = "lxml-5.3.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:96eef5b9f336f623ffc555ab47a775495e7e8846dde88de5f941e2906453a1ce"},
+ {file = "lxml-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:ef45f31aec9be01379fc6c10f1d9c677f032f2bac9383c827d44f620e8a88407"},
+ {file = "lxml-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0611da6b07dd3720f492db1b463a4d1175b096b49438761cc9f35f0d9eaaef5"},
+ {file = "lxml-5.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b2aca14c235c7a08558fe0a4786a1a05873a01e86b474dfa8f6df49101853a4e"},
+ {file = "lxml-5.3.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae82fce1d964f065c32c9517309f0c7be588772352d2f40b1574a214bd6e6098"},
+ {file = "lxml-5.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7aae7a3d63b935babfdc6864b31196afd5145878ddd22f5200729006366bc4d5"},
+ {file = "lxml-5.3.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8e0d177b1fe251c3b1b914ab64135475c5273c8cfd2857964b2e3bb0fe196a7"},
+ {file = "lxml-5.3.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:6c4dd3bfd0c82400060896717dd261137398edb7e524527438c54a8c34f736bf"},
+ {file = "lxml-5.3.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:f1208c1c67ec9e151d78aa3435aa9b08a488b53d9cfac9b699f15255a3461ef2"},
+ {file = "lxml-5.3.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:c6aacf00d05b38a5069826e50ae72751cb5bc27bdc4d5746203988e429b385bb"},
+ {file = "lxml-5.3.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5881aaa4bf3a2d086c5f20371d3a5856199a0d8ac72dd8d0dbd7a2ecfc26ab73"},
+ {file = "lxml-5.3.1-cp38-cp38-win32.whl", hash = "sha256:45fbb70ccbc8683f2fb58bea89498a7274af1d9ec7995e9f4af5604e028233fc"},
+ {file = "lxml-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:7512b4d0fc5339d5abbb14d1843f70499cab90d0b864f790e73f780f041615d7"},
+ {file = "lxml-5.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5885bc586f1edb48e5d68e7a4b4757b5feb2a496b64f462b4d65950f5af3364f"},
+ {file = "lxml-5.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1b92fe86e04f680b848fff594a908edfa72b31bfc3499ef7433790c11d4c8cd8"},
+ {file = "lxml-5.3.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a091026c3bf7519ab1e64655a3f52a59ad4a4e019a6f830c24d6430695b1cf6a"},
+ {file = "lxml-5.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ffb141361108e864ab5f1813f66e4e1164181227f9b1f105b042729b6c15125"},
+ {file = "lxml-5.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3715cdf0dd31b836433af9ee9197af10e3df41d273c19bb249230043667a5dfd"},
+ {file = "lxml-5.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88b72eb7222d918c967202024812c2bfb4048deeb69ca328363fb8e15254c549"},
+ {file = "lxml-5.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa59974880ab5ad8ef3afaa26f9bda148c5f39e06b11a8ada4660ecc9fb2feb3"},
+ {file = "lxml-5.3.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:3bb8149840daf2c3f97cebf00e4ed4a65a0baff888bf2605a8d0135ff5cf764e"},
+ {file = "lxml-5.3.1-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:0d6b2fa86becfa81f0a0271ccb9eb127ad45fb597733a77b92e8a35e53414914"},
+ {file = "lxml-5.3.1-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:136bf638d92848a939fd8f0e06fcf92d9f2e4b57969d94faae27c55f3d85c05b"},
+ {file = "lxml-5.3.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:89934f9f791566e54c1d92cdc8f8fd0009447a5ecdb1ec6b810d5f8c4955f6be"},
+ {file = "lxml-5.3.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a8ade0363f776f87f982572c2860cc43c65ace208db49c76df0a21dde4ddd16e"},
+ {file = "lxml-5.3.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:bfbbab9316330cf81656fed435311386610f78b6c93cc5db4bebbce8dd146675"},
+ {file = "lxml-5.3.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:172d65f7c72a35a6879217bcdb4bb11bc88d55fb4879e7569f55616062d387c2"},
+ {file = "lxml-5.3.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e3c623923967f3e5961d272718655946e5322b8d058e094764180cdee7bab1af"},
+ {file = "lxml-5.3.1-cp39-cp39-win32.whl", hash = "sha256:ce0930a963ff593e8bb6fda49a503911accc67dee7e5445eec972668e672a0f0"},
+ {file = "lxml-5.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:f7b64fcd670bca8800bc10ced36620c6bbb321e7bc1214b9c0c0df269c1dddc2"},
+ {file = "lxml-5.3.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:afa578b6524ff85fb365f454cf61683771d0170470c48ad9d170c48075f86725"},
+ {file = "lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f5e80adf0aafc7b5454f2c1cb0cde920c9b1f2cbd0485f07cc1d0497c35c5d"},
+ {file = "lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dd0b80ac2d8f13ffc906123a6f20b459cb50a99222d0da492360512f3e50f84"},
+ {file = "lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:422c179022ecdedbe58b0e242607198580804253da220e9454ffe848daa1cfd2"},
+ {file = "lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:524ccfded8989a6595dbdda80d779fb977dbc9a7bc458864fc9a0c2fc15dc877"},
+ {file = "lxml-5.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:48fd46bf7155def2e15287c6f2b133a2f78e2d22cdf55647269977b873c65499"},
+ {file = "lxml-5.3.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:05123fad495a429f123307ac6d8fd6f977b71e9a0b6d9aeeb8f80c017cb17131"},
+ {file = "lxml-5.3.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a243132767150a44e6a93cd1dde41010036e1cbc63cc3e9fe1712b277d926ce3"},
+ {file = "lxml-5.3.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c92ea6d9dd84a750b2bae72ff5e8cf5fdd13e58dda79c33e057862c29a8d5b50"},
+ {file = "lxml-5.3.1-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2f1be45d4c15f237209bbf123a0e05b5d630c8717c42f59f31ea9eae2ad89394"},
+ {file = "lxml-5.3.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:a83d3adea1e0ee36dac34627f78ddd7f093bb9cfc0a8e97f1572a949b695cb98"},
+ {file = "lxml-5.3.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:3edbb9c9130bac05d8c3fe150c51c337a471cc7fdb6d2a0a7d3a88e88a829314"},
+ {file = "lxml-5.3.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2f23cf50eccb3255b6e913188291af0150d89dab44137a69e14e4dcb7be981f1"},
+ {file = "lxml-5.3.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df7e5edac4778127f2bf452e0721a58a1cfa4d1d9eac63bdd650535eb8543615"},
+ {file = "lxml-5.3.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:094b28ed8a8a072b9e9e2113a81fda668d2053f2ca9f2d202c2c8c7c2d6516b1"},
+ {file = "lxml-5.3.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:514fe78fc4b87e7a7601c92492210b20a1b0c6ab20e71e81307d9c2e377c64de"},
+ {file = "lxml-5.3.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8fffc08de02071c37865a155e5ea5fce0282e1546fd5bde7f6149fcaa32558ac"},
+ {file = "lxml-5.3.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4b0d5cdba1b655d5b18042ac9c9ff50bda33568eb80feaaca4fc237b9c4fbfde"},
+ {file = "lxml-5.3.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3031e4c16b59424e8d78522c69b062d301d951dc55ad8685736c3335a97fc270"},
+ {file = "lxml-5.3.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb659702a45136c743bc130760c6f137870d4df3a9e14386478b8a0511abcfca"},
+ {file = "lxml-5.3.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a11b16a33656ffc43c92a5343a28dc71eefe460bcc2a4923a96f292692709f6"},
+ {file = "lxml-5.3.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c5ae125276f254b01daa73e2c103363d3e99e3e10505686ac7d9d2442dd4627a"},
+ {file = "lxml-5.3.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c76722b5ed4a31ba103e0dc77ab869222ec36efe1a614e42e9bcea88a36186fe"},
+ {file = "lxml-5.3.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:33e06717c00c788ab4e79bc4726ecc50c54b9bfb55355eae21473c145d83c2d2"},
+ {file = "lxml-5.3.1.tar.gz", hash = "sha256:106b7b5d2977b339f1e97efe2778e2ab20e99994cbb0ec5e55771ed0795920c8"},
+]
+
+[package.extras]
+cssselect = ["cssselect (>=0.7)"]
+html-clean = ["lxml_html_clean"]
+html5 = ["html5lib"]
+htmlsoup = ["BeautifulSoup4"]
+source = ["Cython (>=3.0.11,<3.1.0)"]
+
+[[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 = "minify-html"
+version = "0.15.0"
+description = "Extremely fast and smart HTML + JS + CSS minifier"
+optional = false
+python-versions = "*"
+groups = ["main"]
+files = [
+ {file = "minify_html-0.15.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:afd76ca2dc9afa53b66973a3a66eff9a64692811ead44102aa8044a37872e6e2"},
+ {file = "minify_html-0.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f37ce536305500914fd4ee2bbaa4dd05a039f39eeceae45560c39767d99aede0"},
+ {file = "minify_html-0.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e6d4f97cebb725bc1075f225bdfcd824e0f5c20a37d9ea798d900f96e1b80c0"},
+ {file = "minify_html-0.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e47197849a1c09a95892d32df3c9e15f6d0902c9ae215e73249b9f5bca9aeb97"},
+ {file = "minify_html-0.15.0-cp310-none-win_amd64.whl", hash = "sha256:7af72438d3ae6ea8b0a94c038d35c9c22c5f8540967f5fa2487f77b2cdb12605"},
+ {file = "minify_html-0.15.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a23a8055e65fa01175ddd7d18d101c05e267410fa5956c65597dcc332c7f91dd"},
+ {file = "minify_html-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:597c86f9792437eee0698118fb38dff42b5b4be6d437b6d577453c2f91524ccc"},
+ {file = "minify_html-0.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b2aadba6987e6c15a916a4627b94b1db3cbac65e6ae3613b61b3ab0d2bb4c96"},
+ {file = "minify_html-0.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4c4ae3909e2896c865ebaa3a96939191f904dd337a87d7594130f3dfca55510"},
+ {file = "minify_html-0.15.0-cp311-none-win_amd64.whl", hash = "sha256:dc2df1e5203d89197f530d14c9a82067f3d04b9cb0118abc8f2ef8f88efce109"},
+ {file = "minify_html-0.15.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:2a9aef71b24c3d38c6bece2db3bf707443894958b01f1c27d3a6459ba4200e59"},
+ {file = "minify_html-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:70251bd7174b62c91333110301b27000b547aa2cc06d4fe6ba6c3f11612eecc9"},
+ {file = "minify_html-0.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1056819ea46e9080db6fed678d03511c7e94c2a615e72df82190ea898dc82609"},
+ {file = "minify_html-0.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea315ad6ac33d7463fac3f313bba8c8d9a55f4811971c203eed931203047e5c8"},
+ {file = "minify_html-0.15.0-cp312-none-win_amd64.whl", hash = "sha256:01ea40dc5ae073c47024f02758d5e18e55d853265eb9c099040a6c00ab0abb99"},
+ {file = "minify_html-0.15.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:3b38ea5b446cc69e691a0bf64d1160332ffc220bb5b411775983c87311cab2c7"},
+ {file = "minify_html-0.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b6356541799951c5e8205aabf5970dda687f4ffa736479ce8df031919861e51d"},
+ {file = "minify_html-0.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40f38ddfefbb63beb28df20c2c81c12e6af6838387520506b4eceec807d794a3"},
+ {file = "minify_html-0.15.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f707b233b9c163a546b15ce9af433ddd456bd113f0326e5ffb382b8ee5c1a2d"},
+ {file = "minify_html-0.15.0-cp38-none-win_amd64.whl", hash = "sha256:bd682207673246c78fb895e7065425cc94cb712d94cff816dd9752ce014f23e8"},
+ {file = "minify_html-0.15.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:7a5eb7e830277762da69498ee0f15d4a9fa6e91887a93567d388e4f5aee01ec3"},
+ {file = "minify_html-0.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:92375f0cb3b4074e45005e1b4708b5b4c0781b335659d52918671c083c19c71e"},
+ {file = "minify_html-0.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cda674cc68ec3b9ebf61f2986f3ef62de60ce837a58860c6f16b011862b5d533"},
+ {file = "minify_html-0.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b071ded7aacbb140a7e751d49e246052f204b896d69663a4a5c3a27203d27f6"},
+ {file = "minify_html-0.15.0-cp39-none-win_amd64.whl", hash = "sha256:ef6dc1950e04b7566c1ece72712674416f86fef8966ca026f6c5580d840cd354"},
+ {file = "minify_html-0.15.0.tar.gz", hash = "sha256:cf4c36b6f9af3b0901bd2a0a29db3b09c0cdf0c38d3dde28e6835bce0f605d37"},
+]
+
+[[package]]
+name = "multidict"
+version = "6.1.0"
+description = "multidict implementation"
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"},
+ {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"},
+ {file = "multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53"},
+ {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5"},
+ {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581"},
+ {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56"},
+ {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429"},
+ {file = "multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748"},
+ {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db"},
+ {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056"},
+ {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76"},
+ {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160"},
+ {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7"},
+ {file = "multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0"},
+ {file = "multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d"},
+ {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6"},
+ {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156"},
+ {file = "multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb"},
+ {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b"},
+ {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72"},
+ {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304"},
+ {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351"},
+ {file = "multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb"},
+ {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3"},
+ {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399"},
+ {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423"},
+ {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3"},
+ {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753"},
+ {file = "multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80"},
+ {file = "multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926"},
+ {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa"},
+ {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436"},
+ {file = "multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761"},
+ {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e"},
+ {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef"},
+ {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95"},
+ {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925"},
+ {file = "multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966"},
+ {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305"},
+ {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2"},
+ {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2"},
+ {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6"},
+ {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3"},
+ {file = "multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133"},
+ {file = "multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1"},
+ {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008"},
+ {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f"},
+ {file = "multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28"},
+ {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b"},
+ {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c"},
+ {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3"},
+ {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44"},
+ {file = "multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2"},
+ {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3"},
+ {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa"},
+ {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa"},
+ {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4"},
+ {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6"},
+ {file = "multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81"},
+ {file = "multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774"},
+ {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392"},
+ {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a"},
+ {file = "multidict-6.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2"},
+ {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc"},
+ {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478"},
+ {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4"},
+ {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d"},
+ {file = "multidict-6.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6"},
+ {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2"},
+ {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd"},
+ {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6"},
+ {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492"},
+ {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd"},
+ {file = "multidict-6.1.0-cp38-cp38-win32.whl", hash = "sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167"},
+ {file = "multidict-6.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef"},
+ {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c"},
+ {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1"},
+ {file = "multidict-6.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c"},
+ {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c"},
+ {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f"},
+ {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875"},
+ {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255"},
+ {file = "multidict-6.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30"},
+ {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057"},
+ {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657"},
+ {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28"},
+ {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972"},
+ {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43"},
+ {file = "multidict-6.1.0-cp39-cp39-win32.whl", hash = "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada"},
+ {file = "multidict-6.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a"},
+ {file = "multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506"},
+ {file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"},
+]
+
+[package.dependencies]
+typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""}
+
+[[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.2.2"
+description = "Fundamental package for array computing in Python"
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+files = [
+ {file = "numpy-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7079129b64cb78bdc8d611d1fd7e8002c0a2565da6a47c4df8062349fee90e3e"},
+ {file = "numpy-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ec6c689c61df613b783aeb21f945c4cbe6c51c28cb70aae8430577ab39f163e"},
+ {file = "numpy-2.2.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:40c7ff5da22cd391944a28c6a9c638a5eef77fcf71d6e3a79e1d9d9e82752715"},
+ {file = "numpy-2.2.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:995f9e8181723852ca458e22de5d9b7d3ba4da3f11cc1cb113f093b271d7965a"},
+ {file = "numpy-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b78ea78450fd96a498f50ee096f69c75379af5138f7881a51355ab0e11286c97"},
+ {file = "numpy-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fbe72d347fbc59f94124125e73fc4976a06927ebc503ec5afbfb35f193cd957"},
+ {file = "numpy-2.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8e6da5cffbbe571f93588f562ed130ea63ee206d12851b60819512dd3e1ba50d"},
+ {file = "numpy-2.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:09d6a2032faf25e8d0cadde7fd6145118ac55d2740132c1d845f98721b5ebcfd"},
+ {file = "numpy-2.2.2-cp310-cp310-win32.whl", hash = "sha256:159ff6ee4c4a36a23fe01b7c3d07bd8c14cc433d9720f977fcd52c13c0098160"},
+ {file = "numpy-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:64bd6e1762cd7f0986a740fee4dff927b9ec2c5e4d9a28d056eb17d332158014"},
+ {file = "numpy-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:642199e98af1bd2b6aeb8ecf726972d238c9877b0f6e8221ee5ab945ec8a2189"},
+ {file = "numpy-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6d9fc9d812c81e6168b6d405bf00b8d6739a7f72ef22a9214c4241e0dc70b323"},
+ {file = "numpy-2.2.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:c7d1fd447e33ee20c1f33f2c8e6634211124a9aabde3c617687d8b739aa69eac"},
+ {file = "numpy-2.2.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:451e854cfae0febe723077bd0cf0a4302a5d84ff25f0bfece8f29206c7bed02e"},
+ {file = "numpy-2.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd249bc894af67cbd8bad2c22e7cbcd46cf87ddfca1f1289d1e7e54868cc785c"},
+ {file = "numpy-2.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02935e2c3c0c6cbe9c7955a8efa8908dd4221d7755644c59d1bba28b94fd334f"},
+ {file = "numpy-2.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a972cec723e0563aa0823ee2ab1df0cb196ed0778f173b381c871a03719d4826"},
+ {file = "numpy-2.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d6d6a0910c3b4368d89dde073e630882cdb266755565155bc33520283b2d9df8"},
+ {file = "numpy-2.2.2-cp311-cp311-win32.whl", hash = "sha256:860fd59990c37c3ef913c3ae390b3929d005243acca1a86facb0773e2d8d9e50"},
+ {file = "numpy-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:da1eeb460ecce8d5b8608826595c777728cdf28ce7b5a5a8c8ac8d949beadcf2"},
+ {file = "numpy-2.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ac9bea18d6d58a995fac1b2cb4488e17eceeac413af014b1dd26170b766d8467"},
+ {file = "numpy-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23ae9f0c2d889b7b2d88a3791f6c09e2ef827c2446f1c4a3e3e76328ee4afd9a"},
+ {file = "numpy-2.2.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3074634ea4d6df66be04f6728ee1d173cfded75d002c75fac79503a880bf3825"},
+ {file = "numpy-2.2.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ec0636d3f7d68520afc6ac2dc4b8341ddb725039de042faf0e311599f54eb37"},
+ {file = "numpy-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ffbb1acd69fdf8e89dd60ef6182ca90a743620957afb7066385a7bbe88dc748"},
+ {file = "numpy-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0349b025e15ea9d05c3d63f9657707a4e1d471128a3b1d876c095f328f8ff7f0"},
+ {file = "numpy-2.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:463247edcee4a5537841d5350bc87fe8e92d7dd0e8c71c995d2c6eecb8208278"},
+ {file = "numpy-2.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9dd47ff0cb2a656ad69c38da850df3454da88ee9a6fde0ba79acceee0e79daba"},
+ {file = "numpy-2.2.2-cp312-cp312-win32.whl", hash = "sha256:4525b88c11906d5ab1b0ec1f290996c0020dd318af8b49acaa46f198b1ffc283"},
+ {file = "numpy-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:5acea83b801e98541619af398cc0109ff48016955cc0818f478ee9ef1c5c3dcb"},
+ {file = "numpy-2.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b208cfd4f5fe34e1535c08983a1a6803fdbc7a1e86cf13dd0c61de0b51a0aadc"},
+ {file = "numpy-2.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d0bbe7dd86dca64854f4b6ce2ea5c60b51e36dfd597300057cf473d3615f2369"},
+ {file = "numpy-2.2.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:22ea3bb552ade325530e72a0c557cdf2dea8914d3a5e1fecf58fa5dbcc6f43cd"},
+ {file = "numpy-2.2.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:128c41c085cab8a85dc29e66ed88c05613dccf6bc28b3866cd16050a2f5448be"},
+ {file = "numpy-2.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:250c16b277e3b809ac20d1f590716597481061b514223c7badb7a0f9993c7f84"},
+ {file = "numpy-2.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0c8854b09bc4de7b041148d8550d3bd712b5c21ff6a8ed308085f190235d7ff"},
+ {file = "numpy-2.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b6fb9c32a91ec32a689ec6410def76443e3c750e7cfc3fb2206b985ffb2b85f0"},
+ {file = "numpy-2.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:57b4012e04cc12b78590a334907e01b3a85efb2107df2b8733ff1ed05fce71de"},
+ {file = "numpy-2.2.2-cp313-cp313-win32.whl", hash = "sha256:4dbd80e453bd34bd003b16bd802fac70ad76bd463f81f0c518d1245b1c55e3d9"},
+ {file = "numpy-2.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:5a8c863ceacae696aff37d1fd636121f1a512117652e5dfb86031c8d84836369"},
+ {file = "numpy-2.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b3482cb7b3325faa5f6bc179649406058253d91ceda359c104dac0ad320e1391"},
+ {file = "numpy-2.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9491100aba630910489c1d0158034e1c9a6546f0b1340f716d522dc103788e39"},
+ {file = "numpy-2.2.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:41184c416143defa34cc8eb9d070b0a5ba4f13a0fa96a709e20584638254b317"},
+ {file = "numpy-2.2.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7dca87ca328f5ea7dafc907c5ec100d187911f94825f8700caac0b3f4c384b49"},
+ {file = "numpy-2.2.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bc61b307655d1a7f9f4b043628b9f2b721e80839914ede634e3d485913e1fb2"},
+ {file = "numpy-2.2.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fad446ad0bc886855ddf5909cbf8cb5d0faa637aaa6277fb4b19ade134ab3c7"},
+ {file = "numpy-2.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:149d1113ac15005652e8d0d3f6fd599360e1a708a4f98e43c9c77834a28238cb"},
+ {file = "numpy-2.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:106397dbbb1896f99e044efc90360d098b3335060375c26aa89c0d8a97c5f648"},
+ {file = "numpy-2.2.2-cp313-cp313t-win32.whl", hash = "sha256:0eec19f8af947a61e968d5429f0bd92fec46d92b0008d0a6685b40d6adf8a4f4"},
+ {file = "numpy-2.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:97b974d3ba0fb4612b77ed35d7627490e8e3dff56ab41454d9e8b23448940576"},
+ {file = "numpy-2.2.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b0531f0b0e07643eb089df4c509d30d72c9ef40defa53e41363eca8a8cc61495"},
+ {file = "numpy-2.2.2-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:e9e82dcb3f2ebbc8cb5ce1102d5f1c5ed236bf8a11730fb45ba82e2841ec21df"},
+ {file = "numpy-2.2.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0d4142eb40ca6f94539e4db929410f2a46052a0fe7a2c1c59f6179c39938d2a"},
+ {file = "numpy-2.2.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:356ca982c188acbfa6af0d694284d8cf20e95b1c3d0aefa8929376fea9146f60"},
+ {file = "numpy-2.2.2.tar.gz", hash = "sha256:ed6906f61834d687738d25988ae117683705636936cc605be0bb208b23df4d8f"},
+]
+
+[[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 = "propcache"
+version = "0.2.1"
+description = "Accelerated property cache"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6b3f39a85d671436ee3d12c017f8fdea38509e4f25b28eb25877293c98c243f6"},
+ {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d51fbe4285d5db5d92a929e3e21536ea3dd43732c5b177c7ef03f918dff9f2"},
+ {file = "propcache-0.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6445804cf4ec763dc70de65a3b0d9954e868609e83850a47ca4f0cb64bd79fea"},
+ {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9479aa06a793c5aeba49ce5c5692ffb51fcd9a7016e017d555d5e2b0045d212"},
+ {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9631c5e8b5b3a0fda99cb0d29c18133bca1e18aea9effe55adb3da1adef80d3"},
+ {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3156628250f46a0895f1f36e1d4fbe062a1af8718ec3ebeb746f1d23f0c5dc4d"},
+ {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b6fb63ae352e13748289f04f37868099e69dba4c2b3e271c46061e82c745634"},
+ {file = "propcache-0.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:887d9b0a65404929641a9fabb6452b07fe4572b269d901d622d8a34a4e9043b2"},
+ {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a96dc1fa45bd8c407a0af03b2d5218392729e1822b0c32e62c5bf7eeb5fb3958"},
+ {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a7e65eb5c003a303b94aa2c3852ef130230ec79e349632d030e9571b87c4698c"},
+ {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:999779addc413181912e984b942fbcc951be1f5b3663cd80b2687758f434c583"},
+ {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:19a0f89a7bb9d8048d9c4370c9c543c396e894c76be5525f5e1ad287f1750ddf"},
+ {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1ac2f5fe02fa75f56e1ad473f1175e11f475606ec9bd0be2e78e4734ad575034"},
+ {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:574faa3b79e8ebac7cb1d7930f51184ba1ccf69adfdec53a12f319a06030a68b"},
+ {file = "propcache-0.2.1-cp310-cp310-win32.whl", hash = "sha256:03ff9d3f665769b2a85e6157ac8b439644f2d7fd17615a82fa55739bc97863f4"},
+ {file = "propcache-0.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2d3af2e79991102678f53e0dbf4c35de99b6b8b58f29a27ca0325816364caaba"},
+ {file = "propcache-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ffc3cca89bb438fb9c95c13fc874012f7b9466b89328c3c8b1aa93cdcfadd16"},
+ {file = "propcache-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f174bbd484294ed9fdf09437f889f95807e5f229d5d93588d34e92106fbf6717"},
+ {file = "propcache-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:70693319e0b8fd35dd863e3e29513875eb15c51945bf32519ef52927ca883bc3"},
+ {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b480c6a4e1138e1aa137c0079b9b6305ec6dcc1098a8ca5196283e8a49df95a9"},
+ {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d27b84d5880f6d8aa9ae3edb253c59d9f6642ffbb2c889b78b60361eed449787"},
+ {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:857112b22acd417c40fa4595db2fe28ab900c8c5fe4670c7989b1c0230955465"},
+ {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf6c4150f8c0e32d241436526f3c3f9cbd34429492abddbada2ffcff506c51af"},
+ {file = "propcache-0.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66d4cfda1d8ed687daa4bc0274fcfd5267873db9a5bc0418c2da19273040eeb7"},
+ {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2f992c07c0fca81655066705beae35fc95a2fa7366467366db627d9f2ee097f"},
+ {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4a571d97dbe66ef38e472703067021b1467025ec85707d57e78711c085984e54"},
+ {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bb6178c241278d5fe853b3de743087be7f5f4c6f7d6d22a3b524d323eecec505"},
+ {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ad1af54a62ffe39cf34db1aa6ed1a1873bd548f6401db39d8e7cd060b9211f82"},
+ {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e7048abd75fe40712005bcfc06bb44b9dfcd8e101dda2ecf2f5aa46115ad07ca"},
+ {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:160291c60081f23ee43d44b08a7e5fb76681221a8e10b3139618c5a9a291b84e"},
+ {file = "propcache-0.2.1-cp311-cp311-win32.whl", hash = "sha256:819ce3b883b7576ca28da3861c7e1a88afd08cc8c96908e08a3f4dd64a228034"},
+ {file = "propcache-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:edc9fc7051e3350643ad929df55c451899bb9ae6d24998a949d2e4c87fb596d3"},
+ {file = "propcache-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:081a430aa8d5e8876c6909b67bd2d937bfd531b0382d3fdedb82612c618bc41a"},
+ {file = "propcache-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2ccec9ac47cf4e04897619c0e0c1a48c54a71bdf045117d3a26f80d38ab1fb0"},
+ {file = "propcache-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14d86fe14b7e04fa306e0c43cdbeebe6b2c2156a0c9ce56b815faacc193e320d"},
+ {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:049324ee97bb67285b49632132db351b41e77833678432be52bdd0289c0e05e4"},
+ {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cd9a1d071158de1cc1c71a26014dcdfa7dd3d5f4f88c298c7f90ad6f27bb46d"},
+ {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98110aa363f1bb4c073e8dcfaefd3a5cea0f0834c2aab23dda657e4dab2f53b5"},
+ {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:647894f5ae99c4cf6bb82a1bb3a796f6e06af3caa3d32e26d2350d0e3e3faf24"},
+ {file = "propcache-0.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfd3223c15bebe26518d58ccf9a39b93948d3dcb3e57a20480dfdd315356baff"},
+ {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d71264a80f3fcf512eb4f18f59423fe82d6e346ee97b90625f283df56aee103f"},
+ {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e73091191e4280403bde6c9a52a6999d69cdfde498f1fdf629105247599b57ec"},
+ {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3935bfa5fede35fb202c4b569bb9c042f337ca4ff7bd540a0aa5e37131659348"},
+ {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f508b0491767bb1f2b87fdfacaba5f7eddc2f867740ec69ece6d1946d29029a6"},
+ {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1672137af7c46662a1c2be1e8dc78cb6d224319aaa40271c9257d886be4363a6"},
+ {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b74c261802d3d2b85c9df2dfb2fa81b6f90deeef63c2db9f0e029a3cac50b518"},
+ {file = "propcache-0.2.1-cp312-cp312-win32.whl", hash = "sha256:d09c333d36c1409d56a9d29b3a1b800a42c76a57a5a8907eacdbce3f18768246"},
+ {file = "propcache-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:c214999039d4f2a5b2073ac506bba279945233da8c786e490d411dfc30f855c1"},
+ {file = "propcache-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aca405706e0b0a44cc6bfd41fbe89919a6a56999157f6de7e182a990c36e37bc"},
+ {file = "propcache-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:12d1083f001ace206fe34b6bdc2cb94be66d57a850866f0b908972f90996b3e9"},
+ {file = "propcache-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d93f3307ad32a27bda2e88ec81134b823c240aa3abb55821a8da553eed8d9439"},
+ {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba278acf14471d36316159c94a802933d10b6a1e117b8554fe0d0d9b75c9d536"},
+ {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e6281aedfca15301c41f74d7005e6e3f4ca143584ba696ac69df4f02f40d629"},
+ {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b750a8e5a1262434fb1517ddf64b5de58327f1adc3524a5e44c2ca43305eb0b"},
+ {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf72af5e0fb40e9babf594308911436c8efde3cb5e75b6f206c34ad18be5c052"},
+ {file = "propcache-0.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2d0a12018b04f4cb820781ec0dffb5f7c7c1d2a5cd22bff7fb055a2cb19ebce"},
+ {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e800776a79a5aabdb17dcc2346a7d66d0777e942e4cd251defeb084762ecd17d"},
+ {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4160d9283bd382fa6c0c2b5e017acc95bc183570cd70968b9202ad6d8fc48dce"},
+ {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b43e74f1359353341a7adb783c8f1b1c676367b011709f466f42fda2045e95"},
+ {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:58791550b27d5488b1bb52bc96328456095d96206a250d28d874fafe11b3dfaf"},
+ {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0f022d381747f0dfe27e99d928e31bc51a18b65bb9e481ae0af1380a6725dd1f"},
+ {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:297878dc9d0a334358f9b608b56d02e72899f3b8499fc6044133f0d319e2ec30"},
+ {file = "propcache-0.2.1-cp313-cp313-win32.whl", hash = "sha256:ddfab44e4489bd79bda09d84c430677fc7f0a4939a73d2bba3073036f487a0a6"},
+ {file = "propcache-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:556fc6c10989f19a179e4321e5d678db8eb2924131e64652a51fe83e4c3db0e1"},
+ {file = "propcache-0.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6a9a8c34fb7bb609419a211e59da8887eeca40d300b5ea8e56af98f6fbbb1541"},
+ {file = "propcache-0.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ae1aa1cd222c6d205853b3013c69cd04515f9d6ab6de4b0603e2e1c33221303e"},
+ {file = "propcache-0.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:accb6150ce61c9c4b7738d45550806aa2b71c7668c6942f17b0ac182b6142fd4"},
+ {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5eee736daafa7af6d0a2dc15cc75e05c64f37fc37bafef2e00d77c14171c2097"},
+ {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7a31fc1e1bd362874863fdeed71aed92d348f5336fd84f2197ba40c59f061bd"},
+ {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba4cfa1052819d16699e1d55d18c92b6e094d4517c41dd231a8b9f87b6fa681"},
+ {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f089118d584e859c62b3da0892b88a83d611c2033ac410e929cb6754eec0ed16"},
+ {file = "propcache-0.2.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:781e65134efaf88feb447e8c97a51772aa75e48b794352f94cb7ea717dedda0d"},
+ {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31f5af773530fd3c658b32b6bdc2d0838543de70eb9a2156c03e410f7b0d3aae"},
+ {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:a7a078f5d37bee6690959c813977da5291b24286e7b962e62a94cec31aa5188b"},
+ {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cea7daf9fc7ae6687cf1e2c049752f19f146fdc37c2cc376e7d0032cf4f25347"},
+ {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:8b3489ff1ed1e8315674d0775dc7d2195fb13ca17b3808721b54dbe9fd020faf"},
+ {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9403db39be1393618dd80c746cb22ccda168efce239c73af13c3763ef56ffc04"},
+ {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5d97151bc92d2b2578ff7ce779cdb9174337390a535953cbb9452fb65164c587"},
+ {file = "propcache-0.2.1-cp39-cp39-win32.whl", hash = "sha256:9caac6b54914bdf41bcc91e7eb9147d331d29235a7c967c150ef5df6464fd1bb"},
+ {file = "propcache-0.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:92fc4500fcb33899b05ba73276dfb684a20d31caa567b7cb5252d48f896a91b1"},
+ {file = "propcache-0.2.1-py3-none-any.whl", hash = "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54"},
+ {file = "propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64"},
+]
+
+[[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 = "23.3.0"
+description = "Python wrapper module around the OpenSSL library"
+optional = false
+python-versions = ">=3.7"
+groups = ["main"]
+files = [
+ {file = "pyOpenSSL-23.3.0-py3-none-any.whl", hash = "sha256:6756834481d9ed5470f4a9393455154bc92fe7a64b7bc6ee2c804e78c52099b2"},
+ {file = "pyOpenSSL-23.3.0.tar.gz", hash = "sha256:6b2cba5cc46e822750ec3e5a81ee12819850b11303630d575e98108a079c2b12"},
+]
+
+[package.dependencies]
+cryptography = ">=41.0.5,<42"
+
+[package.extras]
+docs = ["sphinx (!=5.2.0,!=5.2.0.post0,!=7.2.5)", "sphinx-rtd-theme"]
+test = ["flaky", "pretend", "pytest (>=3.0.1)"]
+
+[[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 = ["main"]
+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 = "requests-toolbelt"
+version = "1.0.0"
+description = "A utility belt for advanced users of python-requests"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+groups = ["main"]
+files = [
+ {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"},
+ {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"},
+]
+
+[package.dependencies]
+requests = ">=2.0.1,<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 = "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 = "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 = "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 = "snscrape"
+version = "0.7.0.20230622"
+description = "A social networking service scraper"
+optional = false
+python-versions = "~=3.8"
+groups = ["main"]
+files = [
+ {file = "snscrape-0.7.0.20230622-py3-none-any.whl", hash = "sha256:6eedb85c7e79f35361dde1949e1e7e2dee44e9f8469668438c9f8e72980f482f"},
+ {file = "snscrape-0.7.0.20230622.tar.gz", hash = "sha256:71da8aec489a3b1139caaab699ca489c708d117828a2d5bcdf1ce2c9e76f3708"},
+]
+
+[package.dependencies]
+beautifulsoup4 = "*"
+filelock = "*"
+lxml = "*"
+requests = {version = "*", extras = ["socks"]}
+
+[[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 = "tiktok-downloader"
+version = "0.3.5"
+description = "Tiktok Downloader&Scraper using bs4&requests"
+optional = false
+python-versions = "*"
+groups = ["main"]
+files = [
+ {file = "tiktok_downloader-0.3.5.tar.gz", hash = "sha256:f376ba0d2517fbab87b3185784d6e19481543326121427ae0986b9fdef6f4f75"},
+]
+
+[package.dependencies]
+aiohttp = "*"
+bs4 = "*"
+cloudscraper = "*"
+flask = "*"
+httpx = "*"
+requests = "*"
+rich = "*"
+tqdm = "*"
+
+[[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.28.0"
+description = "A friendly Python library for async concurrency and I/O"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "trio-0.28.0-py3-none-any.whl", hash = "sha256:56d58977acc1635735a96581ec70513cc781b8b6decd299c487d3be2a721cd94"},
+ {file = "trio-0.28.0.tar.gz", hash = "sha256:4e547896fe9e8a5658e54e4c7c5fa1db748cbbbaa7c965e7d40505b928c73c05"},
+]
+
+[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"},
+]
+markers = {dev = "python_version < \"3.13\""}
+
+[[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.2"
+description = "tzinfo object for the local timezone"
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8"},
+ {file = "tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e"},
+]
+
+[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 = "werkzeug"
+version = "3.1.3"
+description = "The comprehensive WSGI web application library."
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"},
+ {file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"},
+]
+
+[package.dependencies]
+MarkupSafe = ">=2.1.1"
+
+[package.extras]
+watchdog = ["watchdog (>=2.3)"]
+
+[[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 = "yarl"
+version = "1.18.3"
+description = "Yet another URL library"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34"},
+ {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7"},
+ {file = "yarl-1.18.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:602d98f2c2d929f8e697ed274fbadc09902c4025c5a9963bf4e9edfc3ab6f7ed"},
+ {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c654d5207c78e0bd6d749f6dae1dcbbfde3403ad3a4b11f3c5544d9906969dde"},
+ {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5094d9206c64181d0f6e76ebd8fb2f8fe274950a63890ee9e0ebfd58bf9d787b"},
+ {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35098b24e0327fc4ebdc8ffe336cee0a87a700c24ffed13161af80124b7dc8e5"},
+ {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3236da9272872443f81fedc389bace88408f64f89f75d1bdb2256069a8730ccc"},
+ {file = "yarl-1.18.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2c08cc9b16f4f4bc522771d96734c7901e7ebef70c6c5c35dd0f10845270bcd"},
+ {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80316a8bd5109320d38eef8833ccf5f89608c9107d02d2a7f985f98ed6876990"},
+ {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1e1cc06da1491e6734f0ea1e6294ce00792193c463350626571c287c9a704db"},
+ {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fea09ca13323376a2fdfb353a5fa2e59f90cd18d7ca4eaa1fd31f0a8b4f91e62"},
+ {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e3b9fd71836999aad54084906f8663dffcd2a7fb5cdafd6c37713b2e72be1760"},
+ {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:757e81cae69244257d125ff31663249b3013b5dc0a8520d73694aed497fb195b"},
+ {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b1771de9944d875f1b98a745bc547e684b863abf8f8287da8466cf470ef52690"},
+ {file = "yarl-1.18.3-cp310-cp310-win32.whl", hash = "sha256:8874027a53e3aea659a6d62751800cf6e63314c160fd607489ba5c2edd753cf6"},
+ {file = "yarl-1.18.3-cp310-cp310-win_amd64.whl", hash = "sha256:93b2e109287f93db79210f86deb6b9bbb81ac32fc97236b16f7433db7fc437d8"},
+ {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069"},
+ {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193"},
+ {file = "yarl-1.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889"},
+ {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8"},
+ {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca"},
+ {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8"},
+ {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae"},
+ {file = "yarl-1.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3"},
+ {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb"},
+ {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e"},
+ {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59"},
+ {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d"},
+ {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e"},
+ {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a"},
+ {file = "yarl-1.18.3-cp311-cp311-win32.whl", hash = "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1"},
+ {file = "yarl-1.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5"},
+ {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50"},
+ {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576"},
+ {file = "yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640"},
+ {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2"},
+ {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75"},
+ {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512"},
+ {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba"},
+ {file = "yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb"},
+ {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272"},
+ {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6"},
+ {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e"},
+ {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb"},
+ {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393"},
+ {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285"},
+ {file = "yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2"},
+ {file = "yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477"},
+ {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb"},
+ {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa"},
+ {file = "yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782"},
+ {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0"},
+ {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482"},
+ {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186"},
+ {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58"},
+ {file = "yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53"},
+ {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2"},
+ {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8"},
+ {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1"},
+ {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a"},
+ {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10"},
+ {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8"},
+ {file = "yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d"},
+ {file = "yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c"},
+ {file = "yarl-1.18.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:61e5e68cb65ac8f547f6b5ef933f510134a6bf31bb178be428994b0cb46c2a04"},
+ {file = "yarl-1.18.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe57328fbc1bfd0bd0514470ac692630f3901c0ee39052ae47acd1d90a436719"},
+ {file = "yarl-1.18.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a440a2a624683108a1b454705ecd7afc1c3438a08e890a1513d468671d90a04e"},
+ {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09c7907c8548bcd6ab860e5f513e727c53b4a714f459b084f6580b49fa1b9cee"},
+ {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4f6450109834af88cb4cc5ecddfc5380ebb9c228695afc11915a0bf82116789"},
+ {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9ca04806f3be0ac6d558fffc2fdf8fcef767e0489d2684a21912cc4ed0cd1b8"},
+ {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77a6e85b90a7641d2e07184df5557132a337f136250caafc9ccaa4a2a998ca2c"},
+ {file = "yarl-1.18.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6333c5a377c8e2f5fae35e7b8f145c617b02c939d04110c76f29ee3676b5f9a5"},
+ {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0b3c92fa08759dbf12b3a59579a4096ba9af8dd344d9a813fc7f5070d86bbab1"},
+ {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4ac515b860c36becb81bb84b667466885096b5fc85596948548b667da3bf9f24"},
+ {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:045b8482ce9483ada4f3f23b3774f4e1bf4f23a2d5c912ed5170f68efb053318"},
+ {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:a4bb030cf46a434ec0225bddbebd4b89e6471814ca851abb8696170adb163985"},
+ {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:54d6921f07555713b9300bee9c50fb46e57e2e639027089b1d795ecd9f7fa910"},
+ {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1d407181cfa6e70077df3377938c08012d18893f9f20e92f7d2f314a437c30b1"},
+ {file = "yarl-1.18.3-cp39-cp39-win32.whl", hash = "sha256:ac36703a585e0929b032fbaab0707b75dc12703766d0b53486eabd5139ebadd5"},
+ {file = "yarl-1.18.3-cp39-cp39-win_amd64.whl", hash = "sha256:ba87babd629f8af77f557b61e49e7c7cac36f22f871156b91e10a6e9d4f829e9"},
+ {file = "yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b"},
+ {file = "yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1"},
+]
+
+[package.dependencies]
+idna = ">=2.0"
+multidict = ">=4.0"
+propcache = ">=0.2.0"
+
+[[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,<4.0"
+content-hash = "700297d0b02a98913b7294fdf285dba894a245157638377d189b42e8f8ab8c84"
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..709a695
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,51 @@
+[project]
+name = "auto-archiver-api"
+version = "0.9.0"
+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,<4.0"
+
+
+dependencies = [
+ "auto-archiver (>=0.12.0,<0.13.0)",
+ "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"
+
+[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"
+
+
+[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/worker.Dockerfile b/worker.Dockerfile
index 5073228..4c70730 100644
--- a/worker.Dockerfile
+++ b/worker.Dockerfile
@@ -7,14 +7,23 @@ WORKDIR /aa-api
RUN curl -fsSL https://get.docker.com -o get-docker.sh && \
sh get-docker.sh
# set environment variables
-ENV PYTHONUNBUFFERED=1
-ENV PYTHONDONTWRITEBYTECODE=1
+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
-RUN pip install --upgrade pip && \
- apt-get update
-COPY ./Pipfile* ./
-RUN pipenv install
# copy source code and .env files over
COPY alembic.ini ./
@@ -22,4 +31,4 @@ COPY .env* ./app/
COPY ./secrets/ ./secrets/
COPY ./app/ ./app/
-ENTRYPOINT ["pipenv", "run"]
\ No newline at end of file
+ENTRYPOINT ["./poetry-venv/bin/poetry", "run"]
\ No newline at end of file
From 6f3d3427c80ccfafaaa31229a24146d749aec883 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Mon, 10 Feb 2025 23:47:38 +0000
Subject: [PATCH 36/75] pyevn annoyances
---
.python-version | 1 +
1 file changed, 1 insertion(+)
create mode 100644 .python-version
diff --git a/.python-version b/.python-version
new file mode 100644
index 0000000..c8cfe39
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
+3.10
From 37ebba73bff9b867b05bd318a4f207cce66a3530 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Mon, 10 Feb 2025 23:49:08 +0000
Subject: [PATCH 37/75] separate images, no more .env
---
Makefile | 12 ++++++------
app/logs/.gitkeep | 0
app/shared/settings.py | 7 ++++---
docker-compose.dev.yml | 2 +-
docker-compose.yml | 5 ++++-
web.Dockerfile | 23 +++++++++++++++++++++++
6 files changed, 38 insertions(+), 11 deletions(-)
delete mode 100644 app/logs/.gitkeep
create mode 100644 web.Dockerfile
diff --git a/Makefile b/Makefile
index 57f5e0e..2fd462c 100644
--- a/Makefile
+++ b/Makefile
@@ -3,20 +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 -f docker-compose.yml -f docker-compose.dev.yml build redis
- docker compose -f docker-compose.yml -f docker-compose.dev.yml up --remove-orphans redis
+ 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/app/logs/.gitkeep b/app/logs/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/app/shared/settings.py b/app/shared/settings.py
index e3b4f77..e962f1d 100644
--- a/app/shared/settings.py
+++ b/app/shared/settings.py
@@ -1,15 +1,16 @@
from functools import lru_cache
+import os
from fastapi_mail import ConnectionConfig
-from pydantic_settings import BaseSettings
-from pydantic import ConfigDict
+from pydantic_settings import BaseSettings, SettingsConfigDict
from typing import Annotated, Set
from annotated_types import Len
class Settings(BaseSettings):
- model_config = ConfigDict(extra='ignore', str_strip_whitespace=True)
+ 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 = ""
USER_GROUPS_FILENAME: str = "user-groups.yaml"
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index 3f5b264..5a8bc83 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -6,6 +6,7 @@ services:
volumes:
- ./app:/aa-api/app # for --reload to work
environment:
+ - 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
@@ -20,7 +21,6 @@ services:
- ./app:/aa-api/app # for watchmedo
redis:
- command: redis-server /conf/redis.conf --requirepass ${REDIS_PASSWORD}
restart: "no"
env_file: .env.dev
ports:
diff --git a/docker-compose.yml b/docker-compose.yml
index d59e1c0..7c5cbd9 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -7,11 +7,12 @@ services:
web:
build:
context: .
- dockerfile: worker.Dockerfile
+ dockerfile: web.Dockerfile
restart: always
env_file: .env.prod
environment:
CELERY_BROKER_URL: redis://:${REDIS_PASSWORD}@redis:6379/0
+ ENVIRONMENT_FILE: .env.prod
ports:
- "127.0.0.1:8004:8000"
#TODO: should prod have the --reload flag?
@@ -42,6 +43,7 @@ services:
- crawls:/crawls # BROWSERTRIX_HOME_HOST:BROWSERTRIX_HOME_CONTAINER, do not change /crawls
environment:
CELERY_BROKER_URL: redis://:${REDIS_PASSWORD}@redis:6379/0
+ 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
@@ -57,6 +59,7 @@ 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
diff --git a/web.Dockerfile b/web.Dockerfile
new file mode 100644
index 0000000..9c73efc
--- /dev/null
+++ b/web.Dockerfile
@@ -0,0 +1,23 @@
+# 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 .
+RUN poetry install --with web --no-interaction --no-ansi --no-root --no-cache
+
+# Copy the application code
+COPY alembic.ini ./
+COPY .env* ./app/
+COPY ./secrets/ ./secrets/
+COPY ./app/ ./app/
+
+# Run the FastAPI app with Uvicorn
+ENTRYPOINT ["poetry", "run"]
From 1877999a7051ed5e2da4f7838b474c677712f3ee Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Mon, 10 Feb 2025 23:49:18 +0000
Subject: [PATCH 38/75] WIP fixing tests
---
app/tests/conftest.py | 14 +++---
app/tests/db/test_crud.py | 46 ++++++++++----------
app/tests/db/test_models.py | 2 +-
app/tests/endpoints/test_interoperability.py | 2 +-
app/tests/endpoints/test_sheet.py | 14 +++---
app/tests/endpoints/test_url.py | 4 +-
app/tests/worker/test_worker_main.py | 3 +-
7 files changed, 43 insertions(+), 42 deletions(-)
diff --git a/app/tests/conftest.py b/app/tests/conftest.py
index dbf1ec5..7505c07 100644
--- a/app/tests/conftest.py
+++ b/app/tests/conftest.py
@@ -3,8 +3,8 @@ from fastapi.testclient import TestClient
import pytest
from unittest.mock import patch
from app.shared.config import ALLOW_ANY_EMAIL
-from db.user_state import UserState
-from shared.settings import Settings
+from app.shared.db.user_state import UserState
+from app.shared.settings import Settings
@pytest.fixture(autouse=True)
@@ -27,9 +27,9 @@ def mock_settings():
@pytest.fixture()
def test_db(get_settings: Settings):
- from db.database import make_engine
- from db import models
- from db.crud import get_user_groups
+ from app.shared.db import models
+ from app.shared.db.database import make_engine
+ from app.shared.db.crud import get_user_groups
get_user_groups.cache_clear()
make_engine.cache_clear()
@@ -54,7 +54,7 @@ def test_db(get_settings: Settings):
@pytest.fixture()
def db_session(test_db):
- from db.database import make_session_local
+ from app.shared.db.database import make_session_local
session_local = make_session_local(test_db)
with session_local() as session:
yield session
@@ -63,7 +63,7 @@ def db_session(test_db):
@pytest.fixture()
def app(db_session):
from web.main import app_factory
- from db import crud
+ from app.shared.db import crud
app = app_factory()
crud.upsert_user_groups(db_session)
return app
diff --git a/app/tests/db/test_crud.py b/app/tests/db/test_crud.py
index bbc8bdd..a7317b3 100644
--- a/app/tests/db/test_crud.py
+++ b/app/tests/db/test_crud.py
@@ -3,7 +3,7 @@ from unittest.mock import patch
import pytest
import yaml
-from db import models
+from app.shared.db import models
from shared.settings import Settings
authors = ["rick@example.com", "morty@example.com", "jerry@example.com"]
@@ -55,14 +55,14 @@ def test_data(db_session):
# setup groups
assert db_session.query(models.Group).count() == 0
- from db import crud
+ from app.shared.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_get_archive(test_data, db_session):
- from db import crud
+ from app.shared.db import crud
from app.shared.config import ALLOW_ANY_EMAIL
print(db_session.query(models.Group).all())
@@ -93,7 +93,7 @@ def test_get_archive(test_data, db_session):
def test_search_archives_by_url(test_data, db_session):
- from db import crud
+ from app.shared.db import crud
from app.shared.config import ALLOW_ANY_EMAIL
# rick's archives are private
@@ -141,7 +141,7 @@ def test_search_archives_by_url(test_data, db_session):
def test_search_archives_by_email(test_data, db_session):
from app.shared.config import ALLOW_ANY_EMAIL
- from db import crud
+ from app.shared.db import crud
# lower/upper case
assert len(crud.search_archives_by_email(db_session, "rick@example.com")) == 34
@@ -162,7 +162,7 @@ def test_search_archives_by_email(test_data, db_session):
@patch("db.crud.DATABASE_QUERY_LIMIT", new=25)
def test_max_query_limit(test_data, db_session):
- from db import crud
+ from app.shared.db import crud
from app.shared.config import ALLOW_ANY_EMAIL
assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL)) == 25
@@ -173,8 +173,8 @@ def test_max_query_limit(test_data, db_session):
def test_create_task(db_session):
- from db import crud
- from db import schemas
+ from app.shared.db import crud
+ from app.shared import schemas
task = schemas.ArchiveCreate(
id="archive-id-456-101",
@@ -218,7 +218,7 @@ def test_create_task(db_session):
def test_soft_delete(test_data, db_session):
- from db import crud
+ from app.shared.db import crud
# none deleted yet
assert crud.get_archive(db_session, "archive-id-456-0", "rick@example.com") is not None
@@ -236,7 +236,7 @@ def test_soft_delete(test_data, db_session):
def test_count_archives(test_data, db_session):
- from db import crud
+ from app.shared.db import crud
assert crud.count_archives(db_session) == 100
db_session.query(models.Archive).filter(models.Archive.id == "archive-id-456-0").delete()
@@ -245,7 +245,7 @@ def test_count_archives(test_data, db_session):
def test_count_archive_urls(test_data, db_session):
- from db import crud
+ from app.shared.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()
@@ -260,7 +260,7 @@ def test_count_archive_urls(test_data, db_session):
def test_count_users(test_data, db_session):
- from db import crud
+ from app.shared.db import crud
assert crud.count_users(db_session) == 3
db_session.query(models.User).filter(models.User.email == "rick@example.com").delete()
@@ -269,7 +269,7 @@ def test_count_users(test_data, db_session):
def test_count_by_users_since(test_data, db_session):
- from db import crud
+ from app.shared.db import crud
# 100y window
assert len(cu := crud.count_by_user_since(db_session, 60 * 60 * 24 * 31 * 12 * 100)) == 3
@@ -279,7 +279,7 @@ def test_count_by_users_since(test_data, db_session):
def test_create_tag(db_session):
- from db import crud
+ from app.shared.db import crud
assert db_session.query(models.Tag).count() == 0
@@ -303,7 +303,7 @@ def test_create_tag(db_session):
def test_is_user_in_group(test_data, db_session):
- from db import crud
+ from app.shared.db import crud
from app.shared.config import ALLOW_ANY_EMAIL
# see user-groups.test.yaml
@@ -343,7 +343,7 @@ def test_is_user_in_group(test_data, db_session):
def test_get_group(test_data, db_session):
- from db import crud
+ from app.shared.db import crud
assert crud.get_group(db_session, "spaceship") is not None
assert crud.get_group(db_session, "interdimensional") is not None
@@ -352,7 +352,7 @@ def test_get_group(test_data, db_session):
def test_create_or_get_user(test_data, db_session):
- from db import crud
+ from app.shared.db import crud
assert db_session.query(models.User).count() == 3
@@ -368,7 +368,7 @@ def test_create_or_get_user(test_data, db_session):
def test_upsert_group(test_data, db_session):
- from db import crud
+ from app.shared.db import crud
assert db_session.query(models.Group).count() == 4
@@ -397,7 +397,7 @@ def test_upsert_group(test_data, db_session):
def test_upsert_user_groups(db_session):
- from db import crud
+ from app.shared.db import crud
@patch('db.crud.get_settings', new=lambda: bad_setings)
def test_missing_yaml(db_session):
@@ -419,7 +419,7 @@ def test_upsert_user_groups(db_session):
def test_create_sheet(db_session):
- from db import crud
+ from app.shared.db import crud
assert db_session.query(models.Sheet).count() == 0
@@ -440,7 +440,7 @@ def test_create_sheet(db_session):
def test_get_user_sheet(test_data, db_session):
- from db import crud
+ from app.shared.db import crud
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
@@ -451,7 +451,7 @@ def test_get_user_sheet(test_data, db_session):
def test_get_user_sheets(test_data, db_session):
- from db import crud
+ from app.shared.db import crud
assert len(crud.get_user_sheets(db_session, "")) == 0
rick_sheets = crud.get_user_sheets(db_session, "rick@example.com")
@@ -460,7 +460,7 @@ def test_get_user_sheets(test_data, db_session):
assert len(crud.get_user_sheets(db_session, "morty@example.com")) == 1
def test_delete_sheet(test_data, db_session):
- from db import crud
+ from app.shared.db import crud
assert crud.delete_sheet(db_session, "sheet-0", "") == False
assert crud.delete_sheet(db_session, "sheet-0", "rick@example.com") == True
diff --git a/app/tests/db/test_models.py b/app/tests/db/test_models.py
index d5ced1e..35ba368 100644
--- a/app/tests/db/test_models.py
+++ b/app/tests/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/endpoints/test_interoperability.py b/app/tests/endpoints/test_interoperability.py
index 2dac484..0d35ca2 100644
--- a/app/tests/endpoints/test_interoperability.py
+++ b/app/tests/endpoints/test_interoperability.py
@@ -3,7 +3,7 @@ import json
from unittest.mock import patch
from app.shared.config import ALLOW_ANY_EMAIL
-from db import crud
+from app.shared.db import crud
def test_submit_manual_archive_unauthenticated(client, test_no_auth):
diff --git a/app/tests/endpoints/test_sheet.py b/app/tests/endpoints/test_sheet.py
index d9c2f31..a39e0c8 100644
--- a/app/tests/endpoints/test_sheet.py
+++ b/app/tests/endpoints/test_sheet.py
@@ -46,7 +46,7 @@ def test_create_sheet_endpoint(app_with_auth, db_session):
# switch to jerry who's got less quota/permissions
from web.security import get_user_state
- from db.user_state import UserState
+ from app.shared.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)
@@ -76,7 +76,7 @@ def test_get_user_sheets_endpoint(client_with_auth, db_session):
assert response.json() == []
# with data
- from db import models
+ 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")
)
@@ -122,7 +122,7 @@ def test_delete_sheet_endpoint(client_with_auth, db_session):
}
# add sheets for deletion
- from db import models
+ 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"),
@@ -146,7 +146,7 @@ def test_delete_sheet_endpoint(client_with_auth, db_session):
class TestArchiveUserSheetEndpoint:
@patch("endpoints.sheet.celery", return_value=MagicMock())
def test_normal_flow(self, m_celery, client_with_auth, db_session):
- from db import models
+ 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()
@@ -169,7 +169,7 @@ class TestArchiveUserSheetEndpoint:
assert r.json() == {"detail": "No access to this sheet."}
def test_no_access(self, client_with_auth, db_session):
- from db import models
+ 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")
@@ -177,7 +177,7 @@ class TestArchiveUserSheetEndpoint:
assert r.json() == {"detail": "No access to this sheet."}
def test_user_not_in_group(self, client_with_auth, db_session):
- from db import models
+ 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")
@@ -185,7 +185,7 @@ class TestArchiveUserSheetEndpoint:
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 db import models
+ 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")
diff --git a/app/tests/endpoints/test_url.py b/app/tests/endpoints/test_url.py
index c5d2fcc..d6f43f0 100644
--- a/app/tests/endpoints/test_url.py
+++ b/app/tests/endpoints/test_url.py
@@ -130,7 +130,7 @@ def test_search_by_url(client_with_auth, client_with_token, db_session):
assert response.status_code == 200
assert response.json() == []
- from db import crud, schemas
+ from app.shared.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"), [], [])
# NB: this insertion is too fast for the ordering to be correct as they are within the same second
@@ -184,7 +184,7 @@ def test_delete_task(client_with_auth, db_session):
assert response.status_code == 200
assert response.json() == {"id": "delete-123-456-789", "deleted": False}
- from db import crud
+ from app.shared.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"), [], [])
response = client_with_auth.delete("/url/delete-123-456-789")
diff --git a/app/tests/worker/test_worker_main.py b/app/tests/worker/test_worker_main.py
index edebd5f..a0fb9ed 100644
--- a/app/tests/worker/test_worker_main.py
+++ b/app/tests/worker/test_worker_main.py
@@ -5,7 +5,8 @@ from unittest.mock import MagicMock, patch
import pytest
-from db import models, schemas
+from app.shared.db import models
+from app.shared import schemas
from auto_archiver import Metadata
from auto_archiver.core import Media
From b452ec9869c1aa5c308358134b757024512728b9 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Mon, 10 Feb 2025 23:51:35 +0000
Subject: [PATCH 39/75] moving user_state out of shared
---
app/tests/conftest.py | 2 +-
app/tests/endpoints/test_sheet.py | 2 +-
app/{shared => web}/db/user_state.py | 0
app/web/endpoints/default.py | 2 +-
app/web/endpoints/sheet.py | 2 +-
app/web/endpoints/url.py | 2 +-
app/web/security.py | 2 +-
7 files changed, 6 insertions(+), 6 deletions(-)
rename app/{shared => web}/db/user_state.py (100%)
diff --git a/app/tests/conftest.py b/app/tests/conftest.py
index 7505c07..41a3fb1 100644
--- a/app/tests/conftest.py
+++ b/app/tests/conftest.py
@@ -3,7 +3,7 @@ from fastapi.testclient import TestClient
import pytest
from unittest.mock import patch
from app.shared.config import ALLOW_ANY_EMAIL
-from app.shared.db.user_state import UserState
+from app.web.db.user_state import UserState
from app.shared.settings import Settings
diff --git a/app/tests/endpoints/test_sheet.py b/app/tests/endpoints/test_sheet.py
index a39e0c8..3241c7c 100644
--- a/app/tests/endpoints/test_sheet.py
+++ b/app/tests/endpoints/test_sheet.py
@@ -46,7 +46,7 @@ def test_create_sheet_endpoint(app_with_auth, db_session):
# switch to jerry who's got less quota/permissions
from web.security import get_user_state
- from app.shared.db.user_state import UserState
+ 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)
diff --git a/app/shared/db/user_state.py b/app/web/db/user_state.py
similarity index 100%
rename from app/shared/db/user_state.py
rename to app/web/db/user_state.py
diff --git a/app/web/endpoints/default.py b/app/web/endpoints/default.py
index 0568e51..86d7d70 100644
--- a/app/web/endpoints/default.py
+++ b/app/web/endpoints/default.py
@@ -7,7 +7,7 @@ from app.shared.config import VERSION, BREAKING_CHANGES
from app.shared.log import log_error
from app.shared.db import crud
from app.shared.schemas import ActiveUser, UsageResponse
-from app.shared.db.user_state import UserState
+from app.web.db.user_state import UserState
from app.web.security import get_user_auth, bearer_security, get_user_state
from app.shared.user_groups import GroupInfo
diff --git a/app/web/endpoints/sheet.py b/app/web/endpoints/sheet.py
index 643ecac..d202834 100644
--- a/app/web/endpoints/sheet.py
+++ b/app/web/endpoints/sheet.py
@@ -5,7 +5,7 @@ from fastapi.responses import JSONResponse
from sqlalchemy import exc
from sqlalchemy.orm import Session
-from app.shared.db.user_state import UserState
+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
diff --git a/app/web/endpoints/url.py b/app/web/endpoints/url.py
index 72e0cc2..86a7c67 100644
--- a/app/web/endpoints/url.py
+++ b/app/web/endpoints/url.py
@@ -10,7 +10,7 @@ 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.shared.db import crud
-from app.shared.db.user_state import UserState
+from app.web.db.user_state import UserState
from app.shared.db.database import get_db_dependency
from urllib.parse import urlparse
diff --git a/app/web/security.py b/app/web/security.py
index cefabdc..87bfe99 100644
--- a/app/web/security.py
+++ b/app/web/security.py
@@ -6,7 +6,7 @@ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from app.shared.config import ALLOW_ANY_EMAIL
from app.shared.settings import get_settings
from app.shared.db.database import get_db
-from app.shared.db.user_state import UserState
+from app.web.db.user_state import UserState
settings = get_settings()
bearer_security = HTTPBearer()
From a9dd278d24b4cb7ba80af38c477ac624beba5e35 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Tue, 11 Feb 2025 18:18:49 +0000
Subject: [PATCH 40/75] further worker/web separation and tests fixed
---
app/logs/.gitkeep | 0
app/shared/business_logic.py | 5 +-
app/shared/db/worker_crud.py | 61 ++++++++
app/shared/schemas.py | 1 -
app/tests/conftest.py | 14 +-
app/tests/{ => shared}/db/test_models.py | 0
app/tests/shared/db/test_worker_crud.py | 98 ++++++++++++
app/tests/user-groups.test.yaml | 24 +--
app/tests/{ => web}/db/test_crud.py | 145 +++---------------
app/tests/{ => web}/endpoints/test_default.py | 20 +--
.../endpoints/test_interoperability.py | 10 +-
app/tests/{ => web}/endpoints/test_sheet.py | 4 +-
app/tests/{ => web}/endpoints/test_task.py | 6 +-
app/tests/{ => web}/endpoints/test_url.py | 20 +--
app/tests/web/test_main.py | 6 +-
app/tests/web/test_security.py | 22 +--
app/tests/worker/test_worker_main.py | 32 ++--
app/{shared => web}/db/crud.py | 58 +------
app/web/db/user_state.py | 3 +-
app/web/endpoints/default.py | 4 +-
app/web/endpoints/interoperability.py | 18 ++-
app/web/endpoints/sheet.py | 2 +-
app/web/endpoints/url.py | 2 +-
app/web/events.py | 6 +-
app/web/main.py | 5 +-
app/web/middleware.py | 2 +-
app/web/utils/metrics.py | 2 +-
app/worker/main.py | 11 +-
28 files changed, 301 insertions(+), 280 deletions(-)
create mode 100644 app/logs/.gitkeep
create mode 100644 app/shared/db/worker_crud.py
rename app/tests/{ => shared}/db/test_models.py (100%)
create mode 100644 app/tests/shared/db/test_worker_crud.py
rename app/tests/{ => web}/db/test_crud.py (77%)
rename app/tests/{ => web}/endpoints/test_default.py (85%)
rename app/tests/{ => web}/endpoints/test_interoperability.py (83%)
rename app/tests/{ => web}/endpoints/test_sheet.py (98%)
rename app/tests/{ => web}/endpoints/test_task.py (91%)
rename app/tests/{ => web}/endpoints/test_url.py (91%)
rename app/{shared => web}/db/crud.py (84%)
diff --git a/app/logs/.gitkeep b/app/logs/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/app/shared/business_logic.py b/app/shared/business_logic.py
index 2691ffe..ffd9cb2 100644
--- a/app/shared/business_logic.py
+++ b/app/shared/business_logic.py
@@ -4,11 +4,12 @@
import datetime
from sqlalchemy.orm import Session
-from app.shared.db import crud
+from app.shared.db import worker_crud
def get_store_archive_until(db: Session, group_id: str) -> datetime.datetime:
- group = crud.get_group(db, group_id)
+ 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
diff --git a/app/shared/db/worker_crud.py b/app/shared/db/worker_crud.py
new file mode 100644
index 0000000..93962b9
--- /dev/null
+++ b/app/shared/db/worker_crud.py
@@ -0,0 +1,61 @@
+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_task(db: Session, task: schemas.ArchiveCreate, tags: list[models.Tag], urls: list[models.ArchiveUrl]) -> models.Archive:
+ # TODO: rename task to archive
+ 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, sheet_id=task.sheet_id, store_until=task.store_until)
+ db_task.tags = tags
+ db_task.urls = urls
+ db.add(db_task)
+ db.commit()
+ db.refresh(db_task)
+ return db_task
+
+
+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]
+ # insert everything
+ db_task = create_task(db, task=archive, tags=db_tags, urls=archive.urls)
+ return db_task
diff --git a/app/shared/schemas.py b/app/shared/schemas.py
index b76e711..f860f45 100644
--- a/app/shared/schemas.py
+++ b/app/shared/schemas.py
@@ -113,5 +113,4 @@ class CelerySheetTask(BaseModel):
class SubmitManualArchive(ArchiveTrigger):
- url: None = None
result: str # should be a Metadata.to_json()
diff --git a/app/tests/conftest.py b/app/tests/conftest.py
index 41a3fb1..89cfe9a 100644
--- a/app/tests/conftest.py
+++ b/app/tests/conftest.py
@@ -3,8 +3,8 @@ from fastapi.testclient import TestClient
import pytest
from unittest.mock import patch
from app.shared.config import ALLOW_ANY_EMAIL
-from app.web.db.user_state import UserState
from app.shared.settings import Settings
+from app.web.db.user_state import UserState
@pytest.fixture(autouse=True)
@@ -21,7 +21,7 @@ def get_settings():
@pytest.fixture(autouse=True)
def mock_settings():
- with patch('shared.settings.Settings', return_value=Settings(_env_file=".env.test")) as mock_settings:
+ with patch('app.shared.settings.Settings', return_value=Settings(_env_file=".env.test")) as mock_settings:
yield mock_settings
@@ -29,7 +29,7 @@ def mock_settings():
def test_db(get_settings: Settings):
from app.shared.db import models
from app.shared.db.database import make_engine
- from app.shared.db.crud import get_user_groups
+ from app.web.db.crud import get_user_groups
get_user_groups.cache_clear()
make_engine.cache_clear()
@@ -62,8 +62,8 @@ def db_session(test_db):
@pytest.fixture()
def app(db_session):
- from web.main import app_factory
- from app.shared.db import crud
+ from app.web.main import app_factory
+ from app.web.db import crud
app = app_factory()
crud.upsert_user_groups(db_session)
return app
@@ -77,7 +77,7 @@ def client(app):
@pytest.fixture()
def app_with_auth(app, db_session):
- from web.security import get_token_or_user_auth, get_user_auth, get_user_state
+ 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")
@@ -92,7 +92,7 @@ def client_with_auth(app_with_auth):
@pytest.fixture()
def app_with_token(app):
- from web.security import token_api_key_auth, get_token_or_user_auth
+ 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
diff --git a/app/tests/db/test_models.py b/app/tests/shared/db/test_models.py
similarity index 100%
rename from app/tests/db/test_models.py
rename to app/tests/shared/db/test_models.py
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..70f0fda
--- /dev/null
+++ b/app/tests/shared/db/test_worker_crud.py
@@ -0,0 +1,98 @@
+from app.shared.db import models
+
+
+from app.tests.web.db.test_crud import test_data
+
+
+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_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 = worker_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
diff --git a/app/tests/user-groups.test.yaml b/app/tests/user-groups.test.yaml
index ccbbfec..16a3ba7 100644
--- a/app/tests/user-groups.test.yaml
+++ b/app/tests/user-groups.test.yaml
@@ -19,17 +19,17 @@ domains:
orchestrators:
- spaceship: tests/orchestration.test.yaml
- interdimensional: tests/orchestration.test.yaml
- default: tests/orchestration.test.yaml
+ spaceship: app/tests/orchestration.test.yaml
+ interdimensional: app/tests/orchestration.test.yaml
+ default: app/tests/orchestration.test.yaml
-default_orchestrator: tests/orchestration.test.yaml
+default_orchestrator: app/tests/orchestration.test.yaml
groups:
spaceship:
description: "The spaceship crew"
- orchestrator: tests/orchestration.test.yaml
- orchestrator_sheet: tests/orchestration.test.yaml
+ orchestrator: app/tests/orchestration.test.yaml
+ orchestrator_sheet: app/tests/orchestration.test.yaml
permissions:
read: ["all"]
archive_url: true
@@ -43,8 +43,8 @@ groups:
priority: "high"
interdimensional:
description: "Interdimensional travelers"
- orchestrator: tests/orchestration.test.yaml
- orchestrator_sheet: tests/orchestration.test.yaml
+ orchestrator: app/tests/orchestration.test.yaml
+ orchestrator_sheet: app/tests/orchestration.test.yaml
permissions:
read: ["interdimensional", "animated-characters"]
archive_url: true
@@ -58,8 +58,8 @@ groups:
priority: "high"
animated-characters:
description: "Animated characters"
- orchestrator: tests/orchestration.test.yaml
- orchestrator_sheet: tests/orchestration.test.yaml
+ orchestrator: app/tests/orchestration.test.yaml
+ orchestrator_sheet: app/tests/orchestration.test.yaml
permissions:
read: ["animated-characters"]
archive_url: true
@@ -72,8 +72,8 @@ groups:
priority: "low"
default:
description: "Public access"
- orchestrator: tests/orchestration.test.yaml
- orchestrator_sheet: tests/orchestration.test.yaml
+ orchestrator: app/tests/orchestration.test.yaml
+ orchestrator_sheet: app/tests/orchestration.test.yaml
permissions:
# read: []
archive_url: true
diff --git a/app/tests/db/test_crud.py b/app/tests/web/db/test_crud.py
similarity index 77%
rename from app/tests/db/test_crud.py
rename to app/tests/web/db/test_crud.py
index a7317b3..625aee7 100644
--- a/app/tests/db/test_crud.py
+++ b/app/tests/web/db/test_crud.py
@@ -4,7 +4,7 @@ from unittest.mock import patch
import pytest
import yaml
from app.shared.db import models
-from shared.settings import Settings
+from app.shared.settings import Settings
authors = ["rick@example.com", "morty@example.com", "jerry@example.com"]
@@ -55,18 +55,16 @@ def test_data(db_session):
# setup groups
assert db_session.query(models.Group).count() == 0
- from app.shared.db import crud
+ 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_get_archive(test_data, db_session):
- from app.shared.db import crud
+ from app.web.db import crud
from app.shared.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"
@@ -93,7 +91,7 @@ def test_get_archive(test_data, db_session):
def test_search_archives_by_url(test_data, db_session):
- from app.shared.db import crud
+ from app.web.db import crud
from app.shared.config import ALLOW_ANY_EMAIL
# rick's archives are private
@@ -141,7 +139,7 @@ def test_search_archives_by_url(test_data, db_session):
def test_search_archives_by_email(test_data, db_session):
from app.shared.config import ALLOW_ANY_EMAIL
- from app.shared.db import crud
+ from app.web.db import crud
# lower/upper case
assert len(crud.search_archives_by_email(db_session, "rick@example.com")) == 34
@@ -160,9 +158,9 @@ def test_search_archives_by_email(test_data, db_session):
assert a2[0].created_at == datetime(2021, 1, 1)
-@patch("db.crud.DATABASE_QUERY_LIMIT", new=25)
+@patch("app.web.db.crud.DATABASE_QUERY_LIMIT", new=25)
def test_max_query_limit(test_data, db_session):
- from app.shared.db import crud
+ from app.web.db import crud
from app.shared.config import ALLOW_ANY_EMAIL
assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL)) == 25
@@ -172,53 +170,8 @@ def test_max_query_limit(test_data, db_session):
assert len(crud.search_archives_by_email(db_session, "rick@example.com", limit=1000)) == 25
-def test_create_task(db_session):
- from app.shared.db import 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 = 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 app.shared.db import crud
+ from app.web.db import crud
# none deleted yet
assert crud.get_archive(db_session, "archive-id-456-0", "rick@example.com") is not None
@@ -236,7 +189,7 @@ def test_soft_delete(test_data, db_session):
def test_count_archives(test_data, db_session):
- from app.shared.db import crud
+ from app.web.db import crud
assert crud.count_archives(db_session) == 100
db_session.query(models.Archive).filter(models.Archive.id == "archive-id-456-0").delete()
@@ -245,7 +198,7 @@ def test_count_archives(test_data, db_session):
def test_count_archive_urls(test_data, db_session):
- from app.shared.db import crud
+ from app.web.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()
@@ -260,7 +213,7 @@ def test_count_archive_urls(test_data, db_session):
def test_count_users(test_data, db_session):
- from app.shared.db import crud
+ from app.web.db import crud
assert crud.count_users(db_session) == 3
db_session.query(models.User).filter(models.User.email == "rick@example.com").delete()
@@ -269,7 +222,7 @@ def test_count_users(test_data, db_session):
def test_count_by_users_since(test_data, db_session):
- from app.shared.db import crud
+ 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
@@ -278,32 +231,8 @@ def test_count_by_users_since(test_data, db_session):
assert cu[2].total == 33
-def test_create_tag(db_session):
- from app.shared.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_user_in_group(test_data, db_session):
- from app.shared.db import crud
+ from app.web.db import crud
from app.shared.config import ALLOW_ANY_EMAIL
# see user-groups.test.yaml
@@ -339,36 +268,12 @@ def test_is_user_in_group(test_data, db_session):
]
for email, group, expected in test_pairs:
print(f"{email} in {group} == {expected}")
- assert crud.is_user_in_group(db_session, email, group) == expected
+ assert crud.is_user_in_group(email, group) == expected
-def test_get_group(test_data, db_session):
- from app.shared.db import crud
-
- assert crud.get_group(db_session, "spaceship") is not None
- assert crud.get_group(db_session, "interdimensional") is not None
- assert crud.get_group(db_session, "animated-characters") is not None
- assert crud.get_group(db_session, "non-existent!@#!%!") is None
-
-
-def test_create_or_get_user(test_data, db_session):
- from app.shared.db import crud
-
- assert db_session.query(models.User).count() == 3
-
- # already exists
- assert (u1 := crud.create_or_get_user(db_session, "rick@example.com")) is not None
- assert u1.email == "rick@example.com"
-
- # new user
- assert (u2 := 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_upsert_group(test_data, db_session):
- from app.shared.db import crud
+ from app.web.db import crud
assert db_session.query(models.Group).count() == 4
@@ -397,29 +302,29 @@ def test_upsert_group(test_data, db_session):
def test_upsert_user_groups(db_session):
- from app.shared.db import crud
+ from app.web.db import crud
- @patch('db.crud.get_settings', new=lambda: bad_setings)
+ @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('db.crud.get_settings', new=lambda: bad_setings)
+ @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 = "tests/user-groups.test.missing.yaml"
+ bad_setings.USER_GROUPS_FILENAME = "app/tests/user-groups.test.missing.yaml"
test_missing_yaml(db_session)
- bad_setings.USER_GROUPS_FILENAME = "tests/user-groups.test.broken.yaml"
+ bad_setings.USER_GROUPS_FILENAME = "app/tests/user-groups.test.broken.yaml"
test_broken_yaml(db_session)
def test_create_sheet(db_session):
- from app.shared.db import crud
+ from app.web.db import crud
assert db_session.query(models.Sheet).count() == 0
@@ -440,7 +345,7 @@ def test_create_sheet(db_session):
def test_get_user_sheet(test_data, db_session):
- from app.shared.db import crud
+ from app.web.db import crud
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
@@ -451,7 +356,7 @@ def test_get_user_sheet(test_data, db_session):
def test_get_user_sheets(test_data, db_session):
- from app.shared.db import crud
+ from app.web.db import crud
assert len(crud.get_user_sheets(db_session, "")) == 0
rick_sheets = crud.get_user_sheets(db_session, "rick@example.com")
@@ -459,10 +364,10 @@ def test_get_user_sheets(test_data, db_session):
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):
- from app.shared.db import crud
+ from app.web.db import crud
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
-
diff --git a/app/tests/endpoints/test_default.py b/app/tests/web/endpoints/test_default.py
similarity index 85%
rename from app/tests/endpoints/test_default.py
rename to app/tests/web/endpoints/test_default.py
index 4215ec6..54bc14b 100644
--- a/app/tests/endpoints/test_default.py
+++ b/app/tests/web/endpoints/test_default.py
@@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
from fastapi.testclient import TestClient
import pytest
from app.shared.config import VERSION
-from tests.db.test_crud import test_data
+from app.tests.web.db.test_crud import test_data
def test_endpoint_home(client_with_auth):
@@ -14,9 +14,9 @@ def test_endpoint_home(client_with_auth):
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"])
+@patch("app.web.endpoints.default.bearer_security", new_callable=AsyncMock)
+@patch("app.web.endpoints.default.get_user_auth", new_callable=AsyncMock, return_value="test@example.com")
+@patch("app.web.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
@@ -27,9 +27,9 @@ def test_endpoint_home_with_groups(m1, m2, m3, client_with_auth):
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'))
+@patch("app.web.endpoints.default.bearer_security", new_callable=AsyncMock)
+@patch("app.web.endpoints.default.get_user_auth", new_callable=AsyncMock, return_value="test@example.com")
+@patch("app.web.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
@@ -52,7 +52,7 @@ def test_endpoint_active_no_auth(client, test_no_auth):
def test_endpoint_active(app):
m_user_state = MagicMock()
- from web.security import get_user_state
+ from app.web.security import get_user_state
app.dependency_overrides[get_user_state] = lambda: m_user_state
# inactive user
@@ -103,7 +103,7 @@ async def test_prometheus_metrics(test_data, client_with_token, get_settings):
assert 'disk_utilization{type="used"}' not in r.text
# after metrics calculation
- from web.utils.metrics import measure_regular_metrics
+ 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
@@ -117,7 +117,7 @@ async def test_prometheus_metrics(test_data, client_with_token, get_settings):
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 web.utils.metrics import measure_regular_metrics
+ 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
diff --git a/app/tests/endpoints/test_interoperability.py b/app/tests/web/endpoints/test_interoperability.py
similarity index 83%
rename from app/tests/endpoints/test_interoperability.py
rename to app/tests/web/endpoints/test_interoperability.py
index 0d35ca2..64629ae 100644
--- a/app/tests/endpoints/test_interoperability.py
+++ b/app/tests/web/endpoints/test_interoperability.py
@@ -1,9 +1,9 @@
from datetime import datetime
import json
-from unittest.mock import patch
+from unittest.mock import MagicMock, patch
from app.shared.config import ALLOW_ANY_EMAIL
-from app.shared.db import crud
+from app.web.db import crud
def test_submit_manual_archive_unauthenticated(client, test_no_auth):
@@ -14,11 +14,11 @@ 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("endpoints.interoperability.get_store_until", return_value=datetime.now())
+@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"]})
+ 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()
@@ -35,6 +35,6 @@ def test_submit_manual_archive(m1, client_with_token, db_session):
# 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"]})
+ 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."}
diff --git a/app/tests/endpoints/test_sheet.py b/app/tests/web/endpoints/test_sheet.py
similarity index 98%
rename from app/tests/endpoints/test_sheet.py
rename to app/tests/web/endpoints/test_sheet.py
index 3241c7c..aedecac 100644
--- a/app/tests/endpoints/test_sheet.py
+++ b/app/tests/web/endpoints/test_sheet.py
@@ -45,7 +45,7 @@ def test_create_sheet_endpoint(app_with_auth, db_session):
assert response.json() == {"detail": "User does not have access to this group."}
# switch to jerry who's got less quota/permissions
- from web.security import get_user_state
+ 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)
@@ -144,7 +144,7 @@ def test_delete_sheet_endpoint(client_with_auth, db_session):
class TestArchiveUserSheetEndpoint:
- @patch("endpoints.sheet.celery", return_value=MagicMock())
+ @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"))
diff --git a/app/tests/endpoints/test_task.py b/app/tests/web/endpoints/test_task.py
similarity index 91%
rename from app/tests/endpoints/test_task.py
rename to app/tests/web/endpoints/test_task.py
index 9585c39..937ad46 100644
--- a/app/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/endpoints/test_url.py b/app/tests/web/endpoints/test_url.py
similarity index 91%
rename from app/tests/endpoints/test_url.py
rename to app/tests/web/endpoints/test_url.py
index d6f43f0..008973f 100644
--- a/app/tests/endpoints/test_url.py
+++ b/app/tests/web/endpoints/test_url.py
@@ -8,8 +8,8 @@ def test_archive_url_unauthenticated(client, test_no_auth):
test_no_auth(client.post, "/url/archive")
-@patch("endpoints.url.UserState")
-@patch("endpoints.url.celery", return_value=MagicMock())
+@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.delay.return_value = TaskResult(id="123-456-789", status="PENDING", result="")
@@ -81,7 +81,7 @@ def test_archive_url(m_celery, m2, client_with_auth):
assert m_signature.delay.call_count == 2
-@patch("endpoints.url.UserState")
+@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
@@ -102,7 +102,7 @@ def test_archive_url_quotas(m1, client_with_auth):
m_user_state.has_quota_max_monthly_mbs.assert_called_once()
-@patch("endpoints.url.celery", return_value=MagicMock())
+@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.delay.return_value = TaskResult(id="123-456-789", status="PENDING", result="")
@@ -130,9 +130,11 @@ def test_search_by_url(client_with_auth, client_with_token, db_session):
assert response.status_code == 200
assert response.json() == []
- from app.shared.db import crud, schemas
+ from app.shared import schemas
+ from app.shared.db import worker_crud
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"), [], [])
+ #TODO: fix as this method is gone to shared.db
+ worker_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"), [], [])
# 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")
@@ -165,7 +167,7 @@ def test_search_by_url(client_with_auth, client_with_token, db_session):
assert len(response.json()) == 10
-@patch("endpoints.url.UserState")
+@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
@@ -184,8 +186,8 @@ def test_delete_task(client_with_auth, db_session):
assert response.status_code == 200
assert response.json() == {"id": "delete-123-456-789", "deleted": False}
- from app.shared.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"), [], [])
+ from app.shared.db import worker_crud
+ worker_crud.create_task(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
diff --git a/app/tests/web/test_main.py b/app/tests/web/test_main.py
index 817125c..95cc5d6 100644
--- a/app/tests/web/test_main.py
+++ b/app/tests/web/test_main.py
@@ -17,9 +17,9 @@ def test_alembic(db_session):
alembic.config.main(argv=['--raiseerr', 'upgrade', 'head'])
alembic.config.main(argv=['--raiseerr', 'downgrade', 'base'])
-@patch("endpoints.default.crud.soft_delete_task", side_effect=Exception('mocked error'))
+@patch("app.web.endpoints.default.crud.soft_delete_task", side_effect=Exception('mocked error'))
def test_logging_middleware(m1, client_with_auth):
- from web.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.delete("/url/123")
@@ -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/app/tests/web/test_security.py b/app/tests/web/test_security.py
index e9cb1e8..3b45f2d 100644
--- a/app/tests/web/test_security.py
+++ b/app/tests/web/test_security.py
@@ -8,7 +8,7 @@ from app.shared.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
+ 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,9 @@ async def test_authenticate_user():
@pytest.mark.asyncio
async def test_authenticate_user_exception():
- from web.security import authenticate_user
+ from app.web.security import authenticate_user
- with patch("web.security.requests.get") as mock_get:
+ 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")
diff --git a/app/tests/worker/test_worker_main.py b/app/tests/worker/test_worker_main.py
index a0fb9ed..b8cb4cb 100644
--- a/app/tests/worker/test_worker_main.py
+++ b/app/tests/worker/test_worker_main.py
@@ -16,12 +16,12 @@ 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("worker.main.insert_result_into_db")
- @patch("worker.main.get_store_until", return_value=datetime.now())
- @patch("worker.main.load_orchestrator")
+ @patch("app.worker.main.insert_result_into_db")
+ @patch("app.worker.main.get_store_until", return_value=datetime.now())
+ @patch("app.worker.main.load_orchestrator")
@patch("celery.app.task.Task.request")
def test_success(self, m_req, m_load, m_store, m_insert, db_session):
- from worker.main import create_archive_task
+ from app.worker.main import create_archive_task
m_req.id = "this-just-in"
mock_orchestrator = self.mock_orchestrator_choice(m_load)
@@ -38,14 +38,14 @@ class Test_create_archive_task():
assert len(task["media"]) == 0
def test_raise_invalid(self):
- from worker.main import create_archive_task
+ from app.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.load_orchestrator")
+ @patch("app.worker.main.insert_result_into_db", side_effect=Exception)
+ @patch("app.worker.main.load_orchestrator")
def test_raise_db_error(self, m_load, m_insert):
- from worker.main import create_archive_task
+ from app.worker.main import create_archive_task
mock_orchestrator = self.mock_orchestrator_choice(m_load)
with pytest.raises(Exception):
@@ -53,10 +53,10 @@ class Test_create_archive_task():
mock_orchestrator.feed_item.assert_called_once()
- @patch("worker.main.insert_result_into_db", return_value=None)
- @patch("worker.main.load_orchestrator")
+ @patch("app.worker.main.insert_result_into_db", return_value=None)
+ @patch("app.worker.main.load_orchestrator")
def test_raise_empty_result(self, m_load, m_insert):
- from worker.main import create_archive_task
+ from app.worker.main import create_archive_task
mock_orchestrator = self.mock_orchestrator_choice(m_load)
with pytest.raises(Exception) as e:
@@ -75,11 +75,11 @@ 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("worker.main.models.generate_uuid", return_value="constant-uuid")
- @patch("worker.main.get_store_until", return_value=datetime.now())
- @patch("worker.main.load_orchestrator")
+ @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.load_orchestrator")
def test_success(self, m_load, m_store, m_uuid, db_session):
- from worker.main import create_sheet_task
+ from app.worker.main import create_sheet_task
assert db_session.query(models.Archive).filter(models.Archive.url == self.URL).count() == 0
@@ -116,7 +116,7 @@ class Test_create_sheet_task():
def test_get_all_urls(db_session):
- from worker.main import get_all_urls
+ from app.worker.main import get_all_urls
from auto_archiver import Metadata
meta = Metadata().set_url("https://example.com")
diff --git a/app/shared/db/crud.py b/app/web/db/crud.py
similarity index 84%
rename from app/shared/db/crud.py
rename to app/web/db/crud.py
index c3c2d00..d1aa92e 100644
--- a/app/shared/db/crud.py
+++ b/app/web/db/crud.py
@@ -9,7 +9,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.shared.config import ALLOW_ANY_EMAIL
from app.shared.db.database import get_db
from app.shared.db import models
-from app.shared import schemas
from app.shared.settings import get_settings
from app.shared.user_groups import UserGroups
from app.shared.utils.misc import fnv1a_hash_mod
@@ -55,16 +54,6 @@ def search_archives_by_url(db: Session, url: str, email: str, skip: int = 0, lim
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()
-#TODO: rename task to archive
-def create_task(db: Session, task: schemas.ArchiveCreate, tags: list[models.Tag], urls: list[models.ArchiveUrl]) -> models.Archive:
- 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, sheet_id=task.sheet_id, store_until=task.store_until)
- 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
@@ -113,17 +102,7 @@ async def soft_delete_expired_archives(db: AsyncSession) -> dict:
# --------------- TAG
-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 is_user_in_group(db: Session, email: str, group_name: str) -> models.Group:
+def is_user_in_group(email: str, group_name: str) -> models.Group:
if email == ALLOW_ANY_EMAIL: return True
return len(group_name) and len(email) and group_name in get_user_groups(email)
@@ -150,21 +129,6 @@ def get_user_groups(email: str) -> list[str]:
# --------------- INIT User-Groups
-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 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:
@@ -285,14 +249,6 @@ async def delete_stale_sheets(db: AsyncSession, inactivity_days: int) -> dict:
await db.commit()
return dict(deleted)
-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
-
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()
@@ -300,15 +256,3 @@ def delete_sheet(db: Session, sheet_id: str, email: str) -> bool:
db.delete(db_sheet)
db.commit()
return db_sheet is not None
-
-
-#--- Celery worker tasks
-
-
-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]
- # insert everything
- db_task = create_task(db, task=archive, tags=db_tags, urls=archive.urls)
- return db_task
\ No newline at end of file
diff --git a/app/web/db/user_state.py b/app/web/db/user_state.py
index d7f5192..dc7b112 100644
--- a/app/web/db/user_state.py
+++ b/app/web/db/user_state.py
@@ -5,9 +5,10 @@ from sqlalchemy.orm import Session
from sqlalchemy import func
from datetime import datetime
-from app.shared.db import crud, models
+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
class UserState:
"""
diff --git a/app/web/endpoints/default.py b/app/web/endpoints/default.py
index 86d7d70..186b574 100644
--- a/app/web/endpoints/default.py
+++ b/app/web/endpoints/default.py
@@ -5,7 +5,7 @@ from fastapi.responses import FileResponse, JSONResponse
from app.shared.config import VERSION, BREAKING_CHANGES
from app.shared.log import log_error
-from app.shared.db import crud
+from app.web.db import crud
from app.shared.schemas import ActiveUser, UsageResponse
from app.web.db.user_state import UserState
from app.web.security import get_user_auth, bearer_security, get_user_state
@@ -56,4 +56,4 @@ def get_user_usage(
@default_router.get('/favicon.ico', include_in_schema=False)
async def favicon() -> FileResponse:
- return FileResponse("web/static/favicon.ico")
+ return FileResponse("app/web/static/favicon.ico")
diff --git a/app/web/endpoints/interoperability.py b/app/web/endpoints/interoperability.py
index 33eff6e..2c7660a 100644
--- a/app/web/endpoints/interoperability.py
+++ b/app/web/endpoints/interoperability.py
@@ -9,7 +9,7 @@ from sqlalchemy.orm import Session
from app.shared.aa_utils import get_all_urls
from app.shared.config import ALLOW_ANY_EMAIL
from app.shared import business_logic, schemas
-from app.shared.db import crud
+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
@@ -26,10 +26,20 @@ def submit_manual_archive(
auth=Depends(token_api_key_auth),
db: Session = Depends(get_db_dependency)
):
- result: Metadata = Metadata.from_json(manual.result)
+ 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,
@@ -40,10 +50,10 @@ def submit_manual_archive(
id=models.generate_uuid(),
result=json.loads(result.to_json()),
urls=get_all_urls(result),
- store_until=business_logic.get_store_archive_until(db, manual.group_id),
+ store_until=store_until,
)
- db_archive = crud.store_archived_url(db, archive)
+ 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:
diff --git a/app/web/endpoints/sheet.py b/app/web/endpoints/sheet.py
index d202834..89699d0 100644
--- a/app/web/endpoints/sheet.py
+++ b/app/web/endpoints/sheet.py
@@ -9,7 +9,7 @@ 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.shared.db import crud
+from app.web.db import crud
from app.shared.db.database import get_db_dependency
sheet_router = APIRouter(prefix="/sheet", tags=["Google Spreadsheet operations"])
diff --git a/app/web/endpoints/url.py b/app/web/endpoints/url.py
index 86a7c67..7e3de02 100644
--- a/app/web/endpoints/url.py
+++ b/app/web/endpoints/url.py
@@ -9,7 +9,7 @@ from app.shared.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.shared.db import crud
+from app.web.db import crud
from app.web.db.user_state import UserState
from app.shared.db.database import get_db_dependency
diff --git a/app/web/events.py b/app/web/events.py
index 65b7347..4dfdf25 100644
--- a/app/web/events.py
+++ b/app/web/events.py
@@ -9,11 +9,12 @@ from fastapi_utils.tasks import repeat_every
from loguru import logger
from fastapi_mail import FastMail, MessageSchema, MessageType
-from app.shared.db import crud, models
+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.utils.metrics import measure_regular_metrics, redis_subscribe_worker_exceptions
celery = get_celery()
@@ -24,12 +25,9 @@ async def lifespan(app: FastAPI):
# see https://fastapi.tiangolo.com/advanced/events/#lifespan
# STARTUP
- logger.debug("HERE 00")
engine = make_engine(get_settings().DATABASE_PATH)
models.Base.metadata.create_all(bind=engine)
- logger.debug("HERE 01")
alembic.config.main(prog="alembic", argv=['--raiseerr', 'upgrade', 'head'])
- logger.debug("HERE 02")
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())
diff --git a/app/web/main.py b/app/web/main.py
index a11908e..aabb4f4 100644
--- a/app/web/main.py
+++ b/app/web/main.py
@@ -15,7 +15,7 @@ from app.web.middleware import logging_middleware
from app.shared import schemas
from app.shared.task_messaging import get_celery
-from app.shared.db import crud
+from app.web.db import crud
from app.web.security import get_user_auth, token_api_key_auth, get_token_or_user_auth
from app.shared.config import VERSION, API_DESCRIPTION
from app.shared.db.database import get_db_dependency
@@ -141,7 +141,8 @@ def app_factory(settings = get_settings()):
def archive_sheet(sheet: schemas.SubmitSheet, email=Depends(get_user_auth), db: Session = Depends(get_db_dependency)):
logger.info(f"SHEET TASK for {sheet=}")
sheet.author_id = email
- if not crud.is_user_in_group(db, email, sheet.group_id):
+ #NB: no longer working
+ if not crud.is_user_in_group(email, sheet.group_id):
raise HTTPException(status_code=403, detail="User does not have access to this group.")
task = celery.signature("create_sheet_task", args=[sheet.model_dump_json()]).delay()
return JSONResponse({"id": task.id})
diff --git a/app/web/middleware.py b/app/web/middleware.py
index 3663af9..aa5c077 100644
--- a/app/web/middleware.py
+++ b/app/web/middleware.py
@@ -2,6 +2,7 @@
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):
@@ -10,7 +11,6 @@ async def logging_middleware(request: Request, call_next):
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 web.utils.metrics import EXCEPTION_COUNTER
EXCEPTION_COUNTER.labels(type=e.__class__.__name__).inc()
logger.info(f"{request.client.host}:{request.client.port} {request.method} {request.url._url} - {e.__class__.__name__} {e}")
log_error(e)
diff --git a/app/web/utils/metrics.py b/app/web/utils/metrics.py
index 0a2d793..64aaf9c 100644
--- a/app/web/utils/metrics.py
+++ b/app/web/utils/metrics.py
@@ -4,7 +4,7 @@ import os
import shutil
from prometheus_client import Counter, Gauge
-from app.shared.db import crud
+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
diff --git a/app/worker/main.py b/app/worker/main.py
index 966cd5c..8ced19b 100644
--- a/app/worker/main.py
+++ b/app/worker/main.py
@@ -7,13 +7,14 @@ from sqlalchemy import exc
from auto_archiver import Config, ArchivingOrchestrator, Metadata
-from app.shared.db import crud, models
+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()
@@ -79,7 +80,7 @@ def create_sheet_task(self, sheet_json: str):
if stats["archived"] > 0:
with get_db() as session:
- crud.update_sheet_last_url_archived_at(session, sheet.sheet_id)
+ 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
@@ -88,11 +89,11 @@ def create_sheet_task(self, sheet_json: str):
def load_orchestrator(group_id: str, orchestrator_for_sheet: bool = False, overwrite_configs: dict = {}) -> ArchivingOrchestrator:
with get_db() as session:
- group = crud.get_group(session, group_id)
+ group = worker_crud.get_group(session, group_id)
if orchestrator_for_sheet:
orchestrator_fn = group.orchestrator_sheet
else:
- orchestrator_fn = crud.get_group(session, group_id).orchestrator
+ orchestrator_fn = worker_crud.get_group(session, group_id).orchestrator
assert orchestrator_fn, f"no orchestrator found for {group_id}"
@@ -103,7 +104,7 @@ def load_orchestrator(group_id: str, orchestrator_for_sheet: bool = False, overw
def insert_result_into_db(archive: schemas.ArchiveCreate) -> str:
with get_db() as session:
- db_task = crud.store_archived_url(session, archive)
+ db_task = worker_crud.store_archived_url(session, archive)
logger.debug(f"[ARCHIVE STORED] {db_task.author_id} {db_task.url}")
return db_task.id
From f24f88c44b14728d65fe2ea9ea1a29c58219081d Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Tue, 11 Feb 2025 19:10:28 +0000
Subject: [PATCH 41/75] version in config
---
app/shared/config.py | 1 +
pyproject.toml | 4 +++-
web.Dockerfile | 4 ++--
3 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/app/shared/config.py b/app/shared/config.py
index 6122b1c..13eed55 100644
--- a/app/shared/config.py
+++ b/app/shared/config.py
@@ -1,4 +1,5 @@
VERSION = "0.9.0"
+
API_DESCRIPTION = """
#### API for the Auto-Archiver project, a tool to archive web pages and Google Sheets.
diff --git a/pyproject.toml b/pyproject.toml
index 709a695..490f6dc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,8 @@
+[tool.poetry]
+package-mode = false
+
[project]
name = "auto-archiver-api"
-version = "0.9.0"
description = "API wrapper for Bellingcat's Auto Archiver, supports users, groups, sheet and url archives."
authors = [
{ name = "Bellingcat", email = "contact-tech@bellingcat.com" },
diff --git a/web.Dockerfile b/web.Dockerfile
index 9c73efc..14a751b 100644
--- a/web.Dockerfile
+++ b/web.Dockerfile
@@ -10,8 +10,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir poetry
-COPY pyproject.toml poetry.lock .
-RUN poetry install --with web --no-interaction --no-ansi --no-root --no-cache
+COPY pyproject.toml poetry.lock README.md .
+RUN poetry install --with web --no-interaction --no-ansi --no-cache
# Copy the application code
COPY alembic.ini ./
From 606e69587ba03680a303d73204d6a0505b853d09 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Tue, 11 Feb 2025 19:13:33 +0000
Subject: [PATCH 42/75] config file is web only
---
app/tests/conftest.py | 2 +-
app/tests/web/db/test_crud.py | 10 +++++-----
app/tests/web/endpoints/test_default.py | 2 +-
app/tests/web/endpoints/test_interoperability.py | 2 +-
app/tests/web/test_security.py | 2 +-
app/{shared => web}/config.py | 0
app/web/db/crud.py | 2 +-
app/web/endpoints/default.py | 2 +-
app/web/endpoints/interoperability.py | 2 +-
app/web/endpoints/url.py | 2 +-
app/web/main.py | 2 +-
app/web/security.py | 2 +-
12 files changed, 15 insertions(+), 15 deletions(-)
rename app/{shared => web}/config.py (100%)
diff --git a/app/tests/conftest.py b/app/tests/conftest.py
index 89cfe9a..d488671 100644
--- a/app/tests/conftest.py
+++ b/app/tests/conftest.py
@@ -2,7 +2,7 @@ import os
from fastapi.testclient import TestClient
import pytest
from unittest.mock import patch
-from app.shared.config import ALLOW_ANY_EMAIL
+from app.web.config import ALLOW_ANY_EMAIL
from app.shared.settings import Settings
from app.web.db.user_state import UserState
diff --git a/app/tests/web/db/test_crud.py b/app/tests/web/db/test_crud.py
index 625aee7..b49fefd 100644
--- a/app/tests/web/db/test_crud.py
+++ b/app/tests/web/db/test_crud.py
@@ -63,7 +63,7 @@ def test_data(db_session):
def test_get_archive(test_data, db_session):
from app.web.db import crud
- from app.shared.config import ALLOW_ANY_EMAIL
+ from app.web.config import ALLOW_ANY_EMAIL
# each author's archives work
assert (a0 := crud.get_archive(db_session, "archive-id-456-0", authors[0])) is not None
@@ -92,7 +92,7 @@ def test_get_archive(test_data, db_session):
def test_search_archives_by_url(test_data, db_session):
from app.web.db import crud
- from app.shared.config import ALLOW_ANY_EMAIL
+ 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")) == 34
@@ -138,7 +138,7 @@ def test_search_archives_by_url(test_data, db_session):
def test_search_archives_by_email(test_data, db_session):
- from app.shared.config import ALLOW_ANY_EMAIL
+ from app.web.config import ALLOW_ANY_EMAIL
from app.web.db import crud
# lower/upper case
@@ -161,7 +161,7 @@ def test_search_archives_by_email(test_data, db_session):
@patch("app.web.db.crud.DATABASE_QUERY_LIMIT", new=25)
def test_max_query_limit(test_data, db_session):
from app.web.db import crud
- from app.shared.config import ALLOW_ANY_EMAIL
+ from app.web.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
@@ -233,7 +233,7 @@ def test_count_by_users_since(test_data, db_session):
def test_is_user_in_group(test_data, db_session):
from app.web.db import crud
- from app.shared.config import ALLOW_ANY_EMAIL
+ from app.web.config import ALLOW_ANY_EMAIL
# see user-groups.test.yaml
test_pairs = [
diff --git a/app/tests/web/endpoints/test_default.py b/app/tests/web/endpoints/test_default.py
index 54bc14b..6de2f01 100644
--- a/app/tests/web/endpoints/test_default.py
+++ b/app/tests/web/endpoints/test_default.py
@@ -1,7 +1,7 @@
from unittest.mock import AsyncMock, MagicMock, patch
from fastapi.testclient import TestClient
import pytest
-from app.shared.config import VERSION
+from app.web.config import VERSION
from app.tests.web.db.test_crud import test_data
diff --git a/app/tests/web/endpoints/test_interoperability.py b/app/tests/web/endpoints/test_interoperability.py
index 64629ae..c3f8cb5 100644
--- a/app/tests/web/endpoints/test_interoperability.py
+++ b/app/tests/web/endpoints/test_interoperability.py
@@ -2,7 +2,7 @@ from datetime import datetime
import json
from unittest.mock import MagicMock, patch
-from app.shared.config import ALLOW_ANY_EMAIL
+from app.web.config import ALLOW_ANY_EMAIL
from app.web.db import crud
diff --git a/app/tests/web/test_security.py b/app/tests/web/test_security.py
index 3b45f2d..4a46823 100644
--- a/app/tests/web/test_security.py
+++ b/app/tests/web/test_security.py
@@ -4,7 +4,7 @@ from fastapi import HTTPException
from fastapi.security import HTTPAuthorizationCredentials
import pytest
-from app.shared.config import ALLOW_ANY_EMAIL
+from app.web.config import ALLOW_ANY_EMAIL
def test_secure_compare():
diff --git a/app/shared/config.py b/app/web/config.py
similarity index 100%
rename from app/shared/config.py
rename to app/web/config.py
diff --git a/app/web/db/crud.py b/app/web/db/crud.py
index d1aa92e..c5ea771 100644
--- a/app/web/db/crud.py
+++ b/app/web/db/crud.py
@@ -6,7 +6,7 @@ from loguru import logger
from datetime import datetime, timedelta
from sqlalchemy.ext.asyncio import AsyncSession
-from app.shared.config import ALLOW_ANY_EMAIL
+from app.web.config import ALLOW_ANY_EMAIL
from app.shared.db.database import get_db
from app.shared.db import models
from app.shared.settings import get_settings
diff --git a/app/web/endpoints/default.py b/app/web/endpoints/default.py
index 186b574..2535ae6 100644
--- a/app/web/endpoints/default.py
+++ b/app/web/endpoints/default.py
@@ -3,7 +3,7 @@ from typing import Dict
from fastapi import APIRouter, Depends, Request, HTTPException
from fastapi.responses import FileResponse, JSONResponse
-from app.shared.config import VERSION, BREAKING_CHANGES
+from app.web.config import VERSION, BREAKING_CHANGES
from app.shared.log import log_error
from app.web.db import crud
from app.shared.schemas import ActiveUser, UsageResponse
diff --git a/app/web/endpoints/interoperability.py b/app/web/endpoints/interoperability.py
index 2c7660a..81c9a76 100644
--- a/app/web/endpoints/interoperability.py
+++ b/app/web/endpoints/interoperability.py
@@ -7,7 +7,7 @@ from auto_archiver import Metadata
from sqlalchemy.orm import Session
from app.shared.aa_utils import get_all_urls
-from app.shared.config import ALLOW_ANY_EMAIL
+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
diff --git a/app/web/endpoints/url.py b/app/web/endpoints/url.py
index 7e3de02..98f905c 100644
--- a/app/web/endpoints/url.py
+++ b/app/web/endpoints/url.py
@@ -5,7 +5,7 @@ from datetime import datetime
from loguru import logger
from sqlalchemy.orm import Session
-from app.shared.config import ALLOW_ANY_EMAIL
+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
diff --git a/app/web/main.py b/app/web/main.py
index aabb4f4..2c7c9ee 100644
--- a/app/web/main.py
+++ b/app/web/main.py
@@ -17,7 +17,7 @@ from app.shared.task_messaging import get_celery
from app.web.db import crud
from app.web.security import get_user_auth, token_api_key_auth, get_token_or_user_auth
-from app.shared.config import VERSION, API_DESCRIPTION
+from app.web.config import VERSION, API_DESCRIPTION
from app.shared.db.database import get_db_dependency
from app.web.events import lifespan
from app.shared.settings import get_settings
diff --git a/app/web/security.py b/app/web/security.py
index 87bfe99..4e5214f 100644
--- a/app/web/security.py
+++ b/app/web/security.py
@@ -3,7 +3,7 @@ import requests, secrets
from fastapi import HTTPException, status, Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
-from app.shared.config import ALLOW_ANY_EMAIL
+from app.web.config import ALLOW_ANY_EMAIL
from app.shared.settings import get_settings
from app.shared.db.database import get_db
from app.web.db.user_state import UserState
From 5405b6e2f6e08a1764f043f6eec63eb57eb6ef31 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Tue, 11 Feb 2025 19:27:44 +0000
Subject: [PATCH 43/75] fix CI for poetry
---
.coveragerc | 3 +++
.github/workflows/ci.yml | 23 +++++++++++------------
app/tests/worker/test_worker_main.py | 1 -
3 files changed, 14 insertions(+), 13 deletions(-)
create mode 100644 .coveragerc
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/.github/workflows/ci.yml b/.github/workflows/ci.yml
index dbc4515..b110d92 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
-#TODO: fix working-directories here
+ 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 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/app/tests/worker/test_worker_main.py b/app/tests/worker/test_worker_main.py
index b8cb4cb..a4b7389 100644
--- a/app/tests/worker/test_worker_main.py
+++ b/app/tests/worker/test_worker_main.py
@@ -61,7 +61,6 @@ class Test_create_archive_task():
with pytest.raises(Exception) as e:
create_archive_task(self.archive.model_dump_json())
- assert "UNABLE TO archive" in str(e)
mock_orchestrator.feed_item.assert_called_once()
def mock_orchestrator_choice(self, m_load):
From 7a7474372c14dfc990b9bbc980f11b85e96e7c0a Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Tue, 11 Feb 2025 19:28:56 +0000
Subject: [PATCH 44/75] fix CI dir
---
.github/workflows/ci.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b110d92..9b63544 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -38,7 +38,7 @@ jobs:
run: echo "ENVIRONMENT_FILE=.env.test" >> $GITHUB_ENV
- name: Run tests with coverage
- run: poetry run coverage run -m pytest -v -ra --color=yes tests/
+ run: poetry run coverage run -m pytest -v -ra --color=yes app/tests/
- name: Report coverage
run: poetry run coverage report
\ No newline at end of file
From 0eef5aa9cee4699fa2c41bb859a4414e674e4de2 Mon Sep 17 00:00:00 2001
From: Miguel Sozinho Ramalho <19508417+msramalho@users.noreply.github.com>
Date: Tue, 11 Feb 2025 19:35:37 +0000
Subject: [PATCH 45/75] adds CI badge
---
README.md | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 4a6e688..18c553f 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
# Auto Archiver API
+[](https://github.com/bellingcat/auto-archiver-api/actions/workflows/ci.yaml)
+
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).
@@ -137,4 +139,4 @@ pipenv run coverage report
pipenv run coverage html
# > open/run server on htmlcov/index.html to navigate through line coverage
-```
\ No newline at end of file
+```
From 0834f5552029a8bc2a6a18e64af064f8c7e9d85b Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Tue, 11 Feb 2025 21:33:02 +0000
Subject: [PATCH 46/75] dynamic CELERY_BROKER_URL property in settings
---
app/shared/settings.py | 6 +++++-
app/shared/task_messaging.py | 2 ++
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/app/shared/settings.py b/app/shared/settings.py
index e962f1d..427de7e 100644
--- a/app/shared/settings.py
+++ b/app/shared/settings.py
@@ -33,7 +33,11 @@ class Settings(BaseSettings):
# redis
REDIS_PASSWORD: str = ""
- CELERY_BROKER_URL: str = "redis://localhost:6379"
+ @property
+ def CELERY_BROKER_URL(self)-> str:
+ if self.REDIS_PASSWORD:
+ return f"redis://:{self.REDIS_PASSWORD}@localhost:6379"
+ return "redis://localhost:6379"
REDIS_EXCEPTIONS_CHANNEL: str = "exceptions-channel"
# observability
diff --git a/app/shared/task_messaging.py b/app/shared/task_messaging.py
index 52dcba3..6f57352 100644
--- a/app/shared/task_messaging.py
+++ b/app/shared/task_messaging.py
@@ -15,4 +15,6 @@ def get_celery(name:str="") -> Celery:
def get_redis() -> redis.Redis:
+ from loguru import logger
+ logger.debug(get_settings().CELERY_BROKER_URL)
return redis.Redis.from_url(get_settings().CELERY_BROKER_URL)
From 17b3705b64106e539a92c89c8265333513d7490b Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Tue, 11 Feb 2025 22:50:00 +0000
Subject: [PATCH 47/75] introduces dynamic service_account emails read from the
group's orchestration files
---
.env.alembic | 2 +-
...dd_new_service_account_email_column_to_.py | 36 +++++++++++++++++++
app/shared/db/models.py | 1 +
app/shared/settings.py | 5 +--
app/shared/task_messaging.py | 2 --
app/shared/user_groups.py | 35 ++++++++++++++++--
app/tests/fake_service_account.json | 3 ++
app/tests/orchestration.test.yaml | 2 ++
app/tests/web/db/test_crud.py | 3 +-
app/web/db/crud.py | 8 +++--
app/web/db/user_state.py | 2 +-
docker-compose.yml | 4 +--
12 files changed, 88 insertions(+), 15 deletions(-)
create mode 100644 app/migrations/versions/63ac79df4ad0_add_new_service_account_email_column_to_.py
create mode 100644 app/tests/fake_service_account.json
diff --git a/.env.alembic b/.env.alembic
index 8691557..da29332 100644
--- a/.env.alembic
+++ b/.env.alembic
@@ -1,5 +1,5 @@
CHROME_APP_IDS='["1234567890"]'
ALLOWED_ORIGINS='["allowed"]'
BLOCKED_EMAILS='[]'
-DATABASE_PATH="sqlite:///./database/auto-archiver.db"
+DATABASE_PATH="sqlite:///./app/database/auto-archiver.db"
API_BEARER_TOKEN=THIS_API_TOKEN_SHOULD_NEVER_BE_USED
\ No newline at end of file
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/shared/db/models.py b/app/shared/db/models.py
index 5447531..0e12c7b 100644
--- a/app/shared/db/models.py
+++ b/app/shared/db/models.py
@@ -87,6 +87,7 @@ class Group(Base):
orchestrator = Column(String, default=None)
orchestrator_sheet = Column(String, default=None)
permissions = Column(JSON, default={})
+ service_account_email = Column(String, default=None)
domains = Column(JSON, default=[])
archives = relationship("Archive", back_populates="group")
diff --git a/app/shared/settings.py b/app/shared/settings.py
index 427de7e..039f4fd 100644
--- a/app/shared/settings.py
+++ b/app/shared/settings.py
@@ -33,11 +33,12 @@ class Settings(BaseSettings):
# redis
REDIS_PASSWORD: str = ""
+ REDIS_HOSTNAME: str = "localhost"
@property
def CELERY_BROKER_URL(self)-> str:
if self.REDIS_PASSWORD:
- return f"redis://:{self.REDIS_PASSWORD}@localhost:6379"
- return "redis://localhost:6379"
+ return f"redis://:{self.REDIS_PASSWORD}@{self.REDIS_HOSTNAME}:6379"
+ return f"redis://{self.REDIS_HOSTNAME}:6379"
REDIS_EXCEPTIONS_CHANNEL: str = "exceptions-channel"
# observability
diff --git a/app/shared/task_messaging.py b/app/shared/task_messaging.py
index 6f57352..52dcba3 100644
--- a/app/shared/task_messaging.py
+++ b/app/shared/task_messaging.py
@@ -15,6 +15,4 @@ def get_celery(name:str="") -> Celery:
def get_redis() -> redis.Redis:
- from loguru import logger
- logger.debug(get_settings().CELERY_BROKER_URL)
return redis.Redis.from_url(get_settings().CELERY_BROKER_URL)
diff --git a/app/shared/user_groups.py b/app/shared/user_groups.py
index 8647ed9..592e012 100644
--- a/app/shared/user_groups.py
+++ b/app/shared/user_groups.py
@@ -1,7 +1,8 @@
+import json
import os
import yaml
from loguru import logger
-from pydantic import BaseModel, field_validator, Field, model_validator
+from pydantic import BaseModel, computed_field, field_validator, Field, model_validator
from typing import Dict, List, Set
from typing_extensions import Self
@@ -74,11 +75,39 @@ class GroupModel(BaseModel):
permissions: GroupPermissions
@field_validator('orchestrator', 'orchestrator_sheet', mode='before')
- def validate_priority(cls, v):
+ 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)
@@ -137,4 +166,4 @@ class UserGroupModel(BaseModel):
class GroupInfo(GroupPermissions):
description: str = ""
- service_account_emails: list[str] = []
+ service_account_email: str = ""
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/app/tests/orchestration.test.yaml b/app/tests/orchestration.test.yaml
index cb79ea9..4ee1880 100644
--- a/app/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/app/tests/web/db/test_crud.py b/app/tests/web/db/test_crud.py
index b49fefd..dbe3dec 100644
--- a/app/tests/web/db/test_crud.py
+++ b/app/tests/web/db/test_crud.py
@@ -277,13 +277,14 @@ def test_upsert_group(test_data, db_session):
assert db_session.query(models.Group).count() == 4
- repeatable_params = ["desc 1", "orch.yaml", "sheet.yaml", {"read": ["all"]}, ["example.com"]]
+ 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
diff --git a/app/web/db/crud.py b/app/web/db/crud.py
index c5ea771..5e1f976 100644
--- a/app/web/db/crud.py
+++ b/app/web/db/crud.py
@@ -129,15 +129,16 @@ def get_user_groups(email: str) -> list[str]:
# --------------- INIT User-Groups
-def upsert_group(db: Session, group_name: str, description: str, orchestrator: str, orchestrator_sheet: str, permissions: dict, domains: list) -> models.Group:
+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, permissions=permissions, domains=domains)
+ 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()
@@ -180,7 +181,8 @@ def upsert_user_groups(db: Session):
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, json.loads(g.permissions.model_dump_json()), list(group_domains.get(group_id, [])))
+ logger.debug(f"GROUP {group_id} => {g.service_account_email}")
+ 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
diff --git a/app/web/db/user_state.py b/app/web/db/user_state.py
index dc7b112..a97df37 100644
--- a/app/web/db/user_state.py
+++ b/app/web/db/user_state.py
@@ -39,7 +39,7 @@ class UserState:
)
for group in self.user_groups:
if not group.permissions: continue
- self._permissions[group.id] = GroupInfo(**group.permissions, description=group.description)
+ self._permissions[group.id] = GroupInfo(**group.permissions, description=group.description, service_account_email=group.service_account_email)
return self._permissions
@property
diff --git a/docker-compose.yml b/docker-compose.yml
index 7c5cbd9..420ef6d 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -11,8 +11,8 @@ services:
restart: always
env_file: .env.prod
environment:
- CELERY_BROKER_URL: redis://:${REDIS_PASSWORD}@redis:6379/0
ENVIRONMENT_FILE: .env.prod
+ REDIS_HOSTNAME: redis
ports:
- "127.0.0.1:8004:8000"
#TODO: should prod have the --reload flag?
@@ -42,7 +42,7 @@ services:
- /var/run/docker.sock:/var/run/docker.sock
- crawls:/crawls # BROWSERTRIX_HOME_HOST:BROWSERTRIX_HOME_CONTAINER, do not change /crawls
environment:
- CELERY_BROKER_URL: redis://:${REDIS_PASSWORD}@redis:6379/0
+ REDIS_HOSTNAME: redis
ENVIRONMENT_FILE: .env.prod
WACZ_ENABLE_DOCKER: 1 # Enable calling docker from this container
BROWSERTRIX_HOME_HOST: auto-archiver-api_crawls
From 4f9d447ec77a1dcaaeea42082bd0c8624709102e Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Tue, 11 Feb 2025 23:30:45 +0000
Subject: [PATCH 48/75] optimizing compose and logging
---
.env.alembic | 2 +-
app/shared/log.py | 4 ++--
app/shared/task_messaging.py | 1 +
app/web/db/crud.py | 1 -
docker-compose.dev.yml | 4 ++--
docker-compose.yml | 12 +++++-------
{app/logs => logs}/.gitkeep | 0
7 files changed, 11 insertions(+), 13 deletions(-)
rename {app/logs => logs}/.gitkeep (100%)
diff --git a/.env.alembic b/.env.alembic
index da29332..8691557 100644
--- a/.env.alembic
+++ b/.env.alembic
@@ -1,5 +1,5 @@
CHROME_APP_IDS='["1234567890"]'
ALLOWED_ORIGINS='["allowed"]'
BLOCKED_EMAILS='[]'
-DATABASE_PATH="sqlite:///./app/database/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/app/shared/log.py b/app/shared/log.py
index d11b7a3..68587e2 100644
--- a/app/shared/log.py
+++ b/app/shared/log.py
@@ -3,8 +3,8 @@ from loguru import logger
# logging configurations
-logger.add("app/logs/api_logs.log", retention="30 days", rotation="3 days")
-logger.add("app/logs/error_logs.log", retention="30 days", level="ERROR")
+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 = ""):
diff --git a/app/shared/task_messaging.py b/app/shared/task_messaging.py
index 52dcba3..7f0f09e 100644
--- a/app/shared/task_messaging.py
+++ b/app/shared/task_messaging.py
@@ -11,6 +11,7 @@ def get_celery(name:str="") -> Celery:
name,
broker_url=get_settings().CELERY_BROKER_URL,
result_backend=get_settings().CELERY_BROKER_URL,
+ broker_connection_retry_on_startup=False
)
diff --git a/app/web/db/crud.py b/app/web/db/crud.py
index 5e1f976..be2a915 100644
--- a/app/web/db/crud.py
+++ b/app/web/db/crud.py
@@ -181,7 +181,6 @@ def upsert_user_groups(db: Session):
import json
# upsert groups and save a map of groupid -> dbobject
for group_id, g in ug.groups.items():
- logger.debug(f"GROUP {group_id} => {g.service_account_email}")
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()}
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index 5a8bc83..3a7129d 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -10,11 +10,11 @@ services:
- 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/app/database/auto-archiver.db
+ - 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=info --logfile=/aa-api/app/logs/celery.log
+ command: watchmedo auto-restart --patterns="*.py" --recursive --ignore-directories -- celery -- --app=app.worker.main.celery worker --loglevel=debug --logfile=/aa-api/logs/celery.log
restart: "no"
env_file: .env.dev
volumes:
diff --git a/docker-compose.yml b/docker-compose.yml
index 420ef6d..074b311 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -15,12 +15,10 @@ services:
REDIS_HOSTNAME: redis
ports:
- "127.0.0.1:8004:8000"
- #TODO: should prod have the --reload flag?
command: uvicorn app.web:app --factory --host 0.0.0.0
volumes:
- # - ./app:/app
- - ./app/logs:/aa-api/app/logs
- - ./app/database:/aa-api/app/database
+ - ./logs:/aa-api/logs
+ - ./app/database:/aa-api/database
depends_on:
- redis
healthcheck:
@@ -35,10 +33,10 @@ services:
dockerfile: worker.Dockerfile
restart: always
env_file: .env.prod
- command: celery --app=app.worker.main.celery worker --loglevel=info --logfile=/aa-api/app/logs/celery.log
+ command: celery --app=app.worker.main.celery worker --loglevel=warning --logfile=/aa-api/logs/celery.log
volumes:
- - ./app/logs:/aa-api/app/logs
- - ./app/database:/aa-api/app/database
+ - ./logs:/aa-api/logs
+ - ./app/database:/aa-api/database
- /var/run/docker.sock:/var/run/docker.sock
- crawls:/crawls # BROWSERTRIX_HOME_HOST:BROWSERTRIX_HOME_CONTAINER, do not change /crawls
environment:
diff --git a/app/logs/.gitkeep b/logs/.gitkeep
similarity index 100%
rename from app/logs/.gitkeep
rename to logs/.gitkeep
From fbfebd467192b467056cb4960235345772047ffb Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Wed, 12 Feb 2025 00:02:08 +0000
Subject: [PATCH 49/75] improves example files
---
.env.example | 35 ++++++++++++++++++++
.env.test | 3 +-
.example.env | 9 ------
app/shared/settings.py | 33 +++++++++----------
app/user-groups.example.yaml | 62 ++++++++++++++++++++++++++++++++++++
app/web/main.py | 2 +-
database/.gitkeep | 0
docker-compose.yml | 4 +--
8 files changed, 116 insertions(+), 32 deletions(-)
create mode 100644 .env.example
delete mode 100644 .example.env
create mode 100644 app/user-groups.example.yaml
create mode 100644 database/.gitkeep
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..46cc8cc
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,35 @@
+# 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_NOTIFY_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
+
diff --git a/.env.test b/.env.test
index f7da607..32318f0 100644
--- a/.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=app/tests/user-groups.test.yaml
-SHEET_ORCHESTRATION_YAML=app/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 b21cf10..0000000
--- a/.example.env
+++ /dev/null
@@ -1,9 +0,0 @@
-REDIS_PASSWORD=TODO
-
-DATABASE_PATH="sqlite:///./database/auto-archiver.db"
-USER_GROUPS_FILENAME=app/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/app/shared/settings.py b/app/shared/settings.py
index 039f4fd..da277f2 100644
--- a/app/shared/settings.py
+++ b/app/shared/settings.py
@@ -13,16 +13,7 @@ class Settings(BaseSettings):
# general
SERVE_LOCAL_ARCHIVE: str = ""
- USER_GROUPS_FILENAME: str = "user-groups.yaml"
- SHEET_ORCHESTRATION_YAML : str = "secrets/orchestration-sheet.yaml"
-
- # cronjobs
- #TODO: disable by default?
- CRON_ARCHIVE_SHEETS: bool = False
- CRON_DELETE_STALE_SHEETS: bool = True
- DELETE_STALE_SHEETS_DAYS: int = 14
- CRON_DELETE_SCHEDULED_ARCHIVES: bool = True
- DELETE_SCHEDULED_ARCHIVES_NOTIFY_DAYS: int = 14
+ USER_GROUPS_FILENAME: str = "app/user-groups.yaml"
# database
DATABASE_PATH: str
@@ -31,26 +22,32 @@ class Settings(BaseSettings):
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"
- REDIS_EXCEPTIONS_CHANNEL: str = "exceptions-channel"
+
+ # 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_NOTIFY_DAYS: int = 14
# 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()
-
# email configuration, if needed
MAIL_FROM: str = "noreply@bellingcat.com"
MAIL_FROM_NAME: str = "Bellingcat's Auto Archiver"
diff --git a/app/user-groups.example.yaml b/app/user-groups.example.yaml
new file mode 100644
index 0000000..ec67f86
--- /dev/null
+++ b/app/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/app/web/main.py b/app/web/main.py
index 2c7c9ee..3bd8962 100644
--- a/app/web/main.py
+++ b/app/web/main.py
@@ -58,7 +58,7 @@ def app_factory(settings = get_settings()):
# 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)])
- # TODO: recheck this for security, currently only needed for when local_storage is used
+ # TODO: recheck this for security, currently only needed for when local_storage is used in development
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", ".")
diff --git a/database/.gitkeep b/database/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/docker-compose.yml b/docker-compose.yml
index 074b311..723f225 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -18,7 +18,7 @@ services:
command: uvicorn app.web:app --factory --host 0.0.0.0
volumes:
- ./logs:/aa-api/logs
- - ./app/database:/aa-api/database
+ - ./database:/aa-api/database
depends_on:
- redis
healthcheck:
@@ -36,7 +36,7 @@ services:
command: celery --app=app.worker.main.celery worker --loglevel=warning --logfile=/aa-api/logs/celery.log
volumes:
- ./logs:/aa-api/logs
- - ./app/database:/aa-api/database
+ - ./database:/aa-api/database
- /var/run/docker.sock:/var/run/docker.sock
- crawls:/crawls # BROWSERTRIX_HOME_HOST:BROWSERTRIX_HOME_CONTAINER, do not change /crawls
environment:
From b654b6218ea7faf799ede1183a6d283798127601 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Wed, 12 Feb 2025 00:02:20 +0000
Subject: [PATCH 50/75] drops pipfile
---
Pipfile | 32 -
Pipfile.lock | 3047 --------------------------------------------------
2 files changed, 3079 deletions(-)
delete mode 100644 Pipfile
delete mode 100644 Pipfile.lock
diff --git a/Pipfile b/Pipfile
deleted file mode 100644
index 90244d0..0000000
--- a/Pipfile
+++ /dev/null
@@ -1,32 +0,0 @@
-[[source]]
-url = "https://pypi.org/simple"
-verify_ssl = true
-name = "pypi"
-
-[packages]
-oscrypto = {git = "https://github.com/wbond/oscrypto.git", ref = "d5f3437ed24257895ae1edd9e503cfb352e635a8"}
-celery = ">=5.0"
-fastapi = "*"
-jinja2 = "*"
-redis = "==3.5.3"
-requests = ">=2.25.1"
-uvicorn = ">=0.13.4"
-aiosqlite = "*"
-loguru = "*"
-sqlalchemy = "*"
-alembic = "*"
-fastapi-utils = "*"
-prometheus-fastapi-instrumentator = "*"
-auto-archiver = "*"
-pydantic-settings = "*"
-fastapi-mail = "*"
-
-[dev-packages]
-watchdog = "*"
-pytest = "*"
-httpx = "*"
-coverage = "*"
-pytest-asyncio = "*"
-
-[requires]
-python_version = "3.10"
diff --git a/Pipfile.lock b/Pipfile.lock
deleted file mode 100644
index e03b705..0000000
--- a/Pipfile.lock
+++ /dev/null
@@ -1,3047 +0,0 @@
-{
- "_meta": {
- "hash": {
- "sha256": "f03b75b94f11f10065e9fd4b4f107a78c37f880f4537b4b19fd0aaad7afa9ab7"
- },
- "pipfile-spec": 6,
- "requires": {
- "python_version": "3.10"
- },
- "sources": [
- {
- "name": "pypi",
- "url": "https://pypi.org/simple",
- "verify_ssl": true
- }
- ]
- },
- "default": {
- "aiohappyeyeballs": {
- "hashes": [
- "sha256:147ec992cf873d74f5062644332c539fcd42956dc69453fe5204195e560517e1",
- "sha256:9b05052f9042985d32ecbe4b59a77ae19c006a78f1344d7fdad69d28ded3d0b0"
- ],
- "markers": "python_version >= '3.9'",
- "version": "==2.4.6"
- },
- "aiohttp": {
- "hashes": [
- "sha256:0450ada317a65383b7cce9576096150fdb97396dcfe559109b403c7242faffef",
- "sha256:0b5263dcede17b6b0c41ef0c3ccce847d82a7da98709e75cf7efde3e9e3b5cae",
- "sha256:0d5176f310a7fe6f65608213cc74f4228e4f4ce9fd10bcb2bb6da8fc66991462",
- "sha256:0ed49efcd0dc1611378beadbd97beb5d9ca8fe48579fc04a6ed0844072261b6a",
- "sha256:145a73850926018ec1681e734cedcf2716d6a8697d90da11284043b745c286d5",
- "sha256:1987770fb4887560363b0e1a9b75aa303e447433c41284d3af2840a2f226d6e0",
- "sha256:246067ba0cf5560cf42e775069c5d80a8989d14a7ded21af529a4e10e3e0f0e6",
- "sha256:2c311e2f63e42c1bf86361d11e2c4a59f25d9e7aabdbdf53dc38b885c5435cdb",
- "sha256:2cee3b117a8d13ab98b38d5b6bdcd040cfb4181068d05ce0c474ec9db5f3c5bb",
- "sha256:2de1378f72def7dfb5dbd73d86c19eda0ea7b0a6873910cc37d57e80f10d64e1",
- "sha256:30f546358dfa0953db92ba620101fefc81574f87b2346556b90b5f3ef16e55ce",
- "sha256:34245498eeb9ae54c687a07ad7f160053911b5745e186afe2d0c0f2898a1ab8a",
- "sha256:392432a2dde22b86f70dd4a0e9671a349446c93965f261dbaecfaf28813e5c42",
- "sha256:3c0600bcc1adfaaac321422d615939ef300df81e165f6522ad096b73439c0f58",
- "sha256:4016e383f91f2814e48ed61e6bda7d24c4d7f2402c75dd28f7e1027ae44ea204",
- "sha256:40cd36749a1035c34ba8d8aaf221b91ca3d111532e5ccb5fa8c3703ab1b967ed",
- "sha256:413ad794dccb19453e2b97c2375f2ca3cdf34dc50d18cc2693bd5aed7d16f4b9",
- "sha256:4a93d28ed4b4b39e6f46fd240896c29b686b75e39cc6992692e3922ff6982b4c",
- "sha256:4ee84c2a22a809c4f868153b178fe59e71423e1f3d6a8cd416134bb231fbf6d3",
- "sha256:50c5c7b8aa5443304c55c262c5693b108c35a3b61ef961f1e782dd52a2f559c7",
- "sha256:525410e0790aab036492eeea913858989c4cb070ff373ec3bc322d700bdf47c1",
- "sha256:526c900397f3bbc2db9cb360ce9c35134c908961cdd0ac25b1ae6ffcaa2507ff",
- "sha256:54775858c7f2f214476773ce785a19ee81d1294a6bedc5cc17225355aab74802",
- "sha256:584096938a001378484aa4ee54e05dc79c7b9dd933e271c744a97b3b6f644957",
- "sha256:6130459189e61baac5a88c10019b21e1f0c6d00ebc770e9ce269475650ff7f73",
- "sha256:67453e603cea8e85ed566b2700efa1f6916aefbc0c9fcb2e86aaffc08ec38e78",
- "sha256:68d54234c8d76d8ef74744f9f9fc6324f1508129e23da8883771cdbb5818cbef",
- "sha256:6dfe7f984f28a8ae94ff3a7953cd9678550dbd2a1f9bda5dd9c5ae627744c78e",
- "sha256:74bd573dde27e58c760d9ca8615c41a57e719bff315c9adb6f2a4281a28e8798",
- "sha256:7603ca26d75b1b86160ce1bbe2787a0b706e592af5b2504e12caa88a217767b0",
- "sha256:76719dd521c20a58a6c256d058547b3a9595d1d885b830013366e27011ffe804",
- "sha256:7c3623053b85b4296cd3925eeb725e386644fd5bc67250b3bb08b0f144803e7b",
- "sha256:7e44eba534381dd2687be50cbd5f2daded21575242ecfdaf86bbeecbc38dae8e",
- "sha256:7fe3d65279bfbee8de0fb4f8c17fc4e893eed2dba21b2f680e930cc2b09075c5",
- "sha256:8340def6737118f5429a5df4e88f440746b791f8f1c4ce4ad8a595f42c980bd5",
- "sha256:84ede78acde96ca57f6cf8ccb8a13fbaf569f6011b9a52f870c662d4dc8cd854",
- "sha256:850ff6155371fd802a280f8d369d4e15d69434651b844bde566ce97ee2277420",
- "sha256:87a2e00bf17da098d90d4145375f1d985a81605267e7f9377ff94e55c5d769eb",
- "sha256:88d385b8e7f3a870146bf5ea31786ef7463e99eb59e31db56e2315535d811f55",
- "sha256:8a2fb742ef378284a50766e985804bd6adb5adb5aa781100b09befdbfa757b65",
- "sha256:8dc0fba9a74b471c45ca1a3cb6e6913ebfae416678d90529d188886278e7f3f6",
- "sha256:8fa1510b96c08aaad49303ab11f8803787c99222288f310a62f493faf883ede1",
- "sha256:8fd12d0f989c6099e7b0f30dc6e0d1e05499f3337461f0b2b0dadea6c64b89df",
- "sha256:9060addfa4ff753b09392efe41e6af06ea5dd257829199747b9f15bfad819460",
- "sha256:930ffa1925393381e1e0a9b82137fa7b34c92a019b521cf9f41263976666a0d6",
- "sha256:936d8a4f0f7081327014742cd51d320296b56aa6d324461a13724ab05f4b2933",
- "sha256:97fe431f2ed646a3b56142fc81d238abcbaff08548d6912acb0b19a0cadc146b",
- "sha256:9bd8695be2c80b665ae3f05cb584093a1e59c35ecb7d794d1edd96e8cc9201d7",
- "sha256:9dec0000d2d8621d8015c293e24589d46fa218637d820894cb7356c77eca3259",
- "sha256:a478aa11b328983c4444dacb947d4513cb371cd323f3845e53caeda6be5589d5",
- "sha256:a481a574af914b6e84624412666cbfbe531a05667ca197804ecc19c97b8ab1b0",
- "sha256:a4ac6a0f0f6402854adca4e3259a623f5c82ec3f0c049374133bcb243132baf9",
- "sha256:a5e69046f83c0d3cb8f0d5bd9b8838271b1bc898e01562a04398e160953e8eb9",
- "sha256:a7442662afebbf7b4c6d28cb7aab9e9ce3a5df055fc4116cc7228192ad6cb484",
- "sha256:aa8a8caca81c0a3e765f19c6953416c58e2f4cc1b84829af01dd1c771bb2f91f",
- "sha256:ab3247d58b393bda5b1c8f31c9edece7162fc13265334217785518dd770792b8",
- "sha256:b10a47e5390c4b30a0d58ee12581003be52eedd506862ab7f97da7a66805befb",
- "sha256:b34508f1cd928ce915ed09682d11307ba4b37d0708d1f28e5774c07a7674cac9",
- "sha256:b8d3bb96c147b39c02d3db086899679f31958c5d81c494ef0fc9ef5bb1359b3d",
- "sha256:b9d45dbb3aaec05cf01525ee1a7ac72de46a8c425cb75c003acd29f76b1ffe94",
- "sha256:bf4480a5438f80e0f1539e15a7eb8b5f97a26fe087e9828e2c0ec2be119a9f72",
- "sha256:c160a04283c8c6f55b5bf6d4cad59bb9c5b9c9cd08903841b25f1f7109ef1259",
- "sha256:c96a43822f1f9f69cc5c3706af33239489a6294be486a0447fb71380070d4d5f",
- "sha256:c9fd9dcf9c91affe71654ef77426f5cf8489305e1c66ed4816f5a21874b094b9",
- "sha256:cddb31f8474695cd61fc9455c644fc1606c164b93bff2490390d90464b4655df",
- "sha256:ce1bb21fc7d753b5f8a5d5a4bae99566386b15e716ebdb410154c16c91494d7f",
- "sha256:d1c031a7572f62f66f1257db37ddab4cb98bfaf9b9434a3b4840bf3560f5e788",
- "sha256:d589264dbba3b16e8951b6f145d1e6b883094075283dafcab4cdd564a9e353a0",
- "sha256:dc065a4285307607df3f3686363e7f8bdd0d8ab35f12226362a847731516e42c",
- "sha256:e10c440d142fa8b32cfdb194caf60ceeceb3e49807072e0dc3a8887ea80e8c16",
- "sha256:e3552fe98e90fdf5918c04769f338a87fa4f00f3b28830ea9b78b1bdc6140e0d",
- "sha256:e392804a38353900c3fd8b7cacbea5132888f7129f8e241915e90b85f00e3250",
- "sha256:e4cecdb52aaa9994fbed6b81d4568427b6002f0a91c322697a4bfcc2b2363f5a",
- "sha256:e5148ca8955affdfeb864aca158ecae11030e952b25b3ae15d4e2b5ba299bad2",
- "sha256:e6b2732ef3bafc759f653a98881b5b9cdef0716d98f013d376ee8dfd7285abf1",
- "sha256:ea756b5a7bac046d202a9a3889b9a92219f885481d78cd318db85b15cc0b7bcf",
- "sha256:edb69b9589324bdc40961cdf0657815df674f1743a8d5ad9ab56a99e4833cfdd",
- "sha256:f0203433121484b32646a5f5ea93ae86f3d9559d7243f07e8c0eab5ff8e3f70e",
- "sha256:f6a19bcab7fbd8f8649d6595624856635159a6527861b9cdc3447af288a00c00",
- "sha256:f752e80606b132140883bb262a457c475d219d7163d996dc9072434ffb0784c4",
- "sha256:f7914ab70d2ee8ab91c13e5402122edbc77821c66d2758abb53aabe87f013287"
- ],
- "markers": "python_version >= '3.9'",
- "version": "==3.11.12"
- },
- "aiosignal": {
- "hashes": [
- "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5",
- "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"
- ],
- "markers": "python_version >= '3.9'",
- "version": "==1.3.2"
- },
- "aiosmtplib": {
- "hashes": [
- "sha256:08fd840f9dbc23258025dca229e8a8f04d2ccf3ecb1319585615bfc7933f7f47",
- "sha256:8783059603a34834c7c90ca51103c3aa129d5922003b5ce98dbaa6d4440f10fc"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==3.0.2"
- },
- "aiosqlite": {
- "hashes": [
- "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3",
- "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0"
- ],
- "index": "pypi",
- "markers": "python_version >= '3.9'",
- "version": "==0.21.0"
- },
- "alembic": {
- "hashes": [
- "sha256:1acdd7a3a478e208b0503cd73614d5e4c6efafa4e73518bb60e4f2846a37b1c5",
- "sha256:496e888245a53adf1498fcab31713a469c65836f8de76e01399aa1c3e90dd213"
- ],
- "index": "pypi",
- "markers": "python_version >= '3.8'",
- "version": "==1.14.1"
- },
- "amqp": {
- "hashes": [
- "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2",
- "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432"
- ],
- "markers": "python_version >= '3.6'",
- "version": "==5.3.1"
- },
- "annotated-types": {
- "hashes": [
- "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53",
- "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==0.7.0"
- },
- "anyio": {
- "hashes": [
- "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a",
- "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"
- ],
- "markers": "python_version >= '3.9'",
- "version": "==4.8.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:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c",
- "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==5.0.1"
- },
- "attrs": {
- "hashes": [
- "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e",
- "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==25.1.0"
- },
- "authlib": {
- "hashes": [
- "sha256:30ead9ea4993cdbab821dc6e01e818362f92da290c04c7f6a1940f86507a790d",
- "sha256:edc29c3f6a3e72cd9e9f45fff67fc663a2c364022eb0371c003f22d5405915c1"
- ],
- "markers": "python_version >= '3.9'",
- "version": "==1.4.1"
- },
- "auto-archiver": {
- "hashes": [
- "sha256:3cee45b9a17feba214503eb1be4e8552e40cadbba128964585e0f53a45966fc8",
- "sha256:b9f1fb490fc268462325ec3f3c97c425a9c62dd0a2b4e58c771b64e8d29f0a87"
- ],
- "index": "pypi",
- "markers": "python_version >= '3.10'",
- "version": "==0.12.0"
- },
- "beautifulsoup4": {
- "hashes": [
- "sha256:1bd32405dacc920b42b83ba01644747ed77456a65760e285fbc47633ceddaf8b",
- "sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16"
- ],
- "markers": "python_full_version >= '3.7.0'",
- "version": "==4.13.3"
- },
- "billiard": {
- "hashes": [
- "sha256:12b641b0c539073fc8d3f5b8b7be998956665c4233c7c1fcd66a7e677c4fb36f",
- "sha256:40b59a4ac8806ba2c2369ea98d876bc6108b051c227baffd928c644d15d8f3cb"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==4.2.1"
- },
- "blinker": {
- "hashes": [
- "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf",
- "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"
- ],
- "markers": "python_version >= '3.9'",
- "version": "==1.9.0"
- },
- "boto3": {
- "hashes": [
- "sha256:0cf92ca0538ab115447e1c58050d43e1273e88c58ddfea2b6f133fdc508b400a",
- "sha256:b10583bf8bd35be1b4027ee7e26b7cdf2078c79eab18357fd602cecb6d39400b"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==1.36.16"
- },
- "botocore": {
- "hashes": [
- "sha256:10c6aa386ba1a9a0faef6bb5dbfc58fc2563a3c6b95352e86a583cd5f14b11f3",
- "sha256:aca0348ccd730332082489b6817fdf89e1526049adcf6e9c8c11c96dd9f42c03"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==1.36.16"
- },
- "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"
- ],
- "version": "==1.1.0"
- },
- "bs4": {
- "hashes": [
- "sha256:a48685c58f50fe127722417bae83fe6badf500d54b55f7e39ffe43b798653925",
- "sha256:abf8742c0805ef7f662dce4b51cca104cffe52b835238afc169142ab9b3fbccc"
- ],
- "version": "==0.0.2"
- },
- "cachetools": {
- "hashes": [
- "sha256:70f238fbba50383ef62e55c6aff6d9673175fe59f7c6782c7a0b9e38f4a9df95",
- "sha256:b76651fdc3b24ead3c648bbdeeb940c1b04d365b38b4af66788f9ec4a81d42bb"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==5.5.1"
- },
- "celery": {
- "hashes": [
- "sha256:369631eb580cf8c51a82721ec538684994f8277637edde2dfc0dacd73ed97f64",
- "sha256:504a19140e8d3029d5acad88330c541d4c3f64c789d85f94756762d8bca7e706"
- ],
- "index": "pypi",
- "markers": "python_version >= '3.8'",
- "version": "==5.4.0"
- },
- "certifi": {
- "hashes": [
- "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651",
- "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"
- ],
- "markers": "python_version >= '3.6'",
- "version": "==2025.1.31"
- },
- "certvalidator": {
- "hashes": [
- "sha256:77520b269f516d4fb0902998d5bd0eb3727fe153b659aa1cb828dcf12ea6b8de",
- "sha256:922d141c94393ab285ca34338e18dd4093e3ae330b1f278e96c837cb62cffaad"
- ],
- "version": "==0.11.1"
- },
- "cffi": {
- "hashes": [
- "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8",
- "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2",
- "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1",
- "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15",
- "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36",
- "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824",
- "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8",
- "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36",
- "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17",
- "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf",
- "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc",
- "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3",
- "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed",
- "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702",
- "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1",
- "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8",
- "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903",
- "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6",
- "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d",
- "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b",
- "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e",
- "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be",
- "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c",
- "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683",
- "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9",
- "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c",
- "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8",
- "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1",
- "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4",
- "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655",
- "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67",
- "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595",
- "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0",
- "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65",
- "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41",
- "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6",
- "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401",
- "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6",
- "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3",
- "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16",
- "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93",
- "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e",
- "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4",
- "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964",
- "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c",
- "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576",
- "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0",
- "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3",
- "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662",
- "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3",
- "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff",
- "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5",
- "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd",
- "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f",
- "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5",
- "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14",
- "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d",
- "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9",
- "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7",
- "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382",
- "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a",
- "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e",
- "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a",
- "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4",
- "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99",
- "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87",
- "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==1.17.1"
- },
- "charset-normalizer": {
- "hashes": [
- "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537",
- "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa",
- "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a",
- "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294",
- "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b",
- "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd",
- "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601",
- "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd",
- "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4",
- "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d",
- "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2",
- "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313",
- "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd",
- "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa",
- "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8",
- "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1",
- "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2",
- "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496",
- "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d",
- "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b",
- "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e",
- "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a",
- "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4",
- "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca",
- "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78",
- "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408",
- "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5",
- "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3",
- "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f",
- "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a",
- "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765",
- "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6",
- "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146",
- "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6",
- "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9",
- "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd",
- "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c",
- "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f",
- "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545",
- "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176",
- "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770",
- "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824",
- "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f",
- "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf",
- "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487",
- "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d",
- "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd",
- "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b",
- "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534",
- "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f",
- "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b",
- "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9",
- "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd",
- "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125",
- "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9",
- "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de",
- "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11",
- "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d",
- "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35",
- "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f",
- "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda",
- "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7",
- "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a",
- "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971",
- "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8",
- "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41",
- "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d",
- "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f",
- "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757",
- "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a",
- "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886",
- "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77",
- "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76",
- "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247",
- "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85",
- "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb",
- "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7",
- "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e",
- "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6",
- "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037",
- "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1",
- "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e",
- "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807",
- "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407",
- "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c",
- "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12",
- "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3",
- "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089",
- "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd",
- "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e",
- "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00",
- "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==3.4.1"
- },
- "click": {
- "hashes": [
- "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2",
- "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==8.1.8"
- },
- "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"
- },
- "cryptography": {
- "hashes": [
- "sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960",
- "sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a",
- "sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc",
- "sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a",
- "sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf",
- "sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1",
- "sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39",
- "sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406",
- "sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a",
- "sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a",
- "sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c",
- "sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be",
- "sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15",
- "sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2",
- "sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d",
- "sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157",
- "sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003",
- "sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248",
- "sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a",
- "sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec",
- "sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309",
- "sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7",
- "sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==41.0.7"
- },
- "dataclasses-json": {
- "hashes": [
- "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a",
- "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0"
- ],
- "markers": "python_version >= '3.7' and python_version < '4.0'",
- "version": "==0.6.7"
- },
- "dateparser": {
- "hashes": [
- "sha256:7e4919aeb48481dbfc01ac9683c8e20bfe95bb715a38c1e9f6af889f4f30ccc3",
- "sha256:bdcac262a467e6260030040748ad7c10d6bacd4f3b9cdb4cfd2251939174508c"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==1.2.1"
- },
- "dnspython": {
- "hashes": [
- "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86",
- "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1"
- ],
- "markers": "python_version >= '3.9'",
- "version": "==2.7.0"
- },
- "email-validator": {
- "hashes": [
- "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631",
- "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==2.2.0"
- },
- "exceptiongroup": {
- "hashes": [
- "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b",
- "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==1.2.2"
- },
- "fastapi": {
- "hashes": [
- "sha256:0ce9111231720190473e222cdf0f07f7206ad7e53ea02beb1d2dc36e2f0741e9",
- "sha256:753a96dd7e036b34eeef8babdfcfe3f28ff79648f86551eb36bfc1b0bf4a8cbf"
- ],
- "index": "pypi",
- "markers": "python_version >= '3.8'",
- "version": "==0.115.8"
- },
- "fastapi-mail": {
- "hashes": [
- "sha256:04bde1005c624f42dfc0a9c1e313fcc544499fdd6b3531e606c500d80ac2ffcb",
- "sha256:3525cf342ff91f6bcb3298570d1783498082e586957f668ee4164a0aab6ec743"
- ],
- "index": "pypi",
- "markers": "python_full_version >= '3.8.1' and python_version < '4.0'",
- "version": "==1.4.2"
- },
- "fastapi-utils": {
- "hashes": [
- "sha256:6c4d507a76bab9a016cee0c4fa3a4638c636b2b2689e39c62254b1b2e4e81825",
- "sha256:eca834e80c09f85df30004fe5e861981262b296f60c93d5a1a1416fe4c784140"
- ],
- "index": "pypi",
- "markers": "python_version >= '3.8' and python_version < '4.0'",
- "version": "==0.8.0"
- },
- "ffmpeg-python": {
- "hashes": [
- "sha256:65225db34627c578ef0e11c8b1eb528bb35e024752f6f10b78c011f6f64c4127",
- "sha256:ac441a0404e053f8b6a1113a77c0f452f1cfc62f6344a769475ffdc0f56c23c5"
- ],
- "version": "==0.2.0"
- },
- "filelock": {
- "hashes": [
- "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338",
- "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e"
- ],
- "markers": "python_version >= '3.9'",
- "version": "==3.17.0"
- },
- "flask": {
- "hashes": [
- "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac",
- "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136"
- ],
- "markers": "python_version >= '3.9'",
- "version": "==3.1.0"
- },
- "frozenlist": {
- "hashes": [
- "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e",
- "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf",
- "sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6",
- "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a",
- "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d",
- "sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f",
- "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28",
- "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b",
- "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9",
- "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2",
- "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec",
- "sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2",
- "sha256:17dcc32fc7bda7ce5875435003220a457bcfa34ab7924a49a1c19f55b6ee185c",
- "sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336",
- "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4",
- "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d",
- "sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b",
- "sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c",
- "sha256:2150cc6305a2c2ab33299453e2968611dacb970d2283a14955923062c8d00b10",
- "sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08",
- "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942",
- "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8",
- "sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f",
- "sha256:2d0da8bbec082bf6bf18345b180958775363588678f64998c2b7609e34719b10",
- "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5",
- "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6",
- "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21",
- "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c",
- "sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d",
- "sha256:374ca2dabdccad8e2a76d40b1d037f5bd16824933bf7bcea3e59c891fd4a0923",
- "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608",
- "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de",
- "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17",
- "sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0",
- "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f",
- "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641",
- "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c",
- "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a",
- "sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0",
- "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9",
- "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab",
- "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f",
- "sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3",
- "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a",
- "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784",
- "sha256:73f2e31ea8dd7df61a359b731716018c2be196e5bb3b74ddba107f694fbd7604",
- "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d",
- "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5",
- "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03",
- "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e",
- "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953",
- "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee",
- "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d",
- "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817",
- "sha256:828afae9f17e6de596825cf4228ff28fbdf6065974e5ac1410cecc22f699d2b3",
- "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039",
- "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f",
- "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9",
- "sha256:91d6c171862df0a6c61479d9724f22efb6109111017c87567cfeb7b5d1449fdf",
- "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76",
- "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba",
- "sha256:97160e245ea33d8609cd2b8fd997c850b56db147a304a262abc2b3be021a9171",
- "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb",
- "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439",
- "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631",
- "sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972",
- "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d",
- "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869",
- "sha256:a72b7a6e3cd2725eff67cd64c8f13335ee18fc3c7befc05aed043d24c7b9ccb9",
- "sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411",
- "sha256:b97f7b575ab4a8af9b7bc1d2ef7f29d3afee2226bd03ca3875c16451ad5a7723",
- "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2",
- "sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b",
- "sha256:c16d2fa63e0800723139137d667e1056bee1a1cf7965153d2d104b62855e9b99",
- "sha256:c1fac3e2ace2eb1052e9f7c7db480818371134410e1f5c55d65e8f3ac6d1407e",
- "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840",
- "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3",
- "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb",
- "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3",
- "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0",
- "sha256:dd94994fc91a6177bfaafd7d9fd951bc8689b0a98168aa26b5f543868548d3ca",
- "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45",
- "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e",
- "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f",
- "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5",
- "sha256:f1577515d35ed5649d52ab4319db757bb881ce3b2b796d7283e6634d99ace307",
- "sha256:f1e6540b7fa044eee0bb5111ada694cf3dc15f2b0347ca125ee9ca984d5e9e6e",
- "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2",
- "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778",
- "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a",
- "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30",
- "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==1.5.0"
- },
- "future": {
- "hashes": [
- "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216",
- "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05"
- ],
- "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'",
- "version": "==1.0.0"
- },
- "google-api-core": {
- "hashes": [
- "sha256:bc78d608f5a5bf853b80bd70a795f703294de656c096c0968320830a4bc280f1",
- "sha256:f8b36f5456ab0dd99a1b693a40a31d1e7757beea380ad1b38faaf8941eae9d8a"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==2.24.1"
- },
- "google-api-python-client": {
- "hashes": [
- "sha256:63d61fb3e4cf3fb31a70a87f45567c22f6dfe87bbfa27252317e3e2c42900db4",
- "sha256:a8ccafaecfa42d15d5b5c3134ced8de08380019717fc9fb1ed510ca58eca3b7e"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==2.160.0"
- },
- "google-auth": {
- "hashes": [
- "sha256:8285113607d3b80a3f1543b75962447ba8a09fe85783432a784fdeef6ac094c4",
- "sha256:e7dae6694313f434a2727bf2906f27ad259bae090d7aa896590d86feec3d9d4a"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==2.38.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:c3e7b33d15fdca5374cc0a7346dd92ffa847425cc4ea941d970f13680052ec8c",
- "sha256:d7abcd75fabb2e0ec9f74466401f6c119a0b498e27370e9be4c94cb7e382b8ed"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==1.66.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.7'",
- "version": "==3.1.1"
- },
- "gspread": {
- "hashes": [
- "sha256:b8eec27de7cadb338bb1b9f14a9be168372dee8965c0da32121816b5050ac1de",
- "sha256:c34781c426031a243ad154952b16f21ac56a5af90687885fbee3d1fba5280dcd"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==6.1.4"
- },
- "h11": {
- "hashes": [
- "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d",
- "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==0.14.0"
- },
- "httpcore": {
- "hashes": [
- "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c",
- "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==1.0.7"
- },
- "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:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc",
- "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==0.28.1"
- },
- "idna": {
- "hashes": [
- "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9",
- "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"
- ],
- "markers": "python_version >= '3.6'",
- "version": "==3.10"
- },
- "instaloader": {
- "hashes": [
- "sha256:43356f696231621ea5a93354f9a4578124fe131940ee9aa1e83c20f57e18f26d",
- "sha256:a41a7372a18fb096b3ed545469479884de9cf768e12020c0e0e67c488d9d599c"
- ],
- "markers": "python_version >= '3.9'",
- "version": "==4.14.1"
- },
- "itsdangerous": {
- "hashes": [
- "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef",
- "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==2.2.0"
- },
- "jinja2": {
- "hashes": [
- "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb",
- "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"
- ],
- "index": "pypi",
- "markers": "python_version >= '3.7'",
- "version": "==3.1.5"
- },
- "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"
- },
- "kombu": {
- "hashes": [
- "sha256:14212f5ccf022fc0a70453bb025a1dcc32782a588c49ea866884047d66e14763",
- "sha256:eef572dd2fd9fc614b37580e3caeafdd5af46c1eff31e7fba89138cdb406f2cf"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==5.4.2"
- },
- "loguru": {
- "hashes": [
- "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6",
- "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c"
- ],
- "index": "pypi",
- "markers": "python_version >= '3.5' and python_version < '4.0'",
- "version": "==0.7.3"
- },
- "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:95920acccb578427a9aa38e37a186b1e43156c87260d7ba18ca63aa4c7cbd3a1",
- "sha256:b5d65ff3462870feec922dbccf38f6efb44e5714d7b593a656be86663d8600ac"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==1.3.9"
- },
- "markdown-it-py": {
- "hashes": [
- "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1",
- "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==3.0.0"
- },
- "markupsafe": {
- "hashes": [
- "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4",
- "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30",
- "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0",
- "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9",
- "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396",
- "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13",
- "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028",
- "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca",
- "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557",
- "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832",
- "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0",
- "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b",
- "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579",
- "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a",
- "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c",
- "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff",
- "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c",
- "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22",
- "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094",
- "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb",
- "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e",
- "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5",
- "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a",
- "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d",
- "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a",
- "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b",
- "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8",
- "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225",
- "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c",
- "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144",
- "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f",
- "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87",
- "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d",
- "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93",
- "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf",
- "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158",
- "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84",
- "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb",
- "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48",
- "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171",
- "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c",
- "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6",
- "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd",
- "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d",
- "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1",
- "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d",
- "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca",
- "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a",
- "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29",
- "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe",
- "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798",
- "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c",
- "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8",
- "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f",
- "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f",
- "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a",
- "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178",
- "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0",
- "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79",
- "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430",
- "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"
- ],
- "markers": "python_version >= '3.9'",
- "version": "==3.0.2"
- },
- "marshmallow": {
- "hashes": [
- "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c",
- "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6"
- ],
- "markers": "python_version >= '3.9'",
- "version": "==3.26.1"
- },
- "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"
- },
- "multidict": {
- "hashes": [
- "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f",
- "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056",
- "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761",
- "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3",
- "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b",
- "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6",
- "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748",
- "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966",
- "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f",
- "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1",
- "sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6",
- "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada",
- "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305",
- "sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2",
- "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d",
- "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a",
- "sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef",
- "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c",
- "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb",
- "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60",
- "sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6",
- "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4",
- "sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478",
- "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81",
- "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7",
- "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56",
- "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3",
- "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6",
- "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30",
- "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb",
- "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506",
- "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0",
- "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925",
- "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c",
- "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6",
- "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e",
- "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95",
- "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2",
- "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133",
- "sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2",
- "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa",
- "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3",
- "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3",
- "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436",
- "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657",
- "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581",
- "sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492",
- "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43",
- "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2",
- "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2",
- "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926",
- "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057",
- "sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc",
- "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80",
- "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255",
- "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1",
- "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972",
- "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53",
- "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1",
- "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423",
- "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a",
- "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160",
- "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c",
- "sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd",
- "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa",
- "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5",
- "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b",
- "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa",
- "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef",
- "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44",
- "sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4",
- "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156",
- "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753",
- "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28",
- "sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d",
- "sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a",
- "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304",
- "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008",
- "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429",
- "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72",
- "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399",
- "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3",
- "sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392",
- "sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167",
- "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c",
- "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774",
- "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351",
- "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76",
- "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875",
- "sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd",
- "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28",
- "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==6.1.0"
- },
- "mutagen": {
- "hashes": [
- "sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99",
- "sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==1.47.0"
- },
- "mypy-extensions": {
- "hashes": [
- "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d",
- "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"
- ],
- "markers": "python_version >= '3.5'",
- "version": "==1.0.0"
- },
- "numpy": {
- "hashes": [
- "sha256:02935e2c3c0c6cbe9c7955a8efa8908dd4221d7755644c59d1bba28b94fd334f",
- "sha256:0349b025e15ea9d05c3d63f9657707a4e1d471128a3b1d876c095f328f8ff7f0",
- "sha256:09d6a2032faf25e8d0cadde7fd6145118ac55d2740132c1d845f98721b5ebcfd",
- "sha256:0bc61b307655d1a7f9f4b043628b9f2b721e80839914ede634e3d485913e1fb2",
- "sha256:0eec19f8af947a61e968d5429f0bd92fec46d92b0008d0a6685b40d6adf8a4f4",
- "sha256:106397dbbb1896f99e044efc90360d098b3335060375c26aa89c0d8a97c5f648",
- "sha256:128c41c085cab8a85dc29e66ed88c05613dccf6bc28b3866cd16050a2f5448be",
- "sha256:149d1113ac15005652e8d0d3f6fd599360e1a708a4f98e43c9c77834a28238cb",
- "sha256:159ff6ee4c4a36a23fe01b7c3d07bd8c14cc433d9720f977fcd52c13c0098160",
- "sha256:22ea3bb552ade325530e72a0c557cdf2dea8914d3a5e1fecf58fa5dbcc6f43cd",
- "sha256:23ae9f0c2d889b7b2d88a3791f6c09e2ef827c2446f1c4a3e3e76328ee4afd9a",
- "sha256:250c16b277e3b809ac20d1f590716597481061b514223c7badb7a0f9993c7f84",
- "sha256:2ec6c689c61df613b783aeb21f945c4cbe6c51c28cb70aae8430577ab39f163e",
- "sha256:2ffbb1acd69fdf8e89dd60ef6182ca90a743620957afb7066385a7bbe88dc748",
- "sha256:3074634ea4d6df66be04f6728ee1d173cfded75d002c75fac79503a880bf3825",
- "sha256:356ca982c188acbfa6af0d694284d8cf20e95b1c3d0aefa8929376fea9146f60",
- "sha256:3fbe72d347fbc59f94124125e73fc4976a06927ebc503ec5afbfb35f193cd957",
- "sha256:40c7ff5da22cd391944a28c6a9c638a5eef77fcf71d6e3a79e1d9d9e82752715",
- "sha256:41184c416143defa34cc8eb9d070b0a5ba4f13a0fa96a709e20584638254b317",
- "sha256:451e854cfae0febe723077bd0cf0a4302a5d84ff25f0bfece8f29206c7bed02e",
- "sha256:4525b88c11906d5ab1b0ec1f290996c0020dd318af8b49acaa46f198b1ffc283",
- "sha256:463247edcee4a5537841d5350bc87fe8e92d7dd0e8c71c995d2c6eecb8208278",
- "sha256:4dbd80e453bd34bd003b16bd802fac70ad76bd463f81f0c518d1245b1c55e3d9",
- "sha256:57b4012e04cc12b78590a334907e01b3a85efb2107df2b8733ff1ed05fce71de",
- "sha256:5a8c863ceacae696aff37d1fd636121f1a512117652e5dfb86031c8d84836369",
- "sha256:5acea83b801e98541619af398cc0109ff48016955cc0818f478ee9ef1c5c3dcb",
- "sha256:642199e98af1bd2b6aeb8ecf726972d238c9877b0f6e8221ee5ab945ec8a2189",
- "sha256:64bd6e1762cd7f0986a740fee4dff927b9ec2c5e4d9a28d056eb17d332158014",
- "sha256:6d9fc9d812c81e6168b6d405bf00b8d6739a7f72ef22a9214c4241e0dc70b323",
- "sha256:7079129b64cb78bdc8d611d1fd7e8002c0a2565da6a47c4df8062349fee90e3e",
- "sha256:7dca87ca328f5ea7dafc907c5ec100d187911f94825f8700caac0b3f4c384b49",
- "sha256:860fd59990c37c3ef913c3ae390b3929d005243acca1a86facb0773e2d8d9e50",
- "sha256:8e6da5cffbbe571f93588f562ed130ea63ee206d12851b60819512dd3e1ba50d",
- "sha256:8ec0636d3f7d68520afc6ac2dc4b8341ddb725039de042faf0e311599f54eb37",
- "sha256:9491100aba630910489c1d0158034e1c9a6546f0b1340f716d522dc103788e39",
- "sha256:97b974d3ba0fb4612b77ed35d7627490e8e3dff56ab41454d9e8b23448940576",
- "sha256:995f9e8181723852ca458e22de5d9b7d3ba4da3f11cc1cb113f093b271d7965a",
- "sha256:9dd47ff0cb2a656ad69c38da850df3454da88ee9a6fde0ba79acceee0e79daba",
- "sha256:9fad446ad0bc886855ddf5909cbf8cb5d0faa637aaa6277fb4b19ade134ab3c7",
- "sha256:a972cec723e0563aa0823ee2ab1df0cb196ed0778f173b381c871a03719d4826",
- "sha256:ac9bea18d6d58a995fac1b2cb4488e17eceeac413af014b1dd26170b766d8467",
- "sha256:b0531f0b0e07643eb089df4c509d30d72c9ef40defa53e41363eca8a8cc61495",
- "sha256:b208cfd4f5fe34e1535c08983a1a6803fdbc7a1e86cf13dd0c61de0b51a0aadc",
- "sha256:b3482cb7b3325faa5f6bc179649406058253d91ceda359c104dac0ad320e1391",
- "sha256:b6fb9c32a91ec32a689ec6410def76443e3c750e7cfc3fb2206b985ffb2b85f0",
- "sha256:b78ea78450fd96a498f50ee096f69c75379af5138f7881a51355ab0e11286c97",
- "sha256:bd249bc894af67cbd8bad2c22e7cbcd46cf87ddfca1f1289d1e7e54868cc785c",
- "sha256:c7d1fd447e33ee20c1f33f2c8e6634211124a9aabde3c617687d8b739aa69eac",
- "sha256:d0bbe7dd86dca64854f4b6ce2ea5c60b51e36dfd597300057cf473d3615f2369",
- "sha256:d6d6a0910c3b4368d89dde073e630882cdb266755565155bc33520283b2d9df8",
- "sha256:da1eeb460ecce8d5b8608826595c777728cdf28ce7b5a5a8c8ac8d949beadcf2",
- "sha256:e0c8854b09bc4de7b041148d8550d3bd712b5c21ff6a8ed308085f190235d7ff",
- "sha256:e0d4142eb40ca6f94539e4db929410f2a46052a0fe7a2c1c59f6179c39938d2a",
- "sha256:e9e82dcb3f2ebbc8cb5ce1102d5f1c5ed236bf8a11730fb45ba82e2841ec21df",
- "sha256:ed6906f61834d687738d25988ae117683705636936cc605be0bb208b23df4d8f"
- ],
- "markers": "python_version >= '3.10'",
- "version": "==2.2.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": {
- "git": "https://github.com/wbond/oscrypto.git",
- "ref": "d5f3437ed24257895ae1edd9e503cfb352e635a8"
- },
- "outcome": {
- "hashes": [
- "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8",
- "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==1.3.0.post0"
- },
- "packaging": {
- "hashes": [
- "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759",
- "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==24.2"
- },
- "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:015c6e863faa4779251436db398ae75051469f7c903b043a48f078e437656f83",
- "sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96",
- "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65",
- "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a",
- "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352",
- "sha256:3362c6ca227e65c54bf71a5f88b3d4565ff1bcbc63ae72c34b07bbb1cc59a43f",
- "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20",
- "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c",
- "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114",
- "sha256:3a5fe20a7b66e8135d7fd617b13272626a28278d0e578c98720d9ba4b2439d49",
- "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91",
- "sha256:4637b88343166249fe8aa94e7c4a62a180c4b3898283bb5d3d2fd5fe10d8e4e0",
- "sha256:4db853948ce4e718f2fc775b75c37ba2efb6aaea41a1a5fc57f0af59eee774b2",
- "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5",
- "sha256:54251ef02a2309b5eec99d151ebf5c9904b77976c8abdcbce7891ed22df53884",
- "sha256:54ce1c9a16a9561b6d6d8cb30089ab1e5eb66918cb47d457bd996ef34182922e",
- "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c",
- "sha256:5bb94705aea800051a743aa4874bb1397d4695fb0583ba5e425ee0328757f196",
- "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756",
- "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861",
- "sha256:73ddde795ee9b06257dac5ad42fcb07f3b9b813f8c1f7f870f402f4dc54b5269",
- "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1",
- "sha256:7d33d2fae0e8b170b6a6c57400e077412240f6f5bb2a342cf1ee512a787942bb",
- "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a",
- "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081",
- "sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1",
- "sha256:89dbdb3e6e9594d512780a5a1c42801879628b38e3efc7038094430844e271d8",
- "sha256:8c730dc3a83e5ac137fbc92dfcfe1511ce3b2b5d7578315b63dbbb76f7f51d90",
- "sha256:8e275ee4cb11c262bd108ab2081f750db2a1c0b8c12c1897f27b160c8bd57bbc",
- "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5",
- "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1",
- "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3",
- "sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35",
- "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f",
- "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c",
- "sha256:a07dba04c5e22824816b2615ad7a7484432d7f540e6fa86af60d2de57b0fcee2",
- "sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2",
- "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf",
- "sha256:a76da0a31da6fcae4210aa94fd779c65c75786bc9af06289cd1c184451ef7a65",
- "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b",
- "sha256:a8d65b38173085f24bc07f8b6c505cbb7418009fa1a1fcb111b1f4961814a442",
- "sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2",
- "sha256:ab8a209b8485d3db694fa97a896d96dd6533d63c22829043fd9de627060beade",
- "sha256:abc56501c3fd148d60659aae0af6ddc149660469082859fa7b066a298bde9482",
- "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe",
- "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc",
- "sha256:b20be51b37a75cc54c2c55def3fa2c65bb94ba859dde241cd0a4fd302de5ae0a",
- "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec",
- "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3",
- "sha256:b6123aa4a59d75f06e9dd3dac5bf8bc9aa383121bb3dd9a7a612e05eabc9961a",
- "sha256:bd165131fd51697e22421d0e467997ad31621b74bfc0b75956608cb2906dda07",
- "sha256:bf902d7413c82a1bfa08b06a070876132a5ae6b2388e2712aab3a7cbc02205c6",
- "sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f",
- "sha256:c1eec9d950b6fe688edee07138993e54ee4ae634c51443cfb7c1e7613322718e",
- "sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192",
- "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0",
- "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6",
- "sha256:d3d8da4a631471dfaf94c10c85f5277b1f8e42ac42bade1ac67da4b4a7359b73",
- "sha256:d44ff19eea13ae4acdaaab0179fa68c0c6f2f45d66a4d8ec1eda7d6cecbcc15f",
- "sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6",
- "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547",
- "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9",
- "sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457",
- "sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8",
- "sha256:e267b0ed063341f3e60acd25c05200df4193e15a4a5807075cd71225a2386e26",
- "sha256:e5449ca63da169a2e6068dd0e2fcc8d91f9558aba89ff6d02121ca8ab11e79e5",
- "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab",
- "sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070",
- "sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71",
- "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9",
- "sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761"
- ],
- "markers": "python_version >= '3.9'",
- "version": "==11.1.0"
- },
- "prometheus-client": {
- "hashes": [
- "sha256:252505a722ac04b0456be05c05f75f45d760c2911ffc45f2a06bcaed9f3ae3fb",
- "sha256:594b45c410d6f4f8888940fe80b5cc2521b305a1fafe1c58609ef715a001f301"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==0.21.1"
- },
- "prometheus-fastapi-instrumentator": {
- "hashes": [
- "sha256:8a4d8fb13dbe19d2882ac6af9ce236e4e1f98dc48e3fa44fe88d8e23ac3c953f",
- "sha256:975e39992acb7a112758ff13ba95317e6c54d1bbf605f9156f31ac9f2800c32d"
- ],
- "index": "pypi",
- "markers": "python_version >= '3.8'",
- "version": "==7.0.2"
- },
- "prompt-toolkit": {
- "hashes": [
- "sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab",
- "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198"
- ],
- "markers": "python_full_version >= '3.8.0'",
- "version": "==3.0.50"
- },
- "propcache": {
- "hashes": [
- "sha256:03ff9d3f665769b2a85e6157ac8b439644f2d7fd17615a82fa55739bc97863f4",
- "sha256:049324ee97bb67285b49632132db351b41e77833678432be52bdd0289c0e05e4",
- "sha256:081a430aa8d5e8876c6909b67bd2d937bfd531b0382d3fdedb82612c618bc41a",
- "sha256:0f022d381747f0dfe27e99d928e31bc51a18b65bb9e481ae0af1380a6725dd1f",
- "sha256:12d1083f001ace206fe34b6bdc2cb94be66d57a850866f0b908972f90996b3e9",
- "sha256:14d86fe14b7e04fa306e0c43cdbeebe6b2c2156a0c9ce56b815faacc193e320d",
- "sha256:160291c60081f23ee43d44b08a7e5fb76681221a8e10b3139618c5a9a291b84e",
- "sha256:1672137af7c46662a1c2be1e8dc78cb6d224319aaa40271c9257d886be4363a6",
- "sha256:19a0f89a7bb9d8048d9c4370c9c543c396e894c76be5525f5e1ad287f1750ddf",
- "sha256:1ac2f5fe02fa75f56e1ad473f1175e11f475606ec9bd0be2e78e4734ad575034",
- "sha256:1cd9a1d071158de1cc1c71a26014dcdfa7dd3d5f4f88c298c7f90ad6f27bb46d",
- "sha256:1ffc3cca89bb438fb9c95c13fc874012f7b9466b89328c3c8b1aa93cdcfadd16",
- "sha256:297878dc9d0a334358f9b608b56d02e72899f3b8499fc6044133f0d319e2ec30",
- "sha256:2d3af2e79991102678f53e0dbf4c35de99b6b8b58f29a27ca0325816364caaba",
- "sha256:30b43e74f1359353341a7adb783c8f1b1c676367b011709f466f42fda2045e95",
- "sha256:3156628250f46a0895f1f36e1d4fbe062a1af8718ec3ebeb746f1d23f0c5dc4d",
- "sha256:31f5af773530fd3c658b32b6bdc2d0838543de70eb9a2156c03e410f7b0d3aae",
- "sha256:3935bfa5fede35fb202c4b569bb9c042f337ca4ff7bd540a0aa5e37131659348",
- "sha256:39d51fbe4285d5db5d92a929e3e21536ea3dd43732c5b177c7ef03f918dff9f2",
- "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64",
- "sha256:4160d9283bd382fa6c0c2b5e017acc95bc183570cd70968b9202ad6d8fc48dce",
- "sha256:4a571d97dbe66ef38e472703067021b1467025ec85707d57e78711c085984e54",
- "sha256:4e6281aedfca15301c41f74d7005e6e3f4ca143584ba696ac69df4f02f40d629",
- "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54",
- "sha256:556fc6c10989f19a179e4321e5d678db8eb2924131e64652a51fe83e4c3db0e1",
- "sha256:574faa3b79e8ebac7cb1d7930f51184ba1ccf69adfdec53a12f319a06030a68b",
- "sha256:58791550b27d5488b1bb52bc96328456095d96206a250d28d874fafe11b3dfaf",
- "sha256:5b750a8e5a1262434fb1517ddf64b5de58327f1adc3524a5e44c2ca43305eb0b",
- "sha256:5d97151bc92d2b2578ff7ce779cdb9174337390a535953cbb9452fb65164c587",
- "sha256:5eee736daafa7af6d0a2dc15cc75e05c64f37fc37bafef2e00d77c14171c2097",
- "sha256:6445804cf4ec763dc70de65a3b0d9954e868609e83850a47ca4f0cb64bd79fea",
- "sha256:647894f5ae99c4cf6bb82a1bb3a796f6e06af3caa3d32e26d2350d0e3e3faf24",
- "sha256:66d4cfda1d8ed687daa4bc0274fcfd5267873db9a5bc0418c2da19273040eeb7",
- "sha256:6a9a8c34fb7bb609419a211e59da8887eeca40d300b5ea8e56af98f6fbbb1541",
- "sha256:6b3f39a85d671436ee3d12c017f8fdea38509e4f25b28eb25877293c98c243f6",
- "sha256:6b6fb63ae352e13748289f04f37868099e69dba4c2b3e271c46061e82c745634",
- "sha256:70693319e0b8fd35dd863e3e29513875eb15c51945bf32519ef52927ca883bc3",
- "sha256:781e65134efaf88feb447e8c97a51772aa75e48b794352f94cb7ea717dedda0d",
- "sha256:819ce3b883b7576ca28da3861c7e1a88afd08cc8c96908e08a3f4dd64a228034",
- "sha256:857112b22acd417c40fa4595db2fe28ab900c8c5fe4670c7989b1c0230955465",
- "sha256:887d9b0a65404929641a9fabb6452b07fe4572b269d901d622d8a34a4e9043b2",
- "sha256:8b3489ff1ed1e8315674d0775dc7d2195fb13ca17b3808721b54dbe9fd020faf",
- "sha256:92fc4500fcb33899b05ba73276dfb684a20d31caa567b7cb5252d48f896a91b1",
- "sha256:9403db39be1393618dd80c746cb22ccda168efce239c73af13c3763ef56ffc04",
- "sha256:98110aa363f1bb4c073e8dcfaefd3a5cea0f0834c2aab23dda657e4dab2f53b5",
- "sha256:999779addc413181912e984b942fbcc951be1f5b3663cd80b2687758f434c583",
- "sha256:9caac6b54914bdf41bcc91e7eb9147d331d29235a7c967c150ef5df6464fd1bb",
- "sha256:a7a078f5d37bee6690959c813977da5291b24286e7b962e62a94cec31aa5188b",
- "sha256:a7e65eb5c003a303b94aa2c3852ef130230ec79e349632d030e9571b87c4698c",
- "sha256:a96dc1fa45bd8c407a0af03b2d5218392729e1822b0c32e62c5bf7eeb5fb3958",
- "sha256:aca405706e0b0a44cc6bfd41fbe89919a6a56999157f6de7e182a990c36e37bc",
- "sha256:accb6150ce61c9c4b7738d45550806aa2b71c7668c6942f17b0ac182b6142fd4",
- "sha256:ad1af54a62ffe39cf34db1aa6ed1a1873bd548f6401db39d8e7cd060b9211f82",
- "sha256:ae1aa1cd222c6d205853b3013c69cd04515f9d6ab6de4b0603e2e1c33221303e",
- "sha256:b2d0a12018b04f4cb820781ec0dffb5f7c7c1d2a5cd22bff7fb055a2cb19ebce",
- "sha256:b480c6a4e1138e1aa137c0079b9b6305ec6dcc1098a8ca5196283e8a49df95a9",
- "sha256:b74c261802d3d2b85c9df2dfb2fa81b6f90deeef63c2db9f0e029a3cac50b518",
- "sha256:ba278acf14471d36316159c94a802933d10b6a1e117b8554fe0d0d9b75c9d536",
- "sha256:bb6178c241278d5fe853b3de743087be7f5f4c6f7d6d22a3b524d323eecec505",
- "sha256:bf72af5e0fb40e9babf594308911436c8efde3cb5e75b6f206c34ad18be5c052",
- "sha256:bfd3223c15bebe26518d58ccf9a39b93948d3dcb3e57a20480dfdd315356baff",
- "sha256:c214999039d4f2a5b2073ac506bba279945233da8c786e490d411dfc30f855c1",
- "sha256:c2f992c07c0fca81655066705beae35fc95a2fa7366467366db627d9f2ee097f",
- "sha256:cba4cfa1052819d16699e1d55d18c92b6e094d4517c41dd231a8b9f87b6fa681",
- "sha256:cea7daf9fc7ae6687cf1e2c049752f19f146fdc37c2cc376e7d0032cf4f25347",
- "sha256:cf6c4150f8c0e32d241436526f3c3f9cbd34429492abddbada2ffcff506c51af",
- "sha256:d09c333d36c1409d56a9d29b3a1b800a42c76a57a5a8907eacdbce3f18768246",
- "sha256:d27b84d5880f6d8aa9ae3edb253c59d9f6642ffbb2c889b78b60361eed449787",
- "sha256:d2ccec9ac47cf4e04897619c0e0c1a48c54a71bdf045117d3a26f80d38ab1fb0",
- "sha256:d71264a80f3fcf512eb4f18f59423fe82d6e346ee97b90625f283df56aee103f",
- "sha256:d93f3307ad32a27bda2e88ec81134b823c240aa3abb55821a8da553eed8d9439",
- "sha256:d9631c5e8b5b3a0fda99cb0d29c18133bca1e18aea9effe55adb3da1adef80d3",
- "sha256:ddfab44e4489bd79bda09d84c430677fc7f0a4939a73d2bba3073036f487a0a6",
- "sha256:e7048abd75fe40712005bcfc06bb44b9dfcd8e101dda2ecf2f5aa46115ad07ca",
- "sha256:e73091191e4280403bde6c9a52a6999d69cdfde498f1fdf629105247599b57ec",
- "sha256:e800776a79a5aabdb17dcc2346a7d66d0777e942e4cd251defeb084762ecd17d",
- "sha256:edc9fc7051e3350643ad929df55c451899bb9ae6d24998a949d2e4c87fb596d3",
- "sha256:f089118d584e859c62b3da0892b88a83d611c2033ac410e929cb6754eec0ed16",
- "sha256:f174bbd484294ed9fdf09437f889f95807e5f229d5d93588d34e92106fbf6717",
- "sha256:f508b0491767bb1f2b87fdfacaba5f7eddc2f867740ec69ece6d1946d29029a6",
- "sha256:f7a31fc1e1bd362874863fdeed71aed92d348f5336fd84f2197ba40c59f061bd",
- "sha256:f9479aa06a793c5aeba49ce5c5692ffb51fcd9a7016e017d555d5e2b0045d212"
- ],
- "markers": "python_version >= '3.9'",
- "version": "==0.2.1"
- },
- "proto-plus": {
- "hashes": [
- "sha256:6e93d5f5ca267b54300880fff156b6a3386b3fa3f43b1da62e680fc0c586ef22",
- "sha256:bf2dfaa3da281fc3187d12d224c707cb57214fb2c22ba854eb0c105a3fb2d4d7"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==1.26.0"
- },
- "protobuf": {
- "hashes": [
- "sha256:0a18ed4a24198528f2333802eb075e59dea9d679ab7a6c5efb017a59004d849f",
- "sha256:0eb32bfa5219fc8d4111803e9a690658aa2e6366384fd0851064b963b6d1f2a7",
- "sha256:3ea51771449e1035f26069c4c7fd51fba990d07bc55ba80701c78f886bf9c888",
- "sha256:5da0f41edaf117bde316404bad1a486cb4ededf8e4a54891296f648e8e076620",
- "sha256:6ce8cc3389a20693bfde6c6562e03474c40851b44975c9b2bf6df7d8c4f864da",
- "sha256:84a57163a0ccef3f96e4b6a20516cedcf5bb3a95a657131c5c3ac62200d23252",
- "sha256:a4fa6f80816a9a0678429e84973f2f98cbc218cca434abe8db2ad0bffc98503a",
- "sha256:a8434404bbf139aa9e1300dbf989667a83d42ddda9153d8ab76e0d5dcaca484e",
- "sha256:b89c115d877892a512f79a8114564fb435943b59067615894c3b13cd3e1fa107",
- "sha256:c027e08a08be10b67c06bf2370b99c811c466398c357e615ca88c91c07f0910f",
- "sha256:daaf63f70f25e8689c072cfad4334ca0ac1d1e05a92fc15c54eb9cf23c3efd84"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==5.29.3"
- },
- "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"
- },
- "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"
- },
- "pycparser": {
- "hashes": [
- "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6",
- "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==2.22"
- },
- "pycryptodomex": {
- "hashes": [
- "sha256:0df2608682db8279a9ebbaf05a72f62a321433522ed0e499bc486a6889b96bf3",
- "sha256:103c133d6cd832ae7266feb0a65b69e3a5e4dbbd6f3a3ae3211a557fd653f516",
- "sha256:1233443f19d278c72c4daae749872a4af3787a813e05c3561c73ab0c153c7b0f",
- "sha256:222d0bd05381dd25c32dd6065c071ebf084212ab79bab4599ba9e6a3e0009e6c",
- "sha256:27e84eeff24250ffec32722334749ac2a57a5fd60332cd6a0680090e7c42877e",
- "sha256:34325b84c8b380675fd2320d0649cdcbc9cf1e0d1526edbe8fce43ed858cdc7e",
- "sha256:365aa5a66d52fd1f9e0530ea97f392c48c409c2f01ff8b9a39c73ed6f527d36c",
- "sha256:3efddfc50ac0ca143364042324046800c126a1d63816d532f2e19e6f2d8c0c31",
- "sha256:46eb1f0c8d309da63a2064c28de54e5e614ad17b7e2f88df0faef58ce192fc7b",
- "sha256:5241bdb53bcf32a9568770a6584774b1b8109342bd033398e4ff2da052123832",
- "sha256:52e23a0a6e61691134aa8c8beba89de420602541afaae70f66e16060fdcd677e",
- "sha256:56435c7124dd0ce0c8bdd99c52e5d183a0ca7fdcd06c5d5509423843f487dd0b",
- "sha256:5823d03e904ea3e53aebd6799d6b8ec63b7675b5d2f4a4bd5e3adcb512d03b37",
- "sha256:65d275e3f866cf6fe891411be9c1454fb58809ccc5de6d3770654c47197acd65",
- "sha256:770d630a5c46605ec83393feaa73a9635a60e55b112e1fb0c3cea84c2897aa0a",
- "sha256:77ac2ea80bcb4b4e1c6a596734c775a1615d23e31794967416afc14852a639d3",
- "sha256:7a1058e6dfe827f4209c5cae466e67610bcd0d66f2f037465daa2a29d92d952b",
- "sha256:8a9d8342cf22b74a746e3c6c9453cb0cfbb55943410e3a2619bd9164b48dc9d9",
- "sha256:8ef436cdeea794015263853311f84c1ff0341b98fc7908e8a70595a68cefd971",
- "sha256:9aa0cf13a1a1128b3e964dc667e5fe5c6235f7d7cfb0277213f0e2a783837cc2",
- "sha256:9ba09a5b407cbb3bcb325221e346a140605714b5e880741dc9a1e9ecf1688d42",
- "sha256:a192fb46c95489beba9c3f002ed7d93979423d1b2a53eab8771dbb1339eb3ddd",
- "sha256:a3d77919e6ff56d89aada1bd009b727b874d464cb0e2e3f00a49f7d2e709d76e",
- "sha256:b0e9765f93fe4890f39875e6c90c96cb341767833cfa767f41b490b506fa9ec0",
- "sha256:bbb07f88e277162b8bfca7134b34f18b400d84eac7375ce73117f865e3c80d4c",
- "sha256:c07e64867a54f7e93186a55bec08a18b7302e7bee1b02fd84c6089ec215e723a",
- "sha256:cc7e111e66c274b0df5f4efa679eb31e23c7545d702333dfd2df10ab02c2a2ce",
- "sha256:da76ebf6650323eae7236b54b1b1f0e57c16483be6e3c1ebf901d4ada47563b6",
- "sha256:dbeb84a399373df84a69e0919c1d733b89e049752426041deeb30d68e9867822",
- "sha256:e859e53d983b7fe18cb8f1b0e29d991a5c93be2c8dd25db7db1fe3bd3617f6f9",
- "sha256:ef046b2e6c425647971b51424f0f88d8a2e0a2a63d3531817968c42078895c00",
- "sha256:feaecdce4e5c0045e7a287de0c4351284391fe170729aa9182f6bd967631b3a8"
- ],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
- "version": "==3.21.0"
- },
- "pydantic": {
- "hashes": [
- "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584",
- "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==2.10.6"
- },
- "pydantic-core": {
- "hashes": [
- "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278",
- "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50",
- "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9",
- "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f",
- "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6",
- "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc",
- "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54",
- "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630",
- "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9",
- "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236",
- "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7",
- "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee",
- "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b",
- "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048",
- "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc",
- "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130",
- "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4",
- "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd",
- "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4",
- "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7",
- "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7",
- "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4",
- "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e",
- "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa",
- "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6",
- "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962",
- "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b",
- "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f",
- "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474",
- "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5",
- "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459",
- "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf",
- "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a",
- "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c",
- "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76",
- "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362",
- "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4",
- "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934",
- "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320",
- "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118",
- "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96",
- "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306",
- "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046",
- "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3",
- "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2",
- "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af",
- "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9",
- "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67",
- "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a",
- "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27",
- "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35",
- "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b",
- "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151",
- "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b",
- "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154",
- "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133",
- "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef",
- "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145",
- "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15",
- "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4",
- "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc",
- "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee",
- "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c",
- "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0",
- "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5",
- "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57",
- "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b",
- "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8",
- "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1",
- "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da",
- "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e",
- "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc",
- "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993",
- "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656",
- "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4",
- "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c",
- "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb",
- "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d",
- "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9",
- "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e",
- "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1",
- "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc",
- "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a",
- "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9",
- "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506",
- "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b",
- "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1",
- "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d",
- "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99",
- "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3",
- "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31",
- "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c",
- "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39",
- "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a",
- "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308",
- "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2",
- "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228",
- "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b",
- "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9",
- "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==2.27.2"
- },
- "pydantic-settings": {
- "hashes": [
- "sha256:10c9caad35e64bfb3c2fbf70a078c0e25cc92499782e5200747f942a065dec93",
- "sha256:590be9e6e24d06db33a4262829edef682500ef008565a969c73d39d5f8bfb3fd"
- ],
- "index": "pypi",
- "markers": "python_version >= '3.8'",
- "version": "==2.7.1"
- },
- "pygments": {
- "hashes": [
- "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f",
- "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==2.19.1"
- },
- "pyopenssl": {
- "hashes": [
- "sha256:6756834481d9ed5470f4a9393455154bc92fe7a64b7bc6ee2c804e78c52099b2",
- "sha256:6b2cba5cc46e822750ec3e5a81ee12819850b11303630d575e98108a079c2b12"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==23.3.0"
- },
- "pyparsing": {
- "hashes": [
- "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1",
- "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a"
- ],
- "markers": "python_version >= '3.9'",
- "version": "==3.2.1"
- },
- "pysocks": {
- "hashes": [
- "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299",
- "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5",
- "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"
- ],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==1.7.1"
- },
- "pysubs2": {
- "hashes": [
- "sha256:05716f5039a9ebe32cd4d7673f923cf36204f3a3e99987f823ab83610b7035a0",
- "sha256:3397bb58a4a15b1325ba2ae3fd4d7c214e2c0ddb9f33190d6280d783bb433b20"
- ],
- "markers": "python_version >= '3.9'",
- "version": "==1.8.0"
- },
- "python-dateutil": {
- "hashes": [
- "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3",
- "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"
- ],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
- "version": "==2.9.0.post0"
- },
- "python-dotenv": {
- "hashes": [
- "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca",
- "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"
- ],
- "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:c032c0b90e824ccd605620eb67cc59601f48a100fe7424090aaf37f243239e82",
- "sha256:dcd41ebfbc1b0ca6a1212870b0ff68b85e2111655e09027a0e42829fe3a63460"
- ],
- "markers": "python_version >= '3.7' and python_version < '4.0'",
- "version": "==0.9.2"
- },
- "pytz": {
- "hashes": [
- "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57",
- "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e"
- ],
- "version": "==2025.1"
- },
- "pyyaml": {
- "hashes": [
- "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff",
- "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48",
- "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086",
- "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e",
- "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133",
- "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5",
- "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484",
- "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee",
- "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5",
- "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68",
- "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a",
- "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf",
- "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99",
- "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8",
- "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85",
- "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19",
- "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc",
- "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a",
- "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1",
- "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317",
- "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c",
- "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631",
- "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d",
- "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652",
- "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5",
- "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e",
- "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b",
- "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8",
- "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476",
- "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706",
- "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563",
- "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237",
- "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b",
- "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083",
- "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180",
- "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425",
- "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e",
- "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f",
- "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725",
- "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183",
- "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab",
- "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774",
- "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725",
- "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e",
- "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5",
- "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d",
- "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290",
- "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44",
- "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed",
- "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4",
- "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba",
- "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12",
- "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==6.0.2"
- },
- "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:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c",
- "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60",
- "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d",
- "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d",
- "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67",
- "sha256:072623554418a9911446278f16ecb398fb3b540147a7828c06e2011fa531e773",
- "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0",
- "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef",
- "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad",
- "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe",
- "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3",
- "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114",
- "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4",
- "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39",
- "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e",
- "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3",
- "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7",
- "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d",
- "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e",
- "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a",
- "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7",
- "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f",
- "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0",
- "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54",
- "sha256:3a51ccc315653ba012774efca4f23d1d2a8a8f278a6072e29c7147eee7da446b",
- "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c",
- "sha256:40291b1b89ca6ad8d3f2b82782cc33807f1406cf68c8d440861da6304d8ffbbd",
- "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57",
- "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34",
- "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d",
- "sha256:50153825ee016b91549962f970d6a4442fa106832e14c918acd1c8e479916c4f",
- "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b",
- "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519",
- "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4",
- "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a",
- "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638",
- "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b",
- "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839",
- "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07",
- "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf",
- "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff",
- "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0",
- "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f",
- "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95",
- "sha256:6f44ec28b1f858c98d3036ad5d7d0bfc568bdd7a74f9c24e25f41ef1ebfd81a4",
- "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e",
- "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13",
- "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519",
- "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2",
- "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008",
- "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9",
- "sha256:89d75e7293d2b3e674db7d4d9b1bee7f8f3d1609428e293771d1a962617150cc",
- "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48",
- "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20",
- "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89",
- "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e",
- "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf",
- "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b",
- "sha256:a36fdf2af13c2b14738f6e973aba563623cb77d753bbbd8d414d18bfaa3105dd",
- "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84",
- "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29",
- "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b",
- "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3",
- "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45",
- "sha256:ad182d02e40de7459b73155deb8996bbd8e96852267879396fb274e8700190e3",
- "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983",
- "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e",
- "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7",
- "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4",
- "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e",
- "sha256:ba9b72e5643641b7d41fa1f6d5abda2c9a263ae835b917348fc3c928182ad467",
- "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577",
- "sha256:bb8f74f2f10dbf13a0be8de623ba4f9491faf58c24064f32b65679b021ed0001",
- "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0",
- "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55",
- "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9",
- "sha256:cdf58d0e516ee426a48f7b2c03a332a4114420716d55769ff7108c37a09951bf",
- "sha256:d1cee317bfc014c2419a76bcc87f071405e3966da434e03e13beb45f8aced1a6",
- "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e",
- "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde",
- "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62",
- "sha256:df951c5f4a1b1910f1a99ff42c473ff60f8225baa1cdd3539fe2819d9543e9df",
- "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51",
- "sha256:ea1bfda2f7162605f6e8178223576856b3d791109f15ea99a9f95c16a7636fb5",
- "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86",
- "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2",
- "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2",
- "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0",
- "sha256:f654882311409afb1d780b940234208a252322c24a93b442ca714d119e68086c",
- "sha256:f65557897fc977a44ab205ea871b690adaef6b9da6afda4790a2484b04293a5f",
- "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6",
- "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2",
- "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9",
- "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==2024.11.6"
- },
- "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"
- },
- "rich": {
- "hashes": [
- "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098",
- "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"
- ],
- "markers": "python_full_version >= '3.8.0'",
- "version": "==13.9.4"
- },
- "rsa": {
- "hashes": [
- "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7",
- "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"
- ],
- "markers": "python_version >= '3.6' and python_version < '4'",
- "version": "==4.9"
- },
- "s3transfer": {
- "hashes": [
- "sha256:3b39185cb72f5acc77db1a58b6e25b977f28d20496b6e58d6813d75f464d632f",
- "sha256:be6ecb39fadd986ef1701097771f87e4d2f821f27f6071c872143884d2950fbc"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==0.11.2"
- },
- "selenium": {
- "hashes": [
- "sha256:0072d08670d7ec32db901bd0107695a330cecac9f196e3afb3fa8163026e022a",
- "sha256:4238847e45e24e4472cfcf3554427512c7aab9443396435b1623ef406fff1cc1"
- ],
- "markers": "python_version >= '3.9'",
- "version": "==4.28.1"
- },
- "six": {
- "hashes": [
- "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274",
- "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"
- ],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
- "version": "==1.17.0"
- },
- "sniffio": {
- "hashes": [
- "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2",
- "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==1.3.1"
- },
- "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:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb",
- "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==2.6"
- },
- "sqlalchemy": {
- "hashes": [
- "sha256:0398361acebb42975deb747a824b5188817d32b5c8f8aba767d51ad0cc7bb08d",
- "sha256:0561832b04c6071bac3aad45b0d3bb6d2c4f46a8409f0a7a9c9fa6673b41bc03",
- "sha256:07258341402a718f166618470cde0c34e4cec85a39767dce4e24f61ba5e667ea",
- "sha256:0a826f21848632add58bef4f755a33d45105d25656a0c849f2dc2df1c71f6f50",
- "sha256:1052723e6cd95312f6a6eff9a279fd41bbae67633415373fdac3c430eca3425d",
- "sha256:12d5b06a1f3aeccf295a5843c86835033797fea292c60e72b07bcb5d820e6dd3",
- "sha256:12f5c9ed53334c3ce719155424dc5407aaa4f6cadeb09c5b627e06abb93933a1",
- "sha256:2a0ef3f98175d77180ffdc623d38e9f1736e8d86b6ba70bff182a7e68bed7727",
- "sha256:2f2951dc4b4f990a4b394d6b382accb33141d4d3bd3ef4e2b27287135d6bdd68",
- "sha256:3868acb639c136d98107c9096303d2d8e5da2880f7706f9f8c06a7f961961149",
- "sha256:386b7d136919bb66ced64d2228b92d66140de5fefb3c7df6bd79069a269a7b06",
- "sha256:3d3043375dd5bbcb2282894cbb12e6c559654c67b5fffb462fda815a55bf93f7",
- "sha256:3e35d5565b35b66905b79ca4ae85840a8d40d31e0b3e2990f2e7692071b179ca",
- "sha256:402c2316d95ed90d3d3c25ad0390afa52f4d2c56b348f212aa9c8d072a40eee5",
- "sha256:40310db77a55512a18827488e592965d3dec6a3f1e3d8af3f8243134029daca3",
- "sha256:40e9cdbd18c1f84631312b64993f7d755d85a3930252f6276a77432a2b25a2f3",
- "sha256:49aa2cdd1e88adb1617c672a09bf4ebf2f05c9448c6dbeba096a3aeeb9d4d443",
- "sha256:57dd41ba32430cbcc812041d4de8d2ca4651aeefad2626921ae2a23deb8cd6ff",
- "sha256:5dba1cdb8f319084f5b00d41207b2079822aa8d6a4667c0f369fce85e34b0c86",
- "sha256:5e1d9e429028ce04f187a9f522818386c8b076723cdbe9345708384f49ebcec6",
- "sha256:63178c675d4c80def39f1febd625a6333f44c0ba269edd8a468b156394b27753",
- "sha256:6493bc0eacdbb2c0f0d260d8988e943fee06089cd239bd7f3d0c45d1657a70e2",
- "sha256:64aa8934200e222f72fcfd82ee71c0130a9c07d5725af6fe6e919017d095b297",
- "sha256:665255e7aae5f38237b3a6eae49d2358d83a59f39ac21036413fab5d1e810578",
- "sha256:6db316d6e340f862ec059dc12e395d71f39746a20503b124edc255973977b728",
- "sha256:70065dfabf023b155a9c2a18f573e47e6ca709b9e8619b2e04c54d5bcf193178",
- "sha256:8455aa60da49cb112df62b4721bd8ad3654a3a02b9452c783e651637a1f21fa2",
- "sha256:8b0ac78898c50e2574e9f938d2e5caa8fe187d7a5b69b65faa1ea4648925b096",
- "sha256:8bf312ed8ac096d674c6aa9131b249093c1b37c35db6a967daa4c84746bc1bc9",
- "sha256:92f99f2623ff16bd4aaf786ccde759c1f676d39c7bf2855eb0b540e1ac4530c8",
- "sha256:9c8bcad7fc12f0cc5896d8e10fdf703c45bd487294a986903fe032c72201596b",
- "sha256:9cd136184dd5f58892f24001cdce986f5d7e96059d004118d5410671579834a4",
- "sha256:9eb4fa13c8c7a2404b6a8e3772c17a55b1ba18bc711e25e4d6c0c9f5f541b02a",
- "sha256:a2bc4e49e8329f3283d99840c136ff2cd1a29e49b5624a46a290f04dff48e079",
- "sha256:a5645cd45f56895cfe3ca3459aed9ff2d3f9aaa29ff7edf557fa7a23515a3725",
- "sha256:a9afbc3909d0274d6ac8ec891e30210563b2c8bdd52ebbda14146354e7a69373",
- "sha256:aa498d1392216fae47eaf10c593e06c34476ced9549657fca713d0d1ba5f7248",
- "sha256:afd776cf1ebfc7f9aa42a09cf19feadb40a26366802d86c1fba080d8e5e74bdd",
- "sha256:b335a7c958bc945e10c522c069cd6e5804f4ff20f9a744dd38e748eb602cbbda",
- "sha256:b3c4817dff8cef5697f5afe5fec6bc1783994d55a68391be24cb7d80d2dbc3a6",
- "sha256:b79ee64d01d05a5476d5cceb3c27b5535e6bb84ee0f872ba60d9a8cd4d0e6579",
- "sha256:b87a90f14c68c925817423b0424381f0e16d80fc9a1a1046ef202ab25b19a444",
- "sha256:bf89e0e4a30714b357f5d46b6f20e0099d38b30d45fa68ea48589faf5f12f62d",
- "sha256:c058b84c3b24812c859300f3b5abf300daa34df20d4d4f42e9652a4d1c48c8a4",
- "sha256:c09a6ea87658695e527104cf857c70f79f14e9484605e205217aae0ec27b45fc",
- "sha256:c57b8e0841f3fce7b703530ed70c7c36269c6d180ea2e02e36b34cb7288c50c7",
- "sha256:c9cea5b756173bb86e2235f2f871b406a9b9d722417ae31e5391ccaef5348f2c",
- "sha256:cb39ed598aaf102251483f3e4675c5dd6b289c8142210ef76ba24aae0a8f8aba",
- "sha256:e036549ad14f2b414c725349cce0772ea34a7ab008e9cd67f9084e4f371d1f32",
- "sha256:e185ea07a99ce8b8edfc788c586c538c4b1351007e614ceb708fd01b095ef33e",
- "sha256:e5a4d82bdb4bf1ac1285a68eab02d253ab73355d9f0fe725a97e1e0fa689decb",
- "sha256:eae27ad7580529a427cfdd52c87abb2dfb15ce2b7a3e0fc29fbb63e2ed6f8120",
- "sha256:ecef029b69843b82048c5b347d8e6049356aa24ed644006c9a9d7098c3bd3bfd",
- "sha256:ee3bee874cb1fadee2ff2b79fc9fc808aa638670f28b2145074538d4a6a5028e",
- "sha256:f0d3de936b192980209d7b5149e3c98977c3810d401482d05fb6d668d53c1c63",
- "sha256:f53c0d6a859b2db58332e0e6a921582a02c1677cc93d4cbb36fdf49709b327b2",
- "sha256:f9d57f1b3061b3e21476b0ad5f0397b112b94ace21d1f439f2db472e568178ae"
- ],
- "index": "pypi",
- "markers": "python_version >= '3.7'",
- "version": "==2.0.38"
- },
- "starlette": {
- "hashes": [
- "sha256:2cbcba2a75806f8a41c722141486f37c28e30a0921c5f6fe4346cb0dcee1302f",
- "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d"
- ],
- "markers": "python_version >= '3.9'",
- "version": "==0.45.3"
- },
- "telethon": {
- "hashes": [
- "sha256:30c187017501bfb982b8af5659f864dda4108f77ea49cfce61e8f6fdb8a18d6e",
- "sha256:f9866c1e37197a0894e0c02aa56a6359bffb14a585e88e18e3e819df4fda399a"
- ],
- "markers": "python_version >= '3.5'",
- "version": "==1.38.1"
- },
- "text-unidecode": {
- "hashes": [
- "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8",
- "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"
- ],
- "version": "==1.3"
- },
- "tiktok-downloader": {
- "hashes": [
- "sha256:f376ba0d2517fbab87b3185784d6e19481543326121427ae0986b9fdef6f4f75"
- ],
- "version": "==0.3.5"
- },
- "tqdm": {
- "hashes": [
- "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2",
- "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==4.67.1"
- },
- "trio": {
- "hashes": [
- "sha256:4e547896fe9e8a5658e54e4c7c5fa1db748cbbbaa7c965e7d40505b928c73c05",
- "sha256:56d58977acc1635735a96581ec70513cc781b8b6decd299c487d3be2a721cd94"
- ],
- "markers": "python_version >= '3.9'",
- "version": "==0.28.0"
- },
- "trio-websocket": {
- "hashes": [
- "sha256:18c11793647703c158b1f6e62de638acada927344d534e3c7628eedcb746839f",
- "sha256:520d046b0d030cf970b8b2b2e00c4c2245b3807853ecd44214acd33d74581638"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==0.11.1"
- },
- "tsp-client": {
- "hashes": [
- "sha256:415ff89aa15775533801bb18bd6b287f30a293d976b8fbb4d30f48873af41ba4",
- "sha256:db7f98e26ac370f5aab0055f74e7b3e4fd5245ef2f57cc56db3caa2694b82fd6"
- ],
- "version": "==0.2.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:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694",
- "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639"
- ],
- "markers": "python_version >= '2'",
- "version": "==2025.1"
- },
- "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:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df",
- "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"
- ],
- "markers": "python_version >= '3.9'",
- "version": "==2.3.0"
- },
- "uvicorn": {
- "hashes": [
- "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4",
- "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"
- ],
- "index": "pypi",
- "markers": "python_version >= '3.9'",
- "version": "==0.34.0"
- },
- "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:133d252ee94ceb1ee9515fb448d410ba471cbccc19e303b548076cd44cc81f30",
- "sha256:c1c001b66b80343a991628080398d8a923e8753183b952f99f40ecafe1087070"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==0.3.27"
- },
- "warcio": {
- "hashes": [
- "sha256:7247b57e68074cfd9433cb6dc226f8567d6777052abec2d3c78346cffa4d19b9",
- "sha256:ca96130bde7747e49da714097d144c6ff939458d4f93e1beb1e42455db4326d4"
- ],
- "version": "==1.7.5"
- },
- "wcwidth": {
- "hashes": [
- "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859",
- "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"
- ],
- "version": "==0.2.13"
- },
- "websocket-client": {
- "hashes": [
- "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526",
- "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==1.8.0"
- },
- "websockets": {
- "hashes": [
- "sha256:02687db35dbc7d25fd541a602b5f8e451a238ffa033030b172ff86a93cb5dc2a",
- "sha256:065ce275e7c4ffb42cb738dd6b20726ac26ac9ad0a2a48e33ca632351a737267",
- "sha256:091ab63dfc8cea748cc22c1db2814eadb77ccbf82829bac6b2fbe3401d548eda",
- "sha256:0a52a6d7cf6938e04e9dceb949d35fbdf58ac14deea26e685ab6368e73744e4c",
- "sha256:0a6f3efd47ffd0d12080594f434faf1cd2549b31e54870b8470b28cc1d3817d9",
- "sha256:0d8c3e2cdb38f31d8bd7d9d28908005f6fa9def3324edb9bf336d7e4266fd397",
- "sha256:1979bee04af6a78608024bad6dfcc0cc930ce819f9e10342a29a05b5320355d0",
- "sha256:1a5a20d5843886d34ff8c57424cc65a1deda4375729cbca4cb6b3353f3ce4142",
- "sha256:1c9b6535c0e2cf8a6bf938064fb754aaceb1e6a4a51a80d884cd5db569886910",
- "sha256:1f20522e624d7ffbdbe259c6b6a65d73c895045f76a93719aa10cd93b3de100c",
- "sha256:2066dc4cbcc19f32c12a5a0e8cc1b7ac734e5b64ac0a325ff8353451c4b15ef2",
- "sha256:20e6dd0984d7ca3037afcb4494e48c74ffb51e8013cac71cf607fffe11df7205",
- "sha256:22441c81a6748a53bfcb98951d58d1af0661ab47a536af08920d129b4d1c3473",
- "sha256:2c6c0097a41968b2e2b54ed3424739aab0b762ca92af2379f152c1aef0187e1c",
- "sha256:2dddacad58e2614a24938a50b85969d56f88e620e3f897b7d80ac0d8a5800258",
- "sha256:2e20c5f517e2163d76e2729104abc42639c41cf91f7b1839295be43302713661",
- "sha256:34277a29f5303d54ec6468fb525d99c99938607bc96b8d72d675dee2b9f5bf1d",
- "sha256:3bdc8c692c866ce5fefcaf07d2b55c91d6922ac397e031ef9b774e5b9ea42166",
- "sha256:3c1426c021c38cf92b453cdf371228d3430acd775edee6bac5a4d577efc72365",
- "sha256:44bba1a956c2c9d268bdcdf234d5e5ff4c9b6dc3e300545cbe99af59dda9dcce",
- "sha256:4b27ece32f63150c268593d5fdb82819584831a83a3f5809b7521df0685cd5d8",
- "sha256:4da98b72009836179bb596a92297b1a61bb5a830c0e483a7d0766d45070a08ad",
- "sha256:4daa0faea5424d8713142b33825fff03c736f781690d90652d2c8b053345b0e7",
- "sha256:5059ed9c54945efb321f097084b4c7e52c246f2c869815876a69d1efc4ad6eb5",
- "sha256:577a4cebf1ceaf0b65ffc42c54856214165fb8ceeba3935852fc33f6b0c55e7f",
- "sha256:647b573f7d3ada919fd60e64d533409a79dcf1ea21daeb4542d1d996519ca967",
- "sha256:669c3e101c246aa85bc8534e495952e2ca208bd87994650b90a23d745902db9a",
- "sha256:6af6a4b26eea4fc06c6818a6b962a952441e0e39548b44773502761ded8cc1d4",
- "sha256:6af99a38e49f66be5a64b1e890208ad026cda49355661549c507152113049990",
- "sha256:6d7ff794c8b36bc402f2e07c0b2ceb4a2424147ed4785ff03e2a7af03711d60a",
- "sha256:6f1372e511c7409a542291bce92d6c83320e02c9cf392223272287ce55bc224e",
- "sha256:714a9b682deb4339d39ffa674f7b674230227d981a37d5d174a4a83e3978a610",
- "sha256:75862126b3d2d505e895893e3deac0a9339ce750bd27b4ba515f008b5acf832d",
- "sha256:7a570862c325af2111343cc9b0257b7119b904823c675b22d4ac547163088d0d",
- "sha256:7a6ceec4ea84469f15cf15807a747e9efe57e369c384fa86e022b3bea679b79b",
- "sha256:7cd5706caec1686c5d233bc76243ff64b1c0dc445339bd538f30547e787c11fe",
- "sha256:80c8efa38957f20bba0117b48737993643204645e9ec45512579132508477cfc",
- "sha256:862e9967b46c07d4dcd2532e9e8e3c2825e004ffbf91a5ef9dde519ee2effb0b",
- "sha256:86cf1aaeca909bf6815ea714d5c5736c8d6dd3a13770e885aafe062ecbd04f1f",
- "sha256:89a71173caaf75fa71a09a5f614f450ba3ec84ad9fca47cb2422a860676716f0",
- "sha256:9f05702e93203a6ff5226e21d9b40c037761b2cfb637187c9802c10f58e40473",
- "sha256:a39d7eceeea35db85b85e1169011bb4321c32e673920ae9c1b6e0978590012a3",
- "sha256:a3c4aa3428b904d5404a0ed85f3644d37e2cb25996b7f096d77caeb0e96a3b42",
- "sha256:a9b0f6c3ba3b1240f602ebb3971d45b02cc12bd1845466dd783496b3b05783a5",
- "sha256:a9e72fb63e5f3feacdcf5b4ff53199ec8c18d66e325c34ee4c551ca748623bbc",
- "sha256:ab95d357cd471df61873dadf66dd05dd4709cae001dd6342edafc8dc6382f307",
- "sha256:ad1c1d02357b7665e700eca43a31d52814ad9ad9b89b58118bdabc365454b574",
- "sha256:b374e8953ad477d17e4851cdc66d83fdc2db88d9e73abf755c94510ebddceb95",
- "sha256:b439ea828c4ba99bb3176dc8d9b933392a2413c0f6b149fdcba48393f573377f",
- "sha256:b4c8cef610e8d7c70dea92e62b6814a8cd24fbd01d7103cc89308d2bfe1659ef",
- "sha256:bbe03eb853e17fd5b15448328b4ec7fb2407d45fb0245036d06a3af251f8e48f",
- "sha256:bc63cee8596a6ec84d9753fd0fcfa0452ee12f317afe4beae6b157f0070c6c7f",
- "sha256:c3ecadc7ce90accf39903815697917643f5b7cfb73c96702318a096c00aa71f5",
- "sha256:c76193c1c044bd1e9b3316dcc34b174bbf9664598791e6fb606d8d29000e070c",
- "sha256:c93215fac5dadc63e51bcc6dceca72e72267c11def401d6668622b47675b097f",
- "sha256:cc45afb9c9b2dc0852d5c8b5321759cf825f82a31bfaf506b65bf4668c96f8b2",
- "sha256:d7d9cafbccba46e768be8a8ad4635fa3eae1ffac4c6e7cb4eb276ba41297ed29",
- "sha256:da85651270c6bfb630136423037dd4975199e5d4114cae6d3066641adcc9d1c7",
- "sha256:dec254fcabc7bd488dab64846f588fc5b6fe0d78f641180030f8ea27b76d72c3",
- "sha256:e3fbd68850c837e57373d95c8fe352203a512b6e49eaae4c2f4088ef8cf21980",
- "sha256:e8179f95323b9ab1c11723e5d91a89403903f7b001828161b480a7810b334885",
- "sha256:e9d0e53530ba7b8b5e389c02282f9d2aa47581514bd6049d3a7cffe1385cf5fe",
- "sha256:eabdb28b972f3729348e632ab08f2a7b616c7e53d5414c12108c29972e655b20",
- "sha256:ec607328ce95a2f12b595f7ae4c5d71bf502212bddcea528290b35c286932b12",
- "sha256:efd9b868d78b194790e6236d9cbc46d68aba4b75b22497eb4ab64fa640c3af56",
- "sha256:f2e53c72052f2596fb792a7acd9704cbc549bf70fcde8a99e899311455974ca3",
- "sha256:f390024a47d904613577df83ba700bd189eedc09c57af0a904e5c39624621270",
- "sha256:f8a86a269759026d2bde227652b87be79f8a734e582debf64c9d302faa1e9f03",
- "sha256:fd475a974d5352390baf865309fe37dec6831aafc3014ffac1eea99e84e83fc2"
- ],
- "markers": "python_version >= '3.9'",
- "version": "==14.2"
- },
- "werkzeug": {
- "hashes": [
- "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e",
- "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"
- ],
- "markers": "python_version >= '3.9'",
- "version": "==3.1.3"
- },
- "wsproto": {
- "hashes": [
- "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065",
- "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"
- ],
- "markers": "python_full_version >= '3.7.0'",
- "version": "==1.2.0"
- },
- "yarl": {
- "hashes": [
- "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba",
- "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193",
- "sha256:045b8482ce9483ada4f3f23b3774f4e1bf4f23a2d5c912ed5170f68efb053318",
- "sha256:09c7907c8548bcd6ab860e5f513e727c53b4a714f459b084f6580b49fa1b9cee",
- "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e",
- "sha256:0b3c92fa08759dbf12b3a59579a4096ba9af8dd344d9a813fc7f5070d86bbab1",
- "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a",
- "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186",
- "sha256:1d407181cfa6e70077df3377938c08012d18893f9f20e92f7d2f314a437c30b1",
- "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50",
- "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640",
- "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb",
- "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8",
- "sha256:3236da9272872443f81fedc389bace88408f64f89f75d1bdb2256069a8730ccc",
- "sha256:35098b24e0327fc4ebdc8ffe336cee0a87a700c24ffed13161af80124b7dc8e5",
- "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58",
- "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2",
- "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393",
- "sha256:4ac515b860c36becb81bb84b667466885096b5fc85596948548b667da3bf9f24",
- "sha256:5094d9206c64181d0f6e76ebd8fb2f8fe274950a63890ee9e0ebfd58bf9d787b",
- "sha256:54d6921f07555713b9300bee9c50fb46e57e2e639027089b1d795ecd9f7fa910",
- "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c",
- "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272",
- "sha256:602d98f2c2d929f8e697ed274fbadc09902c4025c5a9963bf4e9edfc3ab6f7ed",
- "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1",
- "sha256:61e5e68cb65ac8f547f6b5ef933f510134a6bf31bb178be428994b0cb46c2a04",
- "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d",
- "sha256:6333c5a377c8e2f5fae35e7b8f145c617b02c939d04110c76f29ee3676b5f9a5",
- "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d",
- "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889",
- "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae",
- "sha256:757e81cae69244257d125ff31663249b3013b5dc0a8520d73694aed497fb195b",
- "sha256:77a6e85b90a7641d2e07184df5557132a337f136250caafc9ccaa4a2a998ca2c",
- "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576",
- "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34",
- "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477",
- "sha256:80316a8bd5109320d38eef8833ccf5f89608c9107d02d2a7f985f98ed6876990",
- "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2",
- "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512",
- "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069",
- "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a",
- "sha256:8874027a53e3aea659a6d62751800cf6e63314c160fd607489ba5c2edd753cf6",
- "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0",
- "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8",
- "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb",
- "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa",
- "sha256:93b2e109287f93db79210f86deb6b9bbb81ac32fc97236b16f7433db7fc437d8",
- "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e",
- "sha256:a440a2a624683108a1b454705ecd7afc1c3438a08e890a1513d468671d90a04e",
- "sha256:a4bb030cf46a434ec0225bddbebd4b89e6471814ca851abb8696170adb163985",
- "sha256:a9ca04806f3be0ac6d558fffc2fdf8fcef767e0489d2684a21912cc4ed0cd1b8",
- "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1",
- "sha256:ac36703a585e0929b032fbaab0707b75dc12703766d0b53486eabd5139ebadd5",
- "sha256:b1771de9944d875f1b98a745bc547e684b863abf8f8287da8466cf470ef52690",
- "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10",
- "sha256:b4f6450109834af88cb4cc5ecddfc5380ebb9c228695afc11915a0bf82116789",
- "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b",
- "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca",
- "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e",
- "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5",
- "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59",
- "sha256:ba87babd629f8af77f557b61e49e7c7cac36f22f871156b91e10a6e9d4f829e9",
- "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8",
- "sha256:c1e1cc06da1491e6734f0ea1e6294ce00792193c463350626571c287c9a704db",
- "sha256:c654d5207c78e0bd6d749f6dae1dcbbfde3403ad3a4b11f3c5544d9906969dde",
- "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7",
- "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb",
- "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3",
- "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6",
- "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285",
- "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb",
- "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8",
- "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482",
- "sha256:e2c08cc9b16f4f4bc522771d96734c7901e7ebef70c6c5c35dd0f10845270bcd",
- "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75",
- "sha256:e3b9fd71836999aad54084906f8663dffcd2a7fb5cdafd6c37713b2e72be1760",
- "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782",
- "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53",
- "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2",
- "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1",
- "sha256:fe57328fbc1bfd0bd0514470ac692630f3901c0ee39052ae47acd1d90a436719",
- "sha256:fea09ca13323376a2fdfb353a5fa2e59f90cd18d7ca4eaa1fd31f0a8b4f91e62"
- ],
- "markers": "python_version >= '3.9'",
- "version": "==1.18.3"
- },
- "yt-dlp": {
- "hashes": [
- "sha256:1c9738266921ad43c568ad01ac3362fb7c7af549276fbec92bd72f140da16240",
- "sha256:3e76bd896b9f96601021ca192ca0fbdd195e3c3dcc28302a3a34c9bc4979da7b"
- ],
- "markers": "python_version >= '3.9'",
- "version": "==2025.1.26"
- }
- },
- "develop": {
- "anyio": {
- "hashes": [
- "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a",
- "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"
- ],
- "markers": "python_version >= '3.9'",
- "version": "==4.8.0"
- },
- "certifi": {
- "hashes": [
- "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651",
- "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"
- ],
- "markers": "python_version >= '3.6'",
- "version": "==2025.1.31"
- },
- "coverage": {
- "hashes": [
- "sha256:050172741de03525290e67f0161ae5f7f387c88fca50d47fceb4724ceaa591d2",
- "sha256:08e5fb93576a6b054d3d326242af5ef93daaac9bb52bc25f12ccbc3fa94227cd",
- "sha256:09d03f48d9025b8a6a116cddcb6c7b8ce80e4fb4c31dd2e124a7c377036ad58e",
- "sha256:0d03c9452d9d1ccfe5d3a5df0427705022a49b356ac212d529762eaea5ef97b4",
- "sha256:13100f98497086b359bf56fc035a762c674de8ef526daa389ac8932cb9bff1e0",
- "sha256:25575cd5a7d2acc46b42711e8aff826027c0e4f80fb38028a74f31ac22aae69d",
- "sha256:27700d859be68e4fb2e7bf774cf49933dcac6f81a9bc4c13bd41735b8d26a53b",
- "sha256:2c81e53782043b323bd34c7de711ed9b4673414eb517eaf35af92185b873839c",
- "sha256:397489c611b76302dfa1d9ea079e138dddc4af80fc6819d5f5119ec8ca6c0e47",
- "sha256:476f29a258b9cd153f2be5bf5f119d670d2806363595263917bddc167d6e5cce",
- "sha256:4bda710139ea646890d1c000feb533caff86904a0e0638f85e967c28cb8eec50",
- "sha256:4cf96beb05d004e4c51cd846fcdf9eee9eb2681518524b66b2e7610507944c2f",
- "sha256:4f21e3617f48d683f30cf2a6c8b739c838e600cb1454fe6b2eb486ac2bce8fbd",
- "sha256:5128f3ba694c0a1bde55fc480090392c336236c3e1a10dad40dc1ab17c7675ff",
- "sha256:532fe139691af134aa8b54ed60dd3c806aa81312d93693bd2883c7b61592c840",
- "sha256:5a3f7cbbcb4ad95067a6525f83a6fc78d9cbc1e70f8abaeeaeaa72ef34f48fc3",
- "sha256:5b48db06f53d1864fea6dbd855e6d51d41c0f06c212c3004511c0bdc6847b297",
- "sha256:5e7ac966ab110bd94ee844f2643f196d78fde1cd2450399116d3efdd706e19f5",
- "sha256:5edc16712187139ab635a2e644cc41fc239bc6d245b16124045743130455c652",
- "sha256:60d4ad09dfc8c36c4910685faafcb8044c84e4dae302e86c585b3e2e7778726c",
- "sha256:61c834cbb80946d6ebfddd9b393a4c46bec92fcc0fa069321fcb8049117f76ea",
- "sha256:6ba27a0375c5ef4d2a7712f829265102decd5ff78b96d342ac2fa555742c4f4f",
- "sha256:6c96a142057d83ee993eaf71629ca3fb952cda8afa9a70af4132950c2bd3deb9",
- "sha256:6d60577673ba48d8ae8e362e61fd4ad1a640293ffe8991d11c86f195479100b7",
- "sha256:7eb0504bb307401fd08bc5163a351df301438b3beb88a4fa044681295bbefc67",
- "sha256:8e433b6e3a834a43dae2889adc125f3fa4c66668df420d8e49bc4ee817dd7a70",
- "sha256:8fa4fffd90ee92f62ff7404b4801b59e8ea8502e19c9bf2d3241ce745b52926c",
- "sha256:90de4e9ca4489e823138bd13098af9ac8028cc029f33f60098b5c08c675c7bda",
- "sha256:a165b09e7d5f685bf659063334a9a7b1a2d57b531753d3e04bd442b3cfe5845b",
- "sha256:a46d56e99a31d858d6912d31ffa4ede6a325c86af13139539beefca10a1234ce",
- "sha256:ac476e6d0128fb7919b3fae726de72b28b5c9644cb4b579e4a523d693187c551",
- "sha256:ac5d92e2cc121a13270697e4cb37e1eb4511ac01d23fe1b6c097facc3b46489e",
- "sha256:adc2d941c0381edfcf3897f94b9f41b1e504902fab78a04b1677f2f72afead4b",
- "sha256:b6ff5be3b1853e0862da9d349fe87f869f68e63a25f7c37ce1130b321140f963",
- "sha256:bb35ae9f134fbd9cf7302a9654d5a1e597c974202678082dcc569eb39a8cde03",
- "sha256:be05bde21d5e6eefbc3a6de6b9bee2b47894b8945342e8663192809c4d1f08ce",
- "sha256:c27df03730059118b8a923cfc8b84b7e9976742560af528242f201880879c1da",
- "sha256:c7719a5e1dc93883a6b319bc0374ecd46fb6091ed659f3fbe281ab991634b9b0",
- "sha256:c86f4c7a6d1a54a24d804d9684d96e36a62d3ef7c0d7745ae2ea39e3e0293251",
- "sha256:ca95d40900cf614e07f00cee8c2fad0371df03ca4d7a80161d84be2ec132b7a4",
- "sha256:cd4839813b09ab1dd1be1bbc74f9a7787615f931f83952b6a9af1b2d3f708bf7",
- "sha256:db4b1a69976b1b02acda15937538a1d3fe10b185f9d99920b17a740a0a102e06",
- "sha256:dbb1a822fd858d9853333a7c95d4e70dde9a79e65893138ce32c2ec6457d7a36",
- "sha256:de6b079b39246a7da9a40cfa62d5766bd52b4b7a88cf5a82ec4c45bf6e152306",
- "sha256:df6ff122a0a10a30121d9f0cb3fbd03a6fe05861e4ec47adb9f25e9245aabc19",
- "sha256:e0b0f272901a5172090c0802053fbc503cdc3fa2612720d2669a98a7384a7bec",
- "sha256:e2778be4f574b39ec9dcd9e5e13644f770351ee0990a0ecd27e364aba95af89b",
- "sha256:e3b746fa0ffc5b6b8856529de487da8b9aeb4fb394bb58de6502ef45f3434f12",
- "sha256:e642e6a46a04e992ebfdabed79e46f478ec60e2c528e1e1a074d63800eda4286",
- "sha256:eafea49da254a8289bed3fab960f808b322eda5577cb17a3733014928bbfbebd",
- "sha256:f0f334ae844675420164175bf32b04e18a81fe57ad8eb7e0cfd4689d681ffed7",
- "sha256:f382004fa4c93c01016d9226b9d696a08c53f6818b7ad59b4e96cb67e863353a",
- "sha256:f4679fcc9eb9004fdd1b00231ef1ec7167168071bebc4d66327e28c1979b4449",
- "sha256:fd2fffc8ce8692ce540103dff26279d2af22d424516ddebe2d7e4d6dbb3816b2",
- "sha256:ff136607689c1c87f43d24203b6d2055b42030f352d5176f9c8b204d4235ef27",
- "sha256:ff52b4e2ac0080c96e506819586c4b16cdbf46724bda90d308a7330a73cc8521",
- "sha256:ff562952f15eff27247a4c4b03e45ce8a82e3fb197de6a7c54080f9d4ba07845"
- ],
- "index": "pypi",
- "markers": "python_version >= '3.9'",
- "version": "==7.6.11"
- },
- "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:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c",
- "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==1.0.7"
- },
- "httpx": {
- "hashes": [
- "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc",
- "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==0.28.1"
- },
- "idna": {
- "hashes": [
- "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9",
- "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"
- ],
- "markers": "python_version >= '3.6'",
- "version": "==3.10"
- },
- "iniconfig": {
- "hashes": [
- "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3",
- "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==2.0.0"
- },
- "packaging": {
- "hashes": [
- "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759",
- "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==24.2"
- },
- "pluggy": {
- "hashes": [
- "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1",
- "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==1.5.0"
- },
- "pytest": {
- "hashes": [
- "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6",
- "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"
- ],
- "index": "pypi",
- "markers": "python_version >= '3.8'",
- "version": "==8.3.4"
- },
- "pytest-asyncio": {
- "hashes": [
- "sha256:9e89518e0f9bd08928f97a3482fdc4e244df17529460bc038291ccaf8f85c7c3",
- "sha256:fc1da2cf9f125ada7e710b4ddad05518d4cee187ae9412e9ac9271003497f07a"
- ],
- "index": "pypi",
- "markers": "python_version >= '3.9'",
- "version": "==0.25.3"
- },
- "sniffio": {
- "hashes": [
- "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2",
- "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==1.3.1"
- },
- "tomli": {
- "hashes": [
- "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6",
- "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd",
- "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c",
- "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b",
- "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8",
- "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6",
- "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77",
- "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff",
- "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea",
- "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192",
- "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249",
- "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee",
- "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4",
- "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98",
- "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8",
- "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4",
- "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281",
- "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744",
- "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69",
- "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13",
- "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140",
- "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e",
- "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e",
- "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc",
- "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff",
- "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec",
- "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2",
- "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222",
- "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106",
- "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272",
- "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a",
- "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==2.2.1"
- },
- "typing-extensions": {
- "hashes": [
- "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d",
- "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==4.12.2"
- },
- "watchdog": {
- "hashes": [
- "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a",
- "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2",
- "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f",
- "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c",
- "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c",
- "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c",
- "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0",
- "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13",
- "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134",
- "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa",
- "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e",
- "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379",
- "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a",
- "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11",
- "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282",
- "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b",
- "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f",
- "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c",
- "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112",
- "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948",
- "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881",
- "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860",
- "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3",
- "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680",
- "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26",
- "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26",
- "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e",
- "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8",
- "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c",
- "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"
- ],
- "index": "pypi",
- "markers": "python_version >= '3.9'",
- "version": "==6.0.0"
- }
- }
-}
From e422e1126c099ed1c970f655549d8d491937d361 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Wed, 12 Feb 2025 10:17:46 +0000
Subject: [PATCH 51/75] license year bump
---
LICENSE | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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
From 119faf330dd46f87c3ef9a5cdae4993cd851797b Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Wed, 12 Feb 2025 10:18:03 +0000
Subject: [PATCH 52/75] fixing healthchecks
---
docker-compose.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docker-compose.yml b/docker-compose.yml
index 723f225..2b59517 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -22,7 +22,7 @@ services:
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
@@ -49,7 +49,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
From f587a17a93fa6aa39307ea315bfed48133f449b3 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Wed, 12 Feb 2025 13:24:11 +0000
Subject: [PATCH 53/75] introduces low/high priority queue and custom
concurrency
---
.env.example | 3 +++
app/shared/task_messaging.py | 8 ++++++--
app/web/db/crud.py | 22 +++++++++++++++++-----
app/web/db/user_state.py | 17 +++++++++++++++--
app/web/endpoints/sheet.py | 3 ++-
app/web/endpoints/url.py | 3 ++-
app/web/events.py | 3 ++-
app/web/utils/misc.py | 8 ++++++++
app/worker/main.py | 4 ++--
docker-compose.dev.yml | 2 +-
docker-compose.yml | 2 +-
11 files changed, 59 insertions(+), 16 deletions(-)
diff --git a/.env.example b/.env.example
index 46cc8cc..28b0ebc 100644
--- a/.env.example
+++ b/.env.example
@@ -33,3 +33,6 @@ MAIL_PORT=587
MAIL_STARTTLS=False
MAIL_SSL_TLS=True
+
+# celery workers config
+CONCURRENCY=2
\ No newline at end of file
diff --git a/app/shared/task_messaging.py b/app/shared/task_messaging.py
index 7f0f09e..21fb3d1 100644
--- a/app/shared/task_messaging.py
+++ b/app/shared/task_messaging.py
@@ -5,13 +5,17 @@ import redis
from app.shared.settings import get_settings
+
@lru_cache
-def get_celery(name:str="") -> Celery:
+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_connection_retry_on_startup=False,
+ broker_transport_options={
+ 'queue_order_strategy': 'priority',
+ }
)
diff --git a/app/web/db/crud.py b/app/web/db/crud.py
index be2a915..e3a78a0 100644
--- a/app/web/db/crud.py
+++ b/app/web/db/crud.py
@@ -12,6 +12,7 @@ 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
@@ -21,12 +22,14 @@ def get_limit(user_limit: int):
# --------------- 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 get_archive(db: Session, id: str, email: str):
query = base_query(db).filter(models.Archive.id == id)
if email != ALLOW_ANY_EMAIL:
@@ -34,7 +37,8 @@ def get_archive(db: Session, id: str, email: str):
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)-> list[models.Archive]:
+
+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) -> list[models.Archive]:
# searches for partial URLs, if email is * no ownership filtering happens
query = base_query(db)
if email != ALLOW_ANY_EMAIL:
@@ -84,13 +88,15 @@ def count_by_user_since(db: Session, seconds_delta: int = 15):
.order_by(func.count().desc())\
.limit(500).all()
-async def find_by_store_until(db: AsyncSession, store_until_is_before:datetime) -> dict:
- res = await db.execute(
+
+async def find_by_store_until(db: AsyncSession, store_until_is_before: datetime) -> dict:
+ res = await db.execute(
select(models.Archive)
- .filter(models.Archive.deleted ==False, models.Archive.store_until < store_until_is_before)
+ .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
@@ -107,6 +113,11 @@ def is_user_in_group(email: str, group_name: str) -> models.Group:
return len(group_name) and len(email) and group_name in get_user_groups(email)
+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)
+
@lru_cache
def get_user_groups(email: str) -> list[str]:
"""
@@ -129,7 +140,7 @@ def get_user_groups(email: str) -> list[str]:
# --------------- 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:
+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)
@@ -238,6 +249,7 @@ async def get_sheets_by_id_hash(db: AsyncSession, frequency: str, modulo: str, i
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(
diff --git a/app/web/db/user_state.py b/app/web/db/user_state.py
index a97df37..52e17e4 100644
--- a/app/web/db/user_state.py
+++ b/app/web/db/user_state.py
@@ -9,6 +9,8 @@ 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:
"""
@@ -261,7 +263,7 @@ class UserState:
else:
if group_id not in self.permissions: return False
quota = self.permissions[group_id].max_monthly_urls
-
+
if quota == -1:
return True
@@ -269,6 +271,7 @@ class UserState:
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()
@@ -288,13 +291,14 @@ class UserState:
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(
@@ -327,3 +331,12 @@ class UserState:
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/app/web/endpoints/sheet.py b/app/web/endpoints/sheet.py
index 89699d0..9177668 100644
--- a/app/web/endpoints/sheet.py
+++ b/app/web/endpoints/sheet.py
@@ -75,6 +75,7 @@ def archive_user_sheet(
if not user.can_manually_trigger(sheet.group_id):
raise HTTPException(status_code=429, detail="User cannot manually trigger sheet archiving in this group.")
- task = celery.signature("create_sheet_task", args=[schemas.SubmitSheet(sheet_id=id, author_id=user.email, group_id=sheet.group_id).model_dump_json()]).delay()
+ 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/app/web/endpoints/url.py b/app/web/endpoints/url.py
index 98f905c..e7f7000 100644
--- a/app/web/endpoints/url.py
+++ b/app/web/endpoints/url.py
@@ -43,7 +43,8 @@ def archive_url(
archive_create = schemas.ArchiveCreate(**archive.model_dump())
- task = celery.signature("create_archive_task", args=[archive_create.model_dump_json()]).delay()
+ group_queue = user.priority_group(archive_create.group_id)
+ 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)
diff --git a/app/web/events.py b/app/web/events.py
index 4dfdf25..101d055 100644
--- a/app/web/events.py
+++ b/app/web/events.py
@@ -81,7 +81,8 @@ async def archive_sheets_cronjob(frequency: str, interval: int, current_time_uni
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:
- 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 = 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.info(f"[CRON {frequency.upper()}:{current_time_unit}] Triggered {len(triggered_jobs)} sheet tasks: {triggered_jobs}")
diff --git a/app/web/utils/misc.py b/app/web/utils/misc.py
index cfa856b..870a60b 100644
--- a/app/web/utils/misc.py
+++ b/app/web/utils/misc.py
@@ -1,7 +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/main.py b/app/worker/main.py
index 8ced19b..f5f6c7d 100644
--- a/app/worker/main.py
+++ b/app/worker/main.py
@@ -27,7 +27,6 @@ USER_GROUPS_FILENAME = settings.USER_GROUPS_FILENAME
# 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': 0})
def create_archive_task(self, archive_json: str):
- logger.info(archive_json)
archive = schemas.ArchiveCreate.model_validate_json(archive_json)
# call auto-archiver
@@ -49,7 +48,8 @@ def create_archive_task(self, archive_json: str):
@celery.task(name="create_sheet_task", bind=True)
def create_sheet_task(self, sheet_json: str):
sheet = schemas.SubmitSheet.model_validate_json(sheet_json)
- logger.info(f"SHEET START {sheet=}")
+ queue_name = create_sheet_task.request.delivery_info.get('routing_key', 'No queue info')
+ logger.info(f"[queue={queue_name}] SHEET START {sheet=}")
orchestrator = load_orchestrator(sheet.group_id, True, {"configurations": {"gsheet_feeder": {"sheet_id": sheet.sheet_id}}})
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index 3a7129d..82e81e3 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -14,7 +14,7 @@ services:
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
+ 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
restart: "no"
env_file: .env.dev
volumes:
diff --git a/docker-compose.yml b/docker-compose.yml
index 2b59517..90b5f28 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -33,7 +33,7 @@ services:
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
+ command: celery --app=app.worker.main.celery worker --loglevel=warning --logfile=/aa-api/logs/celery.log -Q high_priority,low_priority --concurrency=$CONCURRENCY
volumes:
- ./logs:/aa-api/logs
- ./database:/aa-api/database
From 2892efaa97ffdf2658d787d7dda2c278b3a57b67 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Wed, 12 Feb 2025 13:29:55 +0000
Subject: [PATCH 54/75] drop deprecated endpoints/schemas
---
app/shared/schemas.py | 16 ------
app/web/main.py | 119 +-----------------------------------------
2 files changed, 2 insertions(+), 133 deletions(-)
diff --git a/app/shared/schemas.py b/app/shared/schemas.py
index f860f45..2a91fd7 100644
--- a/app/shared/schemas.py
+++ b/app/shared/schemas.py
@@ -9,19 +9,6 @@ class SubmitSheet(BaseModel):
author_id: str | None = None
group_id: str = "default"
tags: set[str] | None = set()
- columns: dict | None = {} # TODO: implement/remove
-
-
-class SubmitManual(BaseModel): # deprecated
- 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 REQUESTS BELOW
-# TODO: replace existing schemas with these
-
class ArchiveUrl(BaseModel):
url: str
@@ -30,9 +17,6 @@ class ArchiveUrl(BaseModel):
group_id: str | None
tags: set[str] | None = set()
-# API RESPONSES BELOW
-
-
class ArchiveResult(BaseModel):
id: str
url: str
diff --git a/app/web/main.py b/app/web/main.py
index 3bd8962..cc7774c 100644
--- a/app/web/main.py
+++ b/app/web/main.py
@@ -1,24 +1,15 @@
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 import FastAPI, Depends
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from prometheus_fastapi_instrumentator import Instrumentator
-from datetime import datetime
-from sqlalchemy.orm import Session
from loguru import logger
-from app.shared.log import log_error
from app.web.middleware import logging_middleware
-from app.shared import schemas
from app.shared.task_messaging import get_celery
-from app.web.db import crud
-from app.web.security import get_user_auth, token_api_key_auth, get_token_or_user_auth
+from app.web.security import token_api_key_auth
from app.web.config import VERSION, API_DESCRIPTION
-from app.shared.db.database import get_db_dependency
from app.web.events import lifespan
from app.shared.settings import get_settings
@@ -66,110 +57,4 @@ def app_factory(settings = get_settings()):
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 = celery.signature("create_archive_task", args=[archive.model_dump_json()]).delay()
- 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), db: Session = Depends(get_db_dependency)):
- logger.info(f"SHEET TASK for {sheet=}")
- sheet.author_id = email
- #NB: no longer working
- if not crud.is_user_in_group(email, sheet.group_id):
- raise HTTPException(status_code=403, detail="User does not have access to this group.")
- task = celery.signature("create_sheet_task", args=[sheet.model_dump_json()]).delay()
- 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"
-
- task = celery.signature("create_sheet_task", args=[sheet.model_dump_json()]).delay()
- 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)):
- raise HTTPException(status_code=410, detail="This endpoint is deprecated. Use /interop/submit-archive instead.")
- # 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
From 77130e28c5361852233dfd65712752907b8e45c6 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Wed, 12 Feb 2025 15:56:26 +0000
Subject: [PATCH 55/75] refactor scheduled archives deletion
---
.env.example | 2 +-
app/shared/settings.py | 2 +-
app/web/db/crud.py | 2 +-
app/web/events.py | 12 ++++++------
4 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/.env.example b/.env.example
index 28b0ebc..ef3935a 100644
--- a/.env.example
+++ b/.env.example
@@ -18,7 +18,7 @@ CRON_ARCHIVE_SHEETS=true
CRON_DELETE_STALE_SHEETS=true
DELETE_STALE_SHEETS_DAYS=7
CRON_DELETE_SCHEDULED_ARCHIVES=false
-DELETE_SCHEDULED_ARCHIVES_NOTIFY_DAYS=14
+DELETE_SCHEDULED_ARCHIVES_CHECK_EVERY_N_DAYS=14
# observability for prometheus
REPEAT_COUNT_METRICS_SECONDS=30
diff --git a/app/shared/settings.py b/app/shared/settings.py
index da277f2..866b23a 100644
--- a/app/shared/settings.py
+++ b/app/shared/settings.py
@@ -43,7 +43,7 @@ class Settings(BaseSettings):
CRON_DELETE_STALE_SHEETS: bool = False
DELETE_STALE_SHEETS_DAYS: int = 14
CRON_DELETE_SCHEDULED_ARCHIVES: bool = False
- DELETE_SCHEDULED_ARCHIVES_NOTIFY_DAYS: int = 14
+ DELETE_SCHEDULED_ARCHIVES_CHECK_EVERY_N_DAYS: int = 7
# observability
REPEAT_COUNT_METRICS_SECONDS: int = 30
diff --git a/app/web/db/crud.py b/app/web/db/crud.py
index e3a78a0..25ef72b 100644
--- a/app/web/db/crud.py
+++ b/app/web/db/crud.py
@@ -89,7 +89,7 @@ def count_by_user_since(db: Session, seconds_delta: int = 15):
.limit(500).all()
-async def find_by_store_until(db: AsyncSession, store_until_is_before: datetime) -> dict:
+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)
diff --git a/app/web/events.py b/app/web/events.py
index 101d055..383a210 100644
--- a/app/web/events.py
+++ b/app/web/events.py
@@ -89,12 +89,12 @@ async def archive_sheets_cronjob(frequency: str, interval: int, current_time_uni
# TODO: on exception should logerror but also prometheus counter
-DELETE_WINDOW = get_settings().DELETE_SCHEDULED_ARCHIVES_NOTIFY_DAYS * 24 * 60 * 60
+DELETE_WINDOW = get_settings().DELETE_SCHEDULED_ARCHIVES_CHECK_EVERY_N_DAYS * 24 * 60 * 60
@repeat_every(seconds=DELETE_WINDOW, wait_first=180, on_exception=logger.error)
async def notify_about_expired_archives():
- notify_from = datetime.datetime.now() + datetime.timedelta(days=get_settings().DELETE_SCHEDULED_ARCHIVES_NOTIFY_DAYS)
+ 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)
@@ -106,7 +106,7 @@ async def notify_about_expired_archives():
fastmail = FastMail(get_settings().MAIL_CONFIG)
# notify users
for email in user_archives:
- list_of_archives = "\n".join([f'{a.url},{a.id} ' for a in user_archives[email]])
+ 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",
@@ -115,11 +115,11 @@ async def notify_about_expired_archives():
Hi {email},
- Some of your archives will be deleted in the next {get_settings().DELETE_SCHEDULED_ARCHIVES_NOTIFY_DAYS} days, as they are reaching their expiration date according to our retention policy for their groups.
+ 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
+ url,archive_id,time_of_deletion
{list_of_archives}
Best, The Auto Archiver team
@@ -135,7 +135,7 @@ async def notify_about_expired_archives():
asyncio.create_task(delete_expired_archives())
-@repeat_every(max_repetitions=1, wait_first=DELETE_WINDOW - (60 * 60), seconds=0, on_exception=logger.error)
+@repeat_every(max_repetitions=1, wait_first=10, seconds=0, on_exception=logger.error)
async def delete_expired_archives():
async with get_db_async() as db:
count_deleted = await crud.soft_delete_expired_archives(db)
From baf5ae0f733166aba180b49980a87cd7fe428729 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Wed, 12 Feb 2025 16:35:29 +0000
Subject: [PATCH 56/75] fixes tests
---
app/tests/web/endpoints/test_sheet.py | 4 ++--
app/tests/web/endpoints/test_url.py | 12 ++++++------
app/web/endpoints/url.py | 8 ++++++--
app/web/events.py | 7 +++++++
app/worker/main.py | 2 +-
5 files changed, 22 insertions(+), 11 deletions(-)
diff --git a/app/tests/web/endpoints/test_sheet.py b/app/tests/web/endpoints/test_sheet.py
index aedecac..1396d85 100644
--- a/app/tests/web/endpoints/test_sheet.py
+++ b/app/tests/web/endpoints/test_sheet.py
@@ -151,14 +151,14 @@ class TestArchiveUserSheetEndpoint:
db_session.commit()
m_signature = MagicMock()
- m_signature.delay.return_value = TaskResult(id="123-taskid", status="PENDING", result="")
+ 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.delay.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")
diff --git a/app/tests/web/endpoints/test_url.py b/app/tests/web/endpoints/test_url.py
index 008973f..b8bc15b 100644
--- a/app/tests/web/endpoints/test_url.py
+++ b/app/tests/web/endpoints/test_url.py
@@ -12,7 +12,7 @@ def test_archive_url_unauthenticated(client, test_no_auth):
@patch("app.web.endpoints.url.celery", return_value=MagicMock())
def test_archive_url(m_celery, m2, client_with_auth):
m_signature = MagicMock()
- m_signature.delay.return_value = TaskResult(id="123-456-789", status="PENDING", result="")
+ 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()
@@ -36,7 +36,7 @@ def test_archive_url(m_celery, m2, client_with_auth):
assert response.status_code == 201
assert response.json() == {'id': '123-456-789'}
m_celery.signature.assert_called_once()
- m_signature.delay.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}
@@ -57,7 +57,7 @@ def test_archive_url(m_celery, m2, client_with_auth):
assert response.status_code == 201
assert response.json() == {'id': '123-456-789'}
assert m_celery.signature.call_count == 2
- assert m_signature.delay.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")
@@ -78,7 +78,7 @@ def test_archive_url(m_celery, m2, client_with_auth):
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.delay.call_count == 2
+ assert m_signature.apply_async.call_count == 2
@patch("app.web.endpoints.url.UserState")
@@ -105,13 +105,13 @@ def test_archive_url_quotas(m1, client_with_auth):
@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.delay.return_value = TaskResult(id="123-456-789", status="PENDING", result="")
+ 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.delay.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"
diff --git a/app/web/endpoints/url.py b/app/web/endpoints/url.py
index e7f7000..c0c1284 100644
--- a/app/web/endpoints/url.py
+++ b/app/web/endpoints/url.py
@@ -15,6 +15,8 @@ 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()
@@ -32,6 +34,7 @@ def 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):
@@ -40,10 +43,11 @@ def archive_url(
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")
- archive_create = schemas.ArchiveCreate(**archive.model_dump())
- group_queue = user.priority_group(archive_create.group_id)
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)
diff --git a/app/web/events.py b/app/web/events.py
index 383a210..5cba67d 100644
--- a/app/web/events.py
+++ b/app/web/events.py
@@ -176,3 +176,10 @@ async def delete_stale_sheets():
)
await fastmail.send_message(message)
logger.info(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/worker/main.py b/app/worker/main.py
index f5f6c7d..2d946b1 100644
--- a/app/worker/main.py
+++ b/app/worker/main.py
@@ -48,7 +48,7 @@ def create_archive_task(self, archive_json: str):
@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.get('routing_key', 'No queue info')
+ queue_name = (create_sheet_task.request.delivery_info or {}).get('routing_key', 'unknown')
logger.info(f"[queue={queue_name}] SHEET START {sheet=}")
orchestrator = load_orchestrator(sheet.group_id, True, {"configurations": {"gsheet_feeder": {"sheet_id": sheet.sheet_id}}})
From df8f53ef35645bf6b337191f3733acf54226a049 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Wed, 12 Feb 2025 17:34:20 +0000
Subject: [PATCH 57/75] log info->debug
---
app/web/events.py | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/app/web/events.py b/app/web/events.py
index 5cba67d..88bb18f 100644
--- a/app/web/events.py
+++ b/app/web/events.py
@@ -85,7 +85,7 @@ async def archive_sheets_cronjob(frequency: str, interval: int, current_time_uni
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.info(f"[CRON {frequency.upper()}:{current_time_unit}] Triggered {len(triggered_jobs)} sheet tasks: {triggered_jobs}")
+ 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
@@ -129,7 +129,7 @@ async def notify_about_expired_archives():
subtype=MessageType.html
)
await fastmail.send_message(message)
- logger.info(f"[CRON] Email sent to {email} about {len(user_archives[email])} scheduled archives deletion.")
+ 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())
@@ -140,13 +140,13 @@ 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.info(f"[CRON] Deleted {count_deleted} archives.")
+ logger.debug(f"[CRON] Deleted {count_deleted} archives.")
@repeat_every(seconds=86400, wait_first=150, on_exception=logger.error)
async def delete_stale_sheets():
STALE_DAYS = get_settings().DELETE_STALE_SHEETS_DAYS
- logger.info(f"[CRON] Deleting stale sheets older than {STALE_DAYS} 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)
@@ -175,7 +175,7 @@ async def delete_stale_sheets():
subtype=MessageType.html
)
await fastmail.send_message(message)
- logger.info(f"[CRON] Email sent to {email} about stale sheets deletion.")
+ logger.debug(f"[CRON] Email sent to {email} about stale sheets deletion.")
# @repeat_at
From a3b1adb28d4b28ea0961c271bdd1ddf755b55077 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Thu, 13 Feb 2025 00:06:24 +0000
Subject: [PATCH 58/75] documents and simplifies how .env and .user-groups are
passed to the images
---
.gitignore | 5 +-
README.md | 63 +++++++++++++++----
app/example.user-groups.yaml | 18 ------
docker-compose.dev.yml | 6 +-
docker-compose.yml | 2 +
...s.example.yaml => user-groups.example.yaml | 0
web.Dockerfile | 5 +-
worker.Dockerfile | 3 +-
8 files changed, 65 insertions(+), 37 deletions(-)
delete mode 100644 app/example.user-groups.yaml
rename app/user-groups.example.yaml => user-groups.example.yaml (100%)
diff --git a/.gitignore b/.gitignore
index e937aa2..7e92e36 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
+user-groups.dev.yaml
+user-groups.yaml
orchestration.yaml
my-archives
*.pyc
@@ -23,4 +25,5 @@ local_archive
local_archive_test
*db-wal
*db-shm
-copy-files.sh
\ No newline at end of file
+copy-files.sh
+temp/
\ No newline at end of file
diff --git a/README.md b/README.md
index 18c553f..b292107 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,43 @@
[](https://github.com/bellingcat/auto-archiver-api/actions/workflows/ci.yaml)
-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).
+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).
+### setup
+To properly set up the API you need to install `docker` and to edit 2 files:
+1. a `.env` to configure the API, stays at the root level
+2. a `user-groups.yaml` to manage user permissions
+Do not commit those files, they are .gitignored by default.
+
+We have examples for both of those, and here's how to set them up whether you're in development or production:
+
+#### 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
+
+#### 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
@@ -97,6 +132,16 @@ orchestrators:
## Database migrations
check https://alembic.sqlalchemy.org/en/latest/tutorial.html#the-migration-environment
+```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
+```
* 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:
@@ -127,16 +172,12 @@ curl -XPOST -H "Authorization: Bearer GOOGLE_OAUTH_TOKEN" -H "Content-type: appl
### 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
+poetry run coverage report
+# get coverage report in HTML format
+poetry run coverage html
```
diff --git a/app/example.user-groups.yaml b/app/example.user-groups.yaml
deleted file mode 100644
index 707e39f..0000000
--- a/app/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/docker-compose.dev.yml b/docker-compose.dev.yml
index 82e81e3..2151953 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -4,7 +4,8 @@ services:
restart: "no"
env_file: .env.dev
volumes:
- - ./app:/aa-api/app # for --reload to work
+ - ./app/web:/aa-api/app/web # for --reload to work
+ - ./app/shared:/aa-api/app/shared # for --reload to work
environment:
- ENVIRONMENT_FILE=.env.dev
- SERVE_LOCAL_ARCHIVE=/aa-api/app/local_archive # See orchestration.yaml local_storage.save_to
@@ -18,7 +19,8 @@ services:
restart: "no"
env_file: .env.dev
volumes:
- - ./app:/aa-api/app # for watchmedo
+ - ./app/worker:/aa-api/app/worker # for watchmedo to work
+ - ./app/shared:/aa-api/app/shared # for watchmedo to work
redis:
restart: "no"
diff --git a/docker-compose.yml b/docker-compose.yml
index 90b5f28..7730960 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -19,6 +19,7 @@ services:
volumes:
- ./logs:/aa-api/logs
- ./database:/aa-api/database
+ - ./secrets:/aa-api/secrets
depends_on:
- redis
healthcheck:
@@ -37,6 +38,7 @@ services:
volumes:
- ./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:
diff --git a/app/user-groups.example.yaml b/user-groups.example.yaml
similarity index 100%
rename from app/user-groups.example.yaml
rename to user-groups.example.yaml
diff --git a/web.Dockerfile b/web.Dockerfile
index 14a751b..d142877 100644
--- a/web.Dockerfile
+++ b/web.Dockerfile
@@ -13,11 +13,10 @@ 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
+# Copy the application code and configurations
COPY alembic.ini ./
-COPY .env* ./app/
-COPY ./secrets/ ./secrets/
COPY ./app/ ./app/
+COPY user-groups.* ./app/
# Run the FastAPI app with Uvicorn
ENTRYPOINT ["poetry", "run"]
diff --git a/worker.Dockerfile b/worker.Dockerfile
index 4c70730..99ddf8a 100644
--- a/worker.Dockerfile
+++ b/worker.Dockerfile
@@ -27,8 +27,7 @@ RUN ./poetry-venv/bin/poetry install --without dev --no-root --no-cache
# copy source code and .env files over
COPY alembic.ini ./
-COPY .env* ./app/
-COPY ./secrets/ ./secrets/
COPY ./app/ ./app/
+COPY user-groups.* ./app/
ENTRYPOINT ["./poetry-venv/bin/poetry", "run"]
\ No newline at end of file
From 4341b80dfcfada370a047aee5746b6c4c44f1546 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Thu, 13 Feb 2025 00:07:02 +0000
Subject: [PATCH 59/75] fixes bad db usage without proper connection closing
leading to https://docs.sqlalchemy.org/en/20/errors.html#error-3o7r
---
app/web/security.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/app/web/security.py b/app/web/security.py
index 4e5214f..12115af 100644
--- a/app/web/security.py
+++ b/app/web/security.py
@@ -2,10 +2,11 @@ from loguru import logger
import requests, secrets
from fastapi import HTTPException, status, Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
+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
+from app.shared.db.database import get_db_dependency
from app.web.db.user_state import UserState
settings = get_settings()
@@ -78,6 +79,5 @@ def authenticate_user(access_token):
return False, "exception occurred"
-def get_user_state(email=Depends(get_user_auth)):
- with get_db() as db:
- return UserState(db, email)
\ No newline at end of file
+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
From 8b0f0023b13ac8f6b1cf0bea765e5d61afad195a Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Thu, 13 Feb 2025 00:07:15 +0000
Subject: [PATCH 60/75] setting default db pool and verflow sizes
---
app/shared/db/database.py | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/app/shared/db/database.py b/app/shared/db/database.py
index d404b5c..171b97b 100644
--- a/app/shared/db/database.py
+++ b/app/shared/db/database.py
@@ -9,7 +9,13 @@ 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})
+ 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:
@@ -37,6 +43,7 @@ def get_db_dependency():
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
From 4b3d47803a963c1e7d45e84a2abf6fef7a1a1bdf Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Thu, 13 Feb 2025 00:08:15 +0000
Subject: [PATCH 61/75] force the old extension version to display an outdated
version message
---
app/web/config.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/web/config.py b/app/web/config.py
index 13eed55..548e02c 100644
--- a/app/web/config.py
+++ b/app/web/config.py
@@ -8,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 = "*"
From dcb8ee47d8eb66c8c70ae57eedfe986ed21ded35 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Thu, 13 Feb 2025 00:35:09 +0000
Subject: [PATCH 62/75] drops group info from default endpoint
---
app/tests/web/endpoints/test_default.py | 26 -------------------------
app/tests/web/test_main.py | 2 +-
app/web/endpoints/default.py | 17 ++++------------
3 files changed, 5 insertions(+), 40 deletions(-)
diff --git a/app/tests/web/endpoints/test_default.py b/app/tests/web/endpoints/test_default.py
index 6de2f01..1e0e49f 100644
--- a/app/tests/web/endpoints/test_default.py
+++ b/app/tests/web/endpoints/test_default.py
@@ -13,32 +13,6 @@ def test_endpoint_home(client_with_auth):
assert "breakingChanges" in j
assert "groups" not in j
-
-@patch("app.web.endpoints.default.bearer_security", new_callable=AsyncMock)
-@patch("app.web.endpoints.default.get_user_auth", new_callable=AsyncMock, return_value="test@example.com")
-@patch("app.web.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("app.web.endpoints.default.bearer_security", new_callable=AsyncMock)
-@patch("app.web.endpoints.default.get_user_auth", new_callable=AsyncMock, return_value="test@example.com")
-@patch("app.web.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
diff --git a/app/tests/web/test_main.py b/app/tests/web/test_main.py
index 95cc5d6..8cee578 100644
--- a/app/tests/web/test_main.py
+++ b/app/tests/web/test_main.py
@@ -17,7 +17,7 @@ def test_alembic(db_session):
alembic.config.main(argv=['--raiseerr', 'upgrade', 'head'])
alembic.config.main(argv=['--raiseerr', 'downgrade', 'base'])
-@patch("app.web.endpoints.default.crud.soft_delete_task", side_effect=Exception('mocked error'))
+@patch("app.web.endpoints.url.crud.soft_delete_task", side_effect=Exception('mocked error'))
def test_logging_middleware(m1, client_with_auth):
from app.web.utils.metrics import EXCEPTION_COUNTER
assert len(EXCEPTION_COUNTER.collect()[0].samples) == 0
diff --git a/app/web/endpoints/default.py b/app/web/endpoints/default.py
index 2535ae6..9271992 100644
--- a/app/web/endpoints/default.py
+++ b/app/web/endpoints/default.py
@@ -1,29 +1,20 @@
from typing import Dict
-from fastapi import APIRouter, Depends, Request, HTTPException
+from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import FileResponse, JSONResponse
from app.web.config import VERSION, BREAKING_CHANGES
-from app.shared.log import log_error
-from app.web.db import crud
from app.shared.schemas import ActiveUser, UsageResponse
from app.web.db.user_state import UserState
-from app.web.security import get_user_auth, bearer_security, get_user_state
+from app.web.security import get_user_state
from app.shared.user_groups import GroupInfo
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, necessary only for the extension
- status = {"version": VERSION, "breakingChanges": BREAKING_CHANGES}
- try:
- email = await get_user_auth(await bearer_security(request))
- status["groups"] = crud.get_user_groups(email)
- except HTTPException: pass # not authenticated is fine
- except Exception as e: log_error(e)
- return JSONResponse(status)
+async def home():
+ return JSONResponse({"version": VERSION, "breakingChanges": BREAKING_CHANGES})
@default_router.get("/health")
From b7b259909ac143139d6772881865db79caeeec44 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Fri, 14 Feb 2025 13:05:18 +0000
Subject: [PATCH 63/75] ignores .python-version
---
.gitignore | 3 ++-
.python-version | 1 -
2 files changed, 2 insertions(+), 2 deletions(-)
delete mode 100644 .python-version
diff --git a/.gitignore b/.gitignore
index 7e92e36..f002cf0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,4 +26,5 @@ local_archive_test
*db-wal
*db-shm
copy-files.sh
-temp/
\ No newline at end of file
+temp/
+.python-version
\ No newline at end of file
diff --git a/.python-version b/.python-version
deleted file mode 100644
index c8cfe39..0000000
--- a/.python-version
+++ /dev/null
@@ -1 +0,0 @@
-3.10
From 1a3078055bf2f193073558aeaec8592ce80f8f9c Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Fri, 14 Feb 2025 13:07:25 +0000
Subject: [PATCH 64/75] minor fixes and todos
---
app/shared/db/worker_crud.py | 2 +-
app/tests/web/endpoints/test_default.py | 2 +-
app/web/middleware.py | 2 ++
3 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/app/shared/db/worker_crud.py b/app/shared/db/worker_crud.py
index 93962b9..6766786 100644
--- a/app/shared/db/worker_crud.py
+++ b/app/shared/db/worker_crud.py
@@ -55,7 +55,7 @@ def create_task(db: Session, task: schemas.ArchiveCreate, tags: list[models.Tag]
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]
+ db_tags = [create_tag(db, tag) for tag in (archive.tags or [])]
# insert everything
db_task = create_task(db, task=archive, tags=db_tags, urls=archive.urls)
return db_task
diff --git a/app/tests/web/endpoints/test_default.py b/app/tests/web/endpoints/test_default.py
index 1e0e49f..b4ed7a5 100644
--- a/app/tests/web/endpoints/test_default.py
+++ b/app/tests/web/endpoints/test_default.py
@@ -1,4 +1,4 @@
-from unittest.mock import AsyncMock, MagicMock, patch
+from unittest.mock import MagicMock
from fastapi.testclient import TestClient
import pytest
from app.web.config import VERSION
diff --git a/app/web/middleware.py b/app/web/middleware.py
index aa5c077..227a620 100644
--- a/app/web/middleware.py
+++ b/app/web/middleware.py
@@ -8,6 +8,8 @@ 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:
From 1a4508df33bdda672a8f63b848a5615570ad2062 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Fri, 14 Feb 2025 23:56:36 +0000
Subject: [PATCH 65/75] minor refactor and user_state tests
---
app/tests/conftest.py | 4 +-
app/tests/web/db/test_crud.py | 41 ---
app/tests/web/db/test_user_state.py | 433 ++++++++++++++++++++++++++++
app/web/db/crud.py | 41 +--
app/web/db/user_state.py | 9 +-
5 files changed, 461 insertions(+), 67 deletions(-)
create mode 100644 app/tests/web/db/test_user_state.py
diff --git a/app/tests/conftest.py b/app/tests/conftest.py
index d488671..37acbf2 100644
--- a/app/tests/conftest.py
+++ b/app/tests/conftest.py
@@ -29,9 +29,9 @@ def mock_settings():
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_groups
+ from app.web.db.crud import get_user_group_names
- get_user_groups.cache_clear()
+ get_user_group_names.cache_clear()
make_engine.cache_clear()
engine = make_engine(get_settings.DATABASE_PATH)
diff --git a/app/tests/web/db/test_crud.py b/app/tests/web/db/test_crud.py
index dbe3dec..c96c74d 100644
--- a/app/tests/web/db/test_crud.py
+++ b/app/tests/web/db/test_crud.py
@@ -231,47 +231,6 @@ def test_count_by_users_since(test_data, db_session):
assert cu[2].total == 33
-def test_is_user_in_group(test_data, db_session):
- from app.web.db import crud
- from app.web.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(email, group) == expected
-
-
-
def test_upsert_group(test_data, db_session):
from app.web.db import crud
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/web/db/crud.py b/app/web/db/crud.py
index 25ef72b..d3be57c 100644
--- a/app/web/db/crud.py
+++ b/app/web/db/crud.py
@@ -5,15 +5,17 @@ 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.database import get_db
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
@@ -33,7 +35,7 @@ def base_query(db: Session):
def get_archive(db: Session, id: str, email: str):
query = base_query(db).filter(models.Archive.id == id)
if email != ALLOW_ANY_EMAIL:
- groups = get_user_groups(email)
+ groups = get_user_group_names(db ,email)
query = query.filter(or_(models.Archive.public == True, models.Archive.author_id == email, models.Archive.group_id.in_(groups)))
return query.first()
@@ -42,7 +44,7 @@ def search_archives_by_url(db: Session, url: str, email: str, skip: int = 0, lim
# searches for partial URLs, if email is * no ownership filtering happens
query = base_query(db)
if email != ALLOW_ANY_EMAIL:
- groups = get_user_groups(email)
+ groups = get_user_group_names(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)
@@ -108,38 +110,39 @@ async def soft_delete_expired_archives(db: AsyncSession) -> dict:
# --------------- TAG
-def is_user_in_group(email: str, group_name: str) -> models.Group:
- if email == ALLOW_ANY_EMAIL: return True
- return len(group_name) and len(email) and group_name in get_user_groups(email)
-
-
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)
-@lru_cache
-def get_user_groups(email: str) -> list[str]:
+
+@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.
"""
if not email or not len(email) or "@" not in email: return []
- with get_db() as db:
- # 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 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]
+ # 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))
+ 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:
diff --git a/app/web/db/user_state.py b/app/web/db/user_state.py
index 52e17e4..968e1bd 100644
--- a/app/web/db/user_state.py
+++ b/app/web/db/user_state.py
@@ -47,15 +47,13 @@ class UserState:
@property
def user_groups_names(self):
if not hasattr(self, '_user_groups_names'):
- self._user_groups_names = crud.get_user_groups(self.email) + ["default"]
+ 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 = self.db.query(models.Group).filter(
- models.Group.id.in_(self.user_groups_names)
- ).all()
+ self._user_groups = crud.get_user_groups_by_name(self.db, self.user_groups_names)
return self._user_groups
@property
@@ -150,8 +148,9 @@ class UserState:
self._priority = "low"
for group in self.user_groups:
if not group.permissions: continue
- if group.permissions.get("priority", "low") == "high":
+ if group.permissions.get("priority", self._priority) == "high":
self._priority = "high"
+ break
return self._priority
@property
From 8f67457d9ce5538a59aa718adeff9832366bf559 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Sat, 15 Feb 2025 23:13:25 +0000
Subject: [PATCH 66/75] temp fix see
https://github.com/bellingcat/auto-archiver/issues/203
---
docker-compose.dev.yml | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index 2151953..a4b42f1 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -15,7 +15,8 @@ services:
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: 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: .env.dev
volumes:
From 00cdec92f9299175fc520db22d1471147fe06284 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Sat, 15 Feb 2025 23:13:32 +0000
Subject: [PATCH 67/75] fixes env setting
---
docker-compose.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docker-compose.yml b/docker-compose.yml
index 7730960..f9d7253 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -34,7 +34,7 @@ services:
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
+ command: celery --app=app.worker.main.celery worker --loglevel=warning --logfile=/aa-api/logs/celery.log -Q high_priority,low_priority --concurrency=${CONCURRENCY}
volumes:
- ./logs:/aa-api/logs
- ./database:/aa-api/database
From 1497485612796b0d4c92cb22033ed231f72f5d23 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Sat, 15 Feb 2025 23:14:53 +0000
Subject: [PATCH 68/75] updates AA adds missing dependencies
---
app/shared/aa_utils.py | 3 +-
app/tests/worker/test_worker_main.py | 36 +-
app/web/endpoints/interoperability.py | 2 +-
app/worker/main.py | 89 +-
poetry.lock | 1384 ++++++-------------------
pyproject.toml | 8 +-
6 files changed, 383 insertions(+), 1139 deletions(-)
diff --git a/app/shared/aa_utils.py b/app/shared/aa_utils.py
index b9c376c..393a975 100644
--- a/app/shared/aa_utils.py
+++ b/app/shared/aa_utils.py
@@ -2,8 +2,7 @@
from typing import List
from loguru import logger
-from auto_archiver import Metadata
-from auto_archiver.core import Media
+from auto_archiver.core import Media, Metadata
from app.shared.db import models
diff --git a/app/tests/worker/test_worker_main.py b/app/tests/worker/test_worker_main.py
index a4b7389..e4dd549 100644
--- a/app/tests/worker/test_worker_main.py
+++ b/app/tests/worker/test_worker_main.py
@@ -7,8 +7,7 @@ import pytest
from app.shared.db import models
from app.shared import schemas
-from auto_archiver import Metadata
-from auto_archiver.core import Media
+from auto_archiver.core import Media, Metadata
@@ -16,22 +15,24 @@ 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.load_orchestrator")
+ @patch("app.worker.main.get_orchestrator_args", return_value=["arg1", "arg2"])
@patch("celery.app.task.Task.request")
- def test_success(self, m_req, m_load, m_store, m_insert, db_session):
+ 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"
- mock_orchestrator = self.mock_orchestrator_choice(m_load)
+ m_orchestrator.run.return_value = Metadata().set_url(self.URL).success()
task = create_archive_task(self.archive.model_dump_json())
- m_load.assert_called_once_with("interstellar")
+ m_args.assert_called_once()
m_store.assert_called_once_with("interstellar")
m_insert.assert_called_once()
- mock_orchestrator.feed_item.assert_called_once()
+ m_orchestrator.run.assert_called_once()
assert task["status"] == "success"
assert task["metadata"]["url"] == self.URL
@@ -43,10 +44,10 @@ class Test_create_archive_task():
create_archive_task(self.archive.model_dump_json())
@patch("app.worker.main.insert_result_into_db", side_effect=Exception)
- @patch("app.worker.main.load_orchestrator")
- def test_raise_db_error(self, m_load, m_insert):
+ @patch("app.worker.main.get_orchestrator_args")
+ def test_raise_db_error(self, m_args, m_insert):
from app.worker.main import create_archive_task
- mock_orchestrator = self.mock_orchestrator_choice(m_load)
+ mock_orchestrator = self.mock_orchestrator_choice(m_args)
with pytest.raises(Exception):
create_archive_task(self.archive.model_dump_json())
@@ -54,10 +55,10 @@ class Test_create_archive_task():
@patch("app.worker.main.insert_result_into_db", return_value=None)
- @patch("app.worker.main.load_orchestrator")
- def test_raise_empty_result(self, m_load, m_insert):
+ @patch("app.worker.main.get_orchestrator_args")
+ def test_raise_empty_result(self, m_args, m_insert):
from app.worker.main import create_archive_task
- mock_orchestrator = self.mock_orchestrator_choice(m_load)
+ mock_orchestrator = self.mock_orchestrator_choice(m_args)
with pytest.raises(Exception) as e:
create_archive_task(self.archive.model_dump_json())
@@ -76,8 +77,8 @@ class Test_create_sheet_task():
@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.load_orchestrator")
- def test_success(self, m_load, m_store, m_uuid, db_session):
+ @patch("app.worker.main.get_orchestrator_args")
+ def test_success(self, m_args, m_store, m_uuid, db_session):
from app.worker.main import create_sheet_task
assert db_session.query(models.Archive).filter(models.Archive.url == self.URL).count() == 0
@@ -86,11 +87,11 @@ class Test_create_sheet_task():
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_load.return_value = m_orch
+ m_args.return_value = m_orch
res = create_sheet_task(self.sheet.model_dump_json())
- m_load.assert_called_once_with("interstellar", True, {'configurations': {'gsheet_feeder': {'sheet_id': '123'}}})
+ m_args.assert_called_once_with("interstellar", True, {'configurations': {'gsheet_feeder': {'sheet_id': '123'}}})
m_orch.feed.assert_called_once()
m_store.assert_called_with("interstellar")
m_store.call_count == 2
@@ -116,7 +117,6 @@ class Test_create_sheet_task():
def test_get_all_urls(db_session):
from app.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"]))
diff --git a/app/web/endpoints/interoperability.py b/app/web/endpoints/interoperability.py
index 81c9a76..085bacc 100644
--- a/app/web/endpoints/interoperability.py
+++ b/app/web/endpoints/interoperability.py
@@ -3,7 +3,7 @@ from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import JSONResponse
from loguru import logger
import sqlalchemy
-from auto_archiver import Metadata
+from auto_archiver.core import Metadata
from sqlalchemy.orm import Session
from app.shared.aa_utils import get_all_urls
diff --git a/app/worker/main.py b/app/worker/main.py
index 2d946b1..c8fb585 100644
--- a/app/worker/main.py
+++ b/app/worker/main.py
@@ -4,8 +4,8 @@ import traceback, datetime
from celery.signals import task_failure
from loguru import logger
from sqlalchemy import exc
-
-from auto_archiver import Config, ArchivingOrchestrator, Metadata
+import auto_archiver
+from auto_archiver.core.orchestrator import ArchivingOrchestrator
from app.shared.db import models
from app.shared.db.database import get_db
@@ -16,7 +16,6 @@ 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")
@@ -24,19 +23,30 @@ Redis = get_redis()
USER_GROUPS_FILENAME = settings.USER_GROUPS_FILENAME
+# 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': 0})
def create_archive_task(self, archive_json: str):
archive = schemas.ArchiveCreate.model_validate_json(archive_json)
# call auto-archiver
- orchestrator = load_orchestrator(archive.group_id)
- result = orchestrator.feed_item(Metadata().set_url(archive.url))
+ args = get_orchestrator_args(archive.group_id, False, [archive.url])
+ # args = get_orchestrator_args(archive.group_id, False, [archive.url, "--extractors", "generic_extractor"])
+ logger.error(args)
+ try:
+ result = next(ArchivingOrchestrator().run(args), None)
+ 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
- store_until = get_store_until(archive.group_id)
- archive.store_until = store_until
+ 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())
@@ -51,32 +61,36 @@ def create_sheet_task(self, sheet_json: str):
queue_name = (create_sheet_task.request.delivery_info or {}).get('routing_key', 'unknown')
logger.info(f"[queue={queue_name}] SHEET START {sheet=}")
- orchestrator = load_orchestrator(sheet.group_id, True, {"configurations": {"gsheet_feeder": {"sheet_id": sheet.sheet_id}}})
+ args = get_orchestrator_args(sheet.group_id, True, ["--gsheet_feeder.sheet_id", sheet.sheet_id])
stats = {"archived": 0, "failed": 0, "errors": []}
- for result in orchestrator.feed():
- try:
- assert result, f"UNABLE TO archive: {result.get_url()}"
- 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))
+ try:
+ for result in ArchivingOrchestrator().run(args):
+ 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:
@@ -87,7 +101,8 @@ def create_sheet_task(self, sheet_json: str):
return schemas.CelerySheetTask(success=True, sheet_id=sheet.sheet_id, time=datetime.datetime.now().isoformat(), stats=stats).model_dump()
-def load_orchestrator(group_id: str, orchestrator_for_sheet: bool = False, overwrite_configs: dict = {}) -> ArchivingOrchestrator:
+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:
@@ -95,11 +110,9 @@ def load_orchestrator(group_id: str, orchestrator_for_sheet: bool = False, overw
else:
orchestrator_fn = worker_crud.get_group(session, group_id).orchestrator
assert orchestrator_fn, f"no orchestrator found for {group_id}"
-
-
- config = Config()
- config.parse(use_cli=False, yaml_config_filename=orchestrator_fn, overwrite_configs=overwrite_configs)
- return ArchivingOrchestrator(config)
+ aa_configs.extend(["--config", orchestrator_fn])
+ aa_configs.extend(cli_args)
+ return aa_configs
def insert_result_into_db(archive: schemas.ArchiveCreate) -> str:
@@ -108,10 +121,12 @@ def insert_result_into_db(archive: schemas.ArchiveCreate) -> str:
logger.debug(f"[ARCHIVE STORED] {db_task.author_id} {db_task.url}")
return db_task.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:
diff --git a/poetry.lock b/poetry.lock
index 0dba0cf..c818604 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,136 +1,5 @@
# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand.
-[[package]]
-name = "aiohappyeyeballs"
-version = "2.4.6"
-description = "Happy Eyeballs for asyncio"
-optional = false
-python-versions = ">=3.9"
-groups = ["main"]
-files = [
- {file = "aiohappyeyeballs-2.4.6-py3-none-any.whl", hash = "sha256:147ec992cf873d74f5062644332c539fcd42956dc69453fe5204195e560517e1"},
- {file = "aiohappyeyeballs-2.4.6.tar.gz", hash = "sha256:9b05052f9042985d32ecbe4b59a77ae19c006a78f1344d7fdad69d28ded3d0b0"},
-]
-
-[[package]]
-name = "aiohttp"
-version = "3.11.12"
-description = "Async http client/server framework (asyncio)"
-optional = false
-python-versions = ">=3.9"
-groups = ["main"]
-files = [
- {file = "aiohttp-3.11.12-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:aa8a8caca81c0a3e765f19c6953416c58e2f4cc1b84829af01dd1c771bb2f91f"},
- {file = "aiohttp-3.11.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:84ede78acde96ca57f6cf8ccb8a13fbaf569f6011b9a52f870c662d4dc8cd854"},
- {file = "aiohttp-3.11.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:584096938a001378484aa4ee54e05dc79c7b9dd933e271c744a97b3b6f644957"},
- {file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:392432a2dde22b86f70dd4a0e9671a349446c93965f261dbaecfaf28813e5c42"},
- {file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:88d385b8e7f3a870146bf5ea31786ef7463e99eb59e31db56e2315535d811f55"},
- {file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b10a47e5390c4b30a0d58ee12581003be52eedd506862ab7f97da7a66805befb"},
- {file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b5263dcede17b6b0c41ef0c3ccce847d82a7da98709e75cf7efde3e9e3b5cae"},
- {file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50c5c7b8aa5443304c55c262c5693b108c35a3b61ef961f1e782dd52a2f559c7"},
- {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1c031a7572f62f66f1257db37ddab4cb98bfaf9b9434a3b4840bf3560f5e788"},
- {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:7e44eba534381dd2687be50cbd5f2daded21575242ecfdaf86bbeecbc38dae8e"},
- {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:145a73850926018ec1681e734cedcf2716d6a8697d90da11284043b745c286d5"},
- {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:2c311e2f63e42c1bf86361d11e2c4a59f25d9e7aabdbdf53dc38b885c5435cdb"},
- {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ea756b5a7bac046d202a9a3889b9a92219f885481d78cd318db85b15cc0b7bcf"},
- {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:526c900397f3bbc2db9cb360ce9c35134c908961cdd0ac25b1ae6ffcaa2507ff"},
- {file = "aiohttp-3.11.12-cp310-cp310-win32.whl", hash = "sha256:b8d3bb96c147b39c02d3db086899679f31958c5d81c494ef0fc9ef5bb1359b3d"},
- {file = "aiohttp-3.11.12-cp310-cp310-win_amd64.whl", hash = "sha256:7fe3d65279bfbee8de0fb4f8c17fc4e893eed2dba21b2f680e930cc2b09075c5"},
- {file = "aiohttp-3.11.12-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:87a2e00bf17da098d90d4145375f1d985a81605267e7f9377ff94e55c5d769eb"},
- {file = "aiohttp-3.11.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b34508f1cd928ce915ed09682d11307ba4b37d0708d1f28e5774c07a7674cac9"},
- {file = "aiohttp-3.11.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:936d8a4f0f7081327014742cd51d320296b56aa6d324461a13724ab05f4b2933"},
- {file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de1378f72def7dfb5dbd73d86c19eda0ea7b0a6873910cc37d57e80f10d64e1"},
- {file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9d45dbb3aaec05cf01525ee1a7ac72de46a8c425cb75c003acd29f76b1ffe94"},
- {file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:930ffa1925393381e1e0a9b82137fa7b34c92a019b521cf9f41263976666a0d6"},
- {file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8340def6737118f5429a5df4e88f440746b791f8f1c4ce4ad8a595f42c980bd5"},
- {file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4016e383f91f2814e48ed61e6bda7d24c4d7f2402c75dd28f7e1027ae44ea204"},
- {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3c0600bcc1adfaaac321422d615939ef300df81e165f6522ad096b73439c0f58"},
- {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:0450ada317a65383b7cce9576096150fdb97396dcfe559109b403c7242faffef"},
- {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:850ff6155371fd802a280f8d369d4e15d69434651b844bde566ce97ee2277420"},
- {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8fd12d0f989c6099e7b0f30dc6e0d1e05499f3337461f0b2b0dadea6c64b89df"},
- {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:76719dd521c20a58a6c256d058547b3a9595d1d885b830013366e27011ffe804"},
- {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:97fe431f2ed646a3b56142fc81d238abcbaff08548d6912acb0b19a0cadc146b"},
- {file = "aiohttp-3.11.12-cp311-cp311-win32.whl", hash = "sha256:e10c440d142fa8b32cfdb194caf60ceeceb3e49807072e0dc3a8887ea80e8c16"},
- {file = "aiohttp-3.11.12-cp311-cp311-win_amd64.whl", hash = "sha256:246067ba0cf5560cf42e775069c5d80a8989d14a7ded21af529a4e10e3e0f0e6"},
- {file = "aiohttp-3.11.12-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e392804a38353900c3fd8b7cacbea5132888f7129f8e241915e90b85f00e3250"},
- {file = "aiohttp-3.11.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8fa1510b96c08aaad49303ab11f8803787c99222288f310a62f493faf883ede1"},
- {file = "aiohttp-3.11.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dc065a4285307607df3f3686363e7f8bdd0d8ab35f12226362a847731516e42c"},
- {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddb31f8474695cd61fc9455c644fc1606c164b93bff2490390d90464b4655df"},
- {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9dec0000d2d8621d8015c293e24589d46fa218637d820894cb7356c77eca3259"},
- {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3552fe98e90fdf5918c04769f338a87fa4f00f3b28830ea9b78b1bdc6140e0d"},
- {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dfe7f984f28a8ae94ff3a7953cd9678550dbd2a1f9bda5dd9c5ae627744c78e"},
- {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a481a574af914b6e84624412666cbfbe531a05667ca197804ecc19c97b8ab1b0"},
- {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1987770fb4887560363b0e1a9b75aa303e447433c41284d3af2840a2f226d6e0"},
- {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a4ac6a0f0f6402854adca4e3259a623f5c82ec3f0c049374133bcb243132baf9"},
- {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c96a43822f1f9f69cc5c3706af33239489a6294be486a0447fb71380070d4d5f"},
- {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a5e69046f83c0d3cb8f0d5bd9b8838271b1bc898e01562a04398e160953e8eb9"},
- {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:68d54234c8d76d8ef74744f9f9fc6324f1508129e23da8883771cdbb5818cbef"},
- {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9fd9dcf9c91affe71654ef77426f5cf8489305e1c66ed4816f5a21874b094b9"},
- {file = "aiohttp-3.11.12-cp312-cp312-win32.whl", hash = "sha256:0ed49efcd0dc1611378beadbd97beb5d9ca8fe48579fc04a6ed0844072261b6a"},
- {file = "aiohttp-3.11.12-cp312-cp312-win_amd64.whl", hash = "sha256:54775858c7f2f214476773ce785a19ee81d1294a6bedc5cc17225355aab74802"},
- {file = "aiohttp-3.11.12-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:413ad794dccb19453e2b97c2375f2ca3cdf34dc50d18cc2693bd5aed7d16f4b9"},
- {file = "aiohttp-3.11.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a93d28ed4b4b39e6f46fd240896c29b686b75e39cc6992692e3922ff6982b4c"},
- {file = "aiohttp-3.11.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d589264dbba3b16e8951b6f145d1e6b883094075283dafcab4cdd564a9e353a0"},
- {file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5148ca8955affdfeb864aca158ecae11030e952b25b3ae15d4e2b5ba299bad2"},
- {file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:525410e0790aab036492eeea913858989c4cb070ff373ec3bc322d700bdf47c1"},
- {file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bd8695be2c80b665ae3f05cb584093a1e59c35ecb7d794d1edd96e8cc9201d7"},
- {file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0203433121484b32646a5f5ea93ae86f3d9559d7243f07e8c0eab5ff8e3f70e"},
- {file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40cd36749a1035c34ba8d8aaf221b91ca3d111532e5ccb5fa8c3703ab1b967ed"},
- {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a7442662afebbf7b4c6d28cb7aab9e9ce3a5df055fc4116cc7228192ad6cb484"},
- {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8a2fb742ef378284a50766e985804bd6adb5adb5aa781100b09befdbfa757b65"},
- {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2cee3b117a8d13ab98b38d5b6bdcd040cfb4181068d05ce0c474ec9db5f3c5bb"},
- {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f6a19bcab7fbd8f8649d6595624856635159a6527861b9cdc3447af288a00c00"},
- {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e4cecdb52aaa9994fbed6b81d4568427b6002f0a91c322697a4bfcc2b2363f5a"},
- {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:30f546358dfa0953db92ba620101fefc81574f87b2346556b90b5f3ef16e55ce"},
- {file = "aiohttp-3.11.12-cp313-cp313-win32.whl", hash = "sha256:ce1bb21fc7d753b5f8a5d5a4bae99566386b15e716ebdb410154c16c91494d7f"},
- {file = "aiohttp-3.11.12-cp313-cp313-win_amd64.whl", hash = "sha256:f7914ab70d2ee8ab91c13e5402122edbc77821c66d2758abb53aabe87f013287"},
- {file = "aiohttp-3.11.12-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7c3623053b85b4296cd3925eeb725e386644fd5bc67250b3bb08b0f144803e7b"},
- {file = "aiohttp-3.11.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:67453e603cea8e85ed566b2700efa1f6916aefbc0c9fcb2e86aaffc08ec38e78"},
- {file = "aiohttp-3.11.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6130459189e61baac5a88c10019b21e1f0c6d00ebc770e9ce269475650ff7f73"},
- {file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9060addfa4ff753b09392efe41e6af06ea5dd257829199747b9f15bfad819460"},
- {file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34245498eeb9ae54c687a07ad7f160053911b5745e186afe2d0c0f2898a1ab8a"},
- {file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8dc0fba9a74b471c45ca1a3cb6e6913ebfae416678d90529d188886278e7f3f6"},
- {file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a478aa11b328983c4444dacb947d4513cb371cd323f3845e53caeda6be5589d5"},
- {file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c160a04283c8c6f55b5bf6d4cad59bb9c5b9c9cd08903841b25f1f7109ef1259"},
- {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:edb69b9589324bdc40961cdf0657815df674f1743a8d5ad9ab56a99e4833cfdd"},
- {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4ee84c2a22a809c4f868153b178fe59e71423e1f3d6a8cd416134bb231fbf6d3"},
- {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:bf4480a5438f80e0f1539e15a7eb8b5f97a26fe087e9828e2c0ec2be119a9f72"},
- {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:e6b2732ef3bafc759f653a98881b5b9cdef0716d98f013d376ee8dfd7285abf1"},
- {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f752e80606b132140883bb262a457c475d219d7163d996dc9072434ffb0784c4"},
- {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ab3247d58b393bda5b1c8f31c9edece7162fc13265334217785518dd770792b8"},
- {file = "aiohttp-3.11.12-cp39-cp39-win32.whl", hash = "sha256:0d5176f310a7fe6f65608213cc74f4228e4f4ce9fd10bcb2bb6da8fc66991462"},
- {file = "aiohttp-3.11.12-cp39-cp39-win_amd64.whl", hash = "sha256:74bd573dde27e58c760d9ca8615c41a57e719bff315c9adb6f2a4281a28e8798"},
- {file = "aiohttp-3.11.12.tar.gz", hash = "sha256:7603ca26d75b1b86160ce1bbe2787a0b706e592af5b2504e12caa88a217767b0"},
-]
-
-[package.dependencies]
-aiohappyeyeballs = ">=2.3.0"
-aiosignal = ">=1.1.2"
-async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""}
-attrs = ">=17.3.0"
-frozenlist = ">=1.1.1"
-multidict = ">=4.5,<7.0"
-propcache = ">=0.2.0"
-yarl = ">=1.17.0,<2.0"
-
-[package.extras]
-speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"]
-
-[[package]]
-name = "aiosignal"
-version = "1.3.2"
-description = "aiosignal: a list of registered asynchronous callbacks"
-optional = false
-python-versions = ">=3.9"
-groups = ["main"]
-files = [
- {file = "aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5"},
- {file = "aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"},
-]
-
-[package.dependencies]
-frozenlist = ">=1.1.0"
-
[[package]]
name = "aiosmtplib"
version = "3.0.2"
@@ -219,7 +88,7 @@ version = "4.8.0"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
optional = false
python-versions = ">=3.9"
-groups = ["main", "dev", "web"]
+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"},
@@ -236,18 +105,6 @@ doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)",
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"]
trio = ["trio (>=0.26.1)"]
-[[package]]
-name = "argparse"
-version = "1.4.0"
-description = "Python command-line parsing library"
-optional = false
-python-versions = "*"
-groups = ["main"]
-files = [
- {file = "argparse-1.4.0-py2.py3-none-any.whl", hash = "sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314"},
- {file = "argparse-1.4.0.tar.gz", hash = "sha256:62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4"},
-]
-
[[package]]
name = "asn1crypto"
version = "1.5.1"
@@ -260,19 +117,6 @@ files = [
{file = "asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c"},
]
-[[package]]
-name = "async-timeout"
-version = "5.0.1"
-description = "Timeout context manager for asyncio programs"
-optional = false
-python-versions = ">=3.8"
-groups = ["main"]
-markers = "python_version < \"3.11\""
-files = [
- {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"},
- {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"},
-]
-
[[package]]
name = "attrs"
version = "25.1.0"
@@ -310,54 +154,52 @@ cryptography = "*"
[[package]]
name = "auto-archiver"
-version = "0.12.0"
-description = "Easily archive online media content"
+version = "0.13.2"
+description = "Automatically archive links to videos, images, and social media content from Google Sheets (and more)."
optional = false
-python-versions = ">=3.10"
+python-versions = "<3.13,>=3.10"
groups = ["main"]
files = [
- {file = "auto_archiver-0.12.0-py3-none-any.whl", hash = "sha256:3cee45b9a17feba214503eb1be4e8552e40cadbba128964585e0f53a45966fc8"},
- {file = "auto_archiver-0.12.0.tar.gz", hash = "sha256:b9f1fb490fc268462325ec3f3c97c425a9c62dd0a2b4e58c771b64e8d29f0a87"},
+ {file = "auto_archiver-0.13.2-py3-none-any.whl", hash = "sha256:672671080bdc2e4cd50792b3521a8a1d70aabb50ee3f779ed30879162b0c352b"},
+ {file = "auto_archiver-0.13.2.tar.gz", hash = "sha256:b0d5505206bdb02f2ddb1b3a3a622780cc06ace0f3440b272f1d9bc9c314c9e2"},
]
[package.dependencies]
-argparse = "*"
-beautifulsoup4 = "*"
-boto3 = "*"
-bs4 = "*"
-certvalidator = "*"
-cryptography = "*"
-dataclasses-json = "*"
-dateparser = "*"
-ffmpeg-python = "*"
-google-api-python-client = "*"
-google-auth-httplib2 = "*"
-google-auth-oauthlib = "*"
-gspread = "*"
-instaloader = "*"
-jinja2 = "*"
-jsonlines = "*"
-loguru = "*"
-minify-html = "*"
-numpy = "*"
-oauth2client = "*"
-pdqhash = "*"
-pillow = "*"
-pysubs2 = "*"
-python-slugify = "*"
-python-twitter-v2 = "*"
-pyyaml = "*"
-requests = {version = "*", extras = ["socks"]}
-retrying = "*"
-selenium = "*"
-snscrape = "*"
-telethon = "*"
-tiktok-downloader = "*"
-tqdm = "*"
-tsp-client = "*"
-vk-url-scraper = "*"
-warcio = "*"
-yt-dlp = "*"
+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"
@@ -400,7 +242,7 @@ version = "1.9.0"
description = "Fast, simple object-to-object and broadcast signaling"
optional = false
python-versions = ">=3.9"
-groups = ["main", "web"]
+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"},
@@ -408,18 +250,18 @@ files = [
[[package]]
name = "boto3"
-version = "1.36.16"
+version = "1.36.21"
description = "The AWS SDK for Python"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
- {file = "boto3-1.36.16-py3-none-any.whl", hash = "sha256:b10583bf8bd35be1b4027ee7e26b7cdf2078c79eab18357fd602cecb6d39400b"},
- {file = "boto3-1.36.16.tar.gz", hash = "sha256:0cf92ca0538ab115447e1c58050d43e1273e88c58ddfea2b6f133fdc508b400a"},
+ {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.16,<1.37.0"
+botocore = ">=1.36.21,<1.37.0"
jmespath = ">=0.7.1,<2.0.0"
s3transfer = ">=0.11.0,<0.12.0"
@@ -428,14 +270,14 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
[[package]]
name = "botocore"
-version = "1.36.16"
+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.16-py3-none-any.whl", hash = "sha256:aca0348ccd730332082489b6817fdf89e1526049adcf6e9c8c11c96dd9f42c03"},
- {file = "botocore-1.36.16.tar.gz", hash = "sha256:10c6aa386ba1a9a0faef6bb5dbfc58fc2563a3c6b95352e86a583cd5f14b11f3"},
+ {file = "botocore-1.36.21-py3-none-any.whl", hash = "sha256:24a7052e792639dc2726001bd474cd0aaa959c1e18ddd92c17f3adc6efa1b132"},
+ {file = "botocore-1.36.21.tar.gz", hash = "sha256:da746240e2ad64fd4997f7f3664a0a8e303d18075fc1d473727cb6375080ea16"},
]
[package.dependencies]
@@ -943,23 +785,6 @@ prompt-toolkit = ">=3.0.36"
[package.extras]
testing = ["pytest (>=7.2.1)", "pytest-cov (>=4.0.0)", "tox (>=4.4.3)"]
-[[package]]
-name = "cloudscraper"
-version = "1.2.71"
-description = "A Python module to bypass Cloudflare's anti-bot page."
-optional = false
-python-versions = "*"
-groups = ["main"]
-files = [
- {file = "cloudscraper-1.2.71-py2.py3-none-any.whl", hash = "sha256:76f50ca529ed2279e220837befdec892626f9511708e200d48d5bb76ded679b0"},
- {file = "cloudscraper-1.2.71.tar.gz", hash = "sha256:429c6e8aa6916d5bad5c8a5eac50f3ea53c9ac22616f6cb21b18dcc71517d0d3"},
-]
-
-[package.dependencies]
-pyparsing = ">=2.4.7"
-requests = ">=2.9.2"
-requests-toolbelt = ">=0.9.1"
-
[[package]]
name = "colorama"
version = "0.4.6"
@@ -975,69 +800,75 @@ markers = {main = "sys_platform == \"win32\" or platform_system == \"Windows\"",
[[package]]
name = "coverage"
-version = "7.6.11"
+version = "7.6.12"
description = "Code coverage measurement for Python"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
- {file = "coverage-7.6.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eafea49da254a8289bed3fab960f808b322eda5577cb17a3733014928bbfbebd"},
- {file = "coverage-7.6.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5a3f7cbbcb4ad95067a6525f83a6fc78d9cbc1e70f8abaeeaeaa72ef34f48fc3"},
- {file = "coverage-7.6.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de6b079b39246a7da9a40cfa62d5766bd52b4b7a88cf5a82ec4c45bf6e152306"},
- {file = "coverage-7.6.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:60d4ad09dfc8c36c4910685faafcb8044c84e4dae302e86c585b3e2e7778726c"},
- {file = "coverage-7.6.11-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e433b6e3a834a43dae2889adc125f3fa4c66668df420d8e49bc4ee817dd7a70"},
- {file = "coverage-7.6.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ac5d92e2cc121a13270697e4cb37e1eb4511ac01d23fe1b6c097facc3b46489e"},
- {file = "coverage-7.6.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5128f3ba694c0a1bde55fc480090392c336236c3e1a10dad40dc1ab17c7675ff"},
- {file = "coverage-7.6.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:397489c611b76302dfa1d9ea079e138dddc4af80fc6819d5f5119ec8ca6c0e47"},
- {file = "coverage-7.6.11-cp310-cp310-win32.whl", hash = "sha256:c7719a5e1dc93883a6b319bc0374ecd46fb6091ed659f3fbe281ab991634b9b0"},
- {file = "coverage-7.6.11-cp310-cp310-win_amd64.whl", hash = "sha256:c27df03730059118b8a923cfc8b84b7e9976742560af528242f201880879c1da"},
- {file = "coverage-7.6.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:532fe139691af134aa8b54ed60dd3c806aa81312d93693bd2883c7b61592c840"},
- {file = "coverage-7.6.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0b0f272901a5172090c0802053fbc503cdc3fa2612720d2669a98a7384a7bec"},
- {file = "coverage-7.6.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4bda710139ea646890d1c000feb533caff86904a0e0638f85e967c28cb8eec50"},
- {file = "coverage-7.6.11-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a165b09e7d5f685bf659063334a9a7b1a2d57b531753d3e04bd442b3cfe5845b"},
- {file = "coverage-7.6.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ff136607689c1c87f43d24203b6d2055b42030f352d5176f9c8b204d4235ef27"},
- {file = "coverage-7.6.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:050172741de03525290e67f0161ae5f7f387c88fca50d47fceb4724ceaa591d2"},
- {file = "coverage-7.6.11-cp311-cp311-win32.whl", hash = "sha256:27700d859be68e4fb2e7bf774cf49933dcac6f81a9bc4c13bd41735b8d26a53b"},
- {file = "coverage-7.6.11-cp311-cp311-win_amd64.whl", hash = "sha256:cd4839813b09ab1dd1be1bbc74f9a7787615f931f83952b6a9af1b2d3f708bf7"},
- {file = "coverage-7.6.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dbb1a822fd858d9853333a7c95d4e70dde9a79e65893138ce32c2ec6457d7a36"},
- {file = "coverage-7.6.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:61c834cbb80946d6ebfddd9b393a4c46bec92fcc0fa069321fcb8049117f76ea"},
- {file = "coverage-7.6.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a46d56e99a31d858d6912d31ffa4ede6a325c86af13139539beefca10a1234ce"},
- {file = "coverage-7.6.11-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b48db06f53d1864fea6dbd855e6d51d41c0f06c212c3004511c0bdc6847b297"},
- {file = "coverage-7.6.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b6ff5be3b1853e0862da9d349fe87f869f68e63a25f7c37ce1130b321140f963"},
- {file = "coverage-7.6.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be05bde21d5e6eefbc3a6de6b9bee2b47894b8945342e8663192809c4d1f08ce"},
- {file = "coverage-7.6.11-cp312-cp312-win32.whl", hash = "sha256:e3b746fa0ffc5b6b8856529de487da8b9aeb4fb394bb58de6502ef45f3434f12"},
- {file = "coverage-7.6.11-cp312-cp312-win_amd64.whl", hash = "sha256:ac476e6d0128fb7919b3fae726de72b28b5c9644cb4b579e4a523d693187c551"},
- {file = "coverage-7.6.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c86f4c7a6d1a54a24d804d9684d96e36a62d3ef7c0d7745ae2ea39e3e0293251"},
- {file = "coverage-7.6.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7eb0504bb307401fd08bc5163a351df301438b3beb88a4fa044681295bbefc67"},
- {file = "coverage-7.6.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca95d40900cf614e07f00cee8c2fad0371df03ca4d7a80161d84be2ec132b7a4"},
- {file = "coverage-7.6.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db4b1a69976b1b02acda15937538a1d3fe10b185f9d99920b17a740a0a102e06"},
- {file = "coverage-7.6.11-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf96beb05d004e4c51cd846fcdf9eee9eb2681518524b66b2e7610507944c2f"},
- {file = "coverage-7.6.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:08e5fb93576a6b054d3d326242af5ef93daaac9bb52bc25f12ccbc3fa94227cd"},
- {file = "coverage-7.6.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25575cd5a7d2acc46b42711e8aff826027c0e4f80fb38028a74f31ac22aae69d"},
- {file = "coverage-7.6.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8fa4fffd90ee92f62ff7404b4801b59e8ea8502e19c9bf2d3241ce745b52926c"},
- {file = "coverage-7.6.11-cp313-cp313-win32.whl", hash = "sha256:0d03c9452d9d1ccfe5d3a5df0427705022a49b356ac212d529762eaea5ef97b4"},
- {file = "coverage-7.6.11-cp313-cp313-win_amd64.whl", hash = "sha256:fd2fffc8ce8692ce540103dff26279d2af22d424516ddebe2d7e4d6dbb3816b2"},
- {file = "coverage-7.6.11-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:5e7ac966ab110bd94ee844f2643f196d78fde1cd2450399116d3efdd706e19f5"},
- {file = "coverage-7.6.11-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ba27a0375c5ef4d2a7712f829265102decd5ff78b96d342ac2fa555742c4f4f"},
- {file = "coverage-7.6.11-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2778be4f574b39ec9dcd9e5e13644f770351ee0990a0ecd27e364aba95af89b"},
- {file = "coverage-7.6.11-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5edc16712187139ab635a2e644cc41fc239bc6d245b16124045743130455c652"},
- {file = "coverage-7.6.11-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df6ff122a0a10a30121d9f0cb3fbd03a6fe05861e4ec47adb9f25e9245aabc19"},
- {file = "coverage-7.6.11-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ff562952f15eff27247a4c4b03e45ce8a82e3fb197de6a7c54080f9d4ba07845"},
- {file = "coverage-7.6.11-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4f21e3617f48d683f30cf2a6c8b739c838e600cb1454fe6b2eb486ac2bce8fbd"},
- {file = "coverage-7.6.11-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6d60577673ba48d8ae8e362e61fd4ad1a640293ffe8991d11c86f195479100b7"},
- {file = "coverage-7.6.11-cp313-cp313t-win32.whl", hash = "sha256:13100f98497086b359bf56fc035a762c674de8ef526daa389ac8932cb9bff1e0"},
- {file = "coverage-7.6.11-cp313-cp313t-win_amd64.whl", hash = "sha256:2c81e53782043b323bd34c7de711ed9b4673414eb517eaf35af92185b873839c"},
- {file = "coverage-7.6.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ff52b4e2ac0080c96e506819586c4b16cdbf46724bda90d308a7330a73cc8521"},
- {file = "coverage-7.6.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f4679fcc9eb9004fdd1b00231ef1ec7167168071bebc4d66327e28c1979b4449"},
- {file = "coverage-7.6.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:90de4e9ca4489e823138bd13098af9ac8028cc029f33f60098b5c08c675c7bda"},
- {file = "coverage-7.6.11-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c96a142057d83ee993eaf71629ca3fb952cda8afa9a70af4132950c2bd3deb9"},
- {file = "coverage-7.6.11-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:476f29a258b9cd153f2be5bf5f119d670d2806363595263917bddc167d6e5cce"},
- {file = "coverage-7.6.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:09d03f48d9025b8a6a116cddcb6c7b8ce80e4fb4c31dd2e124a7c377036ad58e"},
- {file = "coverage-7.6.11-cp39-cp39-win32.whl", hash = "sha256:bb35ae9f134fbd9cf7302a9654d5a1e597c974202678082dcc569eb39a8cde03"},
- {file = "coverage-7.6.11-cp39-cp39-win_amd64.whl", hash = "sha256:f382004fa4c93c01016d9226b9d696a08c53f6818b7ad59b4e96cb67e863353a"},
- {file = "coverage-7.6.11-pp39.pp310-none-any.whl", hash = "sha256:adc2d941c0381edfcf3897f94b9f41b1e504902fab78a04b1677f2f72afead4b"},
- {file = "coverage-7.6.11-py3-none-any.whl", hash = "sha256:f0f334ae844675420164175bf32b04e18a81fe57ad8eb7e0cfd4689d681ffed7"},
- {file = "coverage-7.6.11.tar.gz", hash = "sha256:e642e6a46a04e992ebfdabed79e46f478ec60e2c528e1e1a074d63800eda4286"},
+ {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]
@@ -1266,148 +1097,6 @@ 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 = "filelock"
-version = "3.17.0"
-description = "A platform independent file lock."
-optional = false
-python-versions = ">=3.9"
-groups = ["main"]
-files = [
- {file = "filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338"},
- {file = "filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e"},
-]
-
-[package.extras]
-docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"]
-testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"]
-typing = ["typing-extensions (>=4.12.2)"]
-
-[[package]]
-name = "flask"
-version = "3.1.0"
-description = "A simple framework for building complex web applications."
-optional = false
-python-versions = ">=3.9"
-groups = ["main"]
-files = [
- {file = "flask-3.1.0-py3-none-any.whl", hash = "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136"},
- {file = "flask-3.1.0.tar.gz", hash = "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac"},
-]
-
-[package.dependencies]
-blinker = ">=1.9"
-click = ">=8.1.3"
-itsdangerous = ">=2.2"
-Jinja2 = ">=3.1.2"
-Werkzeug = ">=3.1"
-
-[package.extras]
-async = ["asgiref (>=3.2)"]
-dotenv = ["python-dotenv"]
-
-[[package]]
-name = "frozenlist"
-version = "1.5.0"
-description = "A list-like structure which implements collections.abc.MutableSequence"
-optional = false
-python-versions = ">=3.8"
-groups = ["main"]
-files = [
- {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a"},
- {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb"},
- {file = "frozenlist-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec"},
- {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5"},
- {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76"},
- {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17"},
- {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba"},
- {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d"},
- {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2"},
- {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f"},
- {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c"},
- {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab"},
- {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5"},
- {file = "frozenlist-1.5.0-cp310-cp310-win32.whl", hash = "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb"},
- {file = "frozenlist-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4"},
- {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30"},
- {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5"},
- {file = "frozenlist-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778"},
- {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a"},
- {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869"},
- {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d"},
- {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45"},
- {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d"},
- {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3"},
- {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a"},
- {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9"},
- {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2"},
- {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf"},
- {file = "frozenlist-1.5.0-cp311-cp311-win32.whl", hash = "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942"},
- {file = "frozenlist-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d"},
- {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21"},
- {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d"},
- {file = "frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e"},
- {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a"},
- {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a"},
- {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee"},
- {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6"},
- {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e"},
- {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9"},
- {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039"},
- {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784"},
- {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631"},
- {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f"},
- {file = "frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8"},
- {file = "frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f"},
- {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953"},
- {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0"},
- {file = "frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2"},
- {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f"},
- {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608"},
- {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b"},
- {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840"},
- {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439"},
- {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de"},
- {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641"},
- {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e"},
- {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9"},
- {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03"},
- {file = "frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c"},
- {file = "frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28"},
- {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:dd94994fc91a6177bfaafd7d9fd951bc8689b0a98168aa26b5f543868548d3ca"},
- {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0da8bbec082bf6bf18345b180958775363588678f64998c2b7609e34719b10"},
- {file = "frozenlist-1.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:73f2e31ea8dd7df61a359b731716018c2be196e5bb3b74ddba107f694fbd7604"},
- {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:828afae9f17e6de596825cf4228ff28fbdf6065974e5ac1410cecc22f699d2b3"},
- {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1577515d35ed5649d52ab4319db757bb881ce3b2b796d7283e6634d99ace307"},
- {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2150cc6305a2c2ab33299453e2968611dacb970d2283a14955923062c8d00b10"},
- {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a72b7a6e3cd2725eff67cd64c8f13335ee18fc3c7befc05aed043d24c7b9ccb9"},
- {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c16d2fa63e0800723139137d667e1056bee1a1cf7965153d2d104b62855e9b99"},
- {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:17dcc32fc7bda7ce5875435003220a457bcfa34ab7924a49a1c19f55b6ee185c"},
- {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:97160e245ea33d8609cd2b8fd997c850b56db147a304a262abc2b3be021a9171"},
- {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f1e6540b7fa044eee0bb5111ada694cf3dc15f2b0347ca125ee9ca984d5e9e6e"},
- {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:91d6c171862df0a6c61479d9724f22efb6109111017c87567cfeb7b5d1449fdf"},
- {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c1fac3e2ace2eb1052e9f7c7db480818371134410e1f5c55d65e8f3ac6d1407e"},
- {file = "frozenlist-1.5.0-cp38-cp38-win32.whl", hash = "sha256:b97f7b575ab4a8af9b7bc1d2ef7f29d3afee2226bd03ca3875c16451ad5a7723"},
- {file = "frozenlist-1.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:374ca2dabdccad8e2a76d40b1d037f5bd16824933bf7bcea3e59c891fd4a0923"},
- {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972"},
- {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336"},
- {file = "frozenlist-1.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f"},
- {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f"},
- {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6"},
- {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411"},
- {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08"},
- {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2"},
- {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d"},
- {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b"},
- {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b"},
- {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0"},
- {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c"},
- {file = "frozenlist-1.5.0-cp39-cp39-win32.whl", hash = "sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3"},
- {file = "frozenlist-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0"},
- {file = "frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3"},
- {file = "frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817"},
-]
-
[[package]]
name = "future"
version = "1.0.0"
@@ -1435,10 +1124,7 @@ files = [
[package.dependencies]
google-auth = ">=2.14.1,<3.0.dev0"
googleapis-common-protos = ">=1.56.2,<2.0.dev0"
-proto-plus = [
- {version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""},
- {version = ">=1.22.3,<2.0.0dev", markers = "python_version < \"3.13\""},
-]
+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"
@@ -1450,14 +1136,14 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"]
[[package]]
name = "google-api-python-client"
-version = "2.160.0"
+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.160.0-py2.py3-none-any.whl", hash = "sha256:63d61fb3e4cf3fb31a70a87f45567c22f6dfe87bbfa27252317e3e2c42900db4"},
- {file = "google_api_python_client-2.160.0.tar.gz", hash = "sha256:a8ccafaecfa42d15d5b5c3134ced8de08380019717fc9fb1ed510ca58eca3b7e"},
+ {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]
@@ -1529,14 +1215,14 @@ tool = ["click (>=6.0.0)"]
[[package]]
name = "googleapis-common-protos"
-version = "1.66.0"
+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.66.0-py2.py3-none-any.whl", hash = "sha256:d7abcd75fabb2e0ec9f74466401f6c119a0b498e27370e9be4c94cb7e382b8ed"},
- {file = "googleapis_common_protos-1.66.0.tar.gz", hash = "sha256:c3e7b33d15fdca5374cc0a7346dd92ffa847425cc4ea941d970f13680052ec8c"},
+ {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]
@@ -1552,7 +1238,7 @@ description = "Lightweight in-process concurrent programming"
optional = false
python-versions = ">=3.7"
groups = ["main", "web"]
-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\")"
+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"},
@@ -1667,7 +1353,7 @@ version = "1.0.7"
description = "A minimal low-level HTTP client."
optional = false
python-versions = ">=3.8"
-groups = ["main", "dev"]
+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"},
@@ -1704,7 +1390,7 @@ version = "0.28.1"
description = "The next generation HTTP client."
optional = false
python-versions = ">=3.8"
-groups = ["main", "dev"]
+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"},
@@ -1768,18 +1454,6 @@ requests = ">=2.25"
[package.extras]
browser-cookie3 = ["browser_cookie3 (>=0.19.1)"]
-[[package]]
-name = "itsdangerous"
-version = "2.2.0"
-description = "Safely pass data to untrusted environments and back."
-optional = false
-python-versions = ">=3.8"
-groups = ["main"]
-files = [
- {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"},
- {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"},
-]
-
[[package]]
name = "jinja2"
version = "3.1.5"
@@ -1878,161 +1552,6 @@ 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 = "lxml"
-version = "5.3.1"
-description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
-optional = false
-python-versions = ">=3.6"
-groups = ["main"]
-files = [
- {file = "lxml-5.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a4058f16cee694577f7e4dd410263cd0ef75644b43802a689c2b3c2a7e69453b"},
- {file = "lxml-5.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:364de8f57d6eda0c16dcfb999af902da31396949efa0e583e12675d09709881b"},
- {file = "lxml-5.3.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:528f3a0498a8edc69af0559bdcf8a9f5a8bf7c00051a6ef3141fdcf27017bbf5"},
- {file = "lxml-5.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db4743e30d6f5f92b6d2b7c86b3ad250e0bad8dee4b7ad8a0c44bfb276af89a3"},
- {file = "lxml-5.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17b5d7f8acf809465086d498d62a981fa6a56d2718135bb0e4aa48c502055f5c"},
- {file = "lxml-5.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:928e75a7200a4c09e6efc7482a1337919cc61fe1ba289f297827a5b76d8969c2"},
- {file = "lxml-5.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a997b784a639e05b9d4053ef3b20c7e447ea80814a762f25b8ed5a89d261eac"},
- {file = "lxml-5.3.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:7b82e67c5feb682dbb559c3e6b78355f234943053af61606af126df2183b9ef9"},
- {file = "lxml-5.3.1-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:f1de541a9893cf8a1b1db9bf0bf670a2decab42e3e82233d36a74eda7822b4c9"},
- {file = "lxml-5.3.1-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:de1fc314c3ad6bc2f6bd5b5a5b9357b8c6896333d27fdbb7049aea8bd5af2d79"},
- {file = "lxml-5.3.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:7c0536bd9178f754b277a3e53f90f9c9454a3bd108b1531ffff720e082d824f2"},
- {file = "lxml-5.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:68018c4c67d7e89951a91fbd371e2e34cd8cfc71f0bb43b5332db38497025d51"},
- {file = "lxml-5.3.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aa826340a609d0c954ba52fd831f0fba2a4165659ab0ee1a15e4aac21f302406"},
- {file = "lxml-5.3.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:796520afa499732191e39fc95b56a3b07f95256f2d22b1c26e217fb69a9db5b5"},
- {file = "lxml-5.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3effe081b3135237da6e4c4530ff2a868d3f80be0bda027e118a5971285d42d0"},
- {file = "lxml-5.3.1-cp310-cp310-win32.whl", hash = "sha256:a22f66270bd6d0804b02cd49dae2b33d4341015545d17f8426f2c4e22f557a23"},
- {file = "lxml-5.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:0bcfadea3cdc68e678d2b20cb16a16716887dd00a881e16f7d806c2138b8ff0c"},
- {file = "lxml-5.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e220f7b3e8656ab063d2eb0cd536fafef396829cafe04cb314e734f87649058f"},
- {file = "lxml-5.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f2cfae0688fd01f7056a17367e3b84f37c545fb447d7282cf2c242b16262607"},
- {file = "lxml-5.3.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67d2f8ad9dcc3a9e826bdc7802ed541a44e124c29b7d95a679eeb58c1c14ade8"},
- {file = "lxml-5.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db0c742aad702fd5d0c6611a73f9602f20aec2007c102630c06d7633d9c8f09a"},
- {file = "lxml-5.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:198bb4b4dd888e8390afa4f170d4fa28467a7eaf857f1952589f16cfbb67af27"},
- {file = "lxml-5.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2a3e412ce1849be34b45922bfef03df32d1410a06d1cdeb793a343c2f1fd666"},
- {file = "lxml-5.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b8969dbc8d09d9cd2ae06362c3bad27d03f433252601ef658a49bd9f2b22d79"},
- {file = "lxml-5.3.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5be8f5e4044146a69c96077c7e08f0709c13a314aa5315981185c1f00235fe65"},
- {file = "lxml-5.3.1-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:133f3493253a00db2c870d3740bc458ebb7d937bd0a6a4f9328373e0db305709"},
- {file = "lxml-5.3.1-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:52d82b0d436edd6a1d22d94a344b9a58abd6c68c357ed44f22d4ba8179b37629"},
- {file = "lxml-5.3.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b6f92e35e2658a5ed51c6634ceb5ddae32053182851d8cad2a5bc102a359b33"},
- {file = "lxml-5.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:203b1d3eaebd34277be06a3eb880050f18a4e4d60861efba4fb946e31071a295"},
- {file = "lxml-5.3.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:155e1a5693cf4b55af652f5c0f78ef36596c7f680ff3ec6eb4d7d85367259b2c"},
- {file = "lxml-5.3.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22ec2b3c191f43ed21f9545e9df94c37c6b49a5af0a874008ddc9132d49a2d9c"},
- {file = "lxml-5.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7eda194dd46e40ec745bf76795a7cccb02a6a41f445ad49d3cf66518b0bd9cff"},
- {file = "lxml-5.3.1-cp311-cp311-win32.whl", hash = "sha256:fb7c61d4be18e930f75948705e9718618862e6fc2ed0d7159b2262be73f167a2"},
- {file = "lxml-5.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:c809eef167bf4a57af4b03007004896f5c60bd38dc3852fcd97a26eae3d4c9e6"},
- {file = "lxml-5.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e69add9b6b7b08c60d7ff0152c7c9a6c45b4a71a919be5abde6f98f1ea16421c"},
- {file = "lxml-5.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4e52e1b148867b01c05e21837586ee307a01e793b94072d7c7b91d2c2da02ffe"},
- {file = "lxml-5.3.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4b382e0e636ed54cd278791d93fe2c4f370772743f02bcbe431a160089025c9"},
- {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e49dc23a10a1296b04ca9db200c44d3eb32c8d8ec532e8c1fd24792276522a"},
- {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4399b4226c4785575fb20998dc571bc48125dc92c367ce2602d0d70e0c455eb0"},
- {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5412500e0dc5481b1ee9cf6b38bb3b473f6e411eb62b83dc9b62699c3b7b79f7"},
- {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c93ed3c998ea8472be98fb55aed65b5198740bfceaec07b2eba551e55b7b9ae"},
- {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:63d57fc94eb0bbb4735e45517afc21ef262991d8758a8f2f05dd6e4174944519"},
- {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:b450d7cabcd49aa7ab46a3c6aa3ac7e1593600a1a0605ba536ec0f1b99a04322"},
- {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:4df0ec814b50275ad6a99bc82a38b59f90e10e47714ac9871e1b223895825468"},
- {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d184f85ad2bb1f261eac55cddfcf62a70dee89982c978e92b9a74a1bfef2e367"},
- {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b725e70d15906d24615201e650d5b0388b08a5187a55f119f25874d0103f90dd"},
- {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a31fa7536ec1fb7155a0cd3a4e3d956c835ad0a43e3610ca32384d01f079ea1c"},
- {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3c3c8b55c7fc7b7e8877b9366568cc73d68b82da7fe33d8b98527b73857a225f"},
- {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d61ec60945d694df806a9aec88e8f29a27293c6e424f8ff91c80416e3c617645"},
- {file = "lxml-5.3.1-cp312-cp312-win32.whl", hash = "sha256:f4eac0584cdc3285ef2e74eee1513a6001681fd9753b259e8159421ed28a72e5"},
- {file = "lxml-5.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:29bfc8d3d88e56ea0a27e7c4897b642706840247f59f4377d81be8f32aa0cfbf"},
- {file = "lxml-5.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c093c7088b40d8266f57ed71d93112bd64c6724d31f0794c1e52cc4857c28e0e"},
- {file = "lxml-5.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b0884e3f22d87c30694e625b1e62e6f30d39782c806287450d9dc2fdf07692fd"},
- {file = "lxml-5.3.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1637fa31ec682cd5760092adfabe86d9b718a75d43e65e211d5931809bc111e7"},
- {file = "lxml-5.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a364e8e944d92dcbf33b6b494d4e0fb3499dcc3bd9485beb701aa4b4201fa414"},
- {file = "lxml-5.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:779e851fd0e19795ccc8a9bb4d705d6baa0ef475329fe44a13cf1e962f18ff1e"},
- {file = "lxml-5.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c4393600915c308e546dc7003d74371744234e8444a28622d76fe19b98fa59d1"},
- {file = "lxml-5.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:673b9d8e780f455091200bba8534d5f4f465944cbdd61f31dc832d70e29064a5"},
- {file = "lxml-5.3.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:2e4a570f6a99e96c457f7bec5ad459c9c420ee80b99eb04cbfcfe3fc18ec6423"},
- {file = "lxml-5.3.1-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:71f31eda4e370f46af42fc9f264fafa1b09f46ba07bdbee98f25689a04b81c20"},
- {file = "lxml-5.3.1-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:42978a68d3825eaac55399eb37a4d52012a205c0c6262199b8b44fcc6fd686e8"},
- {file = "lxml-5.3.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8b1942b3e4ed9ed551ed3083a2e6e0772de1e5e3aca872d955e2e86385fb7ff9"},
- {file = "lxml-5.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85c4f11be9cf08917ac2a5a8b6e1ef63b2f8e3799cec194417e76826e5f1de9c"},
- {file = "lxml-5.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:231cf4d140b22a923b1d0a0a4e0b4f972e5893efcdec188934cc65888fd0227b"},
- {file = "lxml-5.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5865b270b420eda7b68928d70bb517ccbe045e53b1a428129bb44372bf3d7dd5"},
- {file = "lxml-5.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7bebc2275016cddf3c997bf8a0f7044160714c64a9b83975670a04e6d2252"},
- {file = "lxml-5.3.1-cp313-cp313-win32.whl", hash = "sha256:d0751528b97d2b19a388b302be2a0ee05817097bab46ff0ed76feeec24951f78"},
- {file = "lxml-5.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:91fb6a43d72b4f8863d21f347a9163eecbf36e76e2f51068d59cd004c506f332"},
- {file = "lxml-5.3.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:016b96c58e9a4528219bb563acf1aaaa8bc5452e7651004894a973f03b84ba81"},
- {file = "lxml-5.3.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82a4bb10b0beef1434fb23a09f001ab5ca87895596b4581fd53f1e5145a8934a"},
- {file = "lxml-5.3.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d68eeef7b4d08a25e51897dac29bcb62aba830e9ac6c4e3297ee7c6a0cf6439"},
- {file = "lxml-5.3.1-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:f12582b8d3b4c6be1d298c49cb7ae64a3a73efaf4c2ab4e37db182e3545815ac"},
- {file = "lxml-5.3.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2df7ed5edeb6bd5590914cd61df76eb6cce9d590ed04ec7c183cf5509f73530d"},
- {file = "lxml-5.3.1-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:585c4dc429deebc4307187d2b71ebe914843185ae16a4d582ee030e6cfbb4d8a"},
- {file = "lxml-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:06a20d607a86fccab2fc15a77aa445f2bdef7b49ec0520a842c5c5afd8381576"},
- {file = "lxml-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:057e30d0012439bc54ca427a83d458752ccda725c1c161cc283db07bcad43cf9"},
- {file = "lxml-5.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4867361c049761a56bd21de507cab2c2a608c55102311d142ade7dab67b34f32"},
- {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3dddf0fb832486cc1ea71d189cb92eb887826e8deebe128884e15020bb6e3f61"},
- {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bcc211542f7af6f2dfb705f5f8b74e865592778e6cafdfd19c792c244ccce19"},
- {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaca5a812f050ab55426c32177091130b1e49329b3f002a32934cd0245571307"},
- {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:236610b77589faf462337b3305a1be91756c8abc5a45ff7ca8f245a71c5dab70"},
- {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:aed57b541b589fa05ac248f4cb1c46cbb432ab82cbd467d1c4f6a2bdc18aecf9"},
- {file = "lxml-5.3.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:75fa3d6946d317ffc7016a6fcc44f42db6d514b7fdb8b4b28cbe058303cb6e53"},
- {file = "lxml-5.3.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:96eef5b9f336f623ffc555ab47a775495e7e8846dde88de5f941e2906453a1ce"},
- {file = "lxml-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:ef45f31aec9be01379fc6c10f1d9c677f032f2bac9383c827d44f620e8a88407"},
- {file = "lxml-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0611da6b07dd3720f492db1b463a4d1175b096b49438761cc9f35f0d9eaaef5"},
- {file = "lxml-5.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b2aca14c235c7a08558fe0a4786a1a05873a01e86b474dfa8f6df49101853a4e"},
- {file = "lxml-5.3.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae82fce1d964f065c32c9517309f0c7be588772352d2f40b1574a214bd6e6098"},
- {file = "lxml-5.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7aae7a3d63b935babfdc6864b31196afd5145878ddd22f5200729006366bc4d5"},
- {file = "lxml-5.3.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8e0d177b1fe251c3b1b914ab64135475c5273c8cfd2857964b2e3bb0fe196a7"},
- {file = "lxml-5.3.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:6c4dd3bfd0c82400060896717dd261137398edb7e524527438c54a8c34f736bf"},
- {file = "lxml-5.3.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:f1208c1c67ec9e151d78aa3435aa9b08a488b53d9cfac9b699f15255a3461ef2"},
- {file = "lxml-5.3.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:c6aacf00d05b38a5069826e50ae72751cb5bc27bdc4d5746203988e429b385bb"},
- {file = "lxml-5.3.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5881aaa4bf3a2d086c5f20371d3a5856199a0d8ac72dd8d0dbd7a2ecfc26ab73"},
- {file = "lxml-5.3.1-cp38-cp38-win32.whl", hash = "sha256:45fbb70ccbc8683f2fb58bea89498a7274af1d9ec7995e9f4af5604e028233fc"},
- {file = "lxml-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:7512b4d0fc5339d5abbb14d1843f70499cab90d0b864f790e73f780f041615d7"},
- {file = "lxml-5.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5885bc586f1edb48e5d68e7a4b4757b5feb2a496b64f462b4d65950f5af3364f"},
- {file = "lxml-5.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1b92fe86e04f680b848fff594a908edfa72b31bfc3499ef7433790c11d4c8cd8"},
- {file = "lxml-5.3.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a091026c3bf7519ab1e64655a3f52a59ad4a4e019a6f830c24d6430695b1cf6a"},
- {file = "lxml-5.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ffb141361108e864ab5f1813f66e4e1164181227f9b1f105b042729b6c15125"},
- {file = "lxml-5.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3715cdf0dd31b836433af9ee9197af10e3df41d273c19bb249230043667a5dfd"},
- {file = "lxml-5.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88b72eb7222d918c967202024812c2bfb4048deeb69ca328363fb8e15254c549"},
- {file = "lxml-5.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa59974880ab5ad8ef3afaa26f9bda148c5f39e06b11a8ada4660ecc9fb2feb3"},
- {file = "lxml-5.3.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:3bb8149840daf2c3f97cebf00e4ed4a65a0baff888bf2605a8d0135ff5cf764e"},
- {file = "lxml-5.3.1-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:0d6b2fa86becfa81f0a0271ccb9eb127ad45fb597733a77b92e8a35e53414914"},
- {file = "lxml-5.3.1-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:136bf638d92848a939fd8f0e06fcf92d9f2e4b57969d94faae27c55f3d85c05b"},
- {file = "lxml-5.3.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:89934f9f791566e54c1d92cdc8f8fd0009447a5ecdb1ec6b810d5f8c4955f6be"},
- {file = "lxml-5.3.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a8ade0363f776f87f982572c2860cc43c65ace208db49c76df0a21dde4ddd16e"},
- {file = "lxml-5.3.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:bfbbab9316330cf81656fed435311386610f78b6c93cc5db4bebbce8dd146675"},
- {file = "lxml-5.3.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:172d65f7c72a35a6879217bcdb4bb11bc88d55fb4879e7569f55616062d387c2"},
- {file = "lxml-5.3.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e3c623923967f3e5961d272718655946e5322b8d058e094764180cdee7bab1af"},
- {file = "lxml-5.3.1-cp39-cp39-win32.whl", hash = "sha256:ce0930a963ff593e8bb6fda49a503911accc67dee7e5445eec972668e672a0f0"},
- {file = "lxml-5.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:f7b64fcd670bca8800bc10ced36620c6bbb321e7bc1214b9c0c0df269c1dddc2"},
- {file = "lxml-5.3.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:afa578b6524ff85fb365f454cf61683771d0170470c48ad9d170c48075f86725"},
- {file = "lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f5e80adf0aafc7b5454f2c1cb0cde920c9b1f2cbd0485f07cc1d0497c35c5d"},
- {file = "lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dd0b80ac2d8f13ffc906123a6f20b459cb50a99222d0da492360512f3e50f84"},
- {file = "lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:422c179022ecdedbe58b0e242607198580804253da220e9454ffe848daa1cfd2"},
- {file = "lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:524ccfded8989a6595dbdda80d779fb977dbc9a7bc458864fc9a0c2fc15dc877"},
- {file = "lxml-5.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:48fd46bf7155def2e15287c6f2b133a2f78e2d22cdf55647269977b873c65499"},
- {file = "lxml-5.3.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:05123fad495a429f123307ac6d8fd6f977b71e9a0b6d9aeeb8f80c017cb17131"},
- {file = "lxml-5.3.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a243132767150a44e6a93cd1dde41010036e1cbc63cc3e9fe1712b277d926ce3"},
- {file = "lxml-5.3.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c92ea6d9dd84a750b2bae72ff5e8cf5fdd13e58dda79c33e057862c29a8d5b50"},
- {file = "lxml-5.3.1-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2f1be45d4c15f237209bbf123a0e05b5d630c8717c42f59f31ea9eae2ad89394"},
- {file = "lxml-5.3.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:a83d3adea1e0ee36dac34627f78ddd7f093bb9cfc0a8e97f1572a949b695cb98"},
- {file = "lxml-5.3.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:3edbb9c9130bac05d8c3fe150c51c337a471cc7fdb6d2a0a7d3a88e88a829314"},
- {file = "lxml-5.3.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2f23cf50eccb3255b6e913188291af0150d89dab44137a69e14e4dcb7be981f1"},
- {file = "lxml-5.3.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df7e5edac4778127f2bf452e0721a58a1cfa4d1d9eac63bdd650535eb8543615"},
- {file = "lxml-5.3.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:094b28ed8a8a072b9e9e2113a81fda668d2053f2ca9f2d202c2c8c7c2d6516b1"},
- {file = "lxml-5.3.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:514fe78fc4b87e7a7601c92492210b20a1b0c6ab20e71e81307d9c2e377c64de"},
- {file = "lxml-5.3.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8fffc08de02071c37865a155e5ea5fce0282e1546fd5bde7f6149fcaa32558ac"},
- {file = "lxml-5.3.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4b0d5cdba1b655d5b18042ac9c9ff50bda33568eb80feaaca4fc237b9c4fbfde"},
- {file = "lxml-5.3.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3031e4c16b59424e8d78522c69b062d301d951dc55ad8685736c3335a97fc270"},
- {file = "lxml-5.3.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb659702a45136c743bc130760c6f137870d4df3a9e14386478b8a0511abcfca"},
- {file = "lxml-5.3.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a11b16a33656ffc43c92a5343a28dc71eefe460bcc2a4923a96f292692709f6"},
- {file = "lxml-5.3.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c5ae125276f254b01daa73e2c103363d3e99e3e10505686ac7d9d2442dd4627a"},
- {file = "lxml-5.3.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c76722b5ed4a31ba103e0dc77ab869222ec36efe1a614e42e9bcea88a36186fe"},
- {file = "lxml-5.3.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:33e06717c00c788ab4e79bc4726ecc50c54b9bfb55355eae21473c145d83c2d2"},
- {file = "lxml-5.3.1.tar.gz", hash = "sha256:106b7b5d2977b339f1e97efe2778e2ab20e99994cbb0ec5e55771ed0795920c8"},
-]
-
-[package.extras]
-cssselect = ["cssselect (>=0.7)"]
-html-clean = ["lxml_html_clean"]
-html5 = ["html5lib"]
-htmlsoup = ["BeautifulSoup4"]
-source = ["Cython (>=3.0.11,<3.1.0)"]
-
[[package]]
name = "mako"
version = "1.3.9"
@@ -2181,147 +1700,6 @@ files = [
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
]
-[[package]]
-name = "minify-html"
-version = "0.15.0"
-description = "Extremely fast and smart HTML + JS + CSS minifier"
-optional = false
-python-versions = "*"
-groups = ["main"]
-files = [
- {file = "minify_html-0.15.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:afd76ca2dc9afa53b66973a3a66eff9a64692811ead44102aa8044a37872e6e2"},
- {file = "minify_html-0.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f37ce536305500914fd4ee2bbaa4dd05a039f39eeceae45560c39767d99aede0"},
- {file = "minify_html-0.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e6d4f97cebb725bc1075f225bdfcd824e0f5c20a37d9ea798d900f96e1b80c0"},
- {file = "minify_html-0.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e47197849a1c09a95892d32df3c9e15f6d0902c9ae215e73249b9f5bca9aeb97"},
- {file = "minify_html-0.15.0-cp310-none-win_amd64.whl", hash = "sha256:7af72438d3ae6ea8b0a94c038d35c9c22c5f8540967f5fa2487f77b2cdb12605"},
- {file = "minify_html-0.15.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a23a8055e65fa01175ddd7d18d101c05e267410fa5956c65597dcc332c7f91dd"},
- {file = "minify_html-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:597c86f9792437eee0698118fb38dff42b5b4be6d437b6d577453c2f91524ccc"},
- {file = "minify_html-0.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b2aadba6987e6c15a916a4627b94b1db3cbac65e6ae3613b61b3ab0d2bb4c96"},
- {file = "minify_html-0.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4c4ae3909e2896c865ebaa3a96939191f904dd337a87d7594130f3dfca55510"},
- {file = "minify_html-0.15.0-cp311-none-win_amd64.whl", hash = "sha256:dc2df1e5203d89197f530d14c9a82067f3d04b9cb0118abc8f2ef8f88efce109"},
- {file = "minify_html-0.15.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:2a9aef71b24c3d38c6bece2db3bf707443894958b01f1c27d3a6459ba4200e59"},
- {file = "minify_html-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:70251bd7174b62c91333110301b27000b547aa2cc06d4fe6ba6c3f11612eecc9"},
- {file = "minify_html-0.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1056819ea46e9080db6fed678d03511c7e94c2a615e72df82190ea898dc82609"},
- {file = "minify_html-0.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea315ad6ac33d7463fac3f313bba8c8d9a55f4811971c203eed931203047e5c8"},
- {file = "minify_html-0.15.0-cp312-none-win_amd64.whl", hash = "sha256:01ea40dc5ae073c47024f02758d5e18e55d853265eb9c099040a6c00ab0abb99"},
- {file = "minify_html-0.15.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:3b38ea5b446cc69e691a0bf64d1160332ffc220bb5b411775983c87311cab2c7"},
- {file = "minify_html-0.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b6356541799951c5e8205aabf5970dda687f4ffa736479ce8df031919861e51d"},
- {file = "minify_html-0.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40f38ddfefbb63beb28df20c2c81c12e6af6838387520506b4eceec807d794a3"},
- {file = "minify_html-0.15.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f707b233b9c163a546b15ce9af433ddd456bd113f0326e5ffb382b8ee5c1a2d"},
- {file = "minify_html-0.15.0-cp38-none-win_amd64.whl", hash = "sha256:bd682207673246c78fb895e7065425cc94cb712d94cff816dd9752ce014f23e8"},
- {file = "minify_html-0.15.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:7a5eb7e830277762da69498ee0f15d4a9fa6e91887a93567d388e4f5aee01ec3"},
- {file = "minify_html-0.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:92375f0cb3b4074e45005e1b4708b5b4c0781b335659d52918671c083c19c71e"},
- {file = "minify_html-0.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cda674cc68ec3b9ebf61f2986f3ef62de60ce837a58860c6f16b011862b5d533"},
- {file = "minify_html-0.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b071ded7aacbb140a7e751d49e246052f204b896d69663a4a5c3a27203d27f6"},
- {file = "minify_html-0.15.0-cp39-none-win_amd64.whl", hash = "sha256:ef6dc1950e04b7566c1ece72712674416f86fef8966ca026f6c5580d840cd354"},
- {file = "minify_html-0.15.0.tar.gz", hash = "sha256:cf4c36b6f9af3b0901bd2a0a29db3b09c0cdf0c38d3dde28e6835bce0f605d37"},
-]
-
-[[package]]
-name = "multidict"
-version = "6.1.0"
-description = "multidict implementation"
-optional = false
-python-versions = ">=3.8"
-groups = ["main"]
-files = [
- {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"},
- {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"},
- {file = "multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53"},
- {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5"},
- {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581"},
- {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56"},
- {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429"},
- {file = "multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748"},
- {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db"},
- {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056"},
- {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76"},
- {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160"},
- {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7"},
- {file = "multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0"},
- {file = "multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d"},
- {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6"},
- {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156"},
- {file = "multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb"},
- {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b"},
- {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72"},
- {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304"},
- {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351"},
- {file = "multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb"},
- {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3"},
- {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399"},
- {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423"},
- {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3"},
- {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753"},
- {file = "multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80"},
- {file = "multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926"},
- {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa"},
- {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436"},
- {file = "multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761"},
- {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e"},
- {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef"},
- {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95"},
- {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925"},
- {file = "multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966"},
- {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305"},
- {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2"},
- {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2"},
- {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6"},
- {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3"},
- {file = "multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133"},
- {file = "multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1"},
- {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008"},
- {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f"},
- {file = "multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28"},
- {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b"},
- {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c"},
- {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3"},
- {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44"},
- {file = "multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2"},
- {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3"},
- {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa"},
- {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa"},
- {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4"},
- {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6"},
- {file = "multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81"},
- {file = "multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774"},
- {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392"},
- {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a"},
- {file = "multidict-6.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2"},
- {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc"},
- {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478"},
- {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4"},
- {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d"},
- {file = "multidict-6.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6"},
- {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2"},
- {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd"},
- {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6"},
- {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492"},
- {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd"},
- {file = "multidict-6.1.0-cp38-cp38-win32.whl", hash = "sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167"},
- {file = "multidict-6.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef"},
- {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c"},
- {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1"},
- {file = "multidict-6.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c"},
- {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c"},
- {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f"},
- {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875"},
- {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255"},
- {file = "multidict-6.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30"},
- {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057"},
- {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657"},
- {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28"},
- {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972"},
- {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43"},
- {file = "multidict-6.1.0-cp39-cp39-win32.whl", hash = "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada"},
- {file = "multidict-6.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a"},
- {file = "multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506"},
- {file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"},
-]
-
-[package.dependencies]
-typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""}
-
[[package]]
name = "mutagen"
version = "1.47.0"
@@ -2348,67 +1726,67 @@ files = [
[[package]]
name = "numpy"
-version = "2.2.2"
+version = "2.1.3"
description = "Fundamental package for array computing in Python"
optional = false
python-versions = ">=3.10"
groups = ["main"]
files = [
- {file = "numpy-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7079129b64cb78bdc8d611d1fd7e8002c0a2565da6a47c4df8062349fee90e3e"},
- {file = "numpy-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ec6c689c61df613b783aeb21f945c4cbe6c51c28cb70aae8430577ab39f163e"},
- {file = "numpy-2.2.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:40c7ff5da22cd391944a28c6a9c638a5eef77fcf71d6e3a79e1d9d9e82752715"},
- {file = "numpy-2.2.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:995f9e8181723852ca458e22de5d9b7d3ba4da3f11cc1cb113f093b271d7965a"},
- {file = "numpy-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b78ea78450fd96a498f50ee096f69c75379af5138f7881a51355ab0e11286c97"},
- {file = "numpy-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fbe72d347fbc59f94124125e73fc4976a06927ebc503ec5afbfb35f193cd957"},
- {file = "numpy-2.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8e6da5cffbbe571f93588f562ed130ea63ee206d12851b60819512dd3e1ba50d"},
- {file = "numpy-2.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:09d6a2032faf25e8d0cadde7fd6145118ac55d2740132c1d845f98721b5ebcfd"},
- {file = "numpy-2.2.2-cp310-cp310-win32.whl", hash = "sha256:159ff6ee4c4a36a23fe01b7c3d07bd8c14cc433d9720f977fcd52c13c0098160"},
- {file = "numpy-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:64bd6e1762cd7f0986a740fee4dff927b9ec2c5e4d9a28d056eb17d332158014"},
- {file = "numpy-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:642199e98af1bd2b6aeb8ecf726972d238c9877b0f6e8221ee5ab945ec8a2189"},
- {file = "numpy-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6d9fc9d812c81e6168b6d405bf00b8d6739a7f72ef22a9214c4241e0dc70b323"},
- {file = "numpy-2.2.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:c7d1fd447e33ee20c1f33f2c8e6634211124a9aabde3c617687d8b739aa69eac"},
- {file = "numpy-2.2.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:451e854cfae0febe723077bd0cf0a4302a5d84ff25f0bfece8f29206c7bed02e"},
- {file = "numpy-2.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd249bc894af67cbd8bad2c22e7cbcd46cf87ddfca1f1289d1e7e54868cc785c"},
- {file = "numpy-2.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02935e2c3c0c6cbe9c7955a8efa8908dd4221d7755644c59d1bba28b94fd334f"},
- {file = "numpy-2.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a972cec723e0563aa0823ee2ab1df0cb196ed0778f173b381c871a03719d4826"},
- {file = "numpy-2.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d6d6a0910c3b4368d89dde073e630882cdb266755565155bc33520283b2d9df8"},
- {file = "numpy-2.2.2-cp311-cp311-win32.whl", hash = "sha256:860fd59990c37c3ef913c3ae390b3929d005243acca1a86facb0773e2d8d9e50"},
- {file = "numpy-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:da1eeb460ecce8d5b8608826595c777728cdf28ce7b5a5a8c8ac8d949beadcf2"},
- {file = "numpy-2.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ac9bea18d6d58a995fac1b2cb4488e17eceeac413af014b1dd26170b766d8467"},
- {file = "numpy-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23ae9f0c2d889b7b2d88a3791f6c09e2ef827c2446f1c4a3e3e76328ee4afd9a"},
- {file = "numpy-2.2.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3074634ea4d6df66be04f6728ee1d173cfded75d002c75fac79503a880bf3825"},
- {file = "numpy-2.2.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ec0636d3f7d68520afc6ac2dc4b8341ddb725039de042faf0e311599f54eb37"},
- {file = "numpy-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ffbb1acd69fdf8e89dd60ef6182ca90a743620957afb7066385a7bbe88dc748"},
- {file = "numpy-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0349b025e15ea9d05c3d63f9657707a4e1d471128a3b1d876c095f328f8ff7f0"},
- {file = "numpy-2.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:463247edcee4a5537841d5350bc87fe8e92d7dd0e8c71c995d2c6eecb8208278"},
- {file = "numpy-2.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9dd47ff0cb2a656ad69c38da850df3454da88ee9a6fde0ba79acceee0e79daba"},
- {file = "numpy-2.2.2-cp312-cp312-win32.whl", hash = "sha256:4525b88c11906d5ab1b0ec1f290996c0020dd318af8b49acaa46f198b1ffc283"},
- {file = "numpy-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:5acea83b801e98541619af398cc0109ff48016955cc0818f478ee9ef1c5c3dcb"},
- {file = "numpy-2.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b208cfd4f5fe34e1535c08983a1a6803fdbc7a1e86cf13dd0c61de0b51a0aadc"},
- {file = "numpy-2.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d0bbe7dd86dca64854f4b6ce2ea5c60b51e36dfd597300057cf473d3615f2369"},
- {file = "numpy-2.2.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:22ea3bb552ade325530e72a0c557cdf2dea8914d3a5e1fecf58fa5dbcc6f43cd"},
- {file = "numpy-2.2.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:128c41c085cab8a85dc29e66ed88c05613dccf6bc28b3866cd16050a2f5448be"},
- {file = "numpy-2.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:250c16b277e3b809ac20d1f590716597481061b514223c7badb7a0f9993c7f84"},
- {file = "numpy-2.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0c8854b09bc4de7b041148d8550d3bd712b5c21ff6a8ed308085f190235d7ff"},
- {file = "numpy-2.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b6fb9c32a91ec32a689ec6410def76443e3c750e7cfc3fb2206b985ffb2b85f0"},
- {file = "numpy-2.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:57b4012e04cc12b78590a334907e01b3a85efb2107df2b8733ff1ed05fce71de"},
- {file = "numpy-2.2.2-cp313-cp313-win32.whl", hash = "sha256:4dbd80e453bd34bd003b16bd802fac70ad76bd463f81f0c518d1245b1c55e3d9"},
- {file = "numpy-2.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:5a8c863ceacae696aff37d1fd636121f1a512117652e5dfb86031c8d84836369"},
- {file = "numpy-2.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b3482cb7b3325faa5f6bc179649406058253d91ceda359c104dac0ad320e1391"},
- {file = "numpy-2.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9491100aba630910489c1d0158034e1c9a6546f0b1340f716d522dc103788e39"},
- {file = "numpy-2.2.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:41184c416143defa34cc8eb9d070b0a5ba4f13a0fa96a709e20584638254b317"},
- {file = "numpy-2.2.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7dca87ca328f5ea7dafc907c5ec100d187911f94825f8700caac0b3f4c384b49"},
- {file = "numpy-2.2.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bc61b307655d1a7f9f4b043628b9f2b721e80839914ede634e3d485913e1fb2"},
- {file = "numpy-2.2.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fad446ad0bc886855ddf5909cbf8cb5d0faa637aaa6277fb4b19ade134ab3c7"},
- {file = "numpy-2.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:149d1113ac15005652e8d0d3f6fd599360e1a708a4f98e43c9c77834a28238cb"},
- {file = "numpy-2.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:106397dbbb1896f99e044efc90360d098b3335060375c26aa89c0d8a97c5f648"},
- {file = "numpy-2.2.2-cp313-cp313t-win32.whl", hash = "sha256:0eec19f8af947a61e968d5429f0bd92fec46d92b0008d0a6685b40d6adf8a4f4"},
- {file = "numpy-2.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:97b974d3ba0fb4612b77ed35d7627490e8e3dff56ab41454d9e8b23448940576"},
- {file = "numpy-2.2.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b0531f0b0e07643eb089df4c509d30d72c9ef40defa53e41363eca8a8cc61495"},
- {file = "numpy-2.2.2-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:e9e82dcb3f2ebbc8cb5ce1102d5f1c5ed236bf8a11730fb45ba82e2841ec21df"},
- {file = "numpy-2.2.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0d4142eb40ca6f94539e4db929410f2a46052a0fe7a2c1c59f6179c39938d2a"},
- {file = "numpy-2.2.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:356ca982c188acbfa6af0d694284d8cf20e95b1c3d0aefa8929376fea9146f60"},
- {file = "numpy-2.2.2.tar.gz", hash = "sha256:ed6906f61834d687738d25988ae117683705636936cc605be0bb208b23df4d8f"},
+ {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]]
@@ -2665,98 +2043,6 @@ files = [
[package.dependencies]
wcwidth = "*"
-[[package]]
-name = "propcache"
-version = "0.2.1"
-description = "Accelerated property cache"
-optional = false
-python-versions = ">=3.9"
-groups = ["main"]
-files = [
- {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6b3f39a85d671436ee3d12c017f8fdea38509e4f25b28eb25877293c98c243f6"},
- {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d51fbe4285d5db5d92a929e3e21536ea3dd43732c5b177c7ef03f918dff9f2"},
- {file = "propcache-0.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6445804cf4ec763dc70de65a3b0d9954e868609e83850a47ca4f0cb64bd79fea"},
- {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9479aa06a793c5aeba49ce5c5692ffb51fcd9a7016e017d555d5e2b0045d212"},
- {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9631c5e8b5b3a0fda99cb0d29c18133bca1e18aea9effe55adb3da1adef80d3"},
- {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3156628250f46a0895f1f36e1d4fbe062a1af8718ec3ebeb746f1d23f0c5dc4d"},
- {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b6fb63ae352e13748289f04f37868099e69dba4c2b3e271c46061e82c745634"},
- {file = "propcache-0.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:887d9b0a65404929641a9fabb6452b07fe4572b269d901d622d8a34a4e9043b2"},
- {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a96dc1fa45bd8c407a0af03b2d5218392729e1822b0c32e62c5bf7eeb5fb3958"},
- {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a7e65eb5c003a303b94aa2c3852ef130230ec79e349632d030e9571b87c4698c"},
- {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:999779addc413181912e984b942fbcc951be1f5b3663cd80b2687758f434c583"},
- {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:19a0f89a7bb9d8048d9c4370c9c543c396e894c76be5525f5e1ad287f1750ddf"},
- {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1ac2f5fe02fa75f56e1ad473f1175e11f475606ec9bd0be2e78e4734ad575034"},
- {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:574faa3b79e8ebac7cb1d7930f51184ba1ccf69adfdec53a12f319a06030a68b"},
- {file = "propcache-0.2.1-cp310-cp310-win32.whl", hash = "sha256:03ff9d3f665769b2a85e6157ac8b439644f2d7fd17615a82fa55739bc97863f4"},
- {file = "propcache-0.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2d3af2e79991102678f53e0dbf4c35de99b6b8b58f29a27ca0325816364caaba"},
- {file = "propcache-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ffc3cca89bb438fb9c95c13fc874012f7b9466b89328c3c8b1aa93cdcfadd16"},
- {file = "propcache-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f174bbd484294ed9fdf09437f889f95807e5f229d5d93588d34e92106fbf6717"},
- {file = "propcache-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:70693319e0b8fd35dd863e3e29513875eb15c51945bf32519ef52927ca883bc3"},
- {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b480c6a4e1138e1aa137c0079b9b6305ec6dcc1098a8ca5196283e8a49df95a9"},
- {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d27b84d5880f6d8aa9ae3edb253c59d9f6642ffbb2c889b78b60361eed449787"},
- {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:857112b22acd417c40fa4595db2fe28ab900c8c5fe4670c7989b1c0230955465"},
- {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf6c4150f8c0e32d241436526f3c3f9cbd34429492abddbada2ffcff506c51af"},
- {file = "propcache-0.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66d4cfda1d8ed687daa4bc0274fcfd5267873db9a5bc0418c2da19273040eeb7"},
- {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2f992c07c0fca81655066705beae35fc95a2fa7366467366db627d9f2ee097f"},
- {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4a571d97dbe66ef38e472703067021b1467025ec85707d57e78711c085984e54"},
- {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bb6178c241278d5fe853b3de743087be7f5f4c6f7d6d22a3b524d323eecec505"},
- {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ad1af54a62ffe39cf34db1aa6ed1a1873bd548f6401db39d8e7cd060b9211f82"},
- {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e7048abd75fe40712005bcfc06bb44b9dfcd8e101dda2ecf2f5aa46115ad07ca"},
- {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:160291c60081f23ee43d44b08a7e5fb76681221a8e10b3139618c5a9a291b84e"},
- {file = "propcache-0.2.1-cp311-cp311-win32.whl", hash = "sha256:819ce3b883b7576ca28da3861c7e1a88afd08cc8c96908e08a3f4dd64a228034"},
- {file = "propcache-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:edc9fc7051e3350643ad929df55c451899bb9ae6d24998a949d2e4c87fb596d3"},
- {file = "propcache-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:081a430aa8d5e8876c6909b67bd2d937bfd531b0382d3fdedb82612c618bc41a"},
- {file = "propcache-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2ccec9ac47cf4e04897619c0e0c1a48c54a71bdf045117d3a26f80d38ab1fb0"},
- {file = "propcache-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14d86fe14b7e04fa306e0c43cdbeebe6b2c2156a0c9ce56b815faacc193e320d"},
- {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:049324ee97bb67285b49632132db351b41e77833678432be52bdd0289c0e05e4"},
- {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cd9a1d071158de1cc1c71a26014dcdfa7dd3d5f4f88c298c7f90ad6f27bb46d"},
- {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98110aa363f1bb4c073e8dcfaefd3a5cea0f0834c2aab23dda657e4dab2f53b5"},
- {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:647894f5ae99c4cf6bb82a1bb3a796f6e06af3caa3d32e26d2350d0e3e3faf24"},
- {file = "propcache-0.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfd3223c15bebe26518d58ccf9a39b93948d3dcb3e57a20480dfdd315356baff"},
- {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d71264a80f3fcf512eb4f18f59423fe82d6e346ee97b90625f283df56aee103f"},
- {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e73091191e4280403bde6c9a52a6999d69cdfde498f1fdf629105247599b57ec"},
- {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3935bfa5fede35fb202c4b569bb9c042f337ca4ff7bd540a0aa5e37131659348"},
- {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f508b0491767bb1f2b87fdfacaba5f7eddc2f867740ec69ece6d1946d29029a6"},
- {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1672137af7c46662a1c2be1e8dc78cb6d224319aaa40271c9257d886be4363a6"},
- {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b74c261802d3d2b85c9df2dfb2fa81b6f90deeef63c2db9f0e029a3cac50b518"},
- {file = "propcache-0.2.1-cp312-cp312-win32.whl", hash = "sha256:d09c333d36c1409d56a9d29b3a1b800a42c76a57a5a8907eacdbce3f18768246"},
- {file = "propcache-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:c214999039d4f2a5b2073ac506bba279945233da8c786e490d411dfc30f855c1"},
- {file = "propcache-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aca405706e0b0a44cc6bfd41fbe89919a6a56999157f6de7e182a990c36e37bc"},
- {file = "propcache-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:12d1083f001ace206fe34b6bdc2cb94be66d57a850866f0b908972f90996b3e9"},
- {file = "propcache-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d93f3307ad32a27bda2e88ec81134b823c240aa3abb55821a8da553eed8d9439"},
- {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba278acf14471d36316159c94a802933d10b6a1e117b8554fe0d0d9b75c9d536"},
- {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e6281aedfca15301c41f74d7005e6e3f4ca143584ba696ac69df4f02f40d629"},
- {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b750a8e5a1262434fb1517ddf64b5de58327f1adc3524a5e44c2ca43305eb0b"},
- {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf72af5e0fb40e9babf594308911436c8efde3cb5e75b6f206c34ad18be5c052"},
- {file = "propcache-0.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2d0a12018b04f4cb820781ec0dffb5f7c7c1d2a5cd22bff7fb055a2cb19ebce"},
- {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e800776a79a5aabdb17dcc2346a7d66d0777e942e4cd251defeb084762ecd17d"},
- {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4160d9283bd382fa6c0c2b5e017acc95bc183570cd70968b9202ad6d8fc48dce"},
- {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b43e74f1359353341a7adb783c8f1b1c676367b011709f466f42fda2045e95"},
- {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:58791550b27d5488b1bb52bc96328456095d96206a250d28d874fafe11b3dfaf"},
- {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0f022d381747f0dfe27e99d928e31bc51a18b65bb9e481ae0af1380a6725dd1f"},
- {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:297878dc9d0a334358f9b608b56d02e72899f3b8499fc6044133f0d319e2ec30"},
- {file = "propcache-0.2.1-cp313-cp313-win32.whl", hash = "sha256:ddfab44e4489bd79bda09d84c430677fc7f0a4939a73d2bba3073036f487a0a6"},
- {file = "propcache-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:556fc6c10989f19a179e4321e5d678db8eb2924131e64652a51fe83e4c3db0e1"},
- {file = "propcache-0.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6a9a8c34fb7bb609419a211e59da8887eeca40d300b5ea8e56af98f6fbbb1541"},
- {file = "propcache-0.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ae1aa1cd222c6d205853b3013c69cd04515f9d6ab6de4b0603e2e1c33221303e"},
- {file = "propcache-0.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:accb6150ce61c9c4b7738d45550806aa2b71c7668c6942f17b0ac182b6142fd4"},
- {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5eee736daafa7af6d0a2dc15cc75e05c64f37fc37bafef2e00d77c14171c2097"},
- {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7a31fc1e1bd362874863fdeed71aed92d348f5336fd84f2197ba40c59f061bd"},
- {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba4cfa1052819d16699e1d55d18c92b6e094d4517c41dd231a8b9f87b6fa681"},
- {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f089118d584e859c62b3da0892b88a83d611c2033ac410e929cb6754eec0ed16"},
- {file = "propcache-0.2.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:781e65134efaf88feb447e8c97a51772aa75e48b794352f94cb7ea717dedda0d"},
- {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31f5af773530fd3c658b32b6bdc2d0838543de70eb9a2156c03e410f7b0d3aae"},
- {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:a7a078f5d37bee6690959c813977da5291b24286e7b962e62a94cec31aa5188b"},
- {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cea7daf9fc7ae6687cf1e2c049752f19f146fdc37c2cc376e7d0032cf4f25347"},
- {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:8b3489ff1ed1e8315674d0775dc7d2195fb13ca17b3808721b54dbe9fd020faf"},
- {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9403db39be1393618dd80c746cb22ccda168efce239c73af13c3763ef56ffc04"},
- {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5d97151bc92d2b2578ff7ce779cdb9174337390a535953cbb9452fb65164c587"},
- {file = "propcache-0.2.1-cp39-cp39-win32.whl", hash = "sha256:9caac6b54914bdf41bcc91e7eb9147d331d29235a7c967c150ef5df6464fd1bb"},
- {file = "propcache-0.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:92fc4500fcb33899b05ba73276dfb684a20d31caa567b7cb5252d48f896a91b1"},
- {file = "propcache-0.2.1-py3-none-any.whl", hash = "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54"},
- {file = "propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64"},
-]
-
[[package]]
name = "proto-plus"
version = "1.26.0"
@@ -3089,22 +2375,22 @@ windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "pyopenssl"
-version = "23.3.0"
+version = "24.2.1"
description = "Python wrapper module around the OpenSSL library"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
- {file = "pyOpenSSL-23.3.0-py3-none-any.whl", hash = "sha256:6756834481d9ed5470f4a9393455154bc92fe7a64b7bc6ee2c804e78c52099b2"},
- {file = "pyOpenSSL-23.3.0.tar.gz", hash = "sha256:6b2cba5cc46e822750ec3e5a81ee12819850b11303630d575e98108a079c2b12"},
+ {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,<42"
+cryptography = ">=41.0.5,<44"
[package.extras]
docs = ["sphinx (!=5.2.0,!=5.2.0.post0,!=7.2.5)", "sphinx-rtd-theme"]
-test = ["flaky", "pretend", "pytest (>=3.0.1)"]
+test = ["pretend", "pytest (>=3.0.1)", "pytest-rerunfailures"]
[[package]]
name = "pyparsing"
@@ -3271,7 +2557,7 @@ version = "6.0.2"
description = "YAML parser and emitter for Python"
optional = false
python-versions = ">=3.8"
-groups = ["main"]
+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"},
@@ -3489,21 +2775,6 @@ requests = ">=2.0.0"
[package.extras]
rsa = ["oauthlib[signedtoken] (>=3.0.0)"]
-[[package]]
-name = "requests-toolbelt"
-version = "1.0.0"
-description = "A utility belt for advanced users of python-requests"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-groups = ["main"]
-files = [
- {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"},
- {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"},
-]
-
-[package.dependencies]
-requests = ">=2.0.1,<3.0.0"
-
[[package]]
name = "retrying"
version = "1.3.4"
@@ -3539,6 +2810,21 @@ typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.1
[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"
@@ -3554,6 +2840,82 @@ files = [
[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"
@@ -3592,6 +2954,27 @@ 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"
@@ -3616,24 +2999,6 @@ files = [
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
]
-[[package]]
-name = "snscrape"
-version = "0.7.0.20230622"
-description = "A social networking service scraper"
-optional = false
-python-versions = "~=3.8"
-groups = ["main"]
-files = [
- {file = "snscrape-0.7.0.20230622-py3-none-any.whl", hash = "sha256:6eedb85c7e79f35361dde1949e1e7e2dee44e9f8469668438c9f8e72980f482f"},
- {file = "snscrape-0.7.0.20230622.tar.gz", hash = "sha256:71da8aec489a3b1139caaab699ca489c708d117828a2d5bcdf1ce2c9e76f3708"},
-]
-
-[package.dependencies]
-beautifulsoup4 = "*"
-filelock = "*"
-lxml = "*"
-requests = {version = "*", extras = ["socks"]}
-
[[package]]
name = "sortedcontainers"
version = "2.4.0"
@@ -3803,27 +3168,6 @@ files = [
{file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"},
]
-[[package]]
-name = "tiktok-downloader"
-version = "0.3.5"
-description = "Tiktok Downloader&Scraper using bs4&requests"
-optional = false
-python-versions = "*"
-groups = ["main"]
-files = [
- {file = "tiktok_downloader-0.3.5.tar.gz", hash = "sha256:f376ba0d2517fbab87b3185784d6e19481543326121427ae0986b9fdef6f4f75"},
-]
-
-[package.dependencies]
-aiohttp = "*"
-bs4 = "*"
-cloudscraper = "*"
-flask = "*"
-httpx = "*"
-requests = "*"
-rich = "*"
-tqdm = "*"
-
[[package]]
name = "tomli"
version = "2.2.1"
@@ -3891,14 +3235,14 @@ telegram = ["requests"]
[[package]]
name = "trio"
-version = "0.28.0"
+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.28.0-py3-none-any.whl", hash = "sha256:56d58977acc1635735a96581ec70513cc781b8b6decd299c487d3be2a721cd94"},
- {file = "trio-0.28.0.tar.gz", hash = "sha256:4e547896fe9e8a5658e54e4c7c5fa1db748cbbbaa7c965e7d40505b928c73c05"},
+ {file = "trio-0.29.0-py3-none-any.whl", hash = "sha256:d8c463f1a9cc776ff63e331aba44c125f423a5a13c684307e828d930e625ba66"},
+ {file = "trio-0.29.0.tar.gz", hash = "sha256:ea0d3967159fc130acb6939a0be0e558e364fee26b5deeecc893a6b08c361bdf"},
]
[package.dependencies]
@@ -3958,7 +3302,6 @@ 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"},
]
-markers = {dev = "python_version < \"3.13\""}
[[package]]
name = "typing-inspect"
@@ -3990,14 +3333,14 @@ files = [
[[package]]
name = "tzlocal"
-version = "5.2"
+version = "5.3"
description = "tzinfo object for the local timezone"
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
groups = ["main"]
files = [
- {file = "tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8"},
- {file = "tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e"},
+ {file = "tzlocal-5.3-py3-none-any.whl", hash = "sha256:3814135a1bb29763c6e4f08fd6e41dbb435c7a60bfbb03270211bcc537187d8c"},
+ {file = "tzlocal-5.3.tar.gz", hash = "sha256:2fafbfc07e9d8b49ade18f898d6bcd37ae88ce3ad6486842a2e4f03af68323d2"},
]
[package.dependencies]
@@ -4288,24 +3631,6 @@ files = [
{file = "websockets-14.2.tar.gz", hash = "sha256:5059ed9c54945efb321f097084b4c7e52c246f2c869815876a69d1efc4ad6eb5"},
]
-[[package]]
-name = "werkzeug"
-version = "3.1.3"
-description = "The comprehensive WSGI web application library."
-optional = false
-python-versions = ">=3.9"
-groups = ["main"]
-files = [
- {file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"},
- {file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"},
-]
-
-[package.dependencies]
-MarkupSafe = ">=2.1.1"
-
-[package.extras]
-watchdog = ["watchdog (>=2.3)"]
-
[[package]]
name = "win32-setctime"
version = "1.2.0"
@@ -4337,103 +3662,6 @@ files = [
[package.dependencies]
h11 = ">=0.9.0,<1"
-[[package]]
-name = "yarl"
-version = "1.18.3"
-description = "Yet another URL library"
-optional = false
-python-versions = ">=3.9"
-groups = ["main"]
-files = [
- {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34"},
- {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7"},
- {file = "yarl-1.18.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:602d98f2c2d929f8e697ed274fbadc09902c4025c5a9963bf4e9edfc3ab6f7ed"},
- {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c654d5207c78e0bd6d749f6dae1dcbbfde3403ad3a4b11f3c5544d9906969dde"},
- {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5094d9206c64181d0f6e76ebd8fb2f8fe274950a63890ee9e0ebfd58bf9d787b"},
- {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35098b24e0327fc4ebdc8ffe336cee0a87a700c24ffed13161af80124b7dc8e5"},
- {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3236da9272872443f81fedc389bace88408f64f89f75d1bdb2256069a8730ccc"},
- {file = "yarl-1.18.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2c08cc9b16f4f4bc522771d96734c7901e7ebef70c6c5c35dd0f10845270bcd"},
- {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80316a8bd5109320d38eef8833ccf5f89608c9107d02d2a7f985f98ed6876990"},
- {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1e1cc06da1491e6734f0ea1e6294ce00792193c463350626571c287c9a704db"},
- {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fea09ca13323376a2fdfb353a5fa2e59f90cd18d7ca4eaa1fd31f0a8b4f91e62"},
- {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e3b9fd71836999aad54084906f8663dffcd2a7fb5cdafd6c37713b2e72be1760"},
- {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:757e81cae69244257d125ff31663249b3013b5dc0a8520d73694aed497fb195b"},
- {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b1771de9944d875f1b98a745bc547e684b863abf8f8287da8466cf470ef52690"},
- {file = "yarl-1.18.3-cp310-cp310-win32.whl", hash = "sha256:8874027a53e3aea659a6d62751800cf6e63314c160fd607489ba5c2edd753cf6"},
- {file = "yarl-1.18.3-cp310-cp310-win_amd64.whl", hash = "sha256:93b2e109287f93db79210f86deb6b9bbb81ac32fc97236b16f7433db7fc437d8"},
- {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069"},
- {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193"},
- {file = "yarl-1.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889"},
- {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8"},
- {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca"},
- {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8"},
- {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae"},
- {file = "yarl-1.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3"},
- {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb"},
- {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e"},
- {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59"},
- {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d"},
- {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e"},
- {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a"},
- {file = "yarl-1.18.3-cp311-cp311-win32.whl", hash = "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1"},
- {file = "yarl-1.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5"},
- {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50"},
- {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576"},
- {file = "yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640"},
- {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2"},
- {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75"},
- {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512"},
- {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba"},
- {file = "yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb"},
- {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272"},
- {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6"},
- {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e"},
- {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb"},
- {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393"},
- {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285"},
- {file = "yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2"},
- {file = "yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477"},
- {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb"},
- {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa"},
- {file = "yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782"},
- {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0"},
- {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482"},
- {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186"},
- {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58"},
- {file = "yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53"},
- {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2"},
- {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8"},
- {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1"},
- {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a"},
- {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10"},
- {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8"},
- {file = "yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d"},
- {file = "yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c"},
- {file = "yarl-1.18.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:61e5e68cb65ac8f547f6b5ef933f510134a6bf31bb178be428994b0cb46c2a04"},
- {file = "yarl-1.18.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe57328fbc1bfd0bd0514470ac692630f3901c0ee39052ae47acd1d90a436719"},
- {file = "yarl-1.18.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a440a2a624683108a1b454705ecd7afc1c3438a08e890a1513d468671d90a04e"},
- {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09c7907c8548bcd6ab860e5f513e727c53b4a714f459b084f6580b49fa1b9cee"},
- {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4f6450109834af88cb4cc5ecddfc5380ebb9c228695afc11915a0bf82116789"},
- {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9ca04806f3be0ac6d558fffc2fdf8fcef767e0489d2684a21912cc4ed0cd1b8"},
- {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77a6e85b90a7641d2e07184df5557132a337f136250caafc9ccaa4a2a998ca2c"},
- {file = "yarl-1.18.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6333c5a377c8e2f5fae35e7b8f145c617b02c939d04110c76f29ee3676b5f9a5"},
- {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0b3c92fa08759dbf12b3a59579a4096ba9af8dd344d9a813fc7f5070d86bbab1"},
- {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4ac515b860c36becb81bb84b667466885096b5fc85596948548b667da3bf9f24"},
- {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:045b8482ce9483ada4f3f23b3774f4e1bf4f23a2d5c912ed5170f68efb053318"},
- {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:a4bb030cf46a434ec0225bddbebd4b89e6471814ca851abb8696170adb163985"},
- {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:54d6921f07555713b9300bee9c50fb46e57e2e639027089b1d795ecd9f7fa910"},
- {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1d407181cfa6e70077df3377938c08012d18893f9f20e92f7d2f314a437c30b1"},
- {file = "yarl-1.18.3-cp39-cp39-win32.whl", hash = "sha256:ac36703a585e0929b032fbaab0707b75dc12703766d0b53486eabd5139ebadd5"},
- {file = "yarl-1.18.3-cp39-cp39-win_amd64.whl", hash = "sha256:ba87babd629f8af77f557b61e49e7c7cac36f22f871156b91e10a6e9d4f829e9"},
- {file = "yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b"},
- {file = "yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1"},
-]
-
-[package.dependencies]
-idna = ">=2.0"
-multidict = ">=4.0"
-propcache = ">=0.2.0"
-
[[package]]
name = "yt-dlp"
version = "2025.1.26"
@@ -4458,5 +3686,5 @@ test = ["pytest (>=8.1,<9.0)", "pytest-rerunfailures (>=14.0,<15.0)"]
[metadata]
lock-version = "2.1"
-python-versions = ">=3.10,<4.0"
-content-hash = "700297d0b02a98913b7294fdf285dba894a245157638377d189b42e8f8ab8c84"
+python-versions = ">=3.10,<3.13"
+content-hash = "11d734f2ee32206214a7ecb8dc3ec8d19a7b6281ee98b509a5bb8bdb647c674a"
diff --git a/pyproject.toml b/pyproject.toml
index 490f6dc..ea1c87d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -17,11 +17,11 @@ classifiers = [
"Programming Language :: Python :: 3"
]
-requires-python = ">=3.10,<4.0"
+requires-python = ">=3.10,<3.13"
dependencies = [
- "auto-archiver (>=0.12.0,<0.13.0)",
+ "auto-archiver (>=0.13.1)",
"oscrypto @ git+https://github.com/wbond/oscrypto.git@d5f3437ed24257895ae1edd9e503cfb352e635a8",
"celery (>=5.0)",
"redis (==3.5.3)",
@@ -29,10 +29,11 @@ dependencies = [
"pydantic-settings (>=2.7.1,<3.0.0)",
"sqlalchemy (>=2.0.38,<3.0.0)",
"requests (>=2.25.1)",
- "pyopenssl (==23.3.0)",
+ "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"
@@ -43,6 +44,7 @@ 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]
From 834e3d86da4b6f56a5059241f158f0ccd480cc16 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Mon, 17 Feb 2025 15:42:57 +0000
Subject: [PATCH 69/75] adds missing tests
---
app/tests/conftest.py | 46 +++++
app/tests/shared/db/test_worker_crud.py | 21 ++-
app/tests/shared/test_business_logic.py | 36 ++++
app/tests/shared/utils/test_misc.py | 31 +++
app/tests/web/db/test_crud.py | 177 +++++++++++++++---
app/tests/web/endpoints/test_default.py | 77 +++++++-
.../web/endpoints/test_interoperability.py | 19 +-
app/tests/web/test_security.py | 17 +-
app/tests/worker/test_worker_main.py | 58 +++---
app/web/db/crud.py | 2 +-
app/worker/main.py | 1 -
11 files changed, 421 insertions(+), 64 deletions(-)
create mode 100644 app/tests/shared/test_business_logic.py
create mode 100644 app/tests/shared/utils/test_misc.py
diff --git a/app/tests/conftest.py b/app/tests/conftest.py
index 37acbf2..afa76f9 100644
--- a/app/tests/conftest.py
+++ b/app/tests/conftest.py
@@ -1,7 +1,10 @@
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
@@ -60,6 +63,49 @@ def db_session(test_db):
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
diff --git a/app/tests/shared/db/test_worker_crud.py b/app/tests/shared/db/test_worker_crud.py
index 70f0fda..c4e6247 100644
--- a/app/tests/shared/db/test_worker_crud.py
+++ b/app/tests/shared/db/test_worker_crud.py
@@ -1,8 +1,27 @@
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
@@ -95,4 +114,4 @@ def test_create_task(db_session):
assert nt.group_id == "spaceship"
assert len(nt.tags) == 0
assert len(nt.urls) == 0
- assert nt.created_at is not None
+ 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/app/tests/web/db/test_crud.py b/app/tests/web/db/test_crud.py
index c96c74d..c29cbfa 100644
--- a/app/tests/web/db/test_crud.py
+++ b/app/tests/web/db/test_crud.py
@@ -1,4 +1,4 @@
-from datetime import datetime
+from datetime import datetime, timedelta
from unittest.mock import patch
import pytest
@@ -6,6 +6,7 @@ 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"]
@@ -62,7 +63,6 @@ def test_data(db_session):
def test_get_archive(test_data, db_session):
- from app.web.db import crud
from app.web.config import ALLOW_ANY_EMAIL
# each author's archives work
@@ -91,7 +91,6 @@ def test_get_archive(test_data, db_session):
def test_search_archives_by_url(test_data, db_session):
- from app.web.db import crud
from app.web.config import ALLOW_ANY_EMAIL
# rick's archives are private
@@ -139,7 +138,6 @@ def test_search_archives_by_url(test_data, db_session):
def test_search_archives_by_email(test_data, db_session):
from app.web.config import ALLOW_ANY_EMAIL
- from app.web.db import crud
# lower/upper case
assert len(crud.search_archives_by_email(db_session, "rick@example.com")) == 34
@@ -160,7 +158,6 @@ def test_search_archives_by_email(test_data, db_session):
@patch("app.web.db.crud.DATABASE_QUERY_LIMIT", new=25)
def test_max_query_limit(test_data, db_session):
- from app.web.db import crud
from app.web.config import ALLOW_ANY_EMAIL
assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL)) == 25
@@ -171,8 +168,6 @@ def test_max_query_limit(test_data, db_session):
def test_soft_delete(test_data, db_session):
- from app.web.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
@@ -189,8 +184,6 @@ def test_soft_delete(test_data, db_session):
def test_count_archives(test_data, db_session):
- from app.web.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()
@@ -198,8 +191,6 @@ def test_count_archives(test_data, db_session):
def test_count_archive_urls(test_data, db_session):
- from app.web.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()
@@ -213,8 +204,6 @@ def test_count_archive_urls(test_data, db_session):
def test_count_users(test_data, db_session):
- from app.web.db import crud
-
assert crud.count_users(db_session) == 3
db_session.query(models.User).filter(models.User.email == "rick@example.com").delete()
db_session.commit()
@@ -232,8 +221,6 @@ def test_count_by_users_since(test_data, db_session):
def test_upsert_group(test_data, db_session):
- from app.web.db import crud
-
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"]]
@@ -262,8 +249,6 @@ def test_upsert_group(test_data, db_session):
def test_upsert_user_groups(db_session):
- from app.web.db import crud
-
@patch('app.web.db.crud.get_settings', new=lambda: bad_setings)
def test_missing_yaml(db_session):
with pytest.raises(FileNotFoundError):
@@ -284,8 +269,6 @@ def test_upsert_user_groups(db_session):
def test_create_sheet(db_session):
- from app.web.db import crud
-
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")
@@ -305,8 +288,6 @@ def test_create_sheet(db_session):
def test_get_user_sheet(test_data, db_session):
- from app.web.db import crud
-
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
@@ -316,8 +297,6 @@ def test_get_user_sheet(test_data, db_session):
def test_get_user_sheets(test_data, db_session):
- from app.web.db import crud
-
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
@@ -326,8 +305,156 @@ def test_get_user_sheets(test_data, db_session):
def test_delete_sheet(test_data, db_session):
- from app.web.db import crud
-
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/endpoints/test_default.py b/app/tests/web/endpoints/test_default.py
index b4ed7a5..401a164 100644
--- a/app/tests/web/endpoints/test_default.py
+++ b/app/tests/web/endpoints/test_default.py
@@ -1,6 +1,8 @@
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
@@ -13,6 +15,7 @@ def test_endpoint_home(client_with_auth):
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
@@ -28,7 +31,7 @@ def test_endpoint_active(app):
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)
@@ -42,7 +45,6 @@ def test_endpoint_active(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):
@@ -100,3 +102,74 @@ async def test_prometheus_metrics(test_data, client_with_token, get_settings):
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
index c3f8cb5..edf8c0b 100644
--- a/app/tests/web/endpoints/test_interoperability.py
+++ b/app/tests/web/endpoints/test_interoperability.py
@@ -32,9 +32,24 @@ def test_submit_manual_archive(m1, client_with_token, db_session):
assert [u.url for u in inserted.urls] == ["http://example.s3.com"]
assert type(inserted.store_until) == datetime
-
- # cannot have the same URL twice
+ # 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/test_security.py b/app/tests/web/test_security.py
index 4a46823..1a6c00b 100644
--- a/app/tests/web/test_security.py
+++ b/app/tests/web/test_security.py
@@ -1,4 +1,4 @@
-from unittest.mock import patch
+from unittest.mock import Mock, patch
from fastapi import HTTPException
from fastapi.security import HTTPAuthorizationCredentials
@@ -101,8 +101,21 @@ async def test_authenticate_user():
@pytest.mark.asyncio
async def test_authenticate_user_exception():
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
index e4dd549..b6ca8f3 100644
--- a/app/tests/worker/test_worker_main.py
+++ b/app/tests/worker/test_worker_main.py
@@ -10,7 +10,6 @@ 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")
@@ -19,20 +18,21 @@ class Test_create_archive_task():
@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("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.run.return_value = Metadata().set_url(self.URL).success()
+ m_orchestrator.return_value.run.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_orchestrator.run.assert_called_once()
+ m_urls.assert_called_once()
+ m_orchestrator.return_value.run.assert_called_once()
assert task["status"] == "success"
assert task["metadata"]["url"] == self.URL
@@ -43,56 +43,54 @@ class Test_create_archive_task():
with pytest.raises(Exception):
create_archive_task(self.archive.model_dump_json())
- @patch("app.worker.main.insert_result_into_db", side_effect=Exception)
+ @patch("app.worker.main.ArchivingOrchestrator")
@patch("app.worker.main.get_orchestrator_args")
- def test_raise_db_error(self, m_args, m_insert):
+ def test_raise_db_error(self, m_args, m_orchestrator):
from app.worker.main import create_archive_task
- mock_orchestrator = self.mock_orchestrator_choice(m_args)
-
- with pytest.raises(Exception):
- create_archive_task(self.archive.model_dump_json())
- mock_orchestrator.feed_item.assert_called_once()
-
-
- @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):
- from app.worker.main import create_archive_task
- mock_orchestrator = self.mock_orchestrator_choice(m_args)
+ m_orchestrator.return_value.run.side_effect = Exception("Orchestrator failed")
with pytest.raises(Exception) as e:
create_archive_task(self.archive.model_dump_json())
- mock_orchestrator.feed_item.assert_called_once()
+ assert str(e.value) == "Orchestrator failed"
+ m_args.assert_called_once()
+ m_orchestrator.return_value.run.assert_called_once()
- def mock_orchestrator_choice(self, m_load):
- mock_orchestrator = mock.MagicMock()
- mock_orchestrator.configure_mock(feed_item=mock.MagicMock(return_value=Metadata().set_url(self.URL).success()))
- m_load.return_value = mock_orchestrator
- return mock_orchestrator
+ @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.run.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.run.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, db_session):
+ 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_orch = MagicMock()
- m_orch.feed.return_value = iter([False, mock_metadata, mock_metadata])
- m_args.return_value = m_orch
+
+ m_orchestrator.return_value.run.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, {'configurations': {'gsheet_feeder': {'sheet_id': '123'}}})
- m_orch.feed.assert_called_once()
+ m_args.assert_called_once_with("interstellar", True, ["--gsheet_feeder.sheet_id", "123"])
+ m_orchestrator.return_value.run.assert_called_once()
m_store.assert_called_with("interstellar")
m_store.call_count == 2
m_uuid.call_count == 2
diff --git a/app/web/db/crud.py b/app/web/db/crud.py
index d3be57c..e70b01c 100644
--- a/app/web/db/crud.py
+++ b/app/web/db/crud.py
@@ -242,7 +242,7 @@ 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: str) -> list[models.Sheet]:
+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)
)
diff --git a/app/worker/main.py b/app/worker/main.py
index c8fb585..b5e74a2 100644
--- a/app/worker/main.py
+++ b/app/worker/main.py
@@ -4,7 +4,6 @@ import traceback, datetime
from celery.signals import task_failure
from loguru import logger
from sqlalchemy import exc
-import auto_archiver
from auto_archiver.core.orchestrator import ArchivingOrchestrator
from app.shared.db import models
From 9a11c430ea5e34fad1d77145dc80f6d4e733e805 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Tue, 18 Feb 2025 01:11:09 +0000
Subject: [PATCH 70/75] adds todos
---
app/web/db/crud.py | 1 +
app/worker/main.py | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/app/web/db/crud.py b/app/web/db/crud.py
index e70b01c..fa159e8 100644
--- a/app/web/db/crud.py
+++ b/app/web/db/crud.py
@@ -121,6 +121,7 @@ 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
diff --git a/app/worker/main.py b/app/worker/main.py
index b5e74a2..115ea9a 100644
--- a/app/worker/main.py
+++ b/app/worker/main.py
@@ -22,7 +22,7 @@ Redis = get_redis()
USER_GROUPS_FILENAME = settings.USER_GROUPS_FILENAME
-# PATCHES for new aa's functionality
+# 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})")
From 8f0cfd223911b1608cd2982b7902915f46715c23 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Tue, 18 Feb 2025 14:51:31 +0000
Subject: [PATCH 71/75] closes TODOs, renames task to archive, fixes read
permissions and fixes tests
---
app/shared/db/models.py | 2 +-
app/shared/db/worker_crud.py | 19 ++--
app/shared/schemas.py | 2 +-
app/shared/settings.py | 2 +-
app/tests/shared/db/test_worker_crud.py | 4 +-
app/tests/web/db/test_crud.py | 90 +++++++------------
.../web/endpoints/test_interoperability.py | 3 +-
app/tests/web/endpoints/test_url.py | 5 +-
app/tests/web/test_main.py | 2 +-
app/web/db/crud.py | 36 ++++----
app/web/endpoints/sheet.py | 2 +-
app/web/endpoints/url.py | 12 +--
app/web/events.py | 13 +--
app/web/main.py | 14 +--
app/web/middleware.py | 18 +++-
app/web/utils/metrics.py | 2 +-
app/worker/main.py | 11 +--
17 files changed, 114 insertions(+), 123 deletions(-)
diff --git a/app/shared/db/models.py b/app/shared/db/models.py
index 0e12c7b..1736224 100644
--- a/app/shared/db/models.py
+++ b/app/shared/db/models.py
@@ -103,7 +103,7 @@ class Sheet(Base):
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 needed, is it?
+ # 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())
diff --git a/app/shared/db/worker_crud.py b/app/shared/db/worker_crud.py
index 6766786..814689a 100644
--- a/app/shared/db/worker_crud.py
+++ b/app/shared/db/worker_crud.py
@@ -41,15 +41,14 @@ def create_tag(db: Session, tag: str) -> models.Tag:
return db_tag
-def create_task(db: Session, task: schemas.ArchiveCreate, tags: list[models.Tag], urls: list[models.ArchiveUrl]) -> models.Archive:
- # TODO: rename task to archive
- 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, sheet_id=task.sheet_id, store_until=task.store_until)
- db_task.tags = tags
- db_task.urls = urls
- db.add(db_task)
+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_task)
- return db_task
+ db.refresh(db_archive)
+ return db_archive
def store_archived_url(db: Session, archive: schemas.ArchiveCreate) -> models.Archive:
@@ -57,5 +56,5 @@ def store_archived_url(db: Session, archive: schemas.ArchiveCreate) -> models.Ar
create_or_get_user(db, archive.author_id)
db_tags = [create_tag(db, tag) for tag in (archive.tags or [])]
# insert everything
- db_task = create_task(db, task=archive, tags=db_tags, urls=archive.urls)
- return db_task
+ db_archive = create_archive(db, archive=archive, tags=db_tags, urls=archive.urls)
+ return db_archive
diff --git a/app/shared/schemas.py b/app/shared/schemas.py
index 2a91fd7..66119f7 100644
--- a/app/shared/schemas.py
+++ b/app/shared/schemas.py
@@ -34,7 +34,7 @@ class TaskResult(Task):
result: str
-class TaskDelete(Task):
+class DeleteResponse(Task):
deleted: bool
diff --git a/app/shared/settings.py b/app/shared/settings.py
index 866b23a..d884f80 100644
--- a/app/shared/settings.py
+++ b/app/shared/settings.py
@@ -12,7 +12,7 @@ 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 = ""
+ SERVE_LOCAL_ARCHIVE: str | None = None
USER_GROUPS_FILENAME: str = "app/user-groups.yaml"
# database
diff --git a/app/tests/shared/db/test_worker_crud.py b/app/tests/shared/db/test_worker_crud.py
index c4e6247..1098cbe 100644
--- a/app/tests/shared/db/test_worker_crud.py
+++ b/app/tests/shared/db/test_worker_crud.py
@@ -88,7 +88,7 @@ def test_create_task(db_session):
)
# with tags and urls
- nt = worker_crud.create_task(db_session, task, [models.Tag(id="tag-101")], [models.ArchiveUrl(url="https://example-0.com/0", key="media_0")])
+ 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"
@@ -105,7 +105,7 @@ def test_create_task(db_session):
# without tags and urls
task.id = "archive-id-456-102"
- nt = worker_crud.create_task(db_session, task, [], [])
+ 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"
diff --git a/app/tests/web/db/test_crud.py b/app/tests/web/db/test_crud.py
index c29cbfa..aad9d4c 100644
--- a/app/tests/web/db/test_crud.py
+++ b/app/tests/web/db/test_crud.py
@@ -62,78 +62,56 @@ def test_data(db_session):
assert db_session.query(models.User).count() == 3
-def test_get_archive(test_data, db_session):
- from app.web.config import ALLOW_ANY_EMAIL
-
- # 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 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")) == 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
+ 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")) == 16
+ 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")) == 33
- assert len(crud.search_archives_by_url(db_session, "https://example-2.com", "rick@example.com")) == 33
+ 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)) == 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
+ 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, absolute_search=True)) == 0
- assert len(crud.search_archives_by_url(db_session, "https://example-2.com", ALLOW_ANY_EMAIL, absolute_search=True)) == 33
+ 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, 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
+ 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, 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
+ 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, 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
+ 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, limit=10)) == 10
- assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, limit=-1)) == 1
+ 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, skip=10)) == 90
+ 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):
@@ -160,8 +138,8 @@ def test_search_archives_by_email(test_data, db_session):
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)) == 25
- assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, limit=1000)) == 25
+ 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
@@ -169,18 +147,18 @@ def test_max_query_limit(test_data, db_session):
def test_soft_delete(test_data, db_session):
# none deleted yet
- assert crud.get_archive(db_session, "archive-id-456-0", "rick@example.com") is not None
+ 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_task(db_session, "archive-id-456-0", "rick@example.com") == True
+ 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
- assert crud.get_archive(db_session, "archive-id-456-0", "rick@example.com") is None
+ db_session.query(models.Archive).filter(models.Archive.id == "archive-id-456-0").first() is None
# already deleted
- assert crud.soft_delete_task(db_session, "archive-id-456-0", "rick@example.com") == False
+ assert crud.soft_delete_archive(db_session, "archive-id-456-0", "rick@example.com") == False
def test_count_archives(test_data, db_session):
diff --git a/app/tests/web/endpoints/test_interoperability.py b/app/tests/web/endpoints/test_interoperability.py
index edf8c0b..14cd245 100644
--- a/app/tests/web/endpoints/test_interoperability.py
+++ b/app/tests/web/endpoints/test_interoperability.py
@@ -2,6 +2,7 @@ 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
@@ -22,7 +23,7 @@ def test_submit_manual_archive(m1, client_with_token, db_session):
assert r.status_code == 201
assert "id" in r.json()
- inserted = crud.get_archive(db_session, r.json()["id"], ALLOW_ANY_EMAIL)
+ 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"
diff --git a/app/tests/web/endpoints/test_url.py b/app/tests/web/endpoints/test_url.py
index b8bc15b..4a6f342 100644
--- a/app/tests/web/endpoints/test_url.py
+++ b/app/tests/web/endpoints/test_url.py
@@ -133,8 +133,7 @@ def test_search_by_url(client_with_auth, client_with_token, db_session):
from app.shared import schemas
from app.shared.db import worker_crud
for i in range(11):
- #TODO: fix as this method is gone to shared.db
- worker_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"), [], [])
+ 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")
@@ -187,7 +186,7 @@ def test_delete_task(client_with_auth, db_session):
assert response.json() == {"id": "delete-123-456-789", "deleted": False}
from app.shared.db import worker_crud
- worker_crud.create_task(db_session, ArchiveCreate(id="delete-123-456-789", url="https://example.com", result={}, public=True, author_id="morty@example.com"), [], [])
+ 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
diff --git a/app/tests/web/test_main.py b/app/tests/web/test_main.py
index 8cee578..f77d368 100644
--- a/app/tests/web/test_main.py
+++ b/app/tests/web/test_main.py
@@ -17,7 +17,7 @@ def test_alembic(db_session):
alembic.config.main(argv=['--raiseerr', 'upgrade', 'head'])
alembic.config.main(argv=['--raiseerr', 'downgrade', 'base'])
-@patch("app.web.endpoints.url.crud.soft_delete_task", 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 app.web.utils.metrics import EXCEPTION_COUNTER
assert len(EXCEPTION_COUNTER.collect()[0].samples) == 0
diff --git a/app/web/db/crud.py b/app/web/db/crud.py
index fa159e8..c16b09a 100644
--- a/app/web/db/crud.py
+++ b/app/web/db/crud.py
@@ -32,20 +32,18 @@ def base_query(db: Session):
.options(load_only(models.Archive.id, models.Archive.created_at, models.Archive.url, models.Archive.result, models.Archive.store_until))
-def get_archive(db: Session, id: str, email: str):
- query = base_query(db).filter(models.Archive.id == id)
- if email != ALLOW_ANY_EMAIL:
- groups = get_user_group_names(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) -> list[models.Archive]:
- # searches for partial URLs, if email is * no ownership filtering happens
+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:
- groups = get_user_group_names(db, email)
- query = query.filter(or_(models.Archive.public == True, models.Archive.author_id == email, models.Archive.group_id.in_(groups)))
+ 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:
@@ -61,13 +59,13 @@ def search_archives_by_email(db: Session, email: str, skip: int = 0, limit: int
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_task(db: Session, task_id: str, email: str) -> bool:
+def soft_delete_archive(db: Session, 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_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_task is not None
+ return db_archive is not None
def count_archives(db: Session):
@@ -121,7 +119,7 @@ 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
+ # 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
@@ -178,8 +176,8 @@ def upsert_user_groups(db: Session):
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
+ logger.debug(f"Updating user-groups configuration with file {filename}.")
ug = UserGroups(filename)
diff --git a/app/web/endpoints/sheet.py b/app/web/endpoints/sheet.py
index 9177668..7848b5e 100644
--- a/app/web/endpoints/sheet.py
+++ b/app/web/endpoints/sheet.py
@@ -51,7 +51,7 @@ def delete_sheet(
id: str,
user: UserState = Depends(get_user_state),
db: Session = Depends(get_db_dependency),
-) -> schemas.TaskDelete:
+) -> schemas.DeleteResponse:
return JSONResponse({
"id": id,
"deleted": crud.delete_sheet(db, id, user.email)
diff --git a/app/web/endpoints/url.py b/app/web/endpoints/url.py
index c0c1284..c237893 100644
--- a/app/web/endpoints/url.py
+++ b/app/web/endpoints/url.py
@@ -61,22 +61,24 @@ def search_by_url(
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.")
-
- return crud.search_archives_by_url(db, url.strip(), email, skip=skip, limit=limit, archived_after=archived_after, archived_before=archived_before)
+ 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_task(
+def delete_archive(
id:str,
user: UserState = Depends(get_user_state),
db: Session = Depends(get_db_dependency)
-) -> schemas.TaskDelete:
+) -> schemas.DeleteResponse:
logger.info(f"deleting url archive task {id} request by {user.email}")
return JSONResponse({
"id": id,
- "deleted": crud.soft_delete_task(db, id, user.email)
+ "deleted": crud.soft_delete_archive(db, id, user.email)
})
diff --git a/app/web/events.py b/app/web/events.py
index 88bb18f..625731a 100644
--- a/app/web/events.py
+++ b/app/web/events.py
@@ -15,6 +15,7 @@ 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()
@@ -60,17 +61,17 @@ async def lifespan(app: FastAPI):
# CRON JOBS
-@repeat_every(seconds=get_settings().REPEAT_COUNT_METRICS_SECONDS, on_exception=logger.error)
+@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=logger.error)
+@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=logger.error)
+@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)
@@ -92,7 +93,7 @@ async def archive_sheets_cronjob(frequency: str, interval: int, current_time_uni
DELETE_WINDOW = get_settings().DELETE_SCHEDULED_ARCHIVES_CHECK_EVERY_N_DAYS * 24 * 60 * 60
-@repeat_every(seconds=DELETE_WINDOW, wait_first=180, on_exception=logger.error)
+@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:
@@ -135,7 +136,7 @@ async def notify_about_expired_archives():
asyncio.create_task(delete_expired_archives())
-@repeat_every(max_repetitions=1, wait_first=10, seconds=0, on_exception=logger.error)
+@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)
@@ -143,7 +144,7 @@ async def delete_expired_archives():
logger.debug(f"[CRON] Deleted {count_deleted} archives.")
-@repeat_every(seconds=86400, wait_first=150, on_exception=logger.error)
+@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.")
diff --git a/app/web/main.py b/app/web/main.py
index cc7774c..ff2266e 100644
--- a/app/web/main.py
+++ b/app/web/main.py
@@ -49,12 +49,12 @@ def app_factory(settings = get_settings()):
# 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)])
- # TODO: recheck this for security, currently only needed for when local_storage is used in development
- 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)
+ 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
index 227a620..52da626 100644
--- a/app/web/middleware.py
+++ b/app/web/middleware.py
@@ -1,4 +1,5 @@
+import traceback
from loguru import logger
from fastapi import Request
from app.shared.log import log_error
@@ -13,7 +14,18 @@ async def logging_middleware(request: Request, call_next):
logger.info(f"{request.client.host}:{request.client.port} {request.method} {request.url._url} - HTTP {response.status_code}")
return response
except Exception as e:
- EXCEPTION_COUNTER.labels(type=e.__class__.__name__).inc()
- logger.info(f"{request.client.host}:{request.client.port} {request.method} {request.url._url} - {e.__class__.__name__} {e}")
- log_error(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/app/web/utils/metrics.py b/app/web/utils/metrics.py
index 64aaf9c..a885b9a 100644
--- a/app/web/utils/metrics.py
+++ b/app/web/utils/metrics.py
@@ -14,7 +14,7 @@ from app.shared.task_messaging import get_redis
EXCEPTION_COUNTER = Counter(
"exceptions",
"Number of times a certain exception has occurred.",
- labelnames=["type"]
+ labelnames=["type", "location"]
)
WORKER_EXCEPTION = Counter(
"worker_exceptions_total",
diff --git a/app/worker/main.py b/app/worker/main.py
index 115ea9a..a9a10c6 100644
--- a/app/worker/main.py
+++ b/app/worker/main.py
@@ -27,14 +27,14 @@ USER_GROUPS_FILENAME = settings.USER_GROUPS_FILENAME
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': 0})
+@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])
# args = get_orchestrator_args(archive.group_id, False, [archive.url, "--extractors", "generic_extractor"])
- logger.error(args)
+ logger.debug(args)
try:
result = next(ArchivingOrchestrator().run(args), None)
except SystemExit as e:
@@ -61,6 +61,7 @@ def create_sheet_task(self, sheet_json: str):
logger.info(f"[queue={queue_name}] SHEET START {sheet=}")
args = get_orchestrator_args(sheet.group_id, True, ["--gsheet_feeder.sheet_id", sheet.sheet_id])
+ logger.info(f"[queue={queue_name}] {args=}")
stats = {"archived": 0, "failed": 0, "errors": []}
try:
@@ -116,9 +117,9 @@ def get_orchestrator_args(group_id: str, orchestrator_for_sheet: bool, cli_args:
def insert_result_into_db(archive: schemas.ArchiveCreate) -> str:
with get_db() as session:
- db_task = worker_crud.store_archived_url(session, archive)
- logger.debug(f"[ARCHIVE STORED] {db_task.author_id} {db_task.url}")
- return db_task.id
+ 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:
From 057324dae7209f618086cd4fa8101e0c4a9cc723 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Tue, 18 Feb 2025 15:50:47 +0000
Subject: [PATCH 72/75] cleanup readme
---
.gitignore | 3 +-
README.md | 115 ++++-------------
app/test.ipynb | 333 +++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 360 insertions(+), 91 deletions(-)
create mode 100644 app/test.ipynb
diff --git a/.gitignore b/.gitignore
index f002cf0..9bb39a9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,4 +27,5 @@ local_archive_test
*db-shm
copy-files.sh
temp/
-.python-version
\ No newline at end of file
+.python-version
+orchestration2.yaml
\ No newline at end of file
diff --git a/README.md b/README.md
index b292107..7b4fcf9 100644
--- a/README.md
+++ b/README.md
@@ -4,15 +4,19 @@
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).
-### setup
-To properly set up the API you need to install `docker` and to edit 2 files:
-1. a `.env` to configure the API, stays at the root level
+## 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
-Do not commit those files, they are .gitignored by default.
+ 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`.
-We have examples for both of those, and here's how to set them up whether you're in development or production:
+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.
-#### setup for DEVELOPMENT
+
+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:
+
+### setup for DEVELOPMENT
```bash
# copy and modify the .env.dev file according to your needs
cp .env.example .env.dev
@@ -26,7 +30,7 @@ curl 'http://localhost:8004/health'
```
now go to http://localhost:8004/docs#/ and you should see the API documentation
-#### setup for PRODUCTION
+### setup for PRODUCTION
```bash
# copy and modify the .env.prod file according to your needs
cp .env.example .env.prod
@@ -49,86 +53,36 @@ there are 2 ways to access the API
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)
- - domains are associated to groups
+ - 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
+ - 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
- - `monthly_urls` how many total URLs someone can archive per month (`-1` means no limit)
- - `monthly_mbs` how many MBs of data someone can archive per month (`-1` means no limit)
+ - `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
-To figure out:
-- workshop participants should be able to test this. `public`
-- how can people bring their own storage/api keys?
-- how to implement lifespan of archives? 6 months lifespan example. they should expect a way to download all archives locally.
-- how to deactivate unused sheets and notify?
-- how to mark URLs for deletion, and then do a hard delete?
-- what actions can people take:
- - URL (P=needs permission, O=open)
- - P archive
- - P search
- - O find own links
- - DISABLED find by id
- - P delete archive (soft)
- - Sheets
- - P create a new sheet
- - O get my sheets
- - O delete a sheet
- - P archive a sheet now
+## development of web/worker without docker
-
-## Development
-http://localhost:8004
-
-TODO: update .env file instructions, should use .env.prod and .env.dev and only use .env for always overwriting dev/prod settings.
-
-requires `src/.env`
-
-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
-TODO: update description and example
-- users/domains/groups
-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
@@ -143,32 +97,13 @@ poetry run alembic upgrade head
poetry run alembic downgrade -1
```
-* 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`
-
## 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
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
+}
From 6c61d2cd8fc1a4d8b9f6376a8db4a51f362f8a6d Mon Sep 17 00:00:00 2001
From: Miguel Sozinho Ramalho <19508417+msramalho@users.noreply.github.com>
Date: Tue, 18 Feb 2025 15:51:16 +0000
Subject: [PATCH 73/75] Update README.md
---
README.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/README.md b/README.md
index 7b4fcf9..549e1b4 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,8 @@
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).
+
+
## 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
From 7eb6f652be31389ee0801b33582246761dccf05b Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Wed, 19 Feb 2025 18:24:13 +0000
Subject: [PATCH 74/75] ignores database volume
---
.gitignore | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/.gitignore b/.gitignore
index 9bb39a9..562885d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,4 +28,5 @@ local_archive_test
copy-files.sh
temp/
.python-version
-orchestration2.yaml
\ No newline at end of file
+orchestration2.yaml
+database
\ No newline at end of file
From d9f40c8e082953a5084f2c1023a7cf6e19c4d4e8 Mon Sep 17 00:00:00 2001
From: msramalho <19508417+msramalho@users.noreply.github.com>
Date: Wed, 19 Feb 2025 18:24:46 +0000
Subject: [PATCH 75/75] bumps auto-archiver to 0.13.4
---
app/tests/worker/test_worker_main.py | 21 +++++++++++----------
app/worker/main.py | 11 ++++++-----
poetry.lock | 6 +++---
worker.Dockerfile | 2 +-
4 files changed, 21 insertions(+), 19 deletions(-)
diff --git a/app/tests/worker/test_worker_main.py b/app/tests/worker/test_worker_main.py
index b6ca8f3..d40c457 100644
--- a/app/tests/worker/test_worker_main.py
+++ b/app/tests/worker/test_worker_main.py
@@ -1,7 +1,6 @@
from datetime import datetime
-from unittest import mock
-from unittest.mock import MagicMock, patch
+from unittest.mock import patch
import pytest
@@ -24,7 +23,7 @@ class Test_create_archive_task():
from app.worker.main import create_archive_task
m_req.id = "this-just-in"
- m_orchestrator.return_value.run.return_value = iter([Metadata().set_url(self.URL).success()])
+ m_orchestrator.return_value.feed.return_value = iter([Metadata().set_url(self.URL).success()])
task = create_archive_task(self.archive.model_dump_json())
@@ -32,7 +31,8 @@ class Test_create_archive_task():
m_store.assert_called_once_with("interstellar")
m_insert.assert_called_once()
m_urls.assert_called_once()
- m_orchestrator.return_value.run.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
@@ -47,25 +47,25 @@ class Test_create_archive_task():
@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.run.side_effect = Exception("Orchestrator failed")
+ 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.run.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.run.return_value = iter([None])
+ 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.run.assert_called_once()
+ m_orchestrator.return_value.feed.assert_called_once()
class Test_create_sheet_task():
@@ -85,12 +85,13 @@ class Test_create_sheet_task():
mock_metadata = Metadata().set_url(self.URL).success()
mock_metadata.add_media(Media("fn1.txt", urls=["outcome1.com"]))
- m_orchestrator.return_value.run.return_value = iter([False, mock_metadata, mock_metadata])
+ 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.run.assert_called_once()
+ 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
diff --git a/app/worker/main.py b/app/worker/main.py
index a9a10c6..d3e50b8 100644
--- a/app/worker/main.py
+++ b/app/worker/main.py
@@ -33,10 +33,10 @@ def create_archive_task(self, archive_json: str):
# call auto-archiver
args = get_orchestrator_args(archive.group_id, False, [archive.url])
- # args = get_orchestrator_args(archive.group_id, False, [archive.url, "--extractors", "generic_extractor"])
- logger.debug(args)
try:
- result = next(ArchivingOrchestrator().run(args), None)
+ 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:
@@ -61,11 +61,12 @@ def create_sheet_task(self, sheet_json: str):
logger.info(f"[queue={queue_name}] SHEET START {sheet=}")
args = get_orchestrator_args(sheet.group_id, True, ["--gsheet_feeder.sheet_id", sheet.sheet_id])
- logger.info(f"[queue={queue_name}] {args=}")
+ orchestrator = ArchivingOrchestrator()
+ orchestrator.setup(args)
stats = {"archived": 0, "failed": 0, "errors": []}
try:
- for result in ArchivingOrchestrator().run(args):
+ for result in orchestrator.feed():
try:
assert result, f"ERROR archiving URL for sheet {sheet.sheet_id}"
archive = schemas.ArchiveCreate(
diff --git a/poetry.lock b/poetry.lock
index c818604..79f7907 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -154,14 +154,14 @@ cryptography = "*"
[[package]]
name = "auto-archiver"
-version = "0.13.2"
+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.2-py3-none-any.whl", hash = "sha256:672671080bdc2e4cd50792b3521a8a1d70aabb50ee3f779ed30879162b0c352b"},
- {file = "auto_archiver-0.13.2.tar.gz", hash = "sha256:b0d5505206bdb02f2ddb1b3a3a622780cc06ace0f3440b272f1d9bc9c314c9e2"},
+ {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]
diff --git a/worker.Dockerfile b/worker.Dockerfile
index 99ddf8a..4e24f87 100644
--- a/worker.Dockerfile
+++ b/worker.Dockerfile
@@ -1,5 +1,5 @@
# From python:3.10
-FROM bellingcat/auto-archiver
+FROM bellingcat/auto-archiver:v0.13.4
# set work directory
WORKDIR /aa-api