diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..0791816 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,147 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts. +# this is typically a path given in POSIX (e.g. forward slashes) +# format, relative to the token %(here)s which refers to the location of this +# ini file +script_location = %(here)s/alembic + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. for multiple paths, the path separator +# is defined by "path_separator" below. +prepend_sys_path = . + + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library. +# Any required deps can installed by adding `alembic[tz]` to the pip requirements +# string value is passed to ZoneInfo() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to /versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "path_separator" +# below. +# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions + +# path_separator; This indicates what character is used to split lists of file +# paths, including version_locations and prepend_sys_path within configparser +# files such as alembic.ini. +# The default rendered in new alembic.ini files is "os", which uses os.pathsep +# to provide os-dependent path splitting. +# +# Note that in order to support legacy alembic.ini files, this default does NOT +# take place if path_separator is not present in alembic.ini. If this +# option is omitted entirely, fallback logic is as follows: +# +# 1. Parsing of the version_locations option falls back to using the legacy +# "version_path_separator" key, which if absent then falls back to the legacy +# behavior of splitting on spaces and/or commas. +# 2. Parsing of the prepend_sys_path option falls back to the legacy +# behavior of splitting on spaces, commas, or colons. +# +# Valid values for path_separator are: +# +# path_separator = : +# path_separator = ; +# path_separator = space +# path_separator = newline +# +# Use os.pathsep. Default configuration used for new projects. +path_separator = os + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +# database URL. This is consumed by the user-maintained env.py script only. +# other means of configuring database URLs may be customized within the env.py +# file. +sqlalchemy.url = driver://user:pass@localhost/dbname + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module +# hooks = ruff +# ruff.type = module +# ruff.module = ruff +# ruff.options = check --fix REVISION_SCRIPT_FILENAME + +# Alternatively, use the exec runner to execute a binary found on your PATH +# hooks = ruff +# ruff.type = exec +# ruff.executable = ruff +# ruff.options = check --fix REVISION_SCRIPT_FILENAME + +# Logging configuration. This is also consumed by the user-maintained +# env.py script only. +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARNING +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARNING +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/alembic/README b/alembic/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/alembic/env.py b/alembic/env.py new file mode 100644 index 0000000..808027e --- /dev/null +++ b/alembic/env.py @@ -0,0 +1,103 @@ +# STL +import os +import sys +from logging.config import fileConfig + +# PDM +from dotenv import load_dotenv +from sqlalchemy import pool, engine_from_config + +# LOCAL +from alembic import context +from api.backend.database.base import Base +from api.backend.database.models import Job, User, CronJob # type: ignore + +load_dotenv() + +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "api"))) + +# Load the raw async database URL +raw_database_url = os.getenv("DATABASE_URL", "sqlite+aiosqlite:///data/database.db") + +# Map async dialects to sync ones +driver_downgrade_map = { + "sqlite+aiosqlite": "sqlite", + "postgresql+asyncpg": "postgresql", + "mysql+aiomysql": "mysql", +} + +# Extract scheme and convert if async +for async_driver, sync_driver in driver_downgrade_map.items(): + if raw_database_url.startswith(async_driver + "://"): + sync_database_url = raw_database_url.replace(async_driver, sync_driver, 1) + break + +else: + # No async driver detected — assume it's already sync + sync_database_url = raw_database_url + + +# Apply it to Alembic config +config = context.config +config.set_main_option("sqlalchemy.url", sync_database_url) + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = Base.metadata + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure(connection=connection, target_metadata=target_metadata) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/alembic/script.py.mako b/alembic/script.py.mako new file mode 100644 index 0000000..1101630 --- /dev/null +++ b/alembic/script.py.mako @@ -0,0 +1,28 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + """Upgrade schema.""" + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + """Downgrade schema.""" + ${downgrades if downgrades else "pass"} diff --git a/alembic/versions/6aa921d2e637_initial_revision.py b/alembic/versions/6aa921d2e637_initial_revision.py new file mode 100644 index 0000000..036bfc0 --- /dev/null +++ b/alembic/versions/6aa921d2e637_initial_revision.py @@ -0,0 +1,67 @@ +"""initial revision + +Revision ID: 6aa921d2e637 +Revises: +Create Date: 2025-07-12 20:17:44.448034 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '6aa921d2e637' +down_revision: Union[str, Sequence[str], None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('users', + sa.Column('email', sa.String(length=255), nullable=False), + sa.Column('hashed_password', sa.String(length=255), nullable=False), + sa.Column('full_name', sa.String(length=255), nullable=True), + sa.Column('disabled', sa.Boolean(), nullable=True), + sa.PrimaryKeyConstraint('email') + ) + op.create_table('jobs', + sa.Column('id', sa.String(length=64), nullable=False), + sa.Column('url', sa.String(length=2048), nullable=False), + sa.Column('elements', sa.JSON(), nullable=False), + sa.Column('user', sa.String(length=255), nullable=True), + sa.Column('time_created', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('result', sa.JSON(), nullable=False), + sa.Column('status', sa.String(length=50), nullable=False), + sa.Column('chat', sa.JSON(), nullable=True), + sa.Column('job_options', sa.JSON(), nullable=True), + sa.Column('agent_mode', sa.Boolean(), nullable=False), + sa.Column('prompt', sa.String(length=1024), nullable=True), + sa.Column('favorite', sa.Boolean(), nullable=False), + sa.ForeignKeyConstraint(['user'], ['users.email'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('cron_jobs', + sa.Column('id', sa.String(length=64), nullable=False), + sa.Column('user_email', sa.String(length=255), nullable=False), + sa.Column('job_id', sa.String(length=64), nullable=False), + sa.Column('cron_expression', sa.String(length=255), nullable=False), + sa.Column('time_created', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('time_updated', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.ForeignKeyConstraint(['job_id'], ['jobs.id'], ), + sa.ForeignKeyConstraint(['user_email'], ['users.email'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('cron_jobs') + op.drop_table('jobs') + op.drop_table('users') + # ### end Alembic commands ### diff --git a/api/backend/app.py b/api/backend/app.py index 52bea38..08ea7b3 100644 --- a/api/backend/app.py +++ b/api/backend/app.py @@ -15,7 +15,6 @@ from api.backend.scheduler import scheduler from api.backend.ai.ai_router import ai_router from api.backend.job.job_router import job_router from api.backend.auth.auth_router import auth_router -from api.backend.database.startup import init_database from api.backend.stats.stats_router import stats_router from api.backend.job.cron_scheduling.cron_scheduling import start_cron_scheduler @@ -36,10 +35,8 @@ async def lifespan(_: FastAPI): # Startup LOG.info("Starting application...") - init_database() - LOG.info("Starting cron scheduler...") - start_cron_scheduler(scheduler) + await start_cron_scheduler(scheduler) scheduler.start() LOG.info("Cron scheduler started successfully") diff --git a/api/backend/auth/auth_router.py b/api/backend/auth/auth_router.py index 79e2ee5..be0fc4c 100644 --- a/api/backend/auth/auth_router.py +++ b/api/backend/auth/auth_router.py @@ -6,9 +6,11 @@ from datetime import timedelta # PDM from fastapi import Depends, APIRouter, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm +from sqlalchemy.ext.asyncio import AsyncSession # LOCAL from api.backend.auth.schemas import User, Token, UserCreate +from api.backend.database.base import AsyncSessionLocal, get_db from api.backend.auth.auth_utils import ( ACCESS_TOKEN_EXPIRE_MINUTES, get_current_user, @@ -16,7 +18,7 @@ from api.backend.auth.auth_utils import ( get_password_hash, create_access_token, ) -from api.backend.database.common import update +from api.backend.database.models import User as DatabaseUser from api.backend.routers.handle_exceptions import handle_exceptions auth_router = APIRouter() @@ -26,8 +28,8 @@ LOG = logging.getLogger("Auth") @auth_router.post("/auth/token", response_model=Token) @handle_exceptions(logger=LOG) -async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()): - user = await authenticate_user(form_data.username, form_data.password) +async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: AsyncSession = Depends(get_db)): + user = await authenticate_user(form_data.username, form_data.password, db) if not user: raise HTTPException( @@ -56,8 +58,15 @@ async def create_user(user: UserCreate): user_dict["hashed_password"] = hashed_password del user_dict["password"] - query = "INSERT INTO users (email, hashed_password, full_name) VALUES (?, ?, ?)" - _ = update(query, (user_dict["email"], hashed_password, user_dict["full_name"])) + async with AsyncSessionLocal() as session: + new_user = DatabaseUser( + email=user.email, + hashed_password=user_dict["hashed_password"], + full_name=user.full_name, + ) + + session.add(new_user) + await session.commit() return user_dict diff --git a/api/backend/auth/auth_utils.py b/api/backend/auth/auth_utils.py index a96664d..40b2eb9 100644 --- a/api/backend/auth/auth_utils.py +++ b/api/backend/auth/auth_utils.py @@ -8,12 +8,15 @@ from datetime import datetime, timedelta from jose import JWTError, jwt from dotenv import load_dotenv from fastapi import Depends, HTTPException, status +from sqlalchemy import select from passlib.context import CryptContext from fastapi.security import OAuth2PasswordBearer +from sqlalchemy.ext.asyncio import AsyncSession # LOCAL from api.backend.auth.schemas import User, UserInDB, TokenData -from api.backend.database.common import query +from api.backend.database.base import get_db +from api.backend.database.models import User as UserModel LOG = logging.getLogger("Auth") @@ -37,18 +40,24 @@ def get_password_hash(password: str): return pwd_context.hash(password) -async def get_user(email: str): - user_query = "SELECT * FROM users WHERE email = ?" - user = query(user_query, (email,))[0] +async def get_user(session: AsyncSession, email: str) -> UserInDB | None: + stmt = select(UserModel).where(UserModel.email == email) + result = await session.execute(stmt) + user = result.scalars().first() if not user: - return + return None - return UserInDB(**user) + return UserInDB( + email=str(user.email), + hashed_password=str(user.hashed_password), + full_name=str(user.full_name), + disabled=bool(user.disabled), + ) -async def authenticate_user(email: str, password: str): - user = await get_user(email) +async def authenticate_user(email: str, password: str, db: AsyncSession): + user = await get_user(db, email) if not user: return False @@ -74,7 +83,9 @@ def create_access_token( return encoded_jwt -async def get_current_user(token: str = Depends(oauth2_scheme)): +async def get_current_user( + db: AsyncSession = Depends(get_db), token: str = Depends(oauth2_scheme) +): LOG.debug(f"Getting current user with token: {token}") if not token: @@ -82,7 +93,7 @@ async def get_current_user(token: str = Depends(oauth2_scheme)): return EMPTY_USER if len(token.split(".")) != 3: - LOG.error(f"Malformed token: {token}") + LOG.debug(f"Malformed token: {token}") return EMPTY_USER try: @@ -117,7 +128,7 @@ async def get_current_user(token: str = Depends(oauth2_scheme)): LOG.error(f"Exception occurred: {e}") return EMPTY_USER - user = await get_user(email=token_data.email or "") + user = await get_user(db, email=token_data.email or "") if user is None: return EMPTY_USER @@ -125,7 +136,7 @@ async def get_current_user(token: str = Depends(oauth2_scheme)): return user -async def require_user(token: str = Depends(oauth2_scheme)): +async def require_user(db: AsyncSession, token: str = Depends(oauth2_scheme)): credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", @@ -150,7 +161,7 @@ async def require_user(token: str = Depends(oauth2_scheme)): except JWTError: raise credentials_exception - user = await get_user(email=token_data.email or "") + user = await get_user(db, email=token_data.email or "") if user is None: raise credentials_exception diff --git a/api/backend/constants.py b/api/backend/constants.py index e7543a9..a85493e 100644 --- a/api/backend/constants.py +++ b/api/backend/constants.py @@ -2,7 +2,7 @@ import os from pathlib import Path -DATABASE_PATH = "data/database.db" +DATABASE_URL = os.getenv("DATABASE_URL", "sqlite+aiosqlite:///data/database.db") RECORDINGS_DIR = Path("media/recordings") RECORDINGS_ENABLED = os.getenv("RECORDINGS_ENABLED", "true").lower() == "true" MEDIA_DIR = Path("media") diff --git a/api/backend/database/__init__.py b/api/backend/database/__init__.py index 1b881cf..e69de29 100644 --- a/api/backend/database/__init__.py +++ b/api/backend/database/__init__.py @@ -1,5 +0,0 @@ -# LOCAL -from .common import insert, update, connect -from .schema import INIT_QUERY - -__all__ = ["insert", "update", "INIT_QUERY", "connect"] diff --git a/api/backend/database/base.py b/api/backend/database/base.py new file mode 100644 index 0000000..ecdeb81 --- /dev/null +++ b/api/backend/database/base.py @@ -0,0 +1,26 @@ +# STL +from typing import AsyncGenerator + +# PDM +from sqlalchemy.orm import declarative_base +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine + +# LOCAL +from api.backend.constants import DATABASE_URL + +engine = create_async_engine(DATABASE_URL, echo=False, future=True) + +AsyncSessionLocal = async_sessionmaker( + bind=engine, + autoflush=False, + autocommit=False, + expire_on_commit=False, + class_=AsyncSession, +) + +Base = declarative_base() + + +async def get_db() -> AsyncGenerator[AsyncSession, None]: + async with AsyncSessionLocal() as session: + yield session diff --git a/api/backend/database/common.py b/api/backend/database/common.py deleted file mode 100644 index 4f3c740..0000000 --- a/api/backend/database/common.py +++ /dev/null @@ -1,90 +0,0 @@ -# STL -import logging -import sqlite3 -from typing import Any, Optional - -# LOCAL -from api.backend.constants import DATABASE_PATH -from api.backend.database.utils import format_json, format_sql_row_to_python - -LOG = logging.getLogger("Database") - - -def connect(): - connection = sqlite3.connect(DATABASE_PATH) - connection.set_trace_callback(print) - cursor = connection.cursor() - return cursor - - -def insert(query: str, values: tuple[Any, ...]): - connection = sqlite3.connect(DATABASE_PATH) - cursor = connection.cursor() - copy = list(values) - format_json(copy) - - try: - _ = cursor.execute(query, copy) - connection.commit() - - except sqlite3.Error as e: - LOG.error(f"An error occurred: {e}") - raise e - - finally: - cursor.close() - connection.close() - - -def query(query: str, values: Optional[tuple[Any, ...]] = None): - connection = sqlite3.connect(DATABASE_PATH) - connection.row_factory = sqlite3.Row - cursor = connection.cursor() - rows = [] - try: - if values: - _ = cursor.execute(query, values) - else: - _ = cursor.execute(query) - - rows = cursor.fetchall() - - finally: - cursor.close() - connection.close() - - formatted_rows: list[dict[str, Any]] = [] - - for row in rows: - row = dict(row) - formatted_row = format_sql_row_to_python(row) - formatted_rows.append(formatted_row) - - return formatted_rows - - -def update(query: str, values: Optional[tuple[Any, ...]] = None): - connection = sqlite3.connect(DATABASE_PATH) - cursor = connection.cursor() - - copy = None - - if values: - copy = list(values) - format_json(copy) - - try: - if copy: - res = cursor.execute(query, copy) - else: - res = cursor.execute(query) - connection.commit() - return res.rowcount - except sqlite3.Error as e: - LOG.error(f"An error occurred: {e}") - - finally: - cursor.close() - connection.close() - - return 0 diff --git a/api/backend/database/models.py b/api/backend/database/models.py new file mode 100644 index 0000000..3645fc6 --- /dev/null +++ b/api/backend/database/models.py @@ -0,0 +1,65 @@ +# PDM +from sqlalchemy import JSON, Column, String, Boolean, DateTime, ForeignKey, func +from sqlalchemy.orm import relationship + +# LOCAL +from api.backend.database.base import Base + + +class User(Base): + __tablename__ = "users" + + email = Column(String(255), primary_key=True, nullable=False) + hashed_password = Column(String(255), nullable=False) + full_name = Column(String(255), nullable=True) + disabled = Column(Boolean, default=False) + + jobs = relationship("Job", back_populates="user_obj", cascade="all, delete-orphan") + cron_jobs = relationship( + "CronJob", back_populates="user_obj", cascade="all, delete-orphan" + ) + + +class Job(Base): + __tablename__ = "jobs" + + id = Column(String(64), primary_key=True, nullable=False) + url = Column(String(2048), nullable=False) + elements = Column(JSON, nullable=False) + user = Column(String(255), ForeignKey("users.email"), nullable=True) + time_created = Column( + DateTime(timezone=True), server_default=func.now(), nullable=False + ) + result = Column(JSON, nullable=False) + status = Column(String(50), nullable=False) + chat = Column(JSON, nullable=True) + job_options = Column(JSON, nullable=True) + agent_mode = Column(Boolean, default=False, nullable=False) + prompt = Column(String(1024), nullable=True) + favorite = Column(Boolean, default=False, nullable=False) + + user_obj = relationship("User", back_populates="jobs") + cron_jobs = relationship( + "CronJob", back_populates="job_obj", cascade="all, delete-orphan" + ) + + +class CronJob(Base): + __tablename__ = "cron_jobs" + + id = Column(String(64), primary_key=True, nullable=False) + user_email = Column(String(255), ForeignKey("users.email"), nullable=False) + job_id = Column(String(64), ForeignKey("jobs.id"), nullable=False) + cron_expression = Column(String(255), nullable=False) + time_created = Column( + DateTime(timezone=True), server_default=func.now(), nullable=False + ) + time_updated = Column( + DateTime(timezone=True), + server_default=func.now(), + onupdate=func.now(), + nullable=False, + ) + + user_obj = relationship("User", back_populates="cron_jobs") + job_obj = relationship("Job", back_populates="cron_jobs") diff --git a/api/backend/database/queries/__init__.py b/api/backend/database/queries/__init__.py index e11d606..e69de29 100644 --- a/api/backend/database/queries/__init__.py +++ b/api/backend/database/queries/__init__.py @@ -1,4 +0,0 @@ -# LOCAL -from .job.job_queries import DELETE_JOB_QUERY, JOB_INSERT_QUERY - -__all__ = ["JOB_INSERT_QUERY", "DELETE_JOB_QUERY"] diff --git a/api/backend/database/queries/job/job_queries.py b/api/backend/database/queries/job/job_queries.py index b825d6d..d100c48 100644 --- a/api/backend/database/queries/job/job_queries.py +++ b/api/backend/database/queries/job/job_queries.py @@ -2,62 +2,61 @@ import logging from typing import Any +# PDM +from sqlalchemy import delete as sql_delete +from sqlalchemy import select +from sqlalchemy import update as sql_update + # LOCAL -from api.backend.database.utils import format_list_for_query -from api.backend.database.common import query, insert, update - -JOB_INSERT_QUERY = """ -INSERT INTO jobs -(id, url, elements, user, time_created, result, status, chat, job_options, agent_mode, prompt) -VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) -""" - -DELETE_JOB_QUERY = """ -DELETE FROM jobs WHERE id IN () -""" +from api.backend.database.base import AsyncSessionLocal +from api.backend.database.models import Job LOG = logging.getLogger("Database") -def insert_job(item: dict[str, Any]) -> None: - insert( - JOB_INSERT_QUERY, - ( - item["id"], - item["url"], - item["elements"], - item["user"], - item["time_created"], - item["result"], - item["status"], - item["chat"], - item["job_options"], - item["agent_mode"], - item["prompt"], - ), - ) - LOG.info(f"Inserted item: {item}") +async def insert_job(item: dict[str, Any]) -> None: + async with AsyncSessionLocal() as session: + job = Job( + id=item["id"], + url=item["url"], + elements=item["elements"], + user=item["user"], + time_created=item["time_created"], + result=item["result"], + status=item["status"], + chat=item["chat"], + job_options=item["job_options"], + agent_mode=item["agent_mode"], + prompt=item["prompt"], + ) + session.add(job) + await session.commit() + LOG.info(f"Inserted item: {item}") async def get_queued_job(): - queued_job_query = ( - "SELECT * FROM jobs WHERE status = 'Queued' ORDER BY time_created DESC LIMIT 1" - ) - - res = query(queued_job_query) - LOG.info(f"Got queued job: {res}") - return res[0] if res else None + async with AsyncSessionLocal() as session: + stmt = ( + select(Job) + .where(Job.status == "Queued") + .order_by(Job.time_created.desc()) + .limit(1) + ) + result = await session.execute(stmt) + job = result.scalars().first() + LOG.info(f"Got queued job: {job}") + return job async def update_job(ids: list[str], updates: dict[str, Any]): if not updates: return - set_clause = ", ".join(f"{field} = ?" for field in updates.keys()) - query = f"UPDATE jobs SET {set_clause} WHERE id IN {format_list_for_query(ids)}" - values = list(updates.values()) + ids - res = update(query, tuple(values)) - LOG.debug(f"Updated job: {res}") + async with AsyncSessionLocal() as session: + stmt = sql_update(Job).where(Job.id.in_(ids)).values(**updates) + result = await session.execute(stmt) + await session.commit() + LOG.debug(f"Updated job count: {result.rowcount}") async def delete_jobs(jobs: list[str]): @@ -65,9 +64,9 @@ async def delete_jobs(jobs: list[str]): LOG.info("No jobs to delete.") return False - query = f"DELETE FROM jobs WHERE id IN {format_list_for_query(jobs)}" - res = update(query, tuple(jobs)) - - LOG.info(f"Deleted jobs: {res}") - - return res + async with AsyncSessionLocal() as session: + stmt = sql_delete(Job).where(Job.id.in_(jobs)) + result = await session.execute(stmt) + await session.commit() + LOG.info(f"Deleted jobs count: {result.rowcount}") + return result.rowcount diff --git a/api/backend/database/queries/statistics/statistic_queries.py b/api/backend/database/queries/statistics/statistic_queries.py index ea4573d..d4f28ce 100644 --- a/api/backend/database/queries/statistics/statistic_queries.py +++ b/api/backend/database/queries/statistics/statistic_queries.py @@ -1,41 +1,43 @@ +# PDM +from sqlalchemy import func, select +from sqlalchemy.ext.asyncio import AsyncSession + # LOCAL -from api.backend.database.common import query +from api.backend.database.models import Job -async def average_elements_per_link(user: str): - job_query = """ - SELECT - DATE(time_created) AS date, - AVG(json_array_length(elements)) AS average_elements, - COUNT(*) AS count - FROM - jobs - WHERE - status = 'Completed' AND user = ? - GROUP BY - DATE(time_created) - ORDER BY - date ASC; - """ - results = query(job_query, (user,)) +async def average_elements_per_link(session: AsyncSession, user_email: str): + date_func = func.date(Job.time_created) - return results + stmt = ( + select( + date_func.label("date"), + func.avg(func.json_array_length(Job.elements)).label("average_elements"), + func.count().label("count"), + ) + .where(Job.status == "Completed", Job.user == user_email) + .group_by(date_func) + .order_by("date") + ) + + result = await session.execute(stmt) + rows = result.all() + return [dict(row._mapping) for row in rows] -async def get_jobs_per_day(user: str): - job_query = """ - SELECT - DATE(time_created) AS date, - COUNT(*) AS job_count - FROM - jobs - WHERE - status = 'Completed' AND user = ? - GROUP BY - DATE(time_created) - ORDER BY - date ASC; - """ - results = query(job_query, (user,)) +async def get_jobs_per_day(session: AsyncSession, user_email: str): + date_func = func.date(Job.time_created) - return results + stmt = ( + select( + date_func.label("date"), + func.count().label("job_count"), + ) + .where(Job.status == "Completed", Job.user == user_email) + .group_by(date_func) + .order_by("date") + ) + + result = await session.execute(stmt) + rows = result.all() + return [dict(row._mapping) for row in rows] diff --git a/api/backend/database/schema/__init__.py b/api/backend/database/schema/__init__.py deleted file mode 100644 index e0c282a..0000000 --- a/api/backend/database/schema/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .schema import INIT_QUERY - -__all__ = ["INIT_QUERY"] diff --git a/api/backend/database/schema/schema.py b/api/backend/database/schema/schema.py deleted file mode 100644 index f4bb2b3..0000000 --- a/api/backend/database/schema/schema.py +++ /dev/null @@ -1,34 +0,0 @@ -INIT_QUERY = """ -CREATE TABLE IF NOT EXISTS jobs ( - id STRING PRIMARY KEY NOT NULL, - url STRING NOT NULL, - elements JSON NOT NULL, - user STRING, - time_created DATETIME NOT NULL, - result JSON NOT NULL, - status STRING NOT NULL, - chat JSON, - job_options JSON -); - -CREATE TABLE IF NOT EXISTS users ( - email STRING PRIMARY KEY NOT NULL, - hashed_password STRING NOT NULL, - full_name STRING, - disabled BOOLEAN -); - -CREATE TABLE IF NOT EXISTS cron_jobs ( - id STRING PRIMARY KEY NOT NULL, - user_email STRING NOT NULL, - job_id STRING NOT NULL, - cron_expression STRING NOT NULL, - time_created DATETIME NOT NULL, - time_updated DATETIME NOT NULL, - FOREIGN KEY (job_id) REFERENCES jobs(id) -); - -ALTER TABLE jobs ADD COLUMN agent_mode BOOLEAN NOT NULL DEFAULT FALSE; -ALTER TABLE jobs ADD COLUMN prompt STRING; -ALTER TABLE jobs ADD COLUMN favorite BOOLEAN NOT NULL DEFAULT FALSE; -""" diff --git a/api/backend/database/startup.py b/api/backend/database/startup.py index d4e5b42..1d55c99 100644 --- a/api/backend/database/startup.py +++ b/api/backend/database/startup.py @@ -1,6 +1,8 @@ # STL import logging -import sqlite3 + +# PDM +from sqlalchemy.exc import IntegrityError # LOCAL from api.backend.constants import ( @@ -9,61 +11,46 @@ from api.backend.constants import ( DEFAULT_USER_PASSWORD, DEFAULT_USER_FULL_NAME, ) +from api.backend.database.base import Base, AsyncSessionLocal, engine from api.backend.auth.auth_utils import get_password_hash -from api.backend.database.common import insert, connect -from api.backend.database.schema import INIT_QUERY +from api.backend.database.models import User LOG = logging.getLogger("Database") +async def init_database(): + LOG.info("Creating database schema...") -def execute_startup_query(): - cursor = connect() - - for query in INIT_QUERY.strip().split(";"): - query = query.strip() - - if not query: - continue - - try: - LOG.info(f"Executing query: {query}") - _ = cursor.execute(query) - - except sqlite3.OperationalError as e: - if "duplicate column name" in str(e).lower(): - LOG.warning(f"Skipping duplicate column error: {e}") - continue - else: - LOG.error(f"Error executing query: {query}") - raise - - cursor.close() - - -def init_database(): - execute_startup_query() + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) if not REGISTRATION_ENABLED: default_user_email = DEFAULT_USER_EMAIL default_user_password = DEFAULT_USER_PASSWORD default_user_full_name = DEFAULT_USER_FULL_NAME - if ( - not default_user_email - or not default_user_password - or not default_user_full_name - ): - LOG.error( - "DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD, or DEFAULT_USER_FULL_NAME is not set!" - ) + if not (default_user_email and default_user_password and default_user_full_name): + LOG.error("DEFAULT_USER_* env vars are not set!") exit(1) - query = "INSERT INTO users (email, hashed_password, full_name) VALUES (?, ?, ?)" - _ = insert( - query, - ( - default_user_email, - get_password_hash(default_user_password), - default_user_full_name, - ), - ) + async with AsyncSessionLocal() as session: + user = await session.get(User, default_user_email) + if user: + LOG.info("Default user already exists. Skipping creation.") + return + + LOG.info("Creating default user...") + new_user = User( + email=default_user_email, + hashed_password=get_password_hash(default_user_password), + full_name=default_user_full_name, + disabled=False, + ) + + try: + session.add(new_user) + await session.commit() + LOG.info(f"Created default user: {default_user_email}") + except IntegrityError as e: + await session.rollback() + LOG.warning(f"Could not create default user (already exists?): {e}") + diff --git a/api/backend/database/utils.py b/api/backend/database/utils.py index f6c4e57..e3d2859 100644 --- a/api/backend/database/utils.py +++ b/api/backend/database/utils.py @@ -1,6 +1,7 @@ # STL import json from typing import Any +from datetime import datetime def format_list_for_query(ids: list[str]): @@ -28,3 +29,9 @@ def format_json(items: list[Any]): if isinstance(item, (dict, list)): formatted_item = json.dumps(item) items[idx] = formatted_item + + +def parse_datetime(dt_str: str) -> datetime: + if dt_str.endswith("Z"): + dt_str = dt_str.replace("Z", "+00:00") # valid ISO format for UTC + return datetime.fromisoformat(dt_str) diff --git a/api/backend/job/cron_scheduling/cron_scheduling.py b/api/backend/job/cron_scheduling/cron_scheduling.py index 6994d19..9c2d55e 100644 --- a/api/backend/job/cron_scheduling/cron_scheduling.py +++ b/api/backend/job/cron_scheduling/cron_scheduling.py @@ -2,83 +2,74 @@ import uuid import logging import datetime -from typing import Any +from typing import Any, List # PDM +from sqlalchemy import select from apscheduler.triggers.cron import CronTrigger -from apscheduler.schedulers.background import BackgroundScheduler +from apscheduler.schedulers.asyncio import AsyncIOScheduler # LOCAL from api.backend.job import insert as insert_job -from api.backend.schemas.cron import CronJob -from api.backend.database.common import query, insert +from api.backend.database.base import AsyncSessionLocal +from api.backend.database.models import Job, CronJob LOG = logging.getLogger("Cron") -def insert_cron_job(cron_job: CronJob): - query = """ - INSERT INTO cron_jobs (id, user_email, job_id, cron_expression, time_created, time_updated) - VALUES (?, ?, ?, ?, ?, ?) - """ - - values = ( - cron_job.id, - cron_job.user_email, - cron_job.job_id, - cron_job.cron_expression, - cron_job.time_created, - cron_job.time_updated, - ) - - insert(query, values) - +async def insert_cron_job(cron_job: CronJob) -> bool: + async with AsyncSessionLocal() as session: + session.add(cron_job) + await session.commit() return True -def delete_cron_job(id: str, user_email: str): - query = """ - DELETE FROM cron_jobs - WHERE id = ? AND user_email = ? - """ - - values = (id, user_email) - insert(query, values) - +async def delete_cron_job(id: str, user_email: str) -> bool: + async with AsyncSessionLocal() as session: + stmt = select(CronJob).where(CronJob.id == id, CronJob.user_email == user_email) + result = await session.execute(stmt) + cron_job = result.scalars().first() + if cron_job: + await session.delete(cron_job) + await session.commit() return True -def get_cron_jobs(user_email: str): - cron_jobs = query("SELECT * FROM cron_jobs WHERE user_email = ?", (user_email,)) - - return cron_jobs +async def get_cron_jobs(user_email: str) -> List[CronJob]: + async with AsyncSessionLocal() as session: + stmt = select(CronJob).where(CronJob.user_email == user_email) + result = await session.execute(stmt) + return list(result.scalars().all()) -def get_all_cron_jobs(): - cron_jobs = query("SELECT * FROM cron_jobs") - - return cron_jobs +async def get_all_cron_jobs() -> List[CronJob]: + async with AsyncSessionLocal() as session: + stmt = select(CronJob) + result = await session.execute(stmt) + return list(result.scalars().all()) -def insert_job_from_cron_job(job: dict[str, Any]): - insert_job( - { - **job, - "id": uuid.uuid4().hex, - "status": "Queued", - "result": "", - "chat": None, - "time_created": datetime.datetime.now(), - "time_updated": datetime.datetime.now(), - } - ) +async def insert_job_from_cron_job(job: dict[str, Any]): + async with AsyncSessionLocal() as session: + await insert_job( + { + **job, + "id": uuid.uuid4().hex, + "status": "Queued", + "result": "", + "chat": None, + "time_created": datetime.datetime.now(datetime.timezone.utc), + "time_updated": datetime.datetime.now(datetime.timezone.utc), + }, + session, + ) def get_cron_job_trigger(cron_expression: str): expression_parts = cron_expression.split() if len(expression_parts) != 5: - print(f"Invalid cron expression: {cron_expression}") + LOG.warning(f"Invalid cron expression: {cron_expression}") return None minute, hour, day, month, day_of_week = expression_parts @@ -88,19 +79,37 @@ def get_cron_job_trigger(cron_expression: str): ) -def start_cron_scheduler(scheduler: BackgroundScheduler): - cron_jobs = get_all_cron_jobs() +async def start_cron_scheduler(scheduler: AsyncIOScheduler): + async with AsyncSessionLocal() as session: + stmt = select(CronJob) + result = await session.execute(stmt) + cron_jobs = result.scalars().all() - LOG.info(f"Cron jobs: {cron_jobs}") + LOG.info(f"Cron jobs: {cron_jobs}") - for job in cron_jobs: - queried_job = query("SELECT * FROM jobs WHERE id = ?", (job["job_id"],)) + for cron_job in cron_jobs: + stmt = select(Job).where(Job.id == cron_job.job_id) + result = await session.execute(stmt) + queried_job = result.scalars().first() - LOG.info(f"Adding job: {queried_job}") + LOG.info(f"Adding job: {queried_job}") - scheduler.add_job( - insert_job_from_cron_job, - get_cron_job_trigger(job["cron_expression"]), - id=job["id"], - args=[queried_job[0]], - ) + trigger = get_cron_job_trigger(cron_job.cron_expression) # type: ignore + if not trigger: + continue + + job_dict = ( + { + c.key: getattr(queried_job, c.key) + for c in queried_job.__table__.columns + } + if queried_job + else {} + ) + + scheduler.add_job( + insert_job_from_cron_job, + trigger, + id=cron_job.id, + args=[job_dict], + ) diff --git a/api/backend/job/job.py b/api/backend/job/job.py index d286765..aa8f312 100644 --- a/api/backend/job/job.py +++ b/api/backend/job/job.py @@ -3,18 +3,22 @@ import logging import datetime from typing import Any +# PDM +from sqlalchemy import delete as sql_delete +from sqlalchemy import select +from sqlalchemy import update as sql_update +from sqlalchemy.ext.asyncio import AsyncSession + # LOCAL -from api.backend.database.utils import format_list_for_query -from api.backend.database.common import query as common_query -from api.backend.database.common import insert as common_insert -from api.backend.database.common import update as common_update -from api.backend.database.queries.job.job_queries import JOB_INSERT_QUERY +from api.backend.database.base import AsyncSessionLocal +from api.backend.database.models import Job LOG = logging.getLogger("Job") -async def insert(item: dict[str, Any]) -> None: - if check_for_job_completion(item["id"]): +async def insert(item: dict[str, Any], db: AsyncSession) -> None: + existing = await db.get(Job, item["id"]) + if existing: await multi_field_update_job( item["id"], { @@ -24,57 +28,76 @@ async def insert(item: dict[str, Any]) -> None: "elements": item["elements"], "status": "Queued", "result": [], - "time_created": datetime.datetime.now().isoformat(), + "time_created": datetime.datetime.now(datetime.timezone.utc), "chat": None, }, + db, ) return - common_insert( - JOB_INSERT_QUERY, - ( - item["id"], - item["url"], - item["elements"], - item["user"], - item["time_created"], - item["result"], - item["status"], - item["chat"], - item["job_options"], - item["agent_mode"], - item["prompt"], - ), + job = Job( + id=item["id"], + url=item["url"], + elements=item["elements"], + user=item["user"], + time_created=datetime.datetime.now(datetime.timezone.utc), + result=item["result"], + status=item["status"], + chat=item["chat"], + job_options=item["job_options"], + agent_mode=item["agent_mode"], + prompt=item["prompt"], ) + db.add(job) + await db.commit() LOG.debug(f"Inserted item: {item}") -def check_for_job_completion(id: str) -> dict[str, Any]: - query = f"SELECT * FROM jobs WHERE id = ?" - res = common_query(query, (id,)) - return res[0] if res else {} +async def check_for_job_completion(id: str) -> dict[str, Any]: + async with AsyncSessionLocal() as session: + job = await session.get(Job, id) + return job.__dict__ if job else {} async def get_queued_job(): - query = ( - "SELECT * FROM jobs WHERE status = 'Queued' ORDER BY time_created DESC LIMIT 1" - ) - res = common_query(query) - LOG.debug(f"Got queued job: {res}") - return res[0] if res else None + async with AsyncSessionLocal() as session: + stmt = ( + select(Job) + .where(Job.status == "Queued") + .order_by(Job.time_created.desc()) + .limit(1) + ) + result = await session.execute(stmt) + job = result.scalars().first() + LOG.debug(f"Got queued job: {job}") + return job.__dict__ if job else None async def update_job(ids: list[str], field: str, value: Any): - query = f"UPDATE jobs SET {field} = ? WHERE id IN {format_list_for_query(ids)}" - res = common_update(query, tuple([value] + ids)) - LOG.debug(f"Updated job: {res}") + async with AsyncSessionLocal() as session: + stmt = sql_update(Job).where(Job.id.in_(ids)).values({field: value}) + res = await session.execute(stmt) + await session.commit() + LOG.debug(f"Updated job count: {res.rowcount}") -async def multi_field_update_job(id: str, fields: dict[str, Any]): - query = f"UPDATE jobs SET {', '.join(f'{field} = ?' for field in fields.keys())} WHERE id = ?" - res = common_update(query, tuple(list(fields.values()) + [id])) - LOG.debug(f"Updated job: {res}") +async def multi_field_update_job( + id: str, fields: dict[str, Any], session: AsyncSession | None = None +): + close_session = False + if not session: + session = AsyncSessionLocal() + close_session = True + + try: + stmt = sql_update(Job).where(Job.id == id).values(**fields) + await session.execute(stmt) + await session.commit() + LOG.debug(f"Updated job {id} fields: {fields}") + finally: + if close_session: + await session.close() async def delete_jobs(jobs: list[str]): @@ -82,7 +105,9 @@ async def delete_jobs(jobs: list[str]): LOG.debug("No jobs to delete.") return False - query = f"DELETE FROM jobs WHERE id IN {format_list_for_query(jobs)}" - res = common_update(query, tuple(jobs)) - - return res > 0 + async with AsyncSessionLocal() as session: + stmt = sql_delete(Job).where(Job.id.in_(jobs)) + res = await session.execute(stmt) + await session.commit() + LOG.debug(f"Deleted jobs: {res.rowcount}") + return res.rowcount > 0 diff --git a/api/backend/job/job_router.py b/api/backend/job/job_router.py index 294f456..5ccb557 100644 --- a/api/backend/job/job_router.py +++ b/api/backend/job/job_router.py @@ -8,8 +8,10 @@ from io import StringIO # PDM from fastapi import Depends, APIRouter +from sqlalchemy import select from fastapi.encoders import jsonable_encoder from fastapi.responses import FileResponse, JSONResponse, StreamingResponse +from sqlalchemy.ext.asyncio import AsyncSession from apscheduler.triggers.cron import CronTrigger # type: ignore # LOCAL @@ -18,10 +20,12 @@ from api.backend.constants import MEDIA_DIR, MEDIA_TYPES, RECORDINGS_DIR from api.backend.scheduler import scheduler from api.backend.schemas.job import Job, UpdateJobs, DownloadJob, DeleteScrapeJobs from api.backend.auth.schemas import User -from api.backend.schemas.cron import CronJob, DeleteCronJob -from api.backend.database.utils import format_list_for_query +from api.backend.schemas.cron import CronJob as PydanticCronJob +from api.backend.schemas.cron import DeleteCronJob +from api.backend.database.base import get_db from api.backend.auth.auth_utils import get_current_user -from api.backend.database.common import query +from api.backend.database.models import Job as DatabaseJob +from api.backend.database.models import CronJob from api.backend.job.utils.text_utils import clean_text from api.backend.job.models.job_options import FetchOptions from api.backend.routers.handle_exceptions import handle_exceptions @@ -49,14 +53,14 @@ async def update(update_jobs: UpdateJobs, _: User = Depends(get_current_user)): @job_router.post("/submit-scrape-job") @handle_exceptions(logger=LOG) -async def submit_scrape_job(job: Job): +async def submit_scrape_job(job: Job, db: AsyncSession = Depends(get_db)): LOG.info(f"Recieved job: {job}") if not job.id: job.id = uuid.uuid4().hex job_dict = job.model_dump() - await insert(job_dict) + await insert(job_dict, db) return JSONResponse( content={"id": job.id, "message": "Job submitted successfully."} @@ -66,34 +70,49 @@ async def submit_scrape_job(job: Job): @job_router.post("/retrieve-scrape-jobs") @handle_exceptions(logger=LOG) async def retrieve_scrape_jobs( - fetch_options: FetchOptions, user: User = Depends(get_current_user) + fetch_options: FetchOptions, + user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), ): - LOG.info(f"Retrieving jobs for account: {user.email}") - ATTRIBUTES = "chat" if fetch_options.chat else "*" - job_query = ( - f"SELECT {ATTRIBUTES} FROM jobs WHERE user = ? ORDER BY time_created ASC" + LOG.info( + f"Retrieving jobs for account: {user.email if user.email else 'Guest User'}" ) - results = query(job_query, (user.email,)) - return JSONResponse(content=jsonable_encoder(results[::-1])) + if fetch_options.chat: + stmt = select(DatabaseJob.chat).filter(DatabaseJob.user == user.email) + else: + stmt = select(DatabaseJob).filter(DatabaseJob.user == user.email) + + results = await db.execute(stmt) + rows = results.all() if fetch_options.chat else results.scalars().all() + + return JSONResponse(content=jsonable_encoder(rows[::-1])) @job_router.get("/job/{id}") @handle_exceptions(logger=LOG) -async def job(id: str, user: User = Depends(get_current_user)): +async def job( + id: str, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) +): LOG.info(f"Retrieving jobs for account: {user.email}") - job_query = "SELECT * FROM jobs WHERE user = ? AND id = ?" - results = query(job_query, (user.email, id)) - return JSONResponse(content=jsonable_encoder(results)) + + stmt = select(DatabaseJob).filter( + DatabaseJob.user == user.email, DatabaseJob.id == id + ) + + results = await db.execute(stmt) + + return JSONResponse( + content=jsonable_encoder([job.__dict__ for job in results.scalars().all()]) + ) @job_router.post("/download") @handle_exceptions(logger=LOG) -async def download(download_job: DownloadJob): +async def download(download_job: DownloadJob, db: AsyncSession = Depends(get_db)): LOG.info(f"Downloading job with ids: {download_job.ids}") - job_query = ( - f"SELECT * FROM jobs WHERE id IN {format_list_for_query(download_job.ids)}" - ) - results = query(job_query, tuple(download_job.ids)) + stmt = select(DatabaseJob).where(DatabaseJob.id.in_(download_job.ids)) + result = await db.execute(stmt) + results = [job.__dict__ for job in result.scalars().all()] if download_job.job_format == "csv": csv_buffer = StringIO() @@ -151,10 +170,12 @@ async def download(download_job: DownloadJob): @job_router.get("/job/{id}/convert-to-csv") @handle_exceptions(logger=LOG) -async def convert_to_csv(id: str): - job_query = f"SELECT * FROM jobs WHERE id = ?" - results = query(job_query, (id,)) - return JSONResponse(content=clean_job_format(results)) +async def convert_to_csv(id: str, db: AsyncSession = Depends(get_db)): + stmt = select(DatabaseJob).filter(DatabaseJob.id == id) + results = await db.execute(stmt) + jobs = results.scalars().all() + + return JSONResponse(content=clean_job_format([job.__dict__ for job in jobs])) @job_router.post("/delete-scrape-jobs") @@ -170,25 +191,34 @@ async def delete(delete_scrape_jobs: DeleteScrapeJobs): @job_router.post("/schedule-cron-job") @handle_exceptions(logger=LOG) -async def schedule_cron_job(cron_job: CronJob): +async def schedule_cron_job( + cron_job: PydanticCronJob, + db: AsyncSession = Depends(get_db), +): if not cron_job.id: cron_job.id = uuid.uuid4().hex + now = datetime.datetime.now() if not cron_job.time_created: - cron_job.time_created = datetime.datetime.now() + cron_job.time_created = now if not cron_job.time_updated: - cron_job.time_updated = datetime.datetime.now() + cron_job.time_updated = now - insert_cron_job(cron_job) + await insert_cron_job(CronJob(**cron_job.model_dump())) - queried_job = query("SELECT * FROM jobs WHERE id = ?", (cron_job.job_id,)) + stmt = select(DatabaseJob).where(DatabaseJob.id == cron_job.job_id) + result = await db.execute(stmt) + queried_job = result.scalars().first() + + if not queried_job: + return JSONResponse(status_code=404, content={"error": "Related job not found"}) scheduler.add_job( insert_job_from_cron_job, get_cron_job_trigger(cron_job.cron_expression), id=cron_job.id, - args=[queried_job[0]], + args=[queried_job], ) return JSONResponse(content={"message": "Cron job scheduled successfully."}) @@ -202,7 +232,7 @@ async def delete_cron_job_request(request: DeleteCronJob): content={"error": "Cron job id is required."}, status_code=400 ) - delete_cron_job(request.id, request.user_email) + await delete_cron_job(request.id, request.user_email) scheduler.remove_job(request.id) return JSONResponse(content={"message": "Cron job deleted successfully."}) @@ -211,7 +241,7 @@ async def delete_cron_job_request(request: DeleteCronJob): @job_router.get("/cron-jobs") @handle_exceptions(logger=LOG) async def get_cron_jobs_request(user: User = Depends(get_current_user)): - cron_jobs = get_cron_jobs(user.email) + cron_jobs = await get_cron_jobs(user.email) return JSONResponse(content=jsonable_encoder(cron_jobs)) diff --git a/api/backend/job/utils/clean_job_format.py b/api/backend/job/utils/clean_job_format.py index 2f286e2..6e7a4e8 100644 --- a/api/backend/job/utils/clean_job_format.py +++ b/api/backend/job/utils/clean_job_format.py @@ -28,7 +28,9 @@ def clean_job_format(jobs: list[dict[str, Any]]) -> dict[str, Any]: "xpath": value.get("xpath", ""), "text": text, "user": job.get("user", ""), - "time_created": job.get("time_created", ""), + "time_created": job.get( + "time_created", "" + ).isoformat(), } ) diff --git a/api/backend/scheduler.py b/api/backend/scheduler.py index 63b753c..263e9bd 100644 --- a/api/backend/scheduler.py +++ b/api/backend/scheduler.py @@ -1,4 +1,4 @@ # PDM -from apscheduler.schedulers.background import BackgroundScheduler +from apscheduler.schedulers.asyncio import AsyncIOScheduler -scheduler = BackgroundScheduler() +scheduler = AsyncIOScheduler() \ No newline at end of file diff --git a/api/backend/stats/stats_router.py b/api/backend/stats/stats_router.py index 2b55dc0..f98cadf 100644 --- a/api/backend/stats/stats_router.py +++ b/api/backend/stats/stats_router.py @@ -3,9 +3,11 @@ import logging # PDM from fastapi import Depends, APIRouter +from sqlalchemy.ext.asyncio import AsyncSession # LOCAL from api.backend.auth.schemas import User +from api.backend.database.base import get_db from api.backend.auth.auth_utils import get_current_user from api.backend.routers.handle_exceptions import handle_exceptions from api.backend.database.queries.statistics.statistic_queries import ( @@ -20,12 +22,16 @@ stats_router = APIRouter() @stats_router.get("/statistics/get-average-element-per-link") @handle_exceptions(logger=LOG) -async def get_average_element_per_link(user: User = Depends(get_current_user)): - return await average_elements_per_link(user.email) +async def get_average_element_per_link( + user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) +): + return await average_elements_per_link(db, user.email) @stats_router.get("/statistics/get-average-jobs-per-day") @handle_exceptions(logger=LOG) -async def average_jobs_per_day(user: User = Depends(get_current_user)): - data = await get_jobs_per_day(user.email) +async def average_jobs_per_day( + user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) +): + data = await get_jobs_per_day(db, user.email) return data diff --git a/api/backend/tests/conftest.py b/api/backend/tests/conftest.py index e0082e9..4c0e20c 100644 --- a/api/backend/tests/conftest.py +++ b/api/backend/tests/conftest.py @@ -1,15 +1,21 @@ # STL import os -import sqlite3 -from typing import Generator -from unittest.mock import patch +import asyncio +from typing import Any, Generator, AsyncGenerator # PDM import pytest +import pytest_asyncio +from httpx import AsyncClient, ASGITransport from proxy import Proxy +from sqlalchemy import text +from sqlalchemy.pool import NullPool +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine # LOCAL -from api.backend.database.schema import INIT_QUERY +from api.backend.app import app +from api.backend.database.base import get_db +from api.backend.database.models import Base from api.backend.tests.constants import TEST_DB_PATH @@ -21,18 +27,6 @@ def running_proxy(): proxy.shutdown() -@pytest.fixture(scope="session", autouse=True) -def patch_database_path(): - with patch("api.backend.database.common.DATABASE_PATH", TEST_DB_PATH): - yield - - -@pytest.fixture(scope="session", autouse=True) -def patch_recordings_enabled(): - with patch("api.backend.job.scraping.scraping.RECORDINGS_ENABLED", False): - yield - - @pytest.fixture(scope="session") def test_db_path() -> str: return TEST_DB_PATH @@ -46,18 +40,69 @@ def test_db(test_db_path: str) -> Generator[str, None, None]: if os.path.exists(test_db_path): os.remove(test_db_path) - conn = sqlite3.connect(test_db_path) - cursor = conn.cursor() + # Create async engine for test database + test_db_url = f"sqlite+aiosqlite:///{test_db_path}" + engine = create_async_engine(test_db_url, echo=False) - for query in INIT_QUERY.strip().split(";"): - query = query.strip() - if query: - cursor.execute(query) + async def setup_db(): + async with engine.begin() as conn: + # Create tables + # LOCAL + from api.backend.database.models import Base - conn.commit() - conn.close() + await conn.run_sync(Base.metadata.create_all) + + # Run setup + asyncio.run(setup_db()) yield test_db_path if os.path.exists(test_db_path): os.remove(test_db_path) + + +@pytest_asyncio.fixture(scope="session") +async def test_engine(): + test_db_url = f"sqlite+aiosqlite:///{TEST_DB_PATH}" + engine = create_async_engine(test_db_url, poolclass=NullPool) + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + yield engine + await engine.dispose() + + +@pytest_asyncio.fixture(scope="function") +async def db_session(test_engine: Any) -> AsyncGenerator[AsyncSession, None]: + async_session = async_sessionmaker( + bind=test_engine, + class_=AsyncSession, + expire_on_commit=False, + ) + + async with async_session() as session: + try: + yield session + finally: + # Truncate all tables after each test + for table in reversed(Base.metadata.sorted_tables): + await session.execute(text(f"DELETE FROM {table.name}")) + await session.commit() + + +@pytest.fixture() +def override_get_db(db_session: AsyncSession): + async def _override() -> AsyncGenerator[AsyncSession, None]: + yield db_session + + return _override + + +@pytest_asyncio.fixture() +async def client(override_get_db: Any) -> AsyncGenerator[AsyncClient, None]: + app.dependency_overrides[get_db] = override_get_db + + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test") as c: + yield c + + app.dependency_overrides.clear() diff --git a/api/backend/tests/job/test_download_job.py b/api/backend/tests/job/test_download_job.py index 4882aa9..153f7b2 100644 --- a/api/backend/tests/job/test_download_job.py +++ b/api/backend/tests/job/test_download_job.py @@ -1,45 +1,65 @@ # STL -from unittest.mock import AsyncMock, patch +import random +from datetime import datetime, timezone # PDM import pytest -from fastapi.testclient import TestClient +from httpx import AsyncClient +from sqlalchemy.ext.asyncio import AsyncSession # LOCAL -from api.backend.app import app from api.backend.schemas.job import DownloadJob -from api.backend.tests.factories.job_factory import create_completed_job +from api.backend.database.models import Job -client = TestClient(app) - -mocked_job = create_completed_job().model_dump() -mock_results = [mocked_job] mocked_random_int = 123456 @pytest.mark.asyncio -@patch("api.backend.job.job_router.query") -@patch("api.backend.job.job_router.random.randint") -async def test_download(mock_randint: AsyncMock, mock_query: AsyncMock): - # Ensure the mock returns immediately - mock_query.return_value = mock_results - mock_randint.return_value = mocked_random_int +async def test_download(client: AsyncClient, db_session: AsyncSession): + # Insert a test job into the DB + job_id = "test-job-id" + test_job = Job( + id=job_id, + url="https://example.com", + elements=[], + user="test@example.com", + time_created=datetime.now(timezone.utc), + result=[ + { + "https://example.com": { + "element_name": [{"xpath": "//div", "text": "example"}] + } + } + ], + status="Completed", + chat=None, + job_options={}, + agent_mode=False, + prompt="", + favorite=False, + ) + db_session.add(test_job) + await db_session.commit() - # Create a DownloadJob instance - download_job = DownloadJob(ids=[mocked_job["id"]], job_format="csv") + # Force predictable randint + random.seed(0) - # Make a POST request to the /download endpoint - response = client.post("/download", json=download_job.model_dump()) + # Build request + download_job = DownloadJob(ids=[job_id], job_format="csv") + response = await client.post("/download", json=download_job.model_dump()) - # Assertions assert response.status_code == 200 assert response.headers["Content-Disposition"] == "attachment; filename=export.csv" - # Check the content of the CSV + # Validate CSV contents csv_content = response.content.decode("utf-8") - expected_csv = ( - f'"id","url","element_name","xpath","text","user","time_created"\r\n' - f'"{mocked_job["id"]}-{mocked_random_int}","https://example.com","element_name","//div","example",' - f'"{mocked_job["user"]}","{mocked_job["time_created"]}"\r\n' + lines = csv_content.strip().split("\n") + + assert ( + lines[0].strip() + == '"id","url","element_name","xpath","text","user","time_created"' ) - assert csv_content == expected_csv + assert '"https://example.com"' in lines[1] + assert '"element_name"' in lines[1] + assert '"//div"' in lines[1] + assert '"example"' in lines[1] diff --git a/api/backend/tests/scraping/test_scraping.py b/api/backend/tests/scraping/test_scraping.py index 3487369..27defc2 100644 --- a/api/backend/tests/scraping/test_scraping.py +++ b/api/backend/tests/scraping/test_scraping.py @@ -5,15 +5,17 @@ from datetime import datetime # PDM import pytest +from httpx import AsyncClient +from sqlalchemy import select from fastapi.testclient import TestClient from playwright.async_api import Route, Cookie, async_playwright +from sqlalchemy.ext.asyncio import AsyncSession # LOCAL from api.backend.app import app from api.backend.job.models import Proxy, Element, JobOptions from api.backend.schemas.job import Job -from api.backend.database.common import query -from api.backend.job.scraping.scraping import scrape +from api.backend.database.models import Job as JobModel from api.backend.job.scraping.add_custom import add_custom_items logging.basicConfig(level=logging.DEBUG) @@ -68,7 +70,7 @@ async def test_add_custom_items(): @pytest.mark.asyncio -async def test_proxies(): +async def test_proxies(client: AsyncClient, db_session: AsyncSession): job = Job( url="https://example.com", elements=[Element(xpath="//div", name="test")], @@ -84,14 +86,22 @@ async def test_proxies(): time_created=datetime.now().isoformat(), ) - response = client.post("/submit-scrape-job", json=job.model_dump()) + response = await client.post("/submit-scrape-job", json=job.model_dump()) assert response.status_code == 200 - jobs = query("SELECT * FROM jobs") - job = jobs[0] + stmt = select(JobModel) + result = await db_session.execute(stmt) + jobs = result.scalars().all() - assert job is not None - assert job["job_options"]["proxies"] == [ + assert len(jobs) > 0 + job_from_db = jobs[0] + + job_dict = job_from_db.__dict__ + job_dict.pop("_sa_instance_state", None) + + assert job_dict is not None + print(job_dict) + assert job_dict["job_options"]["proxies"] == [ { "server": "127.0.0.1:8080", "username": "user", @@ -99,12 +109,9 @@ async def test_proxies(): } ] - response = await scrape( - id=job["id"], - url=job["url"], - xpaths=[Element(**e) for e in job["elements"]], - job_options=job["job_options"], - ) - - example_response = response[0]["https://example.com/"] - assert example_response is not {} + # Verify the job was stored correctly in the database + assert job_dict["url"] == "https://example.com" + assert job_dict["status"] == "Queued" + assert len(job_dict["elements"]) == 1 + assert job_dict["elements"][0]["xpath"] == "//div" + assert job_dict["elements"][0]["name"] == "test" diff --git a/api/backend/worker/job_worker.py b/api/backend/worker/job_worker.py index 8a41c37..4bdbbfd 100644 --- a/api/backend/worker/job_worker.py +++ b/api/backend/worker/job_worker.py @@ -12,7 +12,6 @@ from api.backend.job import update_job, get_queued_job from api.backend.job.models import Element from api.backend.worker.logger import LOG from api.backend.ai.agent.agent import scrape_with_agent -from api.backend.database.startup import init_database from api.backend.worker.constants import ( TO, EMAIL, @@ -124,8 +123,6 @@ async def process_job(): async def main(): LOG.info("Starting job worker...") - init_database() - RECORDINGS_DIR.mkdir(parents=True, exist_ok=True) while True: diff --git a/docker/api/Dockerfile b/docker/api/Dockerfile index 7edf75f..a0dd498 100644 --- a/docker/api/Dockerfile +++ b/docker/api/Dockerfile @@ -3,7 +3,7 @@ FROM python:3.10.12-slim as pybuilder RUN apt-get update && \ apt-get install -y curl && \ - apt-get install -y x11vnc xvfb uvicorn wget gnupg supervisor libgl1 libglx-mesa0 libglx0 vainfo libva-dev libva-glx2 libva-drm2 ffmpeg && \ + apt-get install -y x11vnc xvfb uvicorn wget gnupg supervisor libgl1 libglx-mesa0 libglx0 vainfo libva-dev libva-glx2 libva-drm2 ffmpeg pkg-config default-libmysqlclient-dev gcc && \ curl -LsSf https://astral.sh/uv/install.sh | sh && \ apt-get remove -y curl && \ apt-get autoremove -y && \ @@ -37,6 +37,9 @@ RUN touch /project/app/data/database.db EXPOSE 5900 +COPY alembic /project/app/alembic +COPY alembic.ini /project/app/alembic.ini + COPY start.sh /project/app/start.sh CMD [ "supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf" ] \ No newline at end of file diff --git a/pdm.lock b/pdm.lock index 1296e22..4f72f1f 100644 --- a/pdm.lock +++ b/pdm.lock @@ -3,9 +3,9 @@ [metadata] groups = ["default", "dev"] -strategy = ["inherit_metadata"] +strategy = [] lock_version = "4.5.0" -content_hash = "sha256:222416fbd48d349e2ae777bf1d167b68e4342f38d5e20d04095cbbb594afb8f3" +content_hash = "sha256:f6a1256ec5ab146190965164dc773bb46df2bd7d1d43a488aeff69ee9af21b3a" [[metadata.targets]] requires_python = ">=3.10" @@ -13,9 +13,7 @@ requires_python = ">=3.10" [[package]] name = "aiofiles" version = "24.1.0" -requires_python = ">=3.8" -summary = "File support for asyncio." -groups = ["default"] +summary = "" files = [ {file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"}, {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}, @@ -24,9 +22,7 @@ files = [ [[package]] name = "aiohappyeyeballs" version = "2.6.1" -requires_python = ">=3.9" -summary = "Happy Eyeballs for asyncio" -groups = ["default"] +summary = "" files = [ {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, @@ -35,18 +31,16 @@ files = [ [[package]] name = "aiohttp" version = "3.11.18" -requires_python = ">=3.9" -summary = "Async http client/server framework (asyncio)" -groups = ["default"] +summary = "" dependencies = [ - "aiohappyeyeballs>=2.3.0", - "aiosignal>=1.1.2", - "async-timeout<6.0,>=4.0; python_version < \"3.11\"", - "attrs>=17.3.0", - "frozenlist>=1.1.1", - "multidict<7.0,>=4.5", - "propcache>=0.2.0", - "yarl<2.0,>=1.17.0", + "aiohappyeyeballs", + "aiosignal", + "async-timeout; python_full_version < \"3.11\"", + "attrs", + "frozenlist", + "multidict", + "propcache", + "yarl", ] files = [ {file = "aiohttp-3.11.18-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:96264854fedbea933a9ca4b7e0c745728f01380691687b7365d18d9e977179c4"}, @@ -116,29 +110,61 @@ files = [ {file = "aiohttp-3.11.18.tar.gz", hash = "sha256:ae856e1138612b7e412db63b7708735cff4d38d0399f6a5435d3dac2669f558a"}, ] +[[package]] +name = "aiomysql" +version = "0.2.0" +summary = "" +dependencies = [ + "pymysql", +] +files = [ + {file = "aiomysql-0.2.0-py3-none-any.whl", hash = "sha256:b7c26da0daf23a5ec5e0b133c03d20657276e4eae9b73e040b72787f6f6ade0a"}, + {file = "aiomysql-0.2.0.tar.gz", hash = "sha256:558b9c26d580d08b8c5fd1be23c5231ce3aeff2dadad989540fee740253deb67"}, +] + [[package]] name = "aiosignal" version = "1.3.2" -requires_python = ">=3.9" -summary = "aiosignal: a list of registered asynchronous callbacks" -groups = ["default"] +summary = "" dependencies = [ - "frozenlist>=1.1.0", + "frozenlist", ] 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]] +name = "aiosqlite" +version = "0.21.0" +summary = "" +dependencies = [ + "typing-extensions", +] +files = [ + {file = "aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0"}, + {file = "aiosqlite-0.21.0.tar.gz", hash = "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3"}, +] + +[[package]] +name = "alembic" +version = "1.16.4" +summary = "" +dependencies = [ + "mako", + "sqlalchemy", + "tomli; python_full_version < \"3.11\"", + "typing-extensions", +] +files = [ + {file = "alembic-1.16.4-py3-none-any.whl", hash = "sha256:b05e51e8e82efc1abd14ba2af6392897e145930c3e0a2faf2b0da2f7f7fd660d"}, + {file = "alembic-1.16.4.tar.gz", hash = "sha256:efab6ada0dd0fae2c92060800e0bf5c1dc26af15a10e02fb4babff164b4725e2"}, +] + [[package]] name = "annotated-types" version = "0.7.0" -requires_python = ">=3.8" -summary = "Reusable constraint types to use with typing.Annotated" -groups = ["default"] -dependencies = [ - "typing-extensions>=4.0.0; python_version < \"3.9\"", -] +summary = "" 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"}, @@ -147,14 +173,12 @@ files = [ [[package]] name = "anyio" version = "4.9.0" -requires_python = ">=3.9" -summary = "High level compatibility layer for multiple asynchronous event loop implementations" -groups = ["default"] +summary = "" dependencies = [ - "exceptiongroup>=1.0.2; python_version < \"3.11\"", - "idna>=2.8", - "sniffio>=1.1", - "typing-extensions>=4.5; python_version < \"3.13\"", + "exceptiongroup; python_full_version < \"3.11\"", + "idna", + "sniffio", + "typing-extensions; python_full_version < \"3.13\"", ] files = [ {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, @@ -164,8 +188,7 @@ files = [ [[package]] name = "appdirs" version = "1.4.4" -summary = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -groups = ["default"] +summary = "" files = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, @@ -174,12 +197,9 @@ files = [ [[package]] name = "apscheduler" version = "3.11.0" -requires_python = ">=3.8" -summary = "In-process task scheduler with Cron-like capabilities" -groups = ["default"] +summary = "" dependencies = [ - "backports-zoneinfo; python_version < \"3.9\"", - "tzlocal>=3.0", + "tzlocal", ] files = [ {file = "APScheduler-3.11.0-py3-none-any.whl", hash = "sha256:fc134ca32e50f5eadcc4938e3a4545ab19131435e851abb40b34d63d5141c6da"}, @@ -189,11 +209,9 @@ files = [ [[package]] name = "asgiref" version = "3.8.1" -requires_python = ">=3.8" -summary = "ASGI specs, helper code, and adapters" -groups = ["default"] +summary = "" dependencies = [ - "typing-extensions>=4; python_version < \"3.11\"", + "typing-extensions; python_full_version < \"3.11\"", ] files = [ {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, @@ -203,9 +221,7 @@ files = [ [[package]] name = "asttokens" version = "3.0.0" -requires_python = ">=3.8" -summary = "Annotate AST trees with source code positions" -groups = ["dev"] +summary = "" files = [ {file = "asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2"}, {file = "asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7"}, @@ -214,8 +230,7 @@ files = [ [[package]] name = "async-property" version = "0.2.2" -summary = "Python decorator for async properties." -groups = ["default"] +summary = "" files = [ {file = "async_property-0.2.2-py2.py3-none-any.whl", hash = "sha256:8924d792b5843994537f8ed411165700b27b2bd966cefc4daeefc1253442a9d7"}, {file = "async_property-0.2.2.tar.gz", hash = "sha256:17d9bd6ca67e27915a75d92549df64b5c7174e9dc806b30a3934dc4ff0506380"}, @@ -224,10 +239,7 @@ files = [ [[package]] name = "async-timeout" version = "5.0.1" -requires_python = ">=3.8" -summary = "Timeout context manager for asyncio programs" -groups = ["default"] -marker = "python_version < \"3.11\"" +summary = "" 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"}, @@ -236,19 +248,59 @@ files = [ [[package]] name = "asyncio" version = "3.4.3" -summary = "reference implementation of PEP 3156" -groups = ["default"] +summary = "" files = [ {file = "asyncio-3.4.3-py3-none-any.whl", hash = "sha256:c4d18b22701821de07bd6aea8b53d21449ec0ec5680645e5317062ea21817d2d"}, {file = "asyncio-3.4.3.tar.gz", hash = "sha256:83360ff8bc97980e4ff25c964c7bd3923d333d177aa4f7fb736b019f26c7cb41"}, ] +[[package]] +name = "asyncpg" +version = "0.30.0" +summary = "" +dependencies = [ + "async-timeout; python_full_version < \"3.11\"", +] +files = [ + {file = "asyncpg-0.30.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfb4dd5ae0699bad2b233672c8fc5ccbd9ad24b89afded02341786887e37927e"}, + {file = "asyncpg-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc1f62c792752a49f88b7e6f774c26077091b44caceb1983509edc18a2222ec0"}, + {file = "asyncpg-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3152fef2e265c9c24eec4ee3d22b4f4d2703d30614b0b6753e9ed4115c8a146f"}, + {file = "asyncpg-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7255812ac85099a0e1ffb81b10dc477b9973345793776b128a23e60148dd1af"}, + {file = "asyncpg-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:578445f09f45d1ad7abddbff2a3c7f7c291738fdae0abffbeb737d3fc3ab8b75"}, + {file = "asyncpg-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c42f6bb65a277ce4d93f3fba46b91a265631c8df7250592dd4f11f8b0152150f"}, + {file = "asyncpg-0.30.0-cp310-cp310-win32.whl", hash = "sha256:aa403147d3e07a267ada2ae34dfc9324e67ccc4cdca35261c8c22792ba2b10cf"}, + {file = "asyncpg-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb622c94db4e13137c4c7f98834185049cc50ee01d8f657ef898b6407c7b9c50"}, + {file = "asyncpg-0.30.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5e0511ad3dec5f6b4f7a9e063591d407eee66b88c14e2ea636f187da1dcfff6a"}, + {file = "asyncpg-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:915aeb9f79316b43c3207363af12d0e6fd10776641a7de8a01212afd95bdf0ed"}, + {file = "asyncpg-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c198a00cce9506fcd0bf219a799f38ac7a237745e1d27f0e1f66d3707c84a5a"}, + {file = "asyncpg-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3326e6d7381799e9735ca2ec9fd7be4d5fef5dcbc3cb555d8a463d8460607956"}, + {file = "asyncpg-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:51da377487e249e35bd0859661f6ee2b81db11ad1f4fc036194bc9cb2ead5056"}, + {file = "asyncpg-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc6d84136f9c4d24d358f3b02be4b6ba358abd09f80737d1ac7c444f36108454"}, + {file = "asyncpg-0.30.0-cp311-cp311-win32.whl", hash = "sha256:574156480df14f64c2d76450a3f3aaaf26105869cad3865041156b38459e935d"}, + {file = "asyncpg-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:3356637f0bd830407b5597317b3cb3571387ae52ddc3bca6233682be88bbbc1f"}, + {file = "asyncpg-0.30.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c902a60b52e506d38d7e80e0dd5399f657220f24635fee368117b8b5fce1142e"}, + {file = "asyncpg-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aca1548e43bbb9f0f627a04666fedaca23db0a31a84136ad1f868cb15deb6e3a"}, + {file = "asyncpg-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c2a2ef565400234a633da0eafdce27e843836256d40705d83ab7ec42074efb3"}, + {file = "asyncpg-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1292b84ee06ac8a2ad8e51c7475aa309245874b61333d97411aab835c4a2f737"}, + {file = "asyncpg-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5712350388d0cd0615caec629ad53c81e506b1abaaf8d14c93f54b35e3595a"}, + {file = "asyncpg-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db9891e2d76e6f425746c5d2da01921e9a16b5a71a1c905b13f30e12a257c4af"}, + {file = "asyncpg-0.30.0-cp312-cp312-win32.whl", hash = "sha256:68d71a1be3d83d0570049cd1654a9bdfe506e794ecc98ad0873304a9f35e411e"}, + {file = "asyncpg-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:9a0292c6af5c500523949155ec17b7fe01a00ace33b68a476d6b5059f9630305"}, + {file = "asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70"}, + {file = "asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3"}, + {file = "asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33"}, + {file = "asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4"}, + {file = "asyncpg-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4"}, + {file = "asyncpg-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba"}, + {file = "asyncpg-0.30.0-cp313-cp313-win32.whl", hash = "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590"}, + {file = "asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e"}, + {file = "asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851"}, +] + [[package]] name = "attrs" version = "25.3.0" -requires_python = ">=3.8" -summary = "Classes Without Boilerplate" -groups = ["default"] +summary = "" files = [ {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, @@ -257,9 +309,7 @@ files = [ [[package]] name = "bcrypt" version = "4.0.1" -requires_python = ">=3.6" -summary = "Modern password hashing for your software and your servers" -groups = ["default"] +summary = "" files = [ {file = "bcrypt-4.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f"}, {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0"}, @@ -278,12 +328,10 @@ files = [ [[package]] name = "beautifulsoup4" version = "4.13.4" -requires_python = ">=3.7.0" -summary = "Screen-scraping library" -groups = ["default"] +summary = "" dependencies = [ - "soupsieve>1.2", - "typing-extensions>=4.0.0", + "soupsieve", + "typing-extensions", ] files = [ {file = "beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b"}, @@ -293,9 +341,7 @@ files = [ [[package]] name = "blinker" version = "1.7.0" -requires_python = ">=3.8" -summary = "Fast, simple object-to-object and broadcast signaling" -groups = ["default"] +summary = "" files = [ {file = "blinker-1.7.0-py3-none-any.whl", hash = "sha256:c3f865d4d54db7abc53758a01601cf343fe55b84c1de4e3fa910e420b438d5b9"}, {file = "blinker-1.7.0.tar.gz", hash = "sha256:e6820ff6fa4e4d1d8e2747c2283749c3f547e4fee112b98555cdcdae32996182"}, @@ -304,13 +350,11 @@ files = [ [[package]] name = "boto3" version = "1.38.2" -requires_python = ">=3.9" -summary = "The AWS SDK for Python" -groups = ["default"] +summary = "" dependencies = [ - "botocore<1.39.0,>=1.38.2", - "jmespath<2.0.0,>=0.7.1", - "s3transfer<0.13.0,>=0.12.0", + "botocore", + "jmespath", + "s3transfer", ] files = [ {file = "boto3-1.38.2-py3-none-any.whl", hash = "sha256:ef3237b169cd906a44a32c03b3229833d923c9e9733355b329ded2151f91ec0b"}, @@ -320,13 +364,11 @@ files = [ [[package]] name = "boto3-stubs" version = "1.38.2" -requires_python = ">=3.8" -summary = "Type annotations for boto3 1.38.2 generated with mypy-boto3-builder 8.10.1" -groups = ["default"] +summary = "" dependencies = [ "botocore-stubs", "types-s3transfer", - "typing-extensions>=4.1.0; python_version < \"3.12\"", + "typing-extensions; python_full_version < \"3.12\"", ] files = [ {file = "boto3_stubs-1.38.2-py3-none-any.whl", hash = "sha256:e18f2dc194c4b8a29f61275ba039689d063c4775a78560e35a5ce820ec257fb5"}, @@ -337,18 +379,16 @@ files = [ name = "boto3-stubs" version = "1.38.2" extras = ["essential"] -requires_python = ">=3.8" -summary = "Type annotations for boto3 1.38.2 generated with mypy-boto3-builder 8.10.1" -groups = ["default"] +summary = "" dependencies = [ "boto3-stubs==1.38.2", - "mypy-boto3-cloudformation<1.39.0,>=1.38.0", - "mypy-boto3-dynamodb<1.39.0,>=1.38.0", - "mypy-boto3-ec2<1.39.0,>=1.38.0", - "mypy-boto3-lambda<1.39.0,>=1.38.0", - "mypy-boto3-rds<1.39.0,>=1.38.0", - "mypy-boto3-s3<1.39.0,>=1.38.0", - "mypy-boto3-sqs<1.39.0,>=1.38.0", + "mypy-boto3-cloudformation", + "mypy-boto3-dynamodb", + "mypy-boto3-ec2", + "mypy-boto3-lambda", + "mypy-boto3-rds", + "mypy-boto3-s3", + "mypy-boto3-sqs", ] files = [ {file = "boto3_stubs-1.38.2-py3-none-any.whl", hash = "sha256:e18f2dc194c4b8a29f61275ba039689d063c4775a78560e35a5ce820ec257fb5"}, @@ -358,14 +398,11 @@ files = [ [[package]] name = "botocore" version = "1.38.2" -requires_python = ">=3.9" -summary = "Low-level, data-driven core of boto 3." -groups = ["default"] +summary = "" dependencies = [ - "jmespath<2.0.0,>=0.7.1", - "python-dateutil<3.0.0,>=2.1", - "urllib3!=2.2.0,<3,>=1.25.4; python_version >= \"3.10\"", - "urllib3<1.27,>=1.25.4; python_version < \"3.10\"", + "jmespath", + "python-dateutil", + "urllib3", ] files = [ {file = "botocore-1.38.2-py3-none-any.whl", hash = "sha256:5d9cffedb1c759a058b43793d16647ed44ec87072f98a1bd6cd673ac0ae6b81d"}, @@ -375,9 +412,7 @@ files = [ [[package]] name = "botocore-stubs" version = "1.38.2" -requires_python = ">=3.8" -summary = "Type annotations and code completion for botocore" -groups = ["default"] +summary = "" dependencies = [ "types-awscrt", ] @@ -389,8 +424,7 @@ files = [ [[package]] name = "brotli" version = "1.1.0" -summary = "Python bindings for the Brotli compression library" -groups = ["default"] +summary = "" 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"}, @@ -460,12 +494,9 @@ files = [ [[package]] name = "browserforge" version = "1.2.1" -requires_python = "<4.0,>=3.8" -summary = "Intelligent browser header & fingerprint generator" -groups = ["default"] +summary = "" dependencies = [ "click", - "typing-extensions; python_version < \"3.10\"", ] files = [ {file = "browserforge-1.2.1-py3-none-any.whl", hash = "sha256:b2813b4de80b9c48c88700c93e3dfa6a64694d04f3263545e28bb03dd95df27e"}, @@ -475,8 +506,7 @@ files = [ [[package]] name = "bs4" version = "0.0.2" -summary = "Dummy package for Beautiful Soup (beautifulsoup4)" -groups = ["default"] +summary = "" dependencies = [ "beautifulsoup4", ] @@ -488,11 +518,9 @@ files = [ [[package]] name = "camoufox" version = "0.4.11" -requires_python = "<4.0,>=3.8" -summary = "Wrapper around Playwright to help launch Camoufox" -groups = ["default"] +summary = "" dependencies = [ - "browserforge<2.0.0,>=1.2.1", + "browserforge", "click", "language-tags", "lxml", @@ -516,9 +544,7 @@ files = [ [[package]] name = "certifi" version = "2025.1.31" -requires_python = ">=3.6" -summary = "Python package for providing Mozilla's CA Bundle." -groups = ["default"] +summary = "" files = [ {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, @@ -527,9 +553,7 @@ files = [ [[package]] name = "cffi" version = "1.17.1" -requires_python = ">=3.8" -summary = "Foreign Function Interface for Python calling C code." -groups = ["default"] +summary = "" dependencies = [ "pycparser", ] @@ -586,9 +610,7 @@ files = [ [[package]] name = "charset-normalizer" version = "3.4.1" -requires_python = ">=3.7" -summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -groups = ["default"] +summary = "" 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"}, @@ -649,12 +671,9 @@ files = [ [[package]] name = "click" version = "8.1.8" -requires_python = ">=3.7" -summary = "Composable command line interface toolkit" -groups = ["default"] +summary = "" dependencies = [ - "colorama; platform_system == \"Windows\"", - "importlib-metadata; python_version < \"3.8\"", + "colorama; sys_platform == \"win32\"", ] files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, @@ -664,10 +683,7 @@ files = [ [[package]] name = "colorama" version = "0.4.6" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -summary = "Cross-platform colored terminal text." -groups = ["default", "dev"] -marker = "sys_platform == \"win32\" or platform_system == \"Windows\"" +summary = "" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -676,11 +692,9 @@ files = [ [[package]] name = "cryptography" version = "44.0.2" -requires_python = "!=3.9.0,!=3.9.1,>=3.7" -summary = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -groups = ["default"] +summary = "" dependencies = [ - "cffi>=1.12; platform_python_implementation != \"PyPy\"", + "cffi; platform_python_implementation != \"PyPy\"", ] files = [ {file = "cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7"}, @@ -723,9 +737,7 @@ files = [ [[package]] name = "cssselect" version = "1.3.0" -requires_python = ">=3.9" -summary = "cssselect parses CSS3 Selectors and translates them to XPath 1.0" -groups = ["default"] +summary = "" files = [ {file = "cssselect-1.3.0-py3-none-any.whl", hash = "sha256:56d1bf3e198080cc1667e137bc51de9cadfca259f03c2d4e09037b3e01e30f0d"}, {file = "cssselect-1.3.0.tar.gz", hash = "sha256:57f8a99424cfab289a1b6a816a43075a4b00948c86b4dcf3ef4ee7e15f7ab0c7"}, @@ -734,51 +746,16 @@ files = [ [[package]] name = "cython" version = "3.1.0" -requires_python = ">=3.8" -summary = "The Cython compiler for writing C extensions in the Python language." -groups = ["default"] -marker = "sys_platform == \"darwin\"" +summary = "" files = [ {file = "cython-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:335982ac0b71a75720b99b980570b9a8416fafd1989ccf4292c0f2e0e1902eac"}, {file = "cython-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c9389b7941e333a1cc11074556adbf6a9f97ed3de141c1b45cc9f957cd7f7fa2"}, - {file = "cython-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:136c938f3c0fe91bea3eab32751b860ab7587285c5225436b76a98fe933c599a"}, - {file = "cython-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d722d311fee9f0dc80b17b8f9d1f46311be63b631b7aeed8530bf5f5e8849507"}, - {file = "cython-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:95eb189635a4542f1f8471bcf9756bffdac5294c41d4a4de935c77852d54e992"}, - {file = "cython-3.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c063146c711751701ad662eefbdf5b396098d646f1239a2f5a6caea2d6707c5d"}, - {file = "cython-3.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d78774a25c221fbda3855acbccb249989a04d334fb4ac8112ab5ffe4f1bcc65e"}, - {file = "cython-3.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:678e204230ece3205c17285383727b9e99097e7a80330fabb75cdd80d1f4c2ee"}, - {file = "cython-3.1.0-cp310-cp310-win32.whl", hash = "sha256:8029dffafa9ec5e83b6cc28f8b061f158132f2b1e800673716f7b9d9f85f2335"}, - {file = "cython-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:8dbefee67f3c9219cc9d2311e4ebf9f7b930e1db4b6eec2863df0c436e3c78d0"}, {file = "cython-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c736405078dc376502617eb41c39e223ae176ebd1a4ddc18179d2517bc8c8658"}, {file = "cython-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1215d3adb4e8691d03e712aed31206d21f387a8003d8de6a574ee75fe6d2e07c"}, - {file = "cython-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:522d4dae1fea71eee5c944fb7a8530de8bdd6df0ccb2bd001d0f75be228eac6c"}, - {file = "cython-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:462ad6142057e81715ada74e2d24b9a07bf36ae3da72bf973478b5c3e809c26d"}, - {file = "cython-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a8f00cdeb14f004ebeacf946e06bad2e3ed5776af96f5af95f92d822c4ba275f"}, - {file = "cython-3.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:37d62b8b8919126c75769e5470b288d76c83a1645e73c7aca4b7d7aecb3c1234"}, - {file = "cython-3.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bea0b6bfde7493acb0529fc603abd4b3b13c3bb2fff7a889ae5a8d3ea7dc5a84"}, - {file = "cython-3.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fe8c1db9ec03d9ef83e33c842c108e892577ade4c5f530c9435beced048e4698"}, - {file = "cython-3.1.0-cp311-cp311-win32.whl", hash = "sha256:5f6417d378bd11ca55f16e3c1c7c3bf6d7f0a0cc326c46e309fcba46c54ba4f1"}, - {file = "cython-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:dde3726aa5acbe879f849a09606b886491f950cfa993b435e50e9561fdf731c6"}, {file = "cython-3.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8f8c4753f6b926046c0cdf6037ba8560f6677730bf0ab9c1db4e0163b4bb30f9"}, {file = "cython-3.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:db8e15c8eeee529468eab08528c9bf714a94354b34375be6c0c110f6012a4768"}, - {file = "cython-3.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a46b34defa672268474fbb5541f6297f45df9e4ecc4def6edd6fe1c44bfdb795"}, - {file = "cython-3.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8818446612461aca3978ebe8e3def817a120d91f85022540843ebe4f24818cd6"}, - {file = "cython-3.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe401e825b0fbeec75f8cc758c8cf32345c673bdb0edaf9585cd43b9d2798824"}, - {file = "cython-3.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c96908b302e87e99915b3b66481a976e32b864e95bf054dcd2cb859dffd8cb10"}, - {file = "cython-3.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cdde5f25fdb8a5d50dbe5d418fe5bfb2260b1acdbd45b788e77b247e9adf2f56"}, - {file = "cython-3.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe3320d13cde70fa8b1936e633b9e0fa68720cc61f97aa371d56d0f84fba3e02"}, - {file = "cython-3.1.0-cp312-cp312-win32.whl", hash = "sha256:d41d17d7cfcfbddf3b7dc0ceddb6361b8e749b0b3c5f8efa40c31c249127fa15"}, - {file = "cython-3.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:61eb67401bd6c977084fc789812bd40f96be500049adb2bab99921d696ae0c87"}, {file = "cython-3.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:539828d14fbd95eff135e8dc9e93012f5b018657868f15a69cb475b8784efb9a"}, {file = "cython-3.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd0003171ad84d4812fdb1eb9a4f678ed027e75fbc2b7bef5db482621b72137a"}, - {file = "cython-3.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4551f9ab91019b6b63cf8b16bf1abb519db67627c31162f604e370e596b8c60c"}, - {file = "cython-3.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c088ac33f4fa04b3589c4e5cfb8a81e9d9a990405409f9c8bfab0f5a9e8b724f"}, - {file = "cython-3.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8926651830ada313a04284e711c2cf11e4e800ca080e83012418208edd4334a2"}, - {file = "cython-3.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e03b3280c7ff99fae7b47327a4e2de7e117b069ce9183dc53774069c3e73d1c8"}, - {file = "cython-3.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0605d364a2cc632c9269990777c2b266611724d1fccaa614fde335c2209b82da"}, - {file = "cython-3.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:856950b7c4282a713bcf4794aaae8f18d4a1ae177d3b63739604c91019ac4117"}, - {file = "cython-3.1.0-cp313-cp313-win32.whl", hash = "sha256:d6854c89d6c1ff472861376822a9df7a0c62b2be362147d313cf7f10bf230c69"}, - {file = "cython-3.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9d6c88e8c86f2c582a2f9b460174ef86d9e01c8bfb12b8f7c44d697242285551"}, {file = "cython-3.1.0-py3-none-any.whl", hash = "sha256:4e460bdf1d8742ddf4914959842f2f23ca4934df97f864be799ddf1912acd0ab"}, {file = "cython-3.1.0.tar.gz", hash = "sha256:1097dd60d43ad0fff614a57524bfd531b35c13a907d13bee2cc2ec152e6bf4a1"}, ] @@ -786,9 +763,7 @@ files = [ [[package]] name = "decorator" version = "5.2.1" -requires_python = ">=3.8" -summary = "Decorators for Humans" -groups = ["dev"] +summary = "" files = [ {file = "decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a"}, {file = "decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360"}, @@ -797,8 +772,7 @@ files = [ [[package]] name = "deprecation" version = "2.1.0" -summary = "A library to handle automated deprecations" -groups = ["default"] +summary = "" dependencies = [ "packaging", ] @@ -810,9 +784,7 @@ files = [ [[package]] name = "distro" version = "1.9.0" -requires_python = ">=3.6" -summary = "Distro - an OS platform information API" -groups = ["default"] +summary = "" files = [ {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, @@ -821,9 +793,7 @@ files = [ [[package]] name = "dnspython" version = "2.7.0" -requires_python = ">=3.9" -summary = "DNS toolkit" -groups = ["default"] +summary = "" files = [ {file = "dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86"}, {file = "dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1"}, @@ -832,13 +802,11 @@ files = [ [[package]] name = "docker" version = "7.1.0" -requires_python = ">=3.8" -summary = "A Python library for the Docker Engine API." -groups = ["default"] +summary = "" dependencies = [ - "pywin32>=304; sys_platform == \"win32\"", - "requests>=2.26.0", - "urllib3>=1.26.0", + "pywin32; sys_platform == \"win32\"", + "requests", + "urllib3", ] files = [ {file = "docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0"}, @@ -848,11 +816,9 @@ files = [ [[package]] name = "ecdsa" version = "0.19.1" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.6" -summary = "ECDSA cryptographic signature library (pure python)" -groups = ["default"] +summary = "" dependencies = [ - "six>=1.9.0", + "six", ] files = [ {file = "ecdsa-0.19.1-py2.py3-none-any.whl", hash = "sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3"}, @@ -862,12 +828,10 @@ files = [ [[package]] name = "email-validator" version = "2.2.0" -requires_python = ">=3.8" -summary = "A robust email address syntax and deliverability validation library." -groups = ["default"] +summary = "" dependencies = [ - "dnspython>=2.0.0", - "idna>=2.0.0", + "dnspython", + "idna", ] files = [ {file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"}, @@ -877,9 +841,7 @@ files = [ [[package]] name = "et-xmlfile" version = "2.0.0" -requires_python = ">=3.8" -summary = "An implementation of lxml.xmlfile for the standard library" -groups = ["default"] +summary = "" files = [ {file = "et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa"}, {file = "et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54"}, @@ -888,9 +850,7 @@ files = [ [[package]] name = "exceptiongroup" version = "1.2.2" -requires_python = ">=3.7" -summary = "Backport of PEP 654 (exception groups)" -groups = ["default", "dev"] +summary = "" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -899,9 +859,7 @@ files = [ [[package]] name = "executing" version = "2.2.0" -requires_python = ">=3.8" -summary = "Get the currently executing AST node of a frame, and other information" -groups = ["dev"] +summary = "" files = [ {file = "executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa"}, {file = "executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755"}, @@ -910,12 +868,7 @@ files = [ [[package]] name = "fake-useragent" version = "2.2.0" -requires_python = ">=3.9" -summary = "Up-to-date simple useragent faker with real world database" -groups = ["default"] -dependencies = [ - "importlib-resources>=6; python_version < \"3.10\"", -] +summary = "" files = [ {file = "fake_useragent-2.2.0-py3-none-any.whl", hash = "sha256:67f35ca4d847b0d298187443aaf020413746e56acd985a611908c73dba2daa24"}, {file = "fake_useragent-2.2.0.tar.gz", hash = "sha256:4e6ab6571e40cc086d788523cf9e018f618d07f9050f822ff409a4dfe17c16b2"}, @@ -924,9 +877,7 @@ files = [ [[package]] name = "faker" version = "37.1.0" -requires_python = ">=3.9" -summary = "Faker is a Python package that generates fake data for you." -groups = ["default"] +summary = "" dependencies = [ "tzdata", ] @@ -938,13 +889,11 @@ files = [ [[package]] name = "fastapi" version = "0.115.12" -requires_python = ">=3.8" -summary = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" -groups = ["default"] +summary = "" dependencies = [ - "pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4", - "starlette<0.47.0,>=0.40.0", - "typing-extensions>=4.8.0", + "pydantic", + "starlette", + "typing-extensions", ] files = [ {file = "fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d"}, @@ -954,31 +903,29 @@ files = [ [[package]] name = "fastapi-keycloak" version = "1.1.0" -requires_python = ">=3.9" -summary = "Keycloak API Client for integrating authentication and authorization with FastAPI" -groups = ["default"] +summary = "" dependencies = [ - "anyio>=3.4.0", - "asgiref>=3.4.1", - "certifi>=2023.7.22", - "charset-normalizer>=2.0.9", - "click>=8.0.3", - "ecdsa>=0.17.0", - "fastapi>=0.70.1", - "h11>=0.12.0", - "idna>=3.3", - "itsdangerous>=2.0.1", - "pyasn1>=0.4.8", - "pydantic>=1.5a1", - "python-jose>=3.3.0", - "requests>=2.31.0", - "rsa>=4.8", - "six>=1.16.0", - "sniffio>=1.2.0", - "starlette>=0.36.2", - "typing-extensions>=4.0.1", - "urllib3>=1.26.17", - "uvicorn>=0.16.0", + "anyio", + "asgiref", + "certifi", + "charset-normalizer", + "click", + "ecdsa", + "fastapi", + "h11", + "idna", + "itsdangerous", + "pyasn1", + "pydantic", + "python-jose", + "requests", + "rsa", + "six", + "sniffio", + "starlette", + "typing-extensions", + "urllib3", + "uvicorn", ] files = [ {file = "fastapi_keycloak-1.1.0-py3-none-any.whl", hash = "sha256:2b61a5a4df709e6f9fe455065cd4d758861867f093c86b7d4e5f0418ffb8f0aa"}, @@ -988,9 +935,7 @@ files = [ [[package]] name = "frozenlist" version = "1.6.0" -requires_python = ">=3.9" -summary = "A list-like structure which implements collections.abc.MutableSequence" -groups = ["default"] +summary = "" files = [ {file = "frozenlist-1.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e6e558ea1e47fd6fa8ac9ccdad403e5dd5ecc6ed8dda94343056fa4277d5c65e"}, {file = "frozenlist-1.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4b3cd7334a4bbc0c472164f3744562cb72d05002cc6fcf58adb104630bbc352"}, @@ -1084,9 +1029,7 @@ files = [ [[package]] name = "greenlet" version = "3.2.2" -requires_python = ">=3.9" -summary = "Lightweight in-process concurrent programming" -groups = ["default"] +summary = "" files = [ {file = "greenlet-3.2.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c49e9f7c6f625507ed83a7485366b46cbe325717c60837f7244fc99ba16ba9d6"}, {file = "greenlet-3.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3cc1a3ed00ecfea8932477f729a9f616ad7347a5e55d50929efa50a86cb7be7"}, @@ -1138,9 +1081,7 @@ files = [ [[package]] name = "h11" version = "0.16.0" -requires_python = ">=3.8" -summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -groups = ["default"] +summary = "" files = [ {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, @@ -1149,13 +1090,10 @@ files = [ [[package]] name = "h2" version = "4.2.0" -requires_python = ">=3.9" -summary = "Pure-Python HTTP/2 protocol implementation" -groups = ["default"] -marker = "python_version >= \"3.6.0\"" +summary = "" dependencies = [ - "hpack<5,>=4.1", - "hyperframe<7,>=6.1", + "hpack", + "hyperframe", ] files = [ {file = "h2-4.2.0-py3-none-any.whl", hash = "sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0"}, @@ -1165,10 +1103,7 @@ files = [ [[package]] name = "hpack" version = "4.1.0" -requires_python = ">=3.9" -summary = "Pure-Python HPACK header encoding" -groups = ["default"] -marker = "python_version >= \"3.6.0\"" +summary = "" files = [ {file = "hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496"}, {file = "hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca"}, @@ -1177,9 +1112,7 @@ files = [ [[package]] name = "html2text" version = "2025.4.15" -requires_python = ">=3.9" -summary = "Turn HTML into equivalent Markdown-structured text." -groups = ["default"] +summary = "" files = [ {file = "html2text-2025.4.15-py3-none-any.whl", hash = "sha256:00569167ffdab3d7767a4cdf589b7f57e777a5ed28d12907d8c58769ec734acc"}, {file = "html2text-2025.4.15.tar.gz", hash = "sha256:948a645f8f0bc3abe7fd587019a2197a12436cd73d0d4908af95bfc8da337588"}, @@ -1188,12 +1121,10 @@ files = [ [[package]] name = "httpcore" version = "1.0.9" -requires_python = ">=3.8" -summary = "A minimal low-level HTTP client." -groups = ["default"] +summary = "" dependencies = [ "certifi", - "h11>=0.16", + "h11", ] files = [ {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, @@ -1203,13 +1134,11 @@ files = [ [[package]] name = "httpx" version = "0.28.1" -requires_python = ">=3.8" -summary = "The next generation HTTP client." -groups = ["default"] +summary = "" dependencies = [ "anyio", "certifi", - "httpcore==1.*", + "httpcore", "idna", ] files = [ @@ -1220,10 +1149,7 @@ files = [ [[package]] name = "hyperframe" version = "6.1.0" -requires_python = ">=3.9" -summary = "Pure-Python HTTP/2 framing" -groups = ["default"] -marker = "python_version >= \"3.6.0\"" +summary = "" files = [ {file = "hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5"}, {file = "hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08"}, @@ -1232,9 +1158,7 @@ files = [ [[package]] name = "idna" version = "3.10" -requires_python = ">=3.6" -summary = "Internationalized Domain Names in Applications (IDNA)" -groups = ["default"] +summary = "" files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -1243,9 +1167,7 @@ files = [ [[package]] name = "iniconfig" version = "2.1.0" -requires_python = ">=3.8" -summary = "brain-dead simple config-ini parsing" -groups = ["default", "dev"] +summary = "" files = [ {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, @@ -1254,21 +1176,19 @@ files = [ [[package]] name = "ipython" version = "8.35.0" -requires_python = ">=3.10" -summary = "IPython: Productive Interactive Computing" -groups = ["dev"] +summary = "" dependencies = [ "colorama; sys_platform == \"win32\"", "decorator", - "exceptiongroup; python_version < \"3.11\"", - "jedi>=0.16", + "exceptiongroup; python_full_version < \"3.11\"", + "jedi", "matplotlib-inline", - "pexpect>4.3; sys_platform != \"win32\" and sys_platform != \"emscripten\"", - "prompt-toolkit<3.1.0,>=3.0.41", - "pygments>=2.4.0", + "pexpect; sys_platform != \"emscripten\" and sys_platform != \"win32\"", + "prompt-toolkit", + "pygments", "stack-data", - "traitlets>=5.13.0", - "typing-extensions>=4.6; python_version < \"3.12\"", + "traitlets", + "typing-extensions; python_full_version < \"3.12\"", ] files = [ {file = "ipython-8.35.0-py3-none-any.whl", hash = "sha256:e6b7470468ba6f1f0a7b116bb688a3ece2f13e2f94138e508201fad677a788ba"}, @@ -1278,9 +1198,7 @@ files = [ [[package]] name = "itsdangerous" version = "2.2.0" -requires_python = ">=3.8" -summary = "Safely pass data to untrusted environments and back." -groups = ["default"] +summary = "" files = [ {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, @@ -1289,11 +1207,9 @@ files = [ [[package]] name = "jedi" version = "0.19.2" -requires_python = ">=3.6" -summary = "An autocompletion tool for Python that can be used for text editors." -groups = ["dev"] +summary = "" dependencies = [ - "parso<0.9.0,>=0.8.4", + "parso", ] files = [ {file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"}, @@ -1303,9 +1219,7 @@ files = [ [[package]] name = "jiter" version = "0.9.0" -requires_python = ">=3.8" -summary = "Fast iterable JSON parser." -groups = ["default"] +summary = "" files = [ {file = "jiter-0.9.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:816ec9b60fdfd1fec87da1d7ed46c66c44ffec37ab2ef7de5b147b2fce3fd5ad"}, {file = "jiter-0.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b1d3086f8a3ee0194ecf2008cf81286a5c3e540d977fa038ff23576c023c0ea"}, @@ -1364,9 +1278,7 @@ files = [ [[package]] name = "jmespath" version = "1.0.1" -requires_python = ">=3.7" -summary = "JSON Matching Expressions" -groups = ["default"] +summary = "" files = [ {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, @@ -1375,12 +1287,10 @@ files = [ [[package]] name = "jwcrypto" version = "1.5.6" -requires_python = ">= 3.8" -summary = "Implementation of JOSE Web standards" -groups = ["default"] +summary = "" dependencies = [ - "cryptography>=3.4", - "typing-extensions>=4.5.0", + "cryptography", + "typing-extensions", ] files = [ {file = "jwcrypto-1.5.6-py3-none-any.whl", hash = "sha256:150d2b0ebbdb8f40b77f543fb44ffd2baeff48788be71f67f03566692fd55789"}, @@ -1390,12 +1300,7 @@ files = [ [[package]] name = "kaitaistruct" version = "0.10" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -summary = "Kaitai Struct declarative parser generator for binary data: runtime library for Python" -groups = ["default"] -dependencies = [ - "enum34; python_version < \"3.4\"", -] +summary = "" files = [ {file = "kaitaistruct-0.10-py2.py3-none-any.whl", hash = "sha256:a97350919adbf37fda881f75e9365e2fb88d04832b7a4e57106ec70119efb235"}, {file = "kaitaistruct-0.10.tar.gz", hash = "sha256:a044dee29173d6afbacf27bcac39daf89b654dd418cfa009ab82d9178a9ae52a"}, @@ -1404,8 +1309,7 @@ files = [ [[package]] name = "language-tags" version = "1.2.0" -summary = "This project is a Python version of the language-tags Javascript project." -groups = ["default"] +summary = "" files = [ {file = "language_tags-1.2.0-py3-none-any.whl", hash = "sha256:d815604622242fdfbbfd747b40c31213617fd03734a267f2e39ee4bd73c88722"}, {file = "language_tags-1.2.0.tar.gz", hash = "sha256:e934acba3e3dc85f867703eca421847a9ab7b7679b11b5d5cfd096febbf8bde6"}, @@ -1414,9 +1318,7 @@ files = [ [[package]] name = "lxml" version = "5.4.0" -requires_python = ">=3.6" -summary = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." -groups = ["default"] +summary = "" files = [ {file = "lxml-5.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e7bc6df34d42322c5289e37e9971d6ed114e3776b45fa879f734bded9d1fea9c"}, {file = "lxml-5.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6854f8bd8a1536f8a1d9a3655e6354faa6406621cf857dc27b681b69860645c7"}, @@ -1495,124 +1397,89 @@ files = [ {file = "lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd"}, ] -[[package]] -name = "lxml-html-clean" -version = "0.4.2" -summary = "HTML cleaner from lxml project" -groups = ["default"] -dependencies = [ - "lxml", -] -files = [ - {file = "lxml_html_clean-0.4.2-py3-none-any.whl", hash = "sha256:74ccfba277adcfea87a1e9294f47dd86b05d65b4da7c5b07966e3d5f3be8a505"}, - {file = "lxml_html_clean-0.4.2.tar.gz", hash = "sha256:91291e7b5db95430abf461bc53440964d58e06cc468950f9e47db64976cebcb3"}, -] - [[package]] name = "lxml-stubs" version = "0.5.1" -summary = "Type annotations for the lxml package" -groups = ["default"] +summary = "" files = [ {file = "lxml-stubs-0.5.1.tar.gz", hash = "sha256:e0ec2aa1ce92d91278b719091ce4515c12adc1d564359dfaf81efa7d4feab79d"}, {file = "lxml_stubs-0.5.1-py3-none-any.whl", hash = "sha256:1f689e5dbc4b9247cb09ae820c7d34daeb1fdbd1db06123814b856dae7787272"}, ] [[package]] -name = "lxml" -version = "5.4.0" -extras = ["html_clean"] -requires_python = ">=3.6" -summary = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." -groups = ["default"] +name = "mako" +version = "1.3.10" +summary = "" dependencies = [ - "lxml-html-clean", - "lxml==5.4.0", + "markupsafe", ] files = [ - {file = "lxml-5.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e7bc6df34d42322c5289e37e9971d6ed114e3776b45fa879f734bded9d1fea9c"}, - {file = "lxml-5.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6854f8bd8a1536f8a1d9a3655e6354faa6406621cf857dc27b681b69860645c7"}, - {file = "lxml-5.4.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:696ea9e87442467819ac22394ca36cb3d01848dad1be6fac3fb612d3bd5a12cf"}, - {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ef80aeac414f33c24b3815ecd560cee272786c3adfa5f31316d8b349bfade28"}, - {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b9c2754cef6963f3408ab381ea55f47dabc6f78f4b8ebb0f0b25cf1ac1f7609"}, - {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a62cc23d754bb449d63ff35334acc9f5c02e6dae830d78dab4dd12b78a524f4"}, - {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f82125bc7203c5ae8633a7d5d20bcfdff0ba33e436e4ab0abc026a53a8960b7"}, - {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b67319b4aef1a6c56576ff544b67a2a6fbd7eaee485b241cabf53115e8908b8f"}, - {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:a8ef956fce64c8551221f395ba21d0724fed6b9b6242ca4f2f7beb4ce2f41997"}, - {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:0a01ce7d8479dce84fc03324e3b0c9c90b1ece9a9bb6a1b6c9025e7e4520e78c"}, - {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:91505d3ddebf268bb1588eb0f63821f738d20e1e7f05d3c647a5ca900288760b"}, - {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a3bcdde35d82ff385f4ede021df801b5c4a5bcdfb61ea87caabcebfc4945dc1b"}, - {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aea7c06667b987787c7d1f5e1dfcd70419b711cdb47d6b4bb4ad4b76777a0563"}, - {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a7fb111eef4d05909b82152721a59c1b14d0f365e2be4c742a473c5d7372f4f5"}, - {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:43d549b876ce64aa18b2328faff70f5877f8c6dede415f80a2f799d31644d776"}, - {file = "lxml-5.4.0-cp310-cp310-win32.whl", hash = "sha256:75133890e40d229d6c5837b0312abbe5bac1c342452cf0e12523477cd3aa21e7"}, - {file = "lxml-5.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:de5b4e1088523e2b6f730d0509a9a813355b7f5659d70eb4f319c76beea2e250"}, - {file = "lxml-5.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:98a3912194c079ef37e716ed228ae0dcb960992100461b704aea4e93af6b0bb9"}, - {file = "lxml-5.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ea0252b51d296a75f6118ed0d8696888e7403408ad42345d7dfd0d1e93309a7"}, - {file = "lxml-5.4.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92b69441d1bd39f4940f9eadfa417a25862242ca2c396b406f9272ef09cdcaa"}, - {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20e16c08254b9b6466526bc1828d9370ee6c0d60a4b64836bc3ac2917d1e16df"}, - {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7605c1c32c3d6e8c990dd28a0970a3cbbf1429d5b92279e37fda05fb0c92190e"}, - {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecf4c4b83f1ab3d5a7ace10bafcb6f11df6156857a3c418244cef41ca9fa3e44"}, - {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cef4feae82709eed352cd7e97ae062ef6ae9c7b5dbe3663f104cd2c0e8d94ba"}, - {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:df53330a3bff250f10472ce96a9af28628ff1f4efc51ccba351a8820bca2a8ba"}, - {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:aefe1a7cb852fa61150fcb21a8c8fcea7b58c4cb11fbe59c97a0a4b31cae3c8c"}, - {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ef5a7178fcc73b7d8c07229e89f8eb45b2908a9238eb90dcfc46571ccf0383b8"}, - {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d2ed1b3cb9ff1c10e6e8b00941bb2e5bb568b307bfc6b17dffbbe8be5eecba86"}, - {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:72ac9762a9f8ce74c9eed4a4e74306f2f18613a6b71fa065495a67ac227b3056"}, - {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f5cb182f6396706dc6cc1896dd02b1c889d644c081b0cdec38747573db88a7d7"}, - {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:3a3178b4873df8ef9457a4875703488eb1622632a9cee6d76464b60e90adbfcd"}, - {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e094ec83694b59d263802ed03a8384594fcce477ce484b0cbcd0008a211ca751"}, - {file = "lxml-5.4.0-cp311-cp311-win32.whl", hash = "sha256:4329422de653cdb2b72afa39b0aa04252fca9071550044904b2e7036d9d97fe4"}, - {file = "lxml-5.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd3be6481ef54b8cfd0e1e953323b7aa9d9789b94842d0e5b142ef4bb7999539"}, - {file = "lxml-5.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4"}, - {file = "lxml-5.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d"}, - {file = "lxml-5.4.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779"}, - {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e"}, - {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9"}, - {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5"}, - {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5"}, - {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4"}, - {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e"}, - {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7"}, - {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079"}, - {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20"}, - {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8"}, - {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f"}, - {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc"}, - {file = "lxml-5.4.0-cp312-cp312-win32.whl", hash = "sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f"}, - {file = "lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2"}, - {file = "lxml-5.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0"}, - {file = "lxml-5.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de"}, - {file = "lxml-5.4.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76"}, - {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d"}, - {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422"}, - {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551"}, - {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c"}, - {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff"}, - {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60"}, - {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8"}, - {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982"}, - {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61"}, - {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54"}, - {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b"}, - {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a"}, - {file = "lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82"}, - {file = "lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f"}, - {file = "lxml-5.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1b717b00a71b901b4667226bba282dd462c42ccf618ade12f9ba3674e1fabc55"}, - {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27a9ded0f0b52098ff89dd4c418325b987feed2ea5cc86e8860b0f844285d740"}, - {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7ce10634113651d6f383aa712a194179dcd496bd8c41e191cec2099fa09de5"}, - {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53370c26500d22b45182f98847243efb518d268374a9570409d2e2276232fd37"}, - {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c6364038c519dffdbe07e3cf42e6a7f8b90c275d4d1617a69bb59734c1a2d571"}, - {file = "lxml-5.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b12cb6527599808ada9eb2cd6e0e7d3d8f13fe7bbb01c6311255a15ded4c7ab4"}, - {file = "lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd"}, + {file = "mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59"}, + {file = "mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28"}, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +summary = "" +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.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, ] [[package]] name = "matplotlib-inline" version = "0.1.7" -requires_python = ">=3.8" -summary = "Inline Matplotlib backend for Jupyter" -groups = ["dev"] +summary = "" dependencies = [ "traitlets", ] @@ -1621,43 +1488,12 @@ files = [ {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"}, ] -[[package]] -name = "motor" -version = "3.7.0" -requires_python = ">=3.9" -summary = "Non-blocking MongoDB driver for Tornado or asyncio" -groups = ["default"] -dependencies = [ - "pymongo<5.0,>=4.9", -] -files = [ - {file = "motor-3.7.0-py3-none-any.whl", hash = "sha256:61bdf1afded179f008d423f98066348157686f25a90776ea155db5f47f57d605"}, - {file = "motor-3.7.0.tar.gz", hash = "sha256:0dfa1f12c812bd90819c519b78bed626b5a9dbb29bba079ccff2bfa8627e0fec"}, -] - -[[package]] -name = "motor" -version = "3.7.0" -extras = ["asyncio"] -requires_python = ">=3.9" -summary = "Non-blocking MongoDB driver for Tornado or asyncio" -groups = ["default"] -dependencies = [ - "motor==3.7.0", -] -files = [ - {file = "motor-3.7.0-py3-none-any.whl", hash = "sha256:61bdf1afded179f008d423f98066348157686f25a90776ea155db5f47f57d605"}, - {file = "motor-3.7.0.tar.gz", hash = "sha256:0dfa1f12c812bd90819c519b78bed626b5a9dbb29bba079ccff2bfa8627e0fec"}, -] - [[package]] name = "multidict" version = "6.4.3" -requires_python = ">=3.9" -summary = "multidict implementation" -groups = ["default"] +summary = "" dependencies = [ - "typing-extensions>=4.1.0; python_version < \"3.11\"", + "typing-extensions; python_full_version < \"3.11\"", ] files = [ {file = "multidict-6.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32a998bd8a64ca48616eac5a8c1cc4fa38fb244a3facf2eeb14abe186e0f6cc5"}, @@ -1752,11 +1588,9 @@ files = [ [[package]] name = "mypy-boto3-cloudformation" version = "1.38.0" -requires_python = ">=3.8" -summary = "Type annotations for boto3 CloudFormation 1.38.0 service generated with mypy-boto3-builder 8.10.1" -groups = ["default"] +summary = "" dependencies = [ - "typing-extensions; python_version < \"3.12\"", + "typing-extensions; python_full_version < \"3.12\"", ] files = [ {file = "mypy_boto3_cloudformation-1.38.0-py3-none-any.whl", hash = "sha256:a1411aa5875b737492aaac5f7e8ce450f034c18f972eb608a9eba6fe35837f6a"}, @@ -1766,11 +1600,9 @@ files = [ [[package]] name = "mypy-boto3-dynamodb" version = "1.38.2" -requires_python = ">=3.8" -summary = "Type annotations for boto3 DynamoDB 1.38.2 service generated with mypy-boto3-builder 8.10.1" -groups = ["default"] +summary = "" dependencies = [ - "typing-extensions; python_version < \"3.12\"", + "typing-extensions; python_full_version < \"3.12\"", ] files = [ {file = "mypy_boto3_dynamodb-1.38.2-py3-none-any.whl", hash = "sha256:c5167c27f7ce387354019e5a39eadd4b71fa0074f918447df1f77afa770c2e25"}, @@ -1780,11 +1612,9 @@ files = [ [[package]] name = "mypy-boto3-ec2" version = "1.38.0" -requires_python = ">=3.8" -summary = "Type annotations for boto3 EC2 1.38.0 service generated with mypy-boto3-builder 8.10.1" -groups = ["default"] +summary = "" dependencies = [ - "typing-extensions; python_version < \"3.12\"", + "typing-extensions; python_full_version < \"3.12\"", ] files = [ {file = "mypy_boto3_ec2-1.38.0-py3-none-any.whl", hash = "sha256:3256942c4de7d95d4b6bd0c81741daead7004753009c48ed8de5545c3292b7ba"}, @@ -1794,11 +1624,9 @@ files = [ [[package]] name = "mypy-boto3-lambda" version = "1.38.0" -requires_python = ">=3.8" -summary = "Type annotations for boto3 Lambda 1.38.0 service generated with mypy-boto3-builder 8.10.1" -groups = ["default"] +summary = "" dependencies = [ - "typing-extensions; python_version < \"3.12\"", + "typing-extensions; python_full_version < \"3.12\"", ] files = [ {file = "mypy_boto3_lambda-1.38.0-py3-none-any.whl", hash = "sha256:0dcb882826f61fd2751f6b98330b0e11085570654db85318aea018374ca88dc9"}, @@ -1808,11 +1636,9 @@ files = [ [[package]] name = "mypy-boto3-rds" version = "1.38.2" -requires_python = ">=3.8" -summary = "Type annotations for boto3 RDS 1.38.2 service generated with mypy-boto3-builder 8.10.1" -groups = ["default"] +summary = "" dependencies = [ - "typing-extensions; python_version < \"3.12\"", + "typing-extensions; python_full_version < \"3.12\"", ] files = [ {file = "mypy_boto3_rds-1.38.2-py3-none-any.whl", hash = "sha256:75d5124c3a49c7932d2218f82b7566c62160add2fd7aecd3f413ab47eb7d902a"}, @@ -1822,11 +1648,9 @@ files = [ [[package]] name = "mypy-boto3-s3" version = "1.38.0" -requires_python = ">=3.8" -summary = "Type annotations for boto3 S3 1.38.0 service generated with mypy-boto3-builder 8.10.1" -groups = ["default"] +summary = "" dependencies = [ - "typing-extensions; python_version < \"3.12\"", + "typing-extensions; python_full_version < \"3.12\"", ] files = [ {file = "mypy_boto3_s3-1.38.0-py3-none-any.whl", hash = "sha256:5cd9449df0ef6cf89e00e6fc9130a0ab641f703a23ab1d2146c394da058e8282"}, @@ -1836,23 +1660,32 @@ files = [ [[package]] name = "mypy-boto3-sqs" version = "1.38.0" -requires_python = ">=3.8" -summary = "Type annotations for boto3 SQS 1.38.0 service generated with mypy-boto3-builder 8.10.1" -groups = ["default"] +summary = "" dependencies = [ - "typing-extensions; python_version < \"3.12\"", + "typing-extensions; python_full_version < \"3.12\"", ] files = [ {file = "mypy_boto3_sqs-1.38.0-py3-none-any.whl", hash = "sha256:8e881c8492f6f51dcbe1cce9d9f05334f4b256b5843e227fa925e0f6e702b31d"}, {file = "mypy_boto3_sqs-1.38.0.tar.gz", hash = "sha256:39aebc121a2fe20f962fd83b617fd916003605d6f6851fdf195337a0aa428fe1"}, ] +[[package]] +name = "mysqlclient" +version = "2.2.7" +summary = "" +files = [ + {file = "mysqlclient-2.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:2e3c11f7625029d7276ca506f8960a7fd3c5a0a0122c9e7404e6a8fe961b3d22"}, + {file = "mysqlclient-2.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:a22d99d26baf4af68ebef430e3131bb5a9b722b79a9fcfac6d9bbf8a88800687"}, + {file = "mysqlclient-2.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:4b4c0200890837fc64014cc938ef2273252ab544c1b12a6c1d674c23943f3f2e"}, + {file = "mysqlclient-2.2.7-cp313-cp313-win_amd64.whl", hash = "sha256:201a6faa301011dd07bca6b651fe5aaa546d7c9a5426835a06c3172e1056a3c5"}, + {file = "mysqlclient-2.2.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:92af368ed9c9144737af569c86d3b6c74a012a6f6b792eb868384787b52bb585"}, + {file = "mysqlclient-2.2.7.tar.gz", hash = "sha256:24ae22b59416d5fcce7e99c9d37548350b4565baac82f95e149cac6ce4163845"}, +] + [[package]] name = "numpy" version = "2.2.5" -requires_python = ">=3.10" -summary = "Fundamental package for array computing in Python" -groups = ["default"] +summary = "" files = [ {file = "numpy-2.2.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1f4a922da1729f4c40932b2af4fe84909c7a6e167e6e99f71838ce3a29f3fe26"}, {file = "numpy-2.2.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b6f91524d31b34f4a5fee24f5bc16dcd1491b668798b6d85585d836c1e633a6a"}, @@ -1914,12 +1747,10 @@ files = [ [[package]] name = "ollama" version = "0.4.8" -requires_python = "<4.0,>=3.8" -summary = "The official Python client for Ollama." -groups = ["default"] +summary = "" dependencies = [ - "httpx<0.29,>=0.27", - "pydantic<3.0.0,>=2.9.0", + "httpx", + "pydantic", ] files = [ {file = "ollama-0.4.8-py3-none-any.whl", hash = "sha256:04312af2c5e72449aaebac4a2776f52ef010877c554103419d3f36066fe8af4c"}, @@ -1929,18 +1760,16 @@ files = [ [[package]] name = "openai" version = "1.76.0" -requires_python = ">=3.8" -summary = "The official Python library for the openai API" -groups = ["default"] +summary = "" dependencies = [ - "anyio<5,>=3.5.0", - "distro<2,>=1.7.0", - "httpx<1,>=0.23.0", - "jiter<1,>=0.4.0", - "pydantic<3,>=1.9.0", + "anyio", + "distro", + "httpx", + "jiter", + "pydantic", "sniffio", - "tqdm>4", - "typing-extensions<5,>=4.11", + "tqdm", + "typing-extensions", ] files = [ {file = "openai-1.76.0-py3-none-any.whl", hash = "sha256:a712b50e78cf78e6d7b2a8f69c4978243517c2c36999756673e07a14ce37dc0a"}, @@ -1950,9 +1779,7 @@ files = [ [[package]] name = "openpyxl" version = "3.1.5" -requires_python = ">=3.8" -summary = "A Python library to read/write Excel 2010 xlsx/xlsm files" -groups = ["default"] +summary = "" dependencies = [ "et-xmlfile", ] @@ -1964,9 +1791,7 @@ files = [ [[package]] name = "orjson" version = "3.10.18" -requires_python = ">=3.9" -summary = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" -groups = ["default"] +summary = "" files = [ {file = "orjson-3.10.18-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a45e5d68066b408e4bc383b6e4ef05e717c65219a9e1390abc6155a520cac402"}, {file = "orjson-3.10.18-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be3b9b143e8b9db05368b13b04c84d37544ec85bb97237b3a923f076265ec89c"}, @@ -2032,11 +1857,9 @@ files = [ [[package]] name = "outcome" version = "1.3.0.post0" -requires_python = ">=3.7" -summary = "Capture the outcome of Python function calls." -groups = ["default"] +summary = "" dependencies = [ - "attrs>=19.2.0", + "attrs", ] files = [ {file = "outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"}, @@ -2046,9 +1869,7 @@ files = [ [[package]] name = "packaging" version = "25.0" -requires_python = ">=3.8" -summary = "Core utilities for Python packages" -groups = ["default", "dev"] +summary = "" files = [ {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, @@ -2057,16 +1878,12 @@ files = [ [[package]] name = "pandas" version = "2.2.3" -requires_python = ">=3.9" -summary = "Powerful data structures for data analysis, time series, and statistics" -groups = ["default"] +summary = "" dependencies = [ - "numpy>=1.22.4; python_version < \"3.11\"", - "numpy>=1.23.2; python_version == \"3.11\"", - "numpy>=1.26.0; python_version >= \"3.12\"", - "python-dateutil>=2.8.2", - "pytz>=2020.1", - "tzdata>=2022.7", + "numpy", + "python-dateutil", + "pytz", + "tzdata", ] files = [ {file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"}, @@ -2109,8 +1926,7 @@ files = [ [[package]] name = "parse" version = "1.20.2" -summary = "parse() is the opposite of format()" -groups = ["default"] +summary = "" files = [ {file = "parse-1.20.2-py2.py3-none-any.whl", hash = "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558"}, {file = "parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce"}, @@ -2119,9 +1935,7 @@ files = [ [[package]] name = "parso" version = "0.8.4" -requires_python = ">=3.6" -summary = "A Python Parser" -groups = ["dev"] +summary = "" files = [ {file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"}, {file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"}, @@ -2130,8 +1944,7 @@ files = [ [[package]] name = "passlib" version = "1.7.4" -summary = "comprehensive password hashing framework supporting over 30 schemes" -groups = ["default"] +summary = "" files = [ {file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"}, {file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"}, @@ -2141,10 +1954,9 @@ files = [ name = "passlib" version = "1.7.4" extras = ["bcrypt"] -summary = "comprehensive password hashing framework supporting over 30 schemes" -groups = ["default"] +summary = "" dependencies = [ - "bcrypt>=3.1.0", + "bcrypt", "passlib==1.7.4", ] files = [ @@ -2155,11 +1967,9 @@ files = [ [[package]] name = "pexpect" version = "4.9.0" -summary = "Pexpect allows easy control of interactive console applications." -groups = ["dev"] -marker = "sys_platform != \"win32\" and sys_platform != \"emscripten\"" +summary = "" dependencies = [ - "ptyprocess>=0.5", + "ptyprocess", ] files = [ {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, @@ -2169,9 +1979,7 @@ files = [ [[package]] name = "platformdirs" version = "4.3.8" -requires_python = ">=3.9" -summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -groups = ["default"] +summary = "" files = [ {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, @@ -2180,12 +1988,10 @@ files = [ [[package]] name = "playwright" version = "1.52.0" -requires_python = ">=3.9" -summary = "A high-level API to automate web browsers" -groups = ["default"] +summary = "" dependencies = [ - "greenlet<4.0.0,>=3.1.1", - "pyee<14,>=13", + "greenlet", + "pyee", ] files = [ {file = "playwright-1.52.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:19b2cb9d4794062008a635a99bd135b03ebb782d460f96534a91cb583f549512"}, @@ -2201,9 +2007,7 @@ files = [ [[package]] name = "pluggy" version = "1.5.0" -requires_python = ">=3.8" -summary = "plugin and hook calling mechanisms for python" -groups = ["default", "dev"] +summary = "" files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -2212,9 +2016,7 @@ files = [ [[package]] name = "prompt-toolkit" version = "3.0.51" -requires_python = ">=3.8" -summary = "Library for building powerful interactive command lines in Python" -groups = ["dev"] +summary = "" dependencies = [ "wcwidth", ] @@ -2226,9 +2028,7 @@ files = [ [[package]] name = "propcache" version = "0.3.1" -requires_python = ">=3.9" -summary = "Accelerated property cache" -groups = ["default"] +summary = "" files = [ {file = "propcache-0.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f27785888d2fdd918bc36de8b8739f2d6c791399552333721b58193f68ea3e98"}, {file = "propcache-0.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4e89cde74154c7b5957f87a355bb9c8ec929c167b59c83d90654ea36aeb6180"}, @@ -2317,20 +2117,71 @@ files = [ [[package]] name = "proxy-py" version = "2.4.10" -requires_python = ">=3.6" -summary = "\\u26a1 Fast \\u2022 \\U0001fab6 Lightweight \\u2022 \\U0001f51f Dependency \\u2022 \\U0001f50c Pluggable \\u2022 \\U0001f608 TLS interception \\u2022 \\U0001f512 DNS-over-HTTPS \\u2022 \\U0001f525 Poor Mans VPN \\u2022 \\u23ea Reverse & \\u23e9 Forward \\u2022 \\U0001f46e\\U0001f3ff Proxy Server framework \\u2022 \\U0001f310 Web Server framework \\u2022 \\u27b5 \\u27b6 \\u27b7 \\u27a0 PubSub framework \\u2022 \\U0001f477 Work acceptor & executor framework." -groups = ["default"] +summary = "" files = [ {file = "proxy.py-2.4.10-py3-none-any.whl", hash = "sha256:ef3a31f6ef3be6ff78559c0e68198523bfe2fb1e820bb16686750c1bb5baf9e8"}, {file = "proxy_py-2.4.10.tar.gz", hash = "sha256:41b9e9d3aae6f80e2304d3726e8e9c583a510d8de224eada53d115f48a63a9ce"}, ] +[[package]] +name = "psycopg2-binary" +version = "2.9.10" +summary = "" +files = [ + {file = "psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:3e9c76f0ac6f92ecfc79516a8034a544926430f7b080ec5a0537bca389ee0906"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48b338f08d93e7be4ab2b5f1dbe69dc5e9ef07170fe1f86514422076d9c010d0"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4152f8f76d2023aac16285576a9ecd2b11a9895373a1f10fd9db54b3ff06b4"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32581b3020c72d7a421009ee1c6bf4a131ef5f0a968fab2e2de0c9d2bb4577f1"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e984839e75e0b60cfe75e351db53d6db750b00de45644c5d1f7ee5d1f34a1ce5"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c4745a90b78e51d9ba06e2088a2fe0c693ae19cc8cb051ccda44e8df8a6eb53"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-win32.whl", hash = "sha256:e5720a5d25e3b99cd0dc5c8a440570469ff82659bb09431c1439b92caf184d3b"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-win_amd64.whl", hash = "sha256:3c18f74eb4386bf35e92ab2354a12c17e5eb4d9798e4c0ad3a00783eae7cd9f1"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-win32.whl", hash = "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142"}, +] + [[package]] name = "ptyprocess" version = "0.7.0" -summary = "Run a subprocess in a pseudo terminal" -groups = ["dev"] -marker = "sys_platform != \"win32\" and sys_platform != \"emscripten\"" +summary = "" files = [ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, @@ -2339,8 +2190,7 @@ files = [ [[package]] name = "pure-eval" version = "0.2.3" -summary = "Safely evaluate AST nodes without side effects" -groups = ["dev"] +summary = "" files = [ {file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"}, {file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"}, @@ -2349,8 +2199,7 @@ files = [ [[package]] name = "pyasn1" version = "0.4.8" -summary = "ASN.1 types and codecs" -groups = ["default"] +summary = "" files = [ {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, @@ -2359,9 +2208,7 @@ files = [ [[package]] name = "pycparser" version = "2.22" -requires_python = ">=3.8" -summary = "C parser in Python" -groups = ["default"] +summary = "" files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, @@ -2370,14 +2217,12 @@ files = [ [[package]] name = "pydantic" version = "2.11.3" -requires_python = ">=3.9" -summary = "Data validation using Python type hints" -groups = ["default"] +summary = "" dependencies = [ - "annotated-types>=0.6.0", - "pydantic-core==2.33.1", - "typing-extensions>=4.12.2", - "typing-inspection>=0.4.0", + "annotated-types", + "pydantic-core", + "typing-extensions", + "typing-inspection", ] files = [ {file = "pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f"}, @@ -2387,11 +2232,9 @@ files = [ [[package]] name = "pydantic-core" version = "2.33.1" -requires_python = ">=3.9" -summary = "Core functionality for Pydantic validation and serialization" -groups = ["default"] +summary = "" dependencies = [ - "typing-extensions!=4.7.0,>=4.6.0", + "typing-extensions", ] files = [ {file = "pydantic_core-2.33.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3077cfdb6125cc8dab61b155fdd714663e401f0e6883f9632118ec12cf42df26"}, @@ -2477,11 +2320,9 @@ files = [ name = "pydantic" version = "2.11.3" extras = ["email"] -requires_python = ">=3.9" -summary = "Data validation using Python type hints" -groups = ["default"] +summary = "" dependencies = [ - "email-validator>=2.0.0", + "email-validator", "pydantic==2.11.3", ] files = [ @@ -2492,13 +2333,7 @@ files = [ [[package]] name = "pydivert" version = "2.1.0" -summary = "Python binding to windivert driver" -groups = ["default"] -marker = "sys_platform == \"win32\"" -dependencies = [ - "enum34>=1.1.6; python_version == \"2.7\" or python_version == \"3.3\"", - "win-inet-pton>=1.0.1; python_version == \"2.7\" or python_version == \"3.3\"", -] +summary = "" files = [ {file = "pydivert-2.1.0-py2.py3-none-any.whl", hash = "sha256:382db488e3c37c03ec9ec94e061a0b24334d78dbaeebb7d4e4d32ce4355d9da1"}, {file = "pydivert-2.1.0.tar.gz", hash = "sha256:f0e150f4ff591b78e35f514e319561dadff7f24a82186a171dd4d465483de5b4"}, @@ -2507,9 +2342,7 @@ files = [ [[package]] name = "pyee" version = "13.0.0" -requires_python = ">=3.8" -summary = "A rough port of Node.js's EventEmitter to Python with a few tricks of its own" -groups = ["default"] +summary = "" dependencies = [ "typing-extensions", ] @@ -2521,9 +2354,7 @@ files = [ [[package]] name = "pygments" version = "2.19.1" -requires_python = ">=3.8" -summary = "Pygments is a syntax highlighting package written in Python." -groups = ["dev"] +summary = "" files = [ {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, @@ -2532,11 +2363,9 @@ files = [ [[package]] name = "pymongo" version = "4.12.0" -requires_python = ">=3.9" -summary = "Python driver for MongoDB " -groups = ["default"] +summary = "" dependencies = [ - "dnspython<3.0.0,>=1.16.0", + "dnspython", ] files = [ {file = "pymongo-4.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e23d9b5e8d2dfc3ac0540966e93008e471345ec9a2797b77be551e64b70fc8ee"}, @@ -2587,13 +2416,19 @@ files = [ {file = "pymongo-4.12.0.tar.gz", hash = "sha256:d9f74a5cf3fccdb72211e33e07a6c05ac09cd0d7c99d21db5c2473fcfdd03152"}, ] +[[package]] +name = "pymysql" +version = "1.1.1" +summary = "" +files = [ + {file = "PyMySQL-1.1.1-py3-none-any.whl", hash = "sha256:4de15da4c61dc132f4fb9ab763063e693d521a80fd0e87943b9a453dd4c19d6c"}, + {file = "pymysql-1.1.1.tar.gz", hash = "sha256:e127611aaf2b417403c60bf4dc570124aeb4a57f5f37b8e95ae399a42f904cd0"}, +] + [[package]] name = "pyobjc-core" version = "11.0" -requires_python = ">=3.8" -summary = "Python<->ObjC Interoperability Module" -groups = ["default"] -marker = "sys_platform == \"darwin\"" +summary = "" files = [ {file = "pyobjc_core-11.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:10866b3a734d47caf48e456eea0d4815c2c9b21856157db5917b61dee06893a1"}, {file = "pyobjc_core-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:50675c0bb8696fe960a28466f9baf6943df2928a1fd85625d678fa2f428bd0bd"}, @@ -2606,12 +2441,9 @@ files = [ [[package]] name = "pyobjc-framework-cocoa" version = "11.0" -requires_python = ">=3.9" -summary = "Wrappers for the Cocoa frameworks on macOS" -groups = ["default"] -marker = "sys_platform == \"darwin\"" +summary = "" dependencies = [ - "pyobjc-core>=11.0", + "pyobjc-core", ] files = [ {file = "pyobjc_framework_Cocoa-11.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fbc65f260d617d5463c7fb9dbaaffc23c9a4fabfe3b1a50b039b61870b8daefd"}, @@ -2625,12 +2457,10 @@ files = [ [[package]] name = "pyopenssl" version = "25.0.0" -requires_python = ">=3.7" -summary = "Python wrapper module around the OpenSSL library" -groups = ["default"] +summary = "" dependencies = [ - "cryptography<45,>=41.0.5", - "typing-extensions>=4.9; python_version < \"3.13\" and python_version >= \"3.8\"", + "cryptography", + "typing-extensions; python_full_version < \"3.13\"", ] files = [ {file = "pyOpenSSL-25.0.0-py3-none-any.whl", hash = "sha256:424c247065e46e76a37411b9ab1782541c23bb658bf003772c3405fbaa128e90"}, @@ -2640,9 +2470,7 @@ files = [ [[package]] name = "pyparsing" version = "3.2.3" -requires_python = ">=3.9" -summary = "pyparsing module - Classes and methods to define and execute parsing grammars" -groups = ["default"] +summary = "" files = [ {file = "pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf"}, {file = "pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be"}, @@ -2651,9 +2479,7 @@ files = [ [[package]] name = "pyppeteer" version = "0.0.25" -requires_python = ">=3.5" -summary = "Headless chrome/chromium automation library (unofficial port of puppeteer)" -groups = ["default"] +summary = "" dependencies = [ "appdirs", "pyee", @@ -2668,11 +2494,10 @@ files = [ [[package]] name = "pyquery" version = "2.0.1" -summary = "A jquery-like library for python" -groups = ["default"] +summary = "" dependencies = [ - "cssselect>=1.2.0", - "lxml>=2.1", + "cssselect", + "lxml", ] files = [ {file = "pyquery-2.0.1-py3-none-any.whl", hash = "sha256:aedfa0bd0eb9afc94b3ddbec8f375a6362b32bc9662f46e3e0d866483f4771b0"}, @@ -2682,9 +2507,7 @@ files = [ [[package]] name = "pysocks" version = "1.7.1" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -summary = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." -groups = ["default"] +summary = "" files = [ {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, @@ -2693,16 +2516,14 @@ files = [ [[package]] name = "pytest" version = "8.3.5" -requires_python = ">=3.8" -summary = "pytest: simple powerful testing with Python" -groups = ["default", "dev"] +summary = "" dependencies = [ "colorama; sys_platform == \"win32\"", - "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", + "exceptiongroup; python_full_version < \"3.11\"", "iniconfig", "packaging", - "pluggy<2,>=1.5", - "tomli>=1; python_version < \"3.11\"", + "pluggy", + "tomli; python_full_version < \"3.11\"", ] files = [ {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, @@ -2712,12 +2533,9 @@ files = [ [[package]] name = "pytest-asyncio" version = "0.26.0" -requires_python = ">=3.9" -summary = "Pytest support for asyncio" -groups = ["default"] +summary = "" dependencies = [ - "pytest<9,>=8.2", - "typing-extensions>=4.12; python_version < \"3.10\"", + "pytest", ] files = [ {file = "pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0"}, @@ -2727,11 +2545,9 @@ files = [ [[package]] name = "python-dateutil" version = "2.9.0.post0" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -summary = "Extensions to the standard Python datetime module" -groups = ["default"] +summary = "" dependencies = [ - "six>=1.5", + "six", ] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, @@ -2741,9 +2557,7 @@ files = [ [[package]] name = "python-dotenv" version = "1.1.0" -requires_python = ">=3.9" -summary = "Read key-value pairs from a .env file and set them as environment variables" -groups = ["default"] +summary = "" files = [ {file = "python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d"}, {file = "python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5"}, @@ -2752,12 +2566,11 @@ files = [ [[package]] name = "python-jose" version = "3.4.0" -summary = "JOSE implementation in Python" -groups = ["default"] +summary = "" dependencies = [ - "ecdsa!=0.15", - "pyasn1<0.5.0,>=0.4.1", - "rsa!=4.1.1,!=4.4,<5.0,>=4.0", + "ecdsa", + "pyasn1", + "rsa", ] files = [ {file = "python-jose-3.4.0.tar.gz", hash = "sha256:9a9a40f418ced8ecaf7e3b28d69887ceaa76adad3bcaa6dae0d9e596fec1d680"}, @@ -2768,10 +2581,9 @@ files = [ name = "python-jose" version = "3.4.0" extras = ["cryptography"] -summary = "JOSE implementation in Python" -groups = ["default"] +summary = "" dependencies = [ - "cryptography>=3.4.0", + "cryptography", "python-jose==3.4.0", ] files = [ @@ -2782,17 +2594,15 @@ files = [ [[package]] name = "python-keycloak" version = "5.5.0" -requires_python = "<4.0,>=3.9" -summary = "python-keycloak is a Python package providing access to the Keycloak API." -groups = ["default"] +summary = "" dependencies = [ - "aiofiles>=24.1.0", - "async-property>=0.2.2", - "deprecation>=2.1.0", - "httpx>=0.23.2", - "jwcrypto>=1.5.4", - "requests-toolbelt>=0.6.0", - "requests>=2.20.0", + "aiofiles", + "async-property", + "deprecation", + "httpx", + "jwcrypto", + "requests", + "requests-toolbelt", ] files = [ {file = "python_keycloak-5.5.0-py3-none-any.whl", hash = "sha256:325ada6c354703b58a944efe5cf711798459bf61e52cdd89b33aad30f07532b2"}, @@ -2802,9 +2612,7 @@ files = [ [[package]] name = "python-multipart" version = "0.0.20" -requires_python = ">=3.8" -summary = "A streaming multipart parser for Python" -groups = ["default"] +summary = "" files = [ {file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"}, {file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"}, @@ -2813,8 +2621,7 @@ files = [ [[package]] name = "pytz" version = "2025.2" -summary = "World timezone definitions, modern and historical" -groups = ["default"] +summary = "" files = [ {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, @@ -2823,9 +2630,7 @@ files = [ [[package]] name = "pywin32" version = "310" -summary = "Python for Window Extensions" -groups = ["default"] -marker = "sys_platform == \"win32\"" +summary = "" files = [ {file = "pywin32-310-cp310-cp310-win32.whl", hash = "sha256:6dd97011efc8bf51d6793a82292419eba2c71cf8e7250cfac03bba284454abc1"}, {file = "pywin32-310-cp310-cp310-win_amd64.whl", hash = "sha256:c3e78706e4229b915a0821941a84e7ef420bf2b77e08c9dae3c76fd03fd2ae3d"}, @@ -2844,9 +2649,7 @@ files = [ [[package]] name = "pyyaml" version = "6.0.2" -requires_python = ">=3.8" -summary = "YAML parser and emitter for Python" -groups = ["default"] +summary = "" 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"}, @@ -2890,14 +2693,12 @@ files = [ [[package]] name = "requests" version = "2.32.3" -requires_python = ">=3.8" -summary = "Python HTTP for Humans." -groups = ["default"] +summary = "" dependencies = [ - "certifi>=2017.4.17", - "charset-normalizer<4,>=2", - "idna<4,>=2.5", - "urllib3<3,>=1.21.1", + "certifi", + "charset-normalizer", + "idna", + "urllib3", ] files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, @@ -2907,14 +2708,12 @@ files = [ [[package]] name = "requests-html" version = "0.10.0" -requires_python = ">=3.6.0" -summary = "HTML Parsing for Humans." -groups = ["default"] +summary = "" dependencies = [ "bs4", "fake-useragent", "parse", - "pyppeteer>=0.0.14", + "pyppeteer", "pyquery", "requests", "w3lib", @@ -2927,11 +2726,9 @@ files = [ [[package]] name = "requests-toolbelt" version = "1.0.0" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -summary = "A utility belt for advanced users of python-requests" -groups = ["default"] +summary = "" dependencies = [ - "requests<3.0.0,>=2.0.1", + "requests", ] files = [ {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, @@ -2941,11 +2738,9 @@ files = [ [[package]] name = "rsa" version = "4.9.1" -requires_python = "<4,>=3.6" -summary = "Pure-Python RSA implementation" -groups = ["default"] +summary = "" dependencies = [ - "pyasn1>=0.1.3", + "pyasn1", ] files = [ {file = "rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762"}, @@ -2955,11 +2750,9 @@ files = [ [[package]] name = "s3transfer" version = "0.12.0" -requires_python = ">=3.9" -summary = "An Amazon S3 Transfer Manager" -groups = ["default"] +summary = "" dependencies = [ - "botocore<2.0a.0,>=1.37.4", + "botocore", ] files = [ {file = "s3transfer-0.12.0-py3-none-any.whl", hash = "sha256:35b314d7d82865756edab59f7baebc6b477189e6ab4c53050e28c1de4d9cce18"}, @@ -2969,13 +2762,10 @@ files = [ [[package]] name = "screeninfo" version = "0.8.1" -requires_python = ">=3.6.2,<4.0.0" -summary = "Fetch location and size of physical screens." -groups = ["default"] +summary = "" dependencies = [ - "Cython; sys_platform == \"darwin\"", - "dataclasses; python_version < \"3.7\"", - "pyobjc-framework-Cocoa; sys_platform == \"darwin\"", + "cython; sys_platform == \"darwin\"", + "pyobjc-framework-cocoa; sys_platform == \"darwin\"", ] files = [ {file = "screeninfo-0.8.1-py3-none-any.whl", hash = "sha256:e97d6b173856edcfa3bd282f81deb528188aff14b11ec3e195584e7641be733c"}, @@ -2985,16 +2775,14 @@ files = [ [[package]] name = "selenium" version = "4.32.0" -requires_python = ">=3.9" -summary = "Official Python bindings for Selenium WebDriver" -groups = ["default"] +summary = "" dependencies = [ - "certifi>=2021.10.8", - "trio-websocket~=0.9", - "trio~=0.17", - "typing-extensions~=4.9", - "urllib3[socks]<3,>=1.26", - "websocket-client~=1.8", + "certifi", + "trio", + "trio-websocket", + "typing-extensions", + "urllib3", + "websocket-client", ] files = [ {file = "selenium-4.32.0-py3-none-any.whl", hash = "sha256:c4d9613f8a45693d61530c9660560fadb52db7d730237bc788ddedf442391f97"}, @@ -3004,25 +2792,22 @@ files = [ [[package]] name = "selenium-wire" version = "5.1.0" -requires_python = ">=3.6" -summary = "Extends Selenium to give you the ability to inspect requests made by the browser." -groups = ["default"] +summary = "" dependencies = [ - "blinker>=1.4", - "brotli>=1.0.9", - "certifi>=2019.9.11", - "dataclasses>=0.7; python_version == \"3.6\"", - "h2>=4.0; python_version >= \"3.6.0\"", - "hyperframe>=6.0; python_version >= \"3.6.0\"", - "kaitaistruct>=0.7", - "pyOpenSSL>=22.0.0", - "pyasn1>=0.3.1", - "pydivert>=2.0.3; sys_platform == \"win32\"", - "pyparsing>=2.4.2", - "pysocks>=1.7.1", - "selenium>=4.0.0", - "wsproto>=0.14", - "zstandard>=0.14.1", + "blinker", + "brotli", + "certifi", + "h2", + "hyperframe", + "kaitaistruct", + "pyasn1", + "pydivert; sys_platform == \"win32\"", + "pyopenssl", + "pyparsing", + "pysocks", + "selenium", + "wsproto", + "zstandard", ] files = [ {file = "selenium-wire-5.1.0.tar.gz", hash = "sha256:b1cd4eae44d9959381abe3bb186472520d063c658e279f98555def3d4e6dd29b"}, @@ -3032,9 +2817,7 @@ files = [ [[package]] name = "setuptools" version = "79.0.1" -requires_python = ">=3.9" -summary = "Easily download, build, install, upgrade, and uninstall Python packages" -groups = ["default"] +summary = "" files = [ {file = "setuptools-79.0.1-py3-none-any.whl", hash = "sha256:e147c0549f27767ba362f9da434eab9c5dc0045d5304feb602a0af001089fc51"}, {file = "setuptools-79.0.1.tar.gz", hash = "sha256:128ce7b8f33c3079fd1b067ecbb4051a66e8526e7b65f6cec075dfc650ddfa88"}, @@ -3043,9 +2826,7 @@ files = [ [[package]] name = "six" version = "1.17.0" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -summary = "Python 2 and 3 compatibility utilities" -groups = ["default"] +summary = "" files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -3054,9 +2835,7 @@ files = [ [[package]] name = "sniffio" version = "1.3.1" -requires_python = ">=3.7" -summary = "Sniff out which async library your code is running under" -groups = ["default"] +summary = "" files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -3065,8 +2844,7 @@ files = [ [[package]] name = "sortedcontainers" version = "2.4.0" -summary = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" -groups = ["default"] +summary = "" files = [ {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, @@ -3075,22 +2853,64 @@ files = [ [[package]] name = "soupsieve" version = "2.7" -requires_python = ">=3.8" -summary = "A modern CSS selector implementation for Beautiful Soup." -groups = ["default"] +summary = "" files = [ {file = "soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4"}, {file = "soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a"}, ] +[[package]] +name = "sqlalchemy" +version = "2.0.41" +summary = "" +dependencies = [ + "greenlet; python_full_version < \"3.14\" and (platform_machine == \"AMD64\" or platform_machine == \"WIN32\" or platform_machine == \"aarch64\" or platform_machine == \"amd64\" or platform_machine == \"ppc64le\" or platform_machine == \"win32\" or platform_machine == \"x86_64\")", + "typing-extensions", +] +files = [ + {file = "sqlalchemy-2.0.41-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b1f09b6821406ea1f94053f346f28f8215e293344209129a9c0fcc3578598d7b"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1936af879e3db023601196a1684d28e12f19ccf93af01bf3280a3262c4b6b4e5"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2ac41acfc8d965fb0c464eb8f44995770239668956dc4cdf502d1b1ffe0d747"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81c24e0c0fde47a9723c81d5806569cddef103aebbf79dbc9fcbb617153dea30"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23a8825495d8b195c4aa9ff1c430c28f2c821e8c5e2d98089228af887e5d7e29"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:60c578c45c949f909a4026b7807044e7e564adf793537fc762b2489d522f3d11"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-win32.whl", hash = "sha256:118c16cd3f1b00c76d69343e38602006c9cfb9998fa4f798606d28d63f23beda"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-win_amd64.whl", hash = "sha256:7492967c3386df69f80cf67efd665c0f667cee67032090fe01d7d74b0e19bb08"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6375cd674fe82d7aa9816d1cb96ec592bac1726c11e0cafbf40eeee9a4516b5f"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f8c9fdd15a55d9465e590a402f42082705d66b05afc3ffd2d2eb3c6ba919560"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32f9dc8c44acdee06c8fc6440db9eae8b4af8b01e4b1aee7bdd7241c22edff4f"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c11ceb9a1f482c752a71f203a81858625d8df5746d787a4786bca4ffdf71c6"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:911cc493ebd60de5f285bcae0491a60b4f2a9f0f5c270edd1c4dbaef7a38fc04"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03968a349db483936c249f4d9cd14ff2c296adfa1290b660ba6516f973139582"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-win32.whl", hash = "sha256:293cd444d82b18da48c9f71cd7005844dbbd06ca19be1ccf6779154439eec0b8"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-win_amd64.whl", hash = "sha256:3d3549fc3e40667ec7199033a4e40a2f669898a00a7b18a931d3efb4c7900504"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:81f413674d85cfd0dfcd6512e10e0f33c19c21860342a4890c3a2b59479929f9"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:598d9ebc1e796431bbd068e41e4de4dc34312b7aa3292571bb3674a0cb415dd1"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a104c5694dfd2d864a6f91b0956eb5d5883234119cb40010115fd45a16da5e70"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6145afea51ff0af7f2564a05fa95eb46f542919e6523729663a5d285ecb3cf5e"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b46fa6eae1cd1c20e6e6f44e19984d438b6b2d8616d21d783d150df714f44078"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41836fe661cc98abfae476e14ba1906220f92c4e528771a8a3ae6a151242d2ae"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-win32.whl", hash = "sha256:a8808d5cf866c781150d36a3c8eb3adccfa41a8105d031bf27e92c251e3969d6"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-win_amd64.whl", hash = "sha256:5b14e97886199c1f52c14629c11d90c11fbb09e9334fa7bb5f6d068d9ced0ce0"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c153265408d18de4cc5ded1941dcd8315894572cddd3c58df5d5b5705b3fa28d"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f67766965996e63bb46cfbf2ce5355fc32d9dd3b8ad7e536a920ff9ee422e23"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-win32.whl", hash = "sha256:bfc9064f6658a3d1cadeaa0ba07570b83ce6801a1314985bf98ec9b95d74e15f"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-win_amd64.whl", hash = "sha256:82ca366a844eb551daff9d2e6e7a9e5e76d2612c8564f58db6c19a726869c1df"}, + {file = "sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576"}, + {file = "sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9"}, +] + [[package]] name = "stack-data" version = "0.6.3" -summary = "Extract data from python stack frames and tracebacks for informative displays" -groups = ["dev"] +summary = "" dependencies = [ - "asttokens>=2.1.0", - "executing>=1.2.0", + "asttokens", + "executing", "pure-eval", ] files = [ @@ -3101,12 +2921,9 @@ files = [ [[package]] name = "starlette" version = "0.46.2" -requires_python = ">=3.9" -summary = "The little ASGI library that shines." -groups = ["default"] +summary = "" dependencies = [ - "anyio<5,>=3.6.2", - "typing-extensions>=3.10.0; python_version < \"3.10\"", + "anyio", ] files = [ {file = "starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35"}, @@ -3116,10 +2933,7 @@ files = [ [[package]] name = "tomli" version = "2.2.1" -requires_python = ">=3.8" -summary = "A lil' TOML parser" -groups = ["default", "dev"] -marker = "python_version < \"3.11\"" +summary = "" 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"}, @@ -3158,11 +2972,9 @@ files = [ [[package]] name = "tqdm" version = "4.67.1" -requires_python = ">=3.7" -summary = "Fast, Extensible Progress Meter" -groups = ["default"] +summary = "" dependencies = [ - "colorama; platform_system == \"Windows\"", + "colorama; sys_platform == \"win32\"", ] files = [ {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, @@ -3172,9 +2984,7 @@ files = [ [[package]] name = "traitlets" version = "5.14.3" -requires_python = ">=3.8" -summary = "Traitlets Python configuration system" -groups = ["dev"] +summary = "" files = [ {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, @@ -3183,16 +2993,14 @@ files = [ [[package]] name = "trio" version = "0.30.0" -requires_python = ">=3.9" -summary = "A friendly Python library for async concurrency and I/O" -groups = ["default"] +summary = "" dependencies = [ - "attrs>=23.2.0", - "cffi>=1.14; os_name == \"nt\" and implementation_name != \"pypy\"", - "exceptiongroup; python_version < \"3.11\"", + "attrs", + "cffi; implementation_name != \"pypy\" and os_name == \"nt\"", + "exceptiongroup; python_full_version < \"3.11\"", "idna", "outcome", - "sniffio>=1.3.0", + "sniffio", "sortedcontainers", ] files = [ @@ -3203,14 +3011,12 @@ files = [ [[package]] name = "trio-websocket" version = "0.12.2" -requires_python = ">=3.8" -summary = "WebSocket library for Trio" -groups = ["default"] +summary = "" dependencies = [ - "exceptiongroup; python_version < \"3.11\"", - "outcome>=1.2.0", - "trio>=0.11", - "wsproto>=0.14", + "exceptiongroup; python_full_version < \"3.11\"", + "outcome", + "trio", + "wsproto", ] files = [ {file = "trio_websocket-0.12.2-py3-none-any.whl", hash = "sha256:df605665f1db533f4a386c94525870851096a223adcb97f72a07e8b4beba45b6"}, @@ -3220,9 +3026,7 @@ files = [ [[package]] name = "types-awscrt" version = "0.26.1" -requires_python = ">=3.8" -summary = "Type annotations and code completion for awscrt" -groups = ["default"] +summary = "" files = [ {file = "types_awscrt-0.26.1-py3-none-any.whl", hash = "sha256:176d320a26990efc057d4bf71396e05be027c142252ac48cc0d87aaea0704280"}, {file = "types_awscrt-0.26.1.tar.gz", hash = "sha256:aca96f889b3745c0e74f42f08f277fed3bf6e9baa2cf9b06a36f78d77720e504"}, @@ -3231,9 +3035,7 @@ files = [ [[package]] name = "types-s3transfer" version = "0.12.0" -requires_python = ">=3.8" -summary = "Type annotations and code completion for s3transfer" -groups = ["default"] +summary = "" files = [ {file = "types_s3transfer-0.12.0-py3-none-any.whl", hash = "sha256:101bbc5b7f00b71512374df881f480fc6bf63c948b5098ab024bf3370fbfb0e8"}, {file = "types_s3transfer-0.12.0.tar.gz", hash = "sha256:f8f59201481e904362873bf0be3267f259d60ad946ebdfcb847d092a1fa26f98"}, @@ -3242,9 +3044,7 @@ files = [ [[package]] name = "typing-extensions" version = "4.13.2" -requires_python = ">=3.8" -summary = "Backported and Experimental Type Hints for Python 3.8+" -groups = ["default", "dev"] +summary = "" files = [ {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, @@ -3253,11 +3053,9 @@ files = [ [[package]] name = "typing-inspection" version = "0.4.0" -requires_python = ">=3.9" -summary = "Runtime typing introspection tools" -groups = ["default"] +summary = "" dependencies = [ - "typing-extensions>=4.12.0", + "typing-extensions", ] files = [ {file = "typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f"}, @@ -3267,9 +3065,7 @@ files = [ [[package]] name = "tzdata" version = "2025.2" -requires_python = ">=2" -summary = "Provider of IANA time zone data" -groups = ["default"] +summary = "" files = [ {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, @@ -3278,11 +3074,9 @@ files = [ [[package]] name = "tzlocal" version = "5.3.1" -requires_python = ">=3.9" -summary = "tzinfo object for the local timezone" -groups = ["default"] +summary = "" dependencies = [ - "tzdata; platform_system == \"Windows\"", + "tzdata; sys_platform == \"win32\"", ] files = [ {file = "tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d"}, @@ -3292,9 +3086,7 @@ files = [ [[package]] name = "ua-parser" version = "1.0.1" -requires_python = ">=3.9" -summary = "Python port of Browserscope's user agent parser" -groups = ["default"] +summary = "" dependencies = [ "ua-parser-builtins", ] @@ -3306,9 +3098,7 @@ files = [ [[package]] name = "ua-parser-builtins" version = "0.18.0.post1" -requires_python = ">=3.9" -summary = "Precompiled rules for User Agent Parser" -groups = ["default"] +summary = "" files = [ {file = "ua_parser_builtins-0.18.0.post1-py3-none-any.whl", hash = "sha256:eb4f93504040c3a990a6b0742a2afd540d87d7f9f05fd66e94c101db1564674d"}, ] @@ -3316,9 +3106,7 @@ files = [ [[package]] name = "urllib3" version = "2.4.0" -requires_python = ">=3.9" -summary = "HTTP library with thread-safe connection pooling, file post, and more." -groups = ["default"] +summary = "" files = [ {file = "urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813"}, {file = "urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466"}, @@ -3328,11 +3116,9 @@ files = [ name = "urllib3" version = "2.4.0" extras = ["socks"] -requires_python = ">=3.9" -summary = "HTTP library with thread-safe connection pooling, file post, and more." -groups = ["default"] +summary = "" dependencies = [ - "pysocks!=1.5.7,<2.0,>=1.5.6", + "pysocks", "urllib3==2.4.0", ] files = [ @@ -3343,13 +3129,11 @@ files = [ [[package]] name = "uvicorn" version = "0.34.2" -requires_python = ">=3.9" -summary = "The lightning-fast ASGI server." -groups = ["default"] +summary = "" dependencies = [ - "click>=7.0", - "h11>=0.8", - "typing-extensions>=4.0; python_version < \"3.11\"", + "click", + "h11", + "typing-extensions; python_full_version < \"3.11\"", ] files = [ {file = "uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403"}, @@ -3359,9 +3143,7 @@ files = [ [[package]] name = "w3lib" version = "2.3.1" -requires_python = ">=3.9" -summary = "Library of web-related functions" -groups = ["default"] +summary = "" files = [ {file = "w3lib-2.3.1-py3-none-any.whl", hash = "sha256:9ccd2ae10c8c41c7279cd8ad4fe65f834be894fe7bfdd7304b991fd69325847b"}, {file = "w3lib-2.3.1.tar.gz", hash = "sha256:5c8ac02a3027576174c2b61eb9a2170ba1b197cae767080771b6f1febda249a4"}, @@ -3370,11 +3152,7 @@ files = [ [[package]] name = "wcwidth" version = "0.2.13" -summary = "Measures the displayed width of unicode strings in a terminal" -groups = ["dev"] -dependencies = [ - "backports-functools-lru-cache>=1.2.1; python_version < \"3.2\"", -] +summary = "" files = [ {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, @@ -3383,9 +3161,7 @@ files = [ [[package]] name = "webdriver-manager" version = "4.0.2" -requires_python = ">=3.7" -summary = "Library provides the way to automatically manage drivers for different browsers" -groups = ["default"] +summary = "" dependencies = [ "packaging", "python-dotenv", @@ -3399,9 +3175,7 @@ files = [ [[package]] name = "websocket-client" version = "1.8.0" -requires_python = ">=3.8" -summary = "WebSocket client for Python with low level API options" -groups = ["default"] +summary = "" 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"}, @@ -3410,9 +3184,7 @@ files = [ [[package]] name = "websockets" version = "15.0.1" -requires_python = ">=3.9" -summary = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" -groups = ["default"] +summary = "" files = [ {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"}, {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"}, @@ -3471,11 +3243,9 @@ files = [ [[package]] name = "wsproto" version = "1.2.0" -requires_python = ">=3.7.0" -summary = "WebSockets state-machine based protocol implementation" -groups = ["default"] +summary = "" dependencies = [ - "h11<1,>=0.9.0", + "h11", ] files = [ {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, @@ -3485,9 +3255,7 @@ files = [ [[package]] name = "xlsxwriter" version = "3.2.3" -requires_python = ">=3.6" -summary = "A Python module for creating Excel XLSX files." -groups = ["default"] +summary = "" files = [ {file = "XlsxWriter-3.2.3-py3-none-any.whl", hash = "sha256:593f8296e8a91790c6d0378ab08b064f34a642b3feb787cf6738236bd0a4860d"}, {file = "xlsxwriter-3.2.3.tar.gz", hash = "sha256:ad6fd41bdcf1b885876b1f6b7087560aecc9ae5a9cc2ba97dcac7ab2e210d3d5"}, @@ -3496,13 +3264,11 @@ files = [ [[package]] name = "yarl" version = "1.20.0" -requires_python = ">=3.9" -summary = "Yet another URL library" -groups = ["default"] +summary = "" dependencies = [ - "idna>=2.0", - "multidict>=4.0", - "propcache>=0.2.1", + "idna", + "multidict", + "propcache", ] files = [ {file = "yarl-1.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f1f6670b9ae3daedb325fa55fbe31c22c8228f6e0b513772c2e1c623caa6ab22"}, @@ -3597,11 +3363,9 @@ files = [ [[package]] name = "zstandard" version = "0.23.0" -requires_python = ">=3.8" -summary = "Zstandard bindings for Python" -groups = ["default"] +summary = "" dependencies = [ - "cffi>=1.17; platform_python_implementation == \"PyPy\"", + "cffi; platform_python_implementation == \"PyPy\"", ] files = [ {file = "zstandard-0.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bf0a05b6059c0528477fba9054d09179beb63744355cab9f38059548fedd46a9"}, diff --git a/pyproject.toml b/pyproject.toml index 0f53464..05a52fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ dependencies = [ "asyncio>=3.4.3", "aiohttp>=3.9.5", "bs4>=0.0.2", - "lxml[html_clean]>=5.2.2", + "lxml>=5.2.2", "lxml-stubs>=0.5.1", "fake-useragent>=1.5.1", "requests-html>=0.10.0", @@ -24,7 +24,6 @@ dependencies = [ "python-keycloak>=4.2.0", "fastapi-keycloak>=1.0.11", "pymongo>=4.8.0", - "motor[asyncio]>=3.5.0", "python-jose[cryptography]>=3.3.0", "passlib[bcrypt]>=1.7.4", "selenium-wire>=5.1.0", @@ -44,6 +43,13 @@ dependencies = [ "html2text>=2025.4.15", "proxy-py>=2.4.10", "browserforge==1.2.1", + "sqlalchemy>=2.0.41", + "aiosqlite>=0.21.0", + "alembic>=1.16.4", + "asyncpg>=0.30.0", + "aiomysql>=0.2.0", + "psycopg2-binary>=2.9.10", + "mysqlclient>=2.2.7", ] requires-python = ">=3.10" readme = "README.md" diff --git a/src/services/api-service/functions/download.ts b/src/services/api-service/functions/download.ts index 4f1948e..8fbf479 100644 --- a/src/services/api-service/functions/download.ts +++ b/src/services/api-service/functions/download.ts @@ -1,6 +1,7 @@ export const download = async (ids: string[], jobFormat: string) => { const response = await fetch("/api/download", { method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ data: { ids, job_format: jobFormat } }), }); diff --git a/start.sh b/start.sh index 6b1fb3c..237f9b3 100755 --- a/start.sh +++ b/start.sh @@ -2,6 +2,8 @@ RECORDINGS_ENABLED=${RECORDINGS_ENABLED:-true} +pdm run alembic upgrade head + if [ "$RECORDINGS_ENABLED" == "false" ]; then pdm run python -m api.backend.worker.job_worker else