diff --git a/backend/alembic.ini b/backend/alembic.ini new file mode 100644 index 0000000..63814c2 --- /dev/null +++ b/backend/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 = sqlite:///events.db + + +[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/backend/alembic/README b/backend/alembic/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/backend/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/backend/alembic/env.py b/backend/alembic/env.py new file mode 100644 index 0000000..d03aefb --- /dev/null +++ b/backend/alembic/env.py @@ -0,0 +1,76 @@ +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +from taxonomy_time_machine.models import Base + +target_metadata = Base.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + 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/backend/alembic/script.py.mako b/backend/alembic/script.py.mako new file mode 100644 index 0000000..1101630 --- /dev/null +++ b/backend/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/backend/alembic/versions/119d8f99f4dc_initial_migration.py b/backend/alembic/versions/119d8f99f4dc_initial_migration.py new file mode 100644 index 0000000..21ffa28 --- /dev/null +++ b/backend/alembic/versions/119d8f99f4dc_initial_migration.py @@ -0,0 +1,56 @@ +"""initial migration + +Revision ID: 119d8f99f4dc +Revises: +Create Date: 2025-07-16 22:20:43.976759 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "119d8f99f4dc" +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( + "taxonomy_source", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("path", sa.Text(), nullable=False), + sa.Column("version_date", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "taxonomy", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("taxonomy_source_id", sa.Integer(), nullable=False), + sa.Column("event_name", sa.Text(), nullable=False), + sa.Column("version_date", sa.DateTime(), nullable=False), + sa.Column("tax_id", sa.Text(), nullable=False), + sa.Column("parent_id", sa.Text(), nullable=True), + sa.Column("rank", sa.Text(), nullable=True), + sa.Column("name", sa.Text(), nullable=True), + sa.ForeignKeyConstraint( + ["taxonomy_source_id"], + ["taxonomy_source.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("taxonomy") + op.drop_table("taxonomy_source") + # ### end Alembic commands ### diff --git a/backend/alembic/versions/4bb4acc2c8d3_add_performance_indexes.py b/backend/alembic/versions/4bb4acc2c8d3_add_performance_indexes.py new file mode 100644 index 0000000..b33e08c --- /dev/null +++ b/backend/alembic/versions/4bb4acc2c8d3_add_performance_indexes.py @@ -0,0 +1,43 @@ +"""add performance indexes + +Revision ID: 4bb4acc2c8d3 +Revises: 119d8f99f4dc +Create Date: 2025-07-16 22:22:44.380181 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "4bb4acc2c8d3" +down_revision: Union[str, Sequence[str], None] = "119d8f99f4dc" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # Create performance indexes matching the original schema + op.create_index("idx_tax_id", "taxonomy", ["tax_id"]) + op.create_index("idx_parent_id", "taxonomy", ["parent_id"]) + op.create_index("idx_name", "taxonomy", [sa.text("lower(name)")]) + op.create_index("idx_tax_id_version_date", "taxonomy", ["tax_id", "version_date"]) + op.create_index("idx_name_version_date", "taxonomy", ["name", "version_date"]) + + # Create FTS virtual table + op.execute("CREATE VIRTUAL TABLE name_fts USING fts5(name)") + + +def downgrade() -> None: + """Downgrade schema.""" + # Drop indexes and FTS table + op.drop_index("idx_name_version_date", "taxonomy") + op.drop_index("idx_tax_id_version_date", "taxonomy") + op.drop_index("idx_name", "taxonomy") + op.drop_index("idx_parent_id", "taxonomy") + op.drop_index("idx_tax_id", "taxonomy") + op.execute("DROP TABLE IF EXISTS name_fts") diff --git a/backend/app.py b/backend/app.py index 007bbe7..254571e 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,14 +1,14 @@ -from taxonomy_time_machine.models import Taxonomy -from datetime import datetime - import os import random -from flask import Flask, g -from flask.views import MethodView +import threading + import marshmallow as ma -from flask_smorest import Api, Blueprint +from flask import Flask +from flask.views import MethodView from flask_cors import CORS -import threading +from flask_smorest import Api, Blueprint + +from taxonomy_time_machine import TaxonomyTimeMachine app = Flask(__name__) @@ -52,7 +52,7 @@ def get_taxonomy(): if not hasattr(_local, "taxonomy"): - _local.taxonomy = Taxonomy(database_path=DATABASE_PATH) + _local.taonomy = TaxonomyTimeMachine(database_path=DATABASE_PATH) return _local.taxonomy @@ -78,7 +78,7 @@ class ChildrenQuerySchema(ma.Schema): ) @ma.pre_load - def coerce_empty_to_none(self, data, **kwargs): + def coerce_empty_to_none(self, data, **_): data = data.copy() for key, value in data.items(): if value == "": @@ -171,7 +171,7 @@ class Versions(MethodView): @blp.response(200, VersionSchema(many=True)) def get(self, args): """Return all available database versions where the given tax ID appears""" - db = Taxonomy(database_path=DATABASE_PATH) + db = get_taxonomy() tax_id = args.get("tax_id") versions = db.get_versions(tax_id=tax_id) if tax_id: diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 50b2e8b..2455260 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -2,14 +2,26 @@ name = "taxonomy-time-machine" version = "0.1.0" description = "Add your description here" -readme = "README.md" requires-python = ">=3.11" dependencies = [ + "alembic>=1.14.0", "flask-cors>=4.0.0", "flask-smorest>=0.45.0", "flask>=3.1.0", - "polars>=1.13.1", - "pytest>=8.3.3", + "polars", + "sqlalchemy>=2.0.0", "taxonomy>=0.10.1", "tqdm>=4.67.0", ] + +[project.scripts] +ttm-load = "taxonomy_time_machine.load_data:main" + +[tool.setuptools.packages.find] +include = ["taxonomy_time_machine*"] +exclude = ["dumps*"] + +[dependency-groups] +dev = [ + "pytest>=8.3.3", +] diff --git a/backend/taxonomy_time_machine/__init__.py b/backend/taxonomy_time_machine/__init__.py index e69de29..712088e 100644 --- a/backend/taxonomy_time_machine/__init__.py +++ b/backend/taxonomy_time_machine/__init__.py @@ -0,0 +1,2 @@ +from .taxonomy_time_machine import TaxonomyTimeMachine +from .event import Event, EventName diff --git a/backend/taxonomy_time_machine/event.py b/backend/taxonomy_time_machine/event.py new file mode 100644 index 0000000..c60dca8 --- /dev/null +++ b/backend/taxonomy_time_machine/event.py @@ -0,0 +1,50 @@ +from datetime import datetime + +from dataclasses import dataclass +from enum import Enum + + +class EventName(Enum): + Create = "create" + Delete = "delete" + Update = "alter" # TODO: change me to Update + + +# TODO: encode what changed using a bitarray +@dataclass +class Event: + event_name: EventName + tax_id: str + version_date: datetime + taxonomy_source_id: int + name: str | None = None + rank: str | None = None + parent_id: str | None = None + + @classmethod + def from_dict(cls, data: dict): + version_date = data["version_date"] + + if type(version_date) is str: + version_date = datetime.fromisoformat(data["version_date"]) + + return cls( + event_name=EventName(data["event_name"]), # Convert to enum + tax_id=data["tax_id"], + version_date=version_date, + name=data.get("name"), + rank=data.get("rank"), + parent_id=data.get("parent_id"), + taxonomy_source_id=data["taxonomy_source_id"], + ) + + def to_dict(self) -> dict: + return { + "event_name": self.event_name.value, + "version_date": self.version_date, + "tax_id": self.tax_id, + "parent_id": self.parent_id, + "name": self.name, + "rank": self.rank, + "taxonomy_source_id": self.taxonomy_source_id, + } diff --git a/backend/taxonomy_time_machine/load_data.py b/backend/taxonomy_time_machine/load_data.py index cc00bac..31d2ab8 100755 --- a/backend/taxonomy_time_machine/load_data.py +++ b/backend/taxonomy_time_machine/load_data.py @@ -1,25 +1,21 @@ #!/usr/bin/env python3 -from pathlib import Path +import argparse from collections import Counter -from tqdm import tqdm -import taxonomy from datetime import datetime -import argparse +from pathlib import Path -import sqlite3 +from sqlalchemy import create_engine, text +from sqlalchemy.orm import sessionmaker +from taxonomy import Taxonomy +from tqdm import tqdm -from models import Event, EventName +from . import Event, EventName, TaxonomyTimeMachine +from .models import Base, TaxonomySource, Taxonomy as TaxonomyModel def parse_args(): parser = argparse.ArgumentParser() - parser.add_argument( - "--always-insert", - action="store_true", - default=False, - help="Always insert nodes instead of only inserting deltas", - ) parser.add_argument("--db-path", required=True, type=str, help="path to output sqltie database") return parser.parse_args() @@ -28,16 +24,60 @@ def dump_path_to_datetime(dump_path: Path) -> datetime: return datetime.strptime(dump_path.name.split("_")[1], "%Y-%m-%d") +def load_current_tax_id_to_node(database_path: str) -> dict[str, Event]: + """Load current tax ID to node state""" + + return TaxonomyTimeMachine(database_path=database_path).iter_most_recent_events() + + +def setup_sqlite_performance(engine): + """Optimize SQLite for bulk inserts""" + with engine.connect() as conn: + conn.execute(text("PRAGMA synchronous = OFF")) + conn.execute(text("PRAGMA journal_mode = MEMORY")) + conn.execute(text("PRAGMA temp_store = MEMORY")) + conn.commit() + + def main() -> None: args = parse_args() - taxdumps = sorted( + # Create SQLAlchemy engine and session + engine = create_engine(f"sqlite:///{args.db_path}") + Session = sessionmaker(bind=engine) + + # stores the *last* state of a Tax ID + # used to determine if a tax ID has changed + tax_id_to_node: dict[str, Event] + try: + tax_id_to_node = load_current_tax_id_to_node(args.db_path) + except Exception: # table doesn't exist yet... starting from scratch + tax_id_to_node = {} + + # Optimize SQLite for bulk inserts + setup_sqlite_performance(engine) + + taxdump_paths = sorted( [p for p in Path("dumps").glob("*") if p.is_dir()], key=dump_path_to_datetime, ) + paths_to_import = [] + + with Session() as session: + for taxdump_path in taxdump_paths: + count = ( + session.query(TaxonomySource) + .filter(TaxonomySource.path == str(taxdump_path)) + .count() + ) + if not count: + paths_to_import.append(taxdump_path) + n_events = 0 - tax_id_to_node: dict = {} + + print(f"Found {len(tax_id_to_node):,} existing taxonomy versions") + data_to_insert: list[dict] = [] last_tax_ids: None | set[str] = None @@ -45,11 +85,17 @@ def main() -> None: last_tax = None - for n, taxdump in enumerate(taxdumps): - taxdump_date = dump_path_to_datetime(taxdump) + for n, taxdump_path in enumerate(paths_to_import): + taxdump_date = dump_path_to_datetime(taxdump_path) + + with Session() as session: + taxonomy_source = TaxonomySource(path=str(taxdump_path), version_date=taxdump_date) + session.add(taxonomy_source) + session.commit() + taxonomy_source_id = taxonomy_source.id - tax = taxonomy.Taxonomy.from_ncbi(str(taxdump)) - print(f"--- loaded {taxdump}: {tax}") + tax = Taxonomy.from_ncbi(str(taxdump_path)) + print(f"--- loaded {taxdump_path}: {tax}") total_seen_taxa += len(tax) @@ -67,6 +113,7 @@ def main() -> None: event = None + # node isn't in tax_id_to_node -- it must be new if from_node is None: event = Event( event_name=EventName.Create, @@ -75,14 +122,13 @@ def main() -> None: name=to_node.name, parent_id=to_node.parent, version_date=taxdump_date, + taxonomy_source_id=taxonomy_source_id, ) - elif args.always_insert or ( - (from_node.parent, from_node.rank, from_node.name) - != ( - to_node.parent, - to_node.rank, - to_node.name, - ) + # *something* changed + elif (from_node.parent_id, from_node.rank, from_node.name) != ( + to_node.parent, + to_node.rank, + to_node.name, ): event = Event( event_name=EventName.Update, @@ -91,14 +137,14 @@ def main() -> None: name=to_node.name, parent_id=to_node.parent, version_date=taxdump_date, + taxonomy_source_id=taxonomy_source_id, ) if event is not None: + tax_id_to_node[tax_id] = event events.append(event) n_new_events += 1 - tax_id_to_node[tax_id] = to_node - # find all the deleted nodes # append deletions @@ -113,11 +159,12 @@ def main() -> None: # taxonomy library type annotation is wrong? parent_id=last_tax[tax_id].parent if last_tax else None, # mypy: ignore version_date=taxdump_date, + taxonomy_source_id=taxonomy_source_id, ) ) # remove from tax_id_to_node in case this tax ID gets re-created - tax_id_to_node[tax_id] = None + del tax_id_to_node[tax_id] last_tax_ids = seen_tax_ids for event in events: @@ -125,84 +172,46 @@ def main() -> None: data_to_insert.append(event.to_dict()) n_events += 1 - print(f"{n}/{len(taxdumps)} total_events={n_events:,} n_new_events={n_new_events:,}") + print(f"{n}/{len(taxdump_paths)} total_events={n_events:,} n_new_events={n_new_events:,}") for event_name, count in event_counts.items(): print(f" {event_name.value:>10} -> {count:,}") print() - # TODO: can probably clean up a lot of this code by just using `last_tax` last_tax = tax + print(Counter([event["event_name"] for event in data_to_insert])) + print(f"--- {total_seen_taxa=:,}") print(f"--- {len(data_to_insert)=:,}") print(f"--- savings={1 - (len(data_to_insert) / total_seen_taxa):.2%}") - # TODO: write to sqlite while parsing to avoid having to store all taxonomy - # nodes in memory... - - # Connect to the SQLite database - conn = sqlite3.connect(args.db_path) - cursor = conn.cursor() - - # Optimized PRAGMA settings for faster inserts - cursor.execute("PRAGMA synchronous = OFF;") - cursor.execute("PRAGMA journal_mode = MEMORY;") - cursor.execute("PRAGMA temp_store = MEMORY;") - - # Create table - # We use TEXT for tax_id and parent_id to support non-NCBI taxonomies such - # as GTDB-Tk which lack IDs - # (you can use the name as the ID) - cursor.execute(""" - CREATE TABLE taxonomy ( - event_name TEXT, - version_date DATETIME, - tax_id TEXT, - parent_id TEXT, - rank TEXT, - name TEXT - ) - """) - batch_size = 10_000 - # Loop through data in batches to avoid memory overload - # Batch insert using transactions and executemany - for i in tqdm(range(0, len(data_to_insert), batch_size)): - batch = data_to_insert[i : i + batch_size] - cursor.executemany( - """ - INSERT INTO taxonomy (event_name, version_date, tax_id, parent_id, rank, name) - VALUES (:event_name, :version_date, :tax_id, :parent_id, :rank, :name) - """, - batch, - ) - - conn.commit() - - print("--- creating b-tree indexes") - cursor.execute("CREATE INDEX idx_tax_id ON taxonomy (tax_id);") - cursor.execute("CREATE INDEX idx_parent_id ON taxonomy (parent_id);") - - # index the lowercase of `name` to speed up case-insensitive searches - # like lower(name) = lower('query'); - cursor.execute("CREATE INDEX idx_name ON taxonomy (lower(name));") - cursor.execute("CREATE INDEX idx_tax_id_version_date ON taxonomy (tax_id, version_date);") - cursor.execute("CREATE INDEX idx_name_version_date ON taxonomy (name, version_date);") - - # Full Text Search (FTS) index - print("--- creating full text index") - cursor.execute("CREATE VIRTUAL TABLE name_fts USING fts5(name);") - cursor.execute("INSERT INTO name_fts (name) SELECT name FROM taxonomy;") - - conn.commit() + # Batch insert using SQLAlchemy bulk operations + with Session() as session: + for i in tqdm(range(0, len(data_to_insert), batch_size)): + batch = data_to_insert[i : i + batch_size] + taxonomy_objects = [ + TaxonomyModel( + event_name=item["event_name"], + version_date=item["version_date"], + tax_id=item["tax_id"], + parent_id=item["parent_id"], + rank=item["rank"], + name=item["name"], + taxonomy_source_id=item["taxonomy_source_id"], + ) + for item in batch + ] + session.bulk_save_objects(taxonomy_objects) + session.commit() print("--- wrapping up") - # cursor.execute("PRAGMA synchronous = FULL;") - # cursor.execute("PRAGMA journal_mode = DELETE;") - conn.close() + with Session() as session: + count = session.query(TaxonomyModel).count() + print(f"taxonomy version table now has {count:,} rows") if __name__ == "__main__": diff --git a/backend/taxonomy_time_machine/models.py b/backend/taxonomy_time_machine/models.py index 3bf3cec..5a2f738 100644 --- a/backend/taxonomy_time_machine/models.py +++ b/backend/taxonomy_time_machine/models.py @@ -1,379 +1,40 @@ -import sqlite3 from datetime import datetime -from typing import Literal -from functools import lru_cache -from dataclasses import dataclass -from enum import Enum +from sqlalchemy import DateTime, ForeignKey, Integer, Text, create_engine +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship -import time -import logging -# Set logging level to INFO so profiling messages are visible -logging.basicConfig(level=logging.WARNING) +class Base(DeclarativeBase): + pass -class EventName(Enum): - Create = "create" - Delete = "delete" - Update = "alter" # TODO: change me to Update +class TaxonomySource(Base): + __tablename__ = "taxonomy_source" + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + path: Mapped[str] = mapped_column(Text) + version_date: Mapped[datetime] = mapped_column(DateTime) -@dataclass -class Event: - event_name: EventName - tax_id: str - version_date: datetime - name: str | None = None - rank: str | None = None - parent_id: str | None = None + # Relationship to taxonomy records + taxonomy_records: Mapped[list["Taxonomy"]] = relationship(back_populates="source") - @classmethod - def from_dict(cls, data: dict): - version_date = data["version_date"] - if type(version_date) is str: - version_date = datetime.fromisoformat(data["version_date"]) +class Taxonomy(Base): + __tablename__ = "taxonomy" - return cls( - event_name=EventName(data["event_name"]), # Convert to enum - tax_id=data["tax_id"], - version_date=version_date, - name=data.get("name"), - rank=data.get("rank"), - parent_id=data.get("parent_id"), - ) + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + taxonomy_source_id: Mapped[int] = mapped_column(Integer, ForeignKey("taxonomy_source.id")) + event_name: Mapped[str] = mapped_column(Text) + version_date: Mapped[datetime] = mapped_column(DateTime) + tax_id: Mapped[str] = mapped_column(Text) + parent_id: Mapped[str | None] = mapped_column(Text, nullable=True) + rank: Mapped[str | None] = mapped_column(Text, nullable=True) + name: Mapped[str | None] = mapped_column(Text, nullable=True) - def to_dict(self) -> dict: - return { - "event_name": self.event_name.value, - "version_date": self.version_date, - "tax_id": self.tax_id, - "parent_id": self.parent_id, - "name": self.name, - "rank": self.rank, - } + # Relationship to source + source: Mapped[TaxonomySource] = relationship(back_populates="taxonomy_records") -def _get_all_events_recursive( - db: "Taxonomy", tax_id: str, seen_tax_ids: set | None = None -) -> list[Event]: - """ - Find all events for a given tax ID and any events for its parent's and - their parents, etc... - - TODO: also find all events for the children of a tax ID - - TODO: this sometimes finds irrelevant events like a new node is created - under a node in the lineage but isn't directly part of the current taxon's - lineage - """ - - events: list[Event] = [] - - if seen_tax_ids is None: - seen_tax_ids = set() - - if tax_id in seen_tax_ids: - return events - - seen_tax_ids.add(tax_id) - - for event in db.get_events(tax_id): - events.append(event) - seen_tax_ids.add(event.tax_id) - if event.parent_id and event.parent_id not in seen_tax_ids: - events.extend( - _get_all_events_recursive(db, tax_id=event.parent_id, seen_tax_ids=seen_tax_ids) - ) - - seen_tax_ids.add(event.parent_id) - - return sorted(events, key=lambda e: e.version_date) - - -class Taxonomy: - def __init__(self, database_path: str = "events.db"): - self.conn = sqlite3.connect(database_path) - self.conn.row_factory = sqlite3.Row # return Row instead of tuple - self.cursor = self.conn.cursor() - - def _profile(self, func_name: str, start: float, end: float): - elapsed = (end - start) * 1000 # ms - logging.info(f"[PROFILE] {func_name} took {elapsed:.2f} ms") - - def _escape_fts_phrase(self, text: str) -> str: - """Escape text for use in FTS5 phrase queries by doubling quotes and wrapping in quotes""" - # Escape internal quotes by doubling them (FTS5 standard) - escaped = text.replace('"', '""') - # Wrap in quotes to make it a phrase query - return f'"{escaped}"' - - def _safe_fts_query(self, sql: str, params: tuple): - """Execute FTS query with fallback to empty results on syntax errors""" - try: - return self.cursor.execute(sql, params).fetchall() - except sqlite3.OperationalError as e: - if "fts5: syntax error" in str(e).lower(): - logging.warning( - f"FTS syntax error with query: {params}, falling back to empty results" - ) - return [] - else: - # Re-raise other operational errors - raise - - @lru_cache(maxsize=128) - def search_names(self, query: str, limit: int | None = 10) -> list[Event]: - _profile_start = time.perf_counter() - matches: list[dict] = [] - exact_matches: list[dict] = [] - - # first, check if the query is tax-ID like - if query.isnumeric(): - _q1_start = time.perf_counter() - rows = self.cursor.execute( - """SELECT * - FROM taxonomy - WHERE tax_id = ? - ORDER BY version_date desc - LIMIT 1""", - (query,), - ).fetchall() - _q1_end = time.perf_counter() - self._profile("search_names:taxid_query", _q1_start, _q1_end) - - exact_matches.extend([dict(r) for r in rows]) - - if limit is None or (len(matches) + len(exact_matches) < limit): - # Use FTS for prefix matches instead of slow LIKE query - _q2_start = time.perf_counter() - prefix_rows = [ - dict(r) - for r in self._safe_fts_query( - """ - SELECT * - FROM name_fts - JOIN taxonomy ON name_fts.name = taxonomy.name - WHERE name_fts MATCH ? - ORDER BY LENGTH(taxonomy.name) ASC - LIMIT ? - ; - """, - (f"{self._escape_fts_phrase(query)}*", 10), - ) - ] - _q2_end = time.perf_counter() - self._profile("search_names:prefix_query", _q2_start, _q2_end) - matches.extend(prefix_rows) - - if limit is None or (len(matches) + len(exact_matches)) < limit: - # fuzzy matches - _q3_start = time.perf_counter() - fuzzy_rows = [ - dict(r) - for r in self._safe_fts_query( - """ - SELECT taxonomy.tax_id, taxonomy.name, taxonomy.rank, taxonomy.event_name, taxonomy.version_date - FROM name_fts - JOIN taxonomy ON name_fts.name = taxonomy.name - WHERE name_fts MATCH ? - ORDER BY LENGTH(taxonomy.name) ASC - LIMIT ? - ; - """, - (self._escape_fts_phrase(query), 10), - ) - ] - _q3_end = time.perf_counter() - self._profile("search_names:fuzzy_query", _q3_start, _q3_end) - matches.extend(fuzzy_rows) - - # sort by closest match (probably the shortest) - matches = sorted(matches, key=lambda m: len(m["name"])) - - # exact matches should always come first - # + convert to Events - events = [Event.from_dict(m) for m in (exact_matches + matches)] - - # deduplicate by name, taking most-recent - name_to_event: dict[str, Event] = {} - for event in events: - existing_event = name_to_event.get(event.name) - if existing_event is None: - name_to_event[event.name] = event - elif existing_event.version_date < event.version_date: - name_to_event[event.name] = event - - events = list(name_to_event.values()) - - # + truncate to limit - events = events[:limit] - - self._profile("search_names", _profile_start, time.perf_counter()) - return events - - @lru_cache(maxsize=256) - def get_events( - self, - tax_id: str, - as_of: datetime | None = None, - query_key: Literal["tax_id", "parent_id"] = "tax_id", - ) -> list[Event]: - """Get all events for a given tax_id or parent_id depending on - query_key (default='tax_id')""" - _profile_start = time.perf_counter() - - if query_key == "tax_id": - self.cursor.execute("SELECT * FROM taxonomy WHERE tax_id = ?;", (tax_id,)) - elif query_key == "parent_id": - self.cursor.execute("SELECT * FROM taxonomy WHERE parent_id = ?;", (tax_id,)) - else: - raise Exception(f"Unable to use handle {query_key=}") - - rows = [Event.from_dict(dict(r)) for r in self.cursor.fetchall()] - - if as_of: - rows = [r for r in rows if r.version_date <= as_of] - - result = sorted(rows, key=lambda r: r.version_date) - self._profile("get_events", _profile_start, time.perf_counter()) - return result - - @lru_cache(maxsize=256) - def get_children(self, tax_id: str, as_of: datetime | None = None): - """Get all children of a node at a given version""" - - _profile_start = time.perf_counter() - - # 1. find all? rows where parent_id=query_tax_id using their tax_ids - # .... - - # find all create/alter events where parent_id = tax_id and - # version_date <= as_of - parent_events = self.get_events(tax_id=tax_id, as_of=as_of, query_key="parent_id") - - # 2. find most recent event by tax ID and make sure that the parent is - # *still* query_tax_id. remove these rows - - child_tax_ids = {e.tax_id for e in parent_events} - deleted_tax_ids = {e.tax_id for e in parent_events if e.event_name is EventName.Delete} - child_events = [] - - # TODO: do this in a single db query - for child_tax_id in child_tax_ids: - if ee := self.get_events(tax_id=child_tax_id, as_of=as_of, query_key="tax_id"): - last_event = ee[-1] - - # catch tax IDs that were deleted then re-created - if ( - last_event.event_name is not EventName.Delete - and last_event.tax_id in deleted_tax_ids - ): - deleted_tax_ids.remove(last_event.tax_id) - child_events.append(ee[-1]) - - keep_tax_ids = {c.tax_id for c in child_events if c.parent_id == tax_id} - events = [e for e in parent_events if e.tax_id in keep_tax_ids] - - # 3. remove any deleted rows - events = [e for e in events if e.tax_id not in deleted_tax_ids] - - # for each tax ID, get the *latest* parent_id - # if that parent_id == tax_id then keep it - - # NOTE: it's faster to do this in Python than in SQL at least with the - # queries I tried. - - latest_row_by_tax_id: dict[str, Event] = {} - for event in events: - if event.tax_id not in latest_row_by_tax_id or ( - event.version_date >= latest_row_by_tax_id[event.tax_id].version_date - ): - latest_row_by_tax_id[event.tax_id] = event - - # if the taxon moved then we can't find it by parent ID because it has - # a new parent ID... so we need to look up each individual taxon's - # events to check for moves and deletions... - - # make sure the row is still a child of tax_id - rows = [r for r in latest_row_by_tax_id.values() if r.parent_id == tax_id] - - # remove anything that got deleted - rows = [r for r in rows if r.event_name is not EventName.Delete] - - self._profile("get_children", _profile_start, time.perf_counter()) - return rows - - def get_all_events_recursive(self, tax_id: str) -> list[Event]: - _profile_start = time.perf_counter() - result = _get_all_events_recursive(db=self, tax_id=tax_id) - self._profile("get_all_events_recursive", _profile_start, time.perf_counter()) - return result - - @lru_cache(maxsize=256) - def get_versions(self, tax_id: str) -> list[datetime]: - """Get the collapsed list of dates at which a taxon's lineage - changed""" - _profile_start = time.perf_counter() - - # TODO: handle deletions (example: 352463) - - events = _get_all_events_recursive(db=self, tax_id=tax_id) - version_dates = sorted({e.version_date for e in events}) - - seen_lineages = set() - versions_with_changes = [] - - for version_date in version_dates: - events = self.get_lineage(tax_id=tax_id, as_of=version_date) - - # TODO: can a tax_id by deleted and created in the same version? - # TODO: can a tax_id be re-created after being deleted? - - key = tuple([(e.rank, e.tax_id, e.parent_id, e.name) for e in events]) - - # prevents versions where the tax ID didn't exist yet from showing - # up for some reason - if len(key) == 0: - continue - - if key not in seen_lineages: - versions_with_changes.append(version_date) - seen_lineages.add(key) - - self._profile("get_versions", _profile_start, time.perf_counter()) - return versions_with_changes - - @lru_cache(maxsize=256) - def get_lineage(self, tax_id: str, as_of: datetime | None = None): - """ - Given a tax_id: return the taxonomy lineage. If `as_of` is specified, - return the taxonomy lineage as of that date. - """ - _profile_start = time.perf_counter() - - lineage = [] - - while True: - events = self.get_events(tax_id=tax_id, as_of=as_of) - - # find most recent event where the parent_id changed - parent = None - for event in events[::-1]: - if event.parent_id: - parent = event - break - - if parent is not None: - lineage.append(parent) - else: - break - - if parent.parent_id is None: - break - - tax_id = parent.parent_id - - self._profile("get_lineage", _profile_start, time.perf_counter()) - return lineage +def create_db_engine(database_path: str): + """Create SQLAlchemy engine for the given database path""" + return create_engine(f"sqlite:///{database_path}") diff --git a/backend/taxonomy_time_machine/taxonomy_time_machine.py b/backend/taxonomy_time_machine/taxonomy_time_machine.py new file mode 100644 index 0000000..fa56fe2 --- /dev/null +++ b/backend/taxonomy_time_machine/taxonomy_time_machine.py @@ -0,0 +1,348 @@ +from datetime import datetime +from functools import lru_cache +import logging +import sqlite3 +import time +from typing import Literal + +from .event import Event, EventName + + +class TaxonomyTimeMachine: + def __init__(self, database_path: str = "events.db"): + self.conn = sqlite3.connect(database_path) + self.conn.row_factory = sqlite3.Row # return Row instead of tuple + self.cursor = self.conn.cursor() + + def _profile(self, func_name: str, start: float, end: float): + elapsed = (end - start) * 1000 # ms + logging.info(f"[PROFILE] {func_name} took {elapsed:.2f} ms") + + def _escape_fts_phrase(self, text: str) -> str: + """Escape text for use in FTS5 phrase queries by doubling quotes and wrapping in quotes""" + # Escape internal quotes by doubling them (FTS5 standard) + escaped = text.replace('"', '""') + # Wrap in quotes to make it a phrase query + return f'"{escaped}"' + + def _safe_fts_query(self, sql: str, params: tuple): + """Execute FTS query with fallback to empty results on syntax errors""" + try: + return self.cursor.execute(sql, params).fetchall() + except sqlite3.OperationalError as e: + if "fts5: syntax error" in str(e).lower(): + logging.warning( + f"FTS syntax error with query: {params}, falling back to empty results" + ) + return [] + else: + # Re-raise other operational errors + raise + + @lru_cache(maxsize=128) + def search_names(self, query: str, limit: int | None = 10) -> list[Event]: + _profile_start = time.perf_counter() + matches: list[dict] = [] + exact_matches: list[dict] = [] + + # first, check if the query is tax-ID like + if query.isnumeric(): + _q1_start = time.perf_counter() + rows = self.cursor.execute( + """SELECT * + FROM taxonomy + WHERE tax_id = ? + ORDER BY version_date desc + LIMIT 1""", + (query,), + ).fetchall() + _q1_end = time.perf_counter() + self._profile("search_names:taxid_query", _q1_start, _q1_end) + + exact_matches.extend([dict(r) for r in rows]) + + if limit is None or (len(matches) + len(exact_matches) < limit): + # Use FTS for prefix matches instead of slow LIKE query + _q2_start = time.perf_counter() + prefix_rows = [ + dict(r) + for r in self._safe_fts_query( + """ + SELECT * + FROM name_fts + JOIN taxonomy ON name_fts.name = taxonomy.name + WHERE name_fts MATCH ? + ORDER BY LENGTH(taxonomy.name) ASC + LIMIT ? + ; + """, + (f"{self._escape_fts_phrase(query)}*", 10), + ) + ] + _q2_end = time.perf_counter() + self._profile("search_names:prefix_query", _q2_start, _q2_end) + matches.extend(prefix_rows) + + if limit is None or (len(matches) + len(exact_matches)) < limit: + # fuzzy matches + _q3_start = time.perf_counter() + fuzzy_rows = [ + dict(r) + for r in self._safe_fts_query( + """ + SELECT taxonomy.tax_id, taxonomy.name, taxonomy.rank, taxonomy.event_name, taxonomy.version_date + FROM name_fts + JOIN taxonomy ON name_fts.name = taxonomy.name + WHERE name_fts MATCH ? + ORDER BY LENGTH(taxonomy.name) ASC + LIMIT ? + ; + """, + (self._escape_fts_phrase(query), 10), + ) + ] + _q3_end = time.perf_counter() + self._profile("search_names:fuzzy_query", _q3_start, _q3_end) + matches.extend(fuzzy_rows) + + # sort by closest match (probably the shortest) + matches = sorted(matches, key=lambda m: len(m["name"])) + + # exact matches should always come first + # + convert to Events + events = [Event.from_dict(m) for m in (exact_matches + matches)] + + # deduplicate by name, taking most-recent + name_to_event: dict[str, Event] = {} + for event in events: + existing_event = name_to_event.get(event.name) + if existing_event is None: + name_to_event[event.name] = event + elif existing_event.version_date < event.version_date: + name_to_event[event.name] = event + + events = list(name_to_event.values()) + + # + truncate to limit + events = events[:limit] + + self._profile("search_names", _profile_start, time.perf_counter()) + return events + + @lru_cache(maxsize=256) + def get_events( + self, + tax_id: str, + as_of: datetime | None = None, + query_key: Literal["tax_id", "parent_id"] = "tax_id", + ) -> list[Event]: + """Get all events for a given tax_id or parent_id depending on + query_key (default='tax_id')""" + _profile_start = time.perf_counter() + + if query_key == "tax_id": + self.cursor.execute("SELECT * FROM taxonomy WHERE tax_id = ?;", (tax_id,)) + elif query_key == "parent_id": + self.cursor.execute("SELECT * FROM taxonomy WHERE parent_id = ?;", (tax_id,)) + else: + raise Exception(f"Unable to use handle {query_key=}") + + rows = [Event.from_dict(dict(r)) for r in self.cursor.fetchall()] + + if as_of: + rows = [r for r in rows if r.version_date <= as_of] + + result = sorted(rows, key=lambda r: r.version_date) + self._profile("get_events", _profile_start, time.perf_counter()) + return result + + @lru_cache(maxsize=256) + def get_children(self, tax_id: str, as_of: datetime | None = None): + """Get all children of a node at a given version""" + + _profile_start = time.perf_counter() + + # 1. find all? rows where parent_id=query_tax_id using their tax_ids + # .... + + # find all create/alter events where parent_id = tax_id and + # version_date <= as_of + parent_events = self.get_events(tax_id=tax_id, as_of=as_of, query_key="parent_id") + + # 2. find most recent event by tax ID and make sure that the parent is + # *still* query_tax_id. remove these rows + + child_tax_ids = {e.tax_id for e in parent_events} + deleted_tax_ids = {e.tax_id for e in parent_events if e.event_name is EventName.Delete} + child_events = [] + + # TODO: do this in a single db query + for child_tax_id in child_tax_ids: + if ee := self.get_events(tax_id=child_tax_id, as_of=as_of, query_key="tax_id"): + last_event = ee[-1] + + # catch tax IDs that were deleted then re-created + if ( + last_event.event_name is not EventName.Delete + and last_event.tax_id in deleted_tax_ids + ): + deleted_tax_ids.remove(last_event.tax_id) + child_events.append(ee[-1]) + + keep_tax_ids = {c.tax_id for c in child_events if c.parent_id == tax_id} + events = [e for e in parent_events if e.tax_id in keep_tax_ids] + + # 3. remove any deleted rows + events = [e for e in events if e.tax_id not in deleted_tax_ids] + + # for each tax ID, get the *latest* parent_id + # if that parent_id == tax_id then keep it + + # NOTE: it's faster to do this in Python than in SQL at least with the + # queries I tried. + + latest_row_by_tax_id: dict[str, Event] = {} + for event in events: + if event.tax_id not in latest_row_by_tax_id or ( + event.version_date >= latest_row_by_tax_id[event.tax_id].version_date + ): + latest_row_by_tax_id[event.tax_id] = event + + # if the taxon moved then we can't find it by parent ID because it has + # a new parent ID... so we need to look up each individual taxon's + # events to check for moves and deletions... + + # make sure the row is still a child of tax_id + rows = [r for r in latest_row_by_tax_id.values() if r.parent_id == tax_id] + + # remove anything that got deleted + rows = [r for r in rows if r.event_name is not EventName.Delete] + + self._profile("get_children", _profile_start, time.perf_counter()) + return rows + + def get_all_events_recursive(self, tax_id: str) -> list[Event]: + _profile_start = time.perf_counter() + result = _get_all_events_recursive(db=self, tax_id=tax_id) + self._profile("get_all_events_recursive", _profile_start, time.perf_counter()) + return result + + @lru_cache(maxsize=256) + def get_versions(self, tax_id: str) -> list[datetime]: + """Get the collapsed list of dates at which a taxon's lineage + changed""" + _profile_start = time.perf_counter() + + # TODO: handle deletions (example: 352463) + + events = _get_all_events_recursive(db=self, tax_id=tax_id) + version_dates = sorted({e.version_date for e in events}) + + seen_lineages = set() + versions_with_changes = [] + + for version_date in version_dates: + events = self.get_lineage(tax_id=tax_id, as_of=version_date) + + # TODO: can a tax_id by deleted and created in the same version? + # TODO: can a tax_id be re-created after being deleted? + + key = tuple([(e.rank, e.tax_id, e.parent_id, e.name) for e in events]) + + # prevents versions where the tax ID didn't exist yet from showing + # up for some reason + if len(key) == 0: + continue + + if key not in seen_lineages: + versions_with_changes.append(version_date) + seen_lineages.add(key) + + self._profile("get_versions", _profile_start, time.perf_counter()) + return versions_with_changes + + @lru_cache(maxsize=256) + def get_lineage(self, tax_id: str, as_of: datetime | None = None): + """ + Given a tax_id: return the taxonomy lineage. If `as_of` is specified, + return the taxonomy lineage as of that date. + """ + _profile_start = time.perf_counter() + + lineage = [] + + while True: + events = self.get_events(tax_id=tax_id, as_of=as_of) + + # find most recent event where the parent_id changed + parent = None + for event in events[::-1]: + if event.parent_id: + parent = event + break + + if parent is not None: + lineage.append(parent) + else: + break + + if parent.parent_id is None: + break + + tax_id = parent.parent_id + + self._profile("get_lineage", _profile_start, time.perf_counter()) + return lineage + + def iter_most_recent_events(self) -> dict[str, Event]: + """Get the most recent event (version) for each Tax ID in the database""" + + print("fetching most recent versions") + rows = self.cursor.execute(""" + SELECT * FROM taxonomy t1 + WHERE t1.version_date = ( + SELECT MAX(t2.version_date) + FROM taxonomy t2 + WHERE t2.tax_id = t1.tax_id + ) + """) + + return {r["tax_id"]: Event.from_dict(dict(r)) for r in rows} + + def _get_all_events_recursive( + self, tax_id: str, seen_tax_ids: set | None = None + ) -> list[Event]: + """ + Find all events for a given tax ID and any events for its parent's and + their parents, etc... + + TODO: also find all events for the children of a tax ID + + TODO: this sometimes finds irrelevant events like a new node is created + under a node in the lineage but isn't directly part of the current taxon's + lineage + """ + + events: list[Event] = [] + + if seen_tax_ids is None: + seen_tax_ids = set() + + if tax_id in seen_tax_ids: + return events + + seen_tax_ids.add(tax_id) + + for event in self.get_events(tax_id): + events.append(event) + seen_tax_ids.add(event.tax_id) + if event.parent_id and event.parent_id not in seen_tax_ids: + events.extend( + self._get_all_events_recursive( + tax_id=event.parent_id, seen_tax_ids=seen_tax_ids + ) + ) + + seen_tax_ids.add(event.parent_id) + + return sorted(events, key=lambda e: e.version_date) diff --git a/backend/test_models.py b/backend/test_models.py index dd22d8b..b18e68e 100644 --- a/backend/test_models.py +++ b/backend/test_models.py @@ -1,12 +1,12 @@ import pytest -from taxonomy_time_machine.models import Taxonomy, EventName, Event +from taxonomy_time_machine import TaxonomyTimeMachine, EventName, Event from datetime import datetime @pytest.fixture def db(): - return Taxonomy() + return TaxonomyTimeMachine() def test_search_names(db): diff --git a/backend/uv.lock b/backend/uv.lock index 520a6eb..b1a6efe 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -1,17 +1,31 @@ version = 1 -revision = 1 +revision = 2 requires-python = ">=3.11" +[[package]] +name = "alembic" +version = "1.16.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/52/72e791b75c6b1efa803e491f7cbab78e963695e76d4ada05385252927e76/alembic-1.16.4.tar.gz", hash = "sha256:efab6ada0dd0fae2c92060800e0bf5c1dc26af15a10e02fb4babff164b4725e2", size = 1968161, upload-time = "2025-07-10T16:17:20.192Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/62/96b5217b742805236614f05904541000f55422a6060a90d7fd4ce26c172d/alembic-1.16.4-py3-none-any.whl", hash = "sha256:b05e51e8e82efc1abd14ba2af6392897e145930c3e0a2faf2b0da2f7f7fd660d", size = 247026, upload-time = "2025-07-10T16:17:21.845Z" }, +] + [[package]] name = "apispec" -version = "6.7.1" +version = "6.8.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/26/ef0e72400707469058a7536f64d4e00e1a1c07a179acd00fb7e424dc9330/apispec-6.7.1.tar.gz", hash = "sha256:c01b8b6ff40ffedf55b79a67f9dd920e9b2fc3909aae116facf6c8372a08b933", size = 76714 } +sdist = { url = "https://files.pythonhosted.org/packages/c0/21/2b86a14a01c800308226c018fa489991cc40ffc227954d4f59d8a4090477/apispec-6.8.2.tar.gz", hash = "sha256:ce5b69b9fcf0250cb56ba0c1a52a75ff22c2f7c586654e57884399018c519f26", size = 77148, upload-time = "2025-05-12T14:57:52.649Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/09/83/8b06f94cb11119bbbe51ac33aaebac5637ef479418012daf0b6ba1cc1a18/apispec-6.7.1-py3-none-any.whl", hash = "sha256:d99e7a564f3871327c17b3e43726cc1e6ade2c97aa05706644a48818fc37999e", size = 30408 }, + { url = "https://files.pythonhosted.org/packages/f7/86/a2e7e0f6e9a82023108f52014eab389b184a06da486139fc5436d50b08d0/apispec-6.8.2-py3-none-any.whl", hash = "sha256:43c52ab6aa7d4056c1dfc6c81310c659b29f4db5858b3b4351819b77d3a1afff", size = 30508, upload-time = "2025-05-12T14:57:49.546Z" }, ] [package.optional-dependencies] @@ -23,46 +37,47 @@ marshmallow = [ name = "blinker" version = "1.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460 } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458 }, + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, ] [[package]] name = "click" -version = "8.1.7" +version = "8.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "flask" -version = "3.1.0" +version = "3.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "blinker" }, { name = "click" }, { name = "itsdangerous" }, { name = "jinja2" }, + { name = "markupsafe" }, { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/89/50/dff6380f1c7f84135484e176e0cac8690af72fa90e932ad2a0a60e28c69b/flask-3.1.0.tar.gz", hash = "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac", size = 680824 } +sdist = { url = "https://files.pythonhosted.org/packages/c0/de/e47735752347f4128bcf354e0da07ef311a78244eba9e3dc1d4a5ab21a98/flask-3.1.1.tar.gz", hash = "sha256:284c7b8f2f58cb737f0cf1c30fd7eaf0ccfcde196099d24ecede3fc2005aa59e", size = 753440, upload-time = "2025-05-13T15:01:17.447Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/47/93213ee66ef8fae3b93b3e29206f6b251e65c97bd91d8e1c5596ef15af0a/flask-3.1.0-py3-none-any.whl", hash = "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136", size = 102979 }, + { url = "https://files.pythonhosted.org/packages/3d/68/9d4508e893976286d2ead7f8f571314af6c2037af34853a30fd769c02e9d/flask-3.1.1-py3-none-any.whl", hash = "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c", size = 103305, upload-time = "2025-05-13T15:01:15.591Z" }, ] [[package]] @@ -73,14 +88,14 @@ dependencies = [ { name = "flask" }, { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/76/37/bcfa6c7d5eec777c4c7cf45ce6b27631cebe5230caf88d85eadd63edd37a/flask_cors-6.0.1.tar.gz", hash = "sha256:d81bcb31f07b0985be7f48406247e9243aced229b7747219160a0559edd678db", size = 13463 } +sdist = { url = "https://files.pythonhosted.org/packages/76/37/bcfa6c7d5eec777c4c7cf45ce6b27631cebe5230caf88d85eadd63edd37a/flask_cors-6.0.1.tar.gz", hash = "sha256:d81bcb31f07b0985be7f48406247e9243aced229b7747219160a0559edd678db", size = 13463, upload-time = "2025-06-11T01:32:08.518Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/f8/01bf35a3afd734345528f98d0353f2a978a476528ad4d7e78b70c4d149dd/flask_cors-6.0.1-py3-none-any.whl", hash = "sha256:c7b2cbfb1a31aa0d2e5341eea03a6805349f7a61647daee1a15c46bbe981494c", size = 13244 }, + { url = "https://files.pythonhosted.org/packages/17/f8/01bf35a3afd734345528f98d0353f2a978a476528ad4d7e78b70c4d149dd/flask_cors-6.0.1-py3-none-any.whl", hash = "sha256:c7b2cbfb1a31aa0d2e5341eea03a6805349f7a61647daee1a15c46bbe981494c", size = 13244, upload-time = "2025-06-11T01:32:07.352Z" }, ] [[package]] name = "flask-smorest" -version = "0.45.0" +version = "0.46.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "apispec", extra = ["marshmallow"] }, @@ -89,162 +104,261 @@ dependencies = [ { name = "webargs" }, { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/29/cc/ad32be9d2a2148000c4c845d234f12cee10d6ec9cc3724fe7a3ff9e93f99/flask_smorest-0.45.0.tar.gz", hash = "sha256:1490291a59e572be5c02e0a769589584180eceba65bc1949456760856b3e990b", size = 77686 } +sdist = { url = "https://files.pythonhosted.org/packages/cf/43/fcbe7a5971465f69ae83e4b2bbef329664791a4231269512f9b83a5e7122/flask_smorest-0.46.1.tar.gz", hash = "sha256:0c423a56df2e18556f8a89a64f67c48d7d057a788cde736a512f1c5cf7a61031", size = 76986, upload-time = "2025-04-25T22:21:33.368Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/08/3709fc8b81a5cbb574218c9feb6c5f35ccb5b0455231fae5ffd563b2adf9/flask_smorest-0.46.1-py3-none-any.whl", hash = "sha256:200820103b31c3b830f15c2049b3d116a1249225d8c10c38bc632d5aaed33b31", size = 32224, upload-time = "2025-04-25T22:21:31.924Z" }, +] + +[[package]] +name = "greenlet" +version = "3.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/92/bb85bd6e80148a4d2e0c59f7c0c2891029f8fd510183afc7d8d2feeed9b6/greenlet-3.2.3.tar.gz", hash = "sha256:8b0dd8ae4c0d6f5e54ee55ba935eeb3d735a9b58a8a1e5b5cbab64e01a39f365", size = 185752, upload-time = "2025-06-05T16:16:09.955Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/ff/bdfbb6a22550e8ad5fdcfa61c5c2ec2b5e0c1d1324466ff6511710b49cd6/flask_smorest-0.45.0-py3-none-any.whl", hash = "sha256:2b11c1e9de16b651291b963661011781d7bf0de28990d5469c05c84d000f101e", size = 32358 }, + { url = "https://files.pythonhosted.org/packages/fc/2e/d4fcb2978f826358b673f779f78fa8a32ee37df11920dc2bb5589cbeecef/greenlet-3.2.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:784ae58bba89fa1fa5733d170d42486580cab9decda3484779f4759345b29822", size = 270219, upload-time = "2025-06-05T16:10:10.414Z" }, + { url = "https://files.pythonhosted.org/packages/16/24/929f853e0202130e4fe163bc1d05a671ce8dcd604f790e14896adac43a52/greenlet-3.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0921ac4ea42a5315d3446120ad48f90c3a6b9bb93dd9b3cf4e4d84a66e42de83", size = 630383, upload-time = "2025-06-05T16:38:51.785Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b2/0320715eb61ae70c25ceca2f1d5ae620477d246692d9cc284c13242ec31c/greenlet-3.2.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d2971d93bb99e05f8c2c0c2f4aa9484a18d98c4c3bd3c62b65b7e6ae33dfcfaf", size = 642422, upload-time = "2025-06-05T16:41:35.259Z" }, + { url = "https://files.pythonhosted.org/packages/bd/49/445fd1a210f4747fedf77615d941444349c6a3a4a1135bba9701337cd966/greenlet-3.2.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c667c0bf9d406b77a15c924ef3285e1e05250948001220368e039b6aa5b5034b", size = 638375, upload-time = "2025-06-05T16:48:18.235Z" }, + { url = "https://files.pythonhosted.org/packages/7e/c8/ca19760cf6eae75fa8dc32b487e963d863b3ee04a7637da77b616703bc37/greenlet-3.2.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:592c12fb1165be74592f5de0d70f82bc5ba552ac44800d632214b76089945147", size = 637627, upload-time = "2025-06-05T16:13:02.858Z" }, + { url = "https://files.pythonhosted.org/packages/65/89/77acf9e3da38e9bcfca881e43b02ed467c1dedc387021fc4d9bd9928afb8/greenlet-3.2.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29e184536ba333003540790ba29829ac14bb645514fbd7e32af331e8202a62a5", size = 585502, upload-time = "2025-06-05T16:12:49.642Z" }, + { url = "https://files.pythonhosted.org/packages/97/c6/ae244d7c95b23b7130136e07a9cc5aadd60d59b5951180dc7dc7e8edaba7/greenlet-3.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:93c0bb79844a367782ec4f429d07589417052e621aa39a5ac1fb99c5aa308edc", size = 1114498, upload-time = "2025-06-05T16:36:46.598Z" }, + { url = "https://files.pythonhosted.org/packages/89/5f/b16dec0cbfd3070658e0d744487919740c6d45eb90946f6787689a7efbce/greenlet-3.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:751261fc5ad7b6705f5f76726567375bb2104a059454e0226e1eef6c756748ba", size = 1139977, upload-time = "2025-06-05T16:12:38.262Z" }, + { url = "https://files.pythonhosted.org/packages/66/77/d48fb441b5a71125bcac042fc5b1494c806ccb9a1432ecaa421e72157f77/greenlet-3.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:83a8761c75312361aa2b5b903b79da97f13f556164a7dd2d5448655425bd4c34", size = 297017, upload-time = "2025-06-05T16:25:05.225Z" }, + { url = "https://files.pythonhosted.org/packages/f3/94/ad0d435f7c48debe960c53b8f60fb41c2026b1d0fa4a99a1cb17c3461e09/greenlet-3.2.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:25ad29caed5783d4bd7a85c9251c651696164622494c00802a139c00d639242d", size = 271992, upload-time = "2025-06-05T16:11:23.467Z" }, + { url = "https://files.pythonhosted.org/packages/93/5d/7c27cf4d003d6e77749d299c7c8f5fd50b4f251647b5c2e97e1f20da0ab5/greenlet-3.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88cd97bf37fe24a6710ec6a3a7799f3f81d9cd33317dcf565ff9950c83f55e0b", size = 638820, upload-time = "2025-06-05T16:38:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7e/807e1e9be07a125bb4c169144937910bf59b9d2f6d931578e57f0bce0ae2/greenlet-3.2.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:baeedccca94880d2f5666b4fa16fc20ef50ba1ee353ee2d7092b383a243b0b0d", size = 653046, upload-time = "2025-06-05T16:41:36.343Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ab/158c1a4ea1068bdbc78dba5a3de57e4c7aeb4e7fa034320ea94c688bfb61/greenlet-3.2.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:be52af4b6292baecfa0f397f3edb3c6092ce071b499dd6fe292c9ac9f2c8f264", size = 647701, upload-time = "2025-06-05T16:48:19.604Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0d/93729068259b550d6a0288da4ff72b86ed05626eaf1eb7c0d3466a2571de/greenlet-3.2.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0cc73378150b8b78b0c9fe2ce56e166695e67478550769536a6742dca3651688", size = 649747, upload-time = "2025-06-05T16:13:04.628Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f6/c82ac1851c60851302d8581680573245c8fc300253fc1ff741ae74a6c24d/greenlet-3.2.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:706d016a03e78df129f68c4c9b4c4f963f7d73534e48a24f5f5a7101ed13dbbb", size = 605461, upload-time = "2025-06-05T16:12:50.792Z" }, + { url = "https://files.pythonhosted.org/packages/98/82/d022cf25ca39cf1200650fc58c52af32c90f80479c25d1cbf57980ec3065/greenlet-3.2.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:419e60f80709510c343c57b4bb5a339d8767bf9aef9b8ce43f4f143240f88b7c", size = 1121190, upload-time = "2025-06-05T16:36:48.59Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e1/25297f70717abe8104c20ecf7af0a5b82d2f5a980eb1ac79f65654799f9f/greenlet-3.2.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:93d48533fade144203816783373f27a97e4193177ebaaf0fc396db19e5d61163", size = 1149055, upload-time = "2025-06-05T16:12:40.457Z" }, + { url = "https://files.pythonhosted.org/packages/1f/8f/8f9e56c5e82eb2c26e8cde787962e66494312dc8cb261c460e1f3a9c88bc/greenlet-3.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:7454d37c740bb27bdeddfc3f358f26956a07d5220818ceb467a483197d84f849", size = 297817, upload-time = "2025-06-05T16:29:49.244Z" }, + { url = "https://files.pythonhosted.org/packages/b1/cf/f5c0b23309070ae93de75c90d29300751a5aacefc0a3ed1b1d8edb28f08b/greenlet-3.2.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:500b8689aa9dd1ab26872a34084503aeddefcb438e2e7317b89b11eaea1901ad", size = 270732, upload-time = "2025-06-05T16:10:08.26Z" }, + { url = "https://files.pythonhosted.org/packages/48/ae/91a957ba60482d3fecf9be49bc3948f341d706b52ddb9d83a70d42abd498/greenlet-3.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a07d3472c2a93117af3b0136f246b2833fdc0b542d4a9799ae5f41c28323faef", size = 639033, upload-time = "2025-06-05T16:38:53.983Z" }, + { url = "https://files.pythonhosted.org/packages/6f/df/20ffa66dd5a7a7beffa6451bdb7400d66251374ab40b99981478c69a67a8/greenlet-3.2.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:8704b3768d2f51150626962f4b9a9e4a17d2e37c8a8d9867bbd9fa4eb938d3b3", size = 652999, upload-time = "2025-06-05T16:41:37.89Z" }, + { url = "https://files.pythonhosted.org/packages/51/b4/ebb2c8cb41e521f1d72bf0465f2f9a2fd803f674a88db228887e6847077e/greenlet-3.2.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5035d77a27b7c62db6cf41cf786cfe2242644a7a337a0e155c80960598baab95", size = 647368, upload-time = "2025-06-05T16:48:21.467Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6a/1e1b5aa10dced4ae876a322155705257748108b7fd2e4fae3f2a091fe81a/greenlet-3.2.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2d8aa5423cd4a396792f6d4580f88bdc6efcb9205891c9d40d20f6e670992efb", size = 650037, upload-time = "2025-06-05T16:13:06.402Z" }, + { url = "https://files.pythonhosted.org/packages/26/f2/ad51331a157c7015c675702e2d5230c243695c788f8f75feba1af32b3617/greenlet-3.2.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c724620a101f8170065d7dded3f962a2aea7a7dae133a009cada42847e04a7b", size = 608402, upload-time = "2025-06-05T16:12:51.91Z" }, + { url = "https://files.pythonhosted.org/packages/26/bc/862bd2083e6b3aff23300900a956f4ea9a4059de337f5c8734346b9b34fc/greenlet-3.2.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:873abe55f134c48e1f2a6f53f7d1419192a3d1a4e873bace00499a4e45ea6af0", size = 1119577, upload-time = "2025-06-05T16:36:49.787Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/1fc0cc068cfde885170e01de40a619b00eaa8f2916bf3541744730ffb4c3/greenlet-3.2.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:024571bbce5f2c1cfff08bf3fbaa43bbc7444f580ae13b0099e95d0e6e67ed36", size = 1147121, upload-time = "2025-06-05T16:12:42.527Z" }, + { url = "https://files.pythonhosted.org/packages/27/1a/199f9587e8cb08a0658f9c30f3799244307614148ffe8b1e3aa22f324dea/greenlet-3.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5195fb1e75e592dd04ce79881c8a22becdfa3e6f500e7feb059b1e6fdd54d3e3", size = 297603, upload-time = "2025-06-05T16:20:12.651Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ca/accd7aa5280eb92b70ed9e8f7fd79dc50a2c21d8c73b9a0856f5b564e222/greenlet-3.2.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:3d04332dddb10b4a211b68111dabaee2e1a073663d117dc10247b5b1642bac86", size = 271479, upload-time = "2025-06-05T16:10:47.525Z" }, + { url = "https://files.pythonhosted.org/packages/55/71/01ed9895d9eb49223280ecc98a557585edfa56b3d0e965b9fa9f7f06b6d9/greenlet-3.2.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8186162dffde068a465deab08fc72c767196895c39db26ab1c17c0b77a6d8b97", size = 683952, upload-time = "2025-06-05T16:38:55.125Z" }, + { url = "https://files.pythonhosted.org/packages/ea/61/638c4bdf460c3c678a0a1ef4c200f347dff80719597e53b5edb2fb27ab54/greenlet-3.2.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f4bfbaa6096b1b7a200024784217defedf46a07c2eee1a498e94a1b5f8ec5728", size = 696917, upload-time = "2025-06-05T16:41:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/22/cc/0bd1a7eb759d1f3e3cc2d1bc0f0b487ad3cc9f34d74da4b80f226fde4ec3/greenlet-3.2.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:ed6cfa9200484d234d8394c70f5492f144b20d4533f69262d530a1a082f6ee9a", size = 692443, upload-time = "2025-06-05T16:48:23.113Z" }, + { url = "https://files.pythonhosted.org/packages/67/10/b2a4b63d3f08362662e89c103f7fe28894a51ae0bc890fabf37d1d780e52/greenlet-3.2.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:02b0df6f63cd15012bed5401b47829cfd2e97052dc89da3cfaf2c779124eb892", size = 692995, upload-time = "2025-06-05T16:13:07.972Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c6/ad82f148a4e3ce9564056453a71529732baf5448ad53fc323e37efe34f66/greenlet-3.2.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86c2d68e87107c1792e2e8d5399acec2487a4e993ab76c792408e59394d52141", size = 655320, upload-time = "2025-06-05T16:12:53.453Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4f/aab73ecaa6b3086a4c89863d94cf26fa84cbff63f52ce9bc4342b3087a06/greenlet-3.2.3-cp314-cp314-win_amd64.whl", hash = "sha256:8c47aae8fbbfcf82cc13327ae802ba13c9c36753b67e760023fd116bc124a62a", size = 301236, upload-time = "2025-06-05T16:15:20.111Z" }, ] [[package]] name = "iniconfig" -version = "2.0.0" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] [[package]] name = "itsdangerous" version = "2.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410 } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234 }, + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, ] [[package]] name = "jinja2" -version = "3.1.4" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "mako" +version = "1.3.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } +sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, + { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, ] [[package]] name = "markupsafe" version = "3.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, - { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, - { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, - { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, - { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, - { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, - { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, - { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, - { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, - { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, - { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, - { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, - { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, - { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, - { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, - { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, - { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, - { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, - { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, - { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, - { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, - { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, - { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, - { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, - { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, - { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, - { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, - { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, - { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, - { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, - { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, - { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, - { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, - { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, - { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, - { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, - { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, - { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, - { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, - { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, ] [[package]] name = "marshmallow" -version = "3.23.1" +version = "4.0.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6d/30/14d8609f65c8aeddddd3181c06d2c9582da6278f063b27c910bbf9903441/marshmallow-3.23.1.tar.gz", hash = "sha256:3a8dfda6edd8dcdbf216c0ede1d1e78d230a6dc9c5a088f58c4083b974a0d468", size = 177488 } +sdist = { url = "https://files.pythonhosted.org/packages/1e/ff/26df5a9f5ac57ccf693a5854916ab47243039d2aa9e0fe5f5a0331e7b74b/marshmallow-4.0.0.tar.gz", hash = "sha256:3b6e80aac299a7935cfb97ed01d1854fb90b5079430969af92118ea1b12a8d55", size = 220507, upload-time = "2025-04-17T02:25:54.925Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/a7/a78ff54e67ef92a3d12126b98eb98ab8abab3de4a8c46d240c87e514d6bb/marshmallow-3.23.1-py3-none-any.whl", hash = "sha256:fece2eb2c941180ea1b7fcbd4a83c51bfdd50093fdd3ad2585ee5e1df2508491", size = 49488 }, + { url = "https://files.pythonhosted.org/packages/d6/26/6cc45d156f44dbe1d5696d9e54042e4dcaf7b946c0b86df6a97d29706f32/marshmallow-4.0.0-py3-none-any.whl", hash = "sha256:e7b0528337e9990fd64950f8a6b3a1baabed09ad17a0dfb844d701151f92d203", size = 48420, upload-time = "2025-04-17T02:25:53.375Z" }, ] [[package]] name = "packaging" -version = "24.2" +version = "25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] [[package]] name = "pluggy" -version = "1.5.0" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "polars" -version = "1.13.1" +version = "1.31.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/75/33/6c5028a3a3253a89b73d8398b52ea357876340b5e5c6bca91633db251dad/polars-1.13.1.tar.gz", hash = "sha256:a8a7bb70aca0657939552a4505eccabb07c9d59d330d5a66409fe67295082860", size = 4125025 } +sdist = { url = "https://files.pythonhosted.org/packages/fd/f5/de1b5ecd7d0bd0dd87aa392937f759f9cc3997c5866a9a7f94eabf37cd48/polars-1.31.0.tar.gz", hash = "sha256:59a88054a5fc0135386268ceefdbb6a6cc012d21b5b44fed4f1d3faabbdcbf32", size = 4681224, upload-time = "2025-06-18T12:00:46.24Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/73/b6db1017f0f80290b408f7576886115ab7c26d98b137bb7c8d20333c1154/polars-1.13.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:9d5c74229fdc180fdbe99e4dc121a2ce0de6f0fcea2769a208f033112d5729dd", size = 34183338 }, - { url = "https://files.pythonhosted.org/packages/6e/ec/5d4b0f6cdbd63b75913bae18f2abc1b30f4e776eab00d646f282bd405aeb/polars-1.13.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:faa436c721179fca978a470ade1072acc5e510396a88ce7e3aa4fcc75186739f", size = 30092667 }, - { url = "https://files.pythonhosted.org/packages/ce/e8/9fd44ad4c091f911724f4cbe34f960c2e8016391e88f4da75ab0a2b83493/polars-1.13.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:405826a78c20721d0f47ee58bafdbd5311551c306cc52ff2e8dc0e2f5fc53d07", size = 35422073 }, - { url = "https://files.pythonhosted.org/packages/67/4e/a337779a9653ff271ef57484fba9e298c9be4ddcedd422ea1eb9fdaae65a/polars-1.13.1-cp39-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:792d9de49de6ebcfb137885e1643e09b35bcad1ae3bc86971f0d82a06372e1a4", size = 31798082 }, - { url = "https://files.pythonhosted.org/packages/45/25/b37b81c70595d1eda0f2c62b2e9163243168cf6a8c888f3836473e172083/polars-1.13.1-cp39-abi3-win_amd64.whl", hash = "sha256:060148c687920c7af2dc16a9de0aa6de293233f1a2634db503c497504fdb19ad", size = 35171062 }, + { url = "https://files.pythonhosted.org/packages/3d/6e/bdd0937653c1e7a564a09ae3bc7757ce83fedbf19da600c8b35d62c0182a/polars-1.31.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ccc68cd6877deecd46b13cbd2663ca89ab2a2cb1fe49d5cfc66a9cef166566d9", size = 34511354, upload-time = "2025-06-18T11:59:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/77/fe/81aaca3540c1a5530b4bc4fd7f1b6f77100243d7bb9b7ad3478b770d8b3e/polars-1.31.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:a94c5550df397ad3c2d6adc212e59fd93d9b044ec974dd3653e121e6487a7d21", size = 31377712, upload-time = "2025-06-18T11:59:45.104Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d9/5e2753784ea30d84b3e769a56f5e50ac5a89c129e87baa16ac0773eb4ef7/polars-1.31.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ada7940ed92bea65d5500ae7ac1f599798149df8faa5a6db150327c9ddbee4f1", size = 35050729, upload-time = "2025-06-18T11:59:48.538Z" }, + { url = "https://files.pythonhosted.org/packages/20/e8/a6bdfe7b687c1fe84bceb1f854c43415eaf0d2fdf3c679a9dc9c4776e462/polars-1.31.0-cp39-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:b324e6e3e8c6cc6593f9d72fe625f06af65e8d9d47c8686583585533a5e731e1", size = 32260836, upload-time = "2025-06-18T11:59:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f6/9d9ad9dc4480d66502497e90ce29efc063373e1598f4bd9b6a38af3e08e7/polars-1.31.0-cp39-abi3-win_amd64.whl", hash = "sha256:3fd874d3432fc932863e8cceff2cff8a12a51976b053f2eb6326a0672134a632", size = 35156211, upload-time = "2025-06-18T11:59:55.805Z" }, + { url = "https://files.pythonhosted.org/packages/40/4b/0673a68ac4d6527fac951970e929c3b4440c654f994f0c957bd5556deb38/polars-1.31.0-cp39-abi3-win_arm64.whl", hash = "sha256:62ef23bb9d10dca4c2b945979f9a50812ac4ace4ed9e158a6b5d32a7322e6f75", size = 31469078, upload-time = "2025-06-18T11:59:59.242Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] [[package]] name = "pytest" -version = "8.3.3" +version = "8.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.41" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 } +sdist = { url = "https://files.pythonhosted.org/packages/63/66/45b165c595ec89aa7dcc2c1cd222ab269bc753f1fc7a1e68f8481bd957bf/sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9", size = 9689424, upload-time = "2025-05-14T17:10:32.339Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, + { url = "https://files.pythonhosted.org/packages/37/4e/b00e3ffae32b74b5180e15d2ab4040531ee1bef4c19755fe7926622dc958/sqlalchemy-2.0.41-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6375cd674fe82d7aa9816d1cb96ec592bac1726c11e0cafbf40eeee9a4516b5f", size = 2121232, upload-time = "2025-05-14T17:48:20.444Z" }, + { url = "https://files.pythonhosted.org/packages/ef/30/6547ebb10875302074a37e1970a5dce7985240665778cfdee2323709f749/sqlalchemy-2.0.41-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f8c9fdd15a55d9465e590a402f42082705d66b05afc3ffd2d2eb3c6ba919560", size = 2110897, upload-time = "2025-05-14T17:48:21.634Z" }, + { url = "https://files.pythonhosted.org/packages/9e/21/59df2b41b0f6c62da55cd64798232d7349a9378befa7f1bb18cf1dfd510a/sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32f9dc8c44acdee06c8fc6440db9eae8b4af8b01e4b1aee7bdd7241c22edff4f", size = 3273313, upload-time = "2025-05-14T17:51:56.205Z" }, + { url = "https://files.pythonhosted.org/packages/62/e4/b9a7a0e5c6f79d49bcd6efb6e90d7536dc604dab64582a9dec220dab54b6/sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c11ceb9a1f482c752a71f203a81858625d8df5746d787a4786bca4ffdf71c6", size = 3273807, upload-time = "2025-05-14T17:55:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/39/d8/79f2427251b44ddee18676c04eab038d043cff0e764d2d8bb08261d6135d/sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:911cc493ebd60de5f285bcae0491a60b4f2a9f0f5c270edd1c4dbaef7a38fc04", size = 3209632, upload-time = "2025-05-14T17:51:59.384Z" }, + { url = "https://files.pythonhosted.org/packages/d4/16/730a82dda30765f63e0454918c982fb7193f6b398b31d63c7c3bd3652ae5/sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03968a349db483936c249f4d9cd14ff2c296adfa1290b660ba6516f973139582", size = 3233642, upload-time = "2025-05-14T17:55:29.901Z" }, + { url = "https://files.pythonhosted.org/packages/04/61/c0d4607f7799efa8b8ea3c49b4621e861c8f5c41fd4b5b636c534fcb7d73/sqlalchemy-2.0.41-cp311-cp311-win32.whl", hash = "sha256:293cd444d82b18da48c9f71cd7005844dbbd06ca19be1ccf6779154439eec0b8", size = 2086475, upload-time = "2025-05-14T17:56:02.095Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8e/8344f8ae1cb6a479d0741c02cd4f666925b2bf02e2468ddaf5ce44111f30/sqlalchemy-2.0.41-cp311-cp311-win_amd64.whl", hash = "sha256:3d3549fc3e40667ec7199033a4e40a2f669898a00a7b18a931d3efb4c7900504", size = 2110903, upload-time = "2025-05-14T17:56:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2a/f1f4e068b371154740dd10fb81afb5240d5af4aa0087b88d8b308b5429c2/sqlalchemy-2.0.41-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:81f413674d85cfd0dfcd6512e10e0f33c19c21860342a4890c3a2b59479929f9", size = 2119645, upload-time = "2025-05-14T17:55:24.854Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e8/c664a7e73d36fbfc4730f8cf2bf930444ea87270f2825efbe17bf808b998/sqlalchemy-2.0.41-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:598d9ebc1e796431bbd068e41e4de4dc34312b7aa3292571bb3674a0cb415dd1", size = 2107399, upload-time = "2025-05-14T17:55:28.097Z" }, + { url = "https://files.pythonhosted.org/packages/5c/78/8a9cf6c5e7135540cb682128d091d6afa1b9e48bd049b0d691bf54114f70/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a104c5694dfd2d864a6f91b0956eb5d5883234119cb40010115fd45a16da5e70", size = 3293269, upload-time = "2025-05-14T17:50:38.227Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/f74add3978c20de6323fb11cb5162702670cc7a9420033befb43d8d5b7a4/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6145afea51ff0af7f2564a05fa95eb46f542919e6523729663a5d285ecb3cf5e", size = 3303364, upload-time = "2025-05-14T17:51:49.829Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d4/c990f37f52c3f7748ebe98883e2a0f7d038108c2c5a82468d1ff3eec50b7/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b46fa6eae1cd1c20e6e6f44e19984d438b6b2d8616d21d783d150df714f44078", size = 3229072, upload-time = "2025-05-14T17:50:39.774Z" }, + { url = "https://files.pythonhosted.org/packages/15/69/cab11fecc7eb64bc561011be2bd03d065b762d87add52a4ca0aca2e12904/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41836fe661cc98abfae476e14ba1906220f92c4e528771a8a3ae6a151242d2ae", size = 3268074, upload-time = "2025-05-14T17:51:51.736Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/0c19ec16858585d37767b167fc9602593f98998a68a798450558239fb04a/sqlalchemy-2.0.41-cp312-cp312-win32.whl", hash = "sha256:a8808d5cf866c781150d36a3c8eb3adccfa41a8105d031bf27e92c251e3969d6", size = 2084514, upload-time = "2025-05-14T17:55:49.915Z" }, + { url = "https://files.pythonhosted.org/packages/7f/23/4c2833d78ff3010a4e17f984c734f52b531a8c9060a50429c9d4b0211be6/sqlalchemy-2.0.41-cp312-cp312-win_amd64.whl", hash = "sha256:5b14e97886199c1f52c14629c11d90c11fbb09e9334fa7bb5f6d068d9ced0ce0", size = 2111557, upload-time = "2025-05-14T17:55:51.349Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ad/2e1c6d4f235a97eeef52d0200d8ddda16f6c4dd70ae5ad88c46963440480/sqlalchemy-2.0.41-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443", size = 2115491, upload-time = "2025-05-14T17:55:31.177Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8d/be490e5db8400dacc89056f78a52d44b04fbf75e8439569d5b879623a53b/sqlalchemy-2.0.41-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc", size = 2102827, upload-time = "2025-05-14T17:55:34.921Z" }, + { url = "https://files.pythonhosted.org/packages/a0/72/c97ad430f0b0e78efaf2791342e13ffeafcbb3c06242f01a3bb8fe44f65d/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1", size = 3225224, upload-time = "2025-05-14T17:50:41.418Z" }, + { url = "https://files.pythonhosted.org/packages/5e/51/5ba9ea3246ea068630acf35a6ba0d181e99f1af1afd17e159eac7e8bc2b8/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a", size = 3230045, upload-time = "2025-05-14T17:51:54.722Z" }, + { url = "https://files.pythonhosted.org/packages/78/2f/8c14443b2acea700c62f9b4a8bad9e49fc1b65cfb260edead71fd38e9f19/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c153265408d18de4cc5ded1941dcd8315894572cddd3c58df5d5b5705b3fa28d", size = 3159357, upload-time = "2025-05-14T17:50:43.483Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b2/43eacbf6ccc5276d76cea18cb7c3d73e294d6fb21f9ff8b4eef9b42bbfd5/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f67766965996e63bb46cfbf2ce5355fc32d9dd3b8ad7e536a920ff9ee422e23", size = 3197511, upload-time = "2025-05-14T17:51:57.308Z" }, + { url = "https://files.pythonhosted.org/packages/fa/2e/677c17c5d6a004c3c45334ab1dbe7b7deb834430b282b8a0f75ae220c8eb/sqlalchemy-2.0.41-cp313-cp313-win32.whl", hash = "sha256:bfc9064f6658a3d1cadeaa0ba07570b83ce6801a1314985bf98ec9b95d74e15f", size = 2082420, upload-time = "2025-05-14T17:55:52.69Z" }, + { url = "https://files.pythonhosted.org/packages/e9/61/e8c1b9b6307c57157d328dd8b8348ddc4c47ffdf1279365a13b2b98b8049/sqlalchemy-2.0.41-cp313-cp313-win_amd64.whl", hash = "sha256:82ca366a844eb551daff9d2e6e7a9e5e76d2612c8564f58db6c19a726869c1df", size = 2108329, upload-time = "2025-05-14T17:55:54.495Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fc/9ba22f01b5cdacc8f5ed0d22304718d2c758fce3fd49a5372b886a86f37c/sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", size = 1911224, upload-time = "2025-05-14T17:39:42.154Z" }, ] [[package]] name = "taxonomy" version = "0.10.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/93/dc/150fdd3664738e5095e3bcb89cc3923b516f95fd36f3579bc4a7c8e5b2a3/taxonomy-0.10.1.tar.gz", hash = "sha256:a77ac372b1d9e4230a0fb0879707ee680327b7032d320a43434ebd15cd57536a", size = 113120 } +sdist = { url = "https://files.pythonhosted.org/packages/93/dc/150fdd3664738e5095e3bcb89cc3923b516f95fd36f3579bc4a7c8e5b2a3/taxonomy-0.10.1.tar.gz", hash = "sha256:a77ac372b1d9e4230a0fb0879707ee680327b7032d320a43434ebd15cd57536a", size = 113120, upload-time = "2025-04-18T18:34:12.439Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/6f/255cb8a14d05d629e5456a33e371e99d8a5fcb7da8b2afb15f305c502863/taxonomy-0.10.1-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:df470468411916165e83ac73ef9da5f79d47e55e2aec717f8dcb549ad12116b2", size = 498768 }, - { url = "https://files.pythonhosted.org/packages/ef/c0/e2206ae58cc4ad1a1c657620836d145b4e324ef5b31fc9ceef916b6a3584/taxonomy-0.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a79bff100e778f2488da68c40e69c25d8be0e77d36a6415fb646efefce52b680", size = 482267 }, - { url = "https://files.pythonhosted.org/packages/45/c7/7aa8eb23f7e45a1c4e8ff2ef4afb3b8dbc27b8694916fcdfd1322fefea04/taxonomy-0.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1dda295f30111a6a87ff22b3c5d29fa9a055a26668fdd25be0c121952f37509a", size = 541537 }, - { url = "https://files.pythonhosted.org/packages/7d/bf/77fb910333401617dbdcb7996bc2fff7473bcde57a56cf6ee0c3bfbc751a/taxonomy-0.10.1-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:21bccbe59cbba584ad93e5f35d768d90c1a03a428ff8a8f99a85092454450a2f", size = 498852 }, - { url = "https://files.pythonhosted.org/packages/20/f1/522bcd813be86300dd26e82d1fc5a82e0c53bcef9766c37666fe7c07b09a/taxonomy-0.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b9298e7b30cfbdd047b8129cd4ad0debee8c761211fe961d7023b736acdd6d3", size = 482369 }, - { url = "https://files.pythonhosted.org/packages/c1/2d/6658ff35b2d2b6d050916d813531e2d382172ea1bd2095260c75da009613/taxonomy-0.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2ba55c8ba67a31aab71a685d5dec05c027452fcb3aa01c66ba67cd0724831f3", size = 541728 }, - { url = "https://files.pythonhosted.org/packages/a8/53/c3fbf6480e10ac79807d0db931470996dc8f61d3ae57872b9c210d99a7c8/taxonomy-0.10.1-cp313-cp313-macosx_10_7_x86_64.whl", hash = "sha256:176c5293057aa790ac917355df10b7d7db0deed3f0a343b1235c8a525d91cb71", size = 498850 }, - { url = "https://files.pythonhosted.org/packages/57/9d/e5da5e87ebd76d7de40a3d6ec8f0377e2b7d13e610eaf0d2f8f2b192599d/taxonomy-0.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:605aa594d4ee1174a2f82472aa4507f687828a3584bb3041ab6b1f67f795fd7a", size = 482369 }, - { url = "https://files.pythonhosted.org/packages/73/d7/1d737d2df2324b78d0f632c473a2a3fa78fef561e05ce148ec859420e35e/taxonomy-0.10.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:019473a0394d2017ff6729a6bd479a283ec9666cef96f0a55d4b77074ba2f0dd", size = 541728 }, + { url = "https://files.pythonhosted.org/packages/20/6f/255cb8a14d05d629e5456a33e371e99d8a5fcb7da8b2afb15f305c502863/taxonomy-0.10.1-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:df470468411916165e83ac73ef9da5f79d47e55e2aec717f8dcb549ad12116b2", size = 498768, upload-time = "2025-04-18T18:34:06.679Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c0/e2206ae58cc4ad1a1c657620836d145b4e324ef5b31fc9ceef916b6a3584/taxonomy-0.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a79bff100e778f2488da68c40e69c25d8be0e77d36a6415fb646efefce52b680", size = 482267, upload-time = "2025-04-18T18:34:02.21Z" }, + { url = "https://files.pythonhosted.org/packages/45/c7/7aa8eb23f7e45a1c4e8ff2ef4afb3b8dbc27b8694916fcdfd1322fefea04/taxonomy-0.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1dda295f30111a6a87ff22b3c5d29fa9a055a26668fdd25be0c121952f37509a", size = 541537, upload-time = "2025-04-18T18:33:57.461Z" }, + { url = "https://files.pythonhosted.org/packages/7d/bf/77fb910333401617dbdcb7996bc2fff7473bcde57a56cf6ee0c3bfbc751a/taxonomy-0.10.1-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:21bccbe59cbba584ad93e5f35d768d90c1a03a428ff8a8f99a85092454450a2f", size = 498852, upload-time = "2025-04-18T18:34:08.253Z" }, + { url = "https://files.pythonhosted.org/packages/20/f1/522bcd813be86300dd26e82d1fc5a82e0c53bcef9766c37666fe7c07b09a/taxonomy-0.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b9298e7b30cfbdd047b8129cd4ad0debee8c761211fe961d7023b736acdd6d3", size = 482369, upload-time = "2025-04-18T18:34:03.767Z" }, + { url = "https://files.pythonhosted.org/packages/c1/2d/6658ff35b2d2b6d050916d813531e2d382172ea1bd2095260c75da009613/taxonomy-0.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2ba55c8ba67a31aab71a685d5dec05c027452fcb3aa01c66ba67cd0724831f3", size = 541728, upload-time = "2025-04-18T18:33:58.747Z" }, + { url = "https://files.pythonhosted.org/packages/a8/53/c3fbf6480e10ac79807d0db931470996dc8f61d3ae57872b9c210d99a7c8/taxonomy-0.10.1-cp313-cp313-macosx_10_7_x86_64.whl", hash = "sha256:176c5293057aa790ac917355df10b7d7db0deed3f0a343b1235c8a525d91cb71", size = 498850, upload-time = "2025-04-18T18:34:10.691Z" }, + { url = "https://files.pythonhosted.org/packages/57/9d/e5da5e87ebd76d7de40a3d6ec8f0377e2b7d13e610eaf0d2f8f2b192599d/taxonomy-0.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:605aa594d4ee1174a2f82472aa4507f687828a3584bb3041ab6b1f67f795fd7a", size = 482369, upload-time = "2025-04-18T18:34:05.168Z" }, + { url = "https://files.pythonhosted.org/packages/73/d7/1d737d2df2324b78d0f632c473a2a3fa78fef561e05ce148ec859420e35e/taxonomy-0.10.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:019473a0394d2017ff6729a6bd479a283ec9666cef96f0a55d4b77074ba2f0dd", size = 541728, upload-time = "2025-04-18T18:34:00.392Z" }, ] [[package]] @@ -252,49 +366,68 @@ name = "taxonomy-time-machine" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "alembic" }, { name = "flask" }, { name = "flask-cors" }, { name = "flask-smorest" }, { name = "polars" }, - { name = "pytest" }, + { name = "sqlalchemy" }, { name = "taxonomy" }, { name = "tqdm" }, ] +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + [package.metadata] requires-dist = [ + { name = "alembic", specifier = ">=1.14.0" }, { name = "flask", specifier = ">=3.1.0" }, { name = "flask-cors", specifier = ">=4.0.0" }, { name = "flask-smorest", specifier = ">=0.45.0" }, - { name = "polars", specifier = ">=1.13.1" }, - { name = "pytest", specifier = ">=8.3.3" }, + { name = "polars" }, + { name = "sqlalchemy", specifier = ">=2.0.0" }, { name = "taxonomy", specifier = ">=0.10.1" }, { name = "tqdm", specifier = ">=4.67.0" }, ] +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=8.3.3" }] + [[package]] name = "tqdm" -version = "4.67.0" +version = "4.67.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e8/4f/0153c21dc5779a49a0598c445b1978126b1344bab9ee71e53e44877e14e0/tqdm-4.67.0.tar.gz", hash = "sha256:fe5a6f95e6fe0b9755e9469b77b9c3cf850048224ecaa8293d7d2d31f97d869a", size = 169739 } +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/78/57043611a16c655c8350b4c01b8d6abfb38cc2acb475238b62c2146186d7/tqdm-4.67.0-py3-none-any.whl", hash = "sha256:0cd8af9d56911acab92182e88d763100d4788bdf421d251616040cc4d44863be", size = 78590 }, + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, ] [[package]] name = "webargs" -version = "8.6.0" +version = "8.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "marshmallow" }, { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8c/51/e9ee5d8315864adf65e92f858f826514538e30db542d4782dd94c2418464/webargs-8.6.0.tar.gz", hash = "sha256:b8d098ab92bd74c659eca705afa31d681475f218cb15c1e57271fa2103c0547a", size = 96610 } +sdist = { url = "https://files.pythonhosted.org/packages/a8/02/d27619cb81e0e136e27276e8bd48c5ebb38272b0920ce43da0f61c598879/webargs-8.7.0.tar.gz", hash = "sha256:0c617dec19ed4f1ff6b247cd73855e949d87052d71900938b71f0cafd92f191b", size = 96803, upload-time = "2025-04-18T20:56:55.591Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/bb/b9b77adeecffd7b41615a7ebd607ac28bd9e09f357d31ce68073b77f0f30/webargs-8.6.0-py3-none-any.whl", hash = "sha256:83da4d7105643d0a50499b06d98a6ade1a330ce66d039eaa51f715172c704aba", size = 31831 }, + { url = "https://files.pythonhosted.org/packages/26/3f/0f68665037bc10d87cf18bcd094a53ad6496ffe44faff721481060fd9149/webargs-8.7.0-py3-none-any.whl", hash = "sha256:4571de9ff5aac98ef528d9cecd7dbc0e05c0e9149e8293a01d1d1398abfcf780", size = 31795, upload-time = "2025-04-18T20:56:53.786Z" }, ] [[package]] @@ -304,7 +437,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925 } +sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925, upload-time = "2024-11-08T15:52:18.093Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498 }, + { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498, upload-time = "2024-11-08T15:52:16.132Z" }, ]