diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5b210e6a..adb43cb1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -46,20 +46,6 @@ updates: - dependency-name: "*" update-types: ["version-update:semver-major"] - - package-ecosystem: "pip" - directory: "/stats-functions" - schedule: - interval: "weekly" - groups: - patch-updates: - patterns: - - "*" - update-types: - - "patch" - ignore: - - dependency-name: "*" - update-types: ["version-update:semver-major"] - - package-ecosystem: "github-actions" # check for updates to github actions monthly directory: "/" diff --git a/.github/workflows/lint-test-function.yml b/.github/workflows/lint-test-function.yml index 8e3853e4..66ea6101 100644 --- a/.github/workflows/lint-test-function.yml +++ b/.github/workflows/lint-test-function.yml @@ -38,11 +38,11 @@ jobs: python -m pip install --upgrade pip pip install -r "$SOURCE_DIR/requirements.txt" - pip install -r "$SOURCE_DIR/requirements-dev.txt" - # - name: Lint - # run: | - # ruff check --output-format=github stats-functions/${{ inputs.function_name }}/src + - name: Lint + working-directory: stats-functions/${{ inputs.function_name }} + run: | + ruff check --output-format=github src - name: Test with pytest working-directory: stats-functions/${{ inputs.function_name }} diff --git a/.github/workflows/lint-test-stats-functions.yml b/.github/workflows/lint-test-stats-functions.yml deleted file mode 100644 index df81517b..00000000 --- a/.github/workflows/lint-test-stats-functions.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Lint and test stats-functions package - -on: - pull_request: - branches: - - main - paths: - - "stats-functions/stats_functions/**" - - "stats-functions/tests/*" - - "stats-functions/pyproject.toml" - - "stats-functions/uv.lock" - workflow_dispatch: - -concurrency: - # cancel any existing unfinished jobs if this workflow is retriggered - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - lint-test: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v7 - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version-file: "stats-api/pyproject.toml" - - - name: Install uv - uses: astral-sh/setup-uv@v7 - - - name: Install the project - run: uv sync --directory stats-functions --locked --all-extras --dev - - - name: Lint - run: | - uv run --directory stats-functions/stats_functions ruff check --output-format=github . - - - name: Type check - run: | - uv run --directory stats-functions/stats_functions ty check --output-format=github . - - - name: Run tests - run: uv run --directory stats-functions pytest tests diff --git a/.gitignore b/.gitignore index 6128a556..e43cc5ed 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ # pytest cache **/.pytest_cache/ +# coverage data +**/.coverage + # ruff cache **/.ruff_cache/ diff --git a/stats-functions/README.md b/stats-functions/README.md index 03789270..6f2cbdb7 100644 --- a/stats-functions/README.md +++ b/stats-functions/README.md @@ -1,7 +1,3 @@ -# Stats Functions package - -The stats-function package contains shared configuration and utilities that can be used with any cloud functions. - # Stats Functions Other directories contain python source code for production cloud functions which collect arXiv site usage data and persist it to the `stats-db.site_usage` database. See below for a description of each. All are cron jobs implemented as GCP Cloud Functions with pubsub triggers. Trigger messages are published by GCP Scheduler Jobs. All infrastructure as code can be found in `terraform/`. diff --git a/stats-functions/aggregate_hourly_downloads/Makefile b/stats-functions/aggregate_hourly_downloads/Makefile new file mode 100644 index 00000000..24177ab8 --- /dev/null +++ b/stats-functions/aggregate_hourly_downloads/Makefile @@ -0,0 +1,15 @@ +VENV := .venv +PIP := $(VENV)/bin/pip + +.PHONY: venv lint test + +venv: + python3 -m venv $(VENV) + $(PIP) install --upgrade pip + $(PIP) install -r src/requirements.txt + +lint: + $(VENV)/bin/ruff check src + +test: + $(VENV)/bin/pytest tests/ --cov=src/ --cov-report=html --cov-fail-under=80 diff --git a/stats-functions/aggregate_hourly_downloads/src/config.py b/stats-functions/aggregate_hourly_downloads/src/config.py index 1c940d7e..440d1756 100644 --- a/stats-functions/aggregate_hourly_downloads/src/config.py +++ b/stats-functions/aggregate_hourly_downloads/src/config.py @@ -1,10 +1,10 @@ -from typing import Optional -from arxiv_functions.config import FunctionConfig, DatabaseConfig + +from arxiv_functions.config import DatabaseConfig, FunctionConfig class Config(FunctionConfig): - read_db: Optional[DatabaseConfig] = None - write_db: Optional[DatabaseConfig] = None + read_db: DatabaseConfig | None = None + write_db: DatabaseConfig | None = None max_event_age_in_minutes: int = 50 batch_size_for_category_query: int = 10000 diff --git a/stats-functions/aggregate_hourly_downloads/src/entities.py b/stats-functions/aggregate_hourly_downloads/src/entities.py index 865f5145..0662c41f 100644 --- a/stats-functions/aggregate_hourly_downloads/src/entities.py +++ b/stats-functions/aggregate_hourly_downloads/src/entities.py @@ -1,4 +1,4 @@ -from sqlalchemy import Column, String, Integer +from sqlalchemy import Column, Integer, String from sqlalchemy.orm import declarative_base ReadBase = declarative_base() diff --git a/stats-functions/aggregate_hourly_downloads/src/main.py b/stats-functions/aggregate_hourly_downloads/src/main.py index bfc8178f..1fa92a8d 100644 --- a/stats-functions/aggregate_hourly_downloads/src/main.py +++ b/stats-functions/aggregate_hourly_downloads/src/main.py @@ -1,40 +1,34 @@ -import os import logging -from typing import Set, Dict, List, Tuple, Any, Union +import os from datetime import datetime, timedelta, timezone +from typing import Any import functions_framework +from arxiv.identifier import Identifier, IdentifierException +from arxiv_functions.exception import NoRetryError +from arxiv_functions.utils import ( + event_time_exceeds_retry_window, + get_engine_unix_socket, + parse_cloud_event_time, + set_up_cloud_logging, +) from cloudevents.http import CloudEvent - from google.cloud import bigquery from google.cloud.bigquery.table import RowIterator, _EmptyRowIterator - from sqlalchemy import Row -from sqlalchemy.orm import sessionmaker, aliased +from sqlalchemy.orm import aliased, sessionmaker +from stats_entities.site_usage import HourlyDownloads from config import get_config from entities import DocumentCategory, Metadata from models import ( - PaperCategories, - DownloadData, + AggregationResult, DownloadCounts, + DownloadData, DownloadKey, - AggregationResult, -) - -from stats_entities.site_usage import HourlyDownloads - -from arxiv_functions.exception import NoRetryError -from arxiv_functions.utils import ( - set_up_cloud_logging, - get_engine_unix_socket, - event_time_exceeds_retry_window, - parse_cloud_event_time, + PaperCategories, ) -from arxiv.identifier import Identifier, IdentifierException - - config = get_config(os.getenv("ENV")) logger = logging.getLogger(__name__) @@ -48,9 +42,9 @@ def process_table_rows( - rows: Union[RowIterator, _EmptyRowIterator], -) -> Tuple[ - Any, Set[str], Set[datetime], int, int + rows: RowIterator | _EmptyRowIterator, +) -> tuple[ + Any, set[str], set[datetime], int, int ]: # Changed return types to accommodate generator """ processes rows of data from bigquery @@ -85,14 +79,14 @@ def download_data_generator(): except IdentifierException: counts["bad_id"] += 1 continue - except Exception: + except Exception: # noqa: BLE001 - one bad row must not abort the whole batch counts["problem"] += 1 continue return download_data_generator(), paper_ids, time_periods, counts -def get_paper_categories(paper_ids: Set[str]) -> List[Row[Tuple[str, str, int]]]: +def get_paper_categories(paper_ids: set[str]) -> list[Row[tuple[str, str, int]]]: meta = aliased(Metadata) dc = aliased(DocumentCategory) @@ -120,10 +114,10 @@ def get_paper_categories(paper_ids: Set[str]) -> List[Row[Tuple[str, str, int]]] def process_paper_categories( - data: List[Row[Tuple[str, str, int]]], -) -> Dict[str, PaperCategories]: + data: list[Row[tuple[str, str, int]]], +) -> dict[str, PaperCategories]: # format paper categories into dictionary - paper_categories: Dict[str, PaperCategories] = {} + paper_categories: dict[str, PaperCategories] = {} for row in data: paper_id, cat, is_primary = row entry = paper_categories.setdefault(paper_id, PaperCategories(paper_id)) @@ -136,14 +130,14 @@ def process_paper_categories( def aggregate_data( - download_data: List[DownloadData], - paper_categories: Dict[str, PaperCategories], -) -> Dict[DownloadKey, DownloadCounts]: + download_data: list[DownloadData], + paper_categories: dict[str, PaperCategories], +) -> dict[DownloadKey, DownloadCounts]: """creates a dictionary of download counts by time, country, download type, and category goes through each download entry, matches it with its caegories and adds the number of downloads to the count """ logger.info("Aggregating download data") - all_data: Dict[DownloadKey, DownloadCounts] = {} + all_data: dict[DownloadKey, DownloadCounts] = {} missing_data_count = 0 for entry in download_data: @@ -185,8 +179,8 @@ def aggregate_data( def insert_into_database( - aggregated_data: Dict[DownloadKey, DownloadCounts], - time_periods: Set[datetime], # Changed to Set + aggregated_data: dict[DownloadKey, DownloadCounts], + time_periods: set[datetime], # Changed to Set ) -> int: """adds the data from an hour of downloads into the database uses bulk insert and update statements to increase efficiency @@ -222,7 +216,7 @@ def insert_into_database( def perform_aggregation( - rows: Union[RowIterator, _EmptyRowIterator], + rows: RowIterator | _EmptyRowIterator, ) -> AggregationResult: logger.info("Processing results of log query") data_gen, paper_ids, time_periods, counts = process_table_rows(rows) @@ -383,7 +377,7 @@ def aggregate_hourly_downloads(cloud_event: CloudEvent): if read_engine: try: read_engine.dispose() - except Exception as dispose_err: + except Exception as dispose_err: # noqa: BLE001 - a dispose failure must not mask the original error logger.warning(f"Failed to dispose read_engine: {dispose_err}") finally: read_engine = None @@ -392,11 +386,11 @@ def aggregate_hourly_downloads(cloud_event: CloudEvent): if write_engine: try: write_engine.dispose() - except Exception as dispose_err: + except Exception as dispose_err: # noqa: BLE001 - a dispose failure must not mask the original error logger.warning(f"Failed to dispose write_engine: {dispose_err}") finally: write_engine = None WriteSessionFactory = None # reraise to log traceback - raise e + raise diff --git a/stats-functions/aggregate_hourly_downloads/src/models.py b/stats-functions/aggregate_hourly_downloads/src/models.py index 203099e7..72e54701 100644 --- a/stats-functions/aggregate_hourly_downloads/src/models.py +++ b/stats-functions/aggregate_hourly_downloads/src/models.py @@ -1,6 +1,6 @@ import logging -from typing import Set, Literal from datetime import datetime +from typing import Literal from arxiv.taxonomy.category import Category from arxiv.taxonomy.definitions import CATEGORIES @@ -14,7 +14,7 @@ class PaperCategories: paper_id: str primary: Category - crosses: Set[Category] + crosses: set[Category] def __init__(self, id: str): self.paper_id = id diff --git a/stats-functions/aggregate_hourly_downloads/src/requirements-dev.txt b/stats-functions/aggregate_hourly_downloads/src/requirements-dev.txt deleted file mode 100644 index d949d9bb..00000000 --- a/stats-functions/aggregate_hourly_downloads/src/requirements-dev.txt +++ /dev/null @@ -1,3 +0,0 @@ -pytest -pytest-cov -ruff \ No newline at end of file diff --git a/stats-functions/aggregate_hourly_downloads/src/requirements.in b/stats-functions/aggregate_hourly_downloads/src/requirements.in index 995b566c..7bd50cbc 100644 --- a/stats-functions/aggregate_hourly_downloads/src/requirements.in +++ b/stats-functions/aggregate_hourly_downloads/src/requirements.in @@ -6,4 +6,7 @@ sqlalchemy>=2.0.26 pydantic==2.* stats-entities @ git+https://github.com/arXiv/stats.git@main#subdirectory=stats-entities arxiv-functions @ git+https://github.com/arXiv/arxiv-base.git@develop#subdirectory=arxiv-functions -pymysql>=1.1.0 \ No newline at end of file +pymysql>=1.1.0 +pytest +pytest-cov +ruff \ No newline at end of file diff --git a/stats-functions/aggregate_hourly_downloads/src/requirements.txt b/stats-functions/aggregate_hourly_downloads/src/requirements.txt index f0bc024b..c27795c4 100644 --- a/stats-functions/aggregate_hourly_downloads/src/requirements.txt +++ b/stats-functions/aggregate_hourly_downloads/src/requirements.txt @@ -6,7 +6,7 @@ anyio==4.13.0 # via starlette arxiv-base @ git+https://github.com/arXiv/arxiv-base.git@a1fed38fcae2d8630acc9aa8b53c2f4a1776f5c5#egg=arxiv_base # via -r requirements.in -arxiv-functions @ git+https://github.com/arXiv/arxiv-base.git@c63ad1cc3884445b755bae321028368c3c8272af#subdirectory=arxiv-functions +arxiv-functions @ git+https://github.com/arXiv/arxiv-base.git@0987739e90c582aa69578c7ffc60e9b15b02410d#subdirectory=arxiv-functions # via -r requirements.in bleach==6.3.0 # via arxiv-base @@ -33,6 +33,8 @@ cloudevents==1.12.0 # via # arxiv-functions # functions-framework +coverage==7.15.4 + # via pytest-cov cryptography==46.0.7 # via # google-auth @@ -140,6 +142,8 @@ idna==3.11 # requests importlib-metadata==8.7.1 # via opentelemetry-api +iniconfig==2.3.0 + # via pytest itsdangerous==2.2.0 # via flask jinja2==3.1.6 @@ -173,6 +177,11 @@ packaging==26.1 # deprecation # google-cloud-bigquery # gunicorn + # pytest +pluggy==1.6.0 + # via + # pytest + # pytest-cov proto-plus==1.27.2 # via # google-api-core @@ -212,10 +221,18 @@ pydantic-settings==2.13.1 # via # arxiv-base # arxiv-functions +pygments==2.20.0 + # via pytest pyjwt==2.12.1 # via arxiv-base pymysql==1.1.2 # via -r requirements.in +pytest==9.1.1 + # via + # -r requirements.in + # pytest-cov +pytest-cov==7.1.0 + # via -r requirements.in python-dateutil==2.9.0.post0 # via # arxiv-functions @@ -243,6 +260,8 @@ ruamel-yaml==0.18.17 # via arxiv-base ruamel-yaml-clib==0.2.15 # via ruamel-yaml +ruff==0.16.2 + # via -r requirements.in s3transfer==0.16.0 # via boto3 setuptools==78.1.1 @@ -260,7 +279,7 @@ sqlalchemy==2.0.49 # stats-entities starlette==0.52.1 # via functions-framework -stats-entities @ git+https://github.com/arXiv/stats.git@3635615cf4ded2540fc684364f17ec68a6c4acde#subdirectory=stats-entities +stats-entities @ git+https://github.com/arXiv/stats.git@4e93c3c6a952acbb392e06e5c6b3d0727b95bb1b#subdirectory=stats-entities # via -r requirements.in termcolor==3.3.0 # via fire diff --git a/stats-functions/hourly_edge_requests/Makefile b/stats-functions/hourly_edge_requests/Makefile new file mode 100644 index 00000000..24177ab8 --- /dev/null +++ b/stats-functions/hourly_edge_requests/Makefile @@ -0,0 +1,15 @@ +VENV := .venv +PIP := $(VENV)/bin/pip + +.PHONY: venv lint test + +venv: + python3 -m venv $(VENV) + $(PIP) install --upgrade pip + $(PIP) install -r src/requirements.txt + +lint: + $(VENV)/bin/ruff check src + +test: + $(VENV)/bin/pytest tests/ --cov=src/ --cov-report=html --cov-fail-under=80 diff --git a/stats-functions/hourly_edge_requests/src/config.py b/stats-functions/hourly_edge_requests/src/config.py index aefe639f..ec4e1e79 100644 --- a/stats-functions/hourly_edge_requests/src/config.py +++ b/stats-functions/hourly_edge_requests/src/config.py @@ -1,12 +1,15 @@ -from typing import Optional -from stats_functions.config import DatabaseConfig, FunctionConfig + +from arxiv_functions.config import DatabaseConfig, FunctionConfig +from pydantic import Field class Config(FunctionConfig): - db: Optional[DatabaseConfig] = None + db: DatabaseConfig | None = None max_event_age_in_minutes: int = 50 - fastly_service_id: dict = {"arxiv.org": "umpGzwE2hXfa2aRXsOQXZ4"} + fastly_service_id: dict = Field( + default_factory=lambda: {"arxiv.org": "umpGzwE2hXfa2aRXsOQXZ4"} + ) fastly_node_number: int = 0 # existing convention, corresponds to 'fastly' hour_delay: int = 1 diff --git a/stats-functions/hourly_edge_requests/src/main.py b/stats-functions/hourly_edge_requests/src/main.py index cbf87e93..aa14a5ef 100644 --- a/stats-functions/hourly_edge_requests/src/main.py +++ b/stats-functions/hourly_edge_requests/src/main.py @@ -1,29 +1,25 @@ -import os import logging +import os from datetime import datetime, timedelta, timezone +import fastly import functions_framework +from arxiv_functions.exception import NoRetryError +from arxiv_functions.utils import ( + event_time_exceeds_retry_window, + get_engine_unix_socket, + parse_cloud_event_time, + set_up_cloud_logging, +) from cloudevents.http import CloudEvent - -import fastly from fastly.api import stats_api from fastly.exceptions import ApiException - +from pydantic import ValidationError from sqlalchemy.orm import sessionmaker +from stats_entities.site_usage import HourlyRequests from config import get_config from models import FastlyStatsApiResponse -from pydantic import ValidationError - -from stats_entities.site_usage import HourlyRequests -from stats_functions.exception import NoRetryError -from stats_functions.utils import ( - set_up_cloud_logging, - get_engine_unix_socket, - event_time_exceeds_retry_window, - parse_cloud_event_time, -) - config = get_config(os.getenv("ENV")) @@ -70,7 +66,7 @@ def get_fastly_stats(start_time: int, end_time: int) -> FastlyStatsApiResponse: def sum_requests(response: FastlyStatsApiResponse) -> int: - return sum(response.stats[pop].edge_requests for pop in response.stats.keys()) + return sum(response.stats[pop].edge_requests for pop in response.stats) def write_to_db(hour: datetime, count: int): @@ -128,11 +124,10 @@ def validate_inputs(cloud_event: CloudEvent) -> datetime: def get_hourly_edge_requests(cloud_event: CloudEvent): global engine, SessionFactory - if config.env != "TEST": - if SessionFactory is None: - logger.info("Initializing engine and sessionmaker") - engine = get_engine_unix_socket(config.db) - SessionFactory = sessionmaker(bind=engine) + if config.env != "TEST" and SessionFactory is None: + logger.info("Initializing engine and sessionmaker") + engine = get_engine_unix_socket(config.db) + SessionFactory = sessionmaker(bind=engine) try: hour = validate_inputs(cloud_event) diff --git a/stats-functions/hourly_edge_requests/src/models.py b/stats-functions/hourly_edge_requests/src/models.py index d51fe263..5a7a6f89 100644 --- a/stats-functions/hourly_edge_requests/src/models.py +++ b/stats-functions/hourly_edge_requests/src/models.py @@ -1,5 +1,5 @@ + from pydantic import BaseModel, ConfigDict -from typing import Dict class Pop(BaseModel): @@ -9,4 +9,4 @@ class Pop(BaseModel): class FastlyStatsApiResponse(BaseModel): - stats: Dict[str, Pop] + stats: dict[str, Pop] diff --git a/stats-functions/hourly_edge_requests/src/requirements-dev.txt b/stats-functions/hourly_edge_requests/src/requirements-dev.txt deleted file mode 100644 index d949d9bb..00000000 --- a/stats-functions/hourly_edge_requests/src/requirements-dev.txt +++ /dev/null @@ -1,3 +0,0 @@ -pytest -pytest-cov -ruff \ No newline at end of file diff --git a/stats-functions/hourly_edge_requests/src/requirements.in b/stats-functions/hourly_edge_requests/src/requirements.in new file mode 100644 index 00000000..37feca47 --- /dev/null +++ b/stats-functions/hourly_edge_requests/src/requirements.in @@ -0,0 +1,11 @@ +functions-framework==3.* +google-cloud-logging +sqlalchemy>=2.0.26 +stats-entities @ git+https://github.com/arXiv/stats.git@main#subdirectory=stats-entities +fastly==12.* +pydantic==2.* +pymysql>=1.1.0 +arxiv-functions @ git+https://github.com/arXiv/arxiv-base.git@develop#subdirectory=arxiv-functions +pytest +pytest-cov +ruff \ No newline at end of file diff --git a/stats-functions/hourly_edge_requests/src/requirements.txt b/stats-functions/hourly_edge_requests/src/requirements.txt index 9b955221..a53d17b2 100644 --- a/stats-functions/hourly_edge_requests/src/requirements.txt +++ b/stats-functions/hourly_edge_requests/src/requirements.txt @@ -1,8 +1,197 @@ -functions-framework==3.* -google-cloud-logging -sqlalchemy>=2.0.26 -stats-entities @ git+https://github.com/arXiv/stats.git@main#subdirectory=stats-entities -fastly==12.* -pydantic==2.* -stats-functions @ git+https://github.com/arXiv/stats.git@main#subdirectory=stats-functions -pymysql>=1.1.0 \ No newline at end of file +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.in -o requirements.txt -p 3.13 +annotated-types==0.8.0 + # via pydantic +anyio==4.14.2 + # via starlette +arxiv-functions @ git+https://github.com/arXiv/arxiv-base.git@0987739e90c582aa69578c7ffc60e9b15b02410d#subdirectory=arxiv-functions + # via -r requirements.in +blinker==1.9.0 + # via flask +certifi==2026.7.22 + # via requests +cffi==2.1.1 + # via cryptography +charset-normalizer==3.5.0 + # via requests +click==8.4.2 + # via + # flask + # functions-framework + # uvicorn +cloudevents==1.12.0 + # via + # arxiv-functions + # functions-framework +coverage==7.15.4 + # via pytest-cov +cryptography==50.0.0 + # via google-auth +deprecation==2.1.0 + # via cloudevents +fastly==12.1.0 + # via -r requirements.in +flask==3.1.3 + # via functions-framework +functions-framework==3.10.2 + # via -r requirements.in +google-api-core==2.34.0 + # via + # google-cloud-appengine-logging + # google-cloud-core + # google-cloud-logging +google-auth==2.56.3 + # via + # google-api-core + # google-cloud-appengine-logging + # google-cloud-core + # google-cloud-logging +google-cloud-appengine-logging==1.10.0 + # via google-cloud-logging +google-cloud-audit-log==0.6.1 + # via google-cloud-logging +google-cloud-core==2.6.1 + # via google-cloud-logging +google-cloud-logging==3.16.2 + # via + # -r requirements.in + # arxiv-functions +googleapis-common-protos==1.75.1 + # via + # google-api-core + # google-cloud-audit-log + # grpc-google-iam-v1 + # grpcio-status +grpc-google-iam-v1==0.14.5 + # via google-cloud-logging +grpcio==1.83.0 + # via + # google-api-core + # google-cloud-appengine-logging + # google-cloud-logging + # googleapis-common-protos + # grpc-google-iam-v1 + # grpcio-status +grpcio-status==1.83.0 + # via google-api-core +gunicorn==26.0.0 + # via + # functions-framework + # uvicorn-worker +h11==0.16.0 + # via uvicorn +idna==3.18 + # via + # anyio + # requests +iniconfig==2.3.0 + # via pytest +itsdangerous==2.2.0 + # via flask +jinja2==3.1.6 + # via flask +markupsafe==3.0.3 + # via + # flask + # jinja2 + # werkzeug +opentelemetry-api==1.44.0 + # via google-cloud-logging +packaging==26.3 + # via + # deprecation + # gunicorn + # pytest +pluggy==1.6.0 + # via + # pytest + # pytest-cov +proto-plus==1.28.3 + # via + # google-api-core + # google-cloud-appengine-logging + # google-cloud-logging +protobuf==7.35.1 + # via + # google-api-core + # google-cloud-appengine-logging + # google-cloud-audit-log + # google-cloud-logging + # googleapis-common-protos + # grpc-google-iam-v1 + # grpcio-status + # proto-plus +pyasn1==0.6.4 + # via pyasn1-modules +pyasn1-modules==0.4.2 + # via google-auth +pycparser==3.0 + # via cffi +pydantic==2.13.4 + # via + # -r requirements.in + # arxiv-functions + # pydantic-settings +pydantic-core==2.46.4 + # via pydantic +pydantic-settings==2.15.0 + # via arxiv-functions +pygments==2.20.0 + # via pytest +pymysql==1.2.0 + # via -r requirements.in +pytest==9.1.1 + # via + # -r requirements.in + # pytest-cov +pytest-cov==7.1.0 + # via -r requirements.in +python-dateutil==2.9.0.post0 + # via + # arxiv-functions + # fastly +python-dotenv==1.2.2 + # via pydantic-settings +requests==2.34.2 + # via google-api-core +ruff==0.16.2 + # via -r requirements.in +six==1.17.0 + # via python-dateutil +sqlalchemy==2.0.52 + # via + # -r requirements.in + # arxiv-functions + # stats-entities +starlette==1.6.0 + # via functions-framework +stats-entities @ git+https://github.com/arXiv/stats.git@4e93c3c6a952acbb392e06e5c6b3d0727b95bb1b#subdirectory=stats-entities + # via -r requirements.in +typing-extensions==4.16.0 + # via + # grpcio + # opentelemetry-api + # pydantic + # pydantic-core + # sqlalchemy + # typing-inspection +typing-inspection==0.4.4 + # via + # pydantic + # pydantic-settings +urllib3==2.7.0 + # via + # fastly + # requests +uvicorn==0.52.1 + # via + # functions-framework + # uvicorn-worker +uvicorn-worker==0.4.0 + # via functions-framework +watchdog==6.0.0 + # via functions-framework +werkzeug==3.1.8 + # via + # flask + # functions-framework diff --git a/stats-functions/hourly_edge_requests/tests/test_hourly_edge_requests.py b/stats-functions/hourly_edge_requests/tests/test_hourly_edge_requests.py index 6916d448..56f66044 100644 --- a/stats-functions/hourly_edge_requests/tests/test_hourly_edge_requests.py +++ b/stats-functions/hourly_edge_requests/tests/test_hourly_edge_requests.py @@ -28,7 +28,7 @@ validate_inputs, ) -from stats_functions.exception import NoRetryError +from arxiv_functions.exception import NoRetryError from stats_entities.site_usage import SiteUsageBase, HourlyRequests diff --git a/stats-functions/monthly_downloads/Makefile b/stats-functions/monthly_downloads/Makefile new file mode 100644 index 00000000..24177ab8 --- /dev/null +++ b/stats-functions/monthly_downloads/Makefile @@ -0,0 +1,15 @@ +VENV := .venv +PIP := $(VENV)/bin/pip + +.PHONY: venv lint test + +venv: + python3 -m venv $(VENV) + $(PIP) install --upgrade pip + $(PIP) install -r src/requirements.txt + +lint: + $(VENV)/bin/ruff check src + +test: + $(VENV)/bin/pytest tests/ --cov=src/ --cov-report=html --cov-fail-under=80 diff --git a/stats-functions/monthly_downloads/src/config.py b/stats-functions/monthly_downloads/src/config.py index 7c925737..84043f24 100644 --- a/stats-functions/monthly_downloads/src/config.py +++ b/stats-functions/monthly_downloads/src/config.py @@ -1,9 +1,9 @@ -from typing import Optional -from stats_functions.config import FunctionConfig, DatabaseConfig + +from arxiv_functions.config import DatabaseConfig, FunctionConfig class Config(FunctionConfig): - db: Optional[DatabaseConfig] = None + db: DatabaseConfig | None = None max_event_age_in_minutes: int = 50 diff --git a/stats-functions/monthly_downloads/src/main.py b/stats-functions/monthly_downloads/src/main.py index 04824c46..dccc2643 100644 --- a/stats-functions/monthly_downloads/src/main.py +++ b/stats-functions/monthly_downloads/src/main.py @@ -1,26 +1,23 @@ -import os import logging +import os from datetime import date, datetime -from dateutil.relativedelta import relativedelta import functions_framework +from arxiv_functions.exception import NoRetryError +from arxiv_functions.utils import ( + event_time_exceeds_retry_window, + get_engine_unix_socket, + parse_cloud_event_time, + set_up_cloud_logging, +) from cloudevents.http import CloudEvent - +from dateutil.relativedelta import relativedelta from sqlalchemy import select from sqlalchemy.orm import sessionmaker from sqlalchemy.sql import func - -from config import get_config - from stats_entities.site_usage import HourlyDownloads, MonthlyDownloads -from stats_functions.exception import NoRetryError -from stats_functions.utils import ( - set_up_cloud_logging, - get_engine_unix_socket, - event_time_exceeds_retry_window, - parse_cloud_event_time, -) +from config import get_config config = get_config(os.getenv("ENV")) @@ -32,10 +29,13 @@ def get_first_and_last_hour(month: date) -> tuple[datetime, datetime]: - first_hour = datetime(month.year, month.month, month.day) + # naive UTC, to match the naive start_dttm column in the DB + first_hour = datetime(month.year, month.month, month.day) # noqa: DTZ001 last_day = (month + relativedelta(months=1)) - relativedelta(days=1) - return first_hour, datetime(last_day.year, last_day.month, last_day.day, 23) + return first_hour, datetime( # noqa: DTZ001 + last_day.year, last_day.month, last_day.day, 23 + ) def get_download_count(start: datetime, end: datetime): @@ -77,7 +77,8 @@ def validate_cloud_event(cloud_event: CloudEvent) -> date: def validate_month(cloud_event: CloudEvent) -> date: month = cloud_event.data["message"]["attributes"]["month"] - return datetime.strptime(month, "%Y-%m-%d").replace(day=1).date() + # naive UTC; feeds both the Date and naive DateTime columns in the DB + return datetime.strptime(month, "%Y-%m-%d").replace(day=1).date() # noqa: DTZ007 def validate_inputs(cloud_event: CloudEvent) -> date: @@ -100,11 +101,10 @@ def validate_inputs(cloud_event: CloudEvent) -> date: def get_monthly_downloads(cloud_event: CloudEvent): global engine, SessionFactory - if config.env != "TEST": - if SessionFactory is None: - logger.info("Initializing engine and sessionmaker") - engine = get_engine_unix_socket(config.db) - SessionFactory = sessionmaker(bind=engine) + if config.env != "TEST" and SessionFactory is None: + logger.info("Initializing engine and sessionmaker") + engine = get_engine_unix_socket(config.db) + SessionFactory = sessionmaker(bind=engine) try: month = validate_inputs(cloud_event) diff --git a/stats-functions/monthly_downloads/src/requirements-dev.txt b/stats-functions/monthly_downloads/src/requirements-dev.txt deleted file mode 100644 index d949d9bb..00000000 --- a/stats-functions/monthly_downloads/src/requirements-dev.txt +++ /dev/null @@ -1,3 +0,0 @@ -pytest -pytest-cov -ruff \ No newline at end of file diff --git a/stats-functions/monthly_downloads/src/requirements.in b/stats-functions/monthly_downloads/src/requirements.in new file mode 100644 index 00000000..093a37cd --- /dev/null +++ b/stats-functions/monthly_downloads/src/requirements.in @@ -0,0 +1,10 @@ +functions-framework==3.* +google-cloud-logging +sqlalchemy>=2.0.26 +stats-entities @ git+https://github.com/arXiv/stats.git@main#subdirectory=stats-entities +pydantic==2.* +pymysql>=1.1.0 +arxiv-functions @ git+https://github.com/arXiv/arxiv-base.git@develop#subdirectory=arxiv-functions +pytest +pytest-cov +ruff \ No newline at end of file diff --git a/stats-functions/monthly_downloads/src/requirements.txt b/stats-functions/monthly_downloads/src/requirements.txt index e0891229..edb3df08 100644 --- a/stats-functions/monthly_downloads/src/requirements.txt +++ b/stats-functions/monthly_downloads/src/requirements.txt @@ -1,7 +1,191 @@ -functions-framework==3.* -google-cloud-logging -sqlalchemy>=2.0.26 -stats-entities @ git+https://github.com/arXiv/stats.git@main#subdirectory=stats-entities -pydantic==2.* -stats-functions @ git+https://github.com/arXiv/stats.git@main#subdirectory=stats-functions -pymysql>=1.1.0 \ No newline at end of file +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.in -o requirements.txt -p 3.13 +annotated-types==0.8.0 + # via pydantic +anyio==4.14.2 + # via starlette +arxiv-functions @ git+https://github.com/arXiv/arxiv-base.git@0987739e90c582aa69578c7ffc60e9b15b02410d#subdirectory=arxiv-functions + # via -r requirements.in +blinker==1.9.0 + # via flask +certifi==2026.7.22 + # via requests +cffi==2.1.1 + # via cryptography +charset-normalizer==3.5.0 + # via requests +click==8.4.2 + # via + # flask + # functions-framework + # uvicorn +cloudevents==1.12.0 + # via + # arxiv-functions + # functions-framework +coverage==7.15.4 + # via pytest-cov +cryptography==50.0.0 + # via google-auth +deprecation==2.1.0 + # via cloudevents +flask==3.1.3 + # via functions-framework +functions-framework==3.10.2 + # via -r requirements.in +google-api-core==2.34.0 + # via + # google-cloud-appengine-logging + # google-cloud-core + # google-cloud-logging +google-auth==2.56.3 + # via + # google-api-core + # google-cloud-appengine-logging + # google-cloud-core + # google-cloud-logging +google-cloud-appengine-logging==1.10.0 + # via google-cloud-logging +google-cloud-audit-log==0.6.1 + # via google-cloud-logging +google-cloud-core==2.6.1 + # via google-cloud-logging +google-cloud-logging==3.16.2 + # via + # -r requirements.in + # arxiv-functions +googleapis-common-protos==1.75.1 + # via + # google-api-core + # google-cloud-audit-log + # grpc-google-iam-v1 + # grpcio-status +grpc-google-iam-v1==0.14.5 + # via google-cloud-logging +grpcio==1.83.0 + # via + # google-api-core + # google-cloud-appengine-logging + # google-cloud-logging + # googleapis-common-protos + # grpc-google-iam-v1 + # grpcio-status +grpcio-status==1.83.0 + # via google-api-core +gunicorn==26.0.0 + # via + # functions-framework + # uvicorn-worker +h11==0.16.0 + # via uvicorn +idna==3.18 + # via + # anyio + # requests +iniconfig==2.3.0 + # via pytest +itsdangerous==2.2.0 + # via flask +jinja2==3.1.6 + # via flask +markupsafe==3.0.3 + # via + # flask + # jinja2 + # werkzeug +opentelemetry-api==1.44.0 + # via google-cloud-logging +packaging==26.3 + # via + # deprecation + # gunicorn + # pytest +pluggy==1.6.0 + # via + # pytest + # pytest-cov +proto-plus==1.28.3 + # via + # google-api-core + # google-cloud-appengine-logging + # google-cloud-logging +protobuf==7.35.1 + # via + # google-api-core + # google-cloud-appengine-logging + # google-cloud-audit-log + # google-cloud-logging + # googleapis-common-protos + # grpc-google-iam-v1 + # grpcio-status + # proto-plus +pyasn1==0.6.4 + # via pyasn1-modules +pyasn1-modules==0.4.2 + # via google-auth +pycparser==3.0 + # via cffi +pydantic==2.13.4 + # via + # -r requirements.in + # arxiv-functions + # pydantic-settings +pydantic-core==2.46.4 + # via pydantic +pydantic-settings==2.15.0 + # via arxiv-functions +pygments==2.20.0 + # via pytest +pymysql==1.2.0 + # via -r requirements.in +pytest==9.1.1 + # via + # -r requirements.in + # pytest-cov +pytest-cov==7.1.0 + # via -r requirements.in +python-dateutil==2.9.0.post0 + # via arxiv-functions +python-dotenv==1.2.2 + # via pydantic-settings +requests==2.34.2 + # via google-api-core +ruff==0.16.2 + # via -r requirements.in +six==1.17.0 + # via python-dateutil +sqlalchemy==2.0.52 + # via + # -r requirements.in + # arxiv-functions + # stats-entities +starlette==1.6.0 + # via functions-framework +stats-entities @ git+https://github.com/arXiv/stats.git@4e93c3c6a952acbb392e06e5c6b3d0727b95bb1b#subdirectory=stats-entities + # via -r requirements.in +typing-extensions==4.16.0 + # via + # grpcio + # opentelemetry-api + # pydantic + # pydantic-core + # sqlalchemy + # typing-inspection +typing-inspection==0.4.4 + # via + # pydantic + # pydantic-settings +urllib3==2.7.0 + # via requests +uvicorn==0.52.1 + # via + # functions-framework + # uvicorn-worker +uvicorn-worker==0.4.0 + # via functions-framework +watchdog==6.0.0 + # via functions-framework +werkzeug==3.1.8 + # via + # flask + # functions-framework diff --git a/stats-functions/monthly_downloads/tests/test_monthly_downloads.py b/stats-functions/monthly_downloads/tests/test_monthly_downloads.py index 5bd137d8..5662357e 100644 --- a/stats-functions/monthly_downloads/tests/test_monthly_downloads.py +++ b/stats-functions/monthly_downloads/tests/test_monthly_downloads.py @@ -23,7 +23,7 @@ validate_inputs, ) from stats_entities.site_usage import SiteUsageBase, HourlyDownloads, MonthlyDownloads -from stats_functions.exception import NoRetryError +from arxiv_functions.exception import NoRetryError @pytest.fixture diff --git a/stats-functions/monthly_submissions/Makefile b/stats-functions/monthly_submissions/Makefile new file mode 100644 index 00000000..24177ab8 --- /dev/null +++ b/stats-functions/monthly_submissions/Makefile @@ -0,0 +1,15 @@ +VENV := .venv +PIP := $(VENV)/bin/pip + +.PHONY: venv lint test + +venv: + python3 -m venv $(VENV) + $(PIP) install --upgrade pip + $(PIP) install -r src/requirements.txt + +lint: + $(VENV)/bin/ruff check src + +test: + $(VENV)/bin/pytest tests/ --cov=src/ --cov-report=html --cov-fail-under=80 diff --git a/stats-functions/monthly_submissions/src/config.py b/stats-functions/monthly_submissions/src/config.py index 3fec3ee5..0f28656d 100644 --- a/stats-functions/monthly_submissions/src/config.py +++ b/stats-functions/monthly_submissions/src/config.py @@ -1,10 +1,10 @@ -from typing import Optional -from stats_functions.config import FunctionConfig, DatabaseConfig + +from arxiv_functions.config import DatabaseConfig, FunctionConfig class Config(FunctionConfig): - read_db: Optional[DatabaseConfig] = None - write_db: Optional[DatabaseConfig] = None + read_db: DatabaseConfig | None = None + write_db: DatabaseConfig | None = None max_event_age_in_minutes: int = 50 diff --git a/stats-functions/monthly_submissions/src/entities.py b/stats-functions/monthly_submissions/src/entities.py index 35d1e277..c45e389e 100644 --- a/stats-functions/monthly_submissions/src/entities.py +++ b/stats-functions/monthly_submissions/src/entities.py @@ -1,4 +1,4 @@ -from sqlalchemy import Column, Integer, String, Text, DateTime +from sqlalchemy import Column, DateTime, Integer, String, Text from sqlalchemy.orm import declarative_base ReadBase = declarative_base() diff --git a/stats-functions/monthly_submissions/src/main.py b/stats-functions/monthly_submissions/src/main.py index 08696099..14864b0a 100644 --- a/stats-functions/monthly_submissions/src/main.py +++ b/stats-functions/monthly_submissions/src/main.py @@ -1,28 +1,25 @@ -import os import logging +import os from datetime import date, datetime -from dateutil.relativedelta import relativedelta import functions_framework +from arxiv_functions.exception import NoRetryError +from arxiv_functions.utils import ( + event_time_exceeds_retry_window, + get_engine_unix_socket, + parse_cloud_event_time, + set_up_cloud_logging, +) from cloudevents.http import CloudEvent - +from dateutil.relativedelta import relativedelta from sqlalchemy import select from sqlalchemy.orm import sessionmaker from sqlalchemy.sql import func +from stats_entities.site_usage import MonthlySubmissions from config import get_config - -from stats_entities.site_usage import MonthlySubmissions from entities import Document -from stats_functions.exception import NoRetryError -from stats_functions.utils import ( - set_up_cloud_logging, - get_engine_unix_socket, - event_time_exceeds_retry_window, - parse_cloud_event_time, -) - config = get_config(os.getenv("ENV")) logger = logging.getLogger(__name__) @@ -78,7 +75,8 @@ def validate_cloud_event(cloud_event: CloudEvent) -> date: def validate_month(cloud_event: CloudEvent) -> date: month = cloud_event.data["message"]["attributes"]["month"] - return datetime.strptime(month, "%Y-%m-%d").replace(day=1).date() + # naive UTC; immediately truncated to a date, matching the Date column in the DB + return datetime.strptime(month, "%Y-%m-%d").replace(day=1).date() # noqa: DTZ007 def validate_inputs(cloud_event: CloudEvent) -> date: diff --git a/stats-functions/monthly_submissions/src/requirements-dev.txt b/stats-functions/monthly_submissions/src/requirements-dev.txt deleted file mode 100644 index d949d9bb..00000000 --- a/stats-functions/monthly_submissions/src/requirements-dev.txt +++ /dev/null @@ -1,3 +0,0 @@ -pytest -pytest-cov -ruff \ No newline at end of file diff --git a/stats-functions/monthly_submissions/src/requirements.in b/stats-functions/monthly_submissions/src/requirements.in new file mode 100644 index 00000000..093a37cd --- /dev/null +++ b/stats-functions/monthly_submissions/src/requirements.in @@ -0,0 +1,10 @@ +functions-framework==3.* +google-cloud-logging +sqlalchemy>=2.0.26 +stats-entities @ git+https://github.com/arXiv/stats.git@main#subdirectory=stats-entities +pydantic==2.* +pymysql>=1.1.0 +arxiv-functions @ git+https://github.com/arXiv/arxiv-base.git@develop#subdirectory=arxiv-functions +pytest +pytest-cov +ruff \ No newline at end of file diff --git a/stats-functions/monthly_submissions/src/requirements.txt b/stats-functions/monthly_submissions/src/requirements.txt index e0891229..edb3df08 100644 --- a/stats-functions/monthly_submissions/src/requirements.txt +++ b/stats-functions/monthly_submissions/src/requirements.txt @@ -1,7 +1,191 @@ -functions-framework==3.* -google-cloud-logging -sqlalchemy>=2.0.26 -stats-entities @ git+https://github.com/arXiv/stats.git@main#subdirectory=stats-entities -pydantic==2.* -stats-functions @ git+https://github.com/arXiv/stats.git@main#subdirectory=stats-functions -pymysql>=1.1.0 \ No newline at end of file +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.in -o requirements.txt -p 3.13 +annotated-types==0.8.0 + # via pydantic +anyio==4.14.2 + # via starlette +arxiv-functions @ git+https://github.com/arXiv/arxiv-base.git@0987739e90c582aa69578c7ffc60e9b15b02410d#subdirectory=arxiv-functions + # via -r requirements.in +blinker==1.9.0 + # via flask +certifi==2026.7.22 + # via requests +cffi==2.1.1 + # via cryptography +charset-normalizer==3.5.0 + # via requests +click==8.4.2 + # via + # flask + # functions-framework + # uvicorn +cloudevents==1.12.0 + # via + # arxiv-functions + # functions-framework +coverage==7.15.4 + # via pytest-cov +cryptography==50.0.0 + # via google-auth +deprecation==2.1.0 + # via cloudevents +flask==3.1.3 + # via functions-framework +functions-framework==3.10.2 + # via -r requirements.in +google-api-core==2.34.0 + # via + # google-cloud-appengine-logging + # google-cloud-core + # google-cloud-logging +google-auth==2.56.3 + # via + # google-api-core + # google-cloud-appengine-logging + # google-cloud-core + # google-cloud-logging +google-cloud-appengine-logging==1.10.0 + # via google-cloud-logging +google-cloud-audit-log==0.6.1 + # via google-cloud-logging +google-cloud-core==2.6.1 + # via google-cloud-logging +google-cloud-logging==3.16.2 + # via + # -r requirements.in + # arxiv-functions +googleapis-common-protos==1.75.1 + # via + # google-api-core + # google-cloud-audit-log + # grpc-google-iam-v1 + # grpcio-status +grpc-google-iam-v1==0.14.5 + # via google-cloud-logging +grpcio==1.83.0 + # via + # google-api-core + # google-cloud-appengine-logging + # google-cloud-logging + # googleapis-common-protos + # grpc-google-iam-v1 + # grpcio-status +grpcio-status==1.83.0 + # via google-api-core +gunicorn==26.0.0 + # via + # functions-framework + # uvicorn-worker +h11==0.16.0 + # via uvicorn +idna==3.18 + # via + # anyio + # requests +iniconfig==2.3.0 + # via pytest +itsdangerous==2.2.0 + # via flask +jinja2==3.1.6 + # via flask +markupsafe==3.0.3 + # via + # flask + # jinja2 + # werkzeug +opentelemetry-api==1.44.0 + # via google-cloud-logging +packaging==26.3 + # via + # deprecation + # gunicorn + # pytest +pluggy==1.6.0 + # via + # pytest + # pytest-cov +proto-plus==1.28.3 + # via + # google-api-core + # google-cloud-appengine-logging + # google-cloud-logging +protobuf==7.35.1 + # via + # google-api-core + # google-cloud-appengine-logging + # google-cloud-audit-log + # google-cloud-logging + # googleapis-common-protos + # grpc-google-iam-v1 + # grpcio-status + # proto-plus +pyasn1==0.6.4 + # via pyasn1-modules +pyasn1-modules==0.4.2 + # via google-auth +pycparser==3.0 + # via cffi +pydantic==2.13.4 + # via + # -r requirements.in + # arxiv-functions + # pydantic-settings +pydantic-core==2.46.4 + # via pydantic +pydantic-settings==2.15.0 + # via arxiv-functions +pygments==2.20.0 + # via pytest +pymysql==1.2.0 + # via -r requirements.in +pytest==9.1.1 + # via + # -r requirements.in + # pytest-cov +pytest-cov==7.1.0 + # via -r requirements.in +python-dateutil==2.9.0.post0 + # via arxiv-functions +python-dotenv==1.2.2 + # via pydantic-settings +requests==2.34.2 + # via google-api-core +ruff==0.16.2 + # via -r requirements.in +six==1.17.0 + # via python-dateutil +sqlalchemy==2.0.52 + # via + # -r requirements.in + # arxiv-functions + # stats-entities +starlette==1.6.0 + # via functions-framework +stats-entities @ git+https://github.com/arXiv/stats.git@4e93c3c6a952acbb392e06e5c6b3d0727b95bb1b#subdirectory=stats-entities + # via -r requirements.in +typing-extensions==4.16.0 + # via + # grpcio + # opentelemetry-api + # pydantic + # pydantic-core + # sqlalchemy + # typing-inspection +typing-inspection==0.4.4 + # via + # pydantic + # pydantic-settings +urllib3==2.7.0 + # via requests +uvicorn==0.52.1 + # via + # functions-framework + # uvicorn-worker +uvicorn-worker==0.4.0 + # via functions-framework +watchdog==6.0.0 + # via functions-framework +werkzeug==3.1.8 + # via + # flask + # functions-framework diff --git a/stats-functions/monthly_submissions/tests/test_monthly_submissions.py b/stats-functions/monthly_submissions/tests/test_monthly_submissions.py index cf380610..7ade3b24 100644 --- a/stats-functions/monthly_submissions/tests/test_monthly_submissions.py +++ b/stats-functions/monthly_submissions/tests/test_monthly_submissions.py @@ -23,7 +23,7 @@ ) from entities import ReadBase, Document from stats_entities.site_usage import SiteUsageBase, MonthlySubmissions -from stats_functions.exception import NoRetryError +from arxiv_functions.exception import NoRetryError @pytest.fixture diff --git a/stats-functions/pyproject.toml b/stats-functions/pyproject.toml deleted file mode 100644 index de886305..00000000 --- a/stats-functions/pyproject.toml +++ /dev/null @@ -1,35 +0,0 @@ -[project] -name = "stats-functions" -version = "0.1.0" -description = "Shared code for stats cloud functions" -readme = "README.md" -requires-python = "~=3.13.0" -dependencies = [ - "cloudevents==1.12.0", - "google-cloud-logging>=3.13.0", - "pydantic>=2.12.5", - "pydantic-settings>=2.12.0", - "python-dateutil>=2.9.0.post0", - "sqlalchemy>=2.0.46", -] - -[dependency-groups] -dev = [ - "pytest>=9.0.3,<10", - "ruff>=0.13.0,<0.14", - "ty>=0.0.4", -] - -[tool.uv] -default-groups = ["dev"] - -[tool.hatch.build.targets.wheel] -packages = ["stats_functions"] - -[tool.hatch.build.targets.sdist] -include = ["/stats_functions", "/tests", "pyproject.toml", "README.md"] -exclude = ["/aggregate_hourly_downloads", "/hourly_edge_requests", "/monthly_submissions", "/monthly_downloads"] - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" diff --git a/stats-functions/stats_functions/__init__.py b/stats-functions/stats_functions/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/stats-functions/stats_functions/config.py b/stats-functions/stats_functions/config.py deleted file mode 100644 index a513cd0b..00000000 --- a/stats-functions/stats_functions/config.py +++ /dev/null @@ -1,31 +0,0 @@ -from typing import Optional -from pydantic_settings import BaseSettings, SettingsConfigDict - - -class BaseConfig(BaseSettings): - model_config = SettingsConfigDict( - env_file=".env", - env_nested_delimiter="__", - env_file_encoding="utf-8", - extra="allow", - ) - - -class Query(BaseConfig): - unix_socket: str # full path - - -class DatabaseConfig(BaseConfig): - drivername: str - username: str - password: str - host: Optional[str] = None - port: Optional[int] = None - database: str - query: Query - - -class FunctionConfig(BaseConfig): - env: str - log_locally: bool = False - max_event_age_in_minutes: int = 50 diff --git a/stats-functions/stats_functions/exception.py b/stats-functions/stats_functions/exception.py deleted file mode 100644 index 9d16b995..00000000 --- a/stats-functions/stats_functions/exception.py +++ /dev/null @@ -1,2 +0,0 @@ -class NoRetryError(Exception): - pass \ No newline at end of file diff --git a/stats-functions/stats_functions/utils.py b/stats-functions/stats_functions/utils.py deleted file mode 100644 index 9728dcf1..00000000 --- a/stats-functions/stats_functions/utils.py +++ /dev/null @@ -1,86 +0,0 @@ -from google.cloud.logging import Client -from cloudevents.http import CloudEvent - -from sqlalchemy import create_engine, Engine, URL - -from datetime import datetime, timedelta, timezone -from dateutil import parser - -from stats_functions.config import FunctionConfig, DatabaseConfig - - -def set_up_cloud_logging(config: FunctionConfig): - """ - Attach a cloud logging handler to the standard logging module - The cloud logging client is project-aware by default - - Example use: - - logger = logging.getLogger(__name__) - set_up_cloud_logging(config) - """ - if config.env != "TEST" and not config.log_locally: - cloud_logging_client = Client() - cloud_logging_client.setup_logging() - - -def get_engine_unix_socket(db: DatabaseConfig) -> Engine: - """ - Initializes a unix socket connection pool for a Cloud SQL instance of MySQL - Must be lazily loaded to allow time for the Cloud Run instance to be configured with a connection to that Cloud SQL instance - Automatically refreshes connections before they reach the 10 minute idle timeout for instance connections to Cloud SQL - - Example use: - - engine = None - SessionFactory = None - - @functions_framework.cloud_event - def function(cloud_event: CloudEvent): - global engine, SessionFactory - - if config.env != "TEST": - if SessionFactory is None: - logger.info("Initializing engine and sessionmaker") - engine = get_engine_unix_socket(config.db) - SessionFactory = sessionmaker(bind=engine) - - """ - return create_engine( - URL.create( - drivername=db.drivername, - username=db.username, - password=db.password, - database=db.database, - query={"unix_socket": db.query.unix_socket}, - ), - pool_recycle=300, # recycle connections older than 5 minutes - pool_pre_ping=True, # check if a connection is alive before handing it to a session - ) - - -def event_time_exceeds_retry_window( - config: FunctionConfig, event_time: datetime -) -> bool: - """ - Helper to prevent infinite retries by dismissing event timestamps that are too old - - Example use: - - if event_time_exceeds_retry_window(config, event_time): - raise NoRetryError - """ - current_time = datetime.now(timezone.utc) - max_event_age = timedelta(minutes=config.max_event_age_in_minutes) - - if (current_time - event_time) > max_event_age: - return True - else: - return False - - -def parse_cloud_event_time(cloud_event: CloudEvent) -> datetime: - """ - Parse the event time from a cloud event and return it as a timezone-aware datetime object - """ - return parser.isoparse(cloud_event["time"]).replace(tzinfo=timezone.utc) diff --git a/stats-functions/tests/test_utils.py b/stats-functions/tests/test_utils.py deleted file mode 100644 index ab27cdeb..00000000 --- a/stats-functions/tests/test_utils.py +++ /dev/null @@ -1,73 +0,0 @@ -import pytest -from unittest.mock import patch -from datetime import datetime, timedelta, timezone - -from cloudevents.http import CloudEvent - -from stats_functions.config import FunctionConfig, DatabaseConfig, Query - -from stats_functions.utils import ( - set_up_cloud_logging, - event_time_exceeds_retry_window, - parse_cloud_event_time, -) - - -@pytest.fixture -def mock_config(): - return FunctionConfig( - env="DEV", - max_event_age_in_minutes=50, - db=DatabaseConfig( - drivername="dialect+driver", - username="mock-user", - password="mock-password", - database="mock-db", - query=Query(unix_socket="/cloudsql/mock:instance:name"), - ), - ) - - -def test_set_up_cloud_logging_remote(mock_config): - with patch("stats_functions.utils.Client") as MockCloudLoggingClient: - mock_cloud_logging_client = MockCloudLoggingClient.return_value - set_up_cloud_logging(mock_config) - - mock_cloud_logging_client.setup_logging.assert_called_once() - - -def test_set_up_cloud_logging_local(mock_config): - mock_config.log_locally = True - - with patch("stats_functions.utils.Client") as MockCloudLoggingClient: - set_up_cloud_logging(mock_config) - MockCloudLoggingClient.assert_not_called() - - -def test_event_time_exceeds_retry_window_true(mock_config): - mock_event_time = datetime.now(timezone.utc) - timedelta(minutes=51) - - assert event_time_exceeds_retry_window(mock_config, mock_event_time) is True - - -def test_event_time_exceeds_retry_window_false(mock_config): - mock_event_time = datetime.now(timezone.utc) - timedelta(minutes=5) - - assert event_time_exceeds_retry_window(mock_config, mock_event_time) is False - - -def test_parse_cloud_event_time(): - mock_attributes = { - "type": "mock_type", - "source": "mock_source", - "time": "2023-10-27T12:00:00Z", - } - - mock_cloud_event = CloudEvent(attributes=mock_attributes, data={}) - - result = parse_cloud_event_time(mock_cloud_event) - - assert result.year == 2023 - assert result.month == 10 - assert result.day == 27 - assert result.tzinfo == timezone.utc diff --git a/stats-functions/uv.lock b/stats-functions/uv.lock deleted file mode 100644 index 94f22040..00000000 --- a/stats-functions/uv.lock +++ /dev/null @@ -1,725 +0,0 @@ -version = 1 -revision = 3 -requires-python = "==3.13.*" - -[[package]] -name = "annotated-types" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, -] - -[[package]] -name = "certifi" -version = "2026.1.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, -] - -[[package]] -name = "cffi" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, - { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, - { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, - { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, - { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, - { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, - { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, - { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, - { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, - { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, - { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, - { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, - { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, - { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, - { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, - { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, - { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, - { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, -] - -[[package]] -name = "cloudevents" -version = "1.12.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "deprecation" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7a/aa/804bdb5f2f021fcc887eeabfa24bad0ffd4b150f60850ae88faa51d393a5/cloudevents-1.12.0.tar.gz", hash = "sha256:ebd5544ceb58c8378a0787b657a2ae895e929b80a82d6675cba63f0e8c5539e0", size = 34494, upload-time = "2025-06-02T18:58:45.104Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/b6/4e29b74bb40daa7580310a5ff0df5f121a08ce98340e01a960b668468aab/cloudevents-1.12.0-py3-none-any.whl", hash = "sha256:49196267f5f963d87ae156f93fc0fa32f4af69485f2c8e62e0db8b0b4b8b8921", size = 55762, upload-time = "2025-06-02T18:58:44.013Z" }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, -] - -[[package]] -name = "cryptography" -version = "46.0.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, - { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, - { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, - { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, - { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, - { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, - { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, - { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, - { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, - { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, - { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, - { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, - { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, - { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, - { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, - { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, - { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, - { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, - { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, - { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, - { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, - { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, - { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, - { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, - { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, - { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, - { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, - { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, -] - -[[package]] -name = "deprecation" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5a/d3/8ae2869247df154b64c1884d7346d412fed0c49df84db635aab2d1c40e62/deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff", size = 173788, upload-time = "2020-04-20T14:23:38.738Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178, upload-time = "2020-04-20T14:23:36.581Z" }, -] - -[[package]] -name = "google-api-core" -version = "2.29.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-auth" }, - { name = "googleapis-common-protos" }, - { name = "proto-plus" }, - { name = "protobuf" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0d/10/05572d33273292bac49c2d1785925f7bc3ff2fe50e3044cf1062c1dde32e/google_api_core-2.29.0.tar.gz", hash = "sha256:84181be0f8e6b04006df75ddfe728f24489f0af57c96a529ff7cf45bc28797f7", size = 177828, upload-time = "2026-01-08T22:21:39.269Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/b6/85c4d21067220b9a78cfb81f516f9725ea6befc1544ec9bd2c1acd97c324/google_api_core-2.29.0-py3-none-any.whl", hash = "sha256:d30bc60980daa36e314b5d5a3e5958b0200cb44ca8fa1be2b614e932b75a3ea9", size = 173906, upload-time = "2026-01-08T22:21:36.093Z" }, -] - -[package.optional-dependencies] -grpc = [ - { name = "grpcio" }, - { name = "grpcio-status" }, -] - -[[package]] -name = "google-auth" -version = "2.48.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "pyasn1-modules" }, - { name = "rsa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0c/41/242044323fbd746615884b1c16639749e73665b718209946ebad7ba8a813/google_auth-2.48.0.tar.gz", hash = "sha256:4f7e706b0cd3208a3d940a19a822c37a476ddba5450156c3e6624a71f7c841ce", size = 326522, upload-time = "2026-01-26T19:22:47.157Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl", hash = "sha256:2e2a537873d449434252a9632c28bfc268b0adb1e53f9fb62afc5333a975903f", size = 236499, upload-time = "2026-01-26T19:22:45.099Z" }, -] - -[[package]] -name = "google-cloud-appengine-logging" -version = "1.8.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-api-core", extra = ["grpc"] }, - { name = "google-auth" }, - { name = "grpcio" }, - { name = "proto-plus" }, - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/65/38/89317773c64b5a7e9b56b9aecb2e39ac02d8d6d09fb5b276710c6892e690/google_cloud_appengine_logging-1.8.0.tar.gz", hash = "sha256:84b705a69e4109fc2f68dfe36ce3df6a34d5c3d989eee6d0ac1b024dda0ba6f5", size = 18071, upload-time = "2026-01-15T13:14:40.024Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/66/4a9be8afb1d0bf49472478cec20fefe4f4cb3a6e67be2231f097041e7339/google_cloud_appengine_logging-1.8.0-py3-none-any.whl", hash = "sha256:a4ce9ce94a9fd8c89ed07fa0b06fcf9ea3642f9532a1be1a8c7b5f82c0a70ec6", size = 18380, upload-time = "2026-01-09T14:52:58.154Z" }, -] - -[[package]] -name = "google-cloud-audit-log" -version = "0.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "googleapis-common-protos" }, - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c7/d2/ad96950410f8a05e921a6da2e1a6ba4aeca674bbb5dda8200c3c7296d7ad/google_cloud_audit_log-0.4.0.tar.gz", hash = "sha256:8467d4dcca9f3e6160520c24d71592e49e874838f174762272ec10e7950b6feb", size = 44682, upload-time = "2025-10-17T02:33:44.641Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/25/532886995f11102ad6de290496de5db227bd3a73827702445928ad32edcb/google_cloud_audit_log-0.4.0-py3-none-any.whl", hash = "sha256:6b88e2349df45f8f4cc0993b687109b1388da1571c502dc1417efa4b66ec55e0", size = 44890, upload-time = "2025-10-17T02:30:55.11Z" }, -] - -[[package]] -name = "google-cloud-core" -version = "2.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-api-core" }, - { name = "google-auth" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a6/03/ef0bc99d0e0faf4fdbe67ac445e18cdaa74824fd93cd069e7bb6548cb52d/google_cloud_core-2.5.0.tar.gz", hash = "sha256:7c1b7ef5c92311717bd05301aa1a91ffbc565673d3b0b4163a52d8413a186963", size = 36027, upload-time = "2025-10-29T23:17:39.513Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/20/bfa472e327c8edee00f04beecc80baeddd2ab33ee0e86fd7654da49d45e9/google_cloud_core-2.5.0-py3-none-any.whl", hash = "sha256:67d977b41ae6c7211ee830c7912e41003ea8194bff15ae7d72fd6f51e57acabc", size = 29469, upload-time = "2025-10-29T23:17:38.548Z" }, -] - -[[package]] -name = "google-cloud-logging" -version = "3.13.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-api-core", extra = ["grpc"] }, - { name = "google-auth" }, - { name = "google-cloud-appengine-logging" }, - { name = "google-cloud-audit-log" }, - { name = "google-cloud-core" }, - { name = "grpc-google-iam-v1" }, - { name = "opentelemetry-api" }, - { name = "proto-plus" }, - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7f/47/31ef0261802fe8b37c221392e1d6ff01d30b03dce5e20e77fc7d57ddf8a3/google_cloud_logging-3.13.0.tar.gz", hash = "sha256:3aae0573b1a1a4f59ecdf4571f4e7881b5823bd129fe469561c1c49a7fa8a4c1", size = 290169, upload-time = "2025-12-16T14:11:07.345Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/5a/778dca2e375171af4085554cb3bc643627717a7e4e1539842ced3afd6ec4/google_cloud_logging-3.13.0-py3-none-any.whl", hash = "sha256:f215e1c76ee29239c6cacf02443dffa985663c74bf47c9818854694805c6019f", size = 230518, upload-time = "2025-12-16T14:11:05.894Z" }, -] - -[[package]] -name = "googleapis-common-protos" -version = "1.72.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, -] - -[package.optional-dependencies] -grpc = [ - { name = "grpcio" }, -] - -[[package]] -name = "greenlet" -version = "3.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/99/1cd3411c56a410994669062bd73dd58270c00cc074cac15f385a1fd91f8a/greenlet-3.3.1.tar.gz", hash = "sha256:41848f3230b58c08bb43dee542e74a2a2e34d3c59dc3076cec9151aeeedcae98", size = 184690, upload-time = "2026-01-23T15:31:02.076Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/ab/d26750f2b7242c2b90ea2ad71de70cfcd73a948a49513188a0fc0d6fc15a/greenlet-3.3.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:7ab327905cabb0622adca5971e488064e35115430cec2c35a50fd36e72a315b3", size = 275205, upload-time = "2026-01-23T15:30:24.556Z" }, - { url = "https://files.pythonhosted.org/packages/10/d3/be7d19e8fad7c5a78eeefb2d896a08cd4643e1e90c605c4be3b46264998f/greenlet-3.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65be2f026ca6a176f88fb935ee23c18333ccea97048076aef4db1ef5bc0713ac", size = 599284, upload-time = "2026-01-23T16:00:58.584Z" }, - { url = "https://files.pythonhosted.org/packages/ae/21/fe703aaa056fdb0f17e5afd4b5c80195bbdab701208918938bd15b00d39b/greenlet-3.3.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a3ae05b3d225b4155bda56b072ceb09d05e974bc74be6c3fc15463cf69f33fd", size = 610274, upload-time = "2026-01-23T16:05:29.312Z" }, - { url = "https://files.pythonhosted.org/packages/cb/86/5c6ab23bb3c28c21ed6bebad006515cfe08b04613eb105ca0041fecca852/greenlet-3.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6423481193bbbe871313de5fd06a082f2649e7ce6e08015d2a76c1e9186ca5b3", size = 612904, upload-time = "2026-01-23T15:32:52.317Z" }, - { url = "https://files.pythonhosted.org/packages/c2/f3/7949994264e22639e40718c2daf6f6df5169bf48fb038c008a489ec53a50/greenlet-3.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:33a956fe78bbbda82bfc95e128d61129b32d66bcf0a20a1f0c08aa4839ffa951", size = 1567316, upload-time = "2026-01-23T16:04:23.316Z" }, - { url = "https://files.pythonhosted.org/packages/8d/6e/d73c94d13b6465e9f7cd6231c68abde838bb22408596c05d9059830b7872/greenlet-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b065d3284be43728dd280f6f9a13990b56470b81be20375a207cdc814a983f2", size = 1636549, upload-time = "2026-01-23T15:33:48.643Z" }, - { url = "https://files.pythonhosted.org/packages/5e/b3/c9c23a6478b3bcc91f979ce4ca50879e4d0b2bd7b9a53d8ecded719b92e2/greenlet-3.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:27289986f4e5b0edec7b5a91063c109f0276abb09a7e9bdab08437525977c946", size = 227042, upload-time = "2026-01-23T15:33:58.216Z" }, - { url = "https://files.pythonhosted.org/packages/90/e7/824beda656097edee36ab15809fd063447b200cc03a7f6a24c34d520bc88/greenlet-3.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:2f080e028001c5273e0b42690eaf359aeef9cb1389da0f171ea51a5dc3c7608d", size = 226294, upload-time = "2026-01-23T15:30:52.73Z" }, -] - -[[package]] -name = "grpc-google-iam-v1" -version = "0.14.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "googleapis-common-protos", extra = ["grpc"] }, - { name = "grpcio" }, - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/76/1e/1011451679a983f2f5c6771a1682542ecb027776762ad031fd0d7129164b/grpc_google_iam_v1-0.14.3.tar.gz", hash = "sha256:879ac4ef33136c5491a6300e27575a9ec760f6cdf9a2518798c1b8977a5dc389", size = 23745, upload-time = "2025-10-15T21:14:53.318Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/bd/330a1bbdb1afe0b96311249e699b6dc9cfc17916394fd4503ac5aca2514b/grpc_google_iam_v1-0.14.3-py3-none-any.whl", hash = "sha256:7a7f697e017a067206a3dfef44e4c634a34d3dee135fe7d7a4613fe3e59217e6", size = 32690, upload-time = "2025-10-15T21:14:51.72Z" }, -] - -[[package]] -name = "grpcio" -version = "1.76.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716, upload-time = "2025-10-21T16:21:48.475Z" }, - { url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522, upload-time = "2025-10-21T16:21:51.142Z" }, - { url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558, upload-time = "2025-10-21T16:21:54.213Z" }, - { url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990, upload-time = "2025-10-21T16:21:56.476Z" }, - { url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387, upload-time = "2025-10-21T16:21:59.051Z" }, - { url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668, upload-time = "2025-10-21T16:22:02.049Z" }, - { url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928, upload-time = "2025-10-21T16:22:04.984Z" }, - { url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983, upload-time = "2025-10-21T16:22:07.881Z" }, - { url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727, upload-time = "2025-10-21T16:22:10.032Z" }, - { url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799, upload-time = "2025-10-21T16:22:12.709Z" }, -] - -[[package]] -name = "grpcio-status" -version = "1.76.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "googleapis-common-protos" }, - { name = "grpcio" }, - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3f/46/e9f19d5be65e8423f886813a2a9d0056ba94757b0c5007aa59aed1a961fa/grpcio_status-1.76.0.tar.gz", hash = "sha256:25fcbfec74c15d1a1cb5da3fab8ee9672852dc16a5a9eeb5baf7d7a9952943cd", size = 13679, upload-time = "2025-10-21T16:28:52.545Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/cc/27ba60ad5a5f2067963e6a858743500df408eb5855e98be778eaef8c9b02/grpcio_status-1.76.0-py3-none-any.whl", hash = "sha256:380568794055a8efbbd8871162df92012e0228a5f6dffaf57f2a00c534103b18", size = 14425, upload-time = "2025-10-21T16:28:40.853Z" }, -] - -[[package]] -name = "idna" -version = "3.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } -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 = "importlib-metadata" -version = "8.7.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, -] - -[[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 = "opentelemetry-api" -version = "1.39.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "importlib-metadata" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, -] - -[[package]] -name = "packaging" -version = "26.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, -] - -[[package]] -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]] -name = "proto-plus" -version = "1.27.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3a/02/8832cde80e7380c600fbf55090b6ab7b62bd6825dbedde6d6657c15a1f8e/proto_plus-1.27.1.tar.gz", hash = "sha256:912a7460446625b792f6448bade9e55cd4e41e6ac10e27009ef71a7f317fa147", size = 56929, upload-time = "2026-02-02T17:34:49.035Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/79/ac273cbbf744691821a9cca88957257f41afe271637794975ca090b9588b/proto_plus-1.27.1-py3-none-any.whl", hash = "sha256:e4643061f3a4d0de092d62aa4ad09fa4756b2cbb89d4627f3985018216f9fefc", size = 50480, upload-time = "2026-02-02T17:34:47.339Z" }, -] - -[[package]] -name = "protobuf" -version = "6.33.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" }, - { url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" }, - { url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" }, - { url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" }, - { url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" }, - { url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" }, - { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, -] - -[[package]] -name = "pyasn1" -version = "0.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, -] - -[[package]] -name = "pyasn1-modules" -version = "0.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyasn1" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, -] - -[[package]] -name = "pycparser" -version = "3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, -] - -[[package]] -name = "pydantic" -version = "2.12.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, -] - -[[package]] -name = "pydantic-core" -version = "2.41.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, - { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, - { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, - { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, - { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, -] - -[[package]] -name = "pydantic-settings" -version = "2.12.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, -] - -[[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 = "pytest" -version = "9.0.3" -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/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, -] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, -] - -[[package]] -name = "python-dotenv" -version = "1.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, -] - -[[package]] -name = "requests" -version = "2.33.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", size = 134232, upload-time = "2026-03-25T15:10:41.586Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017, upload-time = "2026-03-25T15:10:40.382Z" }, -] - -[[package]] -name = "rsa" -version = "4.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyasn1" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, -] - -[[package]] -name = "ruff" -version = "0.13.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/8e/f9f9ca747fea8e3ac954e3690d4698c9737c23b51731d02df999c150b1c9/ruff-0.13.3.tar.gz", hash = "sha256:5b0ba0db740eefdfbcce4299f49e9eaefc643d4d007749d77d047c2bab19908e", size = 5438533, upload-time = "2025-10-02T19:29:31.582Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/33/8f7163553481466a92656d35dea9331095122bb84cf98210bef597dd2ecd/ruff-0.13.3-py3-none-linux_armv6l.whl", hash = "sha256:311860a4c5e19189c89d035638f500c1e191d283d0cc2f1600c8c80d6dcd430c", size = 12484040, upload-time = "2025-10-02T19:28:49.199Z" }, - { url = "https://files.pythonhosted.org/packages/b0/b5/4a21a4922e5dd6845e91896b0d9ef493574cbe061ef7d00a73c61db531af/ruff-0.13.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2bdad6512fb666b40fcadb65e33add2b040fc18a24997d2e47fee7d66f7fcae2", size = 13122975, upload-time = "2025-10-02T19:28:52.446Z" }, - { url = "https://files.pythonhosted.org/packages/40/90/15649af836d88c9f154e5be87e64ae7d2b1baa5a3ef317cb0c8fafcd882d/ruff-0.13.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fc6fa4637284708d6ed4e5e970d52fc3b76a557d7b4e85a53013d9d201d93286", size = 12346621, upload-time = "2025-10-02T19:28:54.712Z" }, - { url = "https://files.pythonhosted.org/packages/a5/42/bcbccb8141305f9a6d3f72549dd82d1134299177cc7eaf832599700f95a7/ruff-0.13.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c9e6469864f94a98f412f20ea143d547e4c652f45e44f369d7b74ee78185838", size = 12574408, upload-time = "2025-10-02T19:28:56.679Z" }, - { url = "https://files.pythonhosted.org/packages/ce/19/0f3681c941cdcfa2d110ce4515624c07a964dc315d3100d889fcad3bfc9e/ruff-0.13.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5bf62b705f319476c78891e0e97e965b21db468b3c999086de8ffb0d40fd2822", size = 12285330, upload-time = "2025-10-02T19:28:58.79Z" }, - { url = "https://files.pythonhosted.org/packages/10/f8/387976bf00d126b907bbd7725219257feea58650e6b055b29b224d8cb731/ruff-0.13.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78cc1abed87ce40cb07ee0667ce99dbc766c9f519eabfd948ed87295d8737c60", size = 13980815, upload-time = "2025-10-02T19:29:01.577Z" }, - { url = "https://files.pythonhosted.org/packages/0c/a6/7c8ec09d62d5a406e2b17d159e4817b63c945a8b9188a771193b7e1cc0b5/ruff-0.13.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4fb75e7c402d504f7a9a259e0442b96403fa4a7310ffe3588d11d7e170d2b1e3", size = 14987733, upload-time = "2025-10-02T19:29:04.036Z" }, - { url = "https://files.pythonhosted.org/packages/97/e5/f403a60a12258e0fd0c2195341cfa170726f254c788673495d86ab5a9a9d/ruff-0.13.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17b951f9d9afb39330b2bdd2dd144ce1c1335881c277837ac1b50bfd99985ed3", size = 14439848, upload-time = "2025-10-02T19:29:06.684Z" }, - { url = "https://files.pythonhosted.org/packages/39/49/3de381343e89364c2334c9f3268b0349dc734fc18b2d99a302d0935c8345/ruff-0.13.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6052f8088728898e0a449f0dde8fafc7ed47e4d878168b211977e3e7e854f662", size = 13421890, upload-time = "2025-10-02T19:29:08.767Z" }, - { url = "https://files.pythonhosted.org/packages/ab/b5/c0feca27d45ae74185a6bacc399f5d8920ab82df2d732a17213fb86a2c4c/ruff-0.13.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc742c50f4ba72ce2a3be362bd359aef7d0d302bf7637a6f942eaa763bd292af", size = 13444870, upload-time = "2025-10-02T19:29:11.234Z" }, - { url = "https://files.pythonhosted.org/packages/50/a1/b655298a1f3fda4fdc7340c3f671a4b260b009068fbeb3e4e151e9e3e1bf/ruff-0.13.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8e5640349493b378431637019366bbd73c927e515c9c1babfea3e932f5e68e1d", size = 13691599, upload-time = "2025-10-02T19:29:13.353Z" }, - { url = "https://files.pythonhosted.org/packages/32/b0/a8705065b2dafae007bcae21354e6e2e832e03eb077bb6c8e523c2becb92/ruff-0.13.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6b139f638a80eae7073c691a5dd8d581e0ba319540be97c343d60fb12949c8d0", size = 12421893, upload-time = "2025-10-02T19:29:15.668Z" }, - { url = "https://files.pythonhosted.org/packages/0d/1e/cbe7082588d025cddbb2f23e6dfef08b1a2ef6d6f8328584ad3015b5cebd/ruff-0.13.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6b547def0a40054825de7cfa341039ebdfa51f3d4bfa6a0772940ed351d2746c", size = 12267220, upload-time = "2025-10-02T19:29:17.583Z" }, - { url = "https://files.pythonhosted.org/packages/a5/99/4086f9c43f85e0755996d09bdcb334b6fee9b1eabdf34e7d8b877fadf964/ruff-0.13.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9cc48a3564423915c93573f1981d57d101e617839bef38504f85f3677b3a0a3e", size = 13177818, upload-time = "2025-10-02T19:29:19.943Z" }, - { url = "https://files.pythonhosted.org/packages/9b/de/7b5db7e39947d9dc1c5f9f17b838ad6e680527d45288eeb568e860467010/ruff-0.13.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1a993b17ec03719c502881cb2d5f91771e8742f2ca6de740034433a97c561989", size = 13618715, upload-time = "2025-10-02T19:29:22.527Z" }, - { url = "https://files.pythonhosted.org/packages/28/d3/bb25ee567ce2f61ac52430cf99f446b0e6d49bdfa4188699ad005fdd16aa/ruff-0.13.3-py3-none-win32.whl", hash = "sha256:f14e0d1fe6460f07814d03c6e32e815bff411505178a1f539a38f6097d3e8ee3", size = 12334488, upload-time = "2025-10-02T19:29:24.782Z" }, - { url = "https://files.pythonhosted.org/packages/cf/49/12f5955818a1139eed288753479ba9d996f6ea0b101784bb1fe6977ec128/ruff-0.13.3-py3-none-win_amd64.whl", hash = "sha256:621e2e5812b691d4f244638d693e640f188bacbb9bc793ddd46837cea0503dd2", size = 13455262, upload-time = "2025-10-02T19:29:26.882Z" }, - { url = "https://files.pythonhosted.org/packages/fe/72/7b83242b26627a00e3af70d0394d68f8f02750d642567af12983031777fc/ruff-0.13.3-py3-none-win_arm64.whl", hash = "sha256:9e9e9d699841eaf4c2c798fa783df2fabc680b72059a02ca0ed81c460bc58330", size = 12538484, upload-time = "2025-10-02T19:29:28.951Z" }, -] - -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - -[[package]] -name = "sqlalchemy" -version = "2.0.46" -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/06/aa/9ce0f3e7a9829ead5c8ce549392f33a12c4555a6c0609bb27d882e9c7ddf/sqlalchemy-2.0.46.tar.gz", hash = "sha256:cf36851ee7219c170bb0793dbc3da3e80c582e04a5437bc601bfe8c85c9216d7", size = 9865393, upload-time = "2026-01-21T18:03:45.119Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/4b/fa7838fe20bb752810feed60e45625a9a8b0102c0c09971e2d1d95362992/sqlalchemy-2.0.46-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93a12da97cca70cea10d4b4fc602589c4511f96c1f8f6c11817620c021d21d00", size = 2150268, upload-time = "2026-01-21T19:05:56.621Z" }, - { url = "https://files.pythonhosted.org/packages/46/c1/b34dccd712e8ea846edf396e00973dda82d598cb93762e55e43e6835eba9/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af865c18752d416798dae13f83f38927c52f085c52e2f32b8ab0fef46fdd02c2", size = 3276511, upload-time = "2026-01-21T18:46:49.022Z" }, - { url = "https://files.pythonhosted.org/packages/96/48/a04d9c94753e5d5d096c628c82a98c4793b9c08ca0e7155c3eb7d7db9f24/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d679b5f318423eacb61f933a9a0f75535bfca7056daeadbf6bd5bcee6183aee", size = 3292881, upload-time = "2026-01-21T18:40:13.089Z" }, - { url = "https://files.pythonhosted.org/packages/be/f4/06eda6e91476f90a7d8058f74311cb65a2fb68d988171aced81707189131/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64901e08c33462acc9ec3bad27fc7a5c2b6491665f2aa57564e57a4f5d7c52ad", size = 3224559, upload-time = "2026-01-21T18:46:50.974Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a2/d2af04095412ca6345ac22b33b89fe8d6f32a481e613ffcb2377d931d8d0/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8ac45e8f4eaac0f9f8043ea0e224158855c6a4329fd4ee37c45c61e3beb518e", size = 3262728, upload-time = "2026-01-21T18:40:14.883Z" }, - { url = "https://files.pythonhosted.org/packages/31/48/1980c7caa5978a3b8225b4d230e69a2a6538a3562b8b31cea679b6933c83/sqlalchemy-2.0.46-cp313-cp313-win32.whl", hash = "sha256:8d3b44b3d0ab2f1319d71d9863d76eeb46766f8cf9e921ac293511804d39813f", size = 2111295, upload-time = "2026-01-21T18:42:52.366Z" }, - { url = "https://files.pythonhosted.org/packages/2d/54/f8d65bbde3d877617c4720f3c9f60e99bb7266df0d5d78b6e25e7c149f35/sqlalchemy-2.0.46-cp313-cp313-win_amd64.whl", hash = "sha256:77f8071d8fbcbb2dd11b7fd40dedd04e8ebe2eb80497916efedba844298065ef", size = 2137076, upload-time = "2026-01-21T18:42:53.924Z" }, - { url = "https://files.pythonhosted.org/packages/56/ba/9be4f97c7eb2b9d5544f2624adfc2853e796ed51d2bb8aec90bc94b7137e/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1e8cc6cc01da346dc92d9509a63033b9b1bda4fed7a7a7807ed385c7dccdc10", size = 3556533, upload-time = "2026-01-21T18:33:06.636Z" }, - { url = "https://files.pythonhosted.org/packages/20/a6/b1fc6634564dbb4415b7ed6419cdfeaadefd2c39cdab1e3aa07a5f2474c2/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96c7cca1a4babaaf3bfff3e4e606e38578856917e52f0384635a95b226c87764", size = 3523208, upload-time = "2026-01-21T18:45:08.436Z" }, - { url = "https://files.pythonhosted.org/packages/a1/d8/41e0bdfc0f930ff236f86fccd12962d8fa03713f17ed57332d38af6a3782/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2a9f9aee38039cf4755891a1e50e1effcc42ea6ba053743f452c372c3152b1b", size = 3464292, upload-time = "2026-01-21T18:33:08.208Z" }, - { url = "https://files.pythonhosted.org/packages/f0/8b/9dcbec62d95bea85f5ecad9b8d65b78cc30fb0ffceeb3597961f3712549b/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db23b1bf8cfe1f7fda19018e7207b20cdb5168f83c437ff7e95d19e39289c447", size = 3473497, upload-time = "2026-01-21T18:45:10.552Z" }, - { url = "https://files.pythonhosted.org/packages/fc/a1/9c4efa03300926601c19c18582531b45aededfb961ab3c3585f1e24f120b/sqlalchemy-2.0.46-py3-none-any.whl", hash = "sha256:f9c11766e7e7c0a2767dda5acb006a118640c9fc0a4104214b96269bfb78399e", size = 1937882, upload-time = "2026-01-21T18:22:10.456Z" }, -] - -[[package]] -name = "stats-functions" -version = "0.1.0" -source = { editable = "." } -dependencies = [ - { name = "cloudevents" }, - { name = "google-cloud-logging" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "python-dateutil" }, - { name = "sqlalchemy" }, -] - -[package.dev-dependencies] -dev = [ - { name = "pytest" }, - { name = "ruff" }, - { name = "ty" }, -] - -[package.metadata] -requires-dist = [ - { name = "cloudevents", specifier = "==1.12.0" }, - { name = "google-cloud-logging", specifier = ">=3.13.0" }, - { name = "pydantic", specifier = ">=2.12.5" }, - { name = "pydantic-settings", specifier = ">=2.12.0" }, - { name = "python-dateutil", specifier = ">=2.9.0.post0" }, - { name = "sqlalchemy", specifier = ">=2.0.46" }, -] - -[package.metadata.requires-dev] -dev = [ - { name = "pytest", specifier = ">=9.0.3,<10" }, - { name = "ruff", specifier = ">=0.13.0,<0.14" }, - { name = "ty", specifier = ">=0.0.4" }, -] - -[[package]] -name = "ty" -version = "0.0.14" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/57/22c3d6bf95c2229120c49ffc2f0da8d9e8823755a1c3194da56e51f1cc31/ty-0.0.14.tar.gz", hash = "sha256:a691010565f59dd7f15cf324cdcd1d9065e010c77a04f887e1ea070ba34a7de2", size = 5036573, upload-time = "2026-01-27T00:57:31.427Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/99/cb/cc6d1d8de59beb17a41f9a614585f884ec2d95450306c173b3b7cc090d2e/ty-0.0.14-py3-none-linux_armv6l.whl", hash = "sha256:32cf2a7596e693094621d3ae568d7ee16707dce28c34d1762947874060fdddaa", size = 10034228, upload-time = "2026-01-27T00:57:53.133Z" }, - { url = "https://files.pythonhosted.org/packages/f3/96/dd42816a2075a8f31542296ae687483a8d047f86a6538dfba573223eaf9a/ty-0.0.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f971bf9805f49ce8c0968ad53e29624d80b970b9eb597b7cbaba25d8a18ce9a2", size = 9939162, upload-time = "2026-01-27T00:57:43.857Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b4/73c4859004e0f0a9eead9ecb67021438b2e8e5fdd8d03e7f5aca77623992/ty-0.0.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:45448b9e4806423523268bc15e9208c4f3f2ead7c344f615549d2e2354d6e924", size = 9418661, upload-time = "2026-01-27T00:58:03.411Z" }, - { url = "https://files.pythonhosted.org/packages/58/35/839c4551b94613db4afa20ee555dd4f33bfa7352d5da74c5fa416ffa0fd2/ty-0.0.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee94a9b747ff40114085206bdb3205a631ef19a4d3fb89e302a88754cbbae54c", size = 9837872, upload-time = "2026-01-27T00:57:23.718Z" }, - { url = "https://files.pythonhosted.org/packages/41/2b/bbecf7e2faa20c04bebd35fc478668953ca50ee5847ce23e08acf20ea119/ty-0.0.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6756715a3c33182e9ab8ffca2bb314d3c99b9c410b171736e145773ee0ae41c3", size = 9848819, upload-time = "2026-01-27T00:57:58.501Z" }, - { url = "https://files.pythonhosted.org/packages/be/60/3c0ba0f19c0f647ad9d2b5b5ac68c0f0b4dc899001bd53b3a7537fb247a2/ty-0.0.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89d0038a2f698ba8b6fec5cf216a4e44e2f95e4a5095a8c0f57fe549f87087c2", size = 10324371, upload-time = "2026-01-27T00:57:29.291Z" }, - { url = "https://files.pythonhosted.org/packages/24/32/99d0a0b37d0397b0a989ffc2682493286aa3bc252b24004a6714368c2c3d/ty-0.0.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c64a83a2d669b77f50a4957039ca1450626fb474619f18f6f8a3eb885bf7544", size = 10865898, upload-time = "2026-01-27T00:57:33.542Z" }, - { url = "https://files.pythonhosted.org/packages/1a/88/30b583a9e0311bb474269cfa91db53350557ebec09002bfc3fb3fc364e8c/ty-0.0.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:242488bfb547ef080199f6fd81369ab9cb638a778bb161511d091ffd49c12129", size = 10555777, upload-time = "2026-01-27T00:58:05.853Z" }, - { url = "https://files.pythonhosted.org/packages/cd/a2/cb53fb6325dcf3d40f2b1d0457a25d55bfbae633c8e337bde8ec01a190eb/ty-0.0.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4790c3866f6c83a4f424fc7d09ebdb225c1f1131647ba8bdc6fcdc28f09ed0ff", size = 10412913, upload-time = "2026-01-27T00:57:38.834Z" }, - { url = "https://files.pythonhosted.org/packages/42/8f/f2f5202d725ed1e6a4e5ffaa32b190a1fe70c0b1a2503d38515da4130b4c/ty-0.0.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:950f320437f96d4ea9a2332bbfb5b68f1c1acd269ebfa4c09b6970cc1565bd9d", size = 9837608, upload-time = "2026-01-27T00:57:55.898Z" }, - { url = "https://files.pythonhosted.org/packages/f7/ba/59a2a0521640c489dafa2c546ae1f8465f92956fede18660653cce73b4c5/ty-0.0.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4a0ec3ee70d83887f86925bbc1c56f4628bd58a0f47f6f32ddfe04e1f05466df", size = 9884324, upload-time = "2026-01-27T00:57:46.786Z" }, - { url = "https://files.pythonhosted.org/packages/03/95/8d2a49880f47b638743212f011088552ecc454dd7a665ddcbdabea25772a/ty-0.0.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a1a4e6b6da0c58b34415955279eff754d6206b35af56a18bb70eb519d8d139ef", size = 10033537, upload-time = "2026-01-27T00:58:01.149Z" }, - { url = "https://files.pythonhosted.org/packages/e9/40/4523b36f2ce69f92ccf783855a9e0ebbbd0f0bb5cdce6211ee1737159ed3/ty-0.0.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dc04384e874c5de4c5d743369c277c8aa73d1edea3c7fc646b2064b637db4db3", size = 10495910, upload-time = "2026-01-27T00:57:26.691Z" }, - { url = "https://files.pythonhosted.org/packages/08/d5/655beb51224d1bfd4f9ddc0bb209659bfe71ff141bcf05c418ab670698f0/ty-0.0.14-py3-none-win32.whl", hash = "sha256:b20e22cf54c66b3e37e87377635da412d9a552c9bf4ad9fc449fed8b2e19dad2", size = 9507626, upload-time = "2026-01-27T00:57:41.43Z" }, - { url = "https://files.pythonhosted.org/packages/b6/d9/c569c9961760e20e0a4bc008eeb1415754564304fd53997a371b7cf3f864/ty-0.0.14-py3-none-win_amd64.whl", hash = "sha256:e312ff9475522d1a33186657fe74d1ec98e4a13e016d66f5758a452c90ff6409", size = 10437980, upload-time = "2026-01-27T00:57:36.422Z" }, - { url = "https://files.pythonhosted.org/packages/ad/0c/186829654f5bfd9a028f6648e9caeb11271960a61de97484627d24443f91/ty-0.0.14-py3-none-win_arm64.whl", hash = "sha256:b6facdbe9b740cb2c15293a1d178e22ffc600653646452632541d01c36d5e378", size = 9885831, upload-time = "2026-01-27T00:57:49.747Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, -] - -[[package]] -name = "typing-inspection" -version = "0.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, -] - -[[package]] -name = "urllib3" -version = "2.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, -] - -[[package]] -name = "zipp" -version = "3.23.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, -]