diff --git a/docker-compose.yaml b/docker-compose.yaml index 7f9ed634..b020541c 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -154,11 +154,13 @@ services: environment: ENV: "DEV" PORT: "7010" - DATABASE_HOST: ${DATABASE_HOST} - DATABASE_PORT: ${DATABASE_PORT} - DATABASE_NAME: ${DATABASE_NAME} - DATABASE_USER: ${DATABASE_USER} - DATABASE_PASSWORD: ${DATABASE_PASSWORD} + # Query reads telemetry + its own metadata tables from ClickHouse over the + # HTTP interface (clickhouse-connect), so port 8123 — gr26 uses native 9000. + CLICKHOUSE_HOST: "clickhouse" + CLICKHOUSE_PORT: "8123" + CLICKHOUSE_USER: "default" + CLICKHOUSE_PASSWORD: "" + CLICKHOUSE_DATABASE: "mapache" KERBECS_ENDPOINT: "http://kerbecs:10300" KERBECS_USER: "admin" KERBECS_PASSWORD: "admin" diff --git a/example.env b/example.env index ea7dbfb2..74022334 100644 --- a/example.env +++ b/example.env @@ -8,6 +8,14 @@ DATABASE_NAME="mapache" DATABASE_USER="postgres" DATABASE_PASSWORD="password" +# Query service telemetry + metadata tables live in ClickHouse, reached over the +# HTTP interface via clickhouse-connect (port 8123; gr26 uses native 9000). +CLICKHOUSE_HOST="clickhouse" +CLICKHOUSE_PORT="8123" +CLICKHOUSE_USER="default" +CLICKHOUSE_PASSWORD="" +CLICKHOUSE_DATABASE="mapache" + SENTINEL_URL="https://sentinel-api.gauchoracing.com" SENTINEL_JWKS_URL="https://sso.gauchoracing.com/.well-known/jwks.json" SENTINEL_CLIENT_ID="z6V9NREjMFhf" diff --git a/query/Dockerfile.dev b/query/Dockerfile.dev index 1f416860..28fe41ca 100644 --- a/query/Dockerfile.dev +++ b/query/Dockerfile.dev @@ -4,4 +4,8 @@ RUN pip install uv WORKDIR /app/query -CMD ["uv", "run", "uvicorn", "query.main:create_app", "--host", "0.0.0.0", "--port", "7010", "--reload"] +# Only watch the application package. The bind-mounted /app/query also holds +# .venv, scripts/, tests/ and .pytest_cache; watching those makes `uv run` +# touching the venv trigger an endless reload loop, taking the service down and +# dropping it from Rincon's registry (gateway then 404s /query/* routes). +CMD ["uv", "run", "uvicorn", "query.main:create_app", "--host", "0.0.0.0", "--port", "7010", "--reload", "--reload-dir", "/app/query/query"] diff --git a/query/pyproject.toml b/query/pyproject.toml index 8ad414f1..5f164e4c 100644 --- a/query/pyproject.toml +++ b/query/pyproject.toml @@ -13,8 +13,7 @@ dependencies = [ "fastapi>=0.115.10,<0.116.0", "uvicorn>=0.34.0,<0.35.0", "dotenv>=0.9.9,<0.10.0", - "sqlalchemy>=2.0.38,<3.0.0", - "psycopg2-binary>=2.9.10,<3.0.0", + "clickhouse-connect>=0.8.0,<0.9.0", "numpy>=2.2.3,<3.0.0", "pandas>=2.2.3,<3.0.0", "loguru>=0.7.3,<0.8.0", @@ -28,6 +27,12 @@ dependencies = [ [project.scripts] query = "query.main:main" +[dependency-groups] +dev = [ + "pytest>=8.0.0,<9.0.0", + "httpx>=0.27.0,<0.28.0", +] + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" diff --git a/query/query/config/config.py b/query/query/config/config.py index 90ab3f61..8fa2d083 100644 --- a/query/query/config/config.py +++ b/query/query/config/config.py @@ -1,7 +1,5 @@ import os -from sqlalchemy import URL - class Config: """Configuration settings for the application""" @@ -9,12 +7,18 @@ class Config: VERSION: str = "3.3.0" PORT: int = int(os.getenv('PORT', 7000)) - # Database settings - DATABASE_HOST: str = os.getenv('DATABASE_HOST') - DATABASE_PORT: int = int(os.getenv('DATABASE_PORT')) - DATABASE_USER: str = os.getenv('DATABASE_USER') - DATABASE_PASSWORD: str = os.getenv('DATABASE_PASSWORD') - DATABASE_NAME: str = os.getenv('DATABASE_NAME') + # ClickHouse settings. Telemetry (signal) plus the query service's own + # metadata tables (signal_definition, query_log, query_token) all live in + # ClickHouse now. The query service talks to it over the HTTP interface + # (port 8123) via clickhouse-connect — distinct from gr26, which uses the + # native protocol (9000) via clickhouse-go. + CLICKHOUSE_HOST: str = os.getenv('CLICKHOUSE_HOST') + CLICKHOUSE_PORT: int = int(os.getenv('CLICKHOUSE_PORT', 8123)) + CLICKHOUSE_USER: str = os.getenv('CLICKHOUSE_USER') + CLICKHOUSE_PASSWORD: str = os.getenv('CLICKHOUSE_PASSWORD') + CLICKHOUSE_DATABASE: str = os.getenv('CLICKHOUSE_DATABASE') + # Set CLICKHOUSE_SECURE=true when the HTTP endpoint is TLS (port 8443). + CLICKHOUSE_SECURE: bool = os.getenv('CLICKHOUSE_SECURE', 'false').lower() == 'true' # Kerbecs admin endpoint — used to resolve service-to-service routes. KERBECS_ENDPOINT: str = os.getenv('KERBECS_ENDPOINT') @@ -26,19 +30,3 @@ class Config: SENTINEL_URL: str = os.getenv('SENTINEL_URL') SENTINEL_JWKS_URL: str = os.getenv('SENTINEL_JWKS_URL') SENTINEL_CLIENT_ID: str = os.getenv('SENTINEL_CLIENT_ID') - - @staticmethod - def get_database_url() -> URL: - # Build via URL.create rather than an f-string so credentials with - # special characters (e.g. '@', '%', or non-ASCII bytes in the - # password) are escaped instead of corrupting the DSN — an unescaped - # '@' in the password otherwise bleeds into the host portion. - return URL.create( - "postgresql+psycopg2", - username=Config.DATABASE_USER, - password=Config.DATABASE_PASSWORD, - host=Config.DATABASE_HOST, - port=Config.DATABASE_PORT, - database=Config.DATABASE_NAME, - ) - diff --git a/query/query/database/connection.py b/query/query/database/connection.py index e10c4232..7efa319e 100644 --- a/query/query/database/connection.py +++ b/query/query/database/connection.py @@ -1,73 +1,104 @@ -from query.model.base import Base -from sqlalchemy import create_engine -from sqlalchemy.orm import scoped_session, sessionmaker +"""ClickHouse connection + schema bootstrap for the query service. + +The signal/telemetry tables (signal, gr26_can, ping) are created and written by +gr26; this service only reads them. The three relational metadata tables the +query service owns — signal_definition, query_log, query_token — used to live in +Postgres and are now created here in ClickHouse: + +- query_log is append-only, so a plain MergeTree. +- query_token and signal_definition are mutable by id (token revoke rewrites + expires_at; definitions get re-seeded), so ReplacingMergeTree keyed on id with + an updated_at version column. Readers use FINAL to collapse to the latest row. +""" + +import clickhouse_connect +from clickhouse_connect.driver.client import Client + from query.config.config import Config -from contextlib import contextmanager -DATABASE_URL = Config.get_database_url() +_client: Client | None = None + +SIGNAL_DEFINITION_DDL = """ +CREATE TABLE IF NOT EXISTS signal_definition ( + id String, + vehicle_type String, + name String, + description String, + updated_at DateTime64(6, 'UTC') DEFAULT now64(6) +) ENGINE = ReplacingMergeTree(updated_at) ORDER BY id +""" + +QUERY_LOG_DDL = """ +CREATE TABLE IF NOT EXISTS query_log ( + id String, + user_id String, + parameters String, + status_code Int32, + latency Int32, + error_message String, + timestamp DateTime64(6, 'UTC') DEFAULT now64(6) +) ENGINE = MergeTree ORDER BY (timestamp, id) +""" + +QUERY_TOKEN_DDL = """ +CREATE TABLE IF NOT EXISTS query_token ( + id String, + user_id String, + created_at DateTime64(6, 'UTC') DEFAULT now64(6), + expires_at DateTime64(6, 'UTC'), + updated_at DateTime64(6, 'UTC') DEFAULT now64(6) +) ENGINE = ReplacingMergeTree(updated_at) ORDER BY id +""" + + +def _build_client() -> Client: + return clickhouse_connect.get_client( + host=Config.CLICKHOUSE_HOST, + port=Config.CLICKHOUSE_PORT, + username=Config.CLICKHOUSE_USER, + password=Config.CLICKHOUSE_PASSWORD or "", + database=Config.CLICKHOUSE_DATABASE, + secure=Config.CLICKHOUSE_SECURE, + ) -db_session = None def init_db(): - """Initialize the database session""" - if not Config.DATABASE_HOST: - raise ValueError("DATABASE_HOST is not set") - elif not Config.DATABASE_PORT: - raise ValueError("DATABASE_PORT is not set") - elif not Config.DATABASE_USER: - raise ValueError("DATABASE_USER is not set") - elif not Config.DATABASE_PASSWORD: - raise ValueError("DATABASE_PASSWORD is not set") - elif not Config.DATABASE_NAME: - raise ValueError("DATABASE_NAME is not set") - else: - global db_session - engine = create_engine(DATABASE_URL) - db_session = scoped_session( - sessionmaker( - autocommit=False, - autoflush=False, - expire_on_commit=False, - bind=engine - ) - ) - - from query.model.log import QueryLog - from query.model.token import QueryToken - from query.model.signal_definition import SignalDefinition - - # Create all tables - Base.metadata.create_all(bind=engine) - print("Database initialized") + """Open the ClickHouse client and create the metadata tables.""" + if not Config.CLICKHOUSE_HOST: + raise ValueError("CLICKHOUSE_HOST is not set") + elif not Config.CLICKHOUSE_PORT: + raise ValueError("CLICKHOUSE_PORT is not set") + elif not Config.CLICKHOUSE_USER: + raise ValueError("CLICKHOUSE_USER is not set") + elif not Config.CLICKHOUSE_DATABASE: + raise ValueError("CLICKHOUSE_DATABASE is not set") + + global _client + _client = _build_client() + for ddl in (SIGNAL_DEFINITION_DDL, QUERY_LOG_DDL, QUERY_TOKEN_DDL): + _client.command(ddl) + print("Database initialized") + def init_test_db(): - global db_session - engine = create_engine(DATABASE_URL) - db_session = scoped_session( - sessionmaker( - autocommit=False, - autoflush=False, - expire_on_commit=False, - bind=engine - ) - ) + global _client + _client = _build_client() + + +def get_client() -> Client: + """Return the shared ClickHouse client. + + clickhouse-connect's Client is safe to share across threads (it sits on a + thread-safe urllib3 pool), so one module-level instance serves all requests. + """ + if _client is None: + raise ValueError("Database client is not initialized") + return _client -@contextmanager -def get_db(): - """Get the database session with proper error handling""" - if not db_session: - raise ValueError("Database session is not initialized") - - try: - yield db_session - db_session.commit() - except Exception as e: - db_session.rollback() - raise e - finally: - db_session.remove() def shutdown_session(exception=None): - """Remove the session at the end of request""" - if db_session: - db_session.remove() \ No newline at end of file + """Close the client (best-effort) on shutdown.""" + global _client + if _client is not None: + _client.close() + _client = None diff --git a/query/query/model/log.py b/query/query/model/log.py index 4c6d3428..c3f025f9 100644 --- a/query/query/model/log.py +++ b/query/query/model/log.py @@ -1,18 +1,17 @@ -from sqlalchemy import Column, Integer, String, DateTime, Text +from dataclasses import dataclass from datetime import datetime -from datetime import timezone -from query.model.base import Base +from typing import Optional -class QueryLog(Base): - __tablename__ = "query_log" - id = Column(String(255), primary_key=True) - user_id = Column(String(255)) - parameters = Column(Text) - status_code = Column(Integer) - latency = Column(Integer) - error_message = Column(Text) - timestamp = Column(DateTime, default=lambda: datetime.now(timezone.utc)) +@dataclass +class QueryLog: + user_id: Optional[str] = None + parameters: Optional[str] = None + status_code: Optional[int] = None + latency: Optional[int] = None + error_message: Optional[str] = None + id: Optional[str] = None + timestamp: Optional[datetime] = None def to_dict(self): return { diff --git a/query/query/model/signal_definition.py b/query/query/model/signal_definition.py index 64ff220e..1e1a5034 100644 --- a/query/query/model/signal_definition.py +++ b/query/query/model/signal_definition.py @@ -1,13 +1,13 @@ -from sqlalchemy import Column, String, Text -from query.model.base import Base +from dataclasses import dataclass +from typing import Optional -class SignalDefinition(Base): - __tablename__ = "signal_definition" - id = Column(String(255), primary_key=True) - vehicle_type = Column(String(255)) - name = Column(String(255)) - description = Column(Text) +@dataclass +class SignalDefinition: + id: Optional[str] = None + vehicle_type: Optional[str] = None + name: Optional[str] = None + description: Optional[str] = None def to_dict(self): return { diff --git a/query/query/model/token.py b/query/query/model/token.py index 20a18da7..f871b3cf 100644 --- a/query/query/model/token.py +++ b/query/query/model/token.py @@ -1,15 +1,14 @@ -from sqlalchemy import Column, String, DateTime +from dataclasses import dataclass from datetime import datetime -from datetime import timezone -from query.model.base import Base +from typing import Optional -class QueryToken(Base): - __tablename__ = "query_token" - id = Column(String(255), primary_key=True) - user_id = Column(String(255)) - created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) - expires_at = Column(DateTime) +@dataclass +class QueryToken: + id: Optional[str] = None + user_id: Optional[str] = None + created_at: Optional[datetime] = None + expires_at: Optional[datetime] = None def to_dict(self): return { @@ -17,4 +16,4 @@ def to_dict(self): "user_id": self.user_id, "created_at": self.created_at.isoformat() + "Z" if self.created_at else None, "expires_at": self.expires_at.isoformat() + "Z" if self.expires_at else None - } \ No newline at end of file + } diff --git a/query/query/routes/query.py b/query/query/routes/query.py index 9a7ce66e..8492e2ba 100644 --- a/query/query/routes/query.py +++ b/query/query/routes/query.py @@ -1,4 +1,5 @@ -from datetime import datetime +from datetime import datetime, timedelta +from zoneinfo import ZoneInfo, ZoneInfoNotFoundError from fastapi import APIRouter, Query, Response, Header from typing import Annotated from loguru import logger @@ -10,6 +11,7 @@ from query.model.log import QueryLog from query.service.auth import AuthService from query.service.log import create_log +from query.service.cluster import get_clusters, get_data_dates, get_signal_names from query.service.query import query_signals, merge_signals from query.service.token import get_token_by_id, validate_token from query.service.trip import get_trip_by_id @@ -29,6 +31,7 @@ async def get_signals( merge: Annotated[str | None, Query(enum=['smallest', 'largest'])] = 'smallest', fill: Annotated[str | None, Query(enum=['none', 'forward', 'backward', 'linear', 'time'])] = 'none', tolerance: Annotated[int | None, Query()] = 50, + max_points: Annotated[int | None, Query()] = None, export: Annotated[str | None, Query(enum=['csv', 'json', 'parquet'])] = 'json' ): user_id = None @@ -117,7 +120,7 @@ async def get_signals( ) start_time = datetime.now() - dfs = query_signals(vehicle_id=vehicle_id, signals=signals.split(","), start=start, end=end) + dfs = query_signals(vehicle_id=vehicle_id, signals=signals.split(","), start=start, end=end, max_points=max_points) merged_df, metadata = merge_signals(*dfs, strategy=merge, tolerance=tolerance, fill=fill) @@ -134,7 +137,7 @@ async def get_signals( create_log(QueryLog( user_id=user_id, - parameters=f"vehicle_id={vehicle_id}, signals={signals}, start={start}, end={end}, merge={merge}, fill={fill}, tolerance={tolerance}, export={export}", + parameters=f"vehicle_id={vehicle_id}, signals={signals}, start={start}, end={end}, merge={merge}, fill={fill}, tolerance={tolerance}, max_points={max_points}, export={export}", latency=metadata.query_latency, status_code=200, error_message="", @@ -179,7 +182,7 @@ async def get_signals( if user_id is not None: create_log(QueryLog( user_id=user_id, - parameters=f"vehicle_id={vehicle_id}, signals={signals}, start={start}, end={end}, merge={merge}, fill={fill}, tolerance={tolerance}, export={export}", + parameters=f"vehicle_id={vehicle_id}, signals={signals}, start={start}, end={end}, merge={merge}, fill={fill}, tolerance={tolerance}, max_points={max_points}, export={export}", latency=0, status_code=500, error_message=str(e), @@ -190,3 +193,138 @@ async def get_signals( "message": str(e), } ) + + +def _authenticate(authorization: str | None) -> str | None: + """Return the authenticated user id, or None if unauthorized.""" + if Config.SKIP_AUTH_CHECK: + return "mock-user" + if authorization and "Bearer " in authorization: + auth_token = authorization.split("Bearer ")[1] + return AuthService.get_user_id_from_token(auth_token) + return None + + +@router.get("/signals/names") +async def get_signal_names_route( + authorization: str = Header(None), + vehicle_id: Annotated[str | None, Query()] = None, + start: Annotated[str | None, Query()] = None, + end: Annotated[str | None, Query()] = None, +): + try: + user_id = _authenticate(authorization) + if user_id is None: + return JSONResponse( + status_code=401, + content={"message": "you are not authorized to access this resource"}, + ) + + if vehicle_id is None: + return JSONResponse( + status_code=400, + content={"message": "vehicle_id is required"}, + ) + + for label, value in (("start", start), ("end", end)): + if value is not None: + try: + pd.to_datetime(value) + except ValueError: + return JSONResponse( + status_code=400, + content={"message": f"invalid {label} timestamp format"}, + ) + + names = get_signal_names(vehicle_id=vehicle_id, start=start, end=end) + return JSONResponse(status_code=200, content={"data": names}) + + except Exception as e: + logger.error(traceback.format_exc()) + return JSONResponse(status_code=500, content={"message": str(e)}) + + +@router.get("/clusters") +async def get_clusters_route( + authorization: str = Header(None), + vehicle_id: Annotated[str | None, Query()] = None, + gap: Annotated[int | None, Query()] = 30, + date: Annotated[str | None, Query()] = None, + tz: Annotated[str, Query()] = "UTC", +): + try: + user_id = _authenticate(authorization) + if user_id is None: + return JSONResponse( + status_code=401, + content={"message": "you are not authorized to access this resource"}, + ) + + if gap is not None and gap <= 0: + return JSONResponse( + status_code=400, + content={"message": "gap must be a positive number of seconds"}, + ) + + # Scope the scan to a single calendar day in the caller's timezone. The + # day boundaries are the local midnights of `date`, converted to instants + # so they line up with the timestamptz `produced_at` column. + start = end = None + if date is not None: + try: + tzinfo = ZoneInfo(tz) + day = datetime.strptime(date, "%Y-%m-%d").replace(tzinfo=tzinfo) + except (ValueError, ZoneInfoNotFoundError): + return JSONResponse( + status_code=400, + content={"message": "invalid date or timezone"}, + ) + start = day + end = day + timedelta(days=1) + + clusters = get_clusters( + vehicle_id=vehicle_id, gap_seconds=gap or 30, start=start, end=end + ) + return JSONResponse( + status_code=200, + content={"data": [c.to_dict() for c in clusters]}, + ) + + except Exception as e: + logger.error(traceback.format_exc()) + return JSONResponse(status_code=500, content={"message": str(e)}) + + +@router.get("/clusters/dates") +async def get_cluster_dates_route( + authorization: str = Header(None), + vehicle_id: Annotated[str | None, Query()] = None, + tz: Annotated[str, Query()] = "UTC", +): + try: + user_id = _authenticate(authorization) + if user_id is None: + return JSONResponse( + status_code=401, + content={"message": "you are not authorized to access this resource"}, + ) + + if vehicle_id is None: + return JSONResponse( + status_code=400, + content={"message": "vehicle_id is required"}, + ) + + try: + ZoneInfo(tz) + except ZoneInfoNotFoundError: + return JSONResponse( + status_code=400, content={"message": "invalid timezone"} + ) + + dates = get_data_dates(vehicle_id=vehicle_id, tz=tz) + return JSONResponse(status_code=200, content={"data": dates}) + + except Exception as e: + logger.error(traceback.format_exc()) + return JSONResponse(status_code=500, content={"message": str(e)}) diff --git a/query/query/routes/signal_definition.py b/query/query/routes/signal_definition.py index d4baa122..ed53d862 100644 --- a/query/query/routes/signal_definition.py +++ b/query/query/routes/signal_definition.py @@ -37,11 +37,10 @@ async def get_signal_definitions( definitions = get_signal_definitions_by_vehicle_type(vehicle_type) else: definitions = get_all_signal_definitions() - return JSONResponse( - status_code=200, - content=[definition.to_dict() for definition in definitions] - ) - + content = [definition.to_dict() for definition in definitions] + + return JSONResponse(status_code=200, content=content) + except Exception as e: logger.error(traceback.format_exc()) return JSONResponse( diff --git a/query/query/service/cluster.py b/query/query/service/cluster.py new file mode 100644 index 00000000..c386a348 --- /dev/null +++ b/query/query/service/cluster.py @@ -0,0 +1,223 @@ +"""Raw-signal browsing: distinct signal names and contiguous data clusters. + +Lapache is the tool used to *create* sessions, so it must browse all raw signal +data regardless of whether a session already covers it. A "cluster" is a +contiguous block of signal data separated from the next block by a gap larger +than `gap_seconds`. Clusters are derived from minute-bucketed timestamps of a +single anchor signal per vehicle, which keeps the query cheap on the large +signal table. +""" + +import time +from dataclasses import dataclass +from datetime import datetime, timezone +from threading import Lock +from typing import Any + +from loguru import logger + +from query.database.connection import get_client + + +def _utc_iso(dt: datetime) -> str: + """Render a datetime as a UTC ISO string for parseDateTime64BestEffort. + + Window bounds arrive tz-aware (built from the caller's timezone); normalise + to UTC so the string compares correctly against the UTC `produced_at`. + """ + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + return dt.astimezone(timezone.utc).isoformat() + +DEFAULT_GAP_SECONDS = 30 + +# The anchor signal for a vehicle is derived from a full per-vehicle aggregate +# (GROUP BY name ORDER BY COUNT(*)), which has to read every row for the +# vehicle. It is effectively static, yet it was being recomputed on every +# /clusters and /clusters/dates request. Cache it per vehicle with a short TTL +# so new ingest is still picked up eventually without paying the scan each call. +_ANCHOR_TTL_SECONDS = 300 +_anchor_cache: dict[str, tuple[float, str | None]] = {} +_anchor_lock = Lock() + + +@dataclass +class Cluster: + """A contiguous block of signal data for a vehicle.""" + + vehicle_id: str + start_time: datetime + end_time: datetime + + def to_dict(self) -> dict[str, Any]: + return { + "vehicle_id": self.vehicle_id, + "start_time": self.start_time.isoformat(), + "end_time": self.end_time.isoformat(), + } + + +def merge_buckets( + vehicle_id: str, + buckets: list[tuple[datetime, datetime]], + gap_seconds: int = DEFAULT_GAP_SECONDS, +) -> list[Cluster]: + """Merge time-ordered (min, max) timestamp buckets into clusters. + + A new cluster begins whenever the gap between the previous bucket's end and + the next bucket's start exceeds `gap_seconds`. Pure function — no DB access. + """ + if not buckets: + return [] + + clusters: list[Cluster] = [] + cs, ce = buckets[0] + for bmin, bmax in buckets[1:]: + if (bmin - ce).total_seconds() > gap_seconds: + clusters.append(Cluster(vehicle_id, cs, ce)) + cs = bmin + ce = bmax + clusters.append(Cluster(vehicle_id, cs, ce)) + return clusters + + +def get_signal_names( + vehicle_id: str, start: str | None = None, end: str | None = None +) -> list[str]: + """Return distinct signal names available for a vehicle in a time window.""" + if not vehicle_id: + raise ValueError("Vehicle ID is required") + + params: dict[str, Any] = {"vehicle_id": vehicle_id} + query_str = "SELECT DISTINCT name FROM signal WHERE vehicle_id = {vehicle_id:String}" + if start is not None: + query_str += " AND produced_at >= parseDateTime64BestEffort({start:String}, 6, 'UTC')" + params["start"] = start + if end is not None: + query_str += " AND produced_at <= parseDateTime64BestEffort({end:String}, 6, 'UTC')" + params["end"] = end + query_str += " ORDER BY name ASC" + + logger.info(f"Signal names query: {query_str} | Params: {params}") + rows = get_client().query(query_str, parameters=params).result_rows + return [row[0] for row in rows] + + +def _distinct_vehicle_ids() -> list[str]: + rows = get_client().query("SELECT DISTINCT vehicle_id FROM signal").result_rows + return [row[0] for row in rows] + + +def _anchor_signal(vehicle_id: str) -> str | None: + """Pick the highest-frequency signal to bucket on. + + The anchor stands in for "the vehicle was producing data at time T", so it + must be a signal that is present continuously. Picking the alphabetically + first name instead would often land on a sparse, low-rate signal (e.g. + gr26's first name has 24 rows total), which collapses every bucket — and + therefore every cluster — to a single zero-width point. + + Cached per vehicle (see `_anchor_cache`): the underlying aggregate scans the + whole vehicle partition, so recomputing it on every request was a dominant + cost on the clusters/dates endpoints. + """ + now = time.monotonic() + with _anchor_lock: + cached = _anchor_cache.get(vehicle_id) + if cached is not None and now - cached[0] < _ANCHOR_TTL_SECONDS: + return cached[1] + + rows = get_client().query( + "SELECT name FROM signal WHERE vehicle_id = {vehicle_id:String} " + "GROUP BY name ORDER BY count() DESC LIMIT 1", + parameters={"vehicle_id": vehicle_id}, + ).result_rows + anchor = rows[0][0] if rows else None + + with _anchor_lock: + _anchor_cache[vehicle_id] = (now, anchor) + return anchor + + +def _bucket_rows( + vehicle_id: str, + anchor: str, + start: datetime | None = None, + end: datetime | None = None, +) -> list[tuple[datetime, datetime]]: + """Minute-bucketed (min, max) produced_at for the anchor signal, ordered. + + When `start`/`end` are given the scan is restricted to that window, which is + what keeps the per-day query cheap: only that day's anchor rows are bucketed. + """ + params: dict[str, Any] = {"vehicle_id": vehicle_id, "anchor": anchor} + query_str = """ + SELECT MIN(produced_at) AS pa_min, MAX(produced_at) AS pa_max + FROM signal + WHERE vehicle_id = {vehicle_id:String} AND name = {anchor:String} + """ + if start is not None: + query_str += " AND produced_at >= parseDateTime64BestEffort({start:String}, 6, 'UTC')" + params["start"] = _utc_iso(start) + if end is not None: + query_str += " AND produced_at < parseDateTime64BestEffort({end:String}, 6, 'UTC')" + params["end"] = _utc_iso(end) + query_str += """ + GROUP BY toStartOfMinute(produced_at) + ORDER BY pa_min ASC + """ + rows = get_client().query(query_str, parameters=params).result_rows + return [(row[0], row[1]) for row in rows] + + +def get_clusters( + vehicle_id: str | None = None, + gap_seconds: int = DEFAULT_GAP_SECONDS, + start: datetime | None = None, + end: datetime | None = None, +) -> list[Cluster]: + """Build contiguous data clusters for one vehicle, or all vehicles. + + `start`/`end` restrict the scan to a time window (e.g. a single day). + """ + vehicle_ids = [vehicle_id] if vehicle_id else _distinct_vehicle_ids() + + all_clusters: list[Cluster] = [] + for vid in vehicle_ids: + anchor = _anchor_signal(vid) + if not anchor: + continue + buckets = _bucket_rows(vid, anchor, start, end) + all_clusters.extend(merge_buckets(vid, buckets, gap_seconds)) + + all_clusters.sort(key=lambda c: c.start_time) + return all_clusters + + +def get_data_dates(vehicle_id: str, tz: str = "UTC") -> list[str]: + """Return the distinct calendar dates (YYYY-MM-DD) that have data for a + vehicle, expressed in timezone `tz`. + + `produced_at` is a UTC DateTime64, so `toDate(produced_at, :tz)` converts + each instant to wall-clock time in the caller's zone before truncating to a + date. This backs the date selector: it picks the default (most recent) day + and marks which days are selectable, without scanning per day. + """ + if not vehicle_id: + raise ValueError("Vehicle ID is required") + + anchor = _anchor_signal(vehicle_id) + if not anchor: + return [] + + query_str = """ + SELECT DISTINCT toDate(produced_at, {tz:String}) AS d + FROM signal + WHERE vehicle_id = {vehicle_id:String} AND name = {anchor:String} + ORDER BY d ASC + """ + rows = get_client().query( + query_str, + parameters={"vehicle_id": vehicle_id, "anchor": anchor, "tz": tz}, + ).result_rows + return [row[0].isoformat() for row in rows] diff --git a/query/query/service/log.py b/query/query/service/log.py index 7aaf2b78..e0eef578 100644 --- a/query/query/service/log.py +++ b/query/query/service/log.py @@ -1,24 +1,65 @@ from datetime import datetime, timezone import ulid -from query.database.connection import get_db +from query.database.connection import get_client from query.model.log import QueryLog -from sqlalchemy.orm import Session + +_COLS = "id, user_id, parameters, status_code, latency, error_message, timestamp" +_INSERT_COLS = [ + "id", "user_id", "parameters", "status_code", + "latency", "error_message", "timestamp", +] + + +def _row_to_log(row) -> QueryLog: + return QueryLog( + id=row[0], + user_id=row[1], + parameters=row[2], + status_code=row[3], + latency=row[4], + error_message=row[5], + timestamp=row[6], + ) + def get_all_logs() -> list[QueryLog]: - with get_db() as db: - return db.query(QueryLog).all() + rows = get_client().query( + f"SELECT {_COLS} FROM query_log ORDER BY timestamp DESC" + ).result_rows + return [_row_to_log(r) for r in rows] + + +def get_logs_by_user_id(user_id: str) -> list[QueryLog]: + rows = get_client().query( + f"SELECT {_COLS} FROM query_log WHERE user_id = {{user_id:String}} " + "ORDER BY timestamp DESC", + parameters={"user_id": user_id}, + ).result_rows + return [_row_to_log(r) for r in rows] + -def get_logs_by_user_id(user_id: int) -> list[QueryLog]: - with get_db() as db: - return db.query(QueryLog).filter(QueryLog.user_id == user_id).all() +def get_log_by_id(log_id: str) -> QueryLog | None: + rows = get_client().query( + f"SELECT {_COLS} FROM query_log WHERE id = {{id:String}} LIMIT 1", + parameters={"id": log_id}, + ).result_rows + return _row_to_log(rows[0]) if rows else None -def get_log_by_id(log_id: int) -> QueryLog: - with get_db() as db: - return db.query(QueryLog).filter(QueryLog.id == log_id).first() def create_log(log: QueryLog) -> QueryLog: log.id = ulid.make().prefixed("qlog") - log.created_at = datetime.now(timezone.utc) - with get_db() as db: - db.add(log) - return log \ No newline at end of file + log.timestamp = datetime.now(timezone.utc) + get_client().insert( + "query_log", + [[ + log.id, + log.user_id or "", + log.parameters or "", + int(log.status_code or 0), + int(log.latency or 0), + log.error_message or "", + log.timestamp, + ]], + column_names=_INSERT_COLS, + ) + return log diff --git a/query/query/service/query.py b/query/query/service/query.py index f406ad49..1b4b1bcd 100644 --- a/query/query/service/query.py +++ b/query/query/service/query.py @@ -1,37 +1,76 @@ from loguru import logger -from query.database.connection import get_db +from query.database.connection import get_client import pandas as pd -from sqlalchemy import bindparam, text from query.model.query import Metadata -def query_signals(vehicle_id: str, signals: list[str], start: str | None = None, end: str | None = None) -> list[pd.DataFrame]: +def _bucket_seconds(start: str | None, end: str | None, max_points: int) -> float | None: + """Bucket width (seconds) that yields at most ~`max_points` per signal over + [start, end], or None if the window is unknown/degenerate (no decimation). + """ + if start is None or end is None or max_points <= 0: + return None + try: + span = (pd.to_datetime(end) - pd.to_datetime(start)).total_seconds() + except (ValueError, TypeError): + return None + if span <= 0: + return None + return span / max_points + + +def query_signals( + vehicle_id: str, + signals: list[str], + start: str | None = None, + end: str | None = None, + max_points: int | None = None, +) -> list[pd.DataFrame]: if not vehicle_id: raise ValueError("Vehicle ID is required") - params = {"vehicle_id": vehicle_id, "signals": list(signals)} - query_str = """ - SELECT produced_at, name, value - FROM signal - WHERE name IN :signals AND vehicle_id = :vehicle_id""" + params: dict = {"vehicle_id": vehicle_id, "signals": list(signals)} + # When `max_points` is set and the window is known, decimate in SQL: bucket + # each signal's rows by time and keep one representative per bucket. This + # caps the rows transferred + pivoted + serialized at ~max_points per signal + # (the map doesn't need every sample), which is the main fix for the + # raw-data timeout on wide windows. Without it the query is full-resolution. + bucket = _bucket_seconds(start, end, max_points) if max_points else None + + # `timestamp` is the raw Int64 microsecond column; bucketing on it integer- + # divided by the bucket width avoids EXTRACT(EPOCH ...) (not a ClickHouse + # function) and keeps the grouping exact. + bucket_expr = "" + if bucket is not None: + params["bm"] = max(int(bucket * 1_000_000), 1) + bucket_expr = "intDiv(timestamp, {bm:UInt64})" + + where = ["name IN {signals:Array(String)}", "vehicle_id = {vehicle_id:String}"] if start is not None: - query_str += " AND produced_at > :start" + where.append("produced_at > parseDateTime64BestEffort({start:String}, 6, 'UTC')") params["start"] = start if end is not None: - query_str += " AND produced_at < :end" + where.append("produced_at < parseDateTime64BestEffort({end:String}, 6, 'UTC')") params["end"] = end - query_str += " ORDER BY produced_at ASC" + query_str = ( + "SELECT produced_at, name, value FROM signal WHERE " + " AND ".join(where) + ) + + if bucket is not None: + # ClickHouse has no DISTINCT ON. `ORDER BY name, bucket, produced_at` + # then `LIMIT 1 BY name, bucket` keeps the earliest row per (signal, + # time bucket) — the same "one representative per bucket" semantics. + query_str += ( + f" ORDER BY name ASC, {bucket_expr} ASC, produced_at ASC" + f" LIMIT 1 BY name, {bucket_expr}" + ) + else: + query_str += " ORDER BY produced_at ASC" logger.info(f"Query: {query_str} | Params: {params}") - # `expanding=True` makes SQLAlchemy render the IN list as individual - # placeholders (... IN (:s_1, :s_2)) on every backend, instead of binding - # a single tuple param (which only works by accident on psycopg2). - stmt = text(query_str).bindparams(bindparam("signals", expanding=True)) - - with get_db() as db: - result = pd.read_sql(stmt.bindparams(**params), db.bind) + result = get_client().query_df(query_str, parameters=params) result["produced_at"] = pd.to_datetime(result["produced_at"], utc=True) diff --git a/query/query/service/signal_definition.py b/query/query/service/signal_definition.py index 9acb9ecf..e6c655ca 100644 --- a/query/query/service/signal_definition.py +++ b/query/query/service/signal_definition.py @@ -1,14 +1,38 @@ -from query.database.connection import get_db +from query.database.connection import get_client from query.model.signal_definition import SignalDefinition +_COLS = "id, vehicle_type, name, description" + + +def _row_to_definition(row) -> SignalDefinition: + return SignalDefinition( + id=row[0], + vehicle_type=row[1], + name=row[2], + description=row[3], + ) + + def get_all_signal_definitions() -> list[SignalDefinition]: - with get_db() as db: - return db.query(SignalDefinition).all() + rows = get_client().query( + f"SELECT {_COLS} FROM signal_definition FINAL ORDER BY id" + ).result_rows + return [_row_to_definition(r) for r in rows] + def get_signal_definitions_by_vehicle_type(vehicle_type: str) -> list[SignalDefinition]: - with get_db() as db: - return db.query(SignalDefinition).filter(SignalDefinition.vehicle_type == vehicle_type).all() + rows = get_client().query( + f"SELECT {_COLS} FROM signal_definition FINAL " + "WHERE vehicle_type = {vehicle_type:String} ORDER BY id", + parameters={"vehicle_type": vehicle_type}, + ).result_rows + return [_row_to_definition(r) for r in rows] + -def get_signal_definition_by_id(signal_definition_id: int) -> SignalDefinition: - with get_db() as db: - return db.query(SignalDefinition).filter(SignalDefinition.id == signal_definition_id).first() \ No newline at end of file +def get_signal_definition_by_id(signal_definition_id: str) -> SignalDefinition | None: + rows = get_client().query( + f"SELECT {_COLS} FROM signal_definition FINAL " + "WHERE id = {id:String} LIMIT 1", + parameters={"id": signal_definition_id}, + ).result_rows + return _row_to_definition(rows[0]) if rows else None diff --git a/query/query/service/token.py b/query/query/service/token.py index c75f327a..b9e5acf9 100644 --- a/query/query/service/token.py +++ b/query/query/service/token.py @@ -1,43 +1,84 @@ from datetime import datetime, timezone import ulid -from query.database.connection import get_db +from query.database.connection import get_client from query.model.token import QueryToken -from sqlalchemy.orm import Session -def get_all_tokens() -> list[QueryToken]: - with get_db() as db: - return db.query(QueryToken).all() +_COLS = "id, user_id, created_at, expires_at" +_INSERT_COLS = ["id", "user_id", "created_at", "expires_at", "updated_at"] -def get_tokens_by_user_id(user_id: int) -> list[QueryToken]: - with get_db() as db: - return db.query(QueryToken).filter(QueryToken.user_id == user_id).all() -def get_token_by_id(token_id: int) -> QueryToken: - with get_db() as db: - return db.query(QueryToken).filter(QueryToken.id == token_id).first() +def _row_to_token(row) -> QueryToken: + return QueryToken( + id=row[0], + user_id=row[1], + created_at=row[2], + expires_at=row[3], + ) + + +def _insert(token: QueryToken) -> None: + # query_token is a ReplacingMergeTree(updated_at) keyed on id; a fresh + # updated_at on every write makes the latest insert win on FINAL. + get_client().insert( + "query_token", + [[token.id, token.user_id, token.created_at, token.expires_at, + datetime.now(timezone.utc)]], + column_names=_INSERT_COLS, + ) + + +def get_all_tokens() -> list[QueryToken]: + rows = get_client().query( + f"SELECT {_COLS} FROM query_token FINAL ORDER BY created_at DESC" + ).result_rows + return [_row_to_token(r) for r in rows] + + +def get_tokens_by_user_id(user_id: str) -> list[QueryToken]: + rows = get_client().query( + f"SELECT {_COLS} FROM query_token FINAL WHERE user_id = {{user_id:String}} " + "ORDER BY created_at DESC", + parameters={"user_id": user_id}, + ).result_rows + return [_row_to_token(r) for r in rows] + + +def get_token_by_id(token_id: str) -> QueryToken | None: + rows = get_client().query( + f"SELECT {_COLS} FROM query_token FINAL WHERE id = {{id:String}} LIMIT 1", + parameters={"id": token_id}, + ).result_rows + return _row_to_token(rows[0]) if rows else None + def create_token(user_id: str, expires_at: datetime) -> QueryToken: token = QueryToken( id=ulid.make().prefixed("qtk"), user_id=user_id, - expires_at=expires_at + created_at=datetime.now(timezone.utc), + expires_at=expires_at, ) - with get_db() as db: - db.add(token) - return token + _insert(token) + return token + + +def revoke_token(token_id: str) -> QueryToken | None: + token = get_token_by_id(token_id) + if token is None: + return None + # Revoking is a re-insert with expires_at pulled back to now; FINAL then + # surfaces the expired row. + token.expires_at = datetime.now(timezone.utc) + _insert(token) + return token -def revoke_token(token_id: str) -> None: - with get_db() as db: - token = db.query(QueryToken).filter(QueryToken.id == token_id).first() - token.expires_at = datetime.now(timezone.utc) - return token def validate_token(token: QueryToken) -> bool: if token is None: return False - + now = datetime.now(timezone.utc) if token.expires_at.tzinfo is None: token.expires_at = token.expires_at.replace(tzinfo=timezone.utc) - - return token.expires_at > now \ No newline at end of file + + return token.expires_at > now diff --git a/query/tests/__init__.py b/query/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/query/tests/conftest.py b/query/tests/conftest.py new file mode 100644 index 00000000..53c4c503 --- /dev/null +++ b/query/tests/conftest.py @@ -0,0 +1,15 @@ +"""Provide dummy environment so Config imports cleanly under test. + +The real values come from a .env at runtime; the cluster/signal-name tests +never touch a real database (DB-backed functions are monkeypatched), so any +placeholder values suffice here. +""" + +import os + +os.environ.setdefault("PORT", "7000") +os.environ.setdefault("CLICKHOUSE_HOST", "localhost") +os.environ.setdefault("CLICKHOUSE_PORT", "8123") +os.environ.setdefault("CLICKHOUSE_USER", "test") +os.environ.setdefault("CLICKHOUSE_PASSWORD", "test") +os.environ.setdefault("CLICKHOUSE_DATABASE", "test") diff --git a/query/tests/test_cluster.py b/query/tests/test_cluster.py new file mode 100644 index 00000000..dd828aac --- /dev/null +++ b/query/tests/test_cluster.py @@ -0,0 +1,122 @@ +from datetime import datetime, timedelta + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from query.config.config import Config +from query.routes import query as query_routes +from query.service.cluster import Cluster, merge_buckets + + +def _ts(minute: int, second: int = 0) -> datetime: + return datetime(2026, 1, 1, 0, minute, second) + + +# --- merge_buckets (pure algorithm) ---------------------------------------- + + +def test_merge_buckets_empty(): + assert merge_buckets("gr26", [], gap_seconds=30) == [] + + +def test_merge_buckets_single_contiguous_block(): + buckets = [(_ts(0, 0), _ts(0, 59)), (_ts(1, 0), _ts(1, 59))] + clusters = merge_buckets("gr26", buckets, gap_seconds=30) + assert clusters == [Cluster("gr26", _ts(0, 0), _ts(1, 59))] + + +def test_merge_buckets_splits_on_large_gap(): + # 2-minute gap between the two blocks (> 30s) → two clusters + buckets = [ + (_ts(0, 0), _ts(0, 59)), + (_ts(3, 0), _ts(3, 59)), + ] + clusters = merge_buckets("gr26", buckets, gap_seconds=30) + assert len(clusters) == 2 + assert clusters[0] == Cluster("gr26", _ts(0, 0), _ts(0, 59)) + assert clusters[1] == Cluster("gr26", _ts(3, 0), _ts(3, 59)) + + +def test_merge_buckets_gap_exactly_at_threshold_does_not_split(): + # gap of exactly 30s is not > 30s, so it stays one cluster + buckets = [ + (_ts(0, 0), _ts(0, 30)), + (_ts(1, 0), _ts(1, 30)), + ] + clusters = merge_buckets("gr26", buckets, gap_seconds=30) + assert len(clusters) == 1 + assert clusters[0] == Cluster("gr26", _ts(0, 0), _ts(1, 30)) + + +def test_merge_buckets_custom_gap_length(): + buckets = [ + (_ts(0, 0), _ts(0, 10)), + (_ts(0, 20), _ts(0, 30)), + ] + # 10s gap: splits when threshold is 5s, stays merged when threshold is 15s + assert len(merge_buckets("gr26", buckets, gap_seconds=5)) == 2 + assert len(merge_buckets("gr26", buckets, gap_seconds=15)) == 1 + + +def test_cluster_to_dict_is_iso(): + c = Cluster("gr26", _ts(0, 0), _ts(1, 0)) + d = c.to_dict() + assert d["vehicle_id"] == "gr26" + assert d["start_time"] == _ts(0, 0).isoformat() + assert d["end_time"] == _ts(1, 0).isoformat() + + +# --- route wiring (no DB; service functions monkeypatched) ------------------ + + +@pytest.fixture +def client(monkeypatch): + monkeypatch.setattr(Config, "SKIP_AUTH_CHECK", True) + app = FastAPI() + app.include_router(query_routes.router, prefix="/query") + return TestClient(app) + + +def test_signal_names_route(client, monkeypatch): + monkeypatch.setattr( + query_routes, "get_signal_names", lambda **kw: ["gps_lat", "gps_lon"] + ) + resp = client.get("/query/signals/names?vehicle_id=gr26") + assert resp.status_code == 200 + assert resp.json() == {"data": ["gps_lat", "gps_lon"]} + + +def test_signal_names_requires_vehicle_id(client): + resp = client.get("/query/signals/names") + assert resp.status_code == 400 + + +def test_signal_names_rejects_bad_timestamp(client, monkeypatch): + monkeypatch.setattr(query_routes, "get_signal_names", lambda **kw: []) + resp = client.get("/query/signals/names?vehicle_id=gr26&start=not-a-date") + assert resp.status_code == 400 + + +def test_clusters_route(client, monkeypatch): + sample = [Cluster("gr26", _ts(0, 0), _ts(1, 0))] + monkeypatch.setattr(query_routes, "get_clusters", lambda **kw: sample) + resp = client.get("/query/clusters?vehicle_id=gr26&gap=30") + assert resp.status_code == 200 + body = resp.json() + assert len(body["data"]) == 1 + assert body["data"][0]["vehicle_id"] == "gr26" + + +def test_clusters_rejects_non_positive_gap(client): + resp = client.get("/query/clusters?vehicle_id=gr26&gap=0") + assert resp.status_code == 400 + + +def test_clusters_unauthorized_when_auth_required(monkeypatch): + monkeypatch.setattr(Config, "SKIP_AUTH_CHECK", False) + app = FastAPI() + app.include_router(query_routes.router, prefix="/query") + c = TestClient(app) + resp = c.get("/query/clusters?vehicle_id=gr26") + assert resp.status_code == 401 diff --git a/query/uv.lock b/query/uv.lock index c424f3b5..c50b2e0b 100644 --- a/query/uv.lock +++ b/query/uv.lock @@ -159,6 +159,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] +[[package]] +name = "clickhouse-connect" +version = "0.8.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "lz4" }, + { name = "pytz" }, + { name = "urllib3" }, + { name = "zstandard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/3d/a06d938d1efb94fdf8a343bbe1dc4ad2458aef08b9c69e0080d695ab24c1/clickhouse_connect-0.8.18.tar.gz", hash = "sha256:206a33decf2d9ed689d3156ef906dc06f1db7eabfe512e3552e08e9e86b4c73a", size = 91383, upload-time = "2025-06-24T19:08:08.903Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/4d/58fd5b141bb5b2e8a315bcaebea5d182a7b775c92767c556a85f2c6cd31a/clickhouse_connect-0.8.18-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21570aa28c0a9753a8172f88fbe492dcc903f5162798725a04920e319b4771bb", size = 262237, upload-time = "2025-06-24T19:06:53.223Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d7/bfa24aa9c0c972883fc5d86331b76d1acaf15f10c1e3a4f46163c61dc96c/clickhouse_connect-0.8.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9494b4d2c74f94ad05ca17ebe91960396af7f45922ba908931eace77b53acca", size = 253924, upload-time = "2025-06-24T19:06:54.926Z" }, + { url = "https://files.pythonhosted.org/packages/39/3a/854622c3114a6b4a1d4239ee710e9e820ed7f76b901ec42c2791cb17940e/clickhouse_connect-0.8.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d438a6d6461f5bdb37344b668049a78e1a0f3353987a1e649001d075196fd688", size = 1058189, upload-time = "2025-06-24T19:06:56.786Z" }, + { url = "https://files.pythonhosted.org/packages/59/d5/3fa03d352103c40e4f9e3dd3dabfcacc14d5d94d4d06fb6fc5daa4a4bf7d/clickhouse_connect-0.8.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:777dfbea92984e1c834a75499f459823edb9e7afde9ed62281455edb5d7be577", size = 1076550, upload-time = "2025-06-24T19:06:58.457Z" }, + { url = "https://files.pythonhosted.org/packages/68/cd/e121f79b2e96d36262df57a5e6ce1efd8d200180331690eb6013d2d0da74/clickhouse_connect-0.8.18-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2eb88acedc3802fb39b2b3eb48b76ec7c7cc895c189b3ac031f880ee12e82bd9", size = 1031815, upload-time = "2025-06-24T19:07:00.028Z" }, + { url = "https://files.pythonhosted.org/packages/04/07/ff6a823d7f9062cc6c932e07c85b39f2fe8067b82d6079e5b63e84eb1eb9/clickhouse_connect-0.8.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:83ae184507c671c3d833e688bd1445bc0fd62a4cf6db6cbaf9f8aaebd4134921", size = 1057457, upload-time = "2025-06-24T19:07:01.653Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c0/2405c796cf561b3592a7cba5f1a6752396783fe0e048c84435c78df701e6/clickhouse_connect-0.8.18-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a444ea5e14135c88349223af00d124e09f3f77384180ca7df96b4aafd8b6e9ee", size = 1074145, upload-time = "2025-06-24T19:07:03.27Z" }, + { url = "https://files.pythonhosted.org/packages/a3/db/4d9f4fa5bdcbecebc53beb4faffc976fbcda96fc9ea2da8cceb941840970/clickhouse_connect-0.8.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1f0594ee5a261f2b89823f049edb7aebfa4cf3262ab324a76e462de0fd404320", size = 1099462, upload-time = "2025-06-24T19:07:04.932Z" }, + { url = "https://files.pythonhosted.org/packages/99/41/67f5d409c7986fee8631379b88583eb245b745f16dfe4e9f2d1f54be1145/clickhouse_connect-0.8.18-cp312-cp312-win32.whl", hash = "sha256:6e087bc4162d156fc040678454e5eb6160f72d470e3817906128069a7881af7d", size = 229493, upload-time = "2025-06-24T19:07:06.117Z" }, + { url = "https://files.pythonhosted.org/packages/3a/06/d65c933af7ef35a481eff8f2adb995274a611535c5067d9097b87f521a8c/clickhouse_connect-0.8.18-cp312-cp312-win_amd64.whl", hash = "sha256:7dfd1280d62f24ff8f991953487958d4b97dd2435fea9d680f7f193049ed7e81", size = 247567, upload-time = "2025-06-24T19:07:07.355Z" }, + { url = "https://files.pythonhosted.org/packages/93/04/ad43f7ac57142c41d0eb33fca64400c61bfe30ae10af9c9f5c16201e2de9/clickhouse_connect-0.8.18-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fbb56d86b6e26016e24a260a2280c9831736797cf8eda8594551e25fa91a1b0e", size = 259470, upload-time = "2025-06-24T19:07:08.475Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f4/691829df8e5ec71a34293be1061a5d161a15948013f5bd022073033427c5/clickhouse_connect-0.8.18-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8e9b47475f82ad31c4981cb5284e6a7d9869dda7c295d69ba650011ec6b6c64e", size = 251140, upload-time = "2025-06-24T19:07:09.686Z" }, + { url = "https://files.pythonhosted.org/packages/42/64/554632a889a9e920904979725bb445cea0a51fb3cd0d1b89bebc098218eb/clickhouse_connect-0.8.18-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da68a0cece7a350d6ebeacc5934406c78c381d6bf6a598765d56f24b65b1ec85", size = 1041149, upload-time = "2025-06-24T19:07:11.249Z" }, + { url = "https://files.pythonhosted.org/packages/3b/bd/52b7c28edc817ceda499c3ef1eaafefcaf319c7cde7c80618d380c8c09ba/clickhouse_connect-0.8.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0036960dcb6bf88b262e9b461ddafb7597c0beb36700aab1e74dde64f02c8cfd", size = 1059571, upload-time = "2025-06-24T19:07:12.977Z" }, + { url = "https://files.pythonhosted.org/packages/5e/50/0ba93932f5096b49d1f04aa6859dcb659abbad957a7b5d9becd9b1d944df/clickhouse_connect-0.8.18-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcd291e920223edb3152bc4e7720b2b514b7a48fb18bb0c3346f66b2691add67", size = 1015179, upload-time = "2025-06-24T19:07:14.241Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d8/ad0422135e359390986739a6fedbacdacc40f2ee58a03be6e7aee8cb68ad/clickhouse_connect-0.8.18-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba1747a569b87f693d2c470faaaf83e02a24792c3533502f92b11ada672c2a6f", size = 1042200, upload-time = "2025-06-24T19:07:15.596Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d2/93838044318e2a51008c43c64e5fddd611ae200ffaaf09a38e7f3fdaa708/clickhouse_connect-0.8.18-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:63c9c1ab6899ff916f4751b1d7604b6e81c3a63f169ec538429be072905cf7c3", size = 1057602, upload-time = "2025-06-24T19:07:16.922Z" }, + { url = "https://files.pythonhosted.org/packages/25/24/da6aa82d26e4db72a20c88ceef076ae5cf1feb0f170ab05331e65bea27b0/clickhouse_connect-0.8.18-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:38bdf84bbbb784ce1ffb813edc1ddc7622cea3cc6ca59f336f82c3365fed36ef", size = 1085044, upload-time = "2025-06-24T19:07:18.346Z" }, + { url = "https://files.pythonhosted.org/packages/97/63/a0540da4db8c8adc1a522615481568296edf7ae29cf8b261697d02627629/clickhouse_connect-0.8.18-cp313-cp313-win32.whl", hash = "sha256:252549ed7596baaf955699f7713ff171cb21292ea333ff01cc295d7bbf20a4d9", size = 228783, upload-time = "2025-06-24T19:07:19.599Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/5e9b71b29eb28066c207cb0da6fb0bcc89f12d5ed2114085854cfa5f4fdf/clickhouse_connect-0.8.18-cp313-cp313-win_amd64.whl", hash = "sha256:a7915cdd844d083905b5fe4f9139c65aa033652a670986381dc2e2b885108266", size = 246541, upload-time = "2025-06-24T19:07:20.802Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -255,45 +290,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/73/3ca978823cb22a9272612cc48ae52d0466afba8c3b0103a90c676d3604ab/gr_ulid-1.1.2-py3-none-any.whl", hash = "sha256:f59b201de9d834e0f5c2690ef3aed04f807611c3cd0172d615c20c296d667a90", size = 12577, upload-time = "2026-03-06T04:55:50.76Z" }, ] -[[package]] -name = "greenlet" -version = "3.3.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, - { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, - { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, - { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, - { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, - { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, - { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" }, - { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" }, - { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, - { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, - { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, - { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, - { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, - { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, - { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961, upload-time = "2026-02-20T20:16:58.461Z" }, - { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221, upload-time = "2026-02-20T20:17:37.152Z" }, - { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, - { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, - { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, - { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" }, - { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" }, - { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, - { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, - { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, - { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, - { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, - { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, - { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" }, -] - [[package]] name = "h11" version = "0.16.0" @@ -303,6 +299,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.27.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", size = 144189, upload-time = "2024-08-27T12:54:01.334Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395, upload-time = "2024-08-27T12:53:59.653Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -312,6 +337,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "loguru" version = "0.7.3" @@ -325,6 +359,46 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, ] +[[package]] +name = "lz4" +version = "4.4.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/51/f1b86d93029f418033dddf9b9f79c8d2641e7454080478ee2aab5123173e/lz4-4.4.5.tar.gz", hash = "sha256:5f0b9e53c1e82e88c10d7c180069363980136b9d7a8306c4dca4f760d60c39f0", size = 172886, upload-time = "2025-11-03T13:02:36.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/ac/016e4f6de37d806f7cc8f13add0a46c9a7cfc41a5ddc2bc831d7954cf1ce/lz4-4.4.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:df5aa4cead2044bab83e0ebae56e0944cc7fcc1505c7787e9e1057d6d549897e", size = 207163, upload-time = "2025-11-03T13:01:45.895Z" }, + { url = "https://files.pythonhosted.org/packages/8d/df/0fadac6e5bd31b6f34a1a8dbd4db6a7606e70715387c27368586455b7fc9/lz4-4.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d0bf51e7745484d2092b3a51ae6eb58c3bd3ce0300cf2b2c14f76c536d5697a", size = 207150, upload-time = "2025-11-03T13:01:47.205Z" }, + { url = "https://files.pythonhosted.org/packages/b7/17/34e36cc49bb16ca73fb57fbd4c5eaa61760c6b64bce91fcb4e0f4a97f852/lz4-4.4.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7b62f94b523c251cf32aa4ab555f14d39bd1a9df385b72443fd76d7c7fb051f5", size = 1292045, upload-time = "2025-11-03T13:01:48.667Z" }, + { url = "https://files.pythonhosted.org/packages/90/1c/b1d8e3741e9fc89ed3b5f7ef5f22586c07ed6bb04e8343c2e98f0fa7ff04/lz4-4.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c3ea562c3af274264444819ae9b14dbbf1ab070aff214a05e97db6896c7597e", size = 1279546, upload-time = "2025-11-03T13:01:50.159Z" }, + { url = "https://files.pythonhosted.org/packages/55/d9/e3867222474f6c1b76e89f3bd914595af69f55bf2c1866e984c548afdc15/lz4-4.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24092635f47538b392c4eaeff14c7270d2c8e806bf4be2a6446a378591c5e69e", size = 1368249, upload-time = "2025-11-03T13:01:51.273Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e7/d667d337367686311c38b580d1ca3d5a23a6617e129f26becd4f5dc458df/lz4-4.4.5-cp312-cp312-win32.whl", hash = "sha256:214e37cfe270948ea7eb777229e211c601a3e0875541c1035ab408fbceaddf50", size = 88189, upload-time = "2025-11-03T13:01:52.605Z" }, + { url = "https://files.pythonhosted.org/packages/a5/0b/a54cd7406995ab097fceb907c7eb13a6ddd49e0b231e448f1a81a50af65c/lz4-4.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:713a777de88a73425cf08eb11f742cd2c98628e79a8673d6a52e3c5f0c116f33", size = 99497, upload-time = "2025-11-03T13:01:53.477Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7e/dc28a952e4bfa32ca16fa2eb026e7a6ce5d1411fcd5986cd08c74ec187b9/lz4-4.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:a88cbb729cc333334ccfb52f070463c21560fca63afcf636a9f160a55fac3301", size = 91279, upload-time = "2025-11-03T13:01:54.419Z" }, + { url = "https://files.pythonhosted.org/packages/2f/46/08fd8ef19b782f301d56a9ccfd7dafec5fd4fc1a9f017cf22a1accb585d7/lz4-4.4.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6bb05416444fafea170b07181bc70640975ecc2a8c92b3b658c554119519716c", size = 207171, upload-time = "2025-11-03T13:01:56.595Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3f/ea3334e59de30871d773963997ecdba96c4584c5f8007fd83cfc8f1ee935/lz4-4.4.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b424df1076e40d4e884cfcc4c77d815368b7fb9ebcd7e634f937725cd9a8a72a", size = 207163, upload-time = "2025-11-03T13:01:57.721Z" }, + { url = "https://files.pythonhosted.org/packages/41/7b/7b3a2a0feb998969f4793c650bb16eff5b06e80d1f7bff867feb332f2af2/lz4-4.4.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:216ca0c6c90719731c64f41cfbd6f27a736d7e50a10b70fad2a9c9b262ec923d", size = 1292136, upload-time = "2025-11-03T13:02:00.375Z" }, + { url = "https://files.pythonhosted.org/packages/89/d1/f1d259352227bb1c185288dd694121ea303e43404aa77560b879c90e7073/lz4-4.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:533298d208b58b651662dd972f52d807d48915176e5b032fb4f8c3b6f5fe535c", size = 1279639, upload-time = "2025-11-03T13:02:01.649Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fb/ba9256c48266a09012ed1d9b0253b9aa4fe9cdff094f8febf5b26a4aa2a2/lz4-4.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:451039b609b9a88a934800b5fc6ee401c89ad9c175abf2f4d9f8b2e4ef1afc64", size = 1368257, upload-time = "2025-11-03T13:02:03.35Z" }, + { url = "https://files.pythonhosted.org/packages/a5/6d/dee32a9430c8b0e01bbb4537573cabd00555827f1a0a42d4e24ca803935c/lz4-4.4.5-cp313-cp313-win32.whl", hash = "sha256:a5f197ffa6fc0e93207b0af71b302e0a2f6f29982e5de0fbda61606dd3a55832", size = 88191, upload-time = "2025-11-03T13:02:04.406Z" }, + { url = "https://files.pythonhosted.org/packages/18/e0/f06028aea741bbecb2a7e9648f4643235279a770c7ffaf70bd4860c73661/lz4-4.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:da68497f78953017deb20edff0dba95641cc86e7423dfadf7c0264e1ac60dc22", size = 99502, upload-time = "2025-11-03T13:02:05.886Z" }, + { url = "https://files.pythonhosted.org/packages/61/72/5bef44afb303e56078676b9f2486f13173a3c1e7f17eaac1793538174817/lz4-4.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:c1cfa663468a189dab510ab231aad030970593f997746d7a324d40104db0d0a9", size = 91285, upload-time = "2025-11-03T13:02:06.77Z" }, + { url = "https://files.pythonhosted.org/packages/49/55/6a5c2952971af73f15ed4ebfdd69774b454bd0dc905b289082ca8664fba1/lz4-4.4.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67531da3b62f49c939e09d56492baf397175ff39926d0bd5bd2d191ac2bff95f", size = 207348, upload-time = "2025-11-03T13:02:08.117Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d7/fd62cbdbdccc35341e83aabdb3f6d5c19be2687d0a4eaf6457ddf53bba64/lz4-4.4.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a1acbbba9edbcbb982bc2cac5e7108f0f553aebac1040fbec67a011a45afa1ba", size = 207340, upload-time = "2025-11-03T13:02:09.152Z" }, + { url = "https://files.pythonhosted.org/packages/77/69/225ffadaacb4b0e0eb5fd263541edd938f16cd21fe1eae3cd6d5b6a259dc/lz4-4.4.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a482eecc0b7829c89b498fda883dbd50e98153a116de612ee7c111c8bcf82d1d", size = 1293398, upload-time = "2025-11-03T13:02:10.272Z" }, + { url = "https://files.pythonhosted.org/packages/c6/9e/2ce59ba4a21ea5dc43460cba6f34584e187328019abc0e66698f2b66c881/lz4-4.4.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e099ddfaa88f59dd8d36c8a3c66bd982b4984edf127eb18e30bb49bdba68ce67", size = 1281209, upload-time = "2025-11-03T13:02:12.091Z" }, + { url = "https://files.pythonhosted.org/packages/80/4f/4d946bd1624ec229b386a3bc8e7a85fa9a963d67d0a62043f0af0978d3da/lz4-4.4.5-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2af2897333b421360fdcce895c6f6281dc3fab018d19d341cf64d043fc8d90d", size = 1369406, upload-time = "2025-11-03T13:02:13.683Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/d429ba4720a9064722698b4b754fb93e42e625f1318b8fe834086c7c783b/lz4-4.4.5-cp313-cp313t-win32.whl", hash = "sha256:66c5de72bf4988e1b284ebdd6524c4bead2c507a2d7f172201572bac6f593901", size = 88325, upload-time = "2025-11-03T13:02:14.743Z" }, + { url = "https://files.pythonhosted.org/packages/4b/85/7ba10c9b97c06af6c8f7032ec942ff127558863df52d866019ce9d2425cf/lz4-4.4.5-cp313-cp313t-win_amd64.whl", hash = "sha256:cdd4bdcbaf35056086d910d219106f6a04e1ab0daa40ec0eeef1626c27d0fddb", size = 99643, upload-time = "2025-11-03T13:02:15.978Z" }, + { url = "https://files.pythonhosted.org/packages/77/4d/a175459fb29f909e13e57c8f475181ad8085d8d7869bd8ad99033e3ee5fa/lz4-4.4.5-cp313-cp313t-win_arm64.whl", hash = "sha256:28ccaeb7c5222454cd5f60fcd152564205bcb801bd80e125949d2dfbadc76bbd", size = 91504, upload-time = "2025-11-03T13:02:17.313Z" }, + { url = "https://files.pythonhosted.org/packages/63/9c/70bdbdb9f54053a308b200b4678afd13efd0eafb6ddcbb7f00077213c2e5/lz4-4.4.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c216b6d5275fc060c6280936bb3bb0e0be6126afb08abccde27eed23dead135f", size = 207586, upload-time = "2025-11-03T13:02:18.263Z" }, + { url = "https://files.pythonhosted.org/packages/b6/cb/bfead8f437741ce51e14b3c7d404e3a1f6b409c440bad9b8f3945d4c40a7/lz4-4.4.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c8e71b14938082ebaf78144f3b3917ac715f72d14c076f384a4c062df96f9df6", size = 207161, upload-time = "2025-11-03T13:02:19.286Z" }, + { url = "https://files.pythonhosted.org/packages/e7/18/b192b2ce465dfbeabc4fc957ece7a1d34aded0d95a588862f1c8a86ac448/lz4-4.4.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b5e6abca8df9f9bdc5c3085f33ff32cdc86ed04c65e0355506d46a5ac19b6e9", size = 1292415, upload-time = "2025-11-03T13:02:20.829Z" }, + { url = "https://files.pythonhosted.org/packages/67/79/a4e91872ab60f5e89bfad3e996ea7dc74a30f27253faf95865771225ccba/lz4-4.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b84a42da86e8ad8537aabef062e7f661f4a877d1c74d65606c49d835d36d668", size = 1279920, upload-time = "2025-11-03T13:02:22.013Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/d52c7b11eaa286d49dae619c0eec4aabc0bf3cda7a7467eb77c62c4471f3/lz4-4.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bba042ec5a61fa77c7e380351a61cb768277801240249841defd2ff0a10742f", size = 1368661, upload-time = "2025-11-03T13:02:23.208Z" }, + { url = "https://files.pythonhosted.org/packages/f7/da/137ddeea14c2cb86864838277b2607d09f8253f152156a07f84e11768a28/lz4-4.4.5-cp314-cp314-win32.whl", hash = "sha256:bd85d118316b53ed73956435bee1997bd06cc66dd2fa74073e3b1322bd520a67", size = 90139, upload-time = "2025-11-03T13:02:24.301Z" }, + { url = "https://files.pythonhosted.org/packages/18/2c/8332080fd293f8337779a440b3a143f85e374311705d243439a3349b81ad/lz4-4.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:92159782a4502858a21e0079d77cdcaade23e8a5d252ddf46b0652604300d7be", size = 101497, upload-time = "2025-11-03T13:02:25.187Z" }, + { url = "https://files.pythonhosted.org/packages/ca/28/2635a8141c9a4f4bc23f5135a92bbcf48d928d8ca094088c962df1879d64/lz4-4.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:d994b87abaa7a88ceb7a37c90f547b8284ff9da694e6afcfaa8568d739faf3f7", size = 93812, upload-time = "2025-11-03T13:02:26.133Z" }, +] + [[package]] name = "mapache-py" version = "3.1.0" @@ -395,6 +469,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1f/b6/7c0d4334c15983cec7f92a69e8ce9b1e6f31857e5ee3a413ac424e6bd63d/numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e", size = 10565454, upload-time = "2026-03-09T07:58:33.031Z" }, ] +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + [[package]] name = "pandas" version = "2.3.3" @@ -443,44 +526,12 @@ wheels = [ ] [[package]] -name = "psycopg2-binary" -version = "2.9.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/91/f870a02f51be4a65987b45a7de4c2e1897dd0d01051e2b559a38fa634e3e/psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", size = 3756603, upload-time = "2025-10-10T11:11:52.213Z" }, - { url = "https://files.pythonhosted.org/packages/27/fa/cae40e06849b6c9a95eb5c04d419942f00d9eaac8d81626107461e268821/psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", size = 3864509, upload-time = "2025-10-10T11:11:56.452Z" }, - { url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159, upload-time = "2025-10-10T11:12:00.49Z" }, - { url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234, upload-time = "2025-10-10T11:12:04.892Z" }, - { url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236, upload-time = "2025-10-10T11:12:11.674Z" }, - { url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083, upload-time = "2025-10-30T02:55:15.73Z" }, - { url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281, upload-time = "2025-10-10T11:12:17.713Z" }, - { url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010, upload-time = "2025-10-10T11:12:22.671Z" }, - { url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641, upload-time = "2025-10-30T02:55:19.929Z" }, - { url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940, upload-time = "2025-10-10T11:12:26.529Z" }, - { url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147, upload-time = "2025-10-10T11:12:29.535Z" }, - { url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" }, - { url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" }, - { url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" }, - { url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" }, - { url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133, upload-time = "2025-10-30T02:55:24.329Z" }, - { url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" }, - { url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" }, - { url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712, upload-time = "2025-10-30T02:55:27.975Z" }, - { url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" }, - { url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" }, - { url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" }, - { url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" }, - { url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" }, - { url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" }, - { url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" }, - { url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184, upload-time = "2025-10-30T02:55:32.483Z" }, - { url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" }, - { url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" }, - { url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737, upload-time = "2025-10-30T02:55:35.69Z" }, - { url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" }, - { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" }, +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +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/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]] @@ -613,6 +664,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, ] +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + [[package]] name = "pyjwt" version = "2.12.1" @@ -627,6 +687,22 @@ crypto = [ { name = "cryptography" }, ] +[[package]] +name = "pytest" +version = "8.4.2" +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/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -659,9 +735,10 @@ wheels = [ [[package]] name = "query" -version = "3.4.5" +version = "3.6.1" source = { editable = "." } dependencies = [ + { name = "clickhouse-connect" }, { name = "dotenv" }, { name = "fastapi" }, { name = "gr-ulid" }, @@ -669,16 +746,21 @@ dependencies = [ { name = "mapache-py" }, { name = "numpy" }, { name = "pandas" }, - { name = "psycopg2-binary" }, { name = "pyarrow" }, { name = "pyjwt", extra = ["crypto"] }, { name = "requests" }, - { name = "sqlalchemy" }, { name = "uvicorn" }, ] +[package.dev-dependencies] +dev = [ + { name = "httpx" }, + { name = "pytest" }, +] + [package.metadata] requires-dist = [ + { name = "clickhouse-connect", specifier = ">=0.8.0,<0.9.0" }, { name = "dotenv", specifier = ">=0.9.9,<0.10.0" }, { name = "fastapi", specifier = ">=0.115.10,<0.116.0" }, { name = "gr-ulid", specifier = ">=1.1.2,<2.0.0" }, @@ -686,14 +768,18 @@ requires-dist = [ { name = "mapache-py", specifier = ">=3.0.1,<4.0.0" }, { name = "numpy", specifier = ">=2.2.3,<3.0.0" }, { name = "pandas", specifier = ">=2.2.3,<3.0.0" }, - { name = "psycopg2-binary", specifier = ">=2.9.10,<3.0.0" }, { name = "pyarrow", specifier = ">=20.0.0,<21.0.0" }, { name = "pyjwt", extras = ["crypto"], specifier = ">=2.11.0,<3.0.0" }, { name = "requests", specifier = ">=2.32.3,<3.0.0" }, - { name = "sqlalchemy", specifier = ">=2.0.38,<3.0.0" }, { name = "uvicorn", specifier = ">=0.34.0,<0.35.0" }, ] +[package.metadata.requires-dev] +dev = [ + { name = "httpx", specifier = ">=0.27.0,<0.28.0" }, + { name = "pytest", specifier = ">=8.0.0,<9.0.0" }, +] + [[package]] name = "requests" version = "2.32.5" @@ -719,49 +805,12 @@ wheels = [ ] [[package]] -name = "sqlalchemy" -version = "2.0.48" +name = "sniffio" +version = "1.3.1" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1f/73/b4a9737255583b5fa858e0bb8e116eb94b88c910164ed2ed719147bde3de/sqlalchemy-2.0.48.tar.gz", hash = "sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7", size = 9886075, upload-time = "2026-03-02T15:28:51.474Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/91/a42ae716f8925e9659df2da21ba941f158686856107a61cc97a95e7647a3/sqlalchemy-2.0.48-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:348174f228b99f33ca1f773e85510e08927620caa59ffe7803b37170df30332b", size = 2155737, upload-time = "2026-03-02T15:49:13.207Z" }, - { url = "https://files.pythonhosted.org/packages/b9/52/f75f516a1f3888f027c1cfb5d22d4376f4b46236f2e8669dcb0cddc60275/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53667b5f668991e279d21f94ccfa6e45b4e3f4500e7591ae59a8012d0f010dcb", size = 3337020, upload-time = "2026-03-02T15:50:34.547Z" }, - { url = "https://files.pythonhosted.org/packages/37/9a/0c28b6371e0cdcb14f8f1930778cb3123acfcbd2c95bb9cf6b4a2ba0cce3/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34634e196f620c7a61d18d5cf7dc841ca6daa7961aed75d532b7e58b309ac894", size = 3349983, upload-time = "2026-03-02T15:53:25.542Z" }, - { url = "https://files.pythonhosted.org/packages/1c/46/0aee8f3ff20b1dcbceb46ca2d87fcc3d48b407925a383ff668218509d132/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:546572a1793cc35857a2ffa1fe0e58571af1779bcc1ffa7c9fb0839885ed69a9", size = 3279690, upload-time = "2026-03-02T15:50:36.277Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8c/a957bc91293b49181350bfd55e6dfc6e30b7f7d83dc6792d72043274a390/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:07edba08061bc277bfdc772dd2a1a43978f5a45994dd3ede26391b405c15221e", size = 3314738, upload-time = "2026-03-02T15:53:27.519Z" }, - { url = "https://files.pythonhosted.org/packages/4b/44/1d257d9f9556661e7bdc83667cc414ba210acfc110c82938cb3611eea58f/sqlalchemy-2.0.48-cp312-cp312-win32.whl", hash = "sha256:908a3fa6908716f803b86896a09a2c4dde5f5ce2bb07aacc71ffebb57986ce99", size = 2115546, upload-time = "2026-03-02T15:54:31.591Z" }, - { url = "https://files.pythonhosted.org/packages/f2/af/c3c7e1f3a2b383155a16454df62ae8c62a30dd238e42e68c24cebebbfae6/sqlalchemy-2.0.48-cp312-cp312-win_amd64.whl", hash = "sha256:68549c403f79a8e25984376480959975212a670405e3913830614432b5daa07a", size = 2142484, upload-time = "2026-03-02T15:54:34.072Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c6/569dc8bf3cd375abc5907e82235923e986799f301cd79a903f784b996fca/sqlalchemy-2.0.48-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e3070c03701037aa418b55d36532ecb8f8446ed0135acb71c678dbdf12f5b6e4", size = 2152599, upload-time = "2026-03-02T15:49:14.41Z" }, - { url = "https://files.pythonhosted.org/packages/6d/ff/f4e04a4bd5a24304f38cb0d4aa2ad4c0fb34999f8b884c656535e1b2b74c/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2645b7d8a738763b664a12a1542c89c940daa55196e8d73e55b169cc5c99f65f", size = 3278825, upload-time = "2026-03-02T15:50:38.269Z" }, - { url = "https://files.pythonhosted.org/packages/fe/88/cb59509e4668d8001818d7355d9995be90c321313078c912420603a7cb95/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b19151e76620a412c2ac1c6f977ab1b9fa7ad43140178345136456d5265b32ed", size = 3295200, upload-time = "2026-03-02T15:53:29.366Z" }, - { url = "https://files.pythonhosted.org/packages/87/dc/1609a4442aefd750ea2f32629559394ec92e89ac1d621a7f462b70f736ff/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b193a7e29fd9fa56e502920dca47dffe60f97c863494946bd698c6058a55658", size = 3226876, upload-time = "2026-03-02T15:50:39.802Z" }, - { url = "https://files.pythonhosted.org/packages/37/c3/6ae2ab5ea2fa989fbac4e674de01224b7a9d744becaf59bb967d62e99bed/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36ac4ddc3d33e852da9cb00ffb08cea62ca05c39711dc67062ca2bb1fae35fd8", size = 3265045, upload-time = "2026-03-02T15:53:31.421Z" }, - { url = "https://files.pythonhosted.org/packages/6f/82/ea4665d1bb98c50c19666e672f21b81356bd6077c4574e3d2bbb84541f53/sqlalchemy-2.0.48-cp313-cp313-win32.whl", hash = "sha256:389b984139278f97757ea9b08993e7b9d1142912e046ab7d82b3fbaeb0209131", size = 2113700, upload-time = "2026-03-02T15:54:35.825Z" }, - { url = "https://files.pythonhosted.org/packages/b7/2b/b9040bec58c58225f073f5b0c1870defe1940835549dafec680cbd58c3c3/sqlalchemy-2.0.48-cp313-cp313-win_amd64.whl", hash = "sha256:d612c976cbc2d17edfcc4c006874b764e85e990c29ce9bd411f926bbfb02b9a2", size = 2139487, upload-time = "2026-03-02T15:54:37.079Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/7b17bd50244b78a49d22cc63c969d71dc4de54567dc152a9b46f6fae40ce/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69f5bc24904d3bc3640961cddd2523e361257ef68585d6e364166dfbe8c78fae", size = 3558851, upload-time = "2026-03-02T15:57:48.607Z" }, - { url = "https://files.pythonhosted.org/packages/20/0d/213668e9aca61d370f7d2a6449ea4ec699747fac67d4bda1bb3d129025be/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd08b90d211c086181caed76931ecfa2bdfc83eea3cfccdb0f82abc6c4b876cb", size = 3525525, upload-time = "2026-03-02T16:04:38.058Z" }, - { url = "https://files.pythonhosted.org/packages/85/d7/a84edf412979e7d59c69b89a5871f90a49228360594680e667cb2c46a828/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1ccd42229aaac2df431562117ac7e667d702e8e44afdb6cf0e50fa3f18160f0b", size = 3466611, upload-time = "2026-03-02T15:57:50.759Z" }, - { url = "https://files.pythonhosted.org/packages/86/55/42404ce5770f6be26a2b0607e7866c31b9a4176c819e9a7a5e0a055770be/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0dcbc588cd5b725162c076eb9119342f6579c7f7f55057bb7e3c6ff27e13121", size = 3475812, upload-time = "2026-03-02T16:04:40.092Z" }, - { url = "https://files.pythonhosted.org/packages/ae/ae/29b87775fadc43e627cf582fe3bda4d02e300f6b8f2747c764950d13784c/sqlalchemy-2.0.48-cp313-cp313t-win32.whl", hash = "sha256:9764014ef5e58aab76220c5664abb5d47d5bc858d9debf821e55cfdd0f128485", size = 2141335, upload-time = "2026-03-02T15:52:51.518Z" }, - { url = "https://files.pythonhosted.org/packages/91/44/f39d063c90f2443e5b46ec4819abd3d8de653893aae92df42a5c4f5843de/sqlalchemy-2.0.48-cp313-cp313t-win_amd64.whl", hash = "sha256:e2f35b4cccd9ed286ad62e0a3c3ac21e06c02abc60e20aa51a3e305a30f5fa79", size = 2173095, upload-time = "2026-03-02T15:52:52.79Z" }, - { url = "https://files.pythonhosted.org/packages/f7/b3/f437eaa1cf028bb3c927172c7272366393e73ccd104dcf5b6963f4ab5318/sqlalchemy-2.0.48-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e2d0d88686e3d35a76f3e15a34e8c12d73fc94c1dea1cd55782e695cc14086dd", size = 2154401, upload-time = "2026-03-02T15:49:17.24Z" }, - { url = "https://files.pythonhosted.org/packages/6c/1c/b3abdf0f402aa3f60f0df6ea53d92a162b458fca2321d8f1f00278506402/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49b7bddc1eebf011ea5ab722fdbe67a401caa34a350d278cc7733c0e88fecb1f", size = 3274528, upload-time = "2026-03-02T15:50:41.489Z" }, - { url = "https://files.pythonhosted.org/packages/f2/5e/327428a034407651a048f5e624361adf3f9fbac9d0fa98e981e9c6ff2f5e/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:426c5ca86415d9b8945c7073597e10de9644802e2ff502b8e1f11a7a2642856b", size = 3279523, upload-time = "2026-03-02T15:53:32.962Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ca/ece73c81a918add0965b76b868b7b5359e068380b90ef1656ee995940c02/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:288937433bd44e3990e7da2402fabc44a3c6c25d3704da066b85b89a85474ae0", size = 3224312, upload-time = "2026-03-02T15:50:42.996Z" }, - { url = "https://files.pythonhosted.org/packages/88/11/fbaf1ae91fa4ee43f4fe79661cead6358644824419c26adb004941bdce7c/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8183dc57ae7d9edc1346e007e840a9f3d6aa7b7f165203a99e16f447150140d2", size = 3246304, upload-time = "2026-03-02T15:53:34.937Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a8/5fb0deb13930b4f2f698c5541ae076c18981173e27dd00376dbaea7a9c82/sqlalchemy-2.0.48-cp314-cp314-win32.whl", hash = "sha256:1182437cb2d97988cfea04cf6cdc0b0bb9c74f4d56ec3d08b81e23d621a28cc6", size = 2116565, upload-time = "2026-03-02T15:54:38.321Z" }, - { url = "https://files.pythonhosted.org/packages/95/7e/e83615cb63f80047f18e61e31e8e32257d39458426c23006deeaf48f463b/sqlalchemy-2.0.48-cp314-cp314-win_amd64.whl", hash = "sha256:144921da96c08feb9e2b052c5c5c1d0d151a292c6135623c6b2c041f2a45f9e0", size = 2142205, upload-time = "2026-03-02T15:54:39.831Z" }, - { url = "https://files.pythonhosted.org/packages/83/e3/69d8711b3f2c5135e9cde5f063bc1605860f0b2c53086d40c04017eb1f77/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aee45fd2c6c0f2b9cdddf48c48535e7471e42d6fb81adfde801da0bd5b93241", size = 3563519, upload-time = "2026-03-02T15:57:52.387Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4f/a7cce98facca73c149ea4578981594aaa5fd841e956834931de503359336/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cddca31edf8b0653090cbb54562ca027c421c58ddde2c0685f49ff56a1690e0", size = 3528611, upload-time = "2026-03-02T16:04:42.097Z" }, - { url = "https://files.pythonhosted.org/packages/cd/7d/5936c7a03a0b0cb0fa0cc425998821c6029756b0855a8f7ee70fba1de955/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7a936f1bb23d370b7c8cc079d5fce4c7d18da87a33c6744e51a93b0f9e97e9b3", size = 3472326, upload-time = "2026-03-02T15:57:54.423Z" }, - { url = "https://files.pythonhosted.org/packages/f4/33/cea7dfc31b52904efe3dcdc169eb4514078887dff1f5ae28a7f4c5d54b3c/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e004aa9248e8cb0a5f9b96d003ca7c1c0a5da8decd1066e7b53f59eb8ce7c62b", size = 3478453, upload-time = "2026-03-02T16:04:44.584Z" }, - { url = "https://files.pythonhosted.org/packages/c8/95/32107c4d13be077a9cae61e9ae49966a35dc4bf442a8852dd871db31f62e/sqlalchemy-2.0.48-cp314-cp314t-win32.whl", hash = "sha256:b8438ec5594980d405251451c5b7ea9aa58dda38eb7ac35fb7e4c696712ee24f", size = 2147209, upload-time = "2026-03-02T15:52:54.274Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d7/1e073da7a4bc645eb83c76067284a0374e643bc4be57f14cc6414656f92c/sqlalchemy-2.0.48-cp314-cp314t-win_amd64.whl", hash = "sha256:d854b3970067297f3a7fbd7a4683587134aa9b3877ee15aa29eea478dc68f933", size = 2182198, upload-time = "2026-03-02T15:52:55.606Z" }, - { url = "https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096", size = 1940202, upload-time = "2026-03-02T15:52:43.285Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] [[package]] @@ -836,3 +885,60 @@ sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b66 wheels = [ { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, ] + +[[package]] +name = "zstandard" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" }, + { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" }, + { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" }, + { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" }, + { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" }, + { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" }, + { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" }, + { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" }, + { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" }, + { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" }, + { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" }, + { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" }, + { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" }, + { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" }, + { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" }, + { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" }, + { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" }, + { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" }, + { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" }, + { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" }, + { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" }, + { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" }, + { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" }, + { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" }, + { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" }, + { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" }, +]