diff --git a/docker/affiliation-cache/Dockerfile b/docker/affiliation-cache/Dockerfile new file mode 100644 index 00000000..96422743 --- /dev/null +++ b/docker/affiliation-cache/Dockerfile @@ -0,0 +1,22 @@ +FROM python:3.11-slim + +ENV PYTHONUNBUFFERED=1 \ + PYTHONPATH=/opt/spider + +WORKDIR /opt/spider + +RUN apt-get update && \ + apt-get install -y --no-install-recommends ca-certificates && \ + rm -rf /var/lib/apt/lists/* && \ + useradd -m -s /bin/bash cmsjobmon && \ + pip install --no-cache-dir --upgrade pip + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt && \ + chown -R cmsjobmon:cmsjobmon /opt/spider + +COPY affiliation_cache.py ./ + +USER cmsjobmon + +CMD ["python", "affiliation_cache.py"] diff --git a/docker/affiliation-cache/affiliation_cache.py b/docker/affiliation-cache/affiliation_cache.py new file mode 100644 index 00000000..7a96d8ca --- /dev/null +++ b/docker/affiliation-cache/affiliation_cache.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Author: Christian Ariza +# This script fetches affiliation data from CRIC API and publishes it to JetStream KV store +# (an indexed structure of username/login and affiliation institution and country from cric data) +# How to run: +# python affiliation_cache.py +# This will fetch the latest data from CRIC API and update the cache in JetStream KV store. +# This script can be setup as a daily cronjob to keep the cache up to date. +import traceback +import asyncio +import json +import logging +import os + +import requests +from nats.aio.client import Client as NATS + +AFFILIATION_API_URL = os.getenv("AFFILIATION_API_URL", "https://cms-cric.cern.ch/api/accounts/user/query/?json") +CA_CERT = os.getenv("CA_CERT", "/etc/pki/tls/certs/CERN-bundle.pem") +ROBOT_CERT = os.getenv("ROBOT_CERT", "/etc/secrets/robot/cert/robotcert.pem") +ROBOT_KEY = os.getenv("ROBOT_KEY", "/etc/secrets/robot/key/robotkey.pem") +NATS_SERVER = os.getenv("NATS_SERVER", "nats://nats.cluster.local:4222") +KV_BUCKET = os.getenv("KV_BUCKET", "spider_affiliations") +KV_KEY = os.getenv("KV_KEY", "affiliations") + + +def fetch_affiliations( + service_url=AFFILIATION_API_URL, + robot_cert=ROBOT_CERT, + robot_key=ROBOT_KEY, + ca_cert=CA_CERT, +): + """ + Fetch affiliation data from CRIC API. + Returns an inverted index of institutions by person login. e.g.: + + { + 'valya':{u'country': u'US', + u'institute': u'Cornell University'}, + 'belforte': {u'country': u'IT', + u'institute': u'Universita e INFN Trieste'} + ... + } + """ + logging.info("Fetching affiliation data from CRIC API") + cert = (robot_cert, robot_key) + response = requests.get(service_url, cert=cert, verify=ca_cert) + response.raise_for_status() + + _json = json.loads(response.text) + affiliations_dict = {} + for person in list(_json.values()): + login = None + for profile in person["profiles"]: + if "login" in profile: + login = profile["login"] + break + if login and "institute" in person: + affiliations_dict[login] = { + "institute": person["institute"], + "country": person["institute_country"], + "dn": person["dn"], + } + + logging.debug("Fetched %d affiliations from CRIC API", len(affiliations_dict)) + return affiliations_dict + + +def publish_to_kv( + affiliations_dict, + nats_server=NATS_SERVER, + kv_bucket_name=KV_BUCKET, +): + """ + Publish affiliations to JetStream KV store. + """ + nats_connection = None + try: + # Create event loop + try: + loop = asyncio.get_event_loop() + if loop.is_closed(): + raise RuntimeError("Event loop is closed") + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + # Connect to NATS + nats_connection = NATS() + nats_servers = [s.strip() for s in nats_server.split(",")] + loop.run_until_complete(nats_connection.connect(servers=nats_servers)) + jetstream = nats_connection.jetstream() + logging.debug("Connected to NATS JetStream at %s", nats_servers) + + # Get KV store + kv = loop.run_until_complete(jetstream.key_value(kv_bucket_name)) + + # Publish to KV store + value = json.dumps(affiliations_dict).encode("utf-8") + loop.run_until_complete(kv.put(KV_KEY, value)) + logging.info( + "Successfully published %d affiliations to KV store %s", + len(affiliations_dict), + kv_bucket_name, + ) + except Exception as e: + logging.error("Failed to publish affiliations to KV store: %s", str(e)) + raise + finally: + # Close NATS connection + if nats_connection is not None and not nats_connection.is_closed: + try: + loop.run_until_complete(nats_connection.close()) + except Exception as e: + logging.debug("Error closing NATS connection: %s", str(e)) + + +def fetch_and_publish( + service_url=AFFILIATION_API_URL, + robot_cert=ROBOT_CERT, + robot_key=ROBOT_KEY, + ca_cert=CA_CERT, + nats_server=NATS_SERVER, + kv_bucket_name=KV_BUCKET, +): + """ + Fetch affiliation data from CRIC API and publish to JetStream KV store. + """ + affiliations_dict = fetch_affiliations( + service_url=service_url, robot_cert=robot_cert, robot_key=robot_key, ca_cert=ca_cert + ) + + if affiliations_dict: + publish_to_kv( + affiliations_dict, nats_server=nats_server, kv_bucket_name=kv_bucket_name + ) + else: + logging.warning("No affiliations to publish") + + +def main(): + """ + Fetch affiliation data from CRIC API and update the cache in JetStream KV store. + """ + try: + fetch_and_publish() + except Exception as e: + traceback.print_exc() + raise + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/docker/affiliation-cache/requirements.txt b/docker/affiliation-cache/requirements.txt new file mode 100644 index 00000000..0e4118f9 --- /dev/null +++ b/docker/affiliation-cache/requirements.txt @@ -0,0 +1,2 @@ +requests~=2.31 +nats-py==2.6.0 diff --git a/docker/spider-query-cronjob/Dockerfile b/docker/spider-query-cronjob/Dockerfile new file mode 100644 index 00000000..f53e3e12 --- /dev/null +++ b/docker/spider-query-cronjob/Dockerfile @@ -0,0 +1,41 @@ +FROM python:3.11-slim + +ENV DEBIAN_FRONTEND=noninteractive \ + PYTHONUNBUFFERED=1 + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + libexpat1 \ + && rm -rf /var/lib/apt/lists/* + +RUN useradd -ms /bin/bash cmsjobmon && \ + mkdir -p /opt/spider/ && \ + chown -R cmsjobmon:cmsjobmon /opt/spider + +WORKDIR /opt/spider + +COPY ./requirements.txt /tmp/requirements.txt +RUN pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir -r /tmp/requirements.txt + +COPY ./src ./src +COPY ./running_jobs.py ./running_jobs.py +COPY ./job_history.py ./job_history.py +RUN chmod +x ./*.py + +RUN chown -R cmsjobmon:cmsjobmon /home/cmsjobmon /opt/spider + +# Build arguments for docker image (used for monitoring) +ARG DOCKER_TAG=unknown +ARG IMAGE_NAME=unknown + +ENV PYTHONPATH=/opt/spider/src:$PYTHONPATH \ + SPIDER_WORKDIR=/opt/spider \ + AFFILIATION_DIR_LOCATION=/opt/spider/.affiliation_dir.json \ + DOCKER_TAG=${DOCKER_TAG} \ + IMAGE_NAME=${IMAGE_NAME} \ + COLLECTORS_FILE=/opt/spider/etc/collectors.json + +USER cmsjobmon diff --git a/docker/spider-query-cronjob/collectors.json b/docker/spider-query-cronjob/collectors.json new file mode 100644 index 00000000..18bc7a30 --- /dev/null +++ b/docker/spider-query-cronjob/collectors.json @@ -0,0 +1,7 @@ +{ + "Global":["cmsgwms-collector-global.cern.ch:9620", "cmsgwms-collector-global.fnal.gov:9620","cmssrv276.fnal.gov"], + "Tier0":["cmsgwms-collector-tier0.cern.ch:9620","cmsgwms-collector-tier0.fnal.gov:9620"], + "Volunteer":["vocms0840.cern.ch"], + "ITB":["cmsgwms-collector-itb.cern.ch"], + "UCSD":["htcondor-cm-ucsd.osg.chtc.io"] +} \ No newline at end of file diff --git a/docker/spider-query-cronjob/job_history.py b/docker/spider-query-cronjob/job_history.py new file mode 100644 index 00000000..573ccba8 --- /dev/null +++ b/docker/spider-query-cronjob/job_history.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +""" +Script for processing the contents of the CMS pool. +""" + +import time +import os + +# Ensure this script has a distinct OpenTelemetry service name (for tracing) +# Must be set BEFORE any src.* imports that may load constants.py +os.environ.setdefault("OTEL_SERVICE_NAME", "spider-job-history") + +from src.utils import get_schedds_from_file, global_logger +from src.history import query_job_history +from src.otel_setup import trace_span +import src.constants as const +from opentelemetry import trace + + +@trace_span("job_history_main") +def main(): + starttime = time.time() + global_logger.info("Starting spider_cms history process.") + + # Get all the schedd ads (these are ClassAds; they can be sent directly + # to worker processes, and `htcondor.Schedd` expects this type). + schedd_ads = get_schedds_from_file(collectors_file=const.COLLECTORS_FILE) + + counts = query_job_history(schedd_ads, starttime) + trace.get_current_span().set_attribute("job.count", counts["count"]) + trace.get_current_span().set_attribute("job.published_count", counts["published_count"]) + return 0 + + +if __name__ == "__main__": + main() diff --git a/docker/spider-query-cronjob/pyproject.toml b/docker/spider-query-cronjob/pyproject.toml new file mode 100644 index 00000000..0f46f475 --- /dev/null +++ b/docker/spider-query-cronjob/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "spider-query-cronjob" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.9" +dependencies = [ + "cmsmonitoring>=0.6.13", + "htcondor==23.0.28", + "ipykernel>=6.31.0", + "opensearch-py>=3.0.0", + "opentelemetry-exporter-otlp>=1.39.0", + "requests>=2.32.5", + "ruff>=0.14.6", +] + +[tool.uv.workspace] +members = [ + "python3.9", +] diff --git a/docker/spider-query-cronjob/query_tests/output.txt b/docker/spider-query-cronjob/query_tests/output.txt new file mode 100644 index 00000000..a24291b6 --- /dev/null +++ b/docker/spider-query-cronjob/query_tests/output.txt @@ -0,0 +1 @@ +Failed to query collector cmsgwms-collector-global.fnal.gov:9620 for schedd crab3@vocms0197.cern.ch: Failed communication with collector. diff --git a/docker/spider-query-cronjob/query_tests/test.py b/docker/spider-query-cronjob/query_tests/test.py new file mode 100644 index 00000000..e5d06e92 --- /dev/null +++ b/docker/spider-query-cronjob/query_tests/test.py @@ -0,0 +1,93 @@ +""" +Test script to translate condor_history command to Python using htcondor bindings. + +Original command: +condor_history -name crab3@vocms0197.cern.ch -constraint '(JobUniverse == 5) && (CMS_Type != "DONOTMONIT") && (EnteredCurrentStatus >= '$(($(date +%s) - 3600))' || CRAB_PostJobLastUpdate >= '$(($(date +%s) - 3600))')' -pool cmsgwms-collector-global.fnal.gov:9620 > output.txt 2>&1 & +""" + +import os +import time +import classad +import htcondor + +# Configuration from the original command +SCHEDD_NAME = "crab3@vocms0197.cern.ch" +POOL = "cmsgwms-collector-global.fnal.gov:9620" + +# Set collector host (equivalent to -pool option in condor_history) +# Must set BEFORE importing or creating Collector objects +os.environ["COLLECTOR_HOST"] = POOL + +# Calculate time threshold (1 hour ago) +current_time = int(time.time()) +time_threshold = current_time - 3600 # 3600 seconds = 1 hour + +# Query collector for the specific schedd +# Since bash works, try using subprocess as a workaround if Python bindings fail +print(f"Querying collector {POOL} for schedd: {SCHEDD_NAME}") + +schedd_ad = None +try: + # Try with explicit collector first + collector = htcondor.Collector(POOL) + schedd_query = classad.ExprTree(f'Name == "{SCHEDD_NAME}"') + schedds = collector.query(htcondor.AdTypes.Schedd, schedd_query) + if schedds: + schedd_ad = schedds[0] + print(f"Found schedd via explicit collector connection") +except Exception as e: + print(f"Explicit collector connection failed: {type(e).__name__}: {e}") + print(f"Note: This is a known issue - Python bindings may have network/security restrictions") + print(f"that don't affect the command-line tools.") + print(f"\nSince bash commands work, you may need to:") + print(f" 1. Check firewall rules for Python processes") + print(f" 2. Check if Python bindings need additional security configuration") + print(f" 3. Use subprocess to call condor_status as a workaround") + exit(1) + +if not schedd_ad: + print(f"Error: Schedd {SCHEDD_NAME} not found in pool {POOL}") + exit(1) + +# Create Schedd object +schedd = htcondor.Schedd(schedd_ad) + +# Build the constraint query +# (JobUniverse == 5) && (CMS_Type != "DONOTMONIT") && +# (EnteredCurrentStatus >= time_threshold || CRAB_PostJobLastUpdate >= time_threshold) +constraint = f""" + (JobUniverse == 5) && (CMS_Type != "DONOTMONIT") + && + ( + EnteredCurrentStatus >= {time_threshold} + || CRAB_PostJobLastUpdate >= {time_threshold} + ) +""" +history_query = classad.ExprTree(constraint) + +print(f"Querying schedd: {SCHEDD_NAME}") +print(f"Pool: {POOL}") +print(f"Time threshold: {time_threshold} ({time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time_threshold))})") +print(f"Constraint: {history_query}") +print("-" * 80) + +try: + # Time the history query itself + query_start_time = time.time() + history_iter = schedd.history(history_query, [], match=-1) + query_end_time = time.time() + query_duration = query_end_time - query_start_time + + print(f"History query initiated in {query_duration:.2f} seconds") + + + # Output timing information + print(f"\nTotal jobs found: {len(history_iter)}") + print("\nTiming information:") + print(f" History query initiation: {query_duration:.2f} seconds") + if len(history_iter) > 0: + print(f" Average time per job: {query_duration/len(history_iter):.4f} seconds") + +except RuntimeError as e: + print(f"Error querying history: {e}") + exit(1) diff --git a/docker/spider-query-cronjob/requirements.txt b/docker/spider-query-cronjob/requirements.txt new file mode 100644 index 00000000..bc6cd392 --- /dev/null +++ b/docker/spider-query-cronjob/requirements.txt @@ -0,0 +1,15 @@ +# be consistent with: https://gitlab.cern.ch/ai/it-puppet-hostgroup-vocms/-/blob/master/data/fqdns/vocms0240.cern.ch.yaml#L7 +# check breaking changes before any update and ask to HTCondor-users if you see any problem +htcondor==23.0.28 + +# exact version is needed, previous versions include breaking changes; +# installs also stomp.py==7.0.0 +CMSMonitoring==0.6.12 + +# last version for Py v3.9 +requests~=2.31 + +click + +# OpenTelemetry for metrics +opentelemetry-exporter-otlp~=1.39.0 diff --git a/docker/spider-query-cronjob/running_jobs.py b/docker/spider-query-cronjob/running_jobs.py new file mode 100644 index 00000000..611889d5 --- /dev/null +++ b/docker/spider-query-cronjob/running_jobs.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +""" +Script for processing the contents of the CMS pool. +""" + +import time +import os + +# Ensure this script has a distinct OpenTelemetry service name (for tracing) +# Must be set BEFORE any src.* imports that may load constants.py +os.environ.setdefault("OTEL_SERVICE_NAME", "spider-running-jobs") + +from src.utils import get_schedds_from_file, global_logger +from src.query import query_running_jobs +from src.otel_setup import trace_span +import src.constants as const +from opentelemetry import trace + + +@trace_span("running_jobs_main") +def main(): + starttime = time.time() + global_logger.info("Starting spider_cms running jobs process.") + + # Get all the schedd ads (these are ClassAds; they can be sent directly + # to worker processes, and `htcondor.Schedd` expects this type). + schedd_ads = get_schedds_from_file(collectors_file=const.COLLECTORS_FILE) + + counts = query_running_jobs(starttime, schedd_ads) + trace.get_current_span().set_attribute("job.count", counts["count"]) + trace.get_current_span().set_attribute("job.published_count", counts["published_count"]) + + return 0 + + +if __name__ == "__main__": + main() diff --git a/docker/spider-query-cronjob/src/checkpoints.py b/docker/spider-query-cronjob/src/checkpoints.py new file mode 100644 index 00000000..0559179b --- /dev/null +++ b/docker/spider-query-cronjob/src/checkpoints.py @@ -0,0 +1,114 @@ +import time +import constants as const +from nats_client import ( + JetStreamContext, + set_checkpoint, + get_all_checkpoints, + close_nats_connection, + get_nats_connection, +) +from utils import global_logger + + +def update_checkpoint( + jetstream: JetStreamContext, + name: str, + completion_date: float, + kv_bucket_name: str = const.CHECKPOINT_KV_BUCKET, +): + """ + Update checkpoint for a schedd in NATS KeyValue store. + + Args: + jetstream: NATS JetStream context + name: Schedd name + completion_date: Completion timestamp + kv_bucket_name: KeyValue bucket name + """ + if jetstream is None: + global_logger.error( + "Cannot update checkpoint: NATS JetStream connection is None" + ) + return + + success = set_checkpoint(jetstream, name, completion_date, kv_bucket_name) + if not success: + global_logger.warning( + "Failed to update checkpoint for %s in KeyValue store. Completion date: %s", + name, + completion_date, + ) + + +def load_checkpoints(schedd_ads): + """ + Load checkpoints from NATS KeyValue store and prepare tasks for processing. + Each worker process will directly update checkpoints in the KV store. + """ + + nats_connection = None + jetstream = None + try: + # Use get_jetstream_context instead of get_nats_connection since + # KeyValue operations don't require a stream to be created + nats_connection, jetstream = get_nats_connection( + const.NATS_SERVER, const.NATS_STREAM_NAME, const.NATS_SUBJECT + ) + except Exception as e: + global_logger.warning( + "Failed to get NATS connection for checkpoints. " + "Will use default time windows. Error: %s", + str(e), + ) + + # Load checkpoints from KeyValue store + if jetstream is not None: + try: + checkpoint = get_all_checkpoints(jetstream, const.CHECKPOINT_KV_BUCKET) + global_logger.info( + "Loaded %d checkpoints from KeyValue store", len(checkpoint) + ) + except Exception as e: + global_logger.warning( + "Failed to load checkpoints from KeyValue store. " + "Empty dict will be used. Error: %s", + str(e), + ) + checkpoint = {} + finally: + if jetstream is not None: + close_nats_connection(connection=nats_connection, timeout=5) + else: + global_logger.warning( + "NATS connection not available. Using empty checkpoint dict. " + "All schedds will use default time windows." + ) + checkpoint = {} + + # Check for last completion time + last_completions = {} + for schedd_ad in schedd_ads: + name = schedd_ad["Name"] + last_completion = checkpoint.get( + name, time.time() - const.HISTORY_QUERY_MAX_N_MINUTES * 60 + ) + last_completions[name] = ( + schedd_ad, + max(last_completion, time.time() - const.RETENTION_POLICY), + ) + + return last_completions + + +def prepare_tasks(last_completions: dict): + tasks = [] + for schedd_ad, last_completion in last_completions.values(): + schedd_name = schedd_ad["Name"] + # For CRAB, only ever get a maximum of 12 h + if ( + schedd_name.startswith("crab") + and last_completion < time.time() - const.CRAB_MAX_QUERY_TIME_SPAN + ): + last_completion = time.time() - const.HISTORY_QUERY_MAX_N_MINUTES * 60 + tasks.append((schedd_ad, last_completion)) + return tasks diff --git a/docker/spider-query-cronjob/src/constants.py b/docker/spider-query-cronjob/src/constants.py new file mode 100644 index 00000000..db422a52 --- /dev/null +++ b/docker/spider-query-cronjob/src/constants.py @@ -0,0 +1,52 @@ +import os + +# IMAGE_NAME has to start with `spider-`, otherwise the Opensearch index +# template won't get the data properly. +IMAGE_NAME = os.getenv("IMAGE_NAME", "spider-query-cronjob") +DOCKER_TAG = os.getenv("DOCKER_TAG", "unknown") + +DRY_RUN = os.getenv("DRY_RUN", "false").lower() == "true" +MAX_PROCESSES = os.getenv("MAX_PROCESSES", 16) +MAX_HISTORY_PROCESSES = int(os.getenv("MAX_HISTORY_PROCESSES", 10)) + + +# TODO: Look into the alerts +EMAIL_ALERTS = os.getenv("EMAIL_ALERTS", "").split(",") # "cms-comp-monit-alerts@cern.ch" + +ROBOT_CERT = os.getenv("ROBOT_CERT", "/etc/secrets/robot/cert/robotcert.pem") +ROBOT_KEY = os.getenv("ROBOT_KEY", "/etc/secrets/robot/key/robotkey.pem") +CA_CERT = os.getenv("CA_CERT", "/etc/pki/tls/certs/CERN-bundle.pem") +AFFILIATION_URL = os.getenv("AFFILIATION_URL", "https://cms-cric.cern.ch/api/accounts/user/query/?json") +WORKDIR = os.getenv("SPIDER_WORKDIR", "/opt/spider") +AFFILIATION_DIR_LOCATION = os.getenv("AFFILIATION_DIR_LOCATION", "/opt/spider/.affiliation_dir.json") +COLLECTORS_FILE = os.getenv("COLLECTORS_FILE", "/opt/spider/etc/collectors.json") +REDUCE_DATA = os.getenv("REDUCE_DATA", "false").lower() == "true" +# NATS +NATS_SERVER = os.getenv("NATS_SERVER", "nats://nats.cluster.local:4222") +NATS_STREAM_NAME = os.getenv("NATS_STREAM_NAME", "CMS_HTCONDOR_QUEUE") +NATS_SUBJECT = os.getenv("NATS_SUBJECT", "cms.htcondor.queue.job") +CHECKPOINT_KV_BUCKET = os.getenv("CHECKPOINT_KV_BUCKET", "spider_checkpoints") +NATS_PUBLISH_TIMEOUT = int(os.getenv("NATS_PUBLISH_TIMEOUT", 10)) # seconds +NATS_BATCH_SIZE = int(os.getenv("NATS_BATCH_SIZE", 100)) + +# OpenTelemetry +OTEL_ENDPOINT = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "opentelemetry-collector.opentelemetry.svc.cluster.local:4317") +OTEL_METRIC_EXPORT_INTERVAL = os.getenv("OTEL_METRIC_EXPORT_INTERVAL", "15000") +OTEL_SERVICE_NAME = os.getenv("OTEL_SERVICE_NAME", "spider-query-cronjob") +OTEL_USERNAME = os.getenv("OPENTELEMETRY_USERNAME") +OTEL_PASSWORD = os.getenv("OPENTELEMETRY_PASSWORD") + +# Timeout for each schedd query (some schedds can be degraded and take a long time to query) +TIMEOUT_MINS = os.getenv("TIMEOUT_MINS", 12) + +# History +# If last query time in checkpoint is too old, but not from crab, results older +# than "now()-RETENTION_POLICY" will be ignored. +RETENTION_POLICY = os.getenv("RETENTION_POLICY", 3600) # 1 hour +# Even in checkpoint.json last query time is older than this, +# results older than "now()-CRAB_MAX_QUERY_TIME_SPAN" will be ignored. +CRAB_MAX_QUERY_TIME_SPAN = os.getenv("CRAB_MAX_QUERY_TIME_SPAN", 12 * 3600) # 12 hours +# Main query time, should be same with cron schedule. +QUERY_TIME_PERIOD = os.getenv("QUERY_TIME_PERIOD", 720) # 12 minutes +# Maximum number of minutes to query for history. +HISTORY_QUERY_MAX_N_MINUTES = os.getenv("HISTORY_QUERY_MAX_N_MINUTES", 60) # 1 hour diff --git a/docker/spider-query-cronjob/src/history.py b/docker/spider-query-cronjob/src/history.py new file mode 100755 index 00000000..93620229 --- /dev/null +++ b/docker/spider-query-cronjob/src/history.py @@ -0,0 +1,312 @@ +""" +Methods for processing the history in a schedd queue. +""" + +import datetime +import time +import traceback +from concurrent.futures import ProcessPoolExecutor, as_completed + +import classad +import htcondor + +from opentelemetry import trace + +from utils import send_email_alert, global_logger +from otel_setup import global_meter, trace_span +from nats_client import ( + get_nats_connection, + publish_jobs_to_nats, + close_nats_connection, +) +from checkpoints import load_checkpoints, prepare_tasks, update_checkpoint +import constants as const + +# Metrics for history job +history_jobs_queried_counter = global_meter.create_counter( + name="spider_cms.history.jobs", + description="Number of jobs queried per schedd in history", + unit="1", +) +history_jobs_not_published_counter = global_meter.create_counter( + name="spider_cms.history.jobs_not_published", + description="Number of jobs not published per schedd in history", + unit="1", +) +history_query_duration_histogram = global_meter.create_histogram( + name="spider_cms.history.query_duration", + description="Duration of history query per schedd in seconds", + unit="s", +) +total_history_query_duration_histogram = global_meter.create_histogram( + name="spider_cms.history.total_query_duration", + description="Total duration of history query for all schedds in seconds", + unit="s", +) +time_since_last_checkpoint_histogram = global_meter.create_histogram( + name="spider_cms.history.time_since_last_checkpoint", + description="Time since last checkpoint in seconds", + unit="s", +) + + +@trace_span("query_schedd_history") +def query_schedd_history(starttime: float, last_completion: float, schedd_ad: classad.ClassAd): + """ + Given a schedd, process its entire set of history since last checkpoint. + If job_queue is provided, jobs are queued for publishing by the main process. + Otherwise, jobs are published directly (backward compatibility). + + Params: + - starttime: The start time of the query in seconds since the epoch. + - last_completion: The last completion time of the query in seconds since the epoch. + - schedd_ad: The schedd ad to query. + - job_queue: Optional multiprocessing Queue to enqueue jobs for publishing. + + Returns: + - Tuple of (latest_completion_time, job_count) if job_queue is provided + - latest_completion_time (float) if job_queue is None (backward compatibility) + """ + schedd_start_time = time.time() + time_since_last_checkpoint_histogram.record(schedd_start_time - last_completion) + from opentelemetry import trace + current_span = trace.get_current_span() + if current_span: + schedd_name = schedd_ad["Name"] + current_span.set_attribute("schedd.name", schedd_name) + current_span.set_attribute("last_completion", last_completion) + + schedd = htcondor.Schedd(schedd_ad) + # TODO: Aren't we missing jobs because of using CRAB_PostJobLastUpdate in the constraint? + _q = """ + (JobUniverse == 5) && (CMS_Type != "DONOTMONIT") + && + ( + EnteredCurrentStatus >= %(last_completion)d + || CRAB_PostJobLastUpdate >= %(last_completion)d + ) + """ + history_query = classad.ExprTree( + _q % {"last_completion": last_completion - const.QUERY_TIME_PERIOD} + ) + global_logger.info( + "Querying %s for history: %s. %.1f minutes of ads", + schedd_ad["Name"], + history_query, + (time.time() - last_completion) / 60.0, + ) + counts = { + "count": 0, + "published_count": 0, + } + error = False + latest_completion = last_completion + jetstream = None + + history_jobs = [] + try: + if not const.DRY_RUN: + try: + nats_connection, jetstream = get_nats_connection( + const.NATS_SERVER, const.NATS_STREAM_NAME, const.NATS_SUBJECT + ) + except Exception as e: + global_logger.error("Failed to get NATS connection for schedd %s: %s", schedd_ad["Name"], str(e)) + error = True + return (latest_completion, counts) + + global_logger.info("Fetching history data for %s...", schedd_ad["Name"]) + history_iter = iter(schedd.history(history_query, [], match=-1)) + # Fetch all jobs into a list - blocking happens here, before NATS is involved + global_logger.info( + "Fetched history jobs for %s", + schedd_ad["Name"], + ) + else: + history_iter = iter([]) + except Exception: + global_logger.error( + "Failed to query schedd %s for job history: %s", + schedd_ad["Name"], + traceback.format_exc(), + ) + if current_span: + current_span.set_attribute("error", True) + current_span.set_attribute("error.message", traceback.format_exc()) + error = True + history_iter = iter([]) + + # Process each job in history + if not error: + job_batch = [] + + try: + global_logger.debug("Enqueuing %d jobs from %s to publish queue", len(history_jobs), schedd_ad["Name"]) + + for idx, job_ad in enumerate(history_iter): + counts["count"] += 1 + job_batch.append(job_ad) + # Update latest completion time based on job's completion date + job_completion = ( + job_ad.get("CompletionDate") + or job_ad.get("EnteredCurrentStatus") + or job_ad.get("RecordTime") + ) + if job_completion and job_completion > latest_completion: + latest_completion = job_completion + + if not const.DRY_RUN: + try: + if len(job_batch) >= const.NATS_BATCH_SIZE: + counts["published_count"] += publish_jobs_to_nats( + jetstream, const.NATS_SUBJECT, job_batch, batch_size=const.NATS_BATCH_SIZE + ) + job_batch = [] + except Exception as e: + global_logger.warning( + "Failed to enqueue job %d/%d for %s: %s", + idx + 1, + len(history_jobs), + schedd_ad["Name"], + str(e) + ) + else: + global_logger.debug("DRY RUN: Would publish history job %s", job_ad) + + global_logger.debug( + "Finished enqueuing all %d jobs for %s", + len(history_jobs), + schedd_ad["Name"] + ) + except Exception: + global_logger.error( + "Failure when processing schedd history query on %s: %s", + schedd_ad["Name"], + traceback.format_exc() + ) + error = True + finally: + # Publish any remaining jobs in the final batch + if job_batch: + counts["published_count"] += publish_jobs_to_nats( + jetstream, const.NATS_SUBJECT, job_batch + ) + global_logger.info( + "Finished publishing all %d jobs for %s. Published: %d, Failed: %d", + counts["count"], + schedd_ad["Name"], + counts["published_count"], + counts["count"] - counts["published_count"] + ) + # if jetstream is not None: + # close_nats_connection(connection=nats_connection, timeout=5) + + + total_time = (time.time() - starttime) / 60.0 + last_formatted = datetime.datetime.fromtimestamp(last_completion).strftime( + "%Y-%m-%d %H:%M:%S" + ) + query_time = time.time() - schedd_start_time + + # Record metrics for this schedd + schedd_attributes = {"schedd": schedd_ad["Name"]} + history_jobs_queried_counter.add(counts["count"], attributes=schedd_attributes) + history_jobs_not_published_counter.add(counts["count"] - counts["published_count"], attributes=schedd_attributes) + history_query_duration_histogram.record(query_time, attributes=schedd_attributes) + + global_logger.info( + "Finished querying %-25s history: queried %5d jobs, enqueued %5d jobs; last completion %s; " + "query time %.2f seconds (total time %.2f min)", + schedd_ad["Name"], + counts["count"], counts["published_count"], last_formatted, query_time, total_time + ) + + return (latest_completion, counts) + + +@trace_span("query_job_history") +def query_job_history(schedd_ads: list[classad.ClassAd], starttime: float) -> dict[str, int]: + """ + Queries the job history for each schedd and publishes the jobs to NATS JetStream. + Uses a queue-based approach: worker processes enqueue jobs, main process publishes them. + This eliminates NATS connection contention between worker processes. + + Params: + - schedd_ads: A list of schedd ads to query. + - starttime: The start time of the query in seconds since the epoch. + """ + current_span = trace.get_current_span() + if current_span: + current_span.set_attribute("schedd.count", len(schedd_ads)) + current_span.set_attribute("max_history_processes", const.MAX_HISTORY_PROCESSES) + + # Load checkpoints and prepare tasks + last_completions = load_checkpoints(schedd_ads) + tasks = prepare_tasks(last_completions) + + total_counts = { + "count": 0, + "published_count": 0, + } + + with ProcessPoolExecutor(max_workers=const.MAX_HISTORY_PROCESSES) as executor: + futures = { + executor.submit( + query_schedd_history, + starttime, + last_completion, + schedd_ad, + ): schedd_ad.get("Name") + for schedd_ad, last_completion in tasks + } + + for future in as_completed(futures): + schedd_name = futures[future] + try: + result = future.result() + # Result is either a tuple (latest_completion, count) or just latest_completion + if isinstance(result, tuple): + latest_completion, counts = result + else: + latest_completion = result + counts = { + "count": 0, + "published_count": 0, + } + + total_counts["count"] += counts.get("count", 0) + total_counts["published_count"] += counts.get("published_count", 0) + + # Update checkpoint for this schedd + if latest_completion: + try: + nats_connection, jetstream = get_nats_connection(const.NATS_SERVER, const.NATS_STREAM_NAME, const.NATS_SUBJECT) + update_checkpoint(jetstream, schedd_name, latest_completion) + finally: + # Clean up the global NATS connection used for checkpoints + try: + close_nats_connection(nats_connection, timeout=5.0) + global_logger.debug("Closed global NATS connection after checkpoint updates") + except Exception as e: + global_logger.warning("Error closing global NATS connection: %s", str(e)) + + except Exception as exc: # pylint: disable=broad-except + message = "Schedd %s history generated an exception: %s" % ( + schedd_name, + str(exc), + ) + exc_trace = traceback.format_exc() + message += "\n{}".format(exc_trace) + global_logger.error(message) + send_email_alert( + const.EMAIL_ALERTS, + "spider_cms history processing error warning", + message, + ) + + total_query_time = time.time() - starttime + total_history_query_duration_histogram.record(total_query_time) + global_logger.warning( + "Processing time for history: %.2f mins, total counts: %d, published counts: %d", ((time.time() - starttime) / 60.0), total_counts["count"], total_counts["published_count"] + ) + return total_counts diff --git a/docker/spider-query-cronjob/src/nats_client.py b/docker/spider-query-cronjob/src/nats_client.py new file mode 100644 index 00000000..5651217d --- /dev/null +++ b/docker/spider-query-cronjob/src/nats_client.py @@ -0,0 +1,587 @@ +""" +NATS JetStream connection and publishing utilities. +""" + +import json +import logging +import asyncio +import time +import sys +from opentelemetry.propagate import inject +from opentelemetry.trace import format_trace_id, format_span_id +from nats.aio.client import Client as NATS +from nats.js import JetStreamContext + +import classad + +# Global NATS JetStream connection +_nats_connection = None +_nats_jetstream = None +_nats_loop = None +_checkpoint_kv = None + + +def _build_root_traceparent() -> str: + """ + Build a W3C traceparent using the root span-id of the current trace. + This lets downstream workers attach directly to the root span. + """ + # Support both import styles (`otel_setup` and `src.otel_setup`) to avoid + # losing root context when modules are loaded under different names. + root_ctx = None + for module_name in ("otel_setup", "src.otel_setup"): + try: + module = __import__(module_name, fromlist=["get_root_span_context"]) + ctx = module.get_root_span_context() + if ctx and ctx.is_valid: + root_ctx = ctx + break + except Exception: + continue + + if not root_ctx or not root_ctx.is_valid: + return None + + trace_flags = f"{int(root_ctx.trace_flags):02x}" + return ( + f"00-{format_trace_id(root_ctx.trace_id)}-" + f"{format_span_id(root_ctx.span_id)}-{trace_flags}" + ) + + +# Usually these spans are not needed and increase noise. Use for debugging if needed. +# @trace_span("get_nats_connection") +def get_nats_connection(nats_servers: str, stream_name: str, subject: str): + """ + Get or create a NATS JetStream connection. + Returns (connection, jetstream) tuple. + The event loop is stored globally and reused for all operations. + """ + from opentelemetry import trace + + current_span = trace.get_current_span() + if current_span: + current_span.set_attribute("nats.stream_name", stream_name) + current_span.set_attribute("nats.subject", subject) + + global _nats_connection, _nats_jetstream, _nats_loop + + if _nats_connection is None or _nats_connection.is_closed: + nats_servers = [s.strip() for s in nats_servers.split(",")] + + try: + # Clean up any existing connection state before creating new ones + # This is critical in multi-process contexts to avoid state conflicts + if _nats_connection is not None: + # Connection exists but is closed - clean it up + try: + if ( + _nats_loop is not None + and not _nats_loop.is_closed() + and not _nats_connection.is_closed + ): + _nats_loop.run_until_complete(_nats_connection.close()) + except Exception as e: + logging.debug( + "Error closing existing NATS connection (may already be closed): %s", + str(e), + ) + _nats_connection = None + _nats_jetstream = None + try: + existing_loop = asyncio.get_event_loop() + if not existing_loop.is_closed(): + existing_loop.close() + except (RuntimeError, AttributeError): + pass + # Create a completely fresh event loop for this process + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + # Store the loop for reuse + _nats_loop = loop + _nats_connection = NATS() + loop.run_until_complete(_nats_connection.connect(servers=nats_servers)) + _nats_jetstream = _nats_connection.jetstream() + + logging.debug("Connected to NATS JetStream at %s", nats_servers) + except Exception as e: + logging.error("Failed to connect to NATS JetStream: %s", str(e)) + raise + + return _nats_connection, _nats_jetstream + + +# This decorator creates a lot of spans and can increase load in the trace storage. Use only for debugging. +# @trace_span("publish_jobs_to_nats") +def publish_jobs_to_nats( + jetstream: JetStreamContext, + subject: str, + job_docs: list[classad.ClassAd], + batch_size: int = 100, + timeout: int = None, + loop=None, +) -> int: + """ + Publish jobs to NATS JetStream in batches. + This is the primary method for publishing jobs. Jobs are published concurrently + within each batch for better performance. + + Args: + jetstream: NATS JetStream context + subject: NATS subject to publish to + job_docs: List of job documents (ClassAd) - can be a single-item list + batch_size: Number of jobs to publish per batch (default: 100) + timeout: Timeout in seconds for each publish operation (default from constants) + loop: Optional event loop to use (for thread-local connections). If None, uses global _nats_loop + + Returns: + int: Number of successfully published jobs + """ + from opentelemetry import trace + + current_span = trace.get_current_span() + if current_span: + current_span.set_attribute("nats.subject", subject) + # Note: job.count and batch_size are set at the query level, not per-publish call + # to avoid overwriting with leftover batch values + + global _nats_loop, _nats_connection + + # Import here to avoid circular imports + import constants as const + + if timeout is None: + timeout = const.NATS_PUBLISH_TIMEOUT + + if not job_docs: + return 0 + + try: + function_start = time.time() + # Check if connection is still alive before attempting to publish + if _nats_connection is not None and _nats_connection.is_closed: + logging.warning("NATS connection is closed, cannot publish") + return 0 + + # Use provided loop (for thread-local connections) or fall back to global + if loop is None: + if _nats_loop is None or _nats_loop.is_closed(): + logging.info("_nats_loop is None or closed, creating new loop") + try: + loop = asyncio.get_event_loop() + if loop.is_closed(): + raise RuntimeError("Event loop is closed") + except RuntimeError: + logging.info("Creating new event loop") + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + else: + loop = _nats_loop + + # Check if loop is already running - run_until_complete() can't be called if it is + if loop.is_running(): + logging.error( + "Event loop is already running, cannot use run_until_complete()" + ) + return 0 + + # Prepare all messages with propagated W3C trace context. + # NATS Python library expects headers as dict[str, str] (not dict[str, list]) + messages = [] + headers = {} + + # Collect tracestate from active context (if any). + w3c_trace_headers = {} + inject(w3c_trace_headers) + tracestate = w3c_trace_headers.get("tracestate") + + # Strict mode: only propagate a root-based traceparent. + root_traceparent = _build_root_traceparent() + if not root_traceparent: + logging.warning( + "Strict root trace propagation enabled but root span context is missing; " + "publishing without trace headers" + ) + else: + headers["traceparent"] = root_traceparent + if tracestate: + headers["tracestate"] = tracestate + + for job_doc in job_docs: + job_str = str(job_doc) + message_bytes = json.dumps(job_str).encode("utf-8") + messages.append((message_bytes, headers)) + + async def _publish_batch(batch_messages): + """Publish a batch of messages concurrently.""" + + async def _publish_single(msg_data): + try: + msg_bytes, msg_headers = msg_data + await jetstream.publish( + subject, msg_bytes, headers=msg_headers, timeout=timeout + ) + return True + except Exception as e: + logging.error("Failed to publish single job in batch: %s", str(e)) + logging.error("Job size: %s kB", sys.getsizeof(msg_bytes) / 1024) + return False + + # Publish all messages in the batch concurrently + results = await asyncio.gather( + *[_publish_single(msg) for msg in batch_messages], + return_exceptions=True, + ) + # Count successful publishes (True values, excluding exceptions) + return sum(1 for r in results if r is True) + + # Process jobs in batches + total_published = 0 + + for batch_idx in range(0, len(job_docs), batch_size): + batch = messages[batch_idx : batch_idx + batch_size] + + try: + # Publish the batch with timeout + publish_coro = asyncio.wait_for( + _publish_batch(batch), timeout=const.NATS_PUBLISH_TIMEOUT + ) + batch_published = loop.run_until_complete(publish_coro) + total_published += batch_published + + except asyncio.TimeoutError: + logging.error( + "Timeout publishing batch to NATS", + ) + # Continue with next batch + continue + except Exception as e: + logging.error( + "Exception publishing batch: %s, type=%s", + str(e), + type(e).__name__, + ) + # Continue with next batch + continue + + total_time = time.time() - function_start + if total_published > 0: + logging.debug( + "Published %d/%d jobs in %.3fs (%.2f jobs/sec)", + total_published, + len(job_docs), + total_time, + total_published / total_time if total_time > 0 else 0, + ) + + # Update span attributes with results + # Note: jobs.published and jobs.failed are set at the query/history level with totals + # to avoid overwriting with per-batch values + if current_span: + current_span.set_attribute("publish.duration_seconds", total_time) + + return total_published + + except Exception as e: + total_time = time.time() - function_start + logging.error( + "Failed to publish jobs batch to NATS (elapsed=%.3fs): %s, type=%s", + total_time, + str(e), + type(e).__name__, + ) + import traceback + + logging.error("Traceback: %s", traceback.format_exc()) + return 0 + + +def encode_key(key: str) -> str: + """ + Encode a schedd name for use as a NATS JetStream KeyValue key. + (NATS KeyValue keys cannot contain certain characters like periods (.) and @ symbols.) + + Args: + key: Original key (e.g., "crab3@vocms0195.cern.ch") + + Returns: + str: Encoded key safe for use in NATS KeyValue store + """ + return key.replace("@", "__at__").replace(".", "__dot__") + + +def decode_key(encoded_key: str) -> str: + """ + Decode a KeyValue key back to the original string. + + Args: + encoded_key: Encoded key from NATS KeyValue store + + Returns: + str: Original schedd name + """ + return encoded_key.replace("__at__", "@").replace("__dot__", ".") + + +def get_checkpoint_kv( + jetstream: JetStreamContext, kv_bucket_name: str = "spider_checkpoints" +): + """ + Get or create a KeyValue store for checkpoints. + + Args: + jetstream: NATS JetStream context + kv_bucket_name: Name of the KeyValue bucket to use for checkpoints + + Returns: + KeyValue store instance + """ + global _checkpoint_kv + + if _checkpoint_kv is None: + try: + loop = asyncio.get_event_loop() + if loop.is_closed(): + raise RuntimeError("Event loop is closed") + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + try: + # Try to get existing KeyValue store + _checkpoint_kv = loop.run_until_complete( + jetstream.key_value(kv_bucket_name) + ) + logging.debug("Connected to existing KeyValue store: %s", kv_bucket_name) + except Exception: + logging.error("Failed to get KeyValue store: %s", kv_bucket_name) + + return _checkpoint_kv + + +def get_checkpoint( + jetstream: JetStreamContext, + schedd_name: str, + kv_bucket_name: str = "spider_checkpoints", +): + """ + Get checkpoint (last completion time) for a schedd from KeyValue store. + + Args: + jetstream: NATS JetStream context + schedd_name: Name of the schedd + kv_bucket_name: Name of the KeyValue bucket + + Returns: + float: Last completion timestamp, or None if not found + """ + try: + kv = get_checkpoint_kv(jetstream, kv_bucket_name) + + try: + loop = asyncio.get_event_loop() + if loop.is_closed(): + raise RuntimeError("Event loop is closed") + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + # Encode the schedd name to handle special characters like . and @ + encoded_key = encode_key(schedd_name) + entry = loop.run_until_complete(kv.get(encoded_key)) + if entry: + completion_time = float(entry.value.decode("utf-8")) + logging.debug( + "Retrieved checkpoint for %s: %s", schedd_name, completion_time + ) + return completion_time + except Exception as e: + # Key not found or other error - this is expected for first run + if "10037" not in str(e) and "not found" not in str(e).lower(): + logging.warning( + "Failed to get checkpoint for %s from KeyValue store: %s", + schedd_name, + str(e), + ) + + return None + + +def set_checkpoint( + jetstream: JetStreamContext, + schedd_name: str, + completion_date: float, + kv_bucket_name: str = "spider_checkpoints", +): + """ + Set checkpoint (last completion time) for a schedd in KeyValue store. + + Args: + jetstream: NATS JetStream context + schedd_name: Name of the schedd + completion_date: Completion timestamp to store + kv_bucket_name: Name of the KeyValue bucket + + Returns: + bool: True if successful, False otherwise + """ + try: + kv = get_checkpoint_kv(jetstream, kv_bucket_name) + try: + loop = asyncio.get_event_loop() + if loop.is_closed(): + raise RuntimeError("Event loop is closed") + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + # Encode the schedd name to handle special characters like . and @ + encoded_key = encode_key(schedd_name) + # Store completion time as string + value = str(completion_date).encode("utf-8") + loop.run_until_complete(kv.put(encoded_key, value)) + logging.info( + "Updated checkpoint for %s: %s", + schedd_name, + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(completion_date)), + ) + return True + except Exception as e: + logging.error( + "Failed to set checkpoint for %s in KeyValue store: %s", schedd_name, str(e) + ) + return False + + +def get_all_checkpoints( + jetstream: JetStreamContext, kv_bucket_name: str = "spider_checkpoints" +): + """ + Get all checkpoints from KeyValue store. + + Args: + jetstream: NATS JetStream context + kv_bucket_name: Name of the KeyValue bucket + + Returns: + dict: Dictionary mapping schedd names to completion timestamps + """ + checkpoints = {} + try: + kv = get_checkpoint_kv(jetstream, kv_bucket_name) + + try: + loop = asyncio.get_event_loop() + if loop.is_closed(): + raise RuntimeError("Event loop is closed") + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + # Get all keys from the KeyValue store (keys() returns a coroutine that returns a list) + async def _get_all_keys(): + return await kv.keys() + + keys = loop.run_until_complete(_get_all_keys()) + for encoded_key in keys: + try: + entry = loop.run_until_complete(kv.get(encoded_key)) + if entry: + completion_time = float(entry.value.decode("utf-8")) + # Decode the key back to the original schedd name + schedd_name = decode_key(encoded_key) + checkpoints[schedd_name] = completion_time + except Exception as e: + logging.warning( + "Failed to get checkpoint for key %s: %s", encoded_key, str(e) + ) + continue + + logging.debug("Retrieved %d checkpoints from KeyValue store", len(checkpoints)) + except Exception as e: + logging.warning("Failed to get all checkpoints from KeyValue store: %s", str(e)) + + return checkpoints + + +def close_nats_connection(connection=None, loop=None, timeout=5.0): + """ + Properly close a NATS connection and wait for all background tasks to complete. + This prevents "Task was destroyed but it is pending" errors. + + Args: + connection: NATS connection to close (if None, closes global connection) + loop: Event loop to use (if None, uses global _nats_loop or gets current loop) + timeout: Maximum time to wait for connection to close (seconds) + + Returns: + bool: True if closed successfully, False otherwise + """ + global _nats_connection, _nats_jetstream, _nats_loop, _checkpoint_kv + + # Use provided connection or global connection + if connection is None: + connection = _nats_connection + + if connection is None or connection.is_closed: + logging.debug("NATS connection is None or already closed") + # Clear cached KV store if this is the global connection (it references the closed connection) + if connection is _nats_connection: + _checkpoint_kv = None + return True + + # Determine which loop to use + if loop is None: + loop = _nats_loop + if loop is None or loop.is_closed(): + try: + loop = asyncio.get_event_loop() + if loop.is_closed(): + raise RuntimeError("Event loop is closed") + except RuntimeError: + logging.warning( + "No valid event loop available for closing NATS connection" + ) + return False + + if loop.is_closed(): + logging.warning("Event loop is closed, cannot close NATS connection properly") + return False + + try: + + async def _close(): + """Close the connection and wait for all tasks to complete.""" + if not connection.is_closed: + # Close the connection - this will stop background tasks + await connection.close() + # Give a small delay to allow tasks to finish + await asyncio.sleep(0.1) + + # Run the close operation with a timeout + if loop.is_running(): + logging.warning( + "Event loop is running, cannot use run_until_complete() to close connection" + ) + return False + + loop.run_until_complete(asyncio.wait_for(_close(), timeout=timeout)) + + # Clear global references if this was the global connection + if connection == _nats_connection: + _nats_connection = None + _nats_jetstream = None + _checkpoint_kv = ( + None # Also clear cached KV store since it uses the closed connection + ) + # Don't close the loop here - it might be reused + # _nats_loop = None + + logging.debug("NATS connection closed successfully") + return True + except asyncio.TimeoutError: + logging.warning("Timeout closing NATS connection after %s seconds", timeout) + return False + except Exception as e: + logging.warning("Error closing NATS connection: %s", str(e)) + return False diff --git a/docker/spider-query-cronjob/src/otel_setup.py b/docker/spider-query-cronjob/src/otel_setup.py new file mode 100644 index 00000000..363b83f6 --- /dev/null +++ b/docker/spider-query-cronjob/src/otel_setup.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python +""" +OpenTelemetry setup for sending metrics to CERN monitoring endpoint. +""" + +import base64 +import logging +import uuid +import functools +import contextvars +import opentelemetry +from opentelemetry.metrics import Meter +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader +from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter +from opentelemetry.sdk.resources import Resource +from opentelemetry.trace import Tracer, Status, StatusCode +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter +from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler +from opentelemetry._logs import set_logger_provider +from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter +from opentelemetry.sdk._logs.export import BatchLogRecordProcessor +from opentelemetry import trace +from opentelemetry.trace import format_trace_id +from typing import Tuple + +import constants as const + +logger = logging.getLogger(__name__) + +# Global execution ID for this cronjob run +EXECUTION_ID = str(uuid.uuid4()) +_root_span_context = contextvars.ContextVar("root_span_context", default=None) + + +def get_execution_id() -> str: + """Return the current trace ID; fallback to process execution ID.""" + span_context = trace.get_current_span().get_span_context() + if span_context and span_context.is_valid: + return format_trace_id(span_context.trace_id) + return EXECUTION_ID + + +def get_root_span_context(): + """Return the root span context for the current execution context, if any.""" + return _root_span_context.get() + + +class CustomLoggingHandler(LoggingHandler): + """Custom LoggingHandler that adds service.name to log record attributes""" + + def __init__(self, service_name: str, service_version: str, *args, **kwargs): + super().__init__(*args, **kwargs) + self._service_name = service_name + self._service_version = service_version + + def _get_attributes(self, record: logging.LogRecord): + """Override to add stable log attributes with highest precedence.""" + # Get base attributes from parent + attributes = super()._get_attributes(record) + + # Add service.name and service.version to log attributes (highest precedence) + attributes["service.name"] = self._service_name + attributes["service.version"] = self._service_version + # Keep execution.id in log attributes (like spider-worker logs), not only resource. + attributes["execution.id"] = get_execution_id() + + return attributes + +# Generate a unique execution ID for this cronjob run - shared across all processes +# This will be included in NATS message headers to link worker pod logs back to this run + + + +def setup_opentelemetry() -> Tuple[Meter, Tracer]: + """ + Initialize OpenTelemetry metrics and tracing exporters to send data to CERN monitoring endpoint. + + Returns: + tuple: (Meter, Tracer) - The configured meter and tracer instances + """ + + # Create resource with service information and run ID + resource = Resource.create({ + "service.name": const.OTEL_SERVICE_NAME, + "service.version": const.DOCKER_TAG, + "execution.id": EXECUTION_ID, + }) + + channel_options=( + ("grpc.keepalive_time_ms", 20000), + ("grpc.keepalive_timeout_ms", 10000), + ("grpc.keepalive_permit_without_calls", False), + ("grpc.http2.min_time_between_pings_ms", 30000), + ) + + creds = f"{const.OTEL_USERNAME}:{const.OTEL_PASSWORD}".encode("utf-8") + token = base64.b64encode(creds).decode("utf-8") + otel_headers = {"authorization": f"Basic {token}"} + + # Create OTLP metric exporter + metric_exporter = OTLPMetricExporter( + endpoint=f"{const.OTEL_ENDPOINT}/v1/metrics", + headers=otel_headers, + channel_options=channel_options, + insecure=True, + ) + + # Create metric reader with periodic export + metric_reader = PeriodicExportingMetricReader( + exporter=metric_exporter, + export_interval_millis=int(const.OTEL_METRIC_EXPORT_INTERVAL), # Default 60s + ) + + # Create and set global meter provider + provider = MeterProvider( + resource=resource, + metric_readers=[metric_reader], + ) + opentelemetry.metrics.set_meter_provider(provider) + + # Get meter for creating metrics + meter = opentelemetry.metrics.get_meter(__name__) + + # Create OTLP trace exporter + trace_exporter = OTLPSpanExporter( + endpoint=f"{const.OTEL_ENDPOINT}/v1/traces", + headers=otel_headers, + channel_options=channel_options, + insecure=True, + ) + + # Create and set global tracer provider + trace_provider = TracerProvider(resource=resource) + trace_provider.add_span_processor(BatchSpanProcessor(trace_exporter)) + opentelemetry.trace.set_tracer_provider(trace_provider) + + # Get tracer for creating spans + tracer = opentelemetry.trace.get_tracer(__name__) + + # Create and set global logger provider for OpenTelemetry logging + logger_provider = LoggerProvider(resource=resource) + set_logger_provider(logger_provider) + + # Create OTLP log exporter + log_exporter = OTLPLogExporter( + endpoint=f"{const.OTEL_ENDPOINT}/v1/logs", + headers=otel_headers, + channel_options=channel_options, + insecure=True, + ) + + # Add log record processor + logger_provider.add_log_record_processor( + BatchLogRecordProcessor(log_exporter) + ) + + # Get root logger and add OpenTelemetry handler + # Use custom handler that adds service.name to log attributes (highest precedence) + root_logger = logging.getLogger() + otel_handler = CustomLoggingHandler( + service_name=const.OTEL_SERVICE_NAME, + service_version=const.DOCKER_TAG, + logger_provider=logger_provider, + level=logging.INFO + ) + root_logger.addHandler(otel_handler) + + logger.warning( + f"OpenTelemetry metrics, tracing, and logging initialized: endpoint={const.OTEL_ENDPOINT}, " + f"service={const.IMAGE_NAME}, version={const.DOCKER_TAG}, execution_id={EXECUTION_ID}" + ) + + return meter, tracer + + +global_meter, global_tracer = setup_opentelemetry() + + +def trace_span(span_name: str = None, **attributes): + """ + Decorator that creates an OpenTelemetry span for a function execution. + The span includes timing information and can have attributes set during execution. + + Args: + span_name: Name of the span (defaults to function name) + **attributes: Additional attributes to set on the span + + Example usage: + @trace_span("query_schedd_queue", schedd="crab3@vocms0195.cern.ch") + def query_schedd_queue(starttime, schedd_ads): + # ... function code ... + with global_tracer.start_as_current_span("nested_operation") as span: + span.set_attribute("jobs_count", count) + return count + """ + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + # Use provided span_name or default to function name + name = span_name or f"{const.OTEL_SERVICE_NAME}.{func.__module__}.{func.__name__}" + parent_span_context = trace.get_current_span().get_span_context() + is_root_span = not ( + parent_span_context and parent_span_context.is_valid + ) + root_token = None + # Start a new span + with global_tracer.start_as_current_span(name) as span: + if is_root_span: + root_token = _root_span_context.set(span.get_span_context()) + # Set initial attributes + for key, value in attributes.items(): + span.set_attribute(key, value) + + # Set span kind to INTERNAL (default for internal operations) + span.set_attribute("execution.id", get_execution_id()) + + try: + # Execute the function + result = func(*args, **kwargs) + # Mark span as successful + span.set_status(Status(StatusCode.OK)) + return result + except Exception as e: + # Mark span as error + span.set_status(Status(StatusCode.ERROR, str(e))) + span.record_exception(e) + raise + finally: + if root_token is not None: + _root_span_context.reset(root_token) + + return wrapper + return decorator diff --git a/docker/spider-query-cronjob/src/query.py b/docker/spider-query-cronjob/src/query.py new file mode 100755 index 00000000..a138efc7 --- /dev/null +++ b/docker/spider-query-cronjob/src/query.py @@ -0,0 +1,294 @@ +""" +Query the jobs in queue for given set of schedds and publish to NATS JetStream. +""" + +import time +import traceback +from concurrent.futures import ProcessPoolExecutor, as_completed +import htcondor +import classad + +from otel_setup import global_meter, trace_span +from utils import send_email_alert, time_remaining, global_logger +from nats_client import publish_jobs_to_nats, get_nats_connection, close_nats_connection +import constants as const +from opentelemetry import trace +from opentelemetry.trace import Status, StatusCode + +# Create metrics for per-schedd operations +schedd_duration_histogram = global_meter.create_histogram( + name="spider_cms.schedd.queue.duration", + description="Duration of queue query per schedd in seconds", + unit="s", +) +jobs_queried_counter = global_meter.create_counter( + name="spider_cms.schedd.queue.jobs", + description="Number of jobs queried per schedd", + unit="1", +) +jobs_published_counter = global_meter.create_counter( + name="spider_cms.schedd.queue.jobs_published", + description="Number of jobs published per schedd", + unit="1", +) + + +@trace_span("query_single_schedd") +def query_single_schedd( + starttime: float, + schedd_ad: classad.ClassAd, +) -> dict: + """ + Query a single schedd for jobs and publish each job to NATS JetStream. + Intended to be safe to use from multiprocessing executors. + Each worker process creates its own NATS connection to avoid pickling issues. + """ + my_start = time.time() + counts = { + "count": 0, + "published_count": 0, + } + schedd_name = schedd_ad["Name"] + pool_name = schedd_ad.get("CMS_Pool", "Unknown") + + # Set span attributes for tracing + current_span = trace.get_current_span() + if current_span: + current_span.set_attribute("schedd.name", schedd_name) + current_span.set_attribute("schedd.pool", pool_name) + + # Create NATS connection in this worker process + # This avoids pickling issues with asyncio objects + nats_connection = None + jetstream = None + try: + nats_connection, jetstream = get_nats_connection( + const.NATS_SERVER, const.NATS_STREAM_NAME, const.NATS_SUBJECT + ) + except Exception as e: + global_logger.error( + "Failed to get NATS connection for schedd %s: %s", schedd_name, str(e) + ) + return counts + + # Use try-finally to ensure NATS connection is always closed, allowing process reuse + try: + global_logger.info("Querying %s queue for jobs.", schedd_name) + + if time_remaining(starttime, timeout=const.TIMEOUT_MINS * 60) < 10: + message = ( + "No time remaining to run queue crawler on %s; exiting." % schedd_ad["Name"] + ) + global_logger.error(message) + send_email_alert( + const.EMAIL_ALERTS, "spider_cms queue timeout warning", message + ) + return counts + + schedd = htcondor.Schedd(schedd_ad) + # Query for a snapshot of the jobs running/idle/held, + # but only the completed that had changed in the last period of time. + _completed_since = starttime - (const.TIMEOUT_MINS + 1) * 60 + query = """ + (JobUniverse == 5) && (CMS_Type != "DONOTMONIT") + && + ( + JobStatus < 3 || JobStatus > 4 + || EnteredCurrentStatus >= %(completed_since)d + || CRAB_PostJobLastUpdate >= %(completed_since)d + ) + """ % {"completed_since": _completed_since} + + job_batch = [] # Initialize outside try block so it's accessible in finally + try: + # TODO: Move the NATS logic to a separate function + query_iter = schedd.xquery(constraint=query) if not const.DRY_RUN else [] + if not const.DRY_RUN: + # Collect jobs in batches for efficient publishing + for job_ad in query_iter: + counts["count"] += 1 + job_batch.append(job_ad) + + if len(job_batch) >= const.NATS_BATCH_SIZE: + batch_published = publish_jobs_to_nats( + jetstream, const.NATS_SUBJECT, job_batch, batch_size=const.NATS_BATCH_SIZE + ) + counts["published_count"] += batch_published + job_batch = [] # Clear batch after publishing + + else: + for _ in range(len(query_iter)): + counts["count"] += 1 + global_logger.info( + "DRY RUN: Would publish jobs from %s: %d", + schedd_name, + counts["count"], + ) + + except RuntimeError as e: + global_logger.error( + "Failed to query schedd %s for jobs: %s", schedd_name, str(e) + ) + except Exception as e: + message = "Failure when querying schedd queue on %s: %s" % ( + schedd_name, + str(e), + ) + global_logger.error(message) + send_email_alert( + const.EMAIL_ALERTS, "spider_cms schedd queue query error", message + ) + traceback.print_exc() + finally: + # Always publish any remaining jobs, even if an exception occurred + if job_batch and not const.DRY_RUN: + global_logger.debug( + "Publishing %d remaining jobs in batch for %s", + len(job_batch), + schedd_name, + ) + try: + batch_published = publish_jobs_to_nats( + jetstream, const.NATS_SUBJECT, job_batch, batch_size=len(job_batch) + ) + counts["published_count"] += batch_published + if batch_published < len(job_batch): + global_logger.warning( + "Only %d/%d remaining jobs published for %s", + batch_published, + len(job_batch), + schedd_name, + ) + except Exception as publish_error: + global_logger.error( + "Failed to publish remaining %d jobs for %s: %s", + len(job_batch), + schedd_name, + str(publish_error), + ) + + total_time = (time.time() - my_start) / 60.0 + + # Record metrics for this schedd + schedd_attributes = { + "schedd": schedd_name, + "pool": pool_name, + } + schedd_duration_histogram.record(total_time, attributes=schedd_attributes) + jobs_queried_counter.add( + counts["count"], + attributes=schedd_attributes, + ) + jobs_published_counter.add( + counts["published_count"], + attributes=schedd_attributes, + ) + + global_logger.info( + "Completed querying %-25s: queried %5d jobs, published %5d jobs; query time %.2f min", + schedd_name, + counts["count"], + counts["published_count"], + total_time, + ) + + # Set trace attributes with final totals (after all publishing is complete) + current_span = trace.get_current_span() + if current_span: + current_span.set_attribute("job.count", counts["count"]) + current_span.set_attribute("batch_size", const.NATS_BATCH_SIZE) + current_span.set_attribute("jobs.published", counts["published_count"]) + current_span.set_attribute("jobs.failed", counts["count"] - counts["published_count"]) + + return counts + finally: + # Close NATS connection to allow process to be immediately reused for next task + # This prevents delays when ProcessPoolExecutor reuses the worker process + # Without this, the next task has to wait for cleanup of the old connection + if nats_connection is not None: + try: + close_nats_connection(connection=nats_connection, timeout=5) + except Exception as close_error: + global_logger.warning( + "Error closing NATS connection for %s (non-fatal): %s", + schedd_name, + str(close_error), + ) + + +@trace_span("query_schedds") +def query_schedds( + starttime: float, + schedd_ads: list[classad.ClassAd], +) -> dict: + """ + Sequential wrapper kept for compatibility; queries all schedds one by one. + """ + total_counts = { + "count": 0, + "published_count": 0, + } + for schedd_ad in schedd_ads: + counts = query_single_schedd( + starttime, schedd_ad + ) + total_counts["count"] += counts.get("count", 0) + total_counts["published_count"] += counts.get("published_count", 0) + + # Check if published count differs from total count + if total_counts["count"] != total_counts["published_count"]: + error_message = ( + "@@@ MISMATCH: Total jobs queried: %d, Total jobs published: %d. " + "Some jobs were not successfully published to NATS." + ) % (total_counts["count"], total_counts["published_count"]) + global_logger.error(error_message) + trace.get_current_span().set_status(Status(StatusCode.ERROR, error_message)) + trace.get_current_span().set_attribute("error", True) + + return total_counts + +@trace_span("query_running_jobs") +def query_running_jobs( + starttime: float, + schedd_ads: list[classad.ClassAd], +) -> dict[str, int]: + """ + Parallel wrapper for querying all schedds for running jobs in parallel. + The jobs are then published to NATS JetStream. + Params: + - starttime: The start time of the query in seconds since the epoch. + - schedd_ads: A list of schedd ads to query. + """ + current_span = trace.get_current_span() + if current_span: + current_span.set_attribute("schedd.count", len(schedd_ads)) + current_span.set_attribute("max_processes", const.MAX_PROCESSES) + + total_counts = { + "count": 0, + "published_count": 0, + } + with ProcessPoolExecutor(max_workers=const.MAX_PROCESSES) as executor: + futures = { + executor.submit( + query_single_schedd, + starttime, + schedd_ad, + ): schedd_ad.get("Name") + for schedd_ad in schedd_ads + } + for future in as_completed(futures): + schedd_name = futures[future] + try: + counts = future.result() + total_counts["count"] += counts.get("count", 0) + total_counts["published_count"] += counts.get("published_count", 0) + except Exception as exc: # pylint: disable=broad-except + global_logger.error( + "Schedd %s generated an exception: %s", schedd_name, str(exc) + ) + + total_duration = time.time() - starttime + global_logger.info("@@@ Total querying time: %.2f mins, queried %d jobs, published %d jobs", (total_duration / 60.0), total_counts["count"], total_counts["published_count"]) + + return total_counts diff --git a/docker/spider-query-cronjob/src/utils.py b/docker/spider-query-cronjob/src/utils.py new file mode 100644 index 00000000..9344bdf0 --- /dev/null +++ b/docker/spider-query-cronjob/src/utils.py @@ -0,0 +1,171 @@ +""" +Various helper utilities for the HTCondor-ES integration +""" + +import os +import pwd +import sys +import time +import shlex +import socket +import logging +import smtplib +import subprocess +import email.mime.text +import json + +import classad +import htcondor + + +def get_schedds_from_file(collectors_file: str = None, schedd_filter: str = None): + schedds = [] + names = set[str]() + try: + # TODO: Check type validation for pools + with open(collectors_file, "r") as f: + pools: dict[str, list[str]] = json.load(f) + for pool in pools.keys(): + _pool_schedds = get_schedds(collectors=pools[pool], pool_name=pool, schedd_filter=schedd_filter) + schedds.extend([s for s in _pool_schedds if s.get("Name") not in names]) + names.update([s.get("Name") for s in _pool_schedds]) + + except (IOError, json.JSONDecodeError): + schedds = get_schedds(schedd_filter=schedd_filter) + + global_logger.info("&&& There are %d schedds to query.", len(schedds)) + return schedds + + +def get_schedds(collectors: list[str] = None, pool_name: str = "Unknown", schedd_filter: str = None) -> list[dict]: + """ + Return a list of schedd ads representing all the schedds in the pool. + """ + collectors = collectors or [] + schedd_query = classad.ExprTree("!isUndefined(CMSGWMS_Type)") + + schedd_ads: dict[str, dict] = {} + for host in collectors: + coll = htcondor.Collector(host) + try: + schedds = coll.query( + htcondor.AdTypes.Schedd, + schedd_query, + projection=["MyAddress", "ScheddIpAddr", "Name", "TotalRunningJobs", "TotalIdleJobs", "TotalHeldJobs"], + ) + except IOError as e: + logging.warning(str(e)) + continue + + for schedd in schedds: + try: + schedd["CMS_Pool"] = pool_name + schedd_ads[schedd["Name"]] = schedd + schedd_ads[schedd["Name"]]["TotalJobs"] = schedd["TotalRunningJobs"] + schedd["TotalIdleJobs"] + schedd["TotalHeldJobs"] + except KeyError: + pass + + schedd_ads = list(schedd_ads.values()) + schedd_ads.sort(key=lambda x: x["TotalJobs"], reverse=True) + + if schedd_filter: + return [s for s in schedd_ads if s["Name"] in schedd_filter.split(",")] + + return schedd_ads + + +def send_email_alert(recipients, subject, message): + """ + Send a simple email alert (typically of failure). + """ + # TMP: somehow send_email_alert still sending alerts + if not recipients: + return + msg = email.mime.text.MIMEText(message) + msg["Subject"] = "%s - %sh: %s" % ( + socket.gethostname(), + time.strftime("%b %d, %H:%M"), + subject, + ) + + domain = socket.getfqdn() + uid = os.geteuid() + pw_info = pwd.getpwuid(uid) + if "cern.ch" not in domain: + domain = "%s.unl.edu" % socket.gethostname() + msg["From"] = "%s@%s" % (pw_info.pw_name, domain) + msg["To"] = recipients[0] + + try: + sess = smtplib.SMTP("localhost") + sess.sendmail(msg["From"], recipients, msg.as_string()) + sess.quit() + except Exception as exn: # pylint: disable=broad-except + logging.warning("Email notification failed: %s", str(exn)) + + +def time_remaining(starttime: float, timeout: int = 3600, positive: bool = True) -> int: + """ + Return the remaining time (in seconds) until starttime + timeout + Returns 0 if there is no time remaining + """ + elapsed = time.time() - starttime + if positive: + return max(0, timeout - elapsed) + return timeout - elapsed + + +def set_up_logging(log_level: int = logging.INFO) -> logging.Logger: + """Configure root logger with rotating file handler""" + logger = logging.getLogger() + logger.setLevel(log_level) + + if log_level <= logging.INFO: + logging.getLogger("CMSMonitoring.StompAMQ").setLevel(log_level + 10) + logging.getLogger("stomp.py").setLevel(log_level + 10) + + # Check if a StreamHandler already exists to avoid duplicate handlers + # This prevents duplicate log messages when the function is called multiple times + has_stream_handler = any( + isinstance(handler, logging.StreamHandler) and handler.stream == sys.stdout + for handler in logger.handlers + ) + + if not has_stream_handler: + stream_handler = logging.StreamHandler(sys.stdout) + stream_handler.setFormatter(logging.Formatter('%(asctime)s : %(name)s:%(levelname)s PID: %(process)d - %(message)s')) + logger.addHandler(stream_handler) + + return logger + +def collect_metadata(): + """ + Return a dictionary with: + - hostname + - username + - current time (in epoch millisec) + - hash of current git commit + """ + result = {} + result["spider_git_hash"] = get_githash() + result["spider_hostname"] = socket.gethostname() + result["spider_username"] = pwd.getpwuid(os.geteuid()).pw_name + result["spider_runtime"] = int(time.time() * 1000) + return result + + +def get_githash(): + """Returns the git hash of the current commit in the scripts repository""" + gitwd = os.path.dirname(os.path.realpath(__file__)) + cmd = r"git rev-parse --verify HEAD" + try: + call = subprocess.Popen(shlex.split(cmd), stdout=subprocess.PIPE, cwd=gitwd) + out, err = call.communicate() + return str(out.strip()) + + except Exception as e: + logging.warning(str(e)) + return "unknown" + + +global_logger = set_up_logging() \ No newline at end of file diff --git a/docker/spider-query-cronjob/src/vals.py b/docker/spider-query-cronjob/src/vals.py new file mode 100644 index 00000000..4dee57f7 --- /dev/null +++ b/docker/spider-query-cronjob/src/vals.py @@ -0,0 +1,521 @@ +string_vals = { + "AutoClusterId", + "AffiliationInstitute", + "AffiliationCountry", + "Processor", + "ChirpCMSSWCPUModels", + "CPUModel", + "CPUModelName", + "ChirpCMSSWCPUModels", + "CMSPrimaryPrimaryDataset", + "CMSPrimaryProcessedDataset", + "CMSPrimaryDataTier", + "CMSSWVersion", + "CMSSWMajorVersion", + "CMSSWReleaseSeries", + "CRAB_JobType", + "CRAB_JobSW", + "CRAB_JobArch", + "CRAB_Id", + "CRAB_ISB", + "CRAB_PostJobStatus", + "CRAB_Workflow", + "CRAB_UserRole", + "CMSGroups", + "CRAB_UserHN", + "CRAB_UserGroup", + "CRAB_TaskWorker", + "CRAB_SiteWhitelist", + "CRAB_SiteBlacklist", + "CRAB_SplitAlgo", + "CRAB_PrimaryDataset", + "Args", + "AccountingGroup", + "Cmd", + "CMS_JobType", + "CMS_WMTool", + "DESIRED_Archs", + "DESIRED_CMSDataLocations", + "DESIRED_CMSDataset", + "DESIRED_Sites", + "ExtDESIRED_Sites", + "FormattedCrabId", + "GlobalJobId", + "GlideinClient", + "GlideinEntryName", + "GlideinFactory", + "GlideinFrontendName", + "GlideinName", + "GLIDECLIENT_Name", + "GLIDEIN_Entry_Name", + "GLIDEIN_Factory", + "GlobusRSL", + "GridJobId", + "LastRemoteHost", + "MachineAttrCMSSubSiteName0", + "MATCH_EXP_JOB_GLIDECLIENT_Name", + "MATCH_EXP_JOB_GLIDEIN_ClusterId", + "MATCH_EXP_JOB_GLIDEIN_CMSSite", + "MATCH_EXP_JOB_GLIDEIN_Entry_Name", + "MATCH_EXP_JOB_GLIDEIN_Factory", + "MATCH_EXP_JOB_GLIDEIN_Name", + "MATCH_EXP_JOB_GLIDEIN_Schedd", + "MATCH_EXP_JOB_GLIDEIN_SEs", + "MATCH_EXP_JOB_GLIDEIN_Site", + "MATCH_EXP_JOB_GLIDEIN_SiteWMS", + "MATCH_EXP_JOB_GLIDEIN_SiteWMS_JobId", + "MATCH_EXP_JOB_GLIDEIN_SiteWMS_Queue", + "MATCH_EXP_JOB_GLIDEIN_SiteWMS_Slot", + "MachineAttrCUDACapability0", + "MachineAttrCUDADeviceName0", + "MachineAttrCUDADriverVersion0", + "Owner", + "Rank", + "RemoteHost", + "REQUIRED_OS", + "ShouldTransferFiles", + "StartdIpAddr", + "StartdPrincipal", + "User", + "WhenToTransferOutput", + "WMAgent_AgentName", + "WMAgent_RequestName", + "WMAgent_SubTaskName", + "x509UserProxyEmail", + "x509UserProxyFirstFQAN", + "x509UserProxyFQAN", + "x509userproxysubject", + "x509UserProxyVOName", + "InputData", + "Original_DESIRED_Sites", + "WMAgent_TaskType", + "NordugridRSL", + "Campaign", + "TaskType", + "DataLocations", + "Workflow", + "Site", + "Tier", + "Country", + "Status", + "Universe", + "ExitReason", + "LastHoldReason", + "RemoveReason", + "DESIRED_Overflow_Region", + "DESIRED_OpSysMajorVers", + "DESIRED_CMSDataset", + "DAGNodeName", + "DAGParentNodeNames", + "OverflowType", + "ScheddName", +} + +int_vals = { + "CRAB_Retry", + "BytesRecvd", + "BytesSent", + "ClusterId", + "CommittedSlotTime", + "CumulativeSlotTime", + "CumulativeSuspensionTime", + "CurrentHosts", + "CRAB_JobCount", + "DelegatedProxyExpiration", + "DiskUsage_RAW", + "ExecutableSize_RAW", + "ExitStatus", + "GlobusStatus", + "ImageSize_RAW", + "JobPrio", + "JobRunCount", + "JobStatus", + "JobFailed", + "JobUniverse", + "LastJobStatus", + "LocalSysCpu", + "LocalUserCpu", + "MachineAttrCpus0", + "MachineAttrSlotWeight0", + "MachineAttrCUDAComputeUnits0", + "MachineAttrCUDACoresPerCU0", + "MachineAttrCUDAGlobalMemoryMb0", + "MATCH_EXP_JOB_GLIDEIN_Job_Max_Time", + "MATCH_EXP_JOB_GLIDEIN_MaxMemMBs", + "MATCH_EXP_JOB_GLIDEIN_Max_Walltime", + "MATCH_EXP_JOB_GLIDEIN_Memory", + "MATCH_EXP_JOB_GLIDEIN_ProcId", + "MATCH_EXP_JOB_GLIDEIN_ToDie", + "MATCH_EXP_JOB_GLIDEIN_ToRetire", + "MaxHosts", + "MaxWallTimeMins_RAW", + "MemoryUsage", + "MinHosts", + "NumGlobusSubmitsNumJobMatches", + "NumJobStarts", + "NumRestarts", + "NumShadowStarts", + "NumSystemHolds", + "PilotRestLifeTimeMins", + "PostJobPrio1", + "PostJobPrio2", + "ProcId", + "RecentBlockReadKbytes", + "RecentBlockReads", + "RecentBlockWriteKbytes", + "RecentBlockWrites", + "RemoteSlotID", + "RemoteSysCpu", + "RemoteUserCpu", + "RemoteWallClockTime", + "RequestCpus", + "RequestDisk_RAW", + "RequestMemory_RAW", + "ResidentSetSize_RAW", + "StatsLifetimeStarter", + "TotalSuspensions", + "TransferInputSizeMB", + "WallClockCheckpoint", + "WMAgent_JobID", + "DesiredSiteCount", + "DataLocationsCount", +} + +date_vals = { + "CompletionDate", + "CRAB_TaskCreationDate", + "EnteredCurrentStatus", + "JobCurrentStartDate", + "JobCurrentStartExecutingDate", + "JobCurrentStartTransferOutputDate", + "JobLastStartDate", + "JobStartDate", + "LastMatchTime", + "LastSuspensionTime", + "LastVacateTime_RAW", + "MATCH_GLIDEIN_ToDie", + "MATCH_GLIDEIN_ToRetire", + "QDate", + "ShadowBday", + "StageInFinish", + "StageInStart", + "JobFinishedHookDone", + "LastJobLeaseRenewal", + "LastRemoteStatusUpdate", + "GLIDEIN_ToDie", + "GLIDEIN_ToRetire", + "DataCollectionDate", + "RecordTime", + "ChirpCMSSWLastUpdate", +} + +ignore = { + "Arguments", + "CmdHash", + "CRAB_UserDN", + "CRAB_Destination", + "CRAB_DBSURL", + "CRAB_ASOURL", + "CRAB_ASODB", + "CRAB_AdditionalOutputFiles", + "CRAB_EDMOutputFiles", + "CRAB_TFileOutputFiles", + "CRAB_oneEventMode", + "CRAB_NumAutomJobRetries", + "CRAB_localOutputFiles", + "CRAB_ASOTimeout", + "CRAB_OutTempLFNDir", + "CRAB_PublishDBSURL", + "CRAB_PublishGroupName", + "CRAB_RestURInoAPI", + "CRAB_RestHost", + "CRAB_ReqName", + "CRAB_RetryOnASOFailures", + "CRAB_StageoutPolicy", + "SubmitEventNotes", + "DAGManNodesMask", + "DAGManNodesLog", + "DAGManJobId", + "accounting_group", + "AcctGroup", + "AcctGroupUser", + "AllowOpportunistic", + "AutoClusterAttrs", + "BufferBlockSize", + "BufferSize", + "CondorPlatform", + "CondorVersion", + "DiskUsage", + "Err", + "Environment", + "EnvDelim", + "Env", + "ExecutableSize", + "HasPrioCorrection", + "GlideinCredentialIdentifier", + "GlideinLogNr", + "GlideinSecurityClass", + "GlideinSlotsLayout", + "GlideinWebBase", + "GlideinWorkDir", + "ImageSize", + "In", + "Iwd", + "JobAdInformationAttrs", + "job_ad_information_attrs", + "JOB_GLIDECLIENT_Name", + "JOB_GLIDEIN_ClusterId", + "JOB_GLIDEIN_CMSSite", + "JOBGLIDEIN_CMSSite", + "JOB_GLIDEIN_Entry_Name", + "JOB_GLIDEIN_Factory", + "JOB_GLIDEIN_Job_Max_Time", + "JOB_GLIDEIN_MaxMemMBs", + "JOB_GLIDEIN_Max_Walltime", + "JOB_GLIDEIN_Memory", + "JOB_GLIDEIN_Name", + "JOB_GLIDEIN_ProcId", + "JOB_GLIDEIN_Schedd", + "JOB_GLIDEIN_SEs", + "JOB_GLIDEIN_Site", + "JOB_GLIDEIN_SiteWMS", + "JOB_GLIDEIN_SiteWMS_JobId", + "JOB_GLIDEIN_SiteWMS_Queue", + "JOB_GLIDEIN_SiteWMS_Slot", + "JOB_GLIDEIN_ToDie", + "JOB_GLIDEIN_ToRetire", + "JobLeaseDuration", + "JobNotification", + "JOB_Site", + "Managed", + "MATCH_EXP_JOBGLIDEIN_CMSSite", + "MATCH_EXP_JOB_Site", + "MATCH_GLIDECLIENT_Name", + "MATCH_GLIDEIN_ClusterId", + "MATCH_GLIDEIN_CMSSite", + "MATCH_GLIDEIN_Entry_Name", + "MATCH_GLIDEIN_Factory", + "MATCH_GLIDEIN_Job_Max_Time", + "MATCH_GLIDEIN_MaxMemMBs", + "MATCH_GLIDEIN_Max_Walltime", + "MATCH_GLIDEIN_Name", + "MATCH_GLIDEIN_ProcId", + "MATCH_GLIDEIN_Schedd", + "MATCH_GLIDEIN_SEs", + "MATCH_GLIDEIN_Site", + "MATCH_GLIDEIN_SiteWMS", + "MATCH_GLIDEIN_SiteWMS_JobId", + "MATCH_GLIDEIN_SiteWMS_Queue", + "MATCH_GLIDEIN_SiteWMS_Slot", + "MATCH_Memory", + "MyType", + "NiceUser", + "NumCkpts", + "NumCkpts_RAW", + "OnExitHold", + "OnExitRemove", + "OrigMaxHosts", + "Out", + "PeriodicHold", + "PeriodicRelease", + "PeriodicRemove", + "Prev_DESIRED_Sites", + "PublicClaimId", + "RequestDisk", + "RequestMemory", + "ResidentSetSize", + "REQUIRES_LOCAL_DATA", + "RecentBlockReadKbytes", + "RecentBlockReads", + "RecentBlockWriteKbytes", + "RecentBlockWrites", + "RootDir", + "ServerTime", + "SpooledOutputFiles", + "StreamErr", + "StreamOut", + "TargetType", + "TransferIn", + "TransferInput", + "TransferOutput", + "UserLog", + "UserLogUseXML", + "use_x509userproxy", + "x509userproxy", + "x509UserProxyExpiration", + "WantCheckpoint", + "WantRemoteIO", + "WantRemoteSyscalls", + "BlockReadKbytes", + "BlockReads", + "BlockWriteKbytes", + "BlockWrites", + "LocalSysCpu", + "LeaveJobInQueue", + "LocalUserCpu", + "JobMachineAttrs", + "LastRejMatchReason", + "MachineAttrGLIDEIN_CMSSite0", + "CMS_ALLOW_OVERFLOW", + "LastPublicClaimId", + "LastRemotePool", + "Used_Gatekeeper", + "DESIRED_OpSyses", +} + +bool_vals = { + "CurrentStatusUnknown", + "CRAB_Publish", + "CRAB_SaveLogsFlag", + "CRAB_TransferOutputs", + "GlobusResubmit", + "TransferQueued", + "TransferringInput", + "HasSingularity", + "NiceUser", + "ExitBySignal", + "CMSSWDone", + "HasBeenRouted", + "HasBeenOverflowRouted", + "HasBeenTimingTuned", + "MachineAttrCUDAECCEnabled0", + "MachineAttrGLIDEIN_OVERLOAD_ENABLED0", +} + +# Fields to be kept in docs concerning running jobs +running_fields = { + "AccountingGroup", + "AutoClusterId", + "AffiliationInstitute", + "AffiliationCountry", + "BenchmarkJobDB12", + "Campaign", + "CMS_CampaignType", + "CMS_JobType", + "CMS_JobRetryCount", + "CMS_Pool", + "CMSGroups", + "CMSPrimaryDataTier", + "CMSSWKLumis", + "CMSSWWallHrs", + "CMSSWVersion", + "CMSSWMajorVersion", + "CMSSWReleaseSeries", + "CommittedCoreHr", + "CommittedTime", + "CoreHr", + "Country", + "CpuBadput", + "CpuEff", + "CpuEffOutlier", + "CpuEventRate", + "CpuTimeHr", + "CpuTimePerEvent", + "CRAB_AsyncDest", + "CRAB_DataBlock", + "CRAB_Id", + "CRAB_JobCount", + "CRAB_PostJobStatus", + "CRAB_Retry", + "CRAB_TaskCreationDate", + "CRAB_UserHN", + "CRAB_Workflow", + "CRAB_SplitAlgo", + "CMS_SubmissionTool", + "CMS_WMTool", + # "DataLocations", + "DESIRED_CMSDataset", + # "DESIRED_Sites", + "EnteredCurrentStatus", + "EventRate", + "FormattedCrabId", + "GlobalJobId", + "GLIDECLIENT_Name", + "GLIDEIN_ClusterId", + "GLIDEIN_Entry_Name", + "GLIDEIN_Factory", + "GLIDEIN_ProcId", + "HasSingularity", + "InputData", + "InputGB", + "JobPrio", + "JobCurrentStartDate", + "JobLastStartDate", + "JobUniverse", + "KEvents", + "MachineAttrCMSSF_ResourceType0", + "MachineAttrCMSSubSiteName0", + "MachineAttrGLIDEIN_OVERLOAD_ENABLED0", + "MaxWallTimeMins", + "MegaEvents", + "MemoryMB", + "OutputGB", + "QueueHrs", + "QDate", + "ReadTimeMins", + "RecordTime", + "RemoteHost", + "RequestCpus", + "RequestMemory", + "RequestMemory_Eval", + "ScheddName", + "Site", + "Status", + "TaskType", + "Tier", + "TimePerEvent", + "Type", + "WallClockHr", + "WMAgent_JobID", + "WMAgent_RequestName", + "WMAgent_SubTaskName", + "Workflow", + "DESIRED_Sites", + "DESIRED_SITES_Diff", + "DESIRED_SITES_Orig", + "EstimatedWallTimeMins", + "EstimatedWallTimeJobCount", + "PilotRestLifeTimeMins", + "LastRouted", + "LastTimingTuned", + "LPCRouted", + "MemoryUsage", + "PeriodicHoldReason", + "RouteType", + "HasBeenOverflowRouted", + "HasBeenRouted", + "HasBeenTimingTuned", +} + +status = { + 0: "Unexpanded", + 1: "Idle", + 2: "Running", + 3: "Removed", + 4: "Completed", + 5: "Held", + 6: "Error", +} + +universe = { + 1: "Standard", + 2: "Pipe", + 3: "Linda", + 4: "PVM", + 5: "Vanilla", + 6: "PVMD", + 7: "Scheduler", + 8: "MPI", + 9: "Grid", + 10: "Java", + 11: "Parallel", + 12: "Local", +} + +postjob_status_decode = { + "NOT RUN": "postProc", + "TRANSFERRING": "transferring", + "COOLOFF": "toRetry", + "FAILED": "failed", + "FINISHED": "finished", +} diff --git a/docker/spider-query-cronjob/test_collectors.json b/docker/spider-query-cronjob/test_collectors.json new file mode 100644 index 00000000..2490359a --- /dev/null +++ b/docker/spider-query-cronjob/test_collectors.json @@ -0,0 +1,7 @@ +{"Default":[ + "cmssrv623.fnal.gov:9620", + "cmsgwms-collector-tier0.cern.ch:9620", + "cmssrv276.fnal.gov", + "cmsgwms-collector-itb.cern.ch", + "vocms0840.cern.ch" +]} diff --git a/docker/spider-query-cronjob/uv.lock b/docker/spider-query-cronjob/uv.lock new file mode 100644 index 00000000..03e82757 --- /dev/null +++ b/docker/spider-query-cronjob/uv.lock @@ -0,0 +1,1730 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version >= '3.11' and python_full_version < '3.13'", + "python_full_version == '3.10.*'", + "python_full_version < '3.10'", +] + +[[package]] +name = "appnope" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, +] + +[[package]] +name = "asttokens" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "certifi" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, +] + +[[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/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/08ed5a43f2996a16b462f64a7055c6e962803534924b9b2f1371d8c00b7b/cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf", size = 184288, upload-time = "2025-09-08T23:23:48.404Z" }, + { url = "https://files.pythonhosted.org/packages/3d/de/38d9726324e127f727b4ecc376bc85e505bfe61ef130eaf3f290c6847dd4/cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7", size = 180509, upload-time = "2025-09-08T23:23:49.73Z" }, + { url = "https://files.pythonhosted.org/packages/9b/13/c92e36358fbcc39cf0962e83223c9522154ee8630e1df7c0b3a39a8124e2/cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c", size = 208813, upload-time = "2025-09-08T23:23:51.263Z" }, + { url = "https://files.pythonhosted.org/packages/15/12/a7a79bd0df4c3bff744b2d7e52cc1b68d5e7e427b384252c42366dc1ecbc/cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165", size = 216498, upload-time = "2025-09-08T23:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/5c51c1c7600bdd7ed9a24a203ec255dccdd0ebf4527f7b922a0bde2fb6ed/cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534", size = 203243, upload-time = "2025-09-08T23:23:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/32/f2/81b63e288295928739d715d00952c8c6034cb6c6a516b17d37e0c8be5600/cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f", size = 203158, upload-time = "2025-09-08T23:23:55.169Z" }, + { url = "https://files.pythonhosted.org/packages/1f/74/cc4096ce66f5939042ae094e2e96f53426a979864aa1f96a621ad128be27/cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63", size = 216548, upload-time = "2025-09-08T23:23:56.506Z" }, + { url = "https://files.pythonhosted.org/packages/e8/be/f6424d1dc46b1091ffcc8964fa7c0ab0cd36839dd2761b49c90481a6ba1b/cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2", size = 218897, upload-time = "2025-09-08T23:23:57.825Z" }, + { url = "https://files.pythonhosted.org/packages/f7/e0/dda537c2309817edf60109e39265f24f24aa7f050767e22c98c53fe7f48b/cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65", size = 211249, upload-time = "2025-09-08T23:23:59.139Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e7/7c769804eb75e4c4b35e658dba01de1640a351a9653c3d49ca89d16ccc91/cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322", size = 218041, upload-time = "2025-09-08T23:24:00.496Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d9/6218d78f920dcd7507fc16a766b5ef8f3b913cc7aa938e7fc80b9978d089/cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a", size = 172138, upload-time = "2025-09-08T23:24:01.7Z" }, + { url = "https://files.pythonhosted.org/packages/54/8f/a1e836f82d8e32a97e6b29cc8f641779181ac7363734f12df27db803ebda/cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9", size = 182794, upload-time = "2025-09-08T23:24:02.943Z" }, +] + +[[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/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { 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/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/46/7c/0c4760bccf082737ca7ab84a4c2034fcc06b1f21cf3032ea98bd6feb1725/charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", size = 209609, upload-time = "2025-10-14T04:42:10.922Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a4/69719daef2f3d7f1819de60c9a6be981b8eeead7542d5ec4440f3c80e111/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", size = 149029, upload-time = "2025-10-14T04:42:12.38Z" }, + { url = "https://files.pythonhosted.org/packages/e6/21/8d4e1d6c1e6070d3672908b8e4533a71b5b53e71d16828cc24d0efec564c/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608", size = 144580, upload-time = "2025-10-14T04:42:13.549Z" }, + { url = "https://files.pythonhosted.org/packages/a7/0a/a616d001b3f25647a9068e0b9199f697ce507ec898cacb06a0d5a1617c99/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", size = 162340, upload-time = "2025-10-14T04:42:14.892Z" }, + { url = "https://files.pythonhosted.org/packages/85/93/060b52deb249a5450460e0585c88a904a83aec474ab8e7aba787f45e79f2/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", size = 159619, upload-time = "2025-10-14T04:42:16.676Z" }, + { url = "https://files.pythonhosted.org/packages/dd/21/0274deb1cc0632cd587a9a0ec6b4674d9108e461cb4cd40d457adaeb0564/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", size = 153980, upload-time = "2025-10-14T04:42:17.917Z" }, + { url = "https://files.pythonhosted.org/packages/28/2b/e3d7d982858dccc11b31906976323d790dded2017a0572f093ff982d692f/charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", size = 152174, upload-time = "2025-10-14T04:42:19.018Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ff/4a269f8e35f1e58b2df52c131a1fa019acb7ef3f8697b7d464b07e9b492d/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", size = 151666, upload-time = "2025-10-14T04:42:20.171Z" }, + { url = "https://files.pythonhosted.org/packages/da/c9/ec39870f0b330d58486001dd8e532c6b9a905f5765f58a6f8204926b4a93/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", size = 145550, upload-time = "2025-10-14T04:42:21.324Z" }, + { url = "https://files.pythonhosted.org/packages/75/8f/d186ab99e40e0ed9f82f033d6e49001701c81244d01905dd4a6924191a30/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", size = 163721, upload-time = "2025-10-14T04:42:22.46Z" }, + { url = "https://files.pythonhosted.org/packages/96/b1/6047663b9744df26a7e479ac1e77af7134b1fcf9026243bb48ee2d18810f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", size = 152127, upload-time = "2025-10-14T04:42:23.712Z" }, + { url = "https://files.pythonhosted.org/packages/59/78/e5a6eac9179f24f704d1be67d08704c3c6ab9f00963963524be27c18ed87/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", size = 161175, upload-time = "2025-10-14T04:42:24.87Z" }, + { url = "https://files.pythonhosted.org/packages/e5/43/0e626e42d54dd2f8dd6fc5e1c5ff00f05fbca17cb699bedead2cae69c62f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", size = 155375, upload-time = "2025-10-14T04:42:27.246Z" }, + { url = "https://files.pythonhosted.org/packages/e9/91/d9615bf2e06f35e4997616ff31248c3657ed649c5ab9d35ea12fce54e380/charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", size = 99692, upload-time = "2025-10-14T04:42:28.425Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a9/6c040053909d9d1ef4fcab45fddec083aedc9052c10078339b47c8573ea8/charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", size = 107192, upload-time = "2025-10-14T04:42:29.482Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c6/4fa536b2c0cd3edfb7ccf8469fa0f363ea67b7213a842b90909ca33dd851/charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", size = 100220, upload-time = "2025-10-14T04:42:30.632Z" }, + { 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 = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version >= '3.11' and python_full_version < '3.13'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "cmsmonitoring" +version = "0.6.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "jsonschema" }, + { name = "nats-py" }, + { name = "setuptools" }, + { name = "stomp-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/16/a97af06bd4f293c7280afa3a1f6706e06c766803376bd33f9a327e9efdfb/cmsmonitoring-0.6.13.tar.gz", hash = "sha256:b3350bc0f2228e5aaf0c500358391ad53869acf1cbaeba51cc506aebbc9a6016", size = 33488, upload-time = "2025-07-09T17:19:27.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/f6/96d3ef07da6dc7c4ba84822b8815482b76f143722bb729527974cfdfdcce/cmsmonitoring-0.6.13-py3-none-any.whl", hash = "sha256:61b60848994b99e67f09a7545a173b31bd97e5f94578b492c7a140b594106fc0", size = 41637, upload-time = "2025-07-09T17:19:26.322Z" }, +] + +[[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 = "comm" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, +] + +[[package]] +name = "debugpy" +version = "1.8.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/ad/71e708ff4ca377c4230530d6a7aa7992592648c122a2cd2b321cf8b35a76/debugpy-1.8.17.tar.gz", hash = "sha256:fd723b47a8c08892b1a16b2c6239a8b96637c62a59b94bb5dab4bac592a58a8e", size = 1644129, upload-time = "2025-09-17T16:33:20.633Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/36/b57c6e818d909f6e59c0182252921cf435e0951126a97e11de37e72ab5e1/debugpy-1.8.17-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:c41d2ce8bbaddcc0009cc73f65318eedfa3dbc88a8298081deb05389f1ab5542", size = 2098021, upload-time = "2025-09-17T16:33:22.556Z" }, + { url = "https://files.pythonhosted.org/packages/be/01/0363c7efdd1e9febd090bb13cee4fb1057215b157b2979a4ca5ccb678217/debugpy-1.8.17-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:1440fd514e1b815edd5861ca394786f90eb24960eb26d6f7200994333b1d79e3", size = 3087399, upload-time = "2025-09-17T16:33:24.292Z" }, + { url = "https://files.pythonhosted.org/packages/79/bc/4a984729674aa9a84856650438b9665f9a1d5a748804ac6f37932ce0d4aa/debugpy-1.8.17-cp310-cp310-win32.whl", hash = "sha256:3a32c0af575749083d7492dc79f6ab69f21b2d2ad4cd977a958a07d5865316e4", size = 5230292, upload-time = "2025-09-17T16:33:26.137Z" }, + { url = "https://files.pythonhosted.org/packages/5d/19/2b9b3092d0cf81a5aa10c86271999453030af354d1a5a7d6e34c574515d7/debugpy-1.8.17-cp310-cp310-win_amd64.whl", hash = "sha256:a3aad0537cf4d9c1996434be68c6c9a6d233ac6f76c2a482c7803295b4e4f99a", size = 5261885, upload-time = "2025-09-17T16:33:27.592Z" }, + { url = "https://files.pythonhosted.org/packages/d8/53/3af72b5c159278c4a0cf4cffa518675a0e73bdb7d1cac0239b815502d2ce/debugpy-1.8.17-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:d3fce3f0e3de262a3b67e69916d001f3e767661c6e1ee42553009d445d1cd840", size = 2207154, upload-time = "2025-09-17T16:33:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/8f/6d/204f407df45600e2245b4a39860ed4ba32552330a0b3f5f160ae4cc30072/debugpy-1.8.17-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:c6bdf134457ae0cac6fb68205776be635d31174eeac9541e1d0c062165c6461f", size = 3170322, upload-time = "2025-09-17T16:33:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/f2/13/1b8f87d39cf83c6b713de2620c31205299e6065622e7dd37aff4808dd410/debugpy-1.8.17-cp311-cp311-win32.whl", hash = "sha256:e79a195f9e059edfe5d8bf6f3749b2599452d3e9380484cd261f6b7cd2c7c4da", size = 5155078, upload-time = "2025-09-17T16:33:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c5/c012c60a2922cc91caa9675d0ddfbb14ba59e1e36228355f41cab6483469/debugpy-1.8.17-cp311-cp311-win_amd64.whl", hash = "sha256:b532282ad4eca958b1b2d7dbcb2b7218e02cb934165859b918e3b6ba7772d3f4", size = 5179011, upload-time = "2025-09-17T16:33:35.711Z" }, + { url = "https://files.pythonhosted.org/packages/08/2b/9d8e65beb2751876c82e1aceb32f328c43ec872711fa80257c7674f45650/debugpy-1.8.17-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:f14467edef672195c6f6b8e27ce5005313cb5d03c9239059bc7182b60c176e2d", size = 2549522, upload-time = "2025-09-17T16:33:38.466Z" }, + { url = "https://files.pythonhosted.org/packages/b4/78/eb0d77f02971c05fca0eb7465b18058ba84bd957062f5eec82f941ac792a/debugpy-1.8.17-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:24693179ef9dfa20dca8605905a42b392be56d410c333af82f1c5dff807a64cc", size = 4309417, upload-time = "2025-09-17T16:33:41.299Z" }, + { url = "https://files.pythonhosted.org/packages/37/42/c40f1d8cc1fed1e75ea54298a382395b8b937d923fcf41ab0797a554f555/debugpy-1.8.17-cp312-cp312-win32.whl", hash = "sha256:6a4e9dacf2cbb60d2514ff7b04b4534b0139facbf2abdffe0639ddb6088e59cf", size = 5277130, upload-time = "2025-09-17T16:33:43.554Z" }, + { url = "https://files.pythonhosted.org/packages/72/22/84263b205baad32b81b36eac076de0cdbe09fe2d0637f5b32243dc7c925b/debugpy-1.8.17-cp312-cp312-win_amd64.whl", hash = "sha256:e8f8f61c518952fb15f74a302e068b48d9c4691768ade433e4adeea961993464", size = 5319053, upload-time = "2025-09-17T16:33:53.033Z" }, + { url = "https://files.pythonhosted.org/packages/50/76/597e5cb97d026274ba297af8d89138dfd9e695767ba0e0895edb20963f40/debugpy-1.8.17-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:857c1dd5d70042502aef1c6d1c2801211f3ea7e56f75e9c335f434afb403e464", size = 2538386, upload-time = "2025-09-17T16:33:54.594Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/ce5c34fcdfec493701f9d1532dba95b21b2f6394147234dce21160bd923f/debugpy-1.8.17-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:3bea3b0b12f3946e098cce9b43c3c46e317b567f79570c3f43f0b96d00788088", size = 4292100, upload-time = "2025-09-17T16:33:56.353Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/7873cf2146577ef71d2a20bf553f12df865922a6f87b9e8ee1df04f01785/debugpy-1.8.17-cp313-cp313-win32.whl", hash = "sha256:e34ee844c2f17b18556b5bbe59e1e2ff4e86a00282d2a46edab73fd7f18f4a83", size = 5277002, upload-time = "2025-09-17T16:33:58.231Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/18c79a1cee5ff539a94ec4aa290c1c069a5580fd5cfd2fb2e282f8e905da/debugpy-1.8.17-cp313-cp313-win_amd64.whl", hash = "sha256:6c5cd6f009ad4fca8e33e5238210dc1e5f42db07d4b6ab21ac7ffa904a196420", size = 5319047, upload-time = "2025-09-17T16:34:00.586Z" }, + { url = "https://files.pythonhosted.org/packages/de/45/115d55b2a9da6de812696064ceb505c31e952c5d89c4ed1d9bb983deec34/debugpy-1.8.17-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:045290c010bcd2d82bc97aa2daf6837443cd52f6328592698809b4549babcee1", size = 2536899, upload-time = "2025-09-17T16:34:02.657Z" }, + { url = "https://files.pythonhosted.org/packages/5a/73/2aa00c7f1f06e997ef57dc9b23d61a92120bec1437a012afb6d176585197/debugpy-1.8.17-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:b69b6bd9dba6a03632534cdf67c760625760a215ae289f7489a452af1031fe1f", size = 4268254, upload-time = "2025-09-17T16:34:04.486Z" }, + { url = "https://files.pythonhosted.org/packages/86/b5/ed3e65c63c68a6634e3ba04bd10255c8e46ec16ebed7d1c79e4816d8a760/debugpy-1.8.17-cp314-cp314-win32.whl", hash = "sha256:5c59b74aa5630f3a5194467100c3b3d1c77898f9ab27e3f7dc5d40fc2f122670", size = 5277203, upload-time = "2025-09-17T16:34:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/b0/26/394276b71c7538445f29e792f589ab7379ae70fd26ff5577dfde71158e96/debugpy-1.8.17-cp314-cp314-win_amd64.whl", hash = "sha256:893cba7bb0f55161de4365584b025f7064e1f88913551bcd23be3260b231429c", size = 5318493, upload-time = "2025-09-17T16:34:08.483Z" }, + { url = "https://files.pythonhosted.org/packages/16/ee/0e9a08878f1b525f85c4e47723ea1f17b1bad69672c84fa910210604e3f8/debugpy-1.8.17-cp39-cp39-macosx_15_0_x86_64.whl", hash = "sha256:f2ac8055a0c4a09b30b931100996ba49ef334c6947e7ae365cdd870416d7513e", size = 2099309, upload-time = "2025-09-17T16:34:17.935Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b5/0327b27efd8826ca92a256a3a250e80ccad6a834b4d12bd9cbd491f2da03/debugpy-1.8.17-cp39-cp39-manylinux_2_34_x86_64.whl", hash = "sha256:eaa85bce251feca8e4c87ce3b954aba84b8c645b90f0e6a515c00394a9f5c0e7", size = 3080100, upload-time = "2025-09-17T16:34:19.885Z" }, + { url = "https://files.pythonhosted.org/packages/0f/f0/2e210fa8884d2ab452fa31ffd1402e13010eaacfa67063d0565d97ac9e0e/debugpy-1.8.17-cp39-cp39-win32.whl", hash = "sha256:b13eea5587e44f27f6c48588b5ad56dcb74a4f3a5f89250443c94587f3eb2ea1", size = 5231016, upload-time = "2025-09-17T16:34:21.887Z" }, + { url = "https://files.pythonhosted.org/packages/d6/9b/6a45fb1553d09b618c9441bcbbf72b651246b83b5618b2f95c0e4cf1b8bd/debugpy-1.8.17-cp39-cp39-win_amd64.whl", hash = "sha256:bb1bbf92317e1f35afcf3ef0450219efb3afe00be79d8664b250ac0933b9015f", size = 5262778, upload-time = "2025-09-17T16:34:24.026Z" }, + { url = "https://files.pythonhosted.org/packages/b0/d0/89247ec250369fc76db477720a26b2fce7ba079ff1380e4ab4529d2fe233/debugpy-1.8.17-py2.py3-none-any.whl", hash = "sha256:60c7dca6571efe660ccb7a9508d73ca14b8796c4ed484c2002abba714226cfef", size = 5283210, upload-time = "2025-09-17T16:34:25.835Z" }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, +] + +[[package]] +name = "docopt" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/55/8f8cab2afd404cf578136ef2cc5dfb50baa1761b68c9da1fb1e4eed343c9/docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491", size = 25901, upload-time = "2014-06-16T11:18:57.406Z" } + +[[package]] +name = "events" +version = "0.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/ed/e47dec0626edd468c84c04d97769e7ab4ea6457b7f54dcb3f72b17fcd876/Events-0.5-py3-none-any.whl", hash = "sha256:a7286af378ba3e46640ac9825156c93bdba7502174dd696090fdfcd4d80a1abd", size = 6758, upload-time = "2023-07-31T08:23:13.645Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + +[[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]] +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/88/17/ff4795dc9a34b6aee6ec379f1b66438a3789cd1315aac0cbab60d92f74b3/grpcio-1.76.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:65a20de41e85648e00305c1bb09a3598f840422e522277641145a32d42dcefcc", size = 5840037, upload-time = "2025-10-21T16:20:25.069Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ff/35f9b96e3fa2f12e1dcd58a4513a2e2294a001d64dec81677361b7040c9a/grpcio-1.76.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:40ad3afe81676fd9ec6d9d406eda00933f218038433980aa19d401490e46ecde", size = 11836482, upload-time = "2025-10-21T16:20:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1c/8374990f9545e99462caacea5413ed783014b3b66ace49e35c533f07507b/grpcio-1.76.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:035d90bc79eaa4bed83f524331d55e35820725c9fbb00ffa1904d5550ed7ede3", size = 6407178, upload-time = "2025-10-21T16:20:32.733Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/36fd7d7c75a6c12542c90a6d647a27935a1ecaad03e0ffdb7c42db6b04d2/grpcio-1.76.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4215d3a102bd95e2e11b5395c78562967959824156af11fa93d18fdd18050990", size = 7075684, upload-time = "2025-10-21T16:20:35.435Z" }, + { url = "https://files.pythonhosted.org/packages/38/f7/e3cdb252492278e004722306c5a8935eae91e64ea11f0af3437a7de2e2b7/grpcio-1.76.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:49ce47231818806067aea3324d4bf13825b658ad662d3b25fada0bdad9b8a6af", size = 6611133, upload-time = "2025-10-21T16:20:37.541Z" }, + { url = "https://files.pythonhosted.org/packages/7e/20/340db7af162ccd20a0893b5f3c4a5d676af7b71105517e62279b5b61d95a/grpcio-1.76.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8cc3309d8e08fd79089e13ed4819d0af72aa935dd8f435a195fd152796752ff2", size = 7195507, upload-time = "2025-10-21T16:20:39.643Z" }, + { url = "https://files.pythonhosted.org/packages/10/f0/b2160addc1487bd8fa4810857a27132fb4ce35c1b330c2f3ac45d697b106/grpcio-1.76.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:971fd5a1d6e62e00d945423a567e42eb1fa678ba89072832185ca836a94daaa6", size = 8160651, upload-time = "2025-10-21T16:20:42.492Z" }, + { url = "https://files.pythonhosted.org/packages/2c/2c/ac6f98aa113c6ef111b3f347854e99ebb7fb9d8f7bb3af1491d438f62af4/grpcio-1.76.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d9adda641db7207e800a7f089068f6f645959f2df27e870ee81d44701dd9db3", size = 7620568, upload-time = "2025-10-21T16:20:45.995Z" }, + { url = "https://files.pythonhosted.org/packages/90/84/7852f7e087285e3ac17a2703bc4129fafee52d77c6c82af97d905566857e/grpcio-1.76.0-cp310-cp310-win32.whl", hash = "sha256:063065249d9e7e0782d03d2bca50787f53bd0fb89a67de9a7b521c4a01f1989b", size = 3998879, upload-time = "2025-10-21T16:20:48.592Z" }, + { url = "https://files.pythonhosted.org/packages/10/30/d3d2adcbb6dd3ff59d6ac3df6ef830e02b437fb5c90990429fd180e52f30/grpcio-1.76.0-cp310-cp310-win_amd64.whl", hash = "sha256:a6ae758eb08088d36812dd5d9af7a9859c05b1e0f714470ea243694b49278e7b", size = 4706892, upload-time = "2025-10-21T16:20:50.697Z" }, + { url = "https://files.pythonhosted.org/packages/a0/00/8163a1beeb6971f66b4bbe6ac9457b97948beba8dd2fc8e1281dce7f79ec/grpcio-1.76.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2e1743fbd7f5fa713a1b0a8ac8ebabf0ec980b5d8809ec358d488e273b9cf02a", size = 5843567, upload-time = "2025-10-21T16:20:52.829Z" }, + { url = "https://files.pythonhosted.org/packages/10/c1/934202f5cf335e6d852530ce14ddb0fef21be612ba9ecbbcbd4d748ca32d/grpcio-1.76.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a8c2cf1209497cf659a667d7dea88985e834c24b7c3b605e6254cbb5076d985c", size = 11848017, upload-time = "2025-10-21T16:20:56.705Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/8dec16b1863d74af6eb3543928600ec2195af49ca58b16334972f6775663/grpcio-1.76.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08caea849a9d3c71a542827d6df9d5a69067b0a1efbea8a855633ff5d9571465", size = 6412027, upload-time = "2025-10-21T16:20:59.3Z" }, + { url = "https://files.pythonhosted.org/packages/d7/64/7b9e6e7ab910bea9d46f2c090380bab274a0b91fb0a2fe9b0cd399fffa12/grpcio-1.76.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f0e34c2079d47ae9f6188211db9e777c619a21d4faba6977774e8fa43b085e48", size = 7075913, upload-time = "2025-10-21T16:21:01.645Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/093c46e9546073cefa789bd76d44c5cb2abc824ca62af0c18be590ff13ba/grpcio-1.76.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8843114c0cfce61b40ad48df65abcfc00d4dba82eae8718fab5352390848c5da", size = 6615417, upload-time = "2025-10-21T16:21:03.844Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b6/5709a3a68500a9c03da6fb71740dcdd5ef245e39266461a03f31a57036d8/grpcio-1.76.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8eddfb4d203a237da6f3cc8a540dad0517d274b5a1e9e636fd8d2c79b5c1d397", size = 7199683, upload-time = "2025-10-21T16:21:06.195Z" }, + { url = "https://files.pythonhosted.org/packages/91/d3/4b1f2bf16ed52ce0b508161df3a2d186e4935379a159a834cb4a7d687429/grpcio-1.76.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:32483fe2aab2c3794101c2a159070584e5db11d0aa091b2c0ea9c4fc43d0d749", size = 8163109, upload-time = "2025-10-21T16:21:08.498Z" }, + { url = "https://files.pythonhosted.org/packages/5c/61/d9043f95f5f4cf085ac5dd6137b469d41befb04bd80280952ffa2a4c3f12/grpcio-1.76.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dcfe41187da8992c5f40aa8c5ec086fa3672834d2be57a32384c08d5a05b4c00", size = 7626676, upload-time = "2025-10-21T16:21:10.693Z" }, + { url = "https://files.pythonhosted.org/packages/36/95/fd9a5152ca02d8881e4dd419cdd790e11805979f499a2e5b96488b85cf27/grpcio-1.76.0-cp311-cp311-win32.whl", hash = "sha256:2107b0c024d1b35f4083f11245c0e23846ae64d02f40b2b226684840260ed054", size = 3997688, upload-time = "2025-10-21T16:21:12.746Z" }, + { url = "https://files.pythonhosted.org/packages/60/9c/5c359c8d4c9176cfa3c61ecd4efe5affe1f38d9bae81e81ac7186b4c9cc8/grpcio-1.76.0-cp311-cp311-win_amd64.whl", hash = "sha256:522175aba7af9113c48ec10cc471b9b9bd4f6ceb36aeb4544a8e2c80ed9d252d", size = 4709315, upload-time = "2025-10-21T16:21:15.26Z" }, + { url = "https://files.pythonhosted.org/packages/bf/05/8e29121994b8d959ffa0afd28996d452f291b48cfc0875619de0bde2c50c/grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8", size = 5799718, upload-time = "2025-10-21T16:21:17.939Z" }, + { url = "https://files.pythonhosted.org/packages/d9/75/11d0e66b3cdf998c996489581bdad8900db79ebd83513e45c19548f1cba4/grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280", size = 11825627, upload-time = "2025-10-21T16:21:20.466Z" }, + { url = "https://files.pythonhosted.org/packages/28/50/2f0aa0498bc188048f5d9504dcc5c2c24f2eb1a9337cd0fa09a61a2e75f0/grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4", size = 6359167, upload-time = "2025-10-21T16:21:23.122Z" }, + { url = "https://files.pythonhosted.org/packages/66/e5/bbf0bb97d29ede1d59d6588af40018cfc345b17ce979b7b45424628dc8bb/grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11", size = 7044267, upload-time = "2025-10-21T16:21:25.995Z" }, + { url = "https://files.pythonhosted.org/packages/f5/86/f6ec2164f743d9609691115ae8ece098c76b894ebe4f7c94a655c6b03e98/grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6", size = 6573963, upload-time = "2025-10-21T16:21:28.631Z" }, + { url = "https://files.pythonhosted.org/packages/60/bc/8d9d0d8505feccfdf38a766d262c71e73639c165b311c9457208b56d92ae/grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8", size = 7164484, upload-time = "2025-10-21T16:21:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/67/e6/5d6c2fc10b95edf6df9b8f19cf10a34263b7fd48493936fffd5085521292/grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980", size = 8127777, upload-time = "2025-10-21T16:21:33.577Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c8/dce8ff21c86abe025efe304d9e31fdb0deaaa3b502b6a78141080f206da0/grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882", size = 7594014, upload-time = "2025-10-21T16:21:41.882Z" }, + { url = "https://files.pythonhosted.org/packages/e0/42/ad28191ebf983a5d0ecef90bab66baa5a6b18f2bfdef9d0a63b1973d9f75/grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958", size = 3984750, upload-time = "2025-10-21T16:21:44.006Z" }, + { url = "https://files.pythonhosted.org/packages/9e/00/7bd478cbb851c04a48baccaa49b75abaa8e4122f7d86da797500cccdd771/grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347", size = 4704003, upload-time = "2025-10-21T16:21:46.244Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/b4/46/39adac80de49d678e6e073b70204091e76631e03e94928b9ea4ecf0f6e0e/grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62", size = 5808417, upload-time = "2025-10-21T16:22:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f5/a4531f7fb8b4e2a60b94e39d5d924469b7a6988176b3422487be61fe2998/grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd", size = 11828219, upload-time = "2025-10-21T16:22:17.954Z" }, + { url = "https://files.pythonhosted.org/packages/4b/1c/de55d868ed7a8bd6acc6b1d6ddc4aa36d07a9f31d33c912c804adb1b971b/grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc", size = 6367826, upload-time = "2025-10-21T16:22:20.721Z" }, + { url = "https://files.pythonhosted.org/packages/59/64/99e44c02b5adb0ad13ab3adc89cb33cb54bfa90c74770f2607eea629b86f/grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a", size = 7049550, upload-time = "2025-10-21T16:22:23.637Z" }, + { url = "https://files.pythonhosted.org/packages/43/28/40a5be3f9a86949b83e7d6a2ad6011d993cbe9b6bd27bea881f61c7788b6/grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba", size = 6575564, upload-time = "2025-10-21T16:22:26.016Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a9/1be18e6055b64467440208a8559afac243c66a8b904213af6f392dc2212f/grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09", size = 7176236, upload-time = "2025-10-21T16:22:28.362Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/dba05d3fcc151ce6e81327541d2cc8394f442f6b350fead67401661bf041/grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc", size = 8125795, upload-time = "2025-10-21T16:22:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/4a/45/122df922d05655f63930cf42c9e3f72ba20aadb26c100ee105cad4ce4257/grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc", size = 7592214, upload-time = "2025-10-21T16:22:33.831Z" }, + { url = "https://files.pythonhosted.org/packages/4a/6e/0b899b7f6b66e5af39e377055fb4a6675c9ee28431df5708139df2e93233/grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e", size = 4062961, upload-time = "2025-10-21T16:22:36.468Z" }, + { url = "https://files.pythonhosted.org/packages/19/41/0b430b01a2eb38ee887f88c1f07644a1df8e289353b78e82b37ef988fb64/grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e", size = 4834462, upload-time = "2025-10-21T16:22:39.772Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d5/301e71c7d22a5c7aabf1953dd1106987bd47f883377d528355f898a850f2/grpcio-1.76.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:8ebe63ee5f8fa4296b1b8cfc743f870d10e902ca18afc65c68cf46fd39bb0783", size = 5840371, upload-time = "2025-10-21T16:22:42.468Z" }, + { url = "https://files.pythonhosted.org/packages/00/55/e3181adccff8808301dd9214b5e03c6db5a404b5ae8a6ec5768a5a65ed63/grpcio-1.76.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:3bf0f392c0b806905ed174dcd8bdd5e418a40d5567a05615a030a5aeddea692d", size = 11840384, upload-time = "2025-10-21T16:22:45.508Z" }, + { url = "https://files.pythonhosted.org/packages/65/36/db1dfe943bce7180f5b6d9be564366ca1024a005e914a1f10212c24a840b/grpcio-1.76.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0b7604868b38c1bfd5cf72d768aedd7db41d78cb6a4a18585e33fb0f9f2363fd", size = 6408765, upload-time = "2025-10-21T16:22:48.761Z" }, + { url = "https://files.pythonhosted.org/packages/1e/79/a8452764aa4b5ca30a970e514ec2fc5cf75451571793f6b276b6807f67dc/grpcio-1.76.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e6d1db20594d9daba22f90da738b1a0441a7427552cc6e2e3d1297aeddc00378", size = 7076220, upload-time = "2025-10-21T16:22:51.546Z" }, + { url = "https://files.pythonhosted.org/packages/e0/61/4cca38c4e7bb3ac5a1e0be6cf700a4dd85c61cbd8a9c5e076c224967084e/grpcio-1.76.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d099566accf23d21037f18a2a63d323075bebace807742e4b0ac210971d4dd70", size = 6610195, upload-time = "2025-10-21T16:22:54.688Z" }, + { url = "https://files.pythonhosted.org/packages/54/3d/3f8bfae264c22c95fa702c35aa2a8105b754b4ace049c66a8b2230c97671/grpcio-1.76.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ebea5cc3aa8ea72e04df9913492f9a96d9348db876f9dda3ad729cfedf7ac416", size = 7193343, upload-time = "2025-10-21T16:22:57.434Z" }, + { url = "https://files.pythonhosted.org/packages/d1/cd/89f9254782b6cd94aa7c93fde370862877113b7189fb49900eaf9a706c82/grpcio-1.76.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0c37db8606c258e2ee0c56b78c62fc9dee0e901b5dbdcf816c2dd4ad652b8b0c", size = 8161922, upload-time = "2025-10-21T16:23:00.135Z" }, + { url = "https://files.pythonhosted.org/packages/af/e0/99eb899d7cb9c676afea70ab6d02a72a9e6ce24d0300f625773fafe6d547/grpcio-1.76.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ebebf83299b0cb1721a8859ea98f3a77811e35dce7609c5c963b9ad90728f886", size = 7617951, upload-time = "2025-10-21T16:23:03.68Z" }, + { url = "https://files.pythonhosted.org/packages/79/26/dca1b2bfaa9981cc28fa995730c80eedb0b86c912c30d1b676f08232e6ab/grpcio-1.76.0-cp39-cp39-win32.whl", hash = "sha256:0aaa82d0813fd4c8e589fac9b65d7dd88702555f702fb10417f96e2a2a6d4c0f", size = 3999306, upload-time = "2025-10-21T16:23:06.187Z" }, + { url = "https://files.pythonhosted.org/packages/de/d1/fb90564a981eedd3cd87dc6bfd7c249e8a515cfad1ed8e9af73be223cd3b/grpcio-1.76.0-cp39-cp39-win_amd64.whl", hash = "sha256:acab0277c40eff7143c2323190ea57b9ee5fd353d8190ee9652369fae735668a", size = 4708771, upload-time = "2025-10-21T16:23:08.902Z" }, +] + +[[package]] +name = "htcondor" +version = "23.0.28" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/42/5fad32c12a1fe2d44530255d34333678479e3c45f0429cd584591ac81c99/htcondor-23.0.28-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b76f0abf1b72d3a961e508d34398c5eb795112eb99c20026ab3bfaf9b61e744", size = 54753447, upload-time = "2025-08-21T18:44:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/9f/dd/960e321729c041dd4c99bce39b6ee3c7bcc50e1df2ce70ae5cdf12ddfffd/htcondor-23.0.28-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f62f7e8287270cb70b20079602c97318e7d6cacb32a3b3c9e749613d2371c91e", size = 54767420, upload-time = "2025-08-21T18:44:56.305Z" }, + { url = "https://files.pythonhosted.org/packages/98/70/71f4b30bcc743deec71c0e50acb7fa164c8ea8cc2b4c50c0b0087b594597/htcondor-23.0.28-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b823eba20bafa26db220bead2d9fc7996056b2a8fb4b84a94f604a56f769a2b", size = 54869798, upload-time = "2025-08-21T18:44:59.107Z" }, + { url = "https://files.pythonhosted.org/packages/de/ac/4690c05ac4dc379521b706a7749c80fdf607358464c487c8d131dfbaffc5/htcondor-23.0.28-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3a247647d40f03acfcd30e883e1a79b5ae4dbf248b2854ab493f6a003821e32", size = 54745217, upload-time = "2025-08-21T18:45:08.213Z" }, +] + +[[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.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + +[[package]] +name = "ipykernel" +version = "6.31.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "appnope", marker = "python_full_version < '3.10' and sys_platform == 'darwin'" }, + { name = "comm", marker = "python_full_version < '3.10'" }, + { name = "debugpy", marker = "python_full_version < '3.10'" }, + { name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "jupyter-client", marker = "python_full_version < '3.10'" }, + { name = "jupyter-core", version = "5.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "matplotlib-inline", marker = "python_full_version < '3.10'" }, + { name = "nest-asyncio", marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "psutil", marker = "python_full_version < '3.10'" }, + { name = "pyzmq", marker = "python_full_version < '3.10'" }, + { name = "tornado", marker = "python_full_version < '3.10'" }, + { name = "traitlets", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/1d/d5ba6edbfe6fae4c3105bca3a9c889563cc752c7f2de45e333164c7f4846/ipykernel-6.31.0.tar.gz", hash = "sha256:2372ce8bc1ff4f34e58cafed3a0feb2194b91fc7cad0fc72e79e47b45ee9e8f6", size = 167493, upload-time = "2025-10-20T11:42:39.948Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl", hash = "sha256:abe5386f6ced727a70e0eb0cf1da801fa7c5fa6ff82147747d5a0406cd8c94af", size = 117003, upload-time = "2025-10-20T11:42:37.502Z" }, +] + +[[package]] +name = "ipykernel" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version >= '3.11' and python_full_version < '3.13'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "appnope", marker = "python_full_version >= '3.10' and sys_platform == 'darwin'" }, + { name = "comm", marker = "python_full_version >= '3.10'" }, + { name = "debugpy", marker = "python_full_version >= '3.10'" }, + { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "ipython", version = "9.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "jupyter-client", marker = "python_full_version >= '3.10'" }, + { name = "jupyter-core", version = "5.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "matplotlib-inline", marker = "python_full_version >= '3.10'" }, + { name = "nest-asyncio", marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "psutil", marker = "python_full_version >= '3.10'" }, + { name = "pyzmq", marker = "python_full_version >= '3.10'" }, + { name = "tornado", marker = "python_full_version >= '3.10'" }, + { name = "traitlets", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/a4/4948be6eb88628505b83a1f2f40d90254cab66abf2043b3c40fa07dfce0f/ipykernel-7.1.0.tar.gz", hash = "sha256:58a3fc88533d5930c3546dc7eac66c6d288acde4f801e2001e65edc5dc9cf0db", size = 174579, upload-time = "2025-10-27T09:46:39.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/17/20c2552266728ceba271967b87919664ecc0e33efca29c3efc6baf88c5f9/ipykernel-7.1.0-py3-none-any.whl", hash = "sha256:763b5ec6c5b7776f6a8d7ce09b267693b4e5ce75cb50ae696aaefb3c85e1ea4c", size = 117968, upload-time = "2025-10-27T09:46:37.805Z" }, +] + +[[package]] +name = "ipython" +version = "8.18.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version < '3.10'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, + { name = "jedi", marker = "python_full_version < '3.10'" }, + { name = "matplotlib-inline", marker = "python_full_version < '3.10'" }, + { name = "pexpect", marker = "python_full_version < '3.10' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version < '3.10'" }, + { name = "pygments", marker = "python_full_version < '3.10'" }, + { name = "stack-data", marker = "python_full_version < '3.10'" }, + { name = "traitlets", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/b9/3ba6c45a6df813c09a48bac313c22ff83efa26cbb55011218d925a46e2ad/ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27", size = 5486330, upload-time = "2023-11-27T09:58:34.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/6b/d9fdcdef2eb6a23f391251fde8781c38d42acd82abe84d054cb74f7863b0/ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397", size = 808161, upload-time = "2023-11-27T09:58:30.538Z" }, +] + +[[package]] +name = "ipython" +version = "8.37.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version == '3.10.*' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version == '3.10.*'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "jedi", marker = "python_full_version == '3.10.*'" }, + { name = "matplotlib-inline", marker = "python_full_version == '3.10.*'" }, + { name = "pexpect", marker = "python_full_version == '3.10.*' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version == '3.10.*'" }, + { name = "pygments", marker = "python_full_version == '3.10.*'" }, + { name = "stack-data", marker = "python_full_version == '3.10.*'" }, + { name = "traitlets", marker = "python_full_version == '3.10.*'" }, + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/31/10ac88f3357fc276dc8a64e8880c82e80e7459326ae1d0a211b40abf6665/ipython-8.37.0.tar.gz", hash = "sha256:ca815841e1a41a1e6b73a0b08f3038af9b2252564d01fc405356d34033012216", size = 5606088, upload-time = "2025-05-31T16:39:09.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/d0/274fbf7b0b12643cbbc001ce13e6a5b1607ac4929d1b11c72460152c9fc3/ipython-8.37.0-py3-none-any.whl", hash = "sha256:ed87326596b878932dbcb171e3e698845434d8c61b8d8cd474bf663041a9dcf2", size = 831864, upload-time = "2025-05-31T16:39:06.38Z" }, +] + +[[package]] +name = "ipython" +version = "9.7.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version >= '3.11' and python_full_version < '3.13'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version >= '3.11'" }, + { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" }, + { name = "jedi", marker = "python_full_version >= '3.11'" }, + { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" }, + { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" }, + { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "stack-data", marker = "python_full_version >= '3.11'" }, + { name = "traitlets", marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/e6/48c74d54039241a456add616464ea28c6ebf782e4110d419411b83dae06f/ipython-9.7.0.tar.gz", hash = "sha256:5f6de88c905a566c6a9d6c400a8fed54a638e1f7543d17aae2551133216b1e4e", size = 4422115, upload-time = "2025-11-05T12:18:54.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/aa/62893d6a591d337aa59dcc4c6f6c842f1fe20cd72c8c5c1f980255243252/ipython-9.7.0-py3-none-any.whl", hash = "sha256:bce8ac85eb9521adc94e1845b4c03d88365fd6ac2f4908ec4ed1eb1b0a065f9f", size = 618911, upload-time = "2025-11-05T12:18:52.484Z" }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, +] + +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing", version = "0.36.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "referencing", version = "0.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "rpds-py", version = "0.27.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "rpds-py", version = "0.29.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing", version = "0.36.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "referencing", version = "0.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "jupyter-client" +version = "8.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jupyter-core", version = "5.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "jupyter-core", version = "5.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "python-dateutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/22/bf9f12fdaeae18019a468b68952a60fe6dbab5d67cd2a103cac7659b41ca/jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419", size = 342019, upload-time = "2024-09-17T10:44:17.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f", size = 106105, upload-time = "2024-09-17T10:44:15.218Z" }, +] + +[[package]] +name = "jupyter-core" +version = "5.8.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pywin32", marker = "python_full_version < '3.10' and platform_python_implementation != 'PyPy' and sys_platform == 'win32'" }, + { name = "traitlets", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/1b/72906d554acfeb588332eaaa6f61577705e9ec752ddb486f302dafa292d9/jupyter_core-5.8.1.tar.gz", hash = "sha256:0a5f9706f70e64786b75acba995988915ebd4601c8a52e534a40b51c95f59941", size = 88923, upload-time = "2025-05-27T07:38:16.655Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/57/6bffd4b20b88da3800c5d691e0337761576ee688eb01299eae865689d2df/jupyter_core-5.8.1-py3-none-any.whl", hash = "sha256:c28d268fc90fb53f1338ded2eb410704c5449a358406e8a948b75706e24863d0", size = 28880, upload-time = "2025-05-27T07:38:15.137Z" }, +] + +[[package]] +name = "jupyter-core" +version = "5.9.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version >= '3.11' and python_full_version < '3.13'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "platformdirs", version = "4.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "traitlets", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, +] + +[[package]] +name = "nats-py" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/c5/2564d917503fe8d68fe630c74bf6b678fbc15c01b58f2565894761010f57/nats_py-2.12.0.tar.gz", hash = "sha256:2981ca4b63b8266c855573fa7871b1be741f1889fd429ee657e5ffc0971a38a1", size = 119821, upload-time = "2025-10-31T05:27:31.247Z" } + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + +[[package]] +name = "opensearch-protobufs" +version = "0.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio", marker = "python_full_version >= '3.10'" }, + { name = "protobuf", marker = "python_full_version >= '3.10'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e2/8a09dbdbfe51e30dfecb625a0f5c524a53bfa4b1fba168f73ac85621dba2/opensearch_protobufs-0.19.0-py3-none-any.whl", hash = "sha256:5137c9c2323cc7debb694754b820ca4cfb5fc8eb180c41ff125698c3ee11bfc2", size = 39778, upload-time = "2025-09-29T20:05:52.379Z" }, +] + +[[package]] +name = "opensearch-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version < '3.10'" }, + { name = "events", marker = "python_full_version < '3.10'" }, + { name = "python-dateutil", marker = "python_full_version < '3.10'" }, + { name = "requests", marker = "python_full_version < '3.10'" }, + { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/58/ecec7f855aae7bcfb08f570088c6cb993f68c361a0727abab35dbf021acb/opensearch_py-3.0.0.tar.gz", hash = "sha256:ebb38f303f8a3f794db816196315bcddad880be0dc75094e3334bc271db2ed39", size = 248890, upload-time = "2025-06-17T05:39:48.453Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/e0/69fd114c607b0323d3f864ab4a5ecb87d76ec5a172d2e36a739c8baebea1/opensearch_py-3.0.0-py3-none-any.whl", hash = "sha256:842bf5d56a4a0d8290eda9bb921c50f3080e5dc4e5fefb9c9648289da3f6a8bb", size = 371491, upload-time = "2025-06-17T05:39:46.539Z" }, +] + +[[package]] +name = "opensearch-py" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version >= '3.11' and python_full_version < '3.13'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version >= '3.10'" }, + { name = "events", marker = "python_full_version >= '3.10'" }, + { name = "opensearch-protobufs", marker = "python_full_version >= '3.10'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.10'" }, + { name = "requests", marker = "python_full_version >= '3.10'" }, + { name = "urllib3", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/9f/d4969f7e8fa221bfebf254cc3056e7c743ce36ac9874e06110474f7c947d/opensearch_py-3.1.0.tar.gz", hash = "sha256:883573af13175ff102b61c80b77934a9e937bdcc40cda2b92051ad53336bc055", size = 258616, upload-time = "2025-11-20T16:37:36.777Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/a1/293c8ad81768ad625283d960685bde07c6302abf20a685e693b48ab6eb91/opensearch_py-3.1.0-py3-none-any.whl", hash = "sha256:e5af83d0454323e6ea9ddee8c0dcc185c0181054592d23cb701da46271a3b65b", size = 385729, upload-time = "2025-11-20T16:37:34.941Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.39.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/0b/e5428c009d4d9af0515b0a8371a8aaae695371af291f45e702f7969dce6b/opentelemetry_api-1.39.0.tar.gz", hash = "sha256:6130644268c5ac6bdffaf660ce878f10906b3e789f7e2daa5e169b047a2933b9", size = 65763, upload-time = "2025-12-03T13:19:56.378Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/85/d831a9bc0a9e0e1a304ff3d12c1489a5fbc9bf6690a15dcbdae372bbca45/opentelemetry_api-1.39.0-py3-none-any.whl", hash = "sha256:3c3b3ca5c5687b1b5b37e5c5027ff68eacea8675241b29f13110a8ffbb8f0459", size = 66357, upload-time = "2025-12-03T13:19:33.043Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp" +version = "1.39.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/be/0e9d889f47e55cadc4041e5b53d4e0cc688f9a74811134fb0ba7cbee6905/opentelemetry_exporter_otlp-1.39.0.tar.gz", hash = "sha256:b405da0287b895fe4e2450dedb2a5b072debba1dfcfed5bdb3d1d183d8daa296", size = 6146, upload-time = "2025-12-03T13:19:58.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/35/212d2cae4fa9a2c02e74438612268b640ab577b8ccb04590371eb4e0f542/opentelemetry_exporter_otlp-1.39.0-py3-none-any.whl", hash = "sha256:fe155d6968d581b325574ad6dc267c8de299397b18d11feeda2206d0a47928a9", size = 7017, upload-time = "2025-12-03T13:19:35.686Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.39.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/cb/3a29ce606b10c76d413d6edd42d25a654af03e73e50696611e757d2602f3/opentelemetry_exporter_otlp_proto_common-1.39.0.tar.gz", hash = "sha256:a135fceed1a6d767f75be65bd2845da344dd8b9258eeed6bc48509d02b184409", size = 20407, upload-time = "2025-12-03T13:19:59.003Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/c6/215edba62d13a3948c718b289539f70e40965bc37fc82ecd55bb0b749c1a/opentelemetry_exporter_otlp_proto_common-1.39.0-py3-none-any.whl", hash = "sha256:3d77be7c4bdf90f1a76666c934368b8abed730b5c6f0547a2ec57feb115849ac", size = 18367, upload-time = "2025-12-03T13:19:36.906Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.39.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/62/4db083ee9620da3065eeb559e9fc128f41a1d15e7c48d7c83aafbccd354c/opentelemetry_exporter_otlp_proto_grpc-1.39.0.tar.gz", hash = "sha256:7e7bb3f436006836c0e0a42ac619097746ad5553ad7128a5bd4d3e727f37fc06", size = 24650, upload-time = "2025-12-03T13:20:00.06Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/e8/d420b94ffddfd8cff85bb4aa5d98da26ce7935dc3cf3eca6b83cd39ab436/opentelemetry_exporter_otlp_proto_grpc-1.39.0-py3-none-any.whl", hash = "sha256:758641278050de9bb895738f35ff8840e4a47685b7e6ef4a201fe83196ba7a05", size = 19765, upload-time = "2025-12-03T13:19:38.143Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.39.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/dc/1e9bf3f6a28e29eba516bc0266e052996d02bc7e92675f3cd38169607609/opentelemetry_exporter_otlp_proto_http-1.39.0.tar.gz", hash = "sha256:28d78fc0eb82d5a71ae552263d5012fa3ebad18dfd189bf8d8095ba0e65ee1ed", size = 17287, upload-time = "2025-12-03T13:20:01.134Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/46/e4a102e17205bb05a50dbf24ef0e92b66b648cd67db9a68865af06a242fd/opentelemetry_exporter_otlp_proto_http-1.39.0-py3-none-any.whl", hash = "sha256:5789cb1375a8b82653328c0ce13a054d285f774099faf9d068032a49de4c7862", size = 19639, upload-time = "2025-12-03T13:19:39.536Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.39.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/b5/64d2f8c3393cd13ea2092106118f7b98461ba09333d40179a31444c6f176/opentelemetry_proto-1.39.0.tar.gz", hash = "sha256:c1fa48678ad1a1624258698e59be73f990b7fc1f39e73e16a9d08eef65dd838c", size = 46153, upload-time = "2025-12-03T13:20:08.729Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/4d/d500e1862beed68318705732d1976c390f4a72ca8009c4983ff627acff20/opentelemetry_proto-1.39.0-py3-none-any.whl", hash = "sha256:1e086552ac79acb501485ff0ce75533f70f3382d43d0a30728eeee594f7bf818", size = 72534, upload-time = "2025-12-03T13:19:50.251Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.39.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/e3/7cd989003e7cde72e0becfe830abff0df55c69d237ee7961a541e0167833/opentelemetry_sdk-1.39.0.tar.gz", hash = "sha256:c22204f12a0529e07aa4d985f1bca9d6b0e7b29fe7f03e923548ae52e0e15dde", size = 171322, upload-time = "2025-12-03T13:20:09.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/b4/2adc8bc83eb1055ecb592708efb6f0c520cc2eb68970b02b0f6ecda149cf/opentelemetry_sdk-1.39.0-py3-none-any.whl", hash = "sha256:90cfb07600dfc0d2de26120cebc0c8f27e69bf77cd80ef96645232372709a514", size = 132413, upload-time = "2025-12-03T13:19:51.364Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/0e/176a7844fe4e3cb5de604212094dffaed4e18b32f1c56b5258bcbcba85c2/opentelemetry_semantic_conventions-0.60b0.tar.gz", hash = "sha256:227d7aa73cbb8a2e418029d6b6465553aa01cf7e78ec9d0bc3255c7b3ac5bf8f", size = 137935, upload-time = "2025-12-03T13:20:12.395Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/56/af0306666f91bae47db14d620775604688361f0f76a872e0005277311131/opentelemetry_semantic_conventions-0.60b0-py3-none-any.whl", hash = "sha256:069530852691136018087b52688857d97bba61cd641d0f8628d2d92788c4f78a", size = 219981, upload-time = "2025-12-03T13:19:53.585Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "parso" +version = "0.8.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/de/53e0bcf53d13e005bd8c92e7855142494f41171b34c2536b86187474184d/parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a", size = 401205, upload-time = "2025-08-23T15:15:28.028Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887", size = 106668, upload-time = "2025-08-23T15:15:25.663Z" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version >= '3.11' and python_full_version < '3.13'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/03/a1440979a3f74f16cab3b75b0da1a1a7f922d56a8ddea96092391998edc0/protobuf-6.33.1.tar.gz", hash = "sha256:97f65757e8d09870de6fd973aeddb92f85435607235d20b2dfed93405d00c85b", size = 443432, upload-time = "2025-11-13T16:44:18.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/f1/446a9bbd2c60772ca36556bac8bfde40eceb28d9cc7838755bc41e001d8f/protobuf-6.33.1-cp310-abi3-win32.whl", hash = "sha256:f8d3fdbc966aaab1d05046d0240dd94d40f2a8c62856d41eaa141ff64a79de6b", size = 425593, upload-time = "2025-11-13T16:44:06.275Z" }, + { url = "https://files.pythonhosted.org/packages/a6/79/8780a378c650e3df849b73de8b13cf5412f521ca2ff9b78a45c247029440/protobuf-6.33.1-cp310-abi3-win_amd64.whl", hash = "sha256:923aa6d27a92bf44394f6abf7ea0500f38769d4b07f4be41cb52bd8b1123b9ed", size = 436883, upload-time = "2025-11-13T16:44:09.222Z" }, + { url = "https://files.pythonhosted.org/packages/cd/93/26213ff72b103ae55bb0d73e7fb91ea570ef407c3ab4fd2f1f27cac16044/protobuf-6.33.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:fe34575f2bdde76ac429ec7b570235bf0c788883e70aee90068e9981806f2490", size = 427522, upload-time = "2025-11-13T16:44:10.475Z" }, + { url = "https://files.pythonhosted.org/packages/c2/32/df4a35247923393aa6b887c3b3244a8c941c32a25681775f96e2b418f90e/protobuf-6.33.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:f8adba2e44cde2d7618996b3fc02341f03f5bc3f2748be72dc7b063319276178", size = 324445, upload-time = "2025-11-13T16:44:11.869Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d0/d796e419e2ec93d2f3fa44888861c3f88f722cde02b7c3488fcc6a166820/protobuf-6.33.1-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:0f4cf01222c0d959c2b399142deb526de420be8236f22c71356e2a544e153c53", size = 339161, upload-time = "2025-11-13T16:44:12.778Z" }, + { url = "https://files.pythonhosted.org/packages/1d/2a/3c5f05a4af06649547027d288747f68525755de692a26a7720dced3652c0/protobuf-6.33.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:8fd7d5e0eb08cd5b87fd3df49bc193f5cfd778701f47e11d127d0afc6c39f1d1", size = 323171, upload-time = "2025-11-13T16:44:14.035Z" }, + { url = "https://files.pythonhosted.org/packages/a0/86/a801cbb316860004bd865b1ded691c53e41d4a8224e3e421f8394174aba7/protobuf-6.33.1-cp39-cp39-win32.whl", hash = "sha256:023af8449482fa884d88b4563d85e83accab54138ae098924a985bcbb734a213", size = 425689, upload-time = "2025-11-13T16:44:15.389Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0f/77b5a12825d59af2596634f062eb1a472f44494965a05dcd97cb5daf3ae5/protobuf-6.33.1-cp39-cp39-win_amd64.whl", hash = "sha256:df051de4fd7e5e4371334e234c62ba43763f15ab605579e04c7008c05735cd82", size = 436877, upload-time = "2025-11-13T16:44:16.71Z" }, + { url = "https://files.pythonhosted.org/packages/08/b4/46310463b4f6ceef310f8348786f3cff181cea671578e3d9743ba61a459e/protobuf-6.33.1-py3-none-any.whl", hash = "sha256:d595a9fd694fdeb061a62fbe10eb039cc1e444df81ec9bb70c7fc59ebcb1eafa", size = 170477, upload-time = "2025-11-13T16:44:17.633Z" }, +] + +[[package]] +name = "psutil" +version = "7.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/88/bdd0a41e5857d5d703287598cbf08dad90aed56774ea52ae071bae9071b6/psutil-7.1.3.tar.gz", hash = "sha256:6c86281738d77335af7aec228328e944b30930899ea760ecf33a4dba66be5e74", size = 489059, upload-time = "2025-11-02T12:25:54.619Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/93/0c49e776b8734fef56ec9c5c57f923922f2cf0497d62e0f419465f28f3d0/psutil-7.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0005da714eee687b4b8decd3d6cc7c6db36215c9e74e5ad2264b90c3df7d92dc", size = 239751, upload-time = "2025-11-02T12:25:58.161Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8d/b31e39c769e70780f007969815195a55c81a63efebdd4dbe9e7a113adb2f/psutil-7.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19644c85dcb987e35eeeaefdc3915d059dac7bd1167cdcdbf27e0ce2df0c08c0", size = 240368, upload-time = "2025-11-02T12:26:00.491Z" }, + { url = "https://files.pythonhosted.org/packages/62/61/23fd4acc3c9eebbf6b6c78bcd89e5d020cfde4acf0a9233e9d4e3fa698b4/psutil-7.1.3-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95ef04cf2e5ba0ab9eaafc4a11eaae91b44f4ef5541acd2ee91d9108d00d59a7", size = 287134, upload-time = "2025-11-02T12:26:02.613Z" }, + { url = "https://files.pythonhosted.org/packages/30/1c/f921a009ea9ceb51aa355cb0cc118f68d354db36eae18174bab63affb3e6/psutil-7.1.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1068c303be3a72f8e18e412c5b2a8f6d31750fb152f9cb106b54090296c9d251", size = 289904, upload-time = "2025-11-02T12:26:05.207Z" }, + { url = "https://files.pythonhosted.org/packages/a6/82/62d68066e13e46a5116df187d319d1724b3f437ddd0f958756fc052677f4/psutil-7.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:18349c5c24b06ac5612c0428ec2a0331c26443d259e2a0144a9b24b4395b58fa", size = 249642, upload-time = "2025-11-02T12:26:07.447Z" }, + { url = "https://files.pythonhosted.org/packages/df/ad/c1cd5fe965c14a0392112f68362cfceb5230819dbb5b1888950d18a11d9f/psutil-7.1.3-cp313-cp313t-win_arm64.whl", hash = "sha256:c525ffa774fe4496282fb0b1187725793de3e7c6b29e41562733cae9ada151ee", size = 245518, upload-time = "2025-11-02T12:26:09.719Z" }, + { url = "https://files.pythonhosted.org/packages/2e/bb/6670bded3e3236eb4287c7bcdc167e9fae6e1e9286e437f7111caed2f909/psutil-7.1.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b403da1df4d6d43973dc004d19cee3b848e998ae3154cc8097d139b77156c353", size = 239843, upload-time = "2025-11-02T12:26:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/b8/66/853d50e75a38c9a7370ddbeefabdd3d3116b9c31ef94dc92c6729bc36bec/psutil-7.1.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad81425efc5e75da3f39b3e636293360ad8d0b49bed7df824c79764fb4ba9b8b", size = 240369, upload-time = "2025-11-02T12:26:14.358Z" }, + { url = "https://files.pythonhosted.org/packages/41/bd/313aba97cb5bfb26916dc29cf0646cbe4dd6a89ca69e8c6edce654876d39/psutil-7.1.3-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f33a3702e167783a9213db10ad29650ebf383946e91bc77f28a5eb083496bc9", size = 288210, upload-time = "2025-11-02T12:26:16.699Z" }, + { url = "https://files.pythonhosted.org/packages/c2/fa/76e3c06e760927a0cfb5705eb38164254de34e9bd86db656d4dbaa228b04/psutil-7.1.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fac9cd332c67f4422504297889da5ab7e05fd11e3c4392140f7370f4208ded1f", size = 291182, upload-time = "2025-11-02T12:26:18.848Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1d/5774a91607035ee5078b8fd747686ebec28a962f178712de100d00b78a32/psutil-7.1.3-cp314-cp314t-win_amd64.whl", hash = "sha256:3792983e23b69843aea49c8f5b8f115572c5ab64c153bada5270086a2123c7e7", size = 250466, upload-time = "2025-11-02T12:26:21.183Z" }, + { url = "https://files.pythonhosted.org/packages/00/ca/e426584bacb43a5cb1ac91fae1937f478cd8fbe5e4ff96574e698a2c77cd/psutil-7.1.3-cp314-cp314t-win_arm64.whl", hash = "sha256:31d77fcedb7529f27bb3a0472bea9334349f9a04160e8e6e5020f22c59893264", size = 245756, upload-time = "2025-11-02T12:26:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/ef/94/46b9154a800253e7ecff5aaacdf8ebf43db99de4a2dfa18575b02548654e/psutil-7.1.3-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2bdbcd0e58ca14996a42adf3621a6244f1bb2e2e528886959c72cf1e326677ab", size = 238359, upload-time = "2025-11-02T12:26:25.284Z" }, + { url = "https://files.pythonhosted.org/packages/68/3a/9f93cff5c025029a36d9a92fef47220ab4692ee7f2be0fba9f92813d0cb8/psutil-7.1.3-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:bc31fa00f1fbc3c3802141eede66f3a2d51d89716a194bf2cd6fc68310a19880", size = 239171, upload-time = "2025-11-02T12:26:27.23Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b1/5f49af514f76431ba4eea935b8ad3725cdeb397e9245ab919dbc1d1dc20f/psutil-7.1.3-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb428f9f05c1225a558f53e30ccbad9930b11c3fc206836242de1091d3e7dd3", size = 263261, upload-time = "2025-11-02T12:26:29.48Z" }, + { url = "https://files.pythonhosted.org/packages/e0/95/992c8816a74016eb095e73585d747e0a8ea21a061ed3689474fabb29a395/psutil-7.1.3-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56d974e02ca2c8eb4812c3f76c30e28836fffc311d55d979f1465c1feeb2b68b", size = 264635, upload-time = "2025-11-02T12:26:31.74Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/c3ed1a622b6ae2fd3c945a366e64eb35247a31e4db16cf5095e269e8eb3c/psutil-7.1.3-cp37-abi3-win_amd64.whl", hash = "sha256:f39c2c19fe824b47484b96f9692932248a54c43799a84282cfe58d05a6449efd", size = 247633, upload-time = "2025-11-02T12:26:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ad/33b2ccec09bf96c2b2ef3f9a6f66baac8253d7565d8839e024a6b905d45d/psutil-7.1.3-cp37-abi3-win_arm64.whl", hash = "sha256:bd0d69cee829226a761e92f28140bec9a5ee9d5b4fb4b0cc589068dbfff559b1", size = 244608, upload-time = "2025-11-02T12:26:36.136Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "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 = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, + { url = "https://files.pythonhosted.org/packages/59/42/b86689aac0cdaee7ae1c58d464b0ff04ca909c19bb6502d4973cdd9f9544/pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b", size = 8760837, upload-time = "2025-07-14T20:12:59.59Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8a/1403d0353f8c5a2f0829d2b1c4becbf9da2f0a4d040886404fc4a5431e4d/pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91", size = 9590187, upload-time = "2025-07-14T20:13:01.419Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/e0e8d802f124772cec9c75430b01a212f86f9de7546bda715e54140d5aeb/pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d", size = 8778162, upload-time = "2025-07-14T20:13:03.544Z" }, +] + +[[package]] +name = "pyzmq" +version = "27.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/b9/52aa9ec2867528b54f1e60846728d8b4d84726630874fee3a91e66c7df81/pyzmq-27.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4", size = 1329850, upload-time = "2025-09-08T23:07:26.274Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/5653e7b7425b169f994835a2b2abf9486264401fdef18df91ddae47ce2cc/pyzmq-27.1.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556", size = 906380, upload-time = "2025-09-08T23:07:29.78Z" }, + { url = "https://files.pythonhosted.org/packages/73/78/7d713284dbe022f6440e391bd1f3c48d9185673878034cfb3939cdf333b2/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b", size = 666421, upload-time = "2025-09-08T23:07:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/30/76/8f099f9d6482450428b17c4d6b241281af7ce6a9de8149ca8c1c649f6792/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e", size = 854149, upload-time = "2025-09-08T23:07:33.17Z" }, + { url = "https://files.pythonhosted.org/packages/59/f0/37fbfff06c68016019043897e4c969ceab18bde46cd2aca89821fcf4fb2e/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526", size = 1655070, upload-time = "2025-09-08T23:07:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/47/14/7254be73f7a8edc3587609554fcaa7bfd30649bf89cd260e4487ca70fdaa/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1", size = 2033441, upload-time = "2025-09-08T23:07:37.432Z" }, + { url = "https://files.pythonhosted.org/packages/22/dc/49f2be26c6f86f347e796a4d99b19167fc94503f0af3fd010ad262158822/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386", size = 1891529, upload-time = "2025-09-08T23:07:39.047Z" }, + { url = "https://files.pythonhosted.org/packages/a3/3e/154fb963ae25be70c0064ce97776c937ecc7d8b0259f22858154a9999769/pyzmq-27.1.0-cp310-cp310-win32.whl", hash = "sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda", size = 567276, upload-time = "2025-09-08T23:07:40.695Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/f4ab56c8c595abcb26b2be5fd9fa9e6899c1e5ad54964e93ae8bb35482be/pyzmq-27.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f", size = 632208, upload-time = "2025-09-08T23:07:42.298Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e3/be2cc7ab8332bdac0522fdb64c17b1b6241a795bee02e0196636ec5beb79/pyzmq-27.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32", size = 559766, upload-time = "2025-09-08T23:07:43.869Z" }, + { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, + { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, + { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, + { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, + { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, + { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, + { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, + { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, + { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, + { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4e/782eb6df91b6a9d9afa96c2dcfc5cac62562a68eb62a02210101f886014d/pyzmq-27.1.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:96c71c32fff75957db6ae33cd961439f386505c6e6b377370af9b24a1ef9eafb", size = 1330426, upload-time = "2025-09-08T23:09:21.03Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ca/2b8693d06b1db4e0c084871e4c9d7842b561d0a6ff9d780640f5e3e9eb55/pyzmq-27.1.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:49d3980544447f6bd2968b6ac913ab963a49dcaa2d4a2990041f16057b04c429", size = 906559, upload-time = "2025-09-08T23:09:22.983Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b3/b99b39e2cfdcebd512959780e4d299447fd7f46010b1d88d63324e2481ec/pyzmq-27.1.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:849ca054d81aa1c175c49484afaaa5db0622092b5eccb2055f9f3bb8f703782d", size = 863816, upload-time = "2025-09-08T23:09:24.556Z" }, + { url = "https://files.pythonhosted.org/packages/61/b2/018fa8e8eefb34a625b1a45e2effcbc9885645b22cdd0a68283f758351e7/pyzmq-27.1.0-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3970778e74cb7f85934d2b926b9900e92bfe597e62267d7499acc39c9c28e345", size = 666735, upload-time = "2025-09-08T23:09:26.297Z" }, + { url = "https://files.pythonhosted.org/packages/01/05/8ae778f7cd7c94030731ae2305e6a38f3a333b6825f56c0c03f2134ccf1b/pyzmq-27.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:da96ecdcf7d3919c3be2de91a8c513c186f6762aa6cf7c01087ed74fad7f0968", size = 1655425, upload-time = "2025-09-08T23:09:28.172Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ad/d69478a97a3f3142f9dbbbd9daa4fcf42541913a85567c36d4cfc19b2218/pyzmq-27.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9541c444cfe1b1c0156c5c86ece2bb926c7079a18e7b47b0b1b3b1b875e5d098", size = 2033729, upload-time = "2025-09-08T23:09:30.097Z" }, + { url = "https://files.pythonhosted.org/packages/9a/6d/e3c6ad05bc1cddd25094e66cc15ae8924e15c67e231e93ed2955c401007e/pyzmq-27.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e30a74a39b93e2e1591b58eb1acef4902be27c957a8720b0e368f579b82dc22f", size = 1891803, upload-time = "2025-09-08T23:09:31.875Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a7/97e8be0daaca157511563160b67a13d4fe76b195e3fa6873cb554ad46be3/pyzmq-27.1.0-cp39-cp39-win32.whl", hash = "sha256:b1267823d72d1e40701dcba7edc45fd17f71be1285557b7fe668887150a14b78", size = 567627, upload-time = "2025-09-08T23:09:33.98Z" }, + { url = "https://files.pythonhosted.org/packages/5c/91/70bbf3a7c5b04c904261ef5ba224d8a76315f6c23454251bf5f55573a8a1/pyzmq-27.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0c996ded912812a2fcd7ab6574f4ad3edc27cb6510349431e4930d4196ade7db", size = 632315, upload-time = "2025-09-08T23:09:36.097Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b5/a4173a83c7fd37f6bdb5a800ea338bc25603284e9ef8681377cec006ede4/pyzmq-27.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:346e9ba4198177a07e7706050f35d733e08c1c1f8ceacd5eb6389d653579ffbc", size = 559833, upload-time = "2025-09-08T23:09:38.183Z" }, + { url = "https://files.pythonhosted.org/packages/f3/81/a65e71c1552f74dec9dff91d95bafb6e0d33338a8dfefbc88aa562a20c92/pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6", size = 836266, upload-time = "2025-09-08T23:09:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/58/ed/0202ca350f4f2b69faa95c6d931e3c05c3a397c184cacb84cb4f8f42f287/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90", size = 800206, upload-time = "2025-09-08T23:09:41.902Z" }, + { url = "https://files.pythonhosted.org/packages/47/42/1ff831fa87fe8f0a840ddb399054ca0009605d820e2b44ea43114f5459f4/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62", size = 567747, upload-time = "2025-09-08T23:09:43.741Z" }, + { url = "https://files.pythonhosted.org/packages/d1/db/5c4d6807434751e3f21231bee98109aa57b9b9b55e058e450d0aef59b70f/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74", size = 747371, upload-time = "2025-09-08T23:09:45.575Z" }, + { url = "https://files.pythonhosted.org/packages/26/af/78ce193dbf03567eb8c0dc30e3df2b9e56f12a670bf7eb20f9fb532c7e8a/pyzmq-27.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba", size = 544862, upload-time = "2025-09-08T23:09:47.448Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, + { url = "https://files.pythonhosted.org/packages/57/f4/c2e978cf6b833708bad7d6396c3a20c19750585a1775af3ff13c435e1912/pyzmq-27.1.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:722ea791aa233ac0a819fc2c475e1292c76930b31f1d828cb61073e2fe5e208f", size = 836257, upload-time = "2025-09-08T23:10:07.635Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5f/4e10c7f57a4c92ab0fbb2396297aa8d618e6f5b9b8f8e9756d56f3e6fc52/pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:01f9437501886d3a1dd4b02ef59fb8cc384fa718ce066d52f175ee49dd5b7ed8", size = 800203, upload-time = "2025-09-08T23:10:09.436Z" }, + { url = "https://files.pythonhosted.org/packages/19/72/a74a007cd636f903448c6ab66628104b1fc5f2ba018733d5eabb94a0a6fb/pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4a19387a3dddcc762bfd2f570d14e2395b2c9701329b266f83dd87a2b3cbd381", size = 758756, upload-time = "2025-09-08T23:10:11.733Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d4/30c25b91f2b4786026372f5ef454134d7f576fcf4ac58539ad7dd5de4762/pyzmq-27.1.0-pp39-pypy39_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c618fbcd069e3a29dcd221739cacde52edcc681f041907867e0f5cc7e85f172", size = 567742, upload-time = "2025-09-08T23:10:14.732Z" }, + { url = "https://files.pythonhosted.org/packages/92/aa/ee86edad943438cd0316964020c4b6d09854414f9f945f8e289ea6fcc019/pyzmq-27.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ff8d114d14ac671d88c89b9224c63d6c4e5a613fe8acd5594ce53d752a3aafe9", size = 544857, upload-time = "2025-09-08T23:10:16.431Z" }, +] + +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "attrs", marker = "python_full_version < '3.10'" }, + { name = "rpds-py", version = "0.27.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version >= '3.11' and python_full_version < '3.13'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "attrs", marker = "python_full_version >= '3.10'" }, + { name = "rpds-py", version = "0.29.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "urllib3", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.27.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479, upload-time = "2025-08-27T12:16:36.024Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/ed/3aef893e2dd30e77e35d20d4ddb45ca459db59cead748cad9796ad479411/rpds_py-0.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef", size = 371606, upload-time = "2025-08-27T12:12:25.189Z" }, + { url = "https://files.pythonhosted.org/packages/6d/82/9818b443e5d3eb4c83c3994561387f116aae9833b35c484474769c4a8faf/rpds_py-0.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be", size = 353452, upload-time = "2025-08-27T12:12:27.433Z" }, + { url = "https://files.pythonhosted.org/packages/99/c7/d2a110ffaaa397fc6793a83c7bd3545d9ab22658b7cdff05a24a4535cc45/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61", size = 381519, upload-time = "2025-08-27T12:12:28.719Z" }, + { url = "https://files.pythonhosted.org/packages/5a/bc/e89581d1f9d1be7d0247eaef602566869fdc0d084008ba139e27e775366c/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb", size = 394424, upload-time = "2025-08-27T12:12:30.207Z" }, + { url = "https://files.pythonhosted.org/packages/ac/2e/36a6861f797530e74bb6ed53495f8741f1ef95939eed01d761e73d559067/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657", size = 523467, upload-time = "2025-08-27T12:12:31.808Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/c1bc2be32564fa499f988f0a5c6505c2f4746ef96e58e4d7de5cf923d77e/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013", size = 402660, upload-time = "2025-08-27T12:12:33.444Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ec/ef8bf895f0628dd0a59e54d81caed6891663cb9c54a0f4bb7da918cb88cf/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a", size = 384062, upload-time = "2025-08-27T12:12:34.857Z" }, + { url = "https://files.pythonhosted.org/packages/69/f7/f47ff154be8d9a5e691c083a920bba89cef88d5247c241c10b9898f595a1/rpds_py-0.27.1-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1", size = 401289, upload-time = "2025-08-27T12:12:36.085Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d9/ca410363efd0615814ae579f6829cafb39225cd63e5ea5ed1404cb345293/rpds_py-0.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10", size = 417718, upload-time = "2025-08-27T12:12:37.401Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a0/8cb5c2ff38340f221cc067cc093d1270e10658ba4e8d263df923daa18e86/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808", size = 558333, upload-time = "2025-08-27T12:12:38.672Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8c/1b0de79177c5d5103843774ce12b84caa7164dfc6cd66378768d37db11bf/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8", size = 589127, upload-time = "2025-08-27T12:12:41.48Z" }, + { url = "https://files.pythonhosted.org/packages/c8/5e/26abb098d5e01266b0f3a2488d299d19ccc26849735d9d2b95c39397e945/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9", size = 554899, upload-time = "2025-08-27T12:12:42.925Z" }, + { url = "https://files.pythonhosted.org/packages/de/41/905cc90ced13550db017f8f20c6d8e8470066c5738ba480d7ba63e3d136b/rpds_py-0.27.1-cp310-cp310-win32.whl", hash = "sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4", size = 217450, upload-time = "2025-08-27T12:12:44.813Z" }, + { url = "https://files.pythonhosted.org/packages/75/3d/6bef47b0e253616ccdf67c283e25f2d16e18ccddd38f92af81d5a3420206/rpds_py-0.27.1-cp310-cp310-win_amd64.whl", hash = "sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1", size = 228447, upload-time = "2025-08-27T12:12:46.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c1/7907329fbef97cbd49db6f7303893bd1dd5a4a3eae415839ffdfb0762cae/rpds_py-0.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881", size = 371063, upload-time = "2025-08-27T12:12:47.856Z" }, + { url = "https://files.pythonhosted.org/packages/11/94/2aab4bc86228bcf7c48760990273653a4900de89c7537ffe1b0d6097ed39/rpds_py-0.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5", size = 353210, upload-time = "2025-08-27T12:12:49.187Z" }, + { url = "https://files.pythonhosted.org/packages/3a/57/f5eb3ecf434342f4f1a46009530e93fd201a0b5b83379034ebdb1d7c1a58/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e", size = 381636, upload-time = "2025-08-27T12:12:50.492Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f4/ef95c5945e2ceb5119571b184dd5a1cc4b8541bbdf67461998cfeac9cb1e/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c", size = 394341, upload-time = "2025-08-27T12:12:52.024Z" }, + { url = "https://files.pythonhosted.org/packages/5a/7e/4bd610754bf492d398b61725eb9598ddd5eb86b07d7d9483dbcd810e20bc/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195", size = 523428, upload-time = "2025-08-27T12:12:53.779Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e5/059b9f65a8c9149361a8b75094864ab83b94718344db511fd6117936ed2a/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52", size = 402923, upload-time = "2025-08-27T12:12:55.15Z" }, + { url = "https://files.pythonhosted.org/packages/f5/48/64cabb7daced2968dd08e8a1b7988bf358d7bd5bcd5dc89a652f4668543c/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed", size = 384094, upload-time = "2025-08-27T12:12:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e1/dc9094d6ff566bff87add8a510c89b9e158ad2ecd97ee26e677da29a9e1b/rpds_py-0.27.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a", size = 401093, upload-time = "2025-08-27T12:12:58.985Z" }, + { url = "https://files.pythonhosted.org/packages/37/8e/ac8577e3ecdd5593e283d46907d7011618994e1d7ab992711ae0f78b9937/rpds_py-0.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde", size = 417969, upload-time = "2025-08-27T12:13:00.367Z" }, + { url = "https://files.pythonhosted.org/packages/66/6d/87507430a8f74a93556fe55c6485ba9c259949a853ce407b1e23fea5ba31/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21", size = 558302, upload-time = "2025-08-27T12:13:01.737Z" }, + { url = "https://files.pythonhosted.org/packages/3a/bb/1db4781ce1dda3eecc735e3152659a27b90a02ca62bfeea17aee45cc0fbc/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9", size = 589259, upload-time = "2025-08-27T12:13:03.127Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/ae1c8943d11a814d01b482e1f8da903f88047a962dff9bbdadf3bd6e6fd1/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948", size = 554983, upload-time = "2025-08-27T12:13:04.516Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/0b2a55415931db4f112bdab072443ff76131b5ac4f4dc98d10d2d357eb03/rpds_py-0.27.1-cp311-cp311-win32.whl", hash = "sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39", size = 217154, upload-time = "2025-08-27T12:13:06.278Z" }, + { url = "https://files.pythonhosted.org/packages/24/75/3b7ffe0d50dc86a6a964af0d1cc3a4a2cdf437cb7b099a4747bbb96d1819/rpds_py-0.27.1-cp311-cp311-win_amd64.whl", hash = "sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15", size = 228627, upload-time = "2025-08-27T12:13:07.625Z" }, + { url = "https://files.pythonhosted.org/packages/8d/3f/4fd04c32abc02c710f09a72a30c9a55ea3cc154ef8099078fd50a0596f8e/rpds_py-0.27.1-cp311-cp311-win_arm64.whl", hash = "sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746", size = 220998, upload-time = "2025-08-27T12:13:08.972Z" }, + { url = "https://files.pythonhosted.org/packages/bd/fe/38de28dee5df58b8198c743fe2bea0c785c6d40941b9950bac4cdb71a014/rpds_py-0.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90", size = 361887, upload-time = "2025-08-27T12:13:10.233Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/4b6c7eedc7dd90986bf0fab6ea2a091ec11c01b15f8ba0a14d3f80450468/rpds_py-0.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5", size = 345795, upload-time = "2025-08-27T12:13:11.65Z" }, + { url = "https://files.pythonhosted.org/packages/6f/0e/e650e1b81922847a09cca820237b0edee69416a01268b7754d506ade11ad/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e", size = 385121, upload-time = "2025-08-27T12:13:13.008Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ea/b306067a712988e2bff00dcc7c8f31d26c29b6d5931b461aa4b60a013e33/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881", size = 398976, upload-time = "2025-08-27T12:13:14.368Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0a/26dc43c8840cb8fe239fe12dbc8d8de40f2365e838f3d395835dde72f0e5/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec", size = 525953, upload-time = "2025-08-27T12:13:15.774Z" }, + { url = "https://files.pythonhosted.org/packages/22/14/c85e8127b573aaf3a0cbd7fbb8c9c99e735a4a02180c84da2a463b766e9e/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb", size = 407915, upload-time = "2025-08-27T12:13:17.379Z" }, + { url = "https://files.pythonhosted.org/packages/ed/7b/8f4fee9ba1fb5ec856eb22d725a4efa3deb47f769597c809e03578b0f9d9/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5", size = 386883, upload-time = "2025-08-27T12:13:18.704Z" }, + { url = "https://files.pythonhosted.org/packages/86/47/28fa6d60f8b74fcdceba81b272f8d9836ac0340570f68f5df6b41838547b/rpds_py-0.27.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a", size = 405699, upload-time = "2025-08-27T12:13:20.089Z" }, + { url = "https://files.pythonhosted.org/packages/d0/fd/c5987b5e054548df56953a21fe2ebed51fc1ec7c8f24fd41c067b68c4a0a/rpds_py-0.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444", size = 423713, upload-time = "2025-08-27T12:13:21.436Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ba/3c4978b54a73ed19a7d74531be37a8bcc542d917c770e14d372b8daea186/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a", size = 562324, upload-time = "2025-08-27T12:13:22.789Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6c/6943a91768fec16db09a42b08644b960cff540c66aab89b74be6d4a144ba/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1", size = 593646, upload-time = "2025-08-27T12:13:24.122Z" }, + { url = "https://files.pythonhosted.org/packages/11/73/9d7a8f4be5f4396f011a6bb7a19fe26303a0dac9064462f5651ced2f572f/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998", size = 558137, upload-time = "2025-08-27T12:13:25.557Z" }, + { url = "https://files.pythonhosted.org/packages/6e/96/6772cbfa0e2485bcceef8071de7821f81aeac8bb45fbfd5542a3e8108165/rpds_py-0.27.1-cp312-cp312-win32.whl", hash = "sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39", size = 221343, upload-time = "2025-08-27T12:13:26.967Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/c82f0faa9af1c6a64669f73a17ee0eeef25aff30bb9a1c318509efe45d84/rpds_py-0.27.1-cp312-cp312-win_amd64.whl", hash = "sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594", size = 232497, upload-time = "2025-08-27T12:13:28.326Z" }, + { url = "https://files.pythonhosted.org/packages/e1/96/2817b44bd2ed11aebacc9251da03689d56109b9aba5e311297b6902136e2/rpds_py-0.27.1-cp312-cp312-win_arm64.whl", hash = "sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502", size = 222790, upload-time = "2025-08-27T12:13:29.71Z" }, + { url = "https://files.pythonhosted.org/packages/cc/77/610aeee8d41e39080c7e14afa5387138e3c9fa9756ab893d09d99e7d8e98/rpds_py-0.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b", size = 361741, upload-time = "2025-08-27T12:13:31.039Z" }, + { url = "https://files.pythonhosted.org/packages/3a/fc/c43765f201c6a1c60be2043cbdb664013def52460a4c7adace89d6682bf4/rpds_py-0.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf", size = 345574, upload-time = "2025-08-27T12:13:32.902Z" }, + { url = "https://files.pythonhosted.org/packages/20/42/ee2b2ca114294cd9847d0ef9c26d2b0851b2e7e00bf14cc4c0b581df0fc3/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83", size = 385051, upload-time = "2025-08-27T12:13:34.228Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e8/1e430fe311e4799e02e2d1af7c765f024e95e17d651612425b226705f910/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf", size = 398395, upload-time = "2025-08-27T12:13:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/82/95/9dc227d441ff2670651c27a739acb2535ccaf8b351a88d78c088965e5996/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2", size = 524334, upload-time = "2025-08-27T12:13:37.562Z" }, + { url = "https://files.pythonhosted.org/packages/87/01/a670c232f401d9ad461d9a332aa4080cd3cb1d1df18213dbd0d2a6a7ab51/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0", size = 407691, upload-time = "2025-08-27T12:13:38.94Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/0a14aebbaa26fe7fab4780c76f2239e76cc95a0090bdb25e31d95c492fcd/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418", size = 386868, upload-time = "2025-08-27T12:13:40.192Z" }, + { url = "https://files.pythonhosted.org/packages/3b/03/8c897fb8b5347ff6c1cc31239b9611c5bf79d78c984430887a353e1409a1/rpds_py-0.27.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d", size = 405469, upload-time = "2025-08-27T12:13:41.496Z" }, + { url = "https://files.pythonhosted.org/packages/da/07/88c60edc2df74850d496d78a1fdcdc7b54360a7f610a4d50008309d41b94/rpds_py-0.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274", size = 422125, upload-time = "2025-08-27T12:13:42.802Z" }, + { url = "https://files.pythonhosted.org/packages/6b/86/5f4c707603e41b05f191a749984f390dabcbc467cf833769b47bf14ba04f/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd", size = 562341, upload-time = "2025-08-27T12:13:44.472Z" }, + { url = "https://files.pythonhosted.org/packages/b2/92/3c0cb2492094e3cd9baf9e49bbb7befeceb584ea0c1a8b5939dca4da12e5/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2", size = 592511, upload-time = "2025-08-27T12:13:45.898Z" }, + { url = "https://files.pythonhosted.org/packages/10/bb/82e64fbb0047c46a168faa28d0d45a7851cd0582f850b966811d30f67ad8/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002", size = 557736, upload-time = "2025-08-27T12:13:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/00/95/3c863973d409210da7fb41958172c6b7dbe7fc34e04d3cc1f10bb85e979f/rpds_py-0.27.1-cp313-cp313-win32.whl", hash = "sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3", size = 221462, upload-time = "2025-08-27T12:13:48.742Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2c/5867b14a81dc217b56d95a9f2a40fdbc56a1ab0181b80132beeecbd4b2d6/rpds_py-0.27.1-cp313-cp313-win_amd64.whl", hash = "sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83", size = 232034, upload-time = "2025-08-27T12:13:50.11Z" }, + { url = "https://files.pythonhosted.org/packages/c7/78/3958f3f018c01923823f1e47f1cc338e398814b92d83cd278364446fac66/rpds_py-0.27.1-cp313-cp313-win_arm64.whl", hash = "sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d", size = 222392, upload-time = "2025-08-27T12:13:52.587Z" }, + { url = "https://files.pythonhosted.org/packages/01/76/1cdf1f91aed5c3a7bf2eba1f1c4e4d6f57832d73003919a20118870ea659/rpds_py-0.27.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228", size = 358355, upload-time = "2025-08-27T12:13:54.012Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6f/bf142541229374287604caf3bb2a4ae17f0a580798fd72d3b009b532db4e/rpds_py-0.27.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92", size = 342138, upload-time = "2025-08-27T12:13:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/1a/77/355b1c041d6be40886c44ff5e798b4e2769e497b790f0f7fd1e78d17e9a8/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2", size = 380247, upload-time = "2025-08-27T12:13:57.683Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a4/d9cef5c3946ea271ce2243c51481971cd6e34f21925af2783dd17b26e815/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723", size = 390699, upload-time = "2025-08-27T12:13:59.137Z" }, + { url = "https://files.pythonhosted.org/packages/3a/06/005106a7b8c6c1a7e91b73169e49870f4af5256119d34a361ae5240a0c1d/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802", size = 521852, upload-time = "2025-08-27T12:14:00.583Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3e/50fb1dac0948e17a02eb05c24510a8fe12d5ce8561c6b7b7d1339ab7ab9c/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f", size = 402582, upload-time = "2025-08-27T12:14:02.034Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b0/f4e224090dc5b0ec15f31a02d746ab24101dd430847c4d99123798661bfc/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2", size = 384126, upload-time = "2025-08-27T12:14:03.437Z" }, + { url = "https://files.pythonhosted.org/packages/54/77/ac339d5f82b6afff1df8f0fe0d2145cc827992cb5f8eeb90fc9f31ef7a63/rpds_py-0.27.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21", size = 399486, upload-time = "2025-08-27T12:14:05.443Z" }, + { url = "https://files.pythonhosted.org/packages/d6/29/3e1c255eee6ac358c056a57d6d6869baa00a62fa32eea5ee0632039c50a3/rpds_py-0.27.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef", size = 414832, upload-time = "2025-08-27T12:14:06.902Z" }, + { url = "https://files.pythonhosted.org/packages/3f/db/6d498b844342deb3fa1d030598db93937a9964fcf5cb4da4feb5f17be34b/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081", size = 557249, upload-time = "2025-08-27T12:14:08.37Z" }, + { url = "https://files.pythonhosted.org/packages/60/f3/690dd38e2310b6f68858a331399b4d6dbb9132c3e8ef8b4333b96caf403d/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd", size = 587356, upload-time = "2025-08-27T12:14:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/86/e3/84507781cccd0145f35b1dc32c72675200c5ce8d5b30f813e49424ef68fc/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7", size = 555300, upload-time = "2025-08-27T12:14:11.783Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ee/375469849e6b429b3516206b4580a79e9ef3eb12920ddbd4492b56eaacbe/rpds_py-0.27.1-cp313-cp313t-win32.whl", hash = "sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688", size = 216714, upload-time = "2025-08-27T12:14:13.629Z" }, + { url = "https://files.pythonhosted.org/packages/21/87/3fc94e47c9bd0742660e84706c311a860dcae4374cf4a03c477e23ce605a/rpds_py-0.27.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797", size = 228943, upload-time = "2025-08-27T12:14:14.937Z" }, + { url = "https://files.pythonhosted.org/packages/70/36/b6e6066520a07cf029d385de869729a895917b411e777ab1cde878100a1d/rpds_py-0.27.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334", size = 362472, upload-time = "2025-08-27T12:14:16.333Z" }, + { url = "https://files.pythonhosted.org/packages/af/07/b4646032e0dcec0df9c73a3bd52f63bc6c5f9cda992f06bd0e73fe3fbebd/rpds_py-0.27.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33", size = 345676, upload-time = "2025-08-27T12:14:17.764Z" }, + { url = "https://files.pythonhosted.org/packages/b0/16/2f1003ee5d0af4bcb13c0cf894957984c32a6751ed7206db2aee7379a55e/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a", size = 385313, upload-time = "2025-08-27T12:14:19.829Z" }, + { url = "https://files.pythonhosted.org/packages/05/cd/7eb6dd7b232e7f2654d03fa07f1414d7dfc980e82ba71e40a7c46fd95484/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b", size = 399080, upload-time = "2025-08-27T12:14:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/20/51/5829afd5000ec1cb60f304711f02572d619040aa3ec033d8226817d1e571/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7", size = 523868, upload-time = "2025-08-27T12:14:23.485Z" }, + { url = "https://files.pythonhosted.org/packages/05/2c/30eebca20d5db95720ab4d2faec1b5e4c1025c473f703738c371241476a2/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136", size = 408750, upload-time = "2025-08-27T12:14:24.924Z" }, + { url = "https://files.pythonhosted.org/packages/90/1a/cdb5083f043597c4d4276eae4e4c70c55ab5accec078da8611f24575a367/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff", size = 387688, upload-time = "2025-08-27T12:14:27.537Z" }, + { url = "https://files.pythonhosted.org/packages/7c/92/cf786a15320e173f945d205ab31585cc43969743bb1a48b6888f7a2b0a2d/rpds_py-0.27.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9", size = 407225, upload-time = "2025-08-27T12:14:28.981Z" }, + { url = "https://files.pythonhosted.org/packages/33/5c/85ee16df5b65063ef26017bef33096557a4c83fbe56218ac7cd8c235f16d/rpds_py-0.27.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60", size = 423361, upload-time = "2025-08-27T12:14:30.469Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8e/1c2741307fcabd1a334ecf008e92c4f47bb6f848712cf15c923becfe82bb/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e", size = 562493, upload-time = "2025-08-27T12:14:31.987Z" }, + { url = "https://files.pythonhosted.org/packages/04/03/5159321baae9b2222442a70c1f988cbbd66b9be0675dd3936461269be360/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212", size = 592623, upload-time = "2025-08-27T12:14:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/ff/39/c09fd1ad28b85bc1d4554a8710233c9f4cefd03d7717a1b8fbfd171d1167/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675", size = 558800, upload-time = "2025-08-27T12:14:35.436Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d6/99228e6bbcf4baa764b18258f519a9035131d91b538d4e0e294313462a98/rpds_py-0.27.1-cp314-cp314-win32.whl", hash = "sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3", size = 221943, upload-time = "2025-08-27T12:14:36.898Z" }, + { url = "https://files.pythonhosted.org/packages/be/07/c802bc6b8e95be83b79bdf23d1aa61d68324cb1006e245d6c58e959e314d/rpds_py-0.27.1-cp314-cp314-win_amd64.whl", hash = "sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456", size = 233739, upload-time = "2025-08-27T12:14:38.386Z" }, + { url = "https://files.pythonhosted.org/packages/c8/89/3e1b1c16d4c2d547c5717377a8df99aee8099ff050f87c45cb4d5fa70891/rpds_py-0.27.1-cp314-cp314-win_arm64.whl", hash = "sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3", size = 223120, upload-time = "2025-08-27T12:14:39.82Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/dc7931dc2fa4a6e46b2a4fa744a9fe5c548efd70e0ba74f40b39fa4a8c10/rpds_py-0.27.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2", size = 358944, upload-time = "2025-08-27T12:14:41.199Z" }, + { url = "https://files.pythonhosted.org/packages/e6/22/4af76ac4e9f336bfb1a5f240d18a33c6b2fcaadb7472ac7680576512b49a/rpds_py-0.27.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4", size = 342283, upload-time = "2025-08-27T12:14:42.699Z" }, + { url = "https://files.pythonhosted.org/packages/1c/15/2a7c619b3c2272ea9feb9ade67a45c40b3eeb500d503ad4c28c395dc51b4/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e", size = 380320, upload-time = "2025-08-27T12:14:44.157Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7d/4c6d243ba4a3057e994bb5bedd01b5c963c12fe38dde707a52acdb3849e7/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817", size = 391760, upload-time = "2025-08-27T12:14:45.845Z" }, + { url = "https://files.pythonhosted.org/packages/b4/71/b19401a909b83bcd67f90221330bc1ef11bc486fe4e04c24388d28a618ae/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec", size = 522476, upload-time = "2025-08-27T12:14:47.364Z" }, + { url = "https://files.pythonhosted.org/packages/e4/44/1a3b9715c0455d2e2f0f6df5ee6d6f5afdc423d0773a8a682ed2b43c566c/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a", size = 403418, upload-time = "2025-08-27T12:14:49.991Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4b/fb6c4f14984eb56673bc868a66536f53417ddb13ed44b391998100a06a96/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8", size = 384771, upload-time = "2025-08-27T12:14:52.159Z" }, + { url = "https://files.pythonhosted.org/packages/c0/56/d5265d2d28b7420d7b4d4d85cad8ef891760f5135102e60d5c970b976e41/rpds_py-0.27.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48", size = 400022, upload-time = "2025-08-27T12:14:53.859Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e9/9f5fc70164a569bdd6ed9046486c3568d6926e3a49bdefeeccfb18655875/rpds_py-0.27.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb", size = 416787, upload-time = "2025-08-27T12:14:55.673Z" }, + { url = "https://files.pythonhosted.org/packages/d4/64/56dd03430ba491db943a81dcdef115a985aac5f44f565cd39a00c766d45c/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734", size = 557538, upload-time = "2025-08-27T12:14:57.245Z" }, + { url = "https://files.pythonhosted.org/packages/3f/36/92cc885a3129993b1d963a2a42ecf64e6a8e129d2c7cc980dbeba84e55fb/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb", size = 588512, upload-time = "2025-08-27T12:14:58.728Z" }, + { url = "https://files.pythonhosted.org/packages/dd/10/6b283707780a81919f71625351182b4f98932ac89a09023cb61865136244/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0", size = 555813, upload-time = "2025-08-27T12:15:00.334Z" }, + { url = "https://files.pythonhosted.org/packages/04/2e/30b5ea18c01379da6272a92825dd7e53dc9d15c88a19e97932d35d430ef7/rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a", size = 217385, upload-time = "2025-08-27T12:15:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/32/7d/97119da51cb1dd3f2f3c0805f155a3aa4a95fa44fe7d78ae15e69edf4f34/rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772", size = 230097, upload-time = "2025-08-27T12:15:03.961Z" }, + { url = "https://files.pythonhosted.org/packages/7f/6c/252e83e1ce7583c81f26d1d884b2074d40a13977e1b6c9c50bbf9a7f1f5a/rpds_py-0.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c918c65ec2e42c2a78d19f18c553d77319119bf43aa9e2edf7fb78d624355527", size = 372140, upload-time = "2025-08-27T12:15:05.441Z" }, + { url = "https://files.pythonhosted.org/packages/9d/71/949c195d927c5aeb0d0629d329a20de43a64c423a6aa53836290609ef7ec/rpds_py-0.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1fea2b1a922c47c51fd07d656324531adc787e415c8b116530a1d29c0516c62d", size = 354086, upload-time = "2025-08-27T12:15:07.404Z" }, + { url = "https://files.pythonhosted.org/packages/9f/02/e43e332ad8ce4f6c4342d151a471a7f2900ed1d76901da62eb3762663a71/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbf94c58e8e0cd6b6f38d8de67acae41b3a515c26169366ab58bdca4a6883bb8", size = 382117, upload-time = "2025-08-27T12:15:09.275Z" }, + { url = "https://files.pythonhosted.org/packages/d0/05/b0fdeb5b577197ad72812bbdfb72f9a08fa1e64539cc3940b1b781cd3596/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c2a8fed130ce946d5c585eddc7c8eeef0051f58ac80a8ee43bd17835c144c2cc", size = 394520, upload-time = "2025-08-27T12:15:10.727Z" }, + { url = "https://files.pythonhosted.org/packages/67/1f/4cfef98b2349a7585181e99294fa2a13f0af06902048a5d70f431a66d0b9/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:037a2361db72ee98d829bc2c5b7cc55598ae0a5e0ec1823a56ea99374cfd73c1", size = 522657, upload-time = "2025-08-27T12:15:12.613Z" }, + { url = "https://files.pythonhosted.org/packages/44/55/ccf37ddc4c6dce7437b335088b5ca18da864b334890e2fe9aa6ddc3f79a9/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5281ed1cc1d49882f9997981c88df1a22e140ab41df19071222f7e5fc4e72125", size = 402967, upload-time = "2025-08-27T12:15:14.113Z" }, + { url = "https://files.pythonhosted.org/packages/74/e5/5903f92e41e293b07707d5bf00ef39a0eb2af7190aff4beaf581a6591510/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd50659a069c15eef8aa3d64bbef0d69fd27bb4a50c9ab4f17f83a16cbf8905", size = 384372, upload-time = "2025-08-27T12:15:15.842Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e3/fbb409e18aeefc01e49f5922ac63d2d914328430e295c12183ce56ebf76b/rpds_py-0.27.1-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:c4b676c4ae3921649a15d28ed10025548e9b561ded473aa413af749503c6737e", size = 401264, upload-time = "2025-08-27T12:15:17.388Z" }, + { url = "https://files.pythonhosted.org/packages/55/79/529ad07794e05cb0f38e2f965fc5bb20853d523976719400acecc447ec9d/rpds_py-0.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:079bc583a26db831a985c5257797b2b5d3affb0386e7ff886256762f82113b5e", size = 418691, upload-time = "2025-08-27T12:15:19.144Z" }, + { url = "https://files.pythonhosted.org/packages/33/39/6554a7fd6d9906fda2521c6d52f5d723dca123529fb719a5b5e074c15e01/rpds_py-0.27.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4e44099bd522cba71a2c6b97f68e19f40e7d85399de899d66cdb67b32d7cb786", size = 558989, upload-time = "2025-08-27T12:15:21.087Z" }, + { url = "https://files.pythonhosted.org/packages/19/b2/76fa15173b6f9f445e5ef15120871b945fb8dd9044b6b8c7abe87e938416/rpds_py-0.27.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e202e6d4188e53c6661af813b46c37ca2c45e497fc558bacc1a7630ec2695aec", size = 589835, upload-time = "2025-08-27T12:15:22.696Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9e/5560a4b39bab780405bed8a88ee85b30178061d189558a86003548dea045/rpds_py-0.27.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f41f814b8eaa48768d1bb551591f6ba45f87ac76899453e8ccd41dba1289b04b", size = 555227, upload-time = "2025-08-27T12:15:24.278Z" }, + { url = "https://files.pythonhosted.org/packages/52/d7/cd9c36215111aa65724c132bf709c6f35175973e90b32115dedc4ced09cb/rpds_py-0.27.1-cp39-cp39-win32.whl", hash = "sha256:9e71f5a087ead99563c11fdaceee83ee982fd39cf67601f4fd66cb386336ee52", size = 217899, upload-time = "2025-08-27T12:15:25.926Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e0/d75ab7b4dd8ba777f6b365adbdfc7614bbfe7c5f05703031dfa4b61c3d6c/rpds_py-0.27.1-cp39-cp39-win_amd64.whl", hash = "sha256:71108900c9c3c8590697244b9519017a400d9ba26a36c48381b3f64743a44aab", size = 228725, upload-time = "2025-08-27T12:15:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/d5/63/b7cc415c345625d5e62f694ea356c58fb964861409008118f1245f8c3347/rpds_py-0.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf", size = 371360, upload-time = "2025-08-27T12:15:29.218Z" }, + { url = "https://files.pythonhosted.org/packages/e5/8c/12e1b24b560cf378b8ffbdb9dc73abd529e1adcfcf82727dfd29c4a7b88d/rpds_py-0.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3", size = 353933, upload-time = "2025-08-27T12:15:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/9b/85/1bb2210c1f7a1b99e91fea486b9f0f894aa5da3a5ec7097cbad7dec6d40f/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636", size = 382962, upload-time = "2025-08-27T12:15:32.348Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c9/a839b9f219cf80ed65f27a7f5ddbb2809c1b85c966020ae2dff490e0b18e/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8", size = 394412, upload-time = "2025-08-27T12:15:33.839Z" }, + { url = "https://files.pythonhosted.org/packages/02/2d/b1d7f928b0b1f4fc2e0133e8051d199b01d7384875adc63b6ddadf3de7e5/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc", size = 523972, upload-time = "2025-08-27T12:15:35.377Z" }, + { url = "https://files.pythonhosted.org/packages/a9/af/2cbf56edd2d07716df1aec8a726b3159deb47cb5c27e1e42b71d705a7c2f/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8", size = 403273, upload-time = "2025-08-27T12:15:37.051Z" }, + { url = "https://files.pythonhosted.org/packages/c0/93/425e32200158d44ff01da5d9612c3b6711fe69f606f06e3895511f17473b/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc", size = 385278, upload-time = "2025-08-27T12:15:38.571Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1a/1a04a915ecd0551bfa9e77b7672d1937b4b72a0fc204a17deef76001cfb2/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71", size = 402084, upload-time = "2025-08-27T12:15:40.529Z" }, + { url = "https://files.pythonhosted.org/packages/51/f7/66585c0fe5714368b62951d2513b684e5215beaceab2c6629549ddb15036/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad", size = 419041, upload-time = "2025-08-27T12:15:42.191Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7e/83a508f6b8e219bba2d4af077c35ba0e0cdd35a751a3be6a7cba5a55ad71/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab", size = 560084, upload-time = "2025-08-27T12:15:43.839Z" }, + { url = "https://files.pythonhosted.org/packages/66/66/bb945683b958a1b19eb0fe715594630d0f36396ebdef4d9b89c2fa09aa56/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059", size = 590115, upload-time = "2025-08-27T12:15:46.647Z" }, + { url = "https://files.pythonhosted.org/packages/12/00/ccfaafaf7db7e7adace915e5c2f2c2410e16402561801e9c7f96683002d3/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b", size = 556561, upload-time = "2025-08-27T12:15:48.219Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b7/92b6ed9aad103bfe1c45df98453dfae40969eef2cb6c6239c58d7e96f1b3/rpds_py-0.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819", size = 229125, upload-time = "2025-08-27T12:15:49.956Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ed/e1fba02de17f4f76318b834425257c8ea297e415e12c68b4361f63e8ae92/rpds_py-0.27.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df", size = 371402, upload-time = "2025-08-27T12:15:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/af/7c/e16b959b316048b55585a697e94add55a4ae0d984434d279ea83442e460d/rpds_py-0.27.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3", size = 354084, upload-time = "2025-08-27T12:15:53.219Z" }, + { url = "https://files.pythonhosted.org/packages/de/c1/ade645f55de76799fdd08682d51ae6724cb46f318573f18be49b1e040428/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9", size = 383090, upload-time = "2025-08-27T12:15:55.158Z" }, + { url = "https://files.pythonhosted.org/packages/1f/27/89070ca9b856e52960da1472efcb6c20ba27cfe902f4f23ed095b9cfc61d/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc", size = 394519, upload-time = "2025-08-27T12:15:57.238Z" }, + { url = "https://files.pythonhosted.org/packages/b3/28/be120586874ef906aa5aeeae95ae8df4184bc757e5b6bd1c729ccff45ed5/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4", size = 523817, upload-time = "2025-08-27T12:15:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/70cc197bc11cfcde02a86f36ac1eed15c56667c2ebddbdb76a47e90306da/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66", size = 403240, upload-time = "2025-08-27T12:16:00.923Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/46936cca449f7f518f2f4996e0e8344db4b57e2081e752441154089d2a5f/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e", size = 385194, upload-time = "2025-08-27T12:16:02.802Z" }, + { url = "https://files.pythonhosted.org/packages/e1/62/29c0d3e5125c3270b51415af7cbff1ec587379c84f55a5761cc9efa8cd06/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c", size = 402086, upload-time = "2025-08-27T12:16:04.806Z" }, + { url = "https://files.pythonhosted.org/packages/8f/66/03e1087679227785474466fdd04157fb793b3b76e3fcf01cbf4c693c1949/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf", size = 419272, upload-time = "2025-08-27T12:16:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/6a/24/e3e72d265121e00b063aef3e3501e5b2473cf1b23511d56e529531acf01e/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf", size = 560003, upload-time = "2025-08-27T12:16:08.06Z" }, + { url = "https://files.pythonhosted.org/packages/26/ca/f5a344c534214cc2d41118c0699fffbdc2c1bc7046f2a2b9609765ab9c92/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6", size = 590482, upload-time = "2025-08-27T12:16:10.137Z" }, + { url = "https://files.pythonhosted.org/packages/ce/08/4349bdd5c64d9d193c360aa9db89adeee6f6682ab8825dca0a3f535f434f/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a", size = 556523, upload-time = "2025-08-27T12:16:12.188Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ea/5463cd5048a7a2fcdae308b6e96432802132c141bfb9420260142632a0f1/rpds_py-0.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:aa8933159edc50be265ed22b401125c9eebff3171f570258854dbce3ecd55475", size = 371778, upload-time = "2025-08-27T12:16:13.851Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c8/f38c099db07f5114029c1467649d308543906933eebbc226d4527a5f4693/rpds_py-0.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a50431bf02583e21bf273c71b89d710e7a710ad5e39c725b14e685610555926f", size = 354394, upload-time = "2025-08-27T12:16:15.609Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/b76f97704d9dd8ddbd76fed4c4048153a847c5d6003afe20a6b5c3339065/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78af06ddc7fe5cc0e967085a9115accee665fb912c22a3f54bad70cc65b05fe6", size = 382348, upload-time = "2025-08-27T12:16:17.251Z" }, + { url = "https://files.pythonhosted.org/packages/8a/3f/ef23d3c1be1b837b648a3016d5bbe7cfe711422ad110b4081c0a90ef5a53/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:70d0738ef8fee13c003b100c2fbd667ec4f133468109b3472d249231108283a3", size = 394159, upload-time = "2025-08-27T12:16:19.251Z" }, + { url = "https://files.pythonhosted.org/packages/74/8a/9e62693af1a34fd28b1a190d463d12407bd7cf561748cb4745845d9548d3/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2f6fd8a1cea5bbe599b6e78a6e5ee08db434fc8ffea51ff201c8765679698b3", size = 522775, upload-time = "2025-08-27T12:16:20.929Z" }, + { url = "https://files.pythonhosted.org/packages/36/0d/8d5bb122bf7a60976b54c5c99a739a3819f49f02d69df3ea2ca2aff47d5c/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8177002868d1426305bb5de1e138161c2ec9eb2d939be38291d7c431c4712df8", size = 402633, upload-time = "2025-08-27T12:16:22.548Z" }, + { url = "https://files.pythonhosted.org/packages/0f/0e/237948c1f425e23e0cf5a566d702652a6e55c6f8fbd332a1792eb7043daf/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:008b839781d6c9bf3b6a8984d1d8e56f0ec46dc56df61fd669c49b58ae800400", size = 384867, upload-time = "2025-08-27T12:16:24.29Z" }, + { url = "https://files.pythonhosted.org/packages/d6/0a/da0813efcd998d260cbe876d97f55b0f469ada8ba9cbc47490a132554540/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:a55b9132bb1ade6c734ddd2759c8dc132aa63687d259e725221f106b83a0e485", size = 401791, upload-time = "2025-08-27T12:16:25.954Z" }, + { url = "https://files.pythonhosted.org/packages/51/78/c6c9e8a8aaca416a6f0d1b6b4a6ee35b88fe2c5401d02235d0a056eceed2/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a46fdec0083a26415f11d5f236b79fa1291c32aaa4a17684d82f7017a1f818b1", size = 419525, upload-time = "2025-08-27T12:16:27.659Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/5af37e1d71487cf6d56dd1420dc7e0c2732c1b6ff612aa7a88374061c0a8/rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:8a63b640a7845f2bdd232eb0d0a4a2dd939bcdd6c57e6bb134526487f3160ec5", size = 559255, upload-time = "2025-08-27T12:16:29.343Z" }, + { url = "https://files.pythonhosted.org/packages/40/7f/8b7b136069ef7ac3960eda25d832639bdb163018a34c960ed042dd1707c8/rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:7e32721e5d4922deaaf963469d795d5bde6093207c52fec719bd22e5d1bedbc4", size = 590384, upload-time = "2025-08-27T12:16:31.005Z" }, + { url = "https://files.pythonhosted.org/packages/d8/06/c316d3f6ff03f43ccb0eba7de61376f8ec4ea850067dddfafe98274ae13c/rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:2c426b99a068601b5f4623573df7a7c3d72e87533a2dd2253353a03e7502566c", size = 555959, upload-time = "2025-08-27T12:16:32.73Z" }, + { url = "https://files.pythonhosted.org/packages/60/94/384cf54c430b9dac742bbd2ec26c23feb78ded0d43d6d78563a281aec017/rpds_py-0.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4fc9b7fe29478824361ead6e14e4f5aed570d477e06088826537e202d25fe859", size = 228784, upload-time = "2025-08-27T12:16:34.428Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.29.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version >= '3.11' and python_full_version < '3.13'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/98/33/23b3b3419b6a3e0f559c7c0d2ca8fc1b9448382b25245033788785921332/rpds_py-0.29.0.tar.gz", hash = "sha256:fe55fe686908f50154d1dc599232016e50c243b438c3b7432f24e2895b0e5359", size = 69359, upload-time = "2025-11-16T14:50:39.532Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/7a/c5b2ff381b74bc742768e8d870f26babac4ef256ba160bdbf8d57af56461/rpds_py-0.29.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:4ae4b88c6617e1b9e5038ab3fccd7bac0842fdda2b703117b2aa99bc85379113", size = 372385, upload-time = "2025-11-16T14:47:36.287Z" }, + { url = "https://files.pythonhosted.org/packages/28/36/531f1eb4d5bed4a9c150f363a7ec4a98d2dc746151bba5473bc38ee85dec/rpds_py-0.29.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7d9128ec9d8cecda6f044001fde4fb71ea7c24325336612ef8179091eb9596b9", size = 362869, upload-time = "2025-11-16T14:47:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/54/df/7e9c0493a2015d9c82807a2d5f023ea9774e27a4c15b33ef1cdb7456138d/rpds_py-0.29.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37812c3da8e06f2bb35b3cf10e4a7b68e776a706c13058997238762b4e07f4f", size = 391582, upload-time = "2025-11-16T14:47:39.746Z" }, + { url = "https://files.pythonhosted.org/packages/15/38/42a981c3592ef46fbd7e17adbf8730cc5ec87e6aa1770c658c44bbb52960/rpds_py-0.29.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:66786c3fb1d8de416a7fa8e1cb1ec6ba0a745b2b0eee42f9b7daa26f1a495545", size = 405685, upload-time = "2025-11-16T14:47:41.472Z" }, + { url = "https://files.pythonhosted.org/packages/12/45/628b8c15856c3849c3f52ec6dac93c046ed5faeed4a435af03b70525fd29/rpds_py-0.29.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58f5c77f1af888b5fd1876c9a0d9858f6f88a39c9dd7c073a88e57e577da66d", size = 527067, upload-time = "2025-11-16T14:47:43.036Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ba/6b56d09badeabd95098016d72a437d4a0fd82d4672ce92a7607df5d70a42/rpds_py-0.29.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:799156ef1f3529ed82c36eb012b5d7a4cf4b6ef556dd7cc192148991d07206ae", size = 412532, upload-time = "2025-11-16T14:47:44.484Z" }, + { url = "https://files.pythonhosted.org/packages/f1/39/2f1f3db92888314b50b8f9641f679188bd24b3665a8cb9923b7201ae8011/rpds_py-0.29.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:453783477aa4f2d9104c4b59b08c871431647cb7af51b549bbf2d9eb9c827756", size = 392736, upload-time = "2025-11-16T14:47:46.053Z" }, + { url = "https://files.pythonhosted.org/packages/60/43/3c3b1dcd827e50f2ae28786d846b8a351080d8a69a3b49bc10ae44cc39b1/rpds_py-0.29.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:24a7231493e3c4a4b30138b50cca089a598e52c34cf60b2f35cebf62f274fdea", size = 406300, upload-time = "2025-11-16T14:47:47.268Z" }, + { url = "https://files.pythonhosted.org/packages/da/02/bc96021b67f8525e6bcdd68935c4543ada61e1f3dcb067ed037d68b8c6d2/rpds_py-0.29.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7033c1010b1f57bb44d8067e8c25aa6fa2e944dbf46ccc8c92b25043839c3fd2", size = 423641, upload-time = "2025-11-16T14:47:48.878Z" }, + { url = "https://files.pythonhosted.org/packages/38/e9/c435ddb602ced19a80b8277a41371734f33ad3f91cc4ceb4d82596800a3c/rpds_py-0.29.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0248b19405422573621172ab8e3a1f29141362d13d9f72bafa2e28ea0cdca5a2", size = 574153, upload-time = "2025-11-16T14:47:50.435Z" }, + { url = "https://files.pythonhosted.org/packages/84/82/dc3c32e1f89ecba8a59600d4cd65fe0ad81b6c636ccdbf6cd177fd6a7bac/rpds_py-0.29.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f9f436aee28d13b9ad2c764fc273e0457e37c2e61529a07b928346b219fcde3b", size = 600304, upload-time = "2025-11-16T14:47:51.599Z" }, + { url = "https://files.pythonhosted.org/packages/35/98/785290e0b7142470735dc1b1f68fb33aae29e5296f062c88396eedf796c8/rpds_py-0.29.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24a16cb7163933906c62c272de20ea3c228e4542c8c45c1d7dc2b9913e17369a", size = 562211, upload-time = "2025-11-16T14:47:53.094Z" }, + { url = "https://files.pythonhosted.org/packages/30/58/4eeddcb0737c6875f3e30c65dc9d7e7a10dfd5779646a990fa602c6d56c5/rpds_py-0.29.0-cp310-cp310-win32.whl", hash = "sha256:1a409b0310a566bfd1be82119891fefbdce615ccc8aa558aff7835c27988cbef", size = 221803, upload-time = "2025-11-16T14:47:54.404Z" }, + { url = "https://files.pythonhosted.org/packages/54/77/b35a8dbdcbeb32505500547cdafaa9f8863e85f8faac50ef34464ec5a256/rpds_py-0.29.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5523b0009e7c3c1263471b69d8da1c7d41b3ecb4cb62ef72be206b92040a950", size = 235530, upload-time = "2025-11-16T14:47:56.061Z" }, + { url = "https://files.pythonhosted.org/packages/36/ab/7fb95163a53ab122c74a7c42d2d2f012819af2cf3deb43fb0d5acf45cc1a/rpds_py-0.29.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9b9c764a11fd637e0322a488560533112837f5334ffeb48b1be20f6d98a7b437", size = 372344, upload-time = "2025-11-16T14:47:57.279Z" }, + { url = "https://files.pythonhosted.org/packages/b3/45/f3c30084c03b0d0f918cb4c5ae2c20b0a148b51ba2b3f6456765b629bedd/rpds_py-0.29.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fd2164d73812026ce970d44c3ebd51e019d2a26a4425a5dcbdfa93a34abc383", size = 363041, upload-time = "2025-11-16T14:47:58.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e9/4d044a1662608c47a87cbb37b999d4d5af54c6d6ebdda93a4d8bbf8b2a10/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a097b7f7f7274164566ae90a221fd725363c0e9d243e2e9ed43d195ccc5495c", size = 391775, upload-time = "2025-11-16T14:48:00.197Z" }, + { url = "https://files.pythonhosted.org/packages/50/c9/7616d3ace4e6731aeb6e3cd85123e03aec58e439044e214b9c5c60fd8eb1/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cdc0490374e31cedefefaa1520d5fe38e82fde8748cbc926e7284574c714d6b", size = 405624, upload-time = "2025-11-16T14:48:01.496Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e2/6d7d6941ca0843609fd2d72c966a438d6f22617baf22d46c3d2156c31350/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89ca2e673ddd5bde9b386da9a0aac0cab0e76f40c8f0aaf0d6311b6bbf2aa311", size = 527894, upload-time = "2025-11-16T14:48:03.167Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f7/aee14dc2db61bb2ae1e3068f134ca9da5f28c586120889a70ff504bb026f/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5d9da3ff5af1ca1249b1adb8ef0573b94c76e6ae880ba1852f033bf429d4588", size = 412720, upload-time = "2025-11-16T14:48:04.413Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e2/2293f236e887c0360c2723d90c00d48dee296406994d6271faf1712e94ec/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8238d1d310283e87376c12f658b61e1ee23a14c0e54c7c0ce953efdbdc72deed", size = 392945, upload-time = "2025-11-16T14:48:06.252Z" }, + { url = "https://files.pythonhosted.org/packages/14/cd/ceea6147acd3bd1fd028d1975228f08ff19d62098078d5ec3eed49703797/rpds_py-0.29.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2d6fb2ad1c36f91c4646989811e84b1ea5e0c3cf9690b826b6e32b7965853a63", size = 406385, upload-time = "2025-11-16T14:48:07.575Z" }, + { url = "https://files.pythonhosted.org/packages/52/36/fe4dead19e45eb77a0524acfdbf51e6cda597b26fc5b6dddbff55fbbb1a5/rpds_py-0.29.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:534dc9df211387547267ccdb42253aa30527482acb38dd9b21c5c115d66a96d2", size = 423943, upload-time = "2025-11-16T14:48:10.175Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7b/4551510803b582fa4abbc8645441a2d15aa0c962c3b21ebb380b7e74f6a1/rpds_py-0.29.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d456e64724a075441e4ed648d7f154dc62e9aabff29bcdf723d0c00e9e1d352f", size = 574204, upload-time = "2025-11-16T14:48:11.499Z" }, + { url = "https://files.pythonhosted.org/packages/64/ba/071ccdd7b171e727a6ae079f02c26f75790b41555f12ca8f1151336d2124/rpds_py-0.29.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a738f2da2f565989401bd6fd0b15990a4d1523c6d7fe83f300b7e7d17212feca", size = 600587, upload-time = "2025-11-16T14:48:12.822Z" }, + { url = "https://files.pythonhosted.org/packages/03/09/96983d48c8cf5a1e03c7d9cc1f4b48266adfb858ae48c7c2ce978dbba349/rpds_py-0.29.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a110e14508fd26fd2e472bb541f37c209409876ba601cf57e739e87d8a53cf95", size = 562287, upload-time = "2025-11-16T14:48:14.108Z" }, + { url = "https://files.pythonhosted.org/packages/40/f0/8c01aaedc0fa92156f0391f39ea93b5952bc0ec56b897763858f95da8168/rpds_py-0.29.0-cp311-cp311-win32.whl", hash = "sha256:923248a56dd8d158389a28934f6f69ebf89f218ef96a6b216a9be6861804d3f4", size = 221394, upload-time = "2025-11-16T14:48:15.374Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a5/a8b21c54c7d234efdc83dc034a4d7cd9668e3613b6316876a29b49dece71/rpds_py-0.29.0-cp311-cp311-win_amd64.whl", hash = "sha256:539eb77eb043afcc45314d1be09ea6d6cafb3addc73e0547c171c6d636957f60", size = 235713, upload-time = "2025-11-16T14:48:16.636Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1f/df3c56219523947b1be402fa12e6323fe6d61d883cf35d6cb5d5bb6db9d9/rpds_py-0.29.0-cp311-cp311-win_arm64.whl", hash = "sha256:bdb67151ea81fcf02d8f494703fb728d4d34d24556cbff5f417d74f6f5792e7c", size = 229157, upload-time = "2025-11-16T14:48:17.891Z" }, + { url = "https://files.pythonhosted.org/packages/3c/50/bc0e6e736d94e420df79be4deb5c9476b63165c87bb8f19ef75d100d21b3/rpds_py-0.29.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0891cfd8db43e085c0ab93ab7e9b0c8fee84780d436d3b266b113e51e79f954", size = 376000, upload-time = "2025-11-16T14:48:19.141Z" }, + { url = "https://files.pythonhosted.org/packages/3e/3a/46676277160f014ae95f24de53bed0e3b7ea66c235e7de0b9df7bd5d68ba/rpds_py-0.29.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3897924d3f9a0361472d884051f9a2460358f9a45b1d85a39a158d2f8f1ad71c", size = 360575, upload-time = "2025-11-16T14:48:20.443Z" }, + { url = "https://files.pythonhosted.org/packages/75/ba/411d414ed99ea1afdd185bbabeeaac00624bd1e4b22840b5e9967ade6337/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a21deb8e0d1571508c6491ce5ea5e25669b1dd4adf1c9d64b6314842f708b5d", size = 392159, upload-time = "2025-11-16T14:48:22.12Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b1/e18aa3a331f705467a48d0296778dc1fea9d7f6cf675bd261f9a846c7e90/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9efe71687d6427737a0a2de9ca1c0a216510e6cd08925c44162be23ed7bed2d5", size = 410602, upload-time = "2025-11-16T14:48:23.563Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6c/04f27f0c9f2299274c76612ac9d2c36c5048bb2c6c2e52c38c60bf3868d9/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40f65470919dc189c833e86b2c4bd21bd355f98436a2cef9e0a9a92aebc8e57e", size = 515808, upload-time = "2025-11-16T14:48:24.949Z" }, + { url = "https://files.pythonhosted.org/packages/83/56/a8412aa464fb151f8bc0d91fb0bb888adc9039bd41c1c6ba8d94990d8cf8/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:def48ff59f181130f1a2cb7c517d16328efac3ec03951cca40c1dc2049747e83", size = 416015, upload-time = "2025-11-16T14:48:26.782Z" }, + { url = "https://files.pythonhosted.org/packages/04/4c/f9b8a05faca3d9e0a6397c90d13acb9307c9792b2bff621430c58b1d6e76/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad7bd570be92695d89285a4b373006930715b78d96449f686af422debb4d3949", size = 395325, upload-time = "2025-11-16T14:48:28.055Z" }, + { url = "https://files.pythonhosted.org/packages/34/60/869f3bfbf8ed7b54f1ad9a5543e0fdffdd40b5a8f587fe300ee7b4f19340/rpds_py-0.29.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:5a572911cd053137bbff8e3a52d31c5d2dba51d3a67ad902629c70185f3f2181", size = 410160, upload-time = "2025-11-16T14:48:29.338Z" }, + { url = "https://files.pythonhosted.org/packages/91/aa/e5b496334e3aba4fe4c8a80187b89f3c1294c5c36f2a926da74338fa5a73/rpds_py-0.29.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d583d4403bcbf10cffc3ab5cee23d7643fcc960dff85973fd3c2d6c86e8dbb0c", size = 425309, upload-time = "2025-11-16T14:48:30.691Z" }, + { url = "https://files.pythonhosted.org/packages/85/68/4e24a34189751ceb6d66b28f18159922828dd84155876551f7ca5b25f14f/rpds_py-0.29.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:070befbb868f257d24c3bb350dbd6e2f645e83731f31264b19d7231dd5c396c7", size = 574644, upload-time = "2025-11-16T14:48:31.964Z" }, + { url = "https://files.pythonhosted.org/packages/8c/cf/474a005ea4ea9c3b4f17b6108b6b13cebfc98ebaff11d6e1b193204b3a93/rpds_py-0.29.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fc935f6b20b0c9f919a8ff024739174522abd331978f750a74bb68abd117bd19", size = 601605, upload-time = "2025-11-16T14:48:33.252Z" }, + { url = "https://files.pythonhosted.org/packages/f4/b1/c56f6a9ab8c5f6bb5c65c4b5f8229167a3a525245b0773f2c0896686b64e/rpds_py-0.29.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8c5a8ecaa44ce2d8d9d20a68a2483a74c07f05d72e94a4dff88906c8807e77b0", size = 564593, upload-time = "2025-11-16T14:48:34.643Z" }, + { url = "https://files.pythonhosted.org/packages/b3/13/0494cecce4848f68501e0a229432620b4b57022388b071eeff95f3e1e75b/rpds_py-0.29.0-cp312-cp312-win32.whl", hash = "sha256:ba5e1aeaf8dd6d8f6caba1f5539cddda87d511331714b7b5fc908b6cfc3636b7", size = 223853, upload-time = "2025-11-16T14:48:36.419Z" }, + { url = "https://files.pythonhosted.org/packages/1f/6a/51e9aeb444a00cdc520b032a28b07e5f8dc7bc328b57760c53e7f96997b4/rpds_py-0.29.0-cp312-cp312-win_amd64.whl", hash = "sha256:b5f6134faf54b3cb83375db0f113506f8b7770785be1f95a631e7e2892101977", size = 239895, upload-time = "2025-11-16T14:48:37.956Z" }, + { url = "https://files.pythonhosted.org/packages/d1/d4/8bce56cdad1ab873e3f27cb31c6a51d8f384d66b022b820525b879f8bed1/rpds_py-0.29.0-cp312-cp312-win_arm64.whl", hash = "sha256:b016eddf00dca7944721bf0cd85b6af7f6c4efaf83ee0b37c4133bd39757a8c7", size = 230321, upload-time = "2025-11-16T14:48:39.71Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d9/c5de60d9d371bbb186c3e9bf75f4fc5665e11117a25a06a6b2e0afb7380e/rpds_py-0.29.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1585648d0760b88292eecab5181f5651111a69d90eff35d6b78aa32998886a61", size = 375710, upload-time = "2025-11-16T14:48:41.063Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b3/0860cdd012291dc21272895ce107f1e98e335509ba986dd83d72658b82b9/rpds_py-0.29.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:521807963971a23996ddaf764c682b3e46459b3c58ccd79fefbe16718db43154", size = 360582, upload-time = "2025-11-16T14:48:42.423Z" }, + { url = "https://files.pythonhosted.org/packages/92/8a/a18c2f4a61b3407e56175f6aab6deacdf9d360191a3d6f38566e1eaf7266/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a8896986efaa243ab713c69e6491a4138410f0fe36f2f4c71e18bd5501e8014", size = 391172, upload-time = "2025-11-16T14:48:43.75Z" }, + { url = "https://files.pythonhosted.org/packages/fd/49/e93354258508c50abc15cdcd5fcf7ac4117f67bb6233ad7859f75e7372a0/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1d24564a700ef41480a984c5ebed62b74e6ce5860429b98b1fede76049e953e6", size = 409586, upload-time = "2025-11-16T14:48:45.498Z" }, + { url = "https://files.pythonhosted.org/packages/5a/8d/a27860dae1c19a6bdc901f90c81f0d581df1943355802961a57cdb5b6cd1/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6596b93c010d386ae46c9fba9bfc9fc5965fa8228edeac51576299182c2e31c", size = 516339, upload-time = "2025-11-16T14:48:47.308Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ad/a75e603161e79b7110c647163d130872b271c6b28712c803c65d492100f7/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5cc58aac218826d054c7da7f95821eba94125d88be673ff44267bb89d12a5866", size = 416201, upload-time = "2025-11-16T14:48:48.615Z" }, + { url = "https://files.pythonhosted.org/packages/b9/42/555b4ee17508beafac135c8b450816ace5a96194ce97fefc49d58e5652ea/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de73e40ebc04dd5d9556f50180395322193a78ec247e637e741c1b954810f295", size = 395095, upload-time = "2025-11-16T14:48:50.027Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f0/c90b671b9031e800ec45112be42ea9f027f94f9ac25faaac8770596a16a1/rpds_py-0.29.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:295ce5ac7f0cf69a651ea75c8f76d02a31f98e5698e82a50a5f4d4982fbbae3b", size = 410077, upload-time = "2025-11-16T14:48:51.515Z" }, + { url = "https://files.pythonhosted.org/packages/3d/80/9af8b640b81fe21e6f718e9dec36c0b5f670332747243130a5490f292245/rpds_py-0.29.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1ea59b23ea931d494459c8338056fe7d93458c0bf3ecc061cd03916505369d55", size = 424548, upload-time = "2025-11-16T14:48:53.237Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0b/b5647446e991736e6a495ef510e6710df91e880575a586e763baeb0aa770/rpds_py-0.29.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f49d41559cebd608042fdcf54ba597a4a7555b49ad5c1c0c03e0af82692661cd", size = 573661, upload-time = "2025-11-16T14:48:54.769Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b3/1b1c9576839ff583d1428efbf59f9ee70498d8ce6c0b328ac02f1e470879/rpds_py-0.29.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:05a2bd42768ea988294ca328206efbcc66e220d2d9b7836ee5712c07ad6340ea", size = 600937, upload-time = "2025-11-16T14:48:56.247Z" }, + { url = "https://files.pythonhosted.org/packages/6c/7b/b6cfca2f9fee4c4494ce54f7fb1b9f578867495a9aa9fc0d44f5f735c8e0/rpds_py-0.29.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33ca7bdfedd83339ca55da3a5e1527ee5870d4b8369456b5777b197756f3ca22", size = 564496, upload-time = "2025-11-16T14:48:57.691Z" }, + { url = "https://files.pythonhosted.org/packages/b9/fb/ba29ec7f0f06eb801bac5a23057a9ff7670623b5e8013bd59bec4aa09de8/rpds_py-0.29.0-cp313-cp313-win32.whl", hash = "sha256:20c51ae86a0bb9accc9ad4e6cdeec58d5ebb7f1b09dd4466331fc65e1766aae7", size = 223126, upload-time = "2025-11-16T14:48:59.058Z" }, + { url = "https://files.pythonhosted.org/packages/3c/6b/0229d3bed4ddaa409e6d90b0ae967ed4380e4bdd0dad6e59b92c17d42457/rpds_py-0.29.0-cp313-cp313-win_amd64.whl", hash = "sha256:6410e66f02803600edb0b1889541f4b5cc298a5ccda0ad789cc50ef23b54813e", size = 239771, upload-time = "2025-11-16T14:49:00.872Z" }, + { url = "https://files.pythonhosted.org/packages/e4/38/d2868f058b164f8efd89754d85d7b1c08b454f5c07ac2e6cc2e9bd4bd05b/rpds_py-0.29.0-cp313-cp313-win_arm64.whl", hash = "sha256:56838e1cd9174dc23c5691ee29f1d1be9eab357f27efef6bded1328b23e1ced2", size = 229994, upload-time = "2025-11-16T14:49:02.673Z" }, + { url = "https://files.pythonhosted.org/packages/52/91/5de91c5ec7d41759beec9b251630824dbb8e32d20c3756da1a9a9d309709/rpds_py-0.29.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:37d94eadf764d16b9a04307f2ab1d7af6dc28774bbe0535c9323101e14877b4c", size = 365886, upload-time = "2025-11-16T14:49:04.133Z" }, + { url = "https://files.pythonhosted.org/packages/85/7c/415d8c1b016d5f47ecec5145d9d6d21002d39dce8761b30f6c88810b455a/rpds_py-0.29.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d472cf73efe5726a067dce63eebe8215b14beabea7c12606fd9994267b3cfe2b", size = 355262, upload-time = "2025-11-16T14:49:05.543Z" }, + { url = "https://files.pythonhosted.org/packages/3d/14/bf83e2daa4f980e4dc848aed9299792a8b84af95e12541d9e7562f84a6ef/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72fdfd5ff8992e4636621826371e3ac5f3e3b8323e9d0e48378e9c13c3dac9d0", size = 384826, upload-time = "2025-11-16T14:49:07.301Z" }, + { url = "https://files.pythonhosted.org/packages/33/b8/53330c50a810ae22b4fbba5e6cf961b68b9d72d9bd6780a7c0a79b070857/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2549d833abdf8275c901313b9e8ff8fba57e50f6a495035a2a4e30621a2f7cc4", size = 394234, upload-time = "2025-11-16T14:49:08.782Z" }, + { url = "https://files.pythonhosted.org/packages/cc/32/01e2e9645cef0e584f518cfde4567563e57db2257244632b603f61b40e50/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4448dad428f28a6a767c3e3b80cde3446a22a0efbddaa2360f4bb4dc836d0688", size = 520008, upload-time = "2025-11-16T14:49:10.253Z" }, + { url = "https://files.pythonhosted.org/packages/98/c3/0d1b95a81affae2b10f950782e33a1fd2edd6ce2a479966cac98c9a66f57/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:115f48170fd4296a33938d8c11f697f5f26e0472e43d28f35624764173a60e4d", size = 409569, upload-time = "2025-11-16T14:49:12.478Z" }, + { url = "https://files.pythonhosted.org/packages/fa/60/aa3b8678f3f009f675b99174fa2754302a7fbfe749162e8043d111de2d88/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e5bb73ffc029820f4348e9b66b3027493ae00bca6629129cd433fd7a76308ee", size = 385188, upload-time = "2025-11-16T14:49:13.88Z" }, + { url = "https://files.pythonhosted.org/packages/92/02/5546c1c8aa89c18d40c1fcffdcc957ba730dee53fb7c3ca3a46f114761d2/rpds_py-0.29.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:b1581fcde18fcdf42ea2403a16a6b646f8eb1e58d7f90a0ce693da441f76942e", size = 398587, upload-time = "2025-11-16T14:49:15.339Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e0/ad6eeaf47e236eba052fa34c4073078b9e092bd44da6bbb35aaae9580669/rpds_py-0.29.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16e9da2bda9eb17ea318b4c335ec9ac1818e88922cbe03a5743ea0da9ecf74fb", size = 416641, upload-time = "2025-11-16T14:49:16.832Z" }, + { url = "https://files.pythonhosted.org/packages/1a/93/0acedfd50ad9cdd3879c615a6dc8c5f1ce78d2fdf8b87727468bb5bb4077/rpds_py-0.29.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:28fd300326dd21198f311534bdb6d7e989dd09b3418b3a91d54a0f384c700967", size = 566683, upload-time = "2025-11-16T14:49:18.342Z" }, + { url = "https://files.pythonhosted.org/packages/62/53/8c64e0f340a9e801459fc6456821abc15b3582cb5dc3932d48705a9d9ac7/rpds_py-0.29.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2aba991e041d031c7939e1358f583ae405a7bf04804ca806b97a5c0e0af1ea5e", size = 592730, upload-time = "2025-11-16T14:49:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/85/ef/3109b6584f8c4b0d2490747c916df833c127ecfa82be04d9a40a376f2090/rpds_py-0.29.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f437026dbbc3f08c99cc41a5b2570c6e1a1ddbe48ab19a9b814254128d4ea7a", size = 557361, upload-time = "2025-11-16T14:49:21.574Z" }, + { url = "https://files.pythonhosted.org/packages/ff/3b/61586475e82d57f01da2c16edb9115a618afe00ce86fe1b58936880b15af/rpds_py-0.29.0-cp313-cp313t-win32.whl", hash = "sha256:6e97846e9800a5d0fe7be4d008f0c93d0feeb2700da7b1f7528dabafb31dfadb", size = 211227, upload-time = "2025-11-16T14:49:23.03Z" }, + { url = "https://files.pythonhosted.org/packages/3b/3a/12dc43f13594a54ea0c9d7e9d43002116557330e3ad45bc56097ddf266e2/rpds_py-0.29.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f49196aec7c4b406495f60e6f947ad71f317a765f956d74bbd83996b9edc0352", size = 225248, upload-time = "2025-11-16T14:49:24.841Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/0b1474e7899371d9540d3bbb2a499a3427ae1fc39c998563fe9035a1073b/rpds_py-0.29.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:394d27e4453d3b4d82bb85665dc1fcf4b0badc30fc84282defed71643b50e1a1", size = 363731, upload-time = "2025-11-16T14:49:26.683Z" }, + { url = "https://files.pythonhosted.org/packages/28/12/3b7cf2068d0a334ed1d7b385a9c3c8509f4c2bcba3d4648ea71369de0881/rpds_py-0.29.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55d827b2ae95425d3be9bc9a5838b6c29d664924f98146557f7715e331d06df8", size = 354343, upload-time = "2025-11-16T14:49:28.24Z" }, + { url = "https://files.pythonhosted.org/packages/eb/73/5afcf8924bc02a749416eda64e17ac9c9b28f825f4737385295a0e99b0c1/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc31a07ed352e5462d3ee1b22e89285f4ce97d5266f6d1169da1142e78045626", size = 385406, upload-time = "2025-11-16T14:49:29.943Z" }, + { url = "https://files.pythonhosted.org/packages/c8/37/5db736730662508535221737a21563591b6f43c77f2e388951c42f143242/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c4695dd224212f6105db7ea62197144230b808d6b2bba52238906a2762f1d1e7", size = 396162, upload-time = "2025-11-16T14:49:31.833Z" }, + { url = "https://files.pythonhosted.org/packages/70/0d/491c1017d14f62ce7bac07c32768d209a50ec567d76d9f383b4cfad19b80/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcae1770b401167f8b9e1e3f566562e6966ffa9ce63639916248a9e25fa8a244", size = 517719, upload-time = "2025-11-16T14:49:33.804Z" }, + { url = "https://files.pythonhosted.org/packages/d7/25/b11132afcb17cd5d82db173f0c8dab270ffdfaba43e5ce7a591837ae9649/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:90f30d15f45048448b8da21c41703b31c61119c06c216a1bf8c245812a0f0c17", size = 409498, upload-time = "2025-11-16T14:49:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7d/e6543cedfb2e6403a1845710a5ab0e0ccf8fc288e0b5af9a70bfe2c12053/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44a91e0ab77bdc0004b43261a4b8cd6d6b451e8d443754cfda830002b5745b32", size = 382743, upload-time = "2025-11-16T14:49:36.704Z" }, + { url = "https://files.pythonhosted.org/packages/75/11/a4ebc9f654293ae9fefb83b2b6be7f3253e85ea42a5db2f77d50ad19aaeb/rpds_py-0.29.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:4aa195e5804d32c682e453b34474f411ca108e4291c6a0f824ebdc30a91c973c", size = 400317, upload-time = "2025-11-16T14:49:39.132Z" }, + { url = "https://files.pythonhosted.org/packages/52/18/97677a60a81c7f0e5f64e51fb3f8271c5c8fcabf3a2df18e97af53d7c2bf/rpds_py-0.29.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7971bdb7bf4ee0f7e6f67fa4c7fbc6019d9850cc977d126904392d363f6f8318", size = 416979, upload-time = "2025-11-16T14:49:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/f0/69/28ab391a9968f6c746b2a2db181eaa4d16afaa859fedc9c2f682d19f7e18/rpds_py-0.29.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8ae33ad9ce580c7a47452c3b3f7d8a9095ef6208e0a0c7e4e2384f9fc5bf8212", size = 567288, upload-time = "2025-11-16T14:49:42.24Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d3/0c7afdcdb830eee94f5611b64e71354ffe6ac8df82d00c2faf2bfffd1d4e/rpds_py-0.29.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c661132ab2fb4eeede2ef69670fd60da5235209874d001a98f1542f31f2a8a94", size = 593157, upload-time = "2025-11-16T14:49:43.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ac/a0fcbc2feed4241cf26d32268c195eb88ddd4bd862adfc9d4b25edfba535/rpds_py-0.29.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bb78b3a0d31ac1bde132c67015a809948db751cb4e92cdb3f0b242e430b6ed0d", size = 554741, upload-time = "2025-11-16T14:49:45.557Z" }, + { url = "https://files.pythonhosted.org/packages/0f/f1/fcc24137c470df8588674a677f33719d5800ec053aaacd1de8a5d5d84d9e/rpds_py-0.29.0-cp314-cp314-win32.whl", hash = "sha256:f475f103488312e9bd4000bc890a95955a07b2d0b6e8884aef4be56132adbbf1", size = 215508, upload-time = "2025-11-16T14:49:47.562Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c7/1d169b2045512eac019918fc1021ea07c30e84a4343f9f344e3e0aa8c788/rpds_py-0.29.0-cp314-cp314-win_amd64.whl", hash = "sha256:b9cf2359a4fca87cfb6801fae83a76aedf66ee1254a7a151f1341632acf67f1b", size = 228125, upload-time = "2025-11-16T14:49:49.064Z" }, + { url = "https://files.pythonhosted.org/packages/be/36/0cec88aaba70ec4a6e381c444b0d916738497d27f0c30406e3d9fcbd3bc2/rpds_py-0.29.0-cp314-cp314-win_arm64.whl", hash = "sha256:9ba8028597e824854f0f1733d8b964e914ae3003b22a10c2c664cb6927e0feb9", size = 221992, upload-time = "2025-11-16T14:49:50.777Z" }, + { url = "https://files.pythonhosted.org/packages/b1/fa/a2e524631717c9c0eb5d90d30f648cfba6b731047821c994acacb618406c/rpds_py-0.29.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:e71136fd0612556b35c575dc2726ae04a1669e6a6c378f2240312cf5d1a2ab10", size = 366425, upload-time = "2025-11-16T14:49:52.691Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a4/6d43ebe0746ff694a30233f63f454aed1677bd50ab7a59ff6b2bb5ac61f2/rpds_py-0.29.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:76fe96632d53f3bf0ea31ede2f53bbe3540cc2736d4aec3b3801b0458499ef3a", size = 355282, upload-time = "2025-11-16T14:49:54.292Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a7/52fd8270e0320b09eaf295766ae81dd175f65394687906709b3e75c71d06/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9459a33f077130dbb2c7c3cea72ee9932271fb3126404ba2a2661e4fe9eb7b79", size = 384968, upload-time = "2025-11-16T14:49:55.857Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/e6bc526b7a14e1ef80579a52c1d4ad39260a058a51d66c6039035d14db9d/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5c9546cfdd5d45e562cc0444b6dddc191e625c62e866bf567a2c69487c7ad28a", size = 394714, upload-time = "2025-11-16T14:49:57.343Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3f/f0ade3954e7db95c791e7eaf978aa7e08a756d2046e8bdd04d08146ed188/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12597d11d97b8f7e376c88929a6e17acb980e234547c92992f9f7c058f1a7310", size = 520136, upload-time = "2025-11-16T14:49:59.162Z" }, + { url = "https://files.pythonhosted.org/packages/87/b3/07122ead1b97009715ab9d4082be6d9bd9546099b2b03fae37c3116f72be/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28de03cf48b8a9e6ec10318f2197b83946ed91e2891f651a109611be4106ac4b", size = 409250, upload-time = "2025-11-16T14:50:00.698Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/dcbee61fd1dc892aedcb1b489ba661313101aa82ec84b1a015d4c63ebfda/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd7951c964069039acc9d67a8ff1f0a7f34845ae180ca542b17dc1456b1f1808", size = 384940, upload-time = "2025-11-16T14:50:02.312Z" }, + { url = "https://files.pythonhosted.org/packages/47/11/914ecb6f3574cf9bf8b38aced4063e0f787d6e1eb30b181a7efbc6c1da9a/rpds_py-0.29.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:c07d107b7316088f1ac0177a7661ca0c6670d443f6fe72e836069025e6266761", size = 399392, upload-time = "2025-11-16T14:50:03.829Z" }, + { url = "https://files.pythonhosted.org/packages/f5/fd/2f4bd9433f58f816434bb934313584caa47dbc6f03ce5484df8ac8980561/rpds_py-0.29.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1de2345af363d25696969befc0c1688a6cb5e8b1d32b515ef84fc245c6cddba3", size = 416796, upload-time = "2025-11-16T14:50:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/79/a5/449f0281af33efa29d5c71014399d74842342ae908d8cd38260320167692/rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:00e56b12d2199ca96068057e1ae7f9998ab6e99cda82431afafd32f3ec98cca9", size = 566843, upload-time = "2025-11-16T14:50:07.243Z" }, + { url = "https://files.pythonhosted.org/packages/ab/32/0a6a1ccee2e37fcb1b7ba9afde762b77182dbb57937352a729c6cd3cf2bb/rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3919a3bbecee589300ed25000b6944174e07cd20db70552159207b3f4bbb45b8", size = 593956, upload-time = "2025-11-16T14:50:09.029Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3d/eb820f95dce4306f07a495ede02fb61bef36ea201d9137d4fcd5ab94ec1e/rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7fa2ccc312bbd91e43aa5e0869e46bc03278a3dddb8d58833150a18b0f0283a", size = 557288, upload-time = "2025-11-16T14:50:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f8/b8ff786f40470462a252918e0836e0db903c28e88e3eec66bc4a7856ee5d/rpds_py-0.29.0-cp314-cp314t-win32.whl", hash = "sha256:97c817863ffc397f1e6a6e9d2d89fe5408c0a9922dac0329672fb0f35c867ea5", size = 211382, upload-time = "2025-11-16T14:50:12.827Z" }, + { url = "https://files.pythonhosted.org/packages/c9/7f/1a65ae870bc9d0576aebb0c501ea5dccf1ae2178fe2821042150ebd2e707/rpds_py-0.29.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2023473f444752f0f82a58dfcbee040d0a1b3d1b3c2ec40e884bd25db6d117d2", size = 225919, upload-time = "2025-11-16T14:50:14.734Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ac/b97e80bf107159e5b9ba9c91df1ab95f69e5e41b435f27bdd737f0d583ac/rpds_py-0.29.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:acd82a9e39082dc5f4492d15a6b6c8599aa21db5c35aaf7d6889aea16502c07d", size = 373963, upload-time = "2025-11-16T14:50:16.205Z" }, + { url = "https://files.pythonhosted.org/packages/40/5a/55e72962d5d29bd912f40c594e68880d3c7a52774b0f75542775f9250712/rpds_py-0.29.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:715b67eac317bf1c7657508170a3e011a1ea6ccb1c9d5f296e20ba14196be6b3", size = 364644, upload-time = "2025-11-16T14:50:18.22Z" }, + { url = "https://files.pythonhosted.org/packages/99/2a/6b6524d0191b7fc1351c3c0840baac42250515afb48ae40c7ed15499a6a2/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3b1b87a237cb2dba4db18bcfaaa44ba4cd5936b91121b62292ff21df577fc43", size = 393847, upload-time = "2025-11-16T14:50:20.012Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b8/c5692a7df577b3c0c7faed7ac01ee3c608b81750fc5d89f84529229b6873/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c3c3e8101bb06e337c88eb0c0ede3187131f19d97d43ea0e1c5407ea74c0cbf", size = 407281, upload-time = "2025-11-16T14:50:21.64Z" }, + { url = "https://files.pythonhosted.org/packages/f0/57/0546c6f84031b7ea08b76646a8e33e45607cc6bd879ff1917dc077bb881e/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8e54d6e61f3ecd3abe032065ce83ea63417a24f437e4a3d73d2f85ce7b7cfe", size = 529213, upload-time = "2025-11-16T14:50:23.219Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c1/01dd5f444233605555bc11fe5fed6a5c18f379f02013870c176c8e630a23/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3fbd4e9aebf110473a420dea85a238b254cf8a15acb04b22a5a6b5ce8925b760", size = 413808, upload-time = "2025-11-16T14:50:25.262Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0a/60f98b06156ea2a7af849fb148e00fbcfdb540909a5174a5ed10c93745c7/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fdf53d36e6c72819993e35d1ebeeb8e8fc688d0c6c2b391b55e335b3afba5a", size = 394600, upload-time = "2025-11-16T14:50:26.956Z" }, + { url = "https://files.pythonhosted.org/packages/37/f1/dc9312fc9bec040ece08396429f2bd9e0977924ba7a11c5ad7056428465e/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:ea7173df5d86f625f8dde6d5929629ad811ed8decda3b60ae603903839ac9ac0", size = 408634, upload-time = "2025-11-16T14:50:28.989Z" }, + { url = "https://files.pythonhosted.org/packages/ed/41/65024c9fd40c89bb7d604cf73beda4cbdbcebe92d8765345dd65855b6449/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:76054d540061eda273274f3d13a21a4abdde90e13eaefdc205db37c05230efce", size = 426064, upload-time = "2025-11-16T14:50:30.674Z" }, + { url = "https://files.pythonhosted.org/packages/a2/e0/cf95478881fc88ca2fdbf56381d7df36567cccc39a05394beac72182cd62/rpds_py-0.29.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:9f84c549746a5be3bc7415830747a3a0312573afc9f95785eb35228bb17742ec", size = 575871, upload-time = "2025-11-16T14:50:33.428Z" }, + { url = "https://files.pythonhosted.org/packages/ea/c0/df88097e64339a0218b57bd5f9ca49898e4c394db756c67fccc64add850a/rpds_py-0.29.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:0ea962671af5cb9a260489e311fa22b2e97103e3f9f0caaea6f81390af96a9ed", size = 601702, upload-time = "2025-11-16T14:50:36.051Z" }, + { url = "https://files.pythonhosted.org/packages/87/f4/09ffb3ebd0cbb9e2c7c9b84d252557ecf434cd71584ee1e32f66013824df/rpds_py-0.29.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:f7728653900035fb7b8d06e1e5900545d8088efc9d5d4545782da7df03ec803f", size = 564054, upload-time = "2025-11-16T14:50:37.733Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/f0/62b5a1a723fe183650109407fa56abb433b00aa1c0b9ba555f9c4efec2c6/ruff-0.14.6.tar.gz", hash = "sha256:6f0c742ca6a7783a736b867a263b9a7a80a45ce9bee391eeda296895f1b4e1cc", size = 5669501, upload-time = "2025-11-21T14:26:17.903Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/d2/7dd544116d107fffb24a0064d41a5d2ed1c9d6372d142f9ba108c8e39207/ruff-0.14.6-py3-none-linux_armv6l.whl", hash = "sha256:d724ac2f1c240dbd01a2ae98db5d1d9a5e1d9e96eba999d1c48e30062df578a3", size = 13326119, upload-time = "2025-11-21T14:25:24.2Z" }, + { url = "https://files.pythonhosted.org/packages/36/6a/ad66d0a3315d6327ed6b01f759d83df3c4d5f86c30462121024361137b6a/ruff-0.14.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9f7539ea257aa4d07b7ce87aed580e485c40143f2473ff2f2b75aee003186004", size = 13526007, upload-time = "2025-11-21T14:25:26.906Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9d/dae6db96df28e0a15dea8e986ee393af70fc97fd57669808728080529c37/ruff-0.14.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7f6007e55b90a2a7e93083ba48a9f23c3158c433591c33ee2e99a49b889c6332", size = 12676572, upload-time = "2025-11-21T14:25:29.826Z" }, + { url = "https://files.pythonhosted.org/packages/76/a4/f319e87759949062cfee1b26245048e92e2acce900ad3a909285f9db1859/ruff-0.14.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a8e7b9d73d8728b68f632aa8e824ef041d068d231d8dbc7808532d3629a6bef", size = 13140745, upload-time = "2025-11-21T14:25:32.788Z" }, + { url = "https://files.pythonhosted.org/packages/95/d3/248c1efc71a0a8ed4e8e10b4b2266845d7dfc7a0ab64354afe049eaa1310/ruff-0.14.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d50d45d4553a3ebcbd33e7c5e0fe6ca4aafd9a9122492de357205c2c48f00775", size = 13076486, upload-time = "2025-11-21T14:25:35.601Z" }, + { url = "https://files.pythonhosted.org/packages/a5/19/b68d4563fe50eba4b8c92aa842149bb56dd24d198389c0ed12e7faff4f7d/ruff-0.14.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:118548dd121f8a21bfa8ab2c5b80e5b4aed67ead4b7567790962554f38e598ce", size = 13727563, upload-time = "2025-11-21T14:25:38.514Z" }, + { url = "https://files.pythonhosted.org/packages/47/ac/943169436832d4b0e867235abbdb57ce3a82367b47e0280fa7b4eabb7593/ruff-0.14.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:57256efafbfefcb8748df9d1d766062f62b20150691021f8ab79e2d919f7c11f", size = 15199755, upload-time = "2025-11-21T14:25:41.516Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b9/288bb2399860a36d4bb0541cb66cce3c0f4156aaff009dc8499be0c24bf2/ruff-0.14.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff18134841e5c68f8e5df1999a64429a02d5549036b394fafbe410f886e1989d", size = 14850608, upload-time = "2025-11-21T14:25:44.428Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b1/a0d549dd4364e240f37e7d2907e97ee80587480d98c7799d2d8dc7a2f605/ruff-0.14.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29c4b7ec1e66a105d5c27bd57fa93203637d66a26d10ca9809dc7fc18ec58440", size = 14118754, upload-time = "2025-11-21T14:25:47.214Z" }, + { url = "https://files.pythonhosted.org/packages/13/ac/9b9fe63716af8bdfddfacd0882bc1586f29985d3b988b3c62ddce2e202c3/ruff-0.14.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:167843a6f78680746d7e226f255d920aeed5e4ad9c03258094a2d49d3028b105", size = 13949214, upload-time = "2025-11-21T14:25:50.002Z" }, + { url = "https://files.pythonhosted.org/packages/12/27/4dad6c6a77fede9560b7df6802b1b697e97e49ceabe1f12baf3ea20862e9/ruff-0.14.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:16a33af621c9c523b1ae006b1b99b159bf5ac7e4b1f20b85b2572455018e0821", size = 14106112, upload-time = "2025-11-21T14:25:52.841Z" }, + { url = "https://files.pythonhosted.org/packages/6a/db/23e322d7177873eaedea59a7932ca5084ec5b7e20cb30f341ab594130a71/ruff-0.14.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1432ab6e1ae2dc565a7eea707d3b03a0c234ef401482a6f1621bc1f427c2ff55", size = 13035010, upload-time = "2025-11-21T14:25:55.536Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9c/20e21d4d69dbb35e6a1df7691e02f363423658a20a2afacf2a2c011800dc/ruff-0.14.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4c55cfbbe7abb61eb914bfd20683d14cdfb38a6d56c6c66efa55ec6570ee4e71", size = 13054082, upload-time = "2025-11-21T14:25:58.625Z" }, + { url = "https://files.pythonhosted.org/packages/66/25/906ee6a0464c3125c8d673c589771a974965c2be1a1e28b5c3b96cb6ef88/ruff-0.14.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:efea3c0f21901a685fff4befda6d61a1bf4cb43de16da87e8226a281d614350b", size = 13303354, upload-time = "2025-11-21T14:26:01.816Z" }, + { url = "https://files.pythonhosted.org/packages/4c/58/60577569e198d56922b7ead07b465f559002b7b11d53f40937e95067ca1c/ruff-0.14.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:344d97172576d75dc6afc0e9243376dbe1668559c72de1864439c4fc95f78185", size = 14054487, upload-time = "2025-11-21T14:26:05.058Z" }, + { url = "https://files.pythonhosted.org/packages/67/0b/8e4e0639e4cc12547f41cb771b0b44ec8225b6b6a93393176d75fe6f7d40/ruff-0.14.6-py3-none-win32.whl", hash = "sha256:00169c0c8b85396516fdd9ce3446c7ca20c2a8f90a77aa945ba6b8f2bfe99e85", size = 13013361, upload-time = "2025-11-21T14:26:08.152Z" }, + { url = "https://files.pythonhosted.org/packages/fb/02/82240553b77fd1341f80ebb3eaae43ba011c7a91b4224a9f317d8e6591af/ruff-0.14.6-py3-none-win_amd64.whl", hash = "sha256:390e6480c5e3659f8a4c8d6a0373027820419ac14fa0d2713bd8e6c3e125b8b9", size = 14432087, upload-time = "2025-11-21T14:26:10.891Z" }, + { url = "https://files.pythonhosted.org/packages/a5/1f/93f9b0fad9470e4c829a5bb678da4012f0c710d09331b860ee555216f4ea/ruff-0.14.6-py3-none-win_arm64.whl", hash = "sha256:d43c81fbeae52cfa8728d8766bbf46ee4298c888072105815b392da70ca836b2", size = 13520930, upload-time = "2025-11-21T14:26:13.951Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + +[[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 = "spider-query-cronjob" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "cmsmonitoring" }, + { name = "htcondor" }, + { name = "ipykernel", version = "6.31.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "ipykernel", version = "7.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "opensearch-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "opensearch-py", version = "3.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "opentelemetry-exporter-otlp" }, + { name = "requests" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "cmsmonitoring", specifier = ">=0.6.13" }, + { name = "htcondor", specifier = "==23.0.28" }, + { name = "ipykernel", specifier = ">=6.31.0" }, + { name = "opensearch-py", specifier = ">=3.0.0" }, + { name = "opentelemetry-exporter-otlp", specifier = ">=1.39.0" }, + { name = "requests", specifier = ">=2.32.5" }, + { name = "ruff", specifier = ">=0.14.6" }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, +] + +[[package]] +name = "stomp-py" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docopt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/21/7c403c9083f28dfca179410b464777c743349cd245deb892960eeeae7467/stomp.py-7.0.0.tar.gz", hash = "sha256:fb301f338292b1b95089c6f1d3a38a9dd8353a5ff3f921e389dfa5f9413b5a8a", size = 34772, upload-time = "2021-04-08T21:32:56.586Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/a7/b3cd7150c8fcfbcd242fb9a862cc86c5a8795503ac6503c9d1a63cd2e7f5/stomp.py-7.0.0-py3-none-any.whl", hash = "sha256:6e1d93f2b2a7c63301f3e09e7ffa82ea80affec59164cd8c9b7807af4fe0e732", size = 37184, upload-time = "2021-04-08T21:32:54.772Z" }, +] + +[[package]] +name = "tornado" +version = "6.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/ce/1eb500eae19f4648281bb2186927bb062d2438c2e5093d1360391afd2f90/tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0", size = 510821, upload-time = "2025-08-08T18:27:00.78Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/48/6a7529df2c9cc12efd2e8f5dd219516184d703b34c06786809670df5b3bd/tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6", size = 442563, upload-time = "2025-08-08T18:26:42.945Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b5/9b575a0ed3e50b00c40b08cbce82eb618229091d09f6d14bce80fc01cb0b/tornado-6.5.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef", size = 440729, upload-time = "2025-08-08T18:26:44.473Z" }, + { url = "https://files.pythonhosted.org/packages/1b/4e/619174f52b120efcf23633c817fd3fed867c30bff785e2cd5a53a70e483c/tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e", size = 444295, upload-time = "2025-08-08T18:26:46.021Z" }, + { url = "https://files.pythonhosted.org/packages/95/fa/87b41709552bbd393c85dd18e4e3499dcd8983f66e7972926db8d96aa065/tornado-6.5.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882", size = 443644, upload-time = "2025-08-08T18:26:47.625Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/fb15f06e33d7430ca89420283a8762a4e6b8025b800ea51796ab5e6d9559/tornado-6.5.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108", size = 443878, upload-time = "2025-08-08T18:26:50.599Z" }, + { url = "https://files.pythonhosted.org/packages/11/92/fe6d57da897776ad2e01e279170ea8ae726755b045fe5ac73b75357a5a3f/tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c", size = 444549, upload-time = "2025-08-08T18:26:51.864Z" }, + { url = "https://files.pythonhosted.org/packages/9b/02/c8f4f6c9204526daf3d760f4aa555a7a33ad0e60843eac025ccfd6ff4a93/tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4", size = 443973, upload-time = "2025-08-08T18:26:53.625Z" }, + { url = "https://files.pythonhosted.org/packages/ae/2d/f5f5707b655ce2317190183868cd0f6822a1121b4baeae509ceb9590d0bd/tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04", size = 443954, upload-time = "2025-08-08T18:26:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/e8/59/593bd0f40f7355806bf6573b47b8c22f8e1374c9b6fd03114bd6b7a3dcfd/tornado-6.5.2-cp39-abi3-win32.whl", hash = "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0", size = 445023, upload-time = "2025-08-08T18:26:56.677Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2a/f609b420c2f564a748a2d80ebfb2ee02a73ca80223af712fca591386cafb/tornado-6.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f", size = 445427, upload-time = "2025-08-08T18:26:57.91Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4f/e1f65e8f8c76d73658b33d33b81eed4322fb5085350e4328d5c956f0c8f9/tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af", size = 444456, upload-time = "2025-08-08T18:26:59.207Z" }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, +] + +[[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 = "urllib3" +version = "1.26.20" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/e8/6ff5e6bc22095cfc59b6ea711b687e2b7ed4bdb373f7eeec370a97d7392f/urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32", size = 307380, upload-time = "2024-08-29T15:43:11.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/cf/8435d5a7159e2a9c83a95896ed596f68cf798005fe107cc655b5c5c14704/urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", size = 144225, upload-time = "2024-08-29T15:43:08.921Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version >= '3.11' and python_full_version < '3.13'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.2.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, +] + +[[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" }, +] diff --git a/docker/spider-worker/Dockerfile b/docker/spider-worker/Dockerfile new file mode 100644 index 00000000..83a07252 --- /dev/null +++ b/docker/spider-worker/Dockerfile @@ -0,0 +1,40 @@ +FROM python:3.11-slim + +ARG DOCKER_TAG=unknown +ARG IMAGE_NAME=unknown + +ENV DEBIAN_FRONTEND=noninteractive \ + PYTHONUNBUFFERED=1 \ + PYTHONPATH=/opt/spider-worker:/opt/spider-worker/src \ + SPIDER_WORKDIR=/opt/spider-worker \ + AFFILIATION_DIR_LOCATION=/opt/spider-worker/.affiliation_dir.json \ + GOPATH=/root/go \ + PATH=$PATH:/root/go/bin \ + DOCKER_TAG=${DOCKER_TAG} \ + IMAGE_NAME=${IMAGE_NAME} + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + ca-certificates \ + libexpat1 \ + golang \ + && rm -rf /var/lib/apt/lists/* && \ + useradd -ms /bin/bash cmsjobmon + +RUN go install github.com/nats-io/natscli/nats@v0.0.33 && \ + cp /root/go/bin/nats /usr/local/bin/nats + +COPY requirements.txt /opt/spider-worker/requirements.txt +RUN pip install --no-cache-dir --upgrade pip setuptools wheel && \ + pip install --no-cache-dir -r /opt/spider-worker/requirements.txt + +WORKDIR /opt/spider-worker + +COPY src ./src + +COPY main.py ./main.py + +RUN chown -R cmsjobmon:cmsjobmon /opt/spider-worker + +USER cmsjobmon + diff --git a/docker/spider-worker/main.py b/docker/spider-worker/main.py new file mode 100644 index 00000000..2134f1ee --- /dev/null +++ b/docker/spider-worker/main.py @@ -0,0 +1,17 @@ +from src.queues import process_nats_queue +import src.constants as const +from src.otel_setup import global_logger + + +def main(): + global_logger.info("Starting spider worker") + process_nats_queue( + nats_batch_size=const.NATS_BATCH_SIZE, + nats_fetch_timeout=const.NATS_FETCH_TIMEOUT, + nats_idle_sleep=const.NATS_IDLE_SLEEP, + metadata=None, + email_alerts=None + ) + +if __name__ == "__main__": + main() diff --git a/docker/spider-worker/requirements.txt b/docker/spider-worker/requirements.txt new file mode 100644 index 00000000..65970ee2 --- /dev/null +++ b/docker/spider-worker/requirements.txt @@ -0,0 +1,7 @@ +CMSMonitoring==0.6.12 +htcondor==23.0.28 +nats-py==2.6.0 +opensearch-py~=2.5 +elasticsearch==7.17.9 +requests~=2.31 +opentelemetry-exporter-otlp~=1.39.0 diff --git a/docker/spider-worker/src/affiliations.py b/docker/spider-worker/src/affiliations.py new file mode 100644 index 00000000..f99ab599 --- /dev/null +++ b/docker/spider-worker/src/affiliations.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Author: Christian Ariza +# pylint: disable=line-too-long +import asyncio +import json +import logging +from nats.aio.client import Client as NATS + +import constants as const + +_KV_KEY = "affiliations" + +logger = logging.getLogger(__name__) + + +class AffiliationException(Exception): + """ + Exception wrapper for problems that prevents us to obtain the affiliation info. + """ + pass + + +def get_affiliations_dict(nats_server=const.NATS_SERVER, kv_bucket_name=const.AFFILIATION_KV_BUCKET): + """ + Load affiliations dict from JetStream KV store. + + Returns a dictionary mapping login to affiliation info, e.g.: + { + 'valya': {'country': 'US', 'institute': 'Cornell University', 'dn': '...'}, + 'belforte': {'country': 'IT', 'institute': 'Universita e INFN Trieste', 'dn': '...'}, + ... + } + + Raises AffiliationManagerException if cache not found or cannot be loaded. + """ + try: + # Get or create event loop + try: + loop = asyncio.get_event_loop() + if loop.is_closed(): + raise RuntimeError("Event loop is closed") + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + # Connect to NATS + nats_connection = NATS() + loop.run_until_complete(nats_connection.connect(servers=nats_server)) + jetstream = nats_connection.jetstream() + + # Get KV store + kv = loop.run_until_complete(jetstream.key_value(kv_bucket_name)) + + # Get affiliations entry + entry = loop.run_until_complete(kv.get(_KV_KEY)) + if entry is None: + loop.run_until_complete(nats_connection.close()) + raise AffiliationException( + f"Affiliation cache not found in KV store {kv_bucket_name}. " + "Ensure the affiliation-cache cronjob has populated the cache." + ) + + # Parse JSON + affiliations = json.loads(entry.value.decode("utf-8")) + + # Close connection + loop.run_until_complete(nats_connection.close()) + + logger.info("Loaded %d affiliations from KV store %s", len(affiliations), kv_bucket_name) + return affiliations + + except Exception as e: + logger.error("Failed to load affiliations from KV store: %s", str(e)) + raise AffiliationException(f"Failed to load affiliations: {str(e)}") from e + + +def get_affiliation_by_dn(affiliations_dict, dn): + """ + Get affiliation info by DN (Distinguished Name). + + Args: + affiliations_dict: The affiliations dictionary from get_affiliations_dict() + dn: User DN (Distinguished Name) + + Returns: + Dictionary with 'institute' and 'country' keys, or None if not found. + """ + for affiliation in affiliations_dict.values(): + if affiliation.get("dn") == dn: + return affiliation + return None diff --git a/docker/spider-worker/src/amq.py b/docker/spider-worker/src/amq.py new file mode 100644 index 00000000..3ee3f49e --- /dev/null +++ b/docker/spider-worker/src/amq.py @@ -0,0 +1,49 @@ +import time +import logging +from CMSMonitoring.StompAMQ7 import StompAMQ7 as StompAMQ + +import constants as const +# TODO: remove this dependency +from convert_to_json import recordTime + +_amq_interface = None + + +def get_amq_interface(): + global _amq_interface + if not _amq_interface: + _amq_interface = StompAMQ( + username=const.AMQ_USERNAME, + password=const.AMQ_PASSWORD, + producer=const.AMQ_PRODUCER, + topic=const.AMQ_TOPIC, + host_and_ports=[(const.AMQ_BROKER, const.AMQ_PORT)], + validation_schema=const.SPIDER_VALIDATION_SCHEMA, + ) + + return _amq_interface + + +def post_ads(ads, metadata=None): + if not len(ads): + logging.warning("No new documents found") + return + + metadata = metadata or {} + interface = get_amq_interface() + list_data = [] + for id_, ad in ads: + notif, _, _ = interface.make_notification( + payload=ad, + doc_type=None, # will be default "metric" + doc_id=id_, + ts=recordTime(ad), + metadata=metadata, + data_subfield=None, + ) + list_data.append(notif) + + starttime = time.time() + failed_to_send = interface.send(list_data) + elapsed = time.time() - starttime + return len(ads) - len(failed_to_send), len(ads), elapsed diff --git a/docker/spider-worker/src/constants.py b/docker/spider-worker/src/constants.py new file mode 100644 index 00000000..f981aa97 --- /dev/null +++ b/docker/spider-worker/src/constants.py @@ -0,0 +1,52 @@ +import os + +# IMAGE_NAME has to start with `spider-`, otherwise the Opensearch logs index +# template won't get the data properly. +IMAGE_NAME = os.environ.get("IMAGE_NAME") + +TIMEOUT_MINS = os.environ.get("TIMEOUT_MINS", 60) +WORKDIR = os.getenv("SPIDER_WORKDIR", "/opt/spider-worker") +SECRET_DIR = os.environ.get("SECRET_DIR", "/etc/secrets") +DOCKER_TAG = os.environ.get("DOCKER_TAG", "unknown") +DRY_RUN = os.getenv("DRY_RUN", "false").lower() == "true" + + +# We reduce the data by default to save space in the Opensearch index. +REDUCE_DATA = os.getenv("REDUCE_DATA", "true").lower() == "true" + +# NATS +NATS_SERVER = os.environ.get("NATS_SERVER", "nats://nats.cluster.local:4222") +NATS_STREAM_NAME = os.environ.get("NATS_STREAM_NAME", "CMS_HTCONDOR_QUEUE") +NATS_SUBJECT = os.environ.get("NATS_SUBJECT", "cms.htcondor.queue.job") +NATS_CONSUMER_NAME = os.environ.get("NATS_CONSUMER_NAME", "spider-worker") +NATS_BATCH_SIZE = int(os.environ.get("NATS_BATCH_SIZE", "1000")) +NATS_FETCH_TIMEOUT = float(os.environ.get("NATS_FETCH_TIMEOUT", "0.5")) +NATS_IDLE_SLEEP = float(os.environ.get("NATS_IDLE_SLEEP", "0.2")) +NATS_CHUNK_SIZE = int(os.environ.get("NATS_CHUNK_SIZE", "200")) +NATS_ACK_WAIT_SECONDS = float(os.environ.get("NATS_ACK_WAIT_SECONDS", "120")) + +# Affiliation KV bucket +AFFILIATION_KV_BUCKET = os.environ.get("AFFILIATION_KV_BUCKET", "spider_affiliations") + +# OpenSearch +OS_HOST = os.environ.get("OS_HOST", "https://os-cms.cern.ch:443/os") +OS_INDEX_TEMPLATE = os.environ.get("OS_INDEX_TEMPLATE", "cms-htcondor-%Y-%m-%d") +OS_CERT_PATH = os.environ.get("OS_CERT_PATH", "/etc/pki/tls/certs/ca-bundle.pem") +OS_USERNAME = os.environ.get("OS_USERNAME") +OS_PASSWORD = os.environ.get("OS_PASSWORD") + +# AMQ +AMQ_USERNAME = os.environ.get("AMQ_USERNAME") +AMQ_PASSWORD = os.environ.get("AMQ_PASSWORD") +AMQ_PRODUCER = os.environ.get("AMQ_PRODUCER") # For testing, set to "condor-test" +AMQ_TOPIC = os.environ.get("AMQ_TOPIC") # For testing, set to "/topic/cms.jobmon.condor" +AMQ_BROKER = os.environ.get("AMQ_BROKER") # For testing, set to "cms-test-mb.cern.ch" +AMQ_PORT = os.environ.get("AMQ_PORT", 61313) +SPIDER_VALIDATION_SCHEMA = os.environ.get("SPIDER_VALIDATION_SCHEMA", "/etc/secrets/spider_validation_schema.json") + +# OpenTelemetry +OTEL_ENDPOINT = os.environ.get("OTEL_ENDPOINT", "opentelemetry-collector.opentelemetry.svc.cluster.local:4317") +OTEL_METRIC_EXPORT_INTERVAL = os.environ.get("OTEL_METRIC_EXPORT_INTERVAL", "15000") +OTEL_SERVICE_NAME = os.environ.get("OTEL_SERVICE_NAME", "spider-worker") +OTEL_USERNAME = os.environ.get("OPENTELEMETRY_USERNAME", "") +OTEL_PASSWORD = os.environ.get("OPENTELEMETRY_PASSWORD", "") diff --git a/docker/spider-worker/src/convert_to_json.py b/docker/spider-worker/src/convert_to_json.py new file mode 100644 index 00000000..f39eeb29 --- /dev/null +++ b/docker/spider-worker/src/convert_to_json.py @@ -0,0 +1,999 @@ +#!/usr/bin/python + +import re +import json +import time +import classad +import calendar +import logging +import datetime +import zlib +import base64 +from affiliations import ( + get_affiliations_dict, + get_affiliation_by_dn, + AffiliationException, +) +from vals import ( + string_vals, + int_vals, + date_vals, + bool_vals, + status, + universe, + postjob_status_decode, + running_fields, + ignore, +) + +_launch_time = int(time.time()) + +# Initialize affiliations dict +affiliations_dict = None +try: + affiliations_dict = get_affiliations_dict() +except AffiliationException as e: + # If its not possible to load affiliations + # Log it + logging.error("There were an error loading affiliations, %s", e) + # Continue execution without affiliation. + + +def make_list_from_string_field(ad, key, split_re=r"[\s,]+\s*", default=None): + default = default or ["UNKNOWN"] + try: + return re.split(split_re, ad[key]) + except (TypeError, KeyError): + return default + + +def get_creation_time_from_taskname(ad): + """ + returns the task creation date as a timestamp given the task name. + CRAB task names includes the creation time in format %y%m%d_%H%M%S: + 190309_085131:adeiorio_crab_80xV2_ST_t-channel_top_4f_scaleup_inclusiveDecays_13TeV-powhegV2-madspin-pythia8 + """ + try: + _str_date = ad["CRAB_Workflow"].split(":")[0] + _naive_date = datetime.datetime.strptime(_str_date, "%y%m%d_%H%M%S") + return int(calendar.timegm(_naive_date.timetuple())) + except (KeyError, TypeError, ValueError): + # fallback to recordtime if there is not a CRAB_Workflow value + # or if it hasn't the expected format. + return recordTime(ad) + + +_cream_re = re.compile(r"CPUNumber = (\d+)") +_nordugrid_re = re.compile(r"\(count=(\d+)\)") +_camp_re = re.compile(r"[A-Za-z0-9_]+_[A-Z0-9]+-([A-Za-z0-9]+)-") +_prep_re = re.compile(r"[A-Za-z0-9_]+_([A-Z]+-([A-Za-z0-9]+)-[0-9]+)") +_rval_re = re.compile(r"[A-Za-z0-9]+_(RVCMSSW_[0-9]+_[0-9]+_[0-9]+)") +_prep_prompt_re = re.compile(r"(PromptReco|Repack|Express)_[A-Za-z0-9]+_([A-Za-z0-9]+)") +_dynamic_bool_key_re = re.compile(r"^MachineAttrGLIDEIN_OVERLOAD_ENABLED\d+$") +# Executable error messages in WMCore +_wmcore_exe_exmsg = re.compile(r"^Chirp_WMCore_[A-Za-z0-9]+_Exception_Message$") +# 2016 reRECO; of the form cerminar_Run2016B-v2-JetHT-23Sep2016_8020_160923_164036_4747 +_rereco_re = re.compile(r"[A-Za-z0-9_]+_Run20[A-Za-z0-9-_]+-([A-Za-z0-9]+)") +_generic_site = re.compile(r"^[A-Za-z0-9]+_[A-Za-z0-9]+_(.*)_") +_cms_site = re.compile(r"CMS[A-Za-z]*_(.*)_") +_cmssw_version = re.compile(r"CMSSW_((\d*)_(\d*)_.*)") + +def process_ad( + ad, cms=True, return_dict=False, reduce_data=False, pool_name="Unknown" +): + if ad.get("TaskType") == "ROOT": + return None + result = {} + result["RecordTime"] = recordTime(ad) + result["DataCollection"] = ad.get("CompletionDate", 0) or _launch_time + result["DataCollectionDate"] = result["RecordTime"] + + result["ScheddName"] = ad.get("GlobalJobId", "UNKNOWN").split("#")[0] + result["CMS_Pool"] = pool_name + # Determine type + if cms: + result["Type"] = ad.get("CMS_Type", "unknown").lower() + analysis = isAnalysisJob(ad) + + if "CRAB_Id" in ad: + result["FormattedCrabId"] = get_formatted_CRAB_Id(ad.get("CRAB_Id")) + + if cms: + ad.setdefault( + "MATCH_EXP_JOB_GLIDEIN_CMSSite", + ad.get( + "MATCH_EXP_JOBGLIDEIN_CMSSite", + ad.get( + "MATCH_GLIDEIN_CMSSite", + ad.get( + "MachineAttrGLIDEIN_CMSSite0", + ad.get("MachineAttrCMSProcessingSiteName0", "Unknown"), + ), + ), + ), + ) + + bulk_convert_ad_data(ad, result) + + # Classify failed jobs + result["JobFailed"] = jobFailed(ad) + result["ErrorType"] = errorType(ad) + result["ErrorClass"] = errorClass(result) + result["ExitCode"] = commonExitCode(ad) + if "ExitCode" in ad: + result["CondorExitCode"] = ad["ExitCode"] + + if cms: + result["task"] = ad.get( + "WMAgent_SubTaskName" + ) # add "task" field to unify with WMArchive + result["CMS_JobType"] = str( + ad.get("CMS_JobType", "Analysis" if analysis else "Unknown") + ) + result["CRAB_AsyncDest"] = str(ad.get("CRAB_AsyncDest", "Unknown")) + result["WMAgent_TaskType"] = ad.get("WMAgent_SubTaskName", "/UNKNOWN").rsplit( + "/", 1 + )[-1] + result["CMS_CampaignType"] = guess_campaign_type(ad, analysis) + result["Campaign"] = guessCampaign(ad, analysis, result["CMS_CampaignType"]) + task_type = result.get("CMS_extendedJobType") + if task_type == "UNKNOWN" or task_type is None: + task_type = result.get( + "CMS_TaskType", result["CMS_JobType"] if analysis else guessTaskType(ad) + ) + result["TaskType"] = task_type + result["Workflow"] = guessWorkflow(ad, analysis) + now = time.time() + if ad.get("JobStatus") == 2 and (ad.get("EnteredCurrentStatus", now + 1) < now): + ad["RemoteWallClockTime"] = int(now - ad["EnteredCurrentStatus"]) + ad["CommittedTime"] = ad["RemoteWallClockTime"] + result["WallClockHr"] = ad.get("RemoteWallClockTime", 0) / 3600.0 + result["PilotRestLifeTimeMins"] = -1 + if analysis and ad.get("JobStatus") == 2 and "LastMatchTime" in ad: + try: + result["PilotRestLifeTimeMins"] = int( + (ad["MATCH_GLIDEIN_ToDie"] - ad["EnteredCurrentStatus"]) / 60 + ) + except (KeyError, ValueError, TypeError): + result["PilotRestLifeTimeMins"] = -72 * 60 + result["HasBeenTimingTuned"] = ad.get("HasBeenTimingTuned", False) + + if "RequestCpus" not in ad: + m = _cream_re.search(ad.get("CreamAttributes", "")) + m2 = _nordugrid_re.search(ad.get("NordugridRSL")) + if m: + try: + ad["RequestCpus"] = int(m.groups()[0]) + except: + pass + elif m2: + try: + ad["RequestCpus"] = int(m2.groups()[0]) + except: + pass + elif "xcount" in ad: + ad["RequestCpus"] = ad["xcount"] + ad.setdefault("RequestCpus", 1) + try: + ad["RequestCpus"] = int(ad.eval("RequestCpus")) + except: + ad["RequestCpus"] = 1.0 + result["RequestCpus"] = ad["RequestCpus"] + result["CoreHr"] = ( + ad.get("RequestCpus", 1.0) * int(ad.get("RemoteWallClockTime", 0)) / 3600.0 + ) + result["CommittedCoreHr"] = ( + ad.get("RequestCpus", 1.0) * ad.get("CommittedTime", 0) / 3600.0 + ) + result["CommittedWallClockHr"] = ad.get("CommittedTime", 0) / 3600.0 + result["CpuTimeHr"] = ( + ad.get("RemoteSysCpu", 0) + ad.get("RemoteUserCpu", 0) + ) / 3600.0 + result["DiskUsageGB"] = ad.get("DiskUsage_RAW", 0) / 1000000.0 + result["MemoryMB"] = ad.get("ResidentSetSize_RAW", 0) / 1024.0 + result["DataLocations"] = make_list_from_string_field( + ad, "DESIRED_CMSDataLocations" + ) + result["DESIRED_Sites"] = make_list_from_string_field(ad, "DESIRED_Sites") + result["Original_DESIRED_Sites"] = make_list_from_string_field( + ad, "ExtDESIRED_Sites" + ) + result["DesiredSiteCount"] = len(result["DESIRED_Sites"]) + result["DataLocationsCount"] = len(result["DataLocations"]) + result["CRAB_TaskCreationDate"] = get_creation_time_from_taskname(ad) + result["CMSPrimaryPrimaryDataset"] = "Unknown" + result["CMSPrimaryProcessedDataset"] = "Unknown" + result["CMSPrimaryDataTier"] = "Unknown" + if "DESIRED_CMSDataset" in result: + info = str(result["DESIRED_CMSDataset"]).split("/") + if len(info) > 3: + result["CMSPrimaryPrimaryDataset"] = info[1] + result["CMSPrimaryProcessedDataset"] = info[2] + result["CMSPrimaryDataTier"] = info[-1] + if cms and analysis: + result["OutputFiles"] = ( + len(ad.get("CRAB_AdditionalOutputFiles", [])) + + len(ad.get("CRAB_TFileOutputFiles", [])) + + len(ad.get("CRAB_EDMOutputFiles", [])) + + ad.get("CRAB_SaveLogsFlag", 0) + ) + if "x509UserProxyFQAN" in ad: + result["x509UserProxyFQAN"] = str(ad["x509UserProxyFQAN"]).split(",") + if "x509UserProxyVOName" in ad: + result["VO"] = str(ad["x509UserProxyVOName"]) + if cms: + result["CMSGroups"] = make_list_from_string_field(ad, "CMSGroups") + result["Site"] = ad.get("MATCH_EXP_JOB_GLIDEIN_CMSSite", "UNKNOWN") + if result["Site"].endswith("_Disk"): + result["Site"] = result["Site"].strip("_Disk") + elif ("GlideinEntryName" in ad) and ("MATCH_EXP_JOBGLIDEIN_ResourceName" not in ad): + m = _generic_site.match(ad["GlideinEntryName"]) + m2 = _cms_site.match(ad["GlideinEntryName"]) + if m2: + result["Site"] = m2.groups()[0] + info = result["Site"].split("_", 2) + if len(info) == 3: + result["Tier"] = info[0] + result["Country"] = info[1] + else: + result["Tier"] = "Unknown" + result["Country"] = "Unknown" + elif m: + result["Site"] = m.groups()[0] + else: + result["Site"] = "UNKNOWN" + else: + result["Site"] = ad.get("MATCH_EXP_JOBGLIDEIN_ResourceName", "UNKNOWN") + if cms: + info = result["Site"].split("_", 2) + if len(info) == 3: + result["Tier"] = info[0] + result["Country"] = info[1] + else: + result["Tier"] = "Unknown" + result["Country"] = "Unknown" + if "Site" not in result or "DESIRED_Sites" not in result: + result["InputData"] = "Unknown" + elif ("DESIRED_CMSDataLocations" not in result) or ( + result["DESIRED_CMSDataLocations"] is None + ): # CRAB2 case. + result["InputData"] = "Onsite" + elif result["Site"] in result["DESIRED_CMSDataLocations"]: + result["InputData"] = "Onsite" + elif (result["Site"] != "UNKNOWN") and (ad.get("JobStatus") != 1): + result["InputData"] = "Offsite" + if analysis: + if result["Site"] not in result["DESIRED_Sites"]: + result["OverflowType"] = "FrontendOverflow" + else: + result["OverflowType"] = "IgnoreLocality" + else: + result["OverflowType"] = "Unified" + if result["WallClockHr"] == 0: + result["CpuEff"] = 0 + else: + result["CpuEff"] = ( + 100 + * result["CpuTimeHr"] + / result["WallClockHr"] + / float(ad.get("RequestCpus", 1.0)) + ) + result["Status"] = status.get(ad.get("JobStatus"), "Unknown") + # Keep JobStatus for backwards compatibility with recordTime function. + result["JobStatus"] = result["Status"] + result["Universe"] = universe.get(ad.get("JobUniverse"), "Unknown") + result["QueueHrs"] = ( + ad.get("JobCurrentStartDate", time.time()) - ad["QDate"] + ) / 3600.0 + result["Badput"] = max(result["CoreHr"] - result["CommittedCoreHr"], 0.0) + result["CpuBadput"] = max(result["CoreHr"] - result["CpuTimeHr"], 0.0) + + handle_chirp_info(ad, result) + + # Parse CRAB3 information on CMSSW version + result["CMSSWVersion"] = "Unknown" + result["CMSSWMajorVersion"] = "Unknown" + result["CMSSWReleaseSeries"] = "Unknown" + if "CRAB_JobSW" in result: + match = _cmssw_version.match(result["CRAB_JobSW"]) + if match: + result["CMSSWVersion"] = match.group(1) + subv, ssubv = int(match.group(2)), int(match.group(3)) + result["CMSSWMajorVersion"] = "%d_X_X" % subv + result["CMSSWReleaseSeries"] = "%d_%d_X" % (subv, ssubv) + # Parse new machine statistics. + try: + cpus = float(result["GLIDEIN_Cpus"]) + result["BenchmarkJobHS06"] = float(ad["MachineAttrMJF_JOB_HS06_JOB0"]) / cpus + if result.get("EventRate", 0) > 0: + result["HS06EventRate"] = result["EventRate"] / result["BenchmarkJobHS06"] + if result.get("CpuEventRate", 0) > 0: + result["HS06CpuEventRate"] = ( + result["CpuEventRate"] / result["BenchmarkJobHS06"] + ) + if result.get("CpuTimePerEvent", 0) > 0: + result["HS06CpuTimePerEvent"] = ( + result["CpuTimePerEvent"] * result["BenchmarkJobHS06"] + ) + if result.get("TimePerEvent", 0) > 0: + result["HS06TimePerEvent"] = ( + result["TimePerEvent"] * result["BenchmarkJobHS06"] + ) + result["HS06CoreHr"] = result["CoreHr"] * result["BenchmarkJobHS06"] + result["HS06CommittedCoreHr"] = ( + result["CommittedCoreHr"] * result["BenchmarkJobHS06"] + ) + result["HS06CpuTimeHr"] = result["CpuTimeHr"] * result["BenchmarkJobHS06"] + except: + result.pop("MachineAttrMJF_JOB_HS06_JOB0", None) + if ("MachineAttrDIRACBenchmark0" in ad) and classad.ExprTree( + "MachineAttrDIRACBenchmark0 isnt undefined" + ).eval(ad): + result["BenchmarkJobDB12"] = float(ad["MachineAttrDIRACBenchmark0"]) + if result.get("EventRate", 0) > 0: + result["DB12EventRate"] = result["EventRate"] / result["BenchmarkJobDB12"] + if result.get("CpuEventRate", 0) > 0: + result["DB12CpuEventRate"] = ( + result["CpuEventRate"] / result["BenchmarkJobDB12"] + ) + if result.get("CpuTimePerEvent", 0) > 0: + result["DB12CpuTimePerEvent"] = ( + result["CpuTimePerEvent"] * result["BenchmarkJobDB12"] + ) + if result.get("TimePerEvent", 0) > 0: + result["DB12TimePerEvent"] = ( + result["TimePerEvent"] * result["BenchmarkJobDB12"] + ) + result["DB12CoreHr"] = result["CoreHr"] * result["BenchmarkJobDB12"] + result["DB12CommittedCoreHr"] = ( + result["CommittedCoreHr"] * result["BenchmarkJobDB12"] + ) + result["DB12CpuTimeHr"] = result["CpuTimeHr"] * result["BenchmarkJobDB12"] + + result["HasSingularity"] = classad.ExprTree( + "MachineAttrHAS_SINGULARITY0 is true" + ).eval(ad) + if "ChirpCMSSWCPUModels" in ad and not isinstance( + ad["ChirpCMSSWCPUModels"], classad.ExprTree + ): + result["CPUModel"] = str(ad["ChirpCMSSWCPUModels"]) + result["CPUModelName"] = str(ad["ChirpCMSSWCPUModels"]) + result["Processor"] = str(ad["ChirpCMSSWCPUModels"]) + elif "MachineAttrCPUModel0" in ad: + result["CPUModel"] = str(ad["MachineAttrCPUModel0"]) + result["CPUModelName"] = str(ad["MachineAttrCPUModel0"]) + result["Processor"] = str(ad["MachineAttrCPUModel0"]) + + # Affiliation data: + if affiliations_dict: + _aff = None + if "CRAB_UserHN" in result: + _aff = affiliations_dict.get(result["CRAB_UserHN"]) + elif "x509userproxysubject" in result: + _aff = get_affiliation_by_dn(affiliations_dict, result["x509userproxysubject"]) + + if _aff is not None: + result["AffiliationInstitute"] = _aff["institute"] + result["AffiliationCountry"] = _aff["country"] + + # We will use the CRAB_PostJobStatus as the actual status. + # If is an analysis task and is not completed + # (or removed if the postjob status is not "NOT RUN"), + # its status is defined by Status, else it will be defined by + # CRAB_PostJobStatus. + # + # We will use the postjob_status_decode dict to decode + # the status. If there is an unknown value it will set to it. + # Note: if the completed task has not a committed time + # or completion date, we will set it using RemoteWallClockTime + # and EnteredCurrentStatus. + _pjst = result.get("CRAB_PostJobStatus", None) + # Sometimes there are some inconsistences in the value from CRAB + # to avoid this we can remove the spaces and make it uppercase. + _pjst = _pjst.strip().upper() if _pjst else None + _status = result["Status"] + if _pjst and ( + (_status == "Removed" and _pjst != "NOT RUN") or (_status == "Completed") + ): + result["CRAB_PostJobStatus"] = postjob_status_decode.get(_pjst, _pjst) + if "CompletionDate" not in result: + result["CompletionDate"] = result.get("EnteredCurrentStatus") + if "CommittedTime" not in result or result.get("CommittedTime", 0) == 0: + result["CommittedTime"] = result.get("RemoteWallClockTime") + elif "CRAB_Id" in result: # If is an analysis or HC test task. + result["CRAB_PostJobStatus"] = _status + # Normalize wmtool value, this is a temporary change + # Not to be merged (The value was fixed upstream, + # this change is only needed while old tasks + # are still being processed + _wmtool = result.get( + "CMS_WMTool", + "UNKNOWN" + if result.get("CMS_SubmissionTool") != "InstitutionalSchedd" + else "User", + ) + result["CMS_WMTool"] = "User" if _wmtool.lower() == "user" else _wmtool + if reduce_data: + result = drop_fields_for_running_jobs(result) + # Set outliers + result = set_outliers(result) + if return_dict: + return result + else: + return json.dumps(result) + + +def set_outliers(result): + """Filter and set appropriate flags for outliers""" + if ("CpuEff" in result) and (result["CpuEff"] >= 100.0): + result["CpuEffOutlier"] = 1 + else: + result["CpuEffOutlier"] = 0 + return result + + +def recordTime(ad): + """ + RecordTime falls back to launch time as last-resort and for jobs in the queue + + For Completed/Removed/Error jobs, try to update it: + - to CompletionDate if present (only available for completed jobs) + - else to JobFinishedHookDone if present (available for all completed and removed jobs) + - else fall back to launch time + """ + if "JobStatus" in ad: + job_status = ad["JobStatus"] + else: + job_status = ad["Status"] + if job_status in [3, 4, 6]: + if ad.get("CompletionDate", 0): + return ad["CompletionDate"] + + elif ad.get("JobFinishedHookDone", 0): + return ad["JobFinishedHookDone"] + + return _launch_time + + +def guessTaskType(ad): + """Guess the TaskType from the WMAgent subtask name""" + jobType = ad.get("CMS_JobType", "UNKNOWN") + + if jobType == "Processing": + return "DataProcessing" + elif jobType == "Production": + ttype = ad.get("WMAgent_SubTaskName", "/UNKNOWN").rsplit("/", 1)[-1] + # Guess an alternate campaign name from the subtask + camp2_info = ttype.split("-") + if len(camp2_info) > 1: + camp2 = camp2_info[1] + else: + camp2 = ttype + + if "CleanupUnmerged" in ttype: + return "Cleanup" + elif "Merge" in ttype: + return "Merge" + elif "LogCollect" in ttype: + return "LogCollect" + elif ("MiniAOD" in ad.get("WMAgent_RequestName", "UNKNOWN")) and ( + ttype == "StepOneProc" + ): + return "MINIAOD" + elif "MiniAOD" in ttype: + return "MINIAOD" + elif ttype == "StepOneProc" and (re.search("[1-9][0-9]DR", camp2)): + return "DIGIRECO" + elif (re.search("[1-9][0-9]GS", camp2)) and ttype.endswith("_0"): + return "GENSIM" + elif ttype.endswith("_0"): + return "DIGI" + elif ttype.endswith("_1") or ttype.lower() == "reco": + return "RECO" + elif ttype == "MonteCarloFromGEN": + return "GENSIM" + else: + return "UNKNOWN" + else: + return jobType + + +def guessCampaign(ad, analysis, cms_campaign_type): + # Guess the campaign from the request name. + camp = ad.get("WMAgent_RequestName", "UNKNOWN") + if ad.get("CMS_CampaignName"): + return ad.get("CMS_CampaignName") + if analysis: + return "crab_" + ad.get("CRAB_UserHN", "UNKNOWN") + if camp.startswith("PromptReco"): + return "PromptReco" + if camp.startswith("Repack"): + return "Repack" + if camp.startswith("Express"): + return "Express" + if "RVCMSSW" in camp: + return "RelVal" + m = _camp_re.match(camp) + if m: + return m.groups()[0] + m = _rereco_re.match(camp) + if m and ("DataProcessing" in ad.get("WMAgent_SubTaskName", "")): + return m.groups()[0] + "Reprocessing" + # [Temp solution] If Campaign not found, return CMS_CampaignType + logging.info("Campaign will be CMS_CampaignType. camp:{}".format(camp)) + return cms_campaign_type + + +def guess_campaign_type(ad, analysis): + """ + Based on the request name return a campaign type. + The campaign type is based on the classification defined at + https://its.cern.ch/jira/browse/CMSMONIT-174#comment-3050384 + """ + camp = ad.get("WMAgent_RequestName", "UNKNOWN") + if analysis: + return "Analysis" + elif re.match(r".*(RunII(Summer|Spring)([12])[0-9]UL|_UL[0-9]+).*", camp): + return "MC Ultralegacy" + elif re.match(r".*UltraLegacy.*", camp): + return "Data Ultralegacy" + elif re.match(r".*Phase2.*", camp): + return "Phase2 requests" + elif re.match(r".*(Run3|RunIII).*", camp): + return "Run3 requests" + elif "RVCMSSW" in camp: + return "RelVal" + elif re.match( + r".*(RunII|(Summer|Fall|Autumn|Winter|Spring)(1[5-9]|20)).*", camp + ): # [!] Should be after UL + return "Run2 requests" + elif "SnowmassWinter21" in camp: + # Example WMAgent_RequestName: pdmvserv_task_TSG-SnowmassWinter21wmLHEGEN-00229__v1_T_211208_125036_3179 + return "SnowmassWinter21" + else: + return "UNKNOWN" + + +def guessWorkflow(ad, analysis): + prep = ad.get("WMAgent_RequestName", "UNKNOWN") + m = _prep_re.match(prep) + if analysis: + return ad.get("CRAB_Workflow", "UNKNOWN").split(":", 1)[-1] + elif m: + return m.groups()[0] + else: + m = _prep_prompt_re.match(prep) + if m: + return m.groups()[0] + "_" + m.groups()[1] + else: + m = _rval_re.match(prep) + if m: + return m.groups()[0] + + return prep + + +def chirpCMSSWIOSiteName(key): + """Extract site name from ChirpCMSS_IOSite key""" + iosite_match = re.match(r"ChirpCMSSW(.*?)IOSite_(.*)_(ReadBytes|ReadTimeMS)", key) + return iosite_match.group(2), iosite_match.group(1).strip("_") + + +def jobFailed(ad): + """ + Returns 0 when none of the exitcode fields has a non-zero value + otherwise returns 1 + """ + if commonExitCode(ad) == 0: + return 0 + else: + return 1 + + +def isAnalysisJob(ad): + """ + Check if this is an analysis job, based on + the CMS_Type/CMS_JobType classads in the job. + """ + if ( + ad.get("CMS_Type", "unknown").lower() == "analysis" + or ad.get("CMS_JobType", "unknown") == "Analysis" + ): + return True + + return False + + +def commonExitCode(ad): + """ + Consolidate the exit code values of JobExitCode, + the chirped CRAB and WMCore values, and + the original condor exit code, according to + the workflow type: production or analysis. + """ + # If the raw ExitCode in the ad is not present, + # the job was removed and its executable did not finish, + # hence we return 50666 for any workflow type. + if not "ExitCode" in ad: + return 50666 + + condorExitCode = ad.get("ExitCode") + + if isAnalysisJob(ad): # CRAB or CMS Connect job + return ad.get("JobExitCode", ad.get("Chirp_CRAB3_Job_ExitCode", condorExitCode)) + else: # production job + # If cmsRun exit code exists and was not 0, we consider the job failed + # even if the wrapper reported a sucess status in condor. + # Also, if cmsRunExitCode was 0, but not wrapper exit code, + # we stick with the wrapper exit code and consider the job failed. + if ad.get("Chirp_WMCore_cmsRun_ExitCode", 0) > 0: + return ad["Chirp_WMCore_cmsRun_ExitCode"] + return condorExitCode + + +def errorType(ad): + """ + Categorization of exit codes into a handful of readable error types. + + Allowed values are: + 'Success', 'Environment' 'Executable', 'Stageout', 'Publication', + 'JobWrapper', 'FileOpen', 'FileRead', 'OutOfBounds', 'Other' + + This currently only works for CRAB jobs. Production jobs will always + fall into 'Other' as they don't have the Chirp_CRAB3_Job_ExitCode + """ + if not jobFailed(ad): + return "Success" + + exitcode = commonExitCode(ad) + + if (10000 <= exitcode <= 19999) or exitcode == 50513: + return "Environment" + + if 60000 <= exitcode <= 69999: + if exitcode >= 69000: # Not yet in classads? + return "Publication" + else: + return "StageOut" + + if 80000 <= exitcode <= 89999: + return "JobWrapper" + + if exitcode in [8020, 8028]: + return "FileOpen" + + if exitcode == 8021: + return "FileRead" + + if exitcode in [8030, 8031, 8032, 9000] or (50660 <= exitcode <= 50669): + return "OutOfBounds" + + if (7000 <= exitcode <= 9000) or exitcode == 139: + return "Executable" + + return "Other" + + +def errorClass(result): + """ + Further classify error types into even broader failure classes + """ + if result["ErrorType"] in [ + "Environment", + "Publication", + "StageOut", + "AsyncStageOut", + ]: + return "System" + + elif result["ErrorType"] in ["FileOpen", "FileRead"]: + return "DataAccess" + + elif result["ErrorType"] in ["JobWrapper", "OutOfBounds", "Executable"]: + return "Application" + + elif result["JobFailed"]: + return "Other" + + return "Success" + + +def handle_chirp_info(ad, result): + """ + Process any data present from the Chirp ads. + + Chirp statistics should be available in CMSSW_8_0_0 and later. + """ + for key, val in list(result.items()): + if key.startswith("ChirpCMSSW") and "IOSite" in key: + sitename, chirpstring = chirpCMSSWIOSiteName(key) + keybase = key.rsplit("_", 1)[0] + try: + readbytes = result.pop(keybase + "_ReadBytes") + readtimems = result.pop(keybase + "_ReadTimeMS") + siteio = {} + siteio["SiteName"] = sitename + siteio["ChirpString"] = chirpstring + siteio["ReadBytes"] = readbytes + siteio["ReadTimeMS"] = readtimems + result.setdefault("ChirpCMSSW_SiteIO", []).append(siteio) + + except KeyError: + # First hit will pop both ReadBytes and ReadTimeMS fields hence + # second hit will throw a KeyError that we want to ignore + pass + + continue + + if key.startswith("ChirpCMSSW_"): + cmssw_key = "ChirpCMSSW" + key.split("_", 2)[-1] + if cmssw_key not in result: + result[cmssw_key] = val + elif ( + cmssw_key.endswith("LastUpdate") + or cmssw_key.endswith("Events") + or cmssw_key.endswith("MaxLumis") + or cmssw_key.endswith("MaxFiles") + ): + result[cmssw_key] = max(result[cmssw_key], val) + else: + result[cmssw_key] += val + + if "ChirpCMSSWFiles" in result: + result["CompletedFiles"] = result["ChirpCMSSWFiles"] + if result.get("ChirpCMSSWMaxFiles", -1) > 0: + result["MaxFiles"] = result["ChirpCMSSWMaxFiles"] + if "ChirpCMSSWDone" in result: + result["CMSSWDone"] = bool(result["ChirpCMSSWDone"]) + result["ChirpCMSSWDone"] = int(result["ChirpCMSSWDone"]) + if "ChirpCMSSWElapsed" in result: + result["CMSSWWallHrs"] = result["ChirpCMSSWElapsed"] / 3600.0 + if "ChirpCMSSWEvents" in result: + result["KEvents"] = result["ChirpCMSSWEvents"] / 1000.0 + result["MegaEvents"] = result["ChirpCMSSWEvents"] / 1e6 + if "ChirpCMSSWLastUpdate" in result: + # Report time since last update - this is likely stageout time for completed jobs + result["SinceLastCMSSWUpdateHrs"] = ( + max(result["RecordTime"] - result["ChirpCMSSWLastUpdate"], 0) / 3600.0 + ) + if result["Status"] == "Completed": + result["StageOutHrs"] = result["SinceLastCMSSWUpdateHrs"] + if "ChirpCMSSWLumis" in result: + result["CMSSWKLumis"] = result["ChirpCMSSWLumis"] / 1000.0 + if "ChirpCMSSWReadBytes" in result: + result["InputGB"] = result["ChirpCMSSWReadBytes"] / 1e9 + if "ChirpCMSSWReadTimeMsecs" in result: + result["ReadTimeHrs"] = result["ChirpCMSSWReadTimeMsecs"] / 3600000.0 + result["ReadTimeMins"] = result["ChirpCMSSWReadTimeMsecs"] / 60000.0 + if "ChirpCMSSWWriteBytes" in result: + result["OutputGB"] = result["ChirpCMSSWWriteBytes"] / 1e9 + if "ChirpCMSSWWriteTimeMsecs" in result: + result["WriteTimeHrs"] = result["ChirpCMSSWWriteTimeMsecs"] / 3600000.0 + result["WriteTimeMins"] = result["ChirpCMSSWWriteTimeMsecs"] / 60000.0 + if result.get("CMSSWDone") and (result.get("ChirpCMSSWElapsed", 0) > 0): + result["CMSSWEventRate"] = result.get("ChirpCMSSWEvents", 0) / float( + result["ChirpCMSSWElapsed"] * ad.get("RequestCpus", 1.0) + ) + if result["CMSSWEventRate"] > 0: + result["CMSSWTimePerEvent"] = 1.0 / result["CMSSWEventRate"] + if result["CoreHr"] > 0: + result["EventRate"] = result.get("ChirpCMSSWEvents", 0) / float( + result["CoreHr"] * 3600.0 + ) + if result["EventRate"] > 0: + result["TimePerEvent"] = 1.0 / result["EventRate"] + if ("ChirpCMSSWReadOps" in result) and ("ChirpCMSSWReadSegments" in result): + ops = result["ChirpCMSSWReadSegments"] + result["ChirpCMSSWReadOps"] + if ops: + result["ReadOpSegmentPercent"] = ( + result["ChirpCMSSWReadOps"] / float(ops) * 100 + ) + if ("ChirpCMSSWReadOps" in result) and ("ChirpCMSSWReadVOps" in result): + ops = result["ChirpCMSSWReadOps"] + result["ChirpCMSSWReadVOps"] + if ops: + result["ReadOpsPercent"] = result["ChirpCMSSWReadOps"] / float(ops) * 100 + + +_CONVERT_COUNT = 0 +_CONVERT_CPU = 0 + +# TODO: Look into making this more efficient +def _is_dynamic_bool_key(key): + """Return True for dynamic boolean keys with numeric suffixes.""" + return bool(_dynamic_bool_key_re.match(key)) + + +def _coerce_bool_value(key, value): + """ + Coerce values to bool with strict parsing. + + Returns: + tuple[bool, bool|None]: (success, parsed_value) + """ + if isinstance(value, bool): + return True, value + + if isinstance(value, str): + normalized = value.strip().lower() + if normalized in {"true", "1"}: + return True, True + if normalized in {"false", "0"}: + return True, False + return False, None + + if isinstance(value, (int, float)): + if value in (0, 1): + return True, bool(value) + return False, None + + logging.debug( + "Unsupported bool conversion for key %s with value type %s", + key, + type(value).__name__, + ) + return False, None + + +def bulk_convert_ad_data(ad, result): + """ + Given a ClassAd, bulk convert to a python dictionary. + """ + _keys = set(ad.keys()) - ignore + for key in _keys: + if key.startswith("HasBeen") and key not in bool_vals: + continue + if key == "DESIRED_SITES": + key = "DESIRED_Sites" + try: + value = ad.eval(key) + except: + continue + if isinstance(value, classad.Value): + _is_err_cond = value is classad.Value.Error + if _is_err_cond: + continue + else: + value = None + elif key in bool_vals or _is_dynamic_bool_key(key): + converted, parsed_value = _coerce_bool_value(key, value) + if not converted: + logging.warning( + "Failed to convert key %s with value %s to bool", + key, + repr(value), + ) + value = None + else: + value = parsed_value + elif key in int_vals: + try: + value = int(value) + except ValueError: + if value == "Unknown": + value = None + elif (key == "MATCH_EXP_JOB_GLIDEIN_MaxMemMBs") and ( + value == "GLIDEIN_MaxMemMBs" + ): + # FIXME after SI/WMA/CRAB teams solve this upstream. This key should be convertible to int + continue + else: + logging.warning( + "Failed to convert key %s with value %s to int" + % (key, repr(value)) + ) + continue + elif key in string_vals: + value = str(value) + elif key in date_vals: + if value == 0 or (isinstance(value, str) and value.lower() == "unknown"): + value = None + else: + try: + value = int(value) + except ValueError: + logging.warning( + "Failed to convert key %s with value %s to int for a date field" + % (key, repr(value)) + ) + value = None + else: + # For uncategorized fields, skip "Unknown" string values to avoid + # OpenSearch mapping conflicts (e.g. float fields receiving a string). + if isinstance(value, str) and value.lower() == "unknown": + value = None + logging.debug("Skipping unknown value for key %s: %s", key, value) + if key.startswith("MATCH_EXP_JOB_"): + key = key[len("MATCH_EXP_JOB_") :] + if key.endswith("_RAW"): + key = key[: -len("_RAW")] + if _wmcore_exe_exmsg.match(key): + value = str(decode_and_decompress(value)) + result[key] = value + evaluate_fields(result, ad) + + +def evaluate_fields(result, ad): + """Evaluates RequestMemory expression in ClassAd""" + if "RequestMemory" in ad: + try: + result["RequestMemory_Eval"] = ad.eval("RequestMemory") + except Exception as eval_exc: + logging.error( + "Could not evaluate RequestMemory exp, error: %s" % (str(eval_exc)) + ) + + if "RequestCpus" in ad: + try: + result["RequestCpus_Eval"] = ad.eval("RequestCpus") + except Exception as eval_exc: + logging.error( + "Could not evaluate RequestCpus expression, error: %s" % (str(eval_exc)) + ) + +def decode_and_decompress(value): + try: + value = str(zlib.decompress(base64.b64decode(value))) + except (TypeError, zlib.error): + logging.warning("Failed to decode and decompress value: %s" % (repr(value))) + + return value + + +def convert_dates_to_millisecs(record): + for date_field in date_vals: + try: + record[date_field] *= 1000 + except (KeyError, TypeError): + continue + + return record + + +def drop_fields_for_running_jobs(record): + """ + Check if the job is running or pending + and prune it if it is. + """ + if "Status" in record and record["Status"] not in ["Running", "Idle", "Held"]: + return record + _fields = running_fields.intersection(set(record.keys())) + skimmed_record = {field: record[field] for field in _fields} + return skimmed_record + + +def unique_doc_id(doc): + """ + Return a string of format "#" + To uniquely identify documents (not jobs) + + Note that this uniqueness breaks if the same jobs are submitted + with the same RecordTime + """ + return "%s#%d" % (doc["GlobalJobId"], recordTime(doc)) + + +def get_formatted_CRAB_Id(CRAB_Id): + # FormattedCrabId + # In this field, we want to format the crab_id (if exists) + # to ensure that the lexicographical order is the desired. + # Currently, there are two CRAB_Id formats: + # a positive integer or an integer tuple formated as X-N + # + # The desired order is start with the 0-N then the integer values then the + # 1-N...X-N + # To do that we will use leading zeros to ensure the lexicographical order, + # e.g: + # 0.000001,0.000002 ...0.000012, 000001,000002,....009999,010000, + # 1.000001....3,009999. + # Falls back to '000000' + # args: CRAB_Id + _cid = CRAB_Id + formatted = "000000" + try: + if "-" in _cid: + formatted = "{}.{:06d}".format(*[int(x) for x in _cid.split("-")]) + elif _cid.isdigit(): + formatted = "{:06d}".format(int(_cid)) + except TypeError: + pass + return formatted diff --git a/docker/spider-worker/src/nats_consumer.py b/docker/spider-worker/src/nats_consumer.py new file mode 100644 index 00000000..fac6d4a7 --- /dev/null +++ b/docker/spider-worker/src/nats_consumer.py @@ -0,0 +1,289 @@ +""" +Helper utilities to consume jobs from the NATS JetStream queue. +""" + +from __future__ import annotations + +import asyncio +import json +from dataclasses import dataclass +from typing import List, Optional, Sequence, Tuple +import classad +from nats.aio.client import Client as NATS +from nats.js.api import ConsumerConfig, AckPolicy, DeliverPolicy + +import src.constants as const +from src.convert_to_json import unique_doc_id +from src.otel_setup import global_logger + + +def _normalize_header_value(value): + """Normalize NATS header value to a single string.""" + if isinstance(value, list): + return value[0] if value else None + if isinstance(value, str): + return value + return None + + +def _get_event_loop() -> asyncio.AbstractEventLoop: + """ + Get the current asyncio loop or create a new one when running + in a synchronous context. + """ + try: + loop = asyncio.get_event_loop() + if loop.is_closed(): + raise RuntimeError("Existing asyncio loop is closed") + return loop + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + return loop + + +def _str_to_classad(s: str) -> classad.ClassAd: + """ + Convert a string to a ClassAd object. + """ + return classad.ClassAd(s) + +@dataclass +class NATSBatch: + """ + Container holding the raw JetStream messages together with the parsed jobs. + """ + + jobs: List[Tuple[str, classad.ClassAd]] + _messages: Sequence + _loop: asyncio.AbstractEventLoop + traceparent: str = None # First valid traceparent seen in the batch + tracestate: str = None # tracestate associated with the first traceparent + latest_traceparent: str = None # Latest traceparent seen (for mismatch diagnostics) + latest_tracestate: str = None # Latest tracestate seen (for mismatch diagnostics) + has_traceparent_mismatch: bool = False + redelivered_messages: int = 0 + + def ack(self) -> None: + """Acknowledge all messages in the batch using parallel async operations.""" + if not self._messages: + return + try: + # Batch all ack operations and run them in parallel for better throughput + ack_tasks = [msg.ack() for msg in self._messages] + self._loop.run_until_complete(asyncio.gather(*ack_tasks, return_exceptions=True)) + except Exception as exc: # pragma: no cover - network failure + global_logger.error("Failed to batch ack NATS messages: %s", exc) + # Fallback to individual acks if batch fails + for msg in self._messages: + try: + self._loop.run_until_complete(msg.ack()) + except Exception as msg_exc: + global_logger.error("Failed to ack individual NATS message: %s", msg_exc) + + def nak(self) -> None: + """Request re-delivery for all messages in the batch using parallel async operations.""" + if not self._messages: + return + try: + # Batch all nak operations and run them in parallel for better throughput + nak_tasks = [msg.nak() for msg in self._messages] + self._loop.run_until_complete(asyncio.gather(*nak_tasks, return_exceptions=True)) + except Exception as exc: # pragma: no cover - network failure + global_logger.error("Failed to batch NAK NATS messages: %s", exc) + # Fallback to individual naks if batch fails + for msg in self._messages: + try: + self._loop.run_until_complete(msg.nak()) + except Exception as msg_exc: + global_logger.error("Failed to NAK individual NATS message: %s", msg_exc) + + def finalize(self, success: bool) -> None: + if success: + self.ack() + else: + self.nak() + + def chunked(self, chunk_size: int) -> List["NATSBatch"]: + """Split this batch into smaller sub-batches for earlier finalize/ack.""" + if chunk_size <= 0 or len(self.jobs) <= chunk_size: + return [self] + + result: List[NATSBatch] = [] + for start in range(0, len(self.jobs), chunk_size): + end = start + chunk_size + result.append( + NATSBatch( + jobs=self.jobs[start:end], + _messages=self._messages[start:end], + _loop=self._loop, + traceparent=self.traceparent, + tracestate=self.tracestate, + latest_traceparent=self.latest_traceparent, + latest_tracestate=self.latest_tracestate, + has_traceparent_mismatch=self.has_traceparent_mismatch, + redelivered_messages=self.redelivered_messages, + ) + ) + return result + + +class NATSQueueConsumer: + """ + Thin synchronous wrapper around the asyncio-based NATS JetStream client. + """ + + def __init__(self): + self.loop = _get_event_loop() + self.nc: Optional[NATS] = None + self.jetstream = None + self.subscription = None + + self.stream_name = const.NATS_STREAM_NAME + self.subject = const.NATS_SUBJECT + self.consumer_name = const.NATS_CONSUMER_NAME + self._connect() + + def _connect(self) -> None: + servers = const.NATS_SERVER + server_list = [s.strip() for s in str(servers).split(",") if s.strip()] + + self.nc = NATS() + self.loop.run_until_complete(self.nc.connect(servers=server_list)) + self.jetstream = self.nc.jetstream() + + # Ensure the stream exists - the publisher should have created it. + try: + self.loop.run_until_complete(self.jetstream.stream_info(self.stream_name)) + except Exception as exc: + global_logger.error( + "NATS stream %s is not available: %s", self.stream_name, exc + ) + raise + + # Ensure there is a durable consumer bound to the queue subject. + # Note: If the consumer already exists, max_ack_pending cannot be changed. + # To update it, delete the existing consumer first (e.g., via NATS CLI). + try: + self.loop.run_until_complete( + self.jetstream.consumer_info(self.stream_name, self.consumer_name) + ) + global_logger.info( + "Using existing consumer %s for stream %s", + self.consumer_name, + self.stream_name, + ) + except Exception: + global_logger.info( + "Creating durable consumer %s for stream %s with optimized settings for throughput", + self.consumer_name, + self.stream_name, + ) + config = ConsumerConfig( + durable_name=self.consumer_name, + ack_policy=AckPolicy.EXPLICIT, + filter_subject=self.subject, + deliver_policy=DeliverPolicy.ALL, + max_ack_pending=100000, # High limit to allow many in-flight messages (you already have 100k) + ack_wait=const.NATS_ACK_WAIT_SECONDS, + # Note: Other throughput optimizations: + # - Increase max_waiting_pulls if you see "Waiting Pulls" hitting limits + # - Consider rate_limit/rate_limit_burst if server-side throttling is an issue + # - Existing consumers keep prior ack_wait unless recreated + ) + self.loop.run_until_complete( + self.jetstream.add_consumer( + stream=self.stream_name, + config=config, + ) + ) + + self.subscription = self.loop.run_until_complete( + self.jetstream.pull_subscribe( + subject=self.subject, + durable=self.consumer_name, + stream=self.stream_name, + ) + ) + + def fetch_jobs(self, batch_size: int, timeout: float) -> NATSBatch: + """ + Fetch a batch of jobs from JetStream. Returns immediately with available + messages, even if fewer than batch_size. Returns an empty batch when no + messages are available before the timeout. + """ + if not self.subscription: + return NATSBatch([], [], self.loop) + + try: + messages = self.loop.run_until_complete( + self.subscription.fetch(batch_size, timeout=timeout) + ) + except asyncio.TimeoutError: + return NATSBatch([], [], self.loop) + + jobs: List[Tuple[str, classad.ClassAd]] = [] + valid_messages = [] + first_traceparent = None + first_tracestate = None + latest_traceparent = None + latest_tracestate = None + has_traceparent_mismatch = False + redelivered_messages = 0 + + for msg in messages: + try: + metadata = getattr(msg, "metadata", None) + if metadata and getattr(metadata, "num_delivered", 1) > 1: + redelivered_messages += 1 + + if msg.headers: + message_traceparent = _normalize_header_value( + msg.headers.get("traceparent") + ) + message_tracestate = _normalize_header_value( + msg.headers.get("tracestate") + ) + if message_traceparent: + latest_traceparent = message_traceparent + latest_tracestate = message_tracestate + if first_traceparent is None: + first_traceparent = message_traceparent + first_tracestate = message_tracestate + elif message_traceparent != first_traceparent: + has_traceparent_mismatch = True + + payload = json.loads(msg.data.decode("utf-8")) + job_doc = _str_to_classad(payload) + job_id = unique_doc_id(job_doc) + jobs.append((job_id, job_doc)) + valid_messages.append(msg) + except Exception as exc: + global_logger.error("Dropping malformed NATS job payload: %s", exc) + try: + self.loop.run_until_complete(msg.ack()) + except Exception as ack_exc: # pragma: no cover + global_logger.error("Failed to ack malformed message: %s", ack_exc) + + batch = NATSBatch(jobs=jobs, _messages=valid_messages, _loop=self.loop) + if first_traceparent: + batch.traceparent = first_traceparent + batch.tracestate = first_tracestate + batch.latest_traceparent = latest_traceparent + batch.latest_tracestate = latest_tracestate + batch.has_traceparent_mismatch = has_traceparent_mismatch + batch.redelivered_messages = redelivered_messages + return batch + + def close(self) -> None: + if self.nc and not self.nc.is_closed: + try: + # Use close() instead of drain() because this is a pull subscriber + # and all fetched messages have already been acked/naked via batch.finalize(). + self.loop.run_until_complete(self.nc.close()) + except Exception as exc: # pragma: no cover + global_logger.warning("Error while closing NATS connection: %s", exc) + + + + diff --git a/docker/spider-worker/src/os_utils.py b/docker/spider-worker/src/os_utils.py new file mode 100644 index 00000000..ceb5f4d1 --- /dev/null +++ b/docker/spider-worker/src/os_utils.py @@ -0,0 +1,112 @@ +import datetime +from typing import Any, Iterable, Optional +from opensearchpy import OpenSearch, helpers, RequestsHttpConnection +import constants as const +import src.vals as vals + + +_INDEX_SETTINGS = {"mapping.total_fields.limit": 2000} +_DATE_MAPPING = {"type": "date", "format": "epoch_millis"} +_OS_CLIENT: Optional[OpenSearch] = None +_ENSURED_DAILY_INDICES: set[str] = set() + +def get_opensearch_client() -> OpenSearch: + global _OS_CLIENT + if _OS_CLIENT is None: + _OS_CLIENT = OpenSearch( + hosts=[const.OS_HOST], + http_auth=(const.OS_USERNAME, const.OS_PASSWORD), + use_ssl=True, + verify_certs=True, + ca_certs=const.OS_CERT_PATH, + connection_class=RequestsHttpConnection, + ) + return _OS_CLIENT + + +def get_daily_index_name(timestamp: float, index_prefix: str) -> str: + """ + Generate a daily index name from a timestamp and prefix. + + Args: + timestamp: Unix timestamp (seconds since epoch) + index_prefix: Prefix for the index name (e.g., 'cms', 'monitoring') + + Returns: + Index name in format: '{prefix}-YYYY-MM-DD' + + Example: + get_daily_index(1704067200.0, 'cms') -> 'cms-2024-01-01' + """ + dt = datetime.datetime.fromtimestamp(timestamp, datetime.timezone.utc) + return f"{index_prefix}-{dt.strftime('%Y-%m-%d')}" + + +def ensure_daily_index( + os_client: OpenSearch, + index_name: str, +) -> bool: + """ + Ensure a daily index exists, creating it if necessary. + + Args: + os_client: OpenSearch client instance + index_name: Name of the index to ensure exists + mappings: Optional index mappings to apply + settings: Optional index settings to apply + + Returns: + True if index was created, False if it already existed + """ + if index_name in _ENSURED_DAILY_INDICES: + return False + + if os_client.indices.exists(index=index_name): + _ENSURED_DAILY_INDICES.add(index_name) + return False + + os_client.indices.create(index=index_name, body=_build_index_body()) + _ENSURED_DAILY_INDICES.add(index_name) + return True + + +def _build_index_body() -> dict[str, Any]: + return { + "settings": {"index": _INDEX_SETTINGS}, + "mappings": { + "properties": {field: dict(_DATE_MAPPING) for field in vals.date_vals} + }, + } + + +def os_upload_docs_in_bulk( + doc_iterable: Iterable, + index_prefix: Optional[str] = None, + timestamp: Optional[float] = None, +): + """ + Upload documents in bulk to OpenSearch. + + Args: + os_host: OpenSearch host URL + ca_cert_path: Path to CA certificate file + credentials: Tuple of (username, password) + doc_iterable: Iterable of documents to upload + timestamp: Optional Unix timestamp. It will create a daily + index by appending it's date to the index_prefix. + index_prefix: Prefix for daily index. It will be used + with the timestamp to create a daily index. + + Returns: + Tuple of (success_count, failures_list) + """ + os_client = get_opensearch_client() + index = get_daily_index_name( + timestamp=timestamp, + index_prefix=index_prefix, + ) + ensure_daily_index(os_client, index) + success, failures = helpers.bulk( + os_client, doc_iterable, max_retries=3, index=index + ) + return success, failures diff --git a/docker/spider-worker/src/otel_setup.py b/docker/spider-worker/src/otel_setup.py new file mode 100644 index 00000000..cf04f0e8 --- /dev/null +++ b/docker/spider-worker/src/otel_setup.py @@ -0,0 +1,334 @@ +#!/usr/bin/env python +""" +OpenTelemetry setup for sending metrics to CERN monitoring endpoint. +""" + +import base64 +import logging +import contextvars +import opentelemetry +import sys +from typing import Optional, Tuple +from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler +from opentelemetry._logs import set_logger_provider +from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter +from opentelemetry.sdk._logs.export import BatchLogRecordProcessor +from opentelemetry.metrics import Meter +from opentelemetry import trace, propagate +from opentelemetry.trace import format_trace_id +from opentelemetry.context import Context +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader +from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor + +import src.constants as const + +# Context variable to store additional metric attributes during function execution +_metric_attributes = contextvars.ContextVar('metric_attributes', default={}) + +# Global logger provider/handler references +_logger_provider = None +_otel_handler = None +_worker_span = None +_worker_scope = None +_worker_traceparent = None +_worker_missing_traceparent_warned = False + + +class CustomLoggingHandler(LoggingHandler): + """Custom LoggingHandler that adds service.name to log record attributes""" + + def __init__(self, service_name: str, service_version: str, *args, **kwargs): + super().__init__(*args, **kwargs) + self._service_name = service_name + self._service_version = service_version + + def _get_attributes(self, record: logging.LogRecord): + """Override to add service.name and service.version to attributes""" + # Get base attributes from parent + attributes = super()._get_attributes(record) + + # Add service.name and service.version to log attributes (highest precedence) + attributes["service.name"] = self._service_name + attributes["service.version"] = self._service_version + + # Always derive execution.id from the active trace context when available. + span_context = trace.get_current_span().get_span_context() + if span_context and span_context.is_valid: + attributes["execution.id"] = format_trace_id(span_context.trace_id) + + return attributes + + +def set_up_logging(log_level: int = logging.INFO) -> logging.Logger: + """Configure global logging and attach it to OpenTelemetry""" + + # Resource.create() automatically merges with default resource and OTELResourceDetector + # Ensure values are not None to prevent service.name from being None + logging_resource = Resource.create({ + "service.name": const.IMAGE_NAME or "spider-worker", + "service.version": const.DOCKER_TAG or "unknown", + }) + logger_provider = LoggerProvider(resource=logging_resource) + set_logger_provider(logger_provider) + + creds = f"{const.OTEL_USERNAME}:{const.OTEL_PASSWORD}".encode("utf-8") + token = base64.b64encode(creds).decode("utf-8") + otel_headers = {"authorization": f"Basic {token}"} + channel_options=( + ("grpc.keepalive_time_ms", 20000), + ("grpc.keepalive_timeout_ms", 10000), + ("grpc.keepalive_permit_without_calls", True), + ("grpc.http2.max_pings_without_data", 0), + ("grpc.http2.min_time_between_pings_ms", 10000), + ) + + exporter = OTLPLogExporter( + endpoint=const.OTEL_ENDPOINT, + insecure=True, + headers=otel_headers, + channel_options=channel_options, + ) + + logger_provider.add_log_record_processor( + BatchLogRecordProcessor(exporter) + ) + + logger = logging.getLogger() + logger.setLevel(log_level) + + # Ensure we don't duplicate handlers if logging is initialized multiple times. + logger.handlers.clear() + + # Stream handler for stdout + stream_handler = logging.StreamHandler(sys.stdout) + stream_handler.setFormatter(logging.Formatter('%(asctime)s : %(name)s:%(levelname)s - %(message)s')) + + # OpenTelemetry handler - adds service.name directly to log attributes + service_name = const.IMAGE_NAME or "spider-worker" + service_version = const.DOCKER_TAG or "unknown" + otel_handler = CustomLoggingHandler( + service_name=service_name, + service_version=service_version, + logger_provider=logger_provider, + level=log_level + ) + + # Store globals for later updates + global _logger_provider, _otel_handler + _logger_provider = logger_provider + _otel_handler = otel_handler + + logger.addHandler(stream_handler) + logger.addHandler(otel_handler) + + # Set StompAMQ logger to ERROR level to suppress WARNING messages + logging.getLogger("StompAMQ").setLevel(logging.ERROR) + logging.getLogger("opensearch").setLevel(logging.ERROR) + + if log_level <= logging.INFO: + logging.getLogger("stomp.py").setLevel(log_level + 10) + + return logger + + +def create_parent_context_from_trace_headers( + traceparent: Optional[str], tracestate: Optional[str] = None +) -> Optional[Context]: + """Build a parent context from W3C trace headers.""" + if not traceparent: + return None + + carrier = {"traceparent": traceparent} + if tracestate: + carrier["tracestate"] = tracestate + + parent_context = propagate.extract(carrier) + span_context = trace.get_current_span(parent_context).get_span_context() + if not span_context.is_valid: + return None + return parent_context + + +def _execution_id_from_parent_context(parent_context: Context) -> Optional[str]: + """Return trace-id string from extracted parent context.""" + span_context = trace.get_current_span(parent_context).get_span_context() + if not span_context.is_valid: + return None + return format_trace_id(span_context.trace_id) + + +def start_worker_span( + parent_context: Context, execution_id: Optional[str] = None +) -> Tuple[trace.Span, object]: + """ + Start a long-lived worker span attached to the supplied parent context. + Returns (span, scope) where scope must be exited by the caller. + """ + lifetime_span = global_tracer.start_span( + name="spider_worker", + context=parent_context, + ) + if execution_id: + lifetime_span.set_attribute("execution.id", execution_id) + lifetime_span.set_attribute("worker.service", const.OTEL_SERVICE_NAME) + + scope = trace.use_span(lifetime_span, end_on_exit=False) + scope.__enter__() + return lifetime_span, scope + + +def initialize_worker_trace( + traceparent: Optional[str], tracestate: Optional[str] +) -> None: + """ + Initialize the worker lifetime span once from remote trace headers. + On failure, logs a warning. + """ + global _worker_span, _worker_scope + global _worker_traceparent, _worker_missing_traceparent_warned + + if _worker_span is not None: + if traceparent and traceparent != _worker_traceparent: + # TODO: Rotate the worker lifetime span when a new execution traceparent arrives. + global_logger.warning( + "Received different root traceparent after worker trace initialization; ignoring. " + "current=%s new=%s", + _worker_traceparent, + traceparent, + ) + return + + if not traceparent: + if not _worker_missing_traceparent_warned: + global_logger.warning( + "Failed to initialize worker lifetime trace: missing traceparent header" + ) + _worker_missing_traceparent_warned = True + return + + parent_context = create_parent_context_from_trace_headers(traceparent, tracestate) + if parent_context is None: + global_logger.warning( + "Failed to initialize worker lifetime trace: invalid traceparent=%s", + traceparent, + ) + return + + execution_id = _execution_id_from_parent_context(parent_context) + _worker_span, _worker_scope = start_worker_span(parent_context, execution_id) + _worker_traceparent = traceparent + global_logger.info("Initialized worker lifetime span from traceparent header") + + +def finalize_worker_trace( + worker_seconds: float, + total_jobs: int, + total_sent_amq: int, + total_sent_os: int, +) -> None: + """End and detach the lifetime span if it was initialized.""" + global _worker_span, _worker_scope + + if _worker_span is not None: + _worker_span.set_attribute( + "worker.lifetime.seconds", worker_seconds + ) + _worker_span.set_attribute("worker.total_jobs", total_jobs) + _worker_span.set_attribute("worker.total_sent_amq", total_sent_amq) + _worker_span.set_attribute("worker.total_sent_os", total_sent_os) + _worker_span.end() + _worker_span = None + + if _worker_scope is not None: + _worker_scope.__exit__(None, None, None) + _worker_scope = None + + +def setup_opentelemetry() -> Tuple[Meter, logging.Logger, trace.Tracer]: + """ + Initialize OpenTelemetry metrics + tracing exporters and logging. + + Returns: + tuple: (Meter, Logger, Tracer) + """ + # OTLP endpoint configuration + # For gRPC, endpoint should be host:port without protocol scheme + otlp_endpoint = const.OTEL_ENDPOINT + + + # Use a fixed service name so worker spans are visually distinct in Grafana. + service_name = const.OTEL_SERVICE_NAME + service_version = const.DOCKER_TAG + + + # Create resource with service information and run ID + # Resource.create() automatically merges with default resource and OTELResourceDetector + # Ensure values are not None to prevent service.name from being None + resource = Resource.create({ + "service.name": service_name, + "service.version": service_version or "unknown", + }) + + channel_options=( + ("grpc.keepalive_time_ms", 20000), + ("grpc.keepalive_timeout_ms", 10000), + ("grpc.keepalive_permit_without_calls", True), + ("grpc.http2.max_pings_without_data", 0), + ("grpc.http2.min_time_between_pings_ms", 10000), + ) + + creds = f"{const.OTEL_USERNAME}:{const.OTEL_PASSWORD}".encode("utf-8") + token = base64.b64encode(creds).decode("utf-8") + otel_headers = {"authorization": f"Basic {token}"} + + # Create OTLP metric exporter + metric_exporter = OTLPMetricExporter( + endpoint=otlp_endpoint, + headers=otel_headers, + channel_options=channel_options, + insecure=True, + ) + + # Create metric reader with periodic export + metric_reader = PeriodicExportingMetricReader( + exporter=metric_exporter, + export_interval_millis=int(const.OTEL_METRIC_EXPORT_INTERVAL), # Default 60s + ) + + # Create and set global meter provider + provider = MeterProvider( + resource=resource, + metric_readers=[metric_reader], + ) + opentelemetry.metrics.set_meter_provider(provider) + + # Get meter for creating metrics + meter = opentelemetry.metrics.get_meter(__name__) + + # Create OTLP trace exporter/provider so worker can emit spans. + trace_exporter = OTLPSpanExporter( + endpoint=otlp_endpoint, + headers=otel_headers, + channel_options=channel_options, + insecure=True, + ) + trace_provider = TracerProvider(resource=resource) + trace_provider.add_span_processor(BatchSpanProcessor(trace_exporter)) + trace.set_tracer_provider(trace_provider) + tracer = trace.get_tracer(__name__) + + logger = set_up_logging() + logger.warning( + f"OpenTelemetry metrics and tracing initialized: endpoint={otlp_endpoint}, " + f"service={service_name}, version={service_version}" + ) + + return meter, logger, tracer + + +global_meter, global_logger, global_tracer = setup_opentelemetry() diff --git a/docker/spider-worker/src/queues.py b/docker/spider-worker/src/queues.py new file mode 100644 index 00000000..b4a5fb46 --- /dev/null +++ b/docker/spider-worker/src/queues.py @@ -0,0 +1,318 @@ +import signal +import time +import logging + +import opentelemetry +import src.amq as amq +import src.os_utils as os_utils +from src.nats_consumer import NATSQueueConsumer +from src.utils import send_email_alert, convert_dates_to_millisecs +import src.constants as const +from src.convert_to_json import process_ad + +from src.otel_setup import ( + global_logger, + global_meter, + initialize_worker_trace, + finalize_worker_trace, +) + + +WORKER_LIFETIME_SECONDS_HISTOGRAM = global_meter.create_histogram( + name="spider_worker_lifetime_seconds", + description="How long a spider worker process stayed alive before shutdown", + unit="s", +) +WORKER_IDLE_SECONDS_HISTOGRAM = global_meter.create_histogram( + name="spider_worker_idle_seconds", + description="How long a spider worker process was idle before processing a batch", + unit="s", +) +WORKER_JOBS_PROCESSED_PER_LIFETIME_HISTOGRAM = global_meter.create_histogram( + name="spider_worker_jobs_processed_per_lifetime", + description="Number of jobs processed by a worker during its lifetime", + unit="1", +) +WORKER_JOBS_PUBLISHED_AMQ_PER_LIFETIME_HISTOGRAM = global_meter.create_histogram( + name="spider_worker_jobs_published_amq_per_lifetime", + description="Number of jobs published to AMQ by a worker during its lifetime", + unit="1", +) +WORKER_JOBS_PUBLISHED_OS_PER_LIFETIME_HISTOGRAM = global_meter.create_histogram( + name="spider_worker_jobs_published_os_per_lifetime", + description="Number of jobs published to OpenSearch by a worker during its lifetime", + unit="1", +) +NATS_FETCH_BATCH_SIZE_HISTOGRAM = global_meter.create_histogram( + name="spider_worker_nats_fetch_batch_size", + description="Number of jobs in each non-empty batch fetched from NATS", + unit="1", +) +NATS_BATCH_PROCESS_SECONDS_HISTOGRAM = global_meter.create_histogram( + name="spider_worker_nats_batch_process_seconds", + description="Elapsed processing time for each non-empty fetched NATS batch", + unit="s", +) +NATS_REDELIVERED_MESSAGES_COUNTER = global_meter.create_counter( + name="spider_worker_nats_redelivered_messages_total", + description="Number of redelivered NATS messages observed while fetching batches", + unit="1", +) +WORKER_SHUTDOWNS_COUNTER = global_meter.create_counter( + name="spider_worker_shutdowns_total", + description="Number of spider worker processes that have shut down", + unit="1", +) + + +def process_nats_queue( + nats_batch_size=const.NATS_BATCH_SIZE, + nats_fetch_timeout=const.NATS_FETCH_TIMEOUT, + nats_idle_sleep=const.NATS_IDLE_SLEEP, + metadata=None, + email_alerts=None, +): + """ + Continuously pull jobs from the NATS JetStream queue and forward them to ES and AMQ. + Runs until interrupted by SIGTERM/SIGINT (graceful shutdown). + + Note: Jobs are published to NATS in batches of 100. The fetch batch size should + ideally be a multiple of 100 (e.g., 100, 200, 500, 1000) to efficiently process + complete publish batches. + + Args: + nats_batch_size: The number of jobs to fetch from the NATS JetStream queue at a time. + Should be a multiple of 100 (the publish batch size) for optimal efficiency. + Default: value of NATS_BATCH_SIZE env var (1000 if unset). + nats_fetch_timeout: The timeout for fetching jobs from the NATS JetStream queue. + A very short timeout (0.1s) returns immediately with available + messages to avoid throttling and process messages as fast as possible. + Default: value of NATS_FETCH_TIMEOUT env var (0.2s if unset). + nats_idle_sleep: The time to sleep between batches when no jobs are available. + Default: value of NATS_IDLE_SLEEP env var (0.2s if unset). + metadata: The metadata to add to the jobs. + email_alerts: The email alerts to send. + + Returns: + None + + Raises: + Exception: If there is an error fetching jobs from the NATS JetStream queue. + Exception: If there is an error posting jobs to the ES index. + Exception: If there is an error posting jobs to the AMQ queue. + """ + shutdown_requested = False + worker_start_time = time.time() + + def signal_handler(signum, frame): + """Handle shutdown signals gracefully.""" + nonlocal shutdown_requested + global_logger.warning("Received signal %d, initiating graceful shutdown...", signum) + shutdown_requested = True + + # Register signal handlers for graceful shutdown + signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGINT, signal_handler) + + consumer = NATSQueueConsumer() + + # Publish batch size - jobs are published in batches of 100 + PUBLISH_BATCH_SIZE = 100 + + counts = { + "total_jobs": 0, + "total_batches": 0, + "total_publish_batches": 0, # Track complete publish batches (100 jobs each) + "total_sent_amq": 0, + "total_upload_time": 0.0, + "total_sent_os": 0, + "total_upload_time_os": 0.0, + "total_idle_seconds": 0.0, + } + sleeping = False + # Warn if fetch batch size is not a multiple of publish batch size + if nats_batch_size % PUBLISH_BATCH_SIZE != 0: + global_logger.warning( + "Fetch batch size (%d) is not a multiple of publish batch size (%d). " + "Consider using a multiple for optimal efficiency.", + nats_batch_size, PUBLISH_BATCH_SIZE + ) + + try: + while not shutdown_requested: + batch = consumer.fetch_jobs( + batch_size=nats_batch_size, timeout=nats_fetch_timeout + ) + + initialize_worker_trace( + traceparent=batch.traceparent, + tracestate=batch.tracestate, + ) + if batch.has_traceparent_mismatch: + # TODO: Rotate span per execution when a batch contains mixed traceparents. + global_logger.warning( + "Batch contained mixed root traceparent values; using first traceparent for this worker. " + "first=%s latest=%s", + batch.traceparent, + batch.latest_traceparent, + ) + + if not batch.jobs: + if shutdown_requested: + break + if not sleeping: + global_logger.info("No jobs in NATS, sleeping until next batch") + counts["total_idle_seconds"] += nats_idle_sleep + time.sleep(nats_idle_sleep) + sleeping = True + continue + + sleeping = False + job_count = len(batch.jobs) + NATS_FETCH_BATCH_SIZE_HISTOGRAM.record(job_count) + if batch.redelivered_messages: + NATS_REDELIVERED_MESSAGES_COUNTER.add(batch.redelivered_messages) + global_logger.debug( + "Jobs fetched from NATS: %d jobs (~%.1f publish batches)", + job_count, job_count / PUBLISH_BATCH_SIZE + ) + counts["total_jobs"] += job_count + + batch_processing_start = time.time() + all_chunks_success = True + for chunk in batch.chunked(const.NATS_CHUNK_SIZE): + success = True + bunch = chunk.jobs + try: + amq_bunch = [] + job_sizes = [] + for job_id, classad_obj in bunch: + try: + # Convert ClassAd to dictionary + dict_ad = process_ad( + classad_obj, + return_dict=True, + reduce_data=const.REDUCE_DATA, + pool_name="Unknown", # TODO: Get pool_name from metadata or job + ) + if dict_ad: + amq_bunch.append((job_id, convert_dates_to_millisecs(dict_ad))) + job_sizes.append(len(dict_ad.keys())) + except Exception as e: + global_logger.warning("Failed to convert ClassAd to dict for job %s: %s", job_id, e) + continue + if global_logger.isEnabledFor(logging.DEBUG) and job_sizes: + global_logger.debug( + "Job sizes: Average %s, Max %s, Min %s", + sum(job_sizes) / len(job_sizes), + max(job_sizes), + min(job_sizes), + ) + try: + if const.DRY_RUN: + global_logger.info("Dry run, would have uploaded %d jobs to OpenSearch", len(amq_bunch)) + else: + os_upload_start_time = time.time() + success, _ = os_utils.os_upload_docs_in_bulk(list(doc[1] for doc in amq_bunch), index_prefix=const.OS_INDEX_TEMPLATE, timestamp=time.time()) + global_logger.debug("Uploaded %d/%d jobs to OpenSearch in %.1f seconds", success, len(amq_bunch), time.time() - os_upload_start_time) + counts["total_sent_os"] += success + counts["total_upload_time_os"] += time.time() - os_upload_start_time + except Exception as e: + global_logger.error("Error uploading to OpenSearch: %s", e) + # success = False + try: + if const.DRY_RUN: + global_logger.info("Dry run, would have uploaded %d jobs to AMQ", len(amq_bunch)) + chunk.finalize(success) + continue + sent, received, elapsed = amq.post_ads(amq_bunch, metadata=metadata) + global_logger.debug( + "Uploaded %d/%d docs to StompAMQ in %.1f seconds", + sent, + received, + elapsed, + ) + counts["total_sent_amq"] += sent + counts["total_upload_time"] += elapsed + except Exception as exc: + success = False + global_logger.error("Error posting to AMQ: %s", exc) + send_email_alert( + email_alerts, + "spider_cms NATS queue AMQ failure", + str(exc), + ) + except Exception as exc: # pylint: disable=broad-except + success = False + global_logger.error( + "Failed to process NATS batch with %d jobs: %s", + len(bunch), + exc, + ) + + chunk.finalize(success) + if not success: + all_chunks_success = False + NATS_BATCH_PROCESS_SECONDS_HISTOGRAM.record( + max(time.time() - batch_processing_start, 0.0) + ) + + counts["total_batches"] += 1 + counts["total_publish_batches"] += job_count / PUBLISH_BATCH_SIZE + if all_chunks_success: + global_logger.debug( + "NATS batch processed successfully: %d jobs (~%.1f publish batches)", + job_count, job_count / PUBLISH_BATCH_SIZE + ) + else: + global_logger.error( + "Failed to process NATS batch (%d jobs, ~%.1f publish batches), sleeping for %s seconds", + job_count, job_count / PUBLISH_BATCH_SIZE, nats_idle_sleep + ) + if not shutdown_requested: + time.sleep(nats_idle_sleep) + + except KeyboardInterrupt: + global_logger.warning("Received KeyboardInterrupt, initiating graceful shutdown...") + finally: + consumer.close() + worker_lifetime_seconds = max(time.time() - worker_start_time, 0.0) + + WORKER_LIFETIME_SECONDS_HISTOGRAM.record(worker_lifetime_seconds) + WORKER_JOBS_PROCESSED_PER_LIFETIME_HISTOGRAM.record(counts["total_jobs"]) + WORKER_JOBS_PUBLISHED_AMQ_PER_LIFETIME_HISTOGRAM.record( + counts["total_sent_amq"] + ) + WORKER_JOBS_PUBLISHED_OS_PER_LIFETIME_HISTOGRAM.record( + counts["total_sent_os"] + ) + WORKER_IDLE_SECONDS_HISTOGRAM.record(counts["total_idle_seconds"]) + WORKER_SHUTDOWNS_COUNTER.add(1) + + finalize_worker_trace( + worker_seconds=worker_lifetime_seconds, + total_jobs=counts["total_jobs"], + total_sent_amq=counts["total_sent_amq"], + total_sent_os=counts["total_sent_os"], + ) + + # Best-effort flush so shutdown metrics are exported before process exit. + try: + opentelemetry.metrics.get_meter_provider().force_flush() + except Exception as exc: # pylint: disable=broad-except + global_logger.warning("Failed to force flush metrics during shutdown: %s", exc) + try: + opentelemetry.trace.get_tracer_provider().force_flush() + except Exception as exc: # pylint: disable=broad-except + global_logger.warning("Failed to force flush traces during shutdown: %s", exc) + + global_logger.info( + "Processing of NATS queue finished. Summary: " + "total_jobs=%d, total_fetch_batches=%d, total_publish_batches=~%.1f, " + "total_sent_amq=%d, total_sent_os=%d", + counts["total_jobs"], + counts["total_batches"], + counts["total_publish_batches"], + counts["total_sent_amq"], + counts["total_sent_os"], + ) diff --git a/docker/spider-worker/src/utils.py b/docker/spider-worker/src/utils.py new file mode 100644 index 00000000..daa76f20 --- /dev/null +++ b/docker/spider-worker/src/utils.py @@ -0,0 +1,88 @@ +""" +Various helper utilities for the HTCondor-ES integration +""" + +import os +import pwd +import time +import shlex +import socket +import logging +import smtplib +import subprocess +import email.mime.text + +import vals + + + +def send_email_alert(recipients, subject, message): + """ + Send a simple email alert (typically of failure). + """ + # TMP: somehow send_email_alert still sending alerts + if not recipients: + return + msg = email.mime.text.MIMEText(message) + msg["Subject"] = "%s - %sh: %s" % ( + socket.gethostname(), + time.strftime("%b %d, %H:%M"), + subject, + ) + + domain = socket.getfqdn() + uid = os.geteuid() + pw_info = pwd.getpwuid(uid) + if "cern.ch" not in domain: + domain = "%s.unl.edu" % socket.gethostname() + msg["From"] = "%s@%s" % (pw_info.pw_name, domain) + msg["To"] = recipients[0] + + try: + sess = smtplib.SMTP("localhost") + sess.sendmail(msg["From"], recipients, msg.as_string()) + sess.quit() + except Exception as exn: # pylint: disable=broad-except + logging.warning("Email notification failed: %s", str(exn)) + + +def collect_metadata(): + """ + Return a dictionary with: + - hostname + - username + - current time (in epoch millisec) + - hash of current git commit + """ + result = {} + result["spider_git_hash"] = get_githash() + result["spider_hostname"] = socket.gethostname() + result["spider_username"] = pwd.getpwuid(os.geteuid()).pw_name + result["spider_runtime"] = int(time.time() * 1000) + return result + + +def get_githash(): + """Returns the git hash of the current commit in the scripts repository""" + gitwd = os.path.dirname(os.path.realpath(__file__)) + cmd = r"git rev-parse --verify HEAD" + try: + call = subprocess.Popen(shlex.split(cmd), stdout=subprocess.PIPE, cwd=gitwd) + out, err = call.communicate() + return str(out.strip()) + + except Exception as e: + logging.warning(str(e)) + return "unknown" + + + + +def convert_dates_to_millisecs(record: dict) -> dict: + for date_field in vals.date_vals: + try: + record[date_field] *= 1000 + except (KeyError, TypeError): + continue + + return record \ No newline at end of file diff --git a/docker/spider-worker/src/vals.py b/docker/spider-worker/src/vals.py new file mode 100644 index 00000000..4dee57f7 --- /dev/null +++ b/docker/spider-worker/src/vals.py @@ -0,0 +1,521 @@ +string_vals = { + "AutoClusterId", + "AffiliationInstitute", + "AffiliationCountry", + "Processor", + "ChirpCMSSWCPUModels", + "CPUModel", + "CPUModelName", + "ChirpCMSSWCPUModels", + "CMSPrimaryPrimaryDataset", + "CMSPrimaryProcessedDataset", + "CMSPrimaryDataTier", + "CMSSWVersion", + "CMSSWMajorVersion", + "CMSSWReleaseSeries", + "CRAB_JobType", + "CRAB_JobSW", + "CRAB_JobArch", + "CRAB_Id", + "CRAB_ISB", + "CRAB_PostJobStatus", + "CRAB_Workflow", + "CRAB_UserRole", + "CMSGroups", + "CRAB_UserHN", + "CRAB_UserGroup", + "CRAB_TaskWorker", + "CRAB_SiteWhitelist", + "CRAB_SiteBlacklist", + "CRAB_SplitAlgo", + "CRAB_PrimaryDataset", + "Args", + "AccountingGroup", + "Cmd", + "CMS_JobType", + "CMS_WMTool", + "DESIRED_Archs", + "DESIRED_CMSDataLocations", + "DESIRED_CMSDataset", + "DESIRED_Sites", + "ExtDESIRED_Sites", + "FormattedCrabId", + "GlobalJobId", + "GlideinClient", + "GlideinEntryName", + "GlideinFactory", + "GlideinFrontendName", + "GlideinName", + "GLIDECLIENT_Name", + "GLIDEIN_Entry_Name", + "GLIDEIN_Factory", + "GlobusRSL", + "GridJobId", + "LastRemoteHost", + "MachineAttrCMSSubSiteName0", + "MATCH_EXP_JOB_GLIDECLIENT_Name", + "MATCH_EXP_JOB_GLIDEIN_ClusterId", + "MATCH_EXP_JOB_GLIDEIN_CMSSite", + "MATCH_EXP_JOB_GLIDEIN_Entry_Name", + "MATCH_EXP_JOB_GLIDEIN_Factory", + "MATCH_EXP_JOB_GLIDEIN_Name", + "MATCH_EXP_JOB_GLIDEIN_Schedd", + "MATCH_EXP_JOB_GLIDEIN_SEs", + "MATCH_EXP_JOB_GLIDEIN_Site", + "MATCH_EXP_JOB_GLIDEIN_SiteWMS", + "MATCH_EXP_JOB_GLIDEIN_SiteWMS_JobId", + "MATCH_EXP_JOB_GLIDEIN_SiteWMS_Queue", + "MATCH_EXP_JOB_GLIDEIN_SiteWMS_Slot", + "MachineAttrCUDACapability0", + "MachineAttrCUDADeviceName0", + "MachineAttrCUDADriverVersion0", + "Owner", + "Rank", + "RemoteHost", + "REQUIRED_OS", + "ShouldTransferFiles", + "StartdIpAddr", + "StartdPrincipal", + "User", + "WhenToTransferOutput", + "WMAgent_AgentName", + "WMAgent_RequestName", + "WMAgent_SubTaskName", + "x509UserProxyEmail", + "x509UserProxyFirstFQAN", + "x509UserProxyFQAN", + "x509userproxysubject", + "x509UserProxyVOName", + "InputData", + "Original_DESIRED_Sites", + "WMAgent_TaskType", + "NordugridRSL", + "Campaign", + "TaskType", + "DataLocations", + "Workflow", + "Site", + "Tier", + "Country", + "Status", + "Universe", + "ExitReason", + "LastHoldReason", + "RemoveReason", + "DESIRED_Overflow_Region", + "DESIRED_OpSysMajorVers", + "DESIRED_CMSDataset", + "DAGNodeName", + "DAGParentNodeNames", + "OverflowType", + "ScheddName", +} + +int_vals = { + "CRAB_Retry", + "BytesRecvd", + "BytesSent", + "ClusterId", + "CommittedSlotTime", + "CumulativeSlotTime", + "CumulativeSuspensionTime", + "CurrentHosts", + "CRAB_JobCount", + "DelegatedProxyExpiration", + "DiskUsage_RAW", + "ExecutableSize_RAW", + "ExitStatus", + "GlobusStatus", + "ImageSize_RAW", + "JobPrio", + "JobRunCount", + "JobStatus", + "JobFailed", + "JobUniverse", + "LastJobStatus", + "LocalSysCpu", + "LocalUserCpu", + "MachineAttrCpus0", + "MachineAttrSlotWeight0", + "MachineAttrCUDAComputeUnits0", + "MachineAttrCUDACoresPerCU0", + "MachineAttrCUDAGlobalMemoryMb0", + "MATCH_EXP_JOB_GLIDEIN_Job_Max_Time", + "MATCH_EXP_JOB_GLIDEIN_MaxMemMBs", + "MATCH_EXP_JOB_GLIDEIN_Max_Walltime", + "MATCH_EXP_JOB_GLIDEIN_Memory", + "MATCH_EXP_JOB_GLIDEIN_ProcId", + "MATCH_EXP_JOB_GLIDEIN_ToDie", + "MATCH_EXP_JOB_GLIDEIN_ToRetire", + "MaxHosts", + "MaxWallTimeMins_RAW", + "MemoryUsage", + "MinHosts", + "NumGlobusSubmitsNumJobMatches", + "NumJobStarts", + "NumRestarts", + "NumShadowStarts", + "NumSystemHolds", + "PilotRestLifeTimeMins", + "PostJobPrio1", + "PostJobPrio2", + "ProcId", + "RecentBlockReadKbytes", + "RecentBlockReads", + "RecentBlockWriteKbytes", + "RecentBlockWrites", + "RemoteSlotID", + "RemoteSysCpu", + "RemoteUserCpu", + "RemoteWallClockTime", + "RequestCpus", + "RequestDisk_RAW", + "RequestMemory_RAW", + "ResidentSetSize_RAW", + "StatsLifetimeStarter", + "TotalSuspensions", + "TransferInputSizeMB", + "WallClockCheckpoint", + "WMAgent_JobID", + "DesiredSiteCount", + "DataLocationsCount", +} + +date_vals = { + "CompletionDate", + "CRAB_TaskCreationDate", + "EnteredCurrentStatus", + "JobCurrentStartDate", + "JobCurrentStartExecutingDate", + "JobCurrentStartTransferOutputDate", + "JobLastStartDate", + "JobStartDate", + "LastMatchTime", + "LastSuspensionTime", + "LastVacateTime_RAW", + "MATCH_GLIDEIN_ToDie", + "MATCH_GLIDEIN_ToRetire", + "QDate", + "ShadowBday", + "StageInFinish", + "StageInStart", + "JobFinishedHookDone", + "LastJobLeaseRenewal", + "LastRemoteStatusUpdate", + "GLIDEIN_ToDie", + "GLIDEIN_ToRetire", + "DataCollectionDate", + "RecordTime", + "ChirpCMSSWLastUpdate", +} + +ignore = { + "Arguments", + "CmdHash", + "CRAB_UserDN", + "CRAB_Destination", + "CRAB_DBSURL", + "CRAB_ASOURL", + "CRAB_ASODB", + "CRAB_AdditionalOutputFiles", + "CRAB_EDMOutputFiles", + "CRAB_TFileOutputFiles", + "CRAB_oneEventMode", + "CRAB_NumAutomJobRetries", + "CRAB_localOutputFiles", + "CRAB_ASOTimeout", + "CRAB_OutTempLFNDir", + "CRAB_PublishDBSURL", + "CRAB_PublishGroupName", + "CRAB_RestURInoAPI", + "CRAB_RestHost", + "CRAB_ReqName", + "CRAB_RetryOnASOFailures", + "CRAB_StageoutPolicy", + "SubmitEventNotes", + "DAGManNodesMask", + "DAGManNodesLog", + "DAGManJobId", + "accounting_group", + "AcctGroup", + "AcctGroupUser", + "AllowOpportunistic", + "AutoClusterAttrs", + "BufferBlockSize", + "BufferSize", + "CondorPlatform", + "CondorVersion", + "DiskUsage", + "Err", + "Environment", + "EnvDelim", + "Env", + "ExecutableSize", + "HasPrioCorrection", + "GlideinCredentialIdentifier", + "GlideinLogNr", + "GlideinSecurityClass", + "GlideinSlotsLayout", + "GlideinWebBase", + "GlideinWorkDir", + "ImageSize", + "In", + "Iwd", + "JobAdInformationAttrs", + "job_ad_information_attrs", + "JOB_GLIDECLIENT_Name", + "JOB_GLIDEIN_ClusterId", + "JOB_GLIDEIN_CMSSite", + "JOBGLIDEIN_CMSSite", + "JOB_GLIDEIN_Entry_Name", + "JOB_GLIDEIN_Factory", + "JOB_GLIDEIN_Job_Max_Time", + "JOB_GLIDEIN_MaxMemMBs", + "JOB_GLIDEIN_Max_Walltime", + "JOB_GLIDEIN_Memory", + "JOB_GLIDEIN_Name", + "JOB_GLIDEIN_ProcId", + "JOB_GLIDEIN_Schedd", + "JOB_GLIDEIN_SEs", + "JOB_GLIDEIN_Site", + "JOB_GLIDEIN_SiteWMS", + "JOB_GLIDEIN_SiteWMS_JobId", + "JOB_GLIDEIN_SiteWMS_Queue", + "JOB_GLIDEIN_SiteWMS_Slot", + "JOB_GLIDEIN_ToDie", + "JOB_GLIDEIN_ToRetire", + "JobLeaseDuration", + "JobNotification", + "JOB_Site", + "Managed", + "MATCH_EXP_JOBGLIDEIN_CMSSite", + "MATCH_EXP_JOB_Site", + "MATCH_GLIDECLIENT_Name", + "MATCH_GLIDEIN_ClusterId", + "MATCH_GLIDEIN_CMSSite", + "MATCH_GLIDEIN_Entry_Name", + "MATCH_GLIDEIN_Factory", + "MATCH_GLIDEIN_Job_Max_Time", + "MATCH_GLIDEIN_MaxMemMBs", + "MATCH_GLIDEIN_Max_Walltime", + "MATCH_GLIDEIN_Name", + "MATCH_GLIDEIN_ProcId", + "MATCH_GLIDEIN_Schedd", + "MATCH_GLIDEIN_SEs", + "MATCH_GLIDEIN_Site", + "MATCH_GLIDEIN_SiteWMS", + "MATCH_GLIDEIN_SiteWMS_JobId", + "MATCH_GLIDEIN_SiteWMS_Queue", + "MATCH_GLIDEIN_SiteWMS_Slot", + "MATCH_Memory", + "MyType", + "NiceUser", + "NumCkpts", + "NumCkpts_RAW", + "OnExitHold", + "OnExitRemove", + "OrigMaxHosts", + "Out", + "PeriodicHold", + "PeriodicRelease", + "PeriodicRemove", + "Prev_DESIRED_Sites", + "PublicClaimId", + "RequestDisk", + "RequestMemory", + "ResidentSetSize", + "REQUIRES_LOCAL_DATA", + "RecentBlockReadKbytes", + "RecentBlockReads", + "RecentBlockWriteKbytes", + "RecentBlockWrites", + "RootDir", + "ServerTime", + "SpooledOutputFiles", + "StreamErr", + "StreamOut", + "TargetType", + "TransferIn", + "TransferInput", + "TransferOutput", + "UserLog", + "UserLogUseXML", + "use_x509userproxy", + "x509userproxy", + "x509UserProxyExpiration", + "WantCheckpoint", + "WantRemoteIO", + "WantRemoteSyscalls", + "BlockReadKbytes", + "BlockReads", + "BlockWriteKbytes", + "BlockWrites", + "LocalSysCpu", + "LeaveJobInQueue", + "LocalUserCpu", + "JobMachineAttrs", + "LastRejMatchReason", + "MachineAttrGLIDEIN_CMSSite0", + "CMS_ALLOW_OVERFLOW", + "LastPublicClaimId", + "LastRemotePool", + "Used_Gatekeeper", + "DESIRED_OpSyses", +} + +bool_vals = { + "CurrentStatusUnknown", + "CRAB_Publish", + "CRAB_SaveLogsFlag", + "CRAB_TransferOutputs", + "GlobusResubmit", + "TransferQueued", + "TransferringInput", + "HasSingularity", + "NiceUser", + "ExitBySignal", + "CMSSWDone", + "HasBeenRouted", + "HasBeenOverflowRouted", + "HasBeenTimingTuned", + "MachineAttrCUDAECCEnabled0", + "MachineAttrGLIDEIN_OVERLOAD_ENABLED0", +} + +# Fields to be kept in docs concerning running jobs +running_fields = { + "AccountingGroup", + "AutoClusterId", + "AffiliationInstitute", + "AffiliationCountry", + "BenchmarkJobDB12", + "Campaign", + "CMS_CampaignType", + "CMS_JobType", + "CMS_JobRetryCount", + "CMS_Pool", + "CMSGroups", + "CMSPrimaryDataTier", + "CMSSWKLumis", + "CMSSWWallHrs", + "CMSSWVersion", + "CMSSWMajorVersion", + "CMSSWReleaseSeries", + "CommittedCoreHr", + "CommittedTime", + "CoreHr", + "Country", + "CpuBadput", + "CpuEff", + "CpuEffOutlier", + "CpuEventRate", + "CpuTimeHr", + "CpuTimePerEvent", + "CRAB_AsyncDest", + "CRAB_DataBlock", + "CRAB_Id", + "CRAB_JobCount", + "CRAB_PostJobStatus", + "CRAB_Retry", + "CRAB_TaskCreationDate", + "CRAB_UserHN", + "CRAB_Workflow", + "CRAB_SplitAlgo", + "CMS_SubmissionTool", + "CMS_WMTool", + # "DataLocations", + "DESIRED_CMSDataset", + # "DESIRED_Sites", + "EnteredCurrentStatus", + "EventRate", + "FormattedCrabId", + "GlobalJobId", + "GLIDECLIENT_Name", + "GLIDEIN_ClusterId", + "GLIDEIN_Entry_Name", + "GLIDEIN_Factory", + "GLIDEIN_ProcId", + "HasSingularity", + "InputData", + "InputGB", + "JobPrio", + "JobCurrentStartDate", + "JobLastStartDate", + "JobUniverse", + "KEvents", + "MachineAttrCMSSF_ResourceType0", + "MachineAttrCMSSubSiteName0", + "MachineAttrGLIDEIN_OVERLOAD_ENABLED0", + "MaxWallTimeMins", + "MegaEvents", + "MemoryMB", + "OutputGB", + "QueueHrs", + "QDate", + "ReadTimeMins", + "RecordTime", + "RemoteHost", + "RequestCpus", + "RequestMemory", + "RequestMemory_Eval", + "ScheddName", + "Site", + "Status", + "TaskType", + "Tier", + "TimePerEvent", + "Type", + "WallClockHr", + "WMAgent_JobID", + "WMAgent_RequestName", + "WMAgent_SubTaskName", + "Workflow", + "DESIRED_Sites", + "DESIRED_SITES_Diff", + "DESIRED_SITES_Orig", + "EstimatedWallTimeMins", + "EstimatedWallTimeJobCount", + "PilotRestLifeTimeMins", + "LastRouted", + "LastTimingTuned", + "LPCRouted", + "MemoryUsage", + "PeriodicHoldReason", + "RouteType", + "HasBeenOverflowRouted", + "HasBeenRouted", + "HasBeenTimingTuned", +} + +status = { + 0: "Unexpanded", + 1: "Idle", + 2: "Running", + 3: "Removed", + 4: "Completed", + 5: "Held", + 6: "Error", +} + +universe = { + 1: "Standard", + 2: "Pipe", + 3: "Linda", + 4: "PVM", + 5: "Vanilla", + 6: "PVMD", + 7: "Scheduler", + 8: "MPI", + 9: "Grid", + 10: "Java", + 11: "Parallel", + 12: "Local", +} + +postjob_status_decode = { + "NOT RUN": "postProc", + "TRANSFERRING": "transferring", + "COOLOFF": "toRetry", + "FAILED": "failed", + "FINISHED": "finished", +}