From 0499ef209144f902da36ab6c2755332808b0d2cf Mon Sep 17 00:00:00 2001 From: Ivan Santos <301291+pragmaticivan@users.noreply.github.com> Date: Thu, 8 Jan 2026 18:58:15 -0600 Subject: [PATCH 1/6] feat: python improvements and uv support --- .github/workflows/ci-python.yml | 23 +- .github/workflows/release-layer-python.yml | 6 +- .gitignore | 6 +- python/src/otel/Dockerfile | 12 +- python/src/otel/Makefile | 14 - .../src/otel/otel_sdk/nodeps-requirements.txt | 4 - python/src/otel/otel_sdk/otel_wrapper.py | 409 +++++- python/src/otel/otel_sdk/requirements.txt | 36 - python/src/otel/pyproject.toml | 82 ++ python/src/otel/tests/nodeps-requirements.txt | 1 - python/src/otel/tests/requirements.txt | 5 - python/src/otel/tests/test_otel.py | 19 +- python/src/otel/uv.lock | 1183 +++++++++++++++++ python/src/run.sh | 1 - python/src/tox.ini | 29 - 15 files changed, 1683 insertions(+), 147 deletions(-) delete mode 100644 python/src/otel/Makefile delete mode 100644 python/src/otel/otel_sdk/nodeps-requirements.txt delete mode 100644 python/src/otel/otel_sdk/requirements.txt create mode 100644 python/src/otel/pyproject.toml delete mode 100644 python/src/otel/tests/nodeps-requirements.txt delete mode 100644 python/src/otel/tests/requirements.txt create mode 100644 python/src/otel/uv.lock delete mode 120000 python/src/run.sh delete mode 100644 python/src/tox.ini diff --git a/.github/workflows/ci-python.yml b/.github/workflows/ci-python.yml index b3456e14c1..339e0f7965 100644 --- a/.github/workflows/ci-python.yml +++ b/.github/workflows/ci-python.yml @@ -16,9 +16,6 @@ on: env: AWS_REGION: us-east-1 - # Copied this CORE_REPO_SHA from - # https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/.github/workflows/test.yml#L9 - CORE_REPO_SHA: v1.19.0 permissions: contents: read @@ -36,19 +33,17 @@ jobs: steps: - name: Checkout this repo uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - name: Setup Python for OTel Python SDK uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: ${{ matrix.python }} - - name: Install tox testing package - working-directory: python/src + + - name: Set up uv + uses: astral-sh/setup-uv@v7 + + - name: Run tests + working-directory: python/src/otel run: | - pip install tox - tox - - name: Set up Go for ADOT Collector - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 - with: - go-version: '^1.20.8' - - name: Build Python Layer which includes ADOT Collector - working-directory: python/src - run: ./run.sh -b + uv sync --extra test --extra instrumentation + uv run pytest tests diff --git a/.github/workflows/release-layer-python.yml b/.github/workflows/release-layer-python.yml index 006575a1c5..0e5797f0b9 100644 --- a/.github/workflows/release-layer-python.yml +++ b/.github/workflows/release-layer-python.yml @@ -37,8 +37,8 @@ jobs: - name: Save PYTHON_OPENTELEMETRY_SDK_VERSION id: save-python-opentelemetry-sdk-version run: | - cd python/src - echo "PYTHON_OPENTELEMETRY_SDK_VERSION=$(cat otel/otel_sdk/requirements.txt | grep opentelemetry-sdk | sed 's/.*==\([^ ]*\).*/\1/')" >> $GITHUB_ENV + cd python/src/otel + echo "PYTHON_OPENTELEMETRY_SDK_VERSION=$(grep 'opentelemetry-sdk' pyproject.toml | sed 's/.*==\([^"]*\).*/\1/')" >> $GITHUB_ENV echo "PYTHON_OPENTELEMETRY_SDK_VERSION=$PYTHON_OPENTELEMETRY_SDK_VERSION" >> $GITHUB_OUTPUT shell: bash @@ -78,7 +78,7 @@ jobs: needs: build-layer strategy: matrix: - aws_region: + aws_region: - ap-northeast-1 - ap-northeast-2 - ap-south-1 diff --git a/.gitignore b/.gitignore index 71c07fce06..29bdbbedc1 100644 --- a/.gitignore +++ b/.gitignore @@ -13,8 +13,10 @@ terraform.* .DS_Store .tox -__pycache__/* -.pyc +.venv +.pytest_cache/ +__pycache__/ +*.pyc build.toml *.zip diff --git a/python/src/otel/Dockerfile b/python/src/otel/Dockerfile index 9e5b16b4ac..5f129ed6ee 100644 --- a/python/src/otel/Dockerfile +++ b/python/src/otel/Dockerfile @@ -6,13 +6,13 @@ ADD . /workspace WORKDIR /workspace +# Install uv +RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \ + export PATH="/root/.local/bin:$PATH" + RUN mkdir -p /build && \ - python3 -m pip install -r otel_sdk/requirements.txt -t /build/python && \ - python3 -m pip install -r otel_sdk/nodeps-requirements.txt -t /build/tmp --no-deps && \ - # We need to use a `/build/tmp/` folder otherwise the instrumentation packages - # do not get fully downloaded to the `opentelemetry/instrumentation/` path. - cp -r /build/tmp/* /build/python/ && \ - rm -rf /build/tmp && \ + /root/.local/bin/uv venv /build/.venv && \ + /root/.local/bin/uv pip install --python /build/.venv/bin/python '.[instrumentation]' --target /build/python && \ mv otel_sdk/otel_wrapper.py /build/python && \ mv otel_sdk/otel-instrument /build && \ mv otel_sdk/otel-handler /build && \ diff --git a/python/src/otel/Makefile b/python/src/otel/Makefile deleted file mode 100644 index bdb34e793e..0000000000 --- a/python/src/otel/Makefile +++ /dev/null @@ -1,14 +0,0 @@ -export SDK=$(shell pwd)/otel_sdk - -build-OTelLayer: - mkdir -p $(ARTIFACTS_DIR)/python - python3 -m pip install -r $(SDK)/requirements.txt -t $(ARTIFACTS_DIR)/python - python3 -m pip install -r $(SDK)/nodeps-requirements.txt -t $(ARTIFACTS_DIR)/tmp --no-deps - python3 -m pip freeze --path $(ARTIFACTS_DIR)/python - cp -r $(ARTIFACTS_DIR)/tmp/* $(ARTIFACTS_DIR)/python/ - rm -rf $(ARTIFACTS_DIR)/tmp - cp -r $(SDK)/* $(ARTIFACTS_DIR)/python - chmod 755 $(ARTIFACTS_DIR)/python/otel-instrument - chmod 755 $(ARTIFACTS_DIR)/python/otel-handler - rm -rf $(ARTIFACTS_DIR)/python/boto* - rm -rf $(ARTIFACTS_DIR)/python/urllib3* diff --git a/python/src/otel/otel_sdk/nodeps-requirements.txt b/python/src/otel/otel_sdk/nodeps-requirements.txt deleted file mode 100644 index 742f35317b..0000000000 --- a/python/src/otel/otel_sdk/nodeps-requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -# TODO: move these dependencies to requirements.txt when they stopped relying on a pinned version of -# opentelemetry-propagator-aws-xray -opentelemetry-instrumentation-aws-lambda==0.60b1 -opentelemetry-instrumentation-botocore==0.60b1 diff --git a/python/src/otel/otel_sdk/otel_wrapper.py b/python/src/otel/otel_sdk/otel_wrapper.py index 295410406c..ab88549562 100644 --- a/python/src/otel/otel_sdk/otel_wrapper.py +++ b/python/src/otel/otel_sdk/otel_wrapper.py @@ -16,32 +16,391 @@ `otel_wrapper.py` This file serves as a wrapper over the user's Lambda function. +""" -Usage ------ -Patch the reserved `_HANDLER` Lambda environment variable to point to this -file's `otel_wrapper.lambda_handler` property. Do this having saved the original -`_HANDLER` in the `ORIG_HANDLER` environment variable. Doing this makes it so -that **on import of this file, the handler is instrumented**. +import importlib +import logging +import os -Instrumenting any earlier will cause the instrumentation to be lost because the -AWS Service uses `imp.load_module` to import the handler which RELOADS the -module. This is why AwsLambdaInstrumentor cannot be instrumented with the -`opentelemetry-instrument` script. +from opentelemetry import metrics, trace +from opentelemetry.instrumentation.aws_lambda import AwsLambdaInstrumentor +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import ( + ConsoleMetricExporter, + PeriodicExportingMetricReader, +) +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ( + BatchSpanProcessor, + ConsoleSpanExporter, + SimpleSpanProcessor, +) -See more: -https://docs.python.org/3/library/imp.html#imp.load_module +# Try to import AWS Lambda resource detector +try: + from opentelemetry.sdk.extension.aws.resource._lambda import ( + AwsLambdaResourceDetector, + ) +except ImportError: + AwsLambdaResourceDetector = None -""" +# Environment Variables +OTEL_LOG_LEVEL = "OTEL_LOG_LEVEL" +OTEL_PROPAGATORS = "OTEL_PROPAGATORS" +OTEL_TRACES_EXPORTER = "OTEL_TRACES_EXPORTER" +OTEL_METRICS_EXPORTER = "OTEL_METRICS_EXPORTER" +OTEL_SERVICE_NAME = "OTEL_SERVICE_NAME" +OTEL_RESOURCE_ATTRIBUTES = "OTEL_RESOURCE_ATTRIBUTES" -import os -from importlib import import_module +# Import exporters +try: + from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter +except ImportError: + OTLPSpanExporter = None -from opentelemetry.instrumentation.aws_lambda import AwsLambdaInstrumentor +try: + from opentelemetry.exporter.otlp.proto.http.metric_exporter import ( + OTLPMetricExporter, + ) +except ImportError: + OTLPMetricExporter = None + +logger = logging.getLogger(__name__) + +# Default instrumentations for Lambda +# Following the Node.js layer pattern, only core network instrumentations are defaults +# dns, http, net in Node.js -> no exact Python equivalent (http is at framework level) +# AWS SDK is always loaded (not part of defaults in Node.js either, it's added separately) +DEFAULT_INSTRUMENTATIONS = [] + + +def _get_active_instrumentations(): + """Determine which instrumentations should be loaded for Lambda. + + Respects OTEL_PYTHON_ENABLED_INSTRUMENTATIONS and OTEL_PYTHON_DISABLED_INSTRUMENTATIONS. + + Note: Unlike explicit defaults, botocore and aws-lambda instrumentations are + always loaded as they are essential for Lambda operation (similar to how Node.js + layer always loads AwsInstrumentation and AwsLambdaInstrumentation). + """ + enabled = os.environ.get("OTEL_PYTHON_ENABLED_INSTRUMENTATIONS") + # If explicitly enabled, only load those; otherwise use defaults + active = set(enabled.split(",")) if enabled else set(DEFAULT_INSTRUMENTATIONS) + + # Remove any disabled instrumentations + disabled = os.environ.get("OTEL_PYTHON_DISABLED_INSTRUMENTATIONS") + if disabled: + for item in disabled.split(","): + active.discard(item.strip()) + + return active + + +def _load_instrumentations(): + """Load and configure instrumentations for Lambda functions. + + Similar to Node.js createInstrumentations() pattern: + - AwsInstrumentation (botocore) is always loaded + - AwsLambdaInstrumentation is always loaded + - Additional instrumentations can be enabled via environment variables + + Conditionally loads instrumentations based on environment variables: + - OTEL_PYTHON_ENABLED_INSTRUMENTATIONS: Only load specified instrumentations (comma-separated) + - OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: Disable specific instrumentations (comma-separated) + + Available optional instrumentations: + - HTTP clients: requests, aiohttp-client, urllib, urllib3 + - Web frameworks: django, flask, fastapi, starlette, falcon, pyramid, tornado + - Databases: psycopg2, pymongo, pymysql, mysql, asyncpg, sqlite3, sqlalchemy + - AWS services: boto, boto3sqs + - Messaging: celery, redis + - Other: grpc, jinja2, pymemcache, elasticsearch, wsgi, asgi, dbapi + + Example: OTEL_PYTHON_ENABLED_INSTRUMENTATIONS=requests,psycopg2,redis + """ + active_instrumentations = _get_active_instrumentations() + + # Instrumentation registry - maps names to import paths and instrumentor classes + # Format: "name": ("module.path", "InstrumentorClass") + INSTRUMENTATIONS = { + # botocore is always loaded (see below), included here for completeness + "botocore": ( + "opentelemetry.instrumentation.botocore", + "BotocoreInstrumentor", + ), + # HTTP Clients + "requests": ( + "opentelemetry.instrumentation.requests", + "RequestsInstrumentor", + ), + "aiohttp-client": ( + "opentelemetry.instrumentation.aiohttp_client", + "AioHttpClientInstrumentor", + ), + "urllib": ("opentelemetry.instrumentation.urllib", "URLLibInstrumentor"), + "urllib3": ("opentelemetry.instrumentation.urllib3", "URLLib3Instrumentor"), + # Web Frameworks + "django": ("opentelemetry.instrumentation.django", "DjangoInstrumentor"), + "flask": ("opentelemetry.instrumentation.flask", "FlaskInstrumentor"), + "fastapi": ("opentelemetry.instrumentation.fastapi", "FastAPIInstrumentor"), + "starlette": ( + "opentelemetry.instrumentation.starlette", + "StarletteInstrumentor", + ), + "falcon": ("opentelemetry.instrumentation.falcon", "FalconInstrumentor"), + "pyramid": ("opentelemetry.instrumentation.pyramid", "PyramidInstrumentor"), + "tornado": ("opentelemetry.instrumentation.tornado", "TornadoInstrumentor"), + # Databases + "psycopg2": ( + "opentelemetry.instrumentation.psycopg2", + "Psycopg2Instrumentor", + ), + "pymongo": ("opentelemetry.instrumentation.pymongo", "PymongoInstrumentor"), + "pymysql": ("opentelemetry.instrumentation.pymysql", "PyMySQLInstrumentor"), + "mysql": ( + "opentelemetry.instrumentation.mysql", + "MySQLInstrumentor", + ), + "asyncpg": ("opentelemetry.instrumentation.asyncpg", "AsyncPGInstrumentor"), + "sqlite3": ("opentelemetry.instrumentation.sqlite3", "SQLite3Instrumentor"), + "sqlalchemy": ( + "opentelemetry.instrumentation.sqlalchemy", + "SQLAlchemyInstrumentor", + ), + # AWS Services + "boto": ("opentelemetry.instrumentation.boto", "BotoInstrumentor"), + "boto3sqs": ("opentelemetry.instrumentation.boto3sqs", "Boto3SQSInstrumentor"), + # Messaging & Caching + "celery": ("opentelemetry.instrumentation.celery", "CeleryInstrumentor"), + "redis": ("opentelemetry.instrumentation.redis", "RedisInstrumentor"), + "pymemcache": ( + "opentelemetry.instrumentation.pymemcache", + "PymemcacheInstrumentor", + ), + # Search + "elasticsearch": ( + "opentelemetry.instrumentation.elasticsearch", + "ElasticsearchInstrumentor", + ), + # RPC + "grpc": ("opentelemetry.instrumentation.grpc", "GrpcInstrumentorClient"), + # Templating + "jinja2": ("opentelemetry.instrumentation.jinja2", "Jinja2Instrumentor"), + # WSGI/ASGI + "wsgi": ("opentelemetry.instrumentation.wsgi", "OpenTelemetryMiddleware"), + "asgi": ("opentelemetry.instrumentation.asgi", "OpenTelemetryMiddleware"), + # Database API + "dbapi": ("opentelemetry.instrumentation.dbapi", "trace_integration"), + } + + # botocore (AWS SDK) - ALWAYS loaded for Lambda (like Node.js AwsInstrumentation) + try: + from opentelemetry.instrumentation.botocore import BotocoreInstrumentor + + BotocoreInstrumentor().instrument() + logger.debug("Loaded botocore instrumentation (always enabled)") + except ImportError: + logger.warning("botocore instrumentation not available") + except Exception as e: + logger.warning(f"Failed to load botocore instrumentation: {e}") + + # Load optional instrumentations based on active_instrumentations + for name, (module_path, class_name) in INSTRUMENTATIONS.items(): + # Skip botocore since it's always loaded above + if name == "botocore": + continue + + # Check if this instrumentation should be loaded + if name not in active_instrumentations: + continue + + try: + # Dynamically import the instrumentation module + module = importlib.import_module(module_path) + instrumentor_class = getattr(module, class_name) + + # Special handling for certain instrumentations + if name in ("wsgi", "asgi"): + # WSGI/ASGI are middleware, not instrumentors - skip auto-loading + logger.debug( + f"Skipping {name} instrumentation (middleware, not auto-instrumentable)" + ) + continue + elif name == "dbapi": + # dbapi is a function, not a class - skip auto-loading + logger.debug( + f"Skipping {name} instrumentation (requires manual integration)" + ) + continue + + # Instrument the library + instrumentor_class().instrument() + logger.debug(f"Loaded {name} instrumentation") + + except ImportError: + logger.debug( + f"{name} instrumentation not available (package not installed)" + ) + except AttributeError as e: + logger.warning(f"Failed to find {class_name} in {module_path}: {e}") + except Exception as e: + logger.warning(f"Failed to load {name} instrumentation: {e}") + + +def _configure_logger(): + log_level = os.environ.get(OTEL_LOG_LEVEL, "INFO").upper() + logging.basicConfig(level=log_level) + + +def _configure_service_name(): + """Set service name to Lambda function name if not already set""" + if not os.environ.get(OTEL_SERVICE_NAME): + function_name = os.environ.get("AWS_LAMBDA_FUNCTION_NAME") + if function_name: + # Check if OTEL_RESOURCE_ATTRIBUTES already has service.name + resource_attrs = os.environ.get(OTEL_RESOURCE_ATTRIBUTES, "") + if "service.name=" not in resource_attrs: + if resource_attrs: + os.environ[OTEL_RESOURCE_ATTRIBUTES] = ( + f"service.name={function_name},{resource_attrs}" + ) + else: + os.environ[OTEL_RESOURCE_ATTRIBUTES] = ( + f"service.name={function_name}" + ) + + +def _configure_propagators(): + """Set default propagators for Lambda if not configured. + + Includes X-Ray propagator for AWS service integration. + """ + if not os.environ.get(OTEL_PROPAGATORS): + # tracecontext: W3C standard propagation + # baggage: W3C baggage propagation + # xray: AWS X-Ray propagation for AWS service integration + os.environ[OTEL_PROPAGATORS] = "tracecontext,baggage,xray" + + +def _get_lambda_resource(): + """Create OpenTelemetry resource with Lambda-specific attributes. + + Returns: + Resource with Lambda function metadata or empty resource if detector unavailable. + """ + if AwsLambdaResourceDetector: + from opentelemetry.sdk.resources import get_aggregated_resources + + return get_aggregated_resources([AwsLambdaResourceDetector()]) + return Resource.create() + + +def _configure_tracer_provider(): + """Configure OpenTelemetry TracerProvider for Lambda. + + Sets up trace export with Lambda resource detection and configured exporters. + """ + provider = trace.get_tracer_provider() + is_proxy = isinstance(provider, trace.ProxyTracerProvider) + + if not is_proxy: + logger.debug("TracerProvider already configured.") + return + + logger.debug("Configuring TracerProvider for Lambda...") + + resource = _get_lambda_resource() + provider = TracerProvider(resource=resource) + + exporter_name = os.environ.get(OTEL_TRACES_EXPORTER, "otlp").lower().strip() + + # Handle "none" exporter - no tracing + if "none" in exporter_name: + logger.debug( + "Traces exporter set to 'none', skipping trace export configuration." + ) + trace.set_tracer_provider(provider) + return + + # Support multiple exporters + exporters = [] + for exp_name in exporter_name.split(","): + exp_name = exp_name.strip() + if exp_name == "otlp": + if OTLPSpanExporter: + exporters.append(OTLPSpanExporter()) + else: + logger.warning("OTLP Exporter not installed.") + elif exp_name == "console": + exporters.append(ConsoleSpanExporter()) + else: + logger.warning(f"Unknown exporter: {exp_name}") + + # Add span processors based on exporter type + for exporter in exporters: + if isinstance(exporter, ConsoleSpanExporter): + # Use SimpleSpanProcessor for console exporter (immediate export) + processor = SimpleSpanProcessor(exporter) + else: + # Use BatchSpanProcessor for other exporters + processor = BatchSpanProcessor(exporter) + provider.add_span_processor(processor) + + trace.set_tracer_provider(provider) + + +def _configure_meter_provider(): + """Configure OpenTelemetry MeterProvider for Lambda. + + Sets up metrics export with Lambda resource detection and configured exporters. + """ + provider = metrics.get_meter_provider() + is_proxy = isinstance(provider, metrics.ProxyMeterProvider) + + if not is_proxy: + logger.debug("MeterProvider already configured.") + return + + logger.debug("Configuring MeterProvider for Lambda...") + + resource = _get_lambda_resource() + + exporter_name = os.environ.get(OTEL_METRICS_EXPORTER, "otlp").lower().strip() + + # Handle "none" exporter - no metrics + if "none" in exporter_name: + logger.debug( + "Metrics exporter set to 'none', skipping metrics export configuration." + ) + return + + readers = [] + for exp_name in exporter_name.split(","): + exp_name = exp_name.strip() + if exp_name == "otlp": + if OTLPMetricExporter: + exporter = OTLPMetricExporter() + readers.append(PeriodicExportingMetricReader(exporter)) + else: + logger.warning("OTLP Metric Exporter not installed.") + elif exp_name == "console": + exporter = ConsoleMetricExporter() + readers.append(PeriodicExportingMetricReader(exporter)) + else: + logger.warning(f"Unknown metric exporter: {exp_name}") + + if readers: + provider = MeterProvider(resource=resource, metric_readers=readers) + metrics.set_meter_provider(provider) def modify_module_name(module_name): - """Returns a valid modified module to get imported""" + """Convert Lambda handler path format to Python module path. + + Converts "/" in handler path to "." for proper Python import. + Example: "handlers/main" -> "handlers.main" + """ return ".".join(module_name.split("/")) @@ -49,6 +408,18 @@ class HandlerError(Exception): pass +# Initialize Configuration +_configure_logger() +_configure_service_name() +_configure_propagators() +_configure_tracer_provider() +_configure_meter_provider() + +# Load instrumentations - botocore (AWS SDK) is always loaded +_load_instrumentations() + +# Instrument Lambda Handler - ALWAYS loaded (like Node.js AwsLambdaInstrumentation) +# This must be called after tracer provider configuration AwsLambdaInstrumentor().instrument() path = os.environ.get("ORIG_HANDLER") @@ -59,8 +430,8 @@ class HandlerError(Exception): try: (mod_name, handler_name) = path.rsplit(".", 1) except ValueError as e: - raise HandlerError("Bad path '{}' for ORIG_HANDLER: {}".format(path, str(e))) + raise HandlerError(f"Bad path '{path}' for ORIG_HANDLER: {e!s}") from e modified_mod_name = modify_module_name(mod_name) -handler_module = import_module(modified_mod_name) +handler_module = importlib.import_module(modified_mod_name) lambda_handler = getattr(handler_module, handler_name) diff --git a/python/src/otel/otel_sdk/requirements.txt b/python/src/otel/otel_sdk/requirements.txt deleted file mode 100644 index 35a5708f4e..0000000000 --- a/python/src/otel/otel_sdk/requirements.txt +++ /dev/null @@ -1,36 +0,0 @@ -opentelemetry-sdk==1.39.1 -opentelemetry-exporter-otlp-proto-http==1.39.1 -opentelemetry-distro==0.60b1 -opentelemetry-instrumentation==0.60b1 -opentelemetry-semantic-conventions==0.60b1 -opentelemetry-propagator-aws-xray==1.0.2 - -# Instrumentation dependencies -opentelemetry-instrumentation-aiohttp-client==0.60b1 -opentelemetry-util-http==0.60b1 -opentelemetry-instrumentation-asgi==0.60b1 -opentelemetry-instrumentation-asyncpg==0.60b1 -opentelemetry-instrumentation-boto==0.60b1 -opentelemetry-instrumentation-boto3sqs==0.60b1 -opentelemetry-instrumentation-celery==0.60b1 -opentelemetry-instrumentation-dbapi==0.60b1 -opentelemetry-instrumentation-django==0.60b1 -opentelemetry-instrumentation-elasticsearch==0.60b1 -opentelemetry-instrumentation-fastapi==0.60b1 -opentelemetry-instrumentation-falcon==0.60b1 -opentelemetry-instrumentation-flask==0.60b1 -opentelemetry-instrumentation-grpc==0.60b1 -opentelemetry-instrumentation-jinja2==0.60b1 -opentelemetry-instrumentation-mysql==0.60b1 -opentelemetry-instrumentation-psycopg2==0.60b1 -opentelemetry-instrumentation-pymemcache==0.60b1 -opentelemetry-instrumentation-pymongo==0.60b1 -opentelemetry-instrumentation-pymysql==0.60b1 -opentelemetry-instrumentation-pyramid==0.60b1 -opentelemetry-instrumentation-redis==0.60b1 -opentelemetry-instrumentation-requests==0.60b1 -opentelemetry-instrumentation-sqlalchemy==0.60b1 -opentelemetry-instrumentation-sqlite3==0.60b1 -opentelemetry-instrumentation-starlette==0.60b1 -opentelemetry-instrumentation-tornado==0.60b1 -opentelemetry-instrumentation-wsgi==0.60b1 diff --git a/python/src/otel/pyproject.toml b/python/src/otel/pyproject.toml new file mode 100644 index 0000000000..c8e228275c --- /dev/null +++ b/python/src/otel/pyproject.toml @@ -0,0 +1,82 @@ +[project] +name = "otel" +version = "0.1.0" +requires-python = ">=3.9" +dependencies = [ + "opentelemetry-sdk==1.39.0", + "opentelemetry-exporter-otlp-proto-http==1.39.0", + "opentelemetry-distro==0.60b0", + "opentelemetry-instrumentation==0.60b0", + "opentelemetry-semantic-conventions==0.60b0", + "opentelemetry-propagator-aws-xray==1.0.2", +] + +[project.optional-dependencies] +test = [ + "opentelemetry-test-utils==0.60b0", + "pytest" +] +dev = [ + "ruff>=0.8.0", +] +instrumentation = [ + "opentelemetry-instrumentation-aiohttp-client==0.60b0", + "opentelemetry-util-http==0.60b0", + "opentelemetry-instrumentation-asgi==0.60b0", + "opentelemetry-instrumentation-asyncpg==0.60b0", + "opentelemetry-instrumentation-aws-lambda==0.60b0", + "opentelemetry-instrumentation-boto==0.60b0", + "opentelemetry-instrumentation-botocore==0.60b0", + "opentelemetry-instrumentation-boto3sqs==0.60b0", + "opentelemetry-instrumentation-celery==0.60b0", + "opentelemetry-instrumentation-dbapi==0.60b0", + "opentelemetry-instrumentation-django==0.60b0", + "opentelemetry-instrumentation-elasticsearch==0.60b0", + "opentelemetry-instrumentation-fastapi==0.60b0", + "opentelemetry-instrumentation-falcon==0.60b0", + "opentelemetry-instrumentation-flask==0.60b0", + "opentelemetry-instrumentation-grpc==0.60b0", + "opentelemetry-instrumentation-jinja2==0.60b0", + "opentelemetry-instrumentation-mysql==0.60b0", + "opentelemetry-instrumentation-psycopg2==0.60b0", + "opentelemetry-instrumentation-pymemcache==0.60b0", + "opentelemetry-instrumentation-pymongo==0.60b0", + "opentelemetry-instrumentation-pymysql==0.60b0", + "opentelemetry-instrumentation-pyramid==0.60b0", + "opentelemetry-instrumentation-redis==0.60b0", + "opentelemetry-instrumentation-requests==0.60b0", + "opentelemetry-instrumentation-sqlalchemy==0.60b0", + "opentelemetry-instrumentation-sqlite3==0.60b0", + "opentelemetry-instrumentation-starlette==0.60b0", + "opentelemetry-instrumentation-tornado==0.60b0", + "opentelemetry-instrumentation-wsgi==0.60b0", +] + +[tool.ruff] +target-version = "py310" +line-length = 88 + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "UP", # pyupgrade + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "SIM", # flake8-simplify + "RUF", # ruff-specific rules +] +ignore = [ + "E501", # line too long (handled by formatter) +] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" + +[dependency-groups] +dev = [ + "ruff>=0.14.11", +] diff --git a/python/src/otel/tests/nodeps-requirements.txt b/python/src/otel/tests/nodeps-requirements.txt deleted file mode 100644 index 3ce1c84589..0000000000 --- a/python/src/otel/tests/nodeps-requirements.txt +++ /dev/null @@ -1 +0,0 @@ -opentelemetry-instrumentation-aws-lambda==0.60b1 diff --git a/python/src/otel/tests/requirements.txt b/python/src/otel/tests/requirements.txt deleted file mode 100644 index 5964f45b2e..0000000000 --- a/python/src/otel/tests/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -# Dependencies used in tests only -opentelemetry-test-utils==0.60b1 -opentelemetry-instrumentation == 0.60b1 -opentelemetry-semantic-conventions == 0.60b1 -opentelemetry-propagator-aws-xray == 1.0.2 diff --git a/python/src/otel/tests/test_otel.py b/python/src/otel/tests/test_otel.py index c59c99015a..b25b3f6798 100644 --- a/python/src/otel/tests/test_otel.py +++ b/python/src/otel/tests/test_otel.py @@ -24,6 +24,7 @@ from importlib import import_module, reload from shutil import which from unittest import mock + from opentelemetry import propagate from opentelemetry.environment_variables import OTEL_PROPAGATORS from opentelemetry.instrumentation.aws_lambda import ( @@ -46,9 +47,7 @@ AWS_LAMBDA_EXEC_WRAPPER = "AWS_LAMBDA_EXEC_WRAPPER" AWS_LAMBDA_FUNCTION_NAME = "AWS_LAMBDA_FUNCTION_NAME" -INIT_OTEL_SCRIPTS_DIR = os.path.join( - *(os.path.dirname(__file__), "..", "otel_sdk") -) +INIT_OTEL_SCRIPTS_DIR = os.path.join(*(os.path.dirname(__file__), "..", "otel_sdk")) TOX_PYTHON_DIRECTORY = os.path.dirname(os.path.dirname(which("python3"))) @@ -68,9 +67,7 @@ def __init__(self, aws_request_id, invoked_function_arn): MOCK_XRAY_PARENT_SPAN_ID = 0x3328B8445A6DBAD2 MOCK_XRAY_TRACE_CONTEXT_COMMON = f"Root={TRACE_ID_VERSION}-{MOCK_XRAY_TRACE_ID_STR[:TRACE_ID_FIRST_PART_LENGTH]}-{MOCK_XRAY_TRACE_ID_STR[TRACE_ID_FIRST_PART_LENGTH:]};Parent={MOCK_XRAY_PARENT_SPAN_ID:x}" MOCK_XRAY_TRACE_CONTEXT_SAMPLED = f"{MOCK_XRAY_TRACE_CONTEXT_COMMON};Sampled=1" -MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED = ( - f"{MOCK_XRAY_TRACE_CONTEXT_COMMON};Sampled=0" -) +MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED = f"{MOCK_XRAY_TRACE_CONTEXT_COMMON};Sampled=0" # See more: # https://www.w3.org/TR/trace-context/#examples-of-http-traceparent-headers @@ -221,7 +218,7 @@ def test_active_tracing(self): **os.environ, # Using Active tracing _X_AMZN_TRACE_ID: MOCK_XRAY_TRACE_CONTEXT_SAMPLED, - OTEL_PROPAGATORS: "xray-lambda" + OTEL_PROPAGATORS: "xray-lambda", }, ) test_env_patch.start() @@ -261,9 +258,7 @@ def test_active_tracing(self): # self.assertEqual(resource_atts[ResourceAttributes.FAAS_VERSION], os.environ["AWS_LAMBDA_FUNCTION_VERSION"]) parent_context = span.parent - self.assertEqual( - parent_context.trace_id, span.get_span_context().trace_id - ) + self.assertEqual(parent_context.trace_id, span.get_span_context().trace_id) self.assertEqual(parent_context.span_id, MOCK_XRAY_PARENT_SPAN_ID) self.assertTrue(parent_context.is_remote) @@ -303,9 +298,7 @@ def test_parent_context_from_lambda_event(self): self.assertEqual(span.get_span_context().trace_id, MOCK_W3C_TRACE_ID) parent_context = span.parent - self.assertEqual( - parent_context.trace_id, span.get_span_context().trace_id - ) + self.assertEqual(parent_context.trace_id, span.get_span_context().trace_id) self.assertEqual(parent_context.span_id, MOCK_W3C_PARENT_SPAN_ID) self.assertEqual(len(parent_context.trace_state), 3) self.assertEqual( diff --git a/python/src/otel/uv.lock b/python/src/otel/uv.lock new file mode 100644 index 0000000000..a50629877b --- /dev/null +++ b/python/src/otel/uv.lock @@ -0,0 +1,1183 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version < '3.10'", +] + +[[package]] +name = "asgiref" +version = "3.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/b9/4db2509eabd14b4a8c71d1b24c8d5734c52b8560a7b1e1a8b56c8d25568b/asgiref-3.11.0.tar.gz", hash = "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4", size = 37969, upload-time = "2025-11-19T15:32:20.106Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/be/317c2c55b8bbec407257d45f5c8d1b6867abc76d12043f2d3d58c538a4ea/asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d", size = 24096, upload-time = "2025-11-19T15:32:19.004Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "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 = "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 = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +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 = "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 = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.39.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-distro" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-sdk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/a8/23c0ad7ffdb4b1977d575e800746734585d096b26ff2288e070a5ff7dec2/opentelemetry_distro-0.60b0.tar.gz", hash = "sha256:dbcbe7a3af846df495691bcaa48383af80e72d0ab49ff6a31bb464c0b1e008fa", size = 2584, upload-time = "2025-12-03T13:21:57.2Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/b6/2ef1b0e2a81cf5b0cc48768ab6d3327257c83c1cc7cfcb4771db9b2f791f/opentelemetry_distro-0.60b0-py3-none-any.whl", hash = "sha256:579ae52d5a509a154f48fe8d06ce8cc7abcc9a684a8b906f6abbe8ad335722ee", size = 3343, upload-time = "2025-12-03T13:20:46.934Z" }, +] + +[[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-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-instrumentation" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/3c/bd53dbb42eff93d18e3047c7be11224aa9966ce98ac4cc5bfb860a32c95a/opentelemetry_instrumentation-0.60b0.tar.gz", hash = "sha256:4e9fec930f283a2677a2217754b40aaf9ef76edae40499c165bc7f1d15366a74", size = 31707, upload-time = "2025-12-03T13:22:00.352Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/7b/5b5b9f8cfe727a28553acf9cd287b1d7f706f5c0a00d6e482df55b169483/opentelemetry_instrumentation-0.60b0-py3-none-any.whl", hash = "sha256:aaafa1483543a402819f1bdfb06af721c87d60dd109501f9997332862a35c76a", size = 33096, upload-time = "2025-12-03T13:20:51.785Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-aiohttp-client" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/b1/97b2c06107b496a93f748c3d12ad439604bdccccdd2be05fe1ad63faa7df/opentelemetry_instrumentation_aiohttp_client-0.60b0.tar.gz", hash = "sha256:22ff1db43404bb67572628b45d99d284251621bfc80f772d05919e6233a88bf4", size = 15906, upload-time = "2025-12-03T13:22:02.415Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/f2/a53315a6278a25373de004fbf95a20dd5f20e1e4b98f5daf431367bb518c/opentelemetry_instrumentation_aiohttp_client-0.60b0-py3-none-any.whl", hash = "sha256:d1a0b2cf1696c52b3b3dc019c714d44e358efbad226cc70cb51f444d7ee36c93", size = 12697, upload-time = "2025-12-03T13:20:53.805Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-asgi" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/0a/715ea7044708d3c215385fb2a1c6ffe429aacb3cd23a348060aaeda52834/opentelemetry_instrumentation_asgi-0.60b0.tar.gz", hash = "sha256:928731218050089dca69f0fe980b8bfe109f384be8b89802d7337372ddb67b91", size = 26083, upload-time = "2025-12-03T13:22:05.672Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/8c/c6c59127fd996107243ca45669355665a7daff578ddafb86d6d2d3b01428/opentelemetry_instrumentation_asgi-0.60b0-py3-none-any.whl", hash = "sha256:9d76a541269452c718a0384478f3291feb650c5a3f29e578fdc6613ea3729cf3", size = 16907, upload-time = "2025-12-03T13:20:58.962Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-asyncpg" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/16/5fd94a8f160dd24e80399b6fda480be84dd8ee5488cd83c0aca1905714ad/opentelemetry_instrumentation_asyncpg-0.60b0.tar.gz", hash = "sha256:a1e128472ee9b071538117c5045ed2344798f2bf0fa769371ee426ef983c6fce", size = 8726, upload-time = "2025-12-03T13:22:07.767Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/27/325a5badb7021d073c18109330a9bf41ee0cd03264545569852991791b2a/opentelemetry_instrumentation_asyncpg-0.60b0-py3-none-any.whl", hash = "sha256:0c1bcc5b3bc46aa95b0771ef517ae4273711bcb9925e07c398f528aeb2845bd2", size = 10088, upload-time = "2025-12-03T13:21:02.795Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-aws-lambda" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-propagator-aws-xray" }, + { name = "opentelemetry-semantic-conventions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/4c/6de6146715820419737d72823829e8a577766cfac4f1d24b3f703d3efd81/opentelemetry_instrumentation_aws_lambda-0.60b0.tar.gz", hash = "sha256:4e5d4b3d731fa246a1f0c1583c9078e555af618fc129457aa1a61952c31cf469", size = 18146, upload-time = "2025-12-03T13:22:08.388Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/b2/94eca1b23be11a9b667e4bba783b7ffd121c4240e2301c0a9b6105a4ec91/opentelemetry_instrumentation_aws_lambda-0.60b0-py3-none-any.whl", hash = "sha256:a4196582595c53b680bc0bdd169d117875970011d25c0962d037a85c4faf14da", size = 12572, upload-time = "2025-12-03T13:21:03.754Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-boto" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/ea/0f41b5dfb59d734d1c9b0367aa7972c0ffb5f54b857bd6cc96fc76f69758/opentelemetry_instrumentation_boto-0.60b0.tar.gz", hash = "sha256:c71b88cf01522b20547a792544ead0cfb0aafbc38e81ff42d6eaeaaf9ec11a59", size = 9702, upload-time = "2025-12-03T13:22:09.203Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/3a/ac9334e1c0f78ccbd7ac34ab3b1d700525edafd3701fd2ea31cdc14b5212/opentelemetry_instrumentation_boto-0.60b0-py3-none-any.whl", hash = "sha256:aeb98fce83ca77a61711c13fdde3e9c0f0765a1afc862fc8167045a46c17f0d7", size = 10154, upload-time = "2025-12-03T13:21:04.664Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-boto3sqs" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/00/3fd3ccdd55851643a5ed4817c29fb5501eed2d0c8edf24db24d6da85e8d9/opentelemetry_instrumentation_boto3sqs-0.60b0.tar.gz", hash = "sha256:21a9d008224e056b87b082ae559234f180ba5600f40853b4ae03e82c3ac2e31d", size = 11712, upload-time = "2025-12-03T13:22:10.539Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/ee/5926583f96a4240f78eda6c5786d84d55940e145244a92b9792412f06696/opentelemetry_instrumentation_boto3sqs-0.60b0-py3-none-any.whl", hash = "sha256:b203d373ac9938a7c964d330dea5a5f50f570e2d890c49ec7ba02c30e4fc7bc6", size = 11675, upload-time = "2025-12-03T13:21:05.624Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-botocore" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-propagator-aws-xray" }, + { name = "opentelemetry-semantic-conventions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/68/21dbd6ede28e3037acbdccf4fdd6b4e35f69ae569639895f888b90146ebb/opentelemetry_instrumentation_botocore-0.60b0.tar.gz", hash = "sha256:320e6f5c20eb320f0ac34e41eca41eb9267a640ff454e82ddecc35b22a87bd96", size = 120936, upload-time = "2025-12-03T13:22:11.366Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/13/027202aea354e4929529cf0bfb5ec9ff2e656a2b6791252b5db65d9a1d69/opentelemetry_instrumentation_botocore-0.60b0-py3-none-any.whl", hash = "sha256:bb647c3668d749b7a783b1fbf9b6c08f39de321ea02b40469dd039ef9c705de4", size = 38134, upload-time = "2025-12-03T13:21:06.613Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-celery" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/04/ece5dfd3e9ee18832b138943f69affb73f982ef51d5f466ff0202a655945/opentelemetry_instrumentation_celery-0.60b0.tar.gz", hash = "sha256:e19e23141c0c8529b2aa75f7d6ec50252828fc7d18329d711bf4df2ca94213bb", size = 14768, upload-time = "2025-12-03T13:22:13.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/e4/4d310504cdb0a362a9e2813248c219ceebd9e94ce95906c7cf42255e9655/opentelemetry_instrumentation_celery-0.60b0-py3-none-any.whl", hash = "sha256:4909203ee318c9801b7002d92afbc61c593878bd63033dd399b859a9c4310b1d", size = 13804, upload-time = "2025-12-03T13:21:09.209Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-dbapi" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/7f/b4c1fbce01b29daad5ef1396427c9cd3c7a55ee68e75f8c11089c7e2533d/opentelemetry_instrumentation_dbapi-0.60b0.tar.gz", hash = "sha256:2b7eb38e46890cebe5bc1a1c03d2ab07fc159b0b7b91342941ee33dd73876d84", size = 16311, upload-time = "2025-12-03T13:22:15.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/0a/65e100c6d803de59a9113a993dcd371a4027453ba15ce4dabdb0343ca154/opentelemetry_instrumentation_dbapi-0.60b0-py3-none-any.whl", hash = "sha256:429d8ca34a44a4296b9b09a1bd373fff350998d200525c6e79883c3328559b03", size = 13966, upload-time = "2025-12-03T13:21:12.435Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-django" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-instrumentation-wsgi" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/d2/8ddd9a5c61cd5048d422be8d22fac40f603aa82f0babf9f7c40db871080c/opentelemetry_instrumentation_django-0.60b0.tar.gz", hash = "sha256:461e6fca27936ba97eec26da38bb5f19310783370478c7ca3a3e40faaceac9cc", size = 26596, upload-time = "2025-12-03T13:22:16.069Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/d6/28684547bf6c699582e998a172ba8bb08405cf6706729b0d6a16042e998f/opentelemetry_instrumentation_django-0.60b0-py3-none-any.whl", hash = "sha256:95495649c8c34ce9217c6873cdd10fc4fcaa67c25f8329adc54f5b286999e40b", size = 21169, upload-time = "2025-12-03T13:21:13.475Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-elasticsearch" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/e0/1eb0161cc87a14c75555812b3fba7324ceb6b8190e27de1022c4c1f088e9/opentelemetry_instrumentation_elasticsearch-0.60b0.tar.gz", hash = "sha256:c396c866acd674fcb0e853583900286574f82566d5af1f80b92330d4f2d1106c", size = 14836, upload-time = "2025-12-03T13:22:17.044Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/34/84e363f39e81d576ad20a55b5b2a9d6de7cbd07e3a48e762bb5b8766674d/opentelemetry_instrumentation_elasticsearch-0.60b0-py3-none-any.whl", hash = "sha256:5aacd5aa55928f2ca4fe6eea8ec4b7eac5b9b0fb91ff75728a89ce47d67ebd9c", size = 12444, upload-time = "2025-12-03T13:21:14.417Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-falcon" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-instrumentation-wsgi" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/da/345031b9cb8f9ae6e3d26f7e555bc151759d4fe1be27fbaee9b8fde02d0a/opentelemetry_instrumentation_falcon-0.60b0.tar.gz", hash = "sha256:2fa7fa3f83a3bd70786e27c85b939a7054cf5df8a318e366b9e78ce9522fcc72", size = 17290, upload-time = "2025-12-03T13:22:17.766Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/52/285182cef8c3f3bc6c1f6e59f9faeddf4acbba42162e1ea15a545840cf37/opentelemetry_instrumentation_falcon-0.60b0-py3-none-any.whl", hash = "sha256:fc8fd6b59a94dcb834b25d6e2783a79e49f6e6282eeeb496b846a2c32cf2b465", size = 14245, upload-time = "2025-12-03T13:21:15.416Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-fastapi" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-instrumentation-asgi" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/51/a021a7c929b5103fcb6bfdfa5a99abcaeb3b505faf9e3ee3ec14612c1ef9/opentelemetry_instrumentation_fastapi-0.60b0.tar.gz", hash = "sha256:5d34d67eb634a08bfe9e530680d6177521cd9da79285144e6d5a8f42683ed1b3", size = 24960, upload-time = "2025-12-03T13:22:18.468Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/5a/e238c108eb65a726d75184439377a87d532050036b54e718e4c789b26d1a/opentelemetry_instrumentation_fastapi-0.60b0-py3-none-any.whl", hash = "sha256:415c6602db01ee339276ea4cabe3e80177c9e955631c087f2ef60a75e31bfaee", size = 13478, upload-time = "2025-12-03T13:21:16.804Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-flask" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-instrumentation-wsgi" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/cc/e0758c23d66fd49956169cb24b5b06130373da2ce8d49945abce82003518/opentelemetry_instrumentation_flask-0.60b0.tar.gz", hash = "sha256:560f08598ef40cdcf7ca05bfb2e3ea74fab076e676f4c18bb36bb379bf5c4a1b", size = 20336, upload-time = "2025-12-03T13:22:19.162Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/b5/387ce11f59e5ce65b890adc3f9c457877143b8a6d107a3a0b305397933a1/opentelemetry_instrumentation_flask-0.60b0-py3-none-any.whl", hash = "sha256:106e5774f79ac9b86dd0d949c1b8f46c807a8af16184301e10d24fc94e680d04", size = 15189, upload-time = "2025-12-03T13:21:18.672Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-grpc" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/41/2bc29e107c7e5bd31e333416443aa3d8bf9914361fb21e0f1abc56143529/opentelemetry_instrumentation_grpc-0.60b0.tar.gz", hash = "sha256:b352998985833e801ad8cea260c18df59dba1312c3704cc940ff35ba2e004d66", size = 31422, upload-time = "2025-12-03T13:22:19.839Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/cc/a4aecc066f0ada8469e31e03dd27bd55079eeb4a952d26ff79846df77d38/opentelemetry_instrumentation_grpc-0.60b0-py3-none-any.whl", hash = "sha256:bcd2465fcecf2dd9d4e770a9f575e0ca7ccc412ad3a624e1552e524475523e1e", size = 27234, upload-time = "2025-12-03T13:21:19.583Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-jinja2" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/90/6dda25b98e984f5f24eef7404f54574c864383d501c4ac98bdf777740629/opentelemetry_instrumentation_jinja2-0.60b0.tar.gz", hash = "sha256:4b82c6087abe218ef3803a95d9a4bcce068840344e0a83d1ac95cebab3486b34", size = 8455, upload-time = "2025-12-03T13:22:21.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/53/e2d5a03167c0213316f322b2d2d8f4fe573b872bf747b7fd6eb4d488bea7/opentelemetry_instrumentation_jinja2-0.60b0-py3-none-any.whl", hash = "sha256:3ecd3e81aedab30257a5edcce8e2b07dbc0e101478435306687a01ee146aa7c0", size = 9425, upload-time = "2025-12-03T13:21:21.801Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-mysql" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-instrumentation-dbapi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/cc/ea07debd43cd293faeae76b9f6fdf56b6e558c16b816fb6184637151d555/opentelemetry_instrumentation_mysql-0.60b0.tar.gz", hash = "sha256:4ae64914ca27d6fb373be96c3176154b3faf4c3b1ec999bddaaad2f17f3417a4", size = 9912, upload-time = "2025-12-03T13:22:23.212Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/72/7c18d9fd752c277e18bc3f332c5d2b846f7b4fbdc99e0203dde49b042691/opentelemetry_instrumentation_mysql-0.60b0-py3-none-any.whl", hash = "sha256:440413bb697148bbce991b150eade3538b9beb078a2d77badb23da1e463bab06", size = 10642, upload-time = "2025-12-03T13:21:25.788Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-psycopg2" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-instrumentation-dbapi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/68/5ae8a3b9a28c2fdf8d3d050e451ddb2612ca963679b08a2959f01f6dda4b/opentelemetry_instrumentation_psycopg2-0.60b0.tar.gz", hash = "sha256:59e527fd97739440380634ffcf9431aa7f2965d939d8d5829790886e2b54ede9", size = 11266, upload-time = "2025-12-03T13:22:26.025Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/66b5a41a2b0d1d07cc9b0fbd80f8b5c66b46a4d4731743505891da8b3cbe/opentelemetry_instrumentation_psycopg2-0.60b0-py3-none-any.whl", hash = "sha256:ea136a32babd559aa717c04dddf6aa78aa94b816fb4e10dfe06751727ef306d4", size = 11284, upload-time = "2025-12-03T13:21:31.23Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-pymemcache" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/c1/e9f397aeeda1c1bc8bf36a13d8ea28afcdc8cd2c51f354c991a27bf5d667/opentelemetry_instrumentation_pymemcache-0.60b0.tar.gz", hash = "sha256:545521e7738db04bcb04c68812dd7e8a602d21779cebfc7599fbd6df069bf7d6", size = 10611, upload-time = "2025-12-03T13:22:26.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/4b/509dfef58d1475e37d582e214a82224114234aa704d2afeebeb80be3418e/opentelemetry_instrumentation_pymemcache-0.60b0-py3-none-any.whl", hash = "sha256:c8fc127337588204c7bed8bdbba23896a810785874ce6d3cdf56ad1b80ffb4de", size = 9684, upload-time = "2025-12-03T13:21:32.217Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-pymongo" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ea/62/5ed5ad75ce6ef83293a5d983405964231baa5f9bbb2a13e7694077cbe5cb/opentelemetry_instrumentation_pymongo-0.60b0.tar.gz", hash = "sha256:aa17874017547c08bd2d9aa68b33d442f7112746ceb8709c58e2744709ee7254", size = 10335, upload-time = "2025-12-03T13:22:27.28Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/1d/2f80457511a4eadecdceb16e7fbffc26d5736149c893a5e4c55092db750b/opentelemetry_instrumentation_pymongo-0.60b0-py3-none-any.whl", hash = "sha256:ad47c9710c1258d7c1e83bdeef58b2fcaaca870e0676c92e3b171fa41cbb962d", size = 11420, upload-time = "2025-12-03T13:21:33.153Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-pymysql" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-instrumentation-dbapi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/5d/04da113be353bfb2202166c67b41a1d9489b8a8b6f204f61e8db0bf8a423/opentelemetry_instrumentation_pymysql-0.60b0.tar.gz", hash = "sha256:47717c2b6985fd35df839f5dec6fbae2eea83146ad4c6f15b9b7a605c93e802b", size = 9709, upload-time = "2025-12-03T13:22:28.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/76/5da8106f74853865c951b9034bd59d60aa1919e8faac3da9564370c037eb/opentelemetry_instrumentation_pymysql-0.60b0-py3-none-any.whl", hash = "sha256:155d76c6a2350fde8963469a138771564da5ddcba61db1616b3f2c23185c2f57", size = 10519, upload-time = "2025-12-03T13:21:34.976Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-pyramid" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-instrumentation-wsgi" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/b6/17e077226b86057e300efaf730a2b8b124bd2aa396dd88a2b1e2cf335ffe/opentelemetry_instrumentation_pyramid-0.60b0.tar.gz", hash = "sha256:4a9b7da2dd5c5911020e4b94d4c28cab8c10b4442ee49b888a38fba3e68683a9", size = 14993, upload-time = "2025-12-03T13:22:29.18Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/6c/32f63f57edc52049f3fd0c0e1cac63ace7c35e4c7d685610673f6f540060/opentelemetry_instrumentation_pyramid-0.60b0-py3-none-any.whl", hash = "sha256:05801bedfdbc0bdf01780825a4017158452022f329001e1118663c94d55c81b5", size = 13950, upload-time = "2025-12-03T13:21:35.998Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-redis" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/54/e08c60b3cc7aadcdd91c6cf83bb69b9b6ba88b6a1fb75e4893fbd2571211/opentelemetry_instrumentation_redis-0.60b0.tar.gz", hash = "sha256:e97121942d9cf8705e310fee3f1413d74356bf0426b96bf9508385e63fb8ab85", size = 14775, upload-time = "2025-12-03T13:22:30.205Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/6d/0ae60b2e6989bd35b8a2dc9e3a8842ecbd7c7e37639d6e69684493911736/opentelemetry_instrumentation_redis-0.60b0-py3-none-any.whl", hash = "sha256:f22aeea7de815228fede1a1bc139bc1634f45af4878c2e9a5fc159913d031d6d", size = 15503, upload-time = "2025-12-03T13:21:36.972Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-requests" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/0f/94c6181e95c867f559715887c418170a9eadd92ea6090122d464e375ff56/opentelemetry_instrumentation_requests-0.60b0.tar.gz", hash = "sha256:5079ed8df96d01dab915a0766cd28a49be7c33439ce43d6d39843ed6dee3204f", size = 16173, upload-time = "2025-12-03T13:22:31.458Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/e1/2f13b41c5679243ba8eae651170c4ce2f532349877819566ae4a89a2b47f/opentelemetry_instrumentation_requests-0.60b0-py3-none-any.whl", hash = "sha256:e9957f3a650ae55502fa227b29ff985b37d63e41c85e6e1555d48039f092ea83", size = 13122, upload-time = "2025-12-03T13:21:38.983Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-sqlalchemy" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/52/6a6e8ef8d0430487f1c65fdb81f2001d0b3b68de72f7437b54ff2330de44/opentelemetry_instrumentation_sqlalchemy-0.60b0.tar.gz", hash = "sha256:df9fdc6f48709138343971cef95a055f52d0262331115231bd5d678c1a452891", size = 15318, upload-time = "2025-12-03T13:22:32.135Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/08/943c1c650e7826b0462f0a2528d37b9cf717957955df6259b9c25dfcaa51/opentelemetry_instrumentation_sqlalchemy-0.60b0-py3-none-any.whl", hash = "sha256:ff66ac39364bfb38fe127f823b36693c6f2a44f929926863640ac1575583a73f", size = 14528, upload-time = "2025-12-03T13:21:40.344Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-sqlite3" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-instrumentation-dbapi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/4d/c60355912219e7a4aaf1b012bf3145ac02aa1f568e61f1c6e1dff2cb6d63/opentelemetry_instrumentation_sqlite3-0.60b0.tar.gz", hash = "sha256:0449ddeb91889486fca15a3e8b44a319094ba532d02aa578b0ce96a89825ae8f", size = 7921, upload-time = "2025-12-03T13:22:33.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/0d/047d34b5772ee8bfbb1993b60a35bf111e3714bcd8dea5d53210c5af4041/opentelemetry_instrumentation_sqlite3-0.60b0-py3-none-any.whl", hash = "sha256:524b93e88cff83ce97bfba9a9e897924356a6850fe64177c68d5f8d21212dfc2", size = 9337, upload-time = "2025-12-03T13:21:41.319Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-starlette" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-instrumentation-asgi" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/8f/8113a6cfcf928c1175c7c5ae0266ea2a9aa9814aca304d5555202a137a6c/opentelemetry_instrumentation_starlette-0.60b0.tar.gz", hash = "sha256:f446c1a928cbc46178a90a351c6d6d78629777760ac132c2b4342d1c5aebe1f3", size = 14643, upload-time = "2025-12-03T13:22:33.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/8e/3b3cb695b9d6e53f996cedb6d2873391baf452c784573791f9fcc02edf41/opentelemetry_instrumentation_starlette-0.60b0-py3-none-any.whl", hash = "sha256:efdbcf71a44e4c4d2aa506f891c98e966e09c507fa4a5a33f2e45b5424b55f36", size = 11764, upload-time = "2025-12-03T13:21:42.254Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-tornado" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/f5/6d866ffe267fa0c65eaeecb82b34052dfe047cd12470cd76e3c47c9e6dcb/opentelemetry_instrumentation_tornado-0.60b0.tar.gz", hash = "sha256:1edb2c50f975d527ad4e374bf17fcaad9385adca65820645fe8bc17daddfe14c", size = 18407, upload-time = "2025-12-03T13:22:36.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/64/186aae059e5bc89680186d5d91a5631ce9f84db287e2b5ff4a957462d694/opentelemetry_instrumentation_tornado-0.60b0-py3-none-any.whl", hash = "sha256:72dbd71ffcf3b5542943c85f2b7886b667b056979f4b088329a3db811b3e124b", size = 15567, upload-time = "2025-12-03T13:21:45.856Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-wsgi" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/ad/ae04e35f3b96d9c20d5d3df94a4c296eabf7a54d35d6c831179471128270/opentelemetry_instrumentation_wsgi-0.60b0.tar.gz", hash = "sha256:5815195b1b9890f55c4baafec94ff98591579a7d9b16256064adea8ee5784651", size = 19104, upload-time = "2025-12-03T13:22:38.733Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/0e/1ed4d3cdce7b2e00a24f79933b3472e642d4db98aaccc09769be5cbe5296/opentelemetry_instrumentation_wsgi-0.60b0-py3-none-any.whl", hash = "sha256:0ff80614c1e73f7e94a5860c7e6222a51195eebab3dc5f50d89013db3d5d2f13", size = 14553, upload-time = "2025-12-03T13:21:50.491Z" }, +] + +[[package]] +name = "opentelemetry-propagator-aws-xray" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/31/40004e9e55b1e5694ef3a7526f0b7637df44196fc68a8b7d248a3684680f/opentelemetry_propagator_aws_xray-1.0.2.tar.gz", hash = "sha256:6b2cee5479d2ef0172307b66ed2ed151f598a0fd29b3c01133ac87ca06326260", size = 10994, upload-time = "2024-08-05T17:45:57.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/89/849a0847871fd9745315896ad9e23d6479db84d90b8b36c4c26dc46e92b8/opentelemetry_propagator_aws_xray-1.0.2-py3-none-any.whl", hash = "sha256:1c99181ee228e99bddb638a0c911a297fa21f1c3a0af951f841e79919b5f1934", size = 10856, upload-time = "2024-08-05T17:45:56.492Z" }, +] + +[[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 = "opentelemetry-test-utils" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/08/56eabb1b5aedeeeb6314a655e7535647ce4ef20af7123c55d3ddf55825b9/opentelemetry_test_utils-0.60b0.tar.gz", hash = "sha256:c4091ab80806aefb598a9b8d60f0ab4cec66b0e741f95bc136ef12e14b358cbc", size = 8709, upload-time = "2025-12-03T13:20:13.136Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/6f/6b8dc752acae918b72f4a919bed0258f5e9d2da3780394fff8b92970b07f/opentelemetry_test_utils-0.60b0-py3-none-any.whl", hash = "sha256:0455d7f3593233be5ccbaf75860a7231a21d528e1c1f6d68e8c2a0a57337c652", size = 15251, upload-time = "2025-12-03T13:19:55.083Z" }, +] + +[[package]] +name = "opentelemetry-util-http" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/0d/786a713445cf338131fef3a84fab1378e4b2ef3c3ea348eeb0c915eb804a/opentelemetry_util_http-0.60b0.tar.gz", hash = "sha256:e42b7bb49bba43b6f34390327d97e5016eb1c47949ceaf37c4795472a4e3a82d", size = 10576, upload-time = "2025-12-03T13:22:41.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/5d/a448862f6d10c95685ed0e703596b6bd1784074e7ad90bffdc550abb7b68/opentelemetry_util_http-0.60b0-py3-none-any.whl", hash = "sha256:4f366f1a48adb74ffa6f80aee26f96882e767e01b03cd1cfb948b6e1020341fe", size = 8742, upload-time = "2025-12-03T13:21:54.553Z" }, +] + +[[package]] +name = "otel" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "opentelemetry-distro" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-propagator-aws-xray" }, + { name = "opentelemetry-sdk" }, + { name = "opentelemetry-semantic-conventions" }, +] + +[package.optional-dependencies] +dev = [ + { name = "ruff" }, +] +instrumentation = [ + { name = "opentelemetry-instrumentation-aiohttp-client" }, + { name = "opentelemetry-instrumentation-asgi" }, + { name = "opentelemetry-instrumentation-asyncpg" }, + { name = "opentelemetry-instrumentation-aws-lambda" }, + { name = "opentelemetry-instrumentation-boto" }, + { name = "opentelemetry-instrumentation-boto3sqs" }, + { name = "opentelemetry-instrumentation-botocore" }, + { name = "opentelemetry-instrumentation-celery" }, + { name = "opentelemetry-instrumentation-dbapi" }, + { name = "opentelemetry-instrumentation-django" }, + { name = "opentelemetry-instrumentation-elasticsearch" }, + { name = "opentelemetry-instrumentation-falcon" }, + { name = "opentelemetry-instrumentation-fastapi" }, + { name = "opentelemetry-instrumentation-flask" }, + { name = "opentelemetry-instrumentation-grpc" }, + { name = "opentelemetry-instrumentation-jinja2" }, + { name = "opentelemetry-instrumentation-mysql" }, + { name = "opentelemetry-instrumentation-psycopg2" }, + { name = "opentelemetry-instrumentation-pymemcache" }, + { name = "opentelemetry-instrumentation-pymongo" }, + { name = "opentelemetry-instrumentation-pymysql" }, + { name = "opentelemetry-instrumentation-pyramid" }, + { name = "opentelemetry-instrumentation-redis" }, + { name = "opentelemetry-instrumentation-requests" }, + { name = "opentelemetry-instrumentation-sqlalchemy" }, + { name = "opentelemetry-instrumentation-sqlite3" }, + { name = "opentelemetry-instrumentation-starlette" }, + { name = "opentelemetry-instrumentation-tornado" }, + { name = "opentelemetry-instrumentation-wsgi" }, + { name = "opentelemetry-util-http" }, +] +test = [ + { name = "opentelemetry-test-utils" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] + +[package.dev-dependencies] +dev = [ + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "opentelemetry-distro", specifier = "==0.60b0" }, + { name = "opentelemetry-exporter-otlp-proto-http", specifier = "==1.39.0" }, + { name = "opentelemetry-instrumentation", specifier = "==0.60b0" }, + { name = "opentelemetry-instrumentation-aiohttp-client", marker = "extra == 'instrumentation'", specifier = "==0.60b0" }, + { name = "opentelemetry-instrumentation-asgi", marker = "extra == 'instrumentation'", specifier = "==0.60b0" }, + { name = "opentelemetry-instrumentation-asyncpg", marker = "extra == 'instrumentation'", specifier = "==0.60b0" }, + { name = "opentelemetry-instrumentation-aws-lambda", marker = "extra == 'instrumentation'", specifier = "==0.60b0" }, + { name = "opentelemetry-instrumentation-boto", marker = "extra == 'instrumentation'", specifier = "==0.60b0" }, + { name = "opentelemetry-instrumentation-boto3sqs", marker = "extra == 'instrumentation'", specifier = "==0.60b0" }, + { name = "opentelemetry-instrumentation-botocore", marker = "extra == 'instrumentation'", specifier = "==0.60b0" }, + { name = "opentelemetry-instrumentation-celery", marker = "extra == 'instrumentation'", specifier = "==0.60b0" }, + { name = "opentelemetry-instrumentation-dbapi", marker = "extra == 'instrumentation'", specifier = "==0.60b0" }, + { name = "opentelemetry-instrumentation-django", marker = "extra == 'instrumentation'", specifier = "==0.60b0" }, + { name = "opentelemetry-instrumentation-elasticsearch", marker = "extra == 'instrumentation'", specifier = "==0.60b0" }, + { name = "opentelemetry-instrumentation-falcon", marker = "extra == 'instrumentation'", specifier = "==0.60b0" }, + { name = "opentelemetry-instrumentation-fastapi", marker = "extra == 'instrumentation'", specifier = "==0.60b0" }, + { name = "opentelemetry-instrumentation-flask", marker = "extra == 'instrumentation'", specifier = "==0.60b0" }, + { name = "opentelemetry-instrumentation-grpc", marker = "extra == 'instrumentation'", specifier = "==0.60b0" }, + { name = "opentelemetry-instrumentation-jinja2", marker = "extra == 'instrumentation'", specifier = "==0.60b0" }, + { name = "opentelemetry-instrumentation-mysql", marker = "extra == 'instrumentation'", specifier = "==0.60b0" }, + { name = "opentelemetry-instrumentation-psycopg2", marker = "extra == 'instrumentation'", specifier = "==0.60b0" }, + { name = "opentelemetry-instrumentation-pymemcache", marker = "extra == 'instrumentation'", specifier = "==0.60b0" }, + { name = "opentelemetry-instrumentation-pymongo", marker = "extra == 'instrumentation'", specifier = "==0.60b0" }, + { name = "opentelemetry-instrumentation-pymysql", marker = "extra == 'instrumentation'", specifier = "==0.60b0" }, + { name = "opentelemetry-instrumentation-pyramid", marker = "extra == 'instrumentation'", specifier = "==0.60b0" }, + { name = "opentelemetry-instrumentation-redis", marker = "extra == 'instrumentation'", specifier = "==0.60b0" }, + { name = "opentelemetry-instrumentation-requests", marker = "extra == 'instrumentation'", specifier = "==0.60b0" }, + { name = "opentelemetry-instrumentation-sqlalchemy", marker = "extra == 'instrumentation'", specifier = "==0.60b0" }, + { name = "opentelemetry-instrumentation-sqlite3", marker = "extra == 'instrumentation'", specifier = "==0.60b0" }, + { name = "opentelemetry-instrumentation-starlette", marker = "extra == 'instrumentation'", specifier = "==0.60b0" }, + { name = "opentelemetry-instrumentation-tornado", marker = "extra == 'instrumentation'", specifier = "==0.60b0" }, + { name = "opentelemetry-instrumentation-wsgi", marker = "extra == 'instrumentation'", specifier = "==0.60b0" }, + { name = "opentelemetry-propagator-aws-xray", specifier = "==1.0.2" }, + { name = "opentelemetry-sdk", specifier = "==1.39.0" }, + { name = "opentelemetry-semantic-conventions", specifier = "==0.60b0" }, + { name = "opentelemetry-test-utils", marker = "extra == 'test'", specifier = "==0.60b0" }, + { name = "opentelemetry-util-http", marker = "extra == 'instrumentation'", specifier = "==0.60b0" }, + { name = "pytest", marker = "extra == 'test'" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.8.0" }, +] +provides-extras = ["test", "dev", "instrumentation"] + +[package.metadata.requires-dev] +dev = [{ name = "ruff", specifier = ">=0.14.11" }] + +[[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 = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/44/e49ecff446afeec9d1a66d6bbf9adc21e3c7cea7803a920ca3773379d4f6/protobuf-6.33.2.tar.gz", hash = "sha256:56dc370c91fbb8ac85bc13582c9e373569668a290aa2e66a590c2a0d35ddb9e4", size = 444296, upload-time = "2025-12-06T00:17:53.311Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/91/1e3a34881a88697a7354ffd177e8746e97a722e5e8db101544b47e84afb1/protobuf-6.33.2-cp310-abi3-win32.whl", hash = "sha256:87eb388bd2d0f78febd8f4c8779c79247b26a5befad525008e49a6955787ff3d", size = 425603, upload-time = "2025-12-06T00:17:41.114Z" }, + { url = "https://files.pythonhosted.org/packages/64/20/4d50191997e917ae13ad0a235c8b42d8c1ab9c3e6fd455ca16d416944355/protobuf-6.33.2-cp310-abi3-win_amd64.whl", hash = "sha256:fc2a0e8b05b180e5fc0dd1559fe8ebdae21a27e81ac77728fb6c42b12c7419b4", size = 436930, upload-time = "2025-12-06T00:17:43.278Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ca/7e485da88ba45c920fb3f50ae78de29ab925d9e54ef0de678306abfbb497/protobuf-6.33.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d9b19771ca75935b3a4422957bc518b0cecb978b31d1dd12037b088f6bcc0e43", size = 427621, upload-time = "2025-12-06T00:17:44.445Z" }, + { url = "https://files.pythonhosted.org/packages/7d/4f/f743761e41d3b2b2566748eb76bbff2b43e14d5fcab694f494a16458b05f/protobuf-6.33.2-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:b5d3b5625192214066d99b2b605f5783483575656784de223f00a8d00754fc0e", size = 324460, upload-time = "2025-12-06T00:17:45.678Z" }, + { url = "https://files.pythonhosted.org/packages/b1/fa/26468d00a92824020f6f2090d827078c09c9c587e34cbfd2d0c7911221f8/protobuf-6.33.2-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8cd7640aee0b7828b6d03ae518b5b4806fdfc1afe8de82f79c3454f8aef29872", size = 339168, upload-time = "2025-12-06T00:17:46.813Z" }, + { url = "https://files.pythonhosted.org/packages/56/13/333b8f421738f149d4fe5e49553bc2a2ab75235486259f689b4b91f96cec/protobuf-6.33.2-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:1f8017c48c07ec5859106533b682260ba3d7c5567b1ca1f24297ce03384d1b4f", size = 323270, upload-time = "2025-12-06T00:17:48.253Z" }, + { url = "https://files.pythonhosted.org/packages/87/85/5c1115e68fd34e8ada6fa75974b4c778a298a3c7170575b49efc1eb99dd2/protobuf-6.33.2-cp39-cp39-win32.whl", hash = "sha256:7109dcc38a680d033ffb8bf896727423528db9163be1b6a02d6a49606dcadbfe", size = 425692, upload-time = "2025-12-06T00:17:49.62Z" }, + { url = "https://files.pythonhosted.org/packages/c5/74/18d9de7fd3c41a8b4808d6268515b320abae003423da1b1319f71bdf0779/protobuf-6.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:2981c58f582f44b6b13173e12bb8656711189c2a70250845f264b877f00b1913", size = 436932, upload-time = "2025-12-06T00:17:51.098Z" }, + { url = "https://files.pythonhosted.org/packages/0e/15/4f02896cc3df04fc465010a4c6a0cd89810f54617a32a70ef531ed75d61c/protobuf-6.33.2-py3-none-any.whl", hash = "sha256:7636aad9bb01768870266de5dc009de2d1b936771b38a793f73cbbf279c91c5c", size = 170501, upload-time = "2025-12-06T00:17:52.211Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +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 = "exceptiongroup", marker = "python_full_version < '3.10'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pluggy", marker = "python_full_version < '3.10'" }, + { name = "pygments", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +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 = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "pluggy", marker = "python_full_version >= '3.10'" }, + { name = "pygments", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +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 = "ruff" +version = "0.14.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/77/9a7fe084d268f8855d493e5031ea03fa0af8cc05887f638bf1c4e3363eb8/ruff-0.14.11.tar.gz", hash = "sha256:f6dc463bfa5c07a59b1ff2c3b9767373e541346ea105503b4c0369c520a66958", size = 5993417, upload-time = "2026-01-08T19:11:58.322Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/a6/a4c40a5aaa7e331f245d2dc1ac8ece306681f52b636b40ef87c88b9f7afd/ruff-0.14.11-py3-none-linux_armv6l.whl", hash = "sha256:f6ff2d95cbd335841a7217bdfd9c1d2e44eac2c584197ab1385579d55ff8830e", size = 12951208, upload-time = "2026-01-08T19:12:09.218Z" }, + { url = "https://files.pythonhosted.org/packages/5c/5c/360a35cb7204b328b685d3129c08aca24765ff92b5a7efedbdd6c150d555/ruff-0.14.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f6eb5c1c8033680f4172ea9c8d3706c156223010b8b97b05e82c59bdc774ee6", size = 13330075, upload-time = "2026-01-08T19:12:02.549Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9e/0cc2f1be7a7d33cae541824cf3f95b4ff40d03557b575912b5b70273c9ec/ruff-0.14.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2fc34cc896f90080fca01259f96c566f74069a04b25b6205d55379d12a6855e", size = 12257809, upload-time = "2026-01-08T19:12:00.366Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e5/5faab97c15bb75228d9f74637e775d26ac703cc2b4898564c01ab3637c02/ruff-0.14.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53386375001773ae812b43205d6064dae49ff0968774e6befe16a994fc233caa", size = 12678447, upload-time = "2026-01-08T19:12:13.899Z" }, + { url = "https://files.pythonhosted.org/packages/1b/33/e9767f60a2bef779fb5855cab0af76c488e0ce90f7bb7b8a45c8a2ba4178/ruff-0.14.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a697737dce1ca97a0a55b5ff0434ee7205943d4874d638fe3ae66166ff46edbe", size = 12758560, upload-time = "2026-01-08T19:11:42.55Z" }, + { url = "https://files.pythonhosted.org/packages/eb/84/4c6cf627a21462bb5102f7be2a320b084228ff26e105510cd2255ea868e5/ruff-0.14.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6845ca1da8ab81ab1dce755a32ad13f1db72e7fba27c486d5d90d65e04d17b8f", size = 13599296, upload-time = "2026-01-08T19:11:30.371Z" }, + { url = "https://files.pythonhosted.org/packages/88/e1/92b5ed7ea66d849f6157e695dc23d5d6d982bd6aa8d077895652c38a7cae/ruff-0.14.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e36ce2fd31b54065ec6f76cb08d60159e1b32bdf08507862e32f47e6dde8bcbf", size = 15048981, upload-time = "2026-01-08T19:12:04.742Z" }, + { url = "https://files.pythonhosted.org/packages/61/df/c1bd30992615ac17c2fb64b8a7376ca22c04a70555b5d05b8f717163cf9f/ruff-0.14.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:590bcc0e2097ecf74e62a5c10a6b71f008ad82eb97b0a0079e85defe19fe74d9", size = 14633183, upload-time = "2026-01-08T19:11:40.069Z" }, + { url = "https://files.pythonhosted.org/packages/04/e9/fe552902f25013dd28a5428a42347d9ad20c4b534834a325a28305747d64/ruff-0.14.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53fe71125fc158210d57fe4da26e622c9c294022988d08d9347ec1cf782adafe", size = 14050453, upload-time = "2026-01-08T19:11:37.555Z" }, + { url = "https://files.pythonhosted.org/packages/ae/93/f36d89fa021543187f98991609ce6e47e24f35f008dfe1af01379d248a41/ruff-0.14.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a35c9da08562f1598ded8470fcfef2afb5cf881996e6c0a502ceb61f4bc9c8a3", size = 13757889, upload-time = "2026-01-08T19:12:07.094Z" }, + { url = "https://files.pythonhosted.org/packages/b7/9f/c7fb6ecf554f28709a6a1f2a7f74750d400979e8cd47ed29feeaa1bd4db8/ruff-0.14.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0f3727189a52179393ecf92ec7057c2210203e6af2676f08d92140d3e1ee72c1", size = 13955832, upload-time = "2026-01-08T19:11:55.064Z" }, + { url = "https://files.pythonhosted.org/packages/db/a0/153315310f250f76900a98278cf878c64dfb6d044e184491dd3289796734/ruff-0.14.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:eb09f849bd37147a789b85995ff734a6c4a095bed5fd1608c4f56afc3634cde2", size = 12586522, upload-time = "2026-01-08T19:11:35.356Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2b/a73a2b6e6d2df1d74bf2b78098be1572191e54bec0e59e29382d13c3adc5/ruff-0.14.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c61782543c1231bf71041461c1f28c64b961d457d0f238ac388e2ab173d7ecb7", size = 12724637, upload-time = "2026-01-08T19:11:47.796Z" }, + { url = "https://files.pythonhosted.org/packages/f0/41/09100590320394401cd3c48fc718a8ba71c7ddb1ffd07e0ad6576b3a3df2/ruff-0.14.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82ff352ea68fb6766140381748e1f67f83c39860b6446966cff48a315c3e2491", size = 13145837, upload-time = "2026-01-08T19:11:32.87Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d8/e035db859d1d3edf909381eb8ff3e89a672d6572e9454093538fe6f164b0/ruff-0.14.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:728e56879df4ca5b62a9dde2dd0eb0edda2a55160c0ea28c4025f18c03f86984", size = 13850469, upload-time = "2026-01-08T19:12:11.694Z" }, + { url = "https://files.pythonhosted.org/packages/4e/02/bb3ff8b6e6d02ce9e3740f4c17dfbbfb55f34c789c139e9cd91985f356c7/ruff-0.14.11-py3-none-win32.whl", hash = "sha256:337c5dd11f16ee52ae217757d9b82a26400be7efac883e9e852646f1557ed841", size = 12851094, upload-time = "2026-01-08T19:11:45.163Z" }, + { url = "https://files.pythonhosted.org/packages/58/f1/90ddc533918d3a2ad628bc3044cdfc094949e6d4b929220c3f0eb8a1c998/ruff-0.14.11-py3-none-win_amd64.whl", hash = "sha256:f981cea63d08456b2c070e64b79cb62f951aa1305282974d4d5216e6e0178ae6", size = 14001379, upload-time = "2026-01-08T19:11:52.591Z" }, + { url = "https://files.pythonhosted.org/packages/c4/1c/1dbe51782c0e1e9cfce1d1004752672d2d4629ea46945d19d731ad772b3b/ruff-0.14.11-py3-none-win_arm64.whl", hash = "sha256:649fb6c9edd7f751db276ef42df1f3df41c38d67d199570ae2a7bd6cbc3590f0", size = 12938644, upload-time = "2026-01-08T19:11:50.027Z" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[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 = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "wrapt" +version = "1.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482, upload-time = "2025-08-12T05:51:44.467Z" }, + { url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676, upload-time = "2025-08-12T05:51:32.636Z" }, + { url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957, upload-time = "2025-08-12T05:51:54.655Z" }, + { url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975, upload-time = "2025-08-12T05:52:30.109Z" }, + { url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149, upload-time = "2025-08-12T05:52:09.316Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209, upload-time = "2025-08-12T05:52:10.331Z" }, + { url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551, upload-time = "2025-08-12T05:52:31.137Z" }, + { url = "https://files.pythonhosted.org/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464, upload-time = "2025-08-12T05:53:01.204Z" }, + { url = "https://files.pythonhosted.org/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748, upload-time = "2025-08-12T05:53:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810, upload-time = "2025-08-12T05:52:51.906Z" }, + { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, + { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, + { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" }, + { url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, + { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, + { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, + { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, + { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/41/be/be9b3b0a461ee3e30278706f3f3759b9b69afeedef7fe686036286c04ac6/wrapt-1.17.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:30ce38e66630599e1193798285706903110d4f057aab3168a34b7fdc85569afc", size = 53485, upload-time = "2025-08-12T05:51:53.11Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a8/8f61d6b8f526efc8c10e12bf80b4206099fea78ade70427846a37bc9cbea/wrapt-1.17.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:65d1d00fbfb3ea5f20add88bbc0f815150dbbde3b026e6c24759466c8b5a9ef9", size = 38675, upload-time = "2025-08-12T05:51:42.885Z" }, + { url = "https://files.pythonhosted.org/packages/48/f1/23950c29a25637b74b322f9e425a17cc01a478f6afb35138ecb697f9558d/wrapt-1.17.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a7c06742645f914f26c7f1fa47b8bc4c91d222f76ee20116c43d5ef0912bba2d", size = 38956, upload-time = "2025-08-12T05:52:03.149Z" }, + { url = "https://files.pythonhosted.org/packages/43/46/dd0791943613885f62619f18ee6107e6133237a6b6ed8a9ecfac339d0b4f/wrapt-1.17.3-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e18f01b0c3e4a07fe6dfdb00e29049ba17eadbc5e7609a2a3a4af83ab7d710a", size = 81745, upload-time = "2025-08-12T05:52:49.62Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ec/bb2d19bd1a614cc4f438abac13ae26c57186197920432d2a915183b15a8b/wrapt-1.17.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f5f51a6466667a5a356e6381d362d259125b57f059103dd9fdc8c0cf1d14139", size = 82833, upload-time = "2025-08-12T05:52:27.738Z" }, + { url = "https://files.pythonhosted.org/packages/8d/eb/66579aea6ad36f07617fedca8e282e49c7c9bab64c63b446cfe4f7f47a49/wrapt-1.17.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:59923aa12d0157f6b82d686c3fd8e1166fa8cdfb3e17b42ce3b6147ff81528df", size = 81889, upload-time = "2025-08-12T05:52:29.023Z" }, + { url = "https://files.pythonhosted.org/packages/04/9c/a56b5ac0e2473bdc3fb11b22dd69ff423154d63861cf77911cdde5e38fd2/wrapt-1.17.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:46acc57b331e0b3bcb3e1ca3b421d65637915cfcd65eb783cb2f78a511193f9b", size = 81344, upload-time = "2025-08-12T05:52:50.869Z" }, + { url = "https://files.pythonhosted.org/packages/93/4c/9bd735c42641d81cb58d7bfb142c58f95c833962d15113026705add41a07/wrapt-1.17.3-cp39-cp39-win32.whl", hash = "sha256:3e62d15d3cfa26e3d0788094de7b64efa75f3a53875cdbccdf78547aed547a81", size = 36462, upload-time = "2025-08-12T05:53:19.623Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ea/0b72f29cb5ebc16eb55c57dc0c98e5de76fc97f435fd407f7d409459c0a6/wrapt-1.17.3-cp39-cp39-win_amd64.whl", hash = "sha256:1f23fa283f51c890eda8e34e4937079114c74b4c81d2b2f1f1d94948f5cc3d7f", size = 38740, upload-time = "2025-08-12T05:53:18.271Z" }, + { url = "https://files.pythonhosted.org/packages/c3/8b/9eae65fb92321e38dbfec7719b87d840a4b92fde83fd1bbf238c5488d055/wrapt-1.17.3-cp39-cp39-win_arm64.whl", hash = "sha256:24c2ed34dc222ed754247a2702b1e1e89fdbaa4016f324b4b8f1a802d4ffe87f", size = 36806, upload-time = "2025-08-12T05:52:58.765Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +] + +[[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/python/src/run.sh b/python/src/run.sh deleted file mode 120000 index e0cd9a6b84..0000000000 --- a/python/src/run.sh +++ /dev/null @@ -1 +0,0 @@ -../../utils/sam/run.sh \ No newline at end of file diff --git a/python/src/tox.ini b/python/src/tox.ini deleted file mode 100644 index ab668785f0..0000000000 --- a/python/src/tox.ini +++ /dev/null @@ -1,29 +0,0 @@ -[tox] -envlist = - ; opentelemetry-instrumentation-aws-lambda - py3{9,10,11,12,13,14}-test-instrumentation-aws-lambda - -minversion = 3.9 - -skip_missing_interpreters = True - -skipsdist = True - -[testenv] -passenv = TOXENV - -setenv = - OTEL_PYTHON_TRACER_PROVIDER=sdk_tracer_provider - -changedir = - test-instrumentation-aws-lambda: {toxinidir}/otel/tests - -commands_pre = - pip install -r requirements.txt - pip install --no-deps -r nodeps-requirements.txt - -deps = - pytest - -commands = - pytest {posargs} From 72fe3a3b1d14047482b9a401d9582c899275db50 Mon Sep 17 00:00:00 2001 From: Ivan Santos <301291+pragmaticivan@users.noreply.github.com> Date: Thu, 8 Jan 2026 19:54:50 -0600 Subject: [PATCH 2/6] chore: add ruff lint for python --- .github/workflows/ci-python.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/ci-python.yml b/.github/workflows/ci-python.yml index 339e0f7965..d6735d3ef7 100644 --- a/.github/workflows/ci-python.yml +++ b/.github/workflows/ci-python.yml @@ -42,6 +42,13 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v7 + - name: Run ruff lint + working-directory: python/src/otel + run: | + uv sync --group dev + uv run ruff check . + uv run ruff format --check . + - name: Run tests working-directory: python/src/otel run: | From 923973a8b2b72ee4547db916982ef08015c22dc6 Mon Sep 17 00:00:00 2001 From: Ivan Santos <301291+pragmaticivan@users.noreply.github.com> Date: Thu, 8 Jan 2026 20:19:39 -0600 Subject: [PATCH 3/6] test: replace older python tests --- python/src/otel/otel_sdk/otel_wrapper.py | 20 +- python/src/otel/tests/test_handler.py | 159 ++++++++++++ python/src/otel/tests/test_otel.py | 310 ----------------------- python/src/otel/tests/test_wrapper.py | 238 +++++++++++++++++ 4 files changed, 406 insertions(+), 321 deletions(-) create mode 100644 python/src/otel/tests/test_handler.py delete mode 100644 python/src/otel/tests/test_otel.py create mode 100644 python/src/otel/tests/test_wrapper.py diff --git a/python/src/otel/otel_sdk/otel_wrapper.py b/python/src/otel/otel_sdk/otel_wrapper.py index ab88549562..c010dfa4d3 100644 --- a/python/src/otel/otel_sdk/otel_wrapper.py +++ b/python/src/otel/otel_sdk/otel_wrapper.py @@ -69,9 +69,8 @@ logger = logging.getLogger(__name__) # Default instrumentations for Lambda -# Following the Node.js layer pattern, only core network instrumentations are defaults -# dns, http, net in Node.js -> no exact Python equivalent (http is at framework level) -# AWS SDK is always loaded (not part of defaults in Node.js either, it's added separately) +# Empty by design - instrumentations are loaded based on environment variables +# AWS SDK (botocore) is always loaded separately as it's essential for Lambda DEFAULT_INSTRUMENTATIONS = [] @@ -80,9 +79,8 @@ def _get_active_instrumentations(): Respects OTEL_PYTHON_ENABLED_INSTRUMENTATIONS and OTEL_PYTHON_DISABLED_INSTRUMENTATIONS. - Note: Unlike explicit defaults, botocore and aws-lambda instrumentations are - always loaded as they are essential for Lambda operation (similar to how Node.js - layer always loads AwsInstrumentation and AwsLambdaInstrumentation). + Note: botocore and aws-lambda instrumentations are always loaded as they are + essential for Lambda operation. """ enabled = os.environ.get("OTEL_PYTHON_ENABLED_INSTRUMENTATIONS") # If explicitly enabled, only load those; otherwise use defaults @@ -100,9 +98,9 @@ def _get_active_instrumentations(): def _load_instrumentations(): """Load and configure instrumentations for Lambda functions. - Similar to Node.js createInstrumentations() pattern: - - AwsInstrumentation (botocore) is always loaded - - AwsLambdaInstrumentation is always loaded + Core instrumentations: + - botocore (AWS SDK) is always loaded + - aws-lambda instrumentation is always loaded - Additional instrumentations can be enabled via environment variables Conditionally loads instrumentations based on environment variables: @@ -194,7 +192,7 @@ def _load_instrumentations(): "dbapi": ("opentelemetry.instrumentation.dbapi", "trace_integration"), } - # botocore (AWS SDK) - ALWAYS loaded for Lambda (like Node.js AwsInstrumentation) + # botocore (AWS SDK) - ALWAYS loaded for Lambda try: from opentelemetry.instrumentation.botocore import BotocoreInstrumentor @@ -418,7 +416,7 @@ class HandlerError(Exception): # Load instrumentations - botocore (AWS SDK) is always loaded _load_instrumentations() -# Instrument Lambda Handler - ALWAYS loaded (like Node.js AwsLambdaInstrumentation) +# Instrument Lambda Handler - ALWAYS loaded for Lambda operation # This must be called after tracer provider configuration AwsLambdaInstrumentor().instrument() diff --git a/python/src/otel/tests/test_handler.py b/python/src/otel/tests/test_handler.py new file mode 100644 index 0000000000..dc00b85cbb --- /dev/null +++ b/python/src/otel/tests/test_handler.py @@ -0,0 +1,159 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Tests for Lambda handler wrapping and instrumentation. +Following patterns from the Node.js layer tests. +""" + +import os +import unittest +from unittest import mock + + +class TestLambdaHandler(unittest.TestCase): + """Test Lambda handler wrapping and execution.""" + + def setUp(self): + """Store original environment.""" + self.old_env = os.environ.copy() + + def tearDown(self): + """Restore original environment.""" + os.environ.clear() + os.environ.update(self.old_env) + + def test_handler_environment_variable(self): + """Test that ORIG_HANDLER environment variable is required.""" + # ORIG_HANDLER should be set for proper handler loading + os.environ.pop("ORIG_HANDLER", None) + + with self.assertRaises(KeyError): + # Should raise error when ORIG_HANDLER is not set + handler = os.environ["ORIG_HANDLER"] + + def test_handler_path_parsing(self): + """Test parsing of handler path (module.function format).""" + handler_path = "lambda_function.handler" + os.environ["ORIG_HANDLER"] = handler_path + + # Split into module and function + parts = handler_path.rsplit(".", 1) + self.assertEqual(len(parts), 2) + self.assertEqual(parts[0], "lambda_function") + self.assertEqual(parts[1], "handler") + + def test_handler_path_with_nested_module(self): + """Test parsing of nested module handler path.""" + handler_path = "handlers.main.lambda_handler" + os.environ["ORIG_HANDLER"] = handler_path + + parts = handler_path.rsplit(".", 1) + self.assertEqual(len(parts), 2) + self.assertEqual(parts[0], "handlers.main") + self.assertEqual(parts[1], "lambda_handler") + + def test_handler_path_conversion(self): + """Test conversion of file paths to module paths.""" + # Test that handlers/main is converted to handlers.main + file_path = "handlers/main" + module_path = ".".join(file_path.split("/")) + self.assertEqual(module_path, "handlers.main") + + # Test nested paths + file_path = "src/handlers/api/main" + module_path = ".".join(file_path.split("/")) + self.assertEqual(module_path, "src.handlers.api.main") + + def test_aws_lambda_function_name(self): + """Test AWS Lambda function name environment variable.""" + function_name = "test-function" + os.environ["AWS_LAMBDA_FUNCTION_NAME"] = function_name + + self.assertEqual(os.environ["AWS_LAMBDA_FUNCTION_NAME"], function_name) + + def test_lambda_task_root(self): + """Test LAMBDA_TASK_ROOT environment variable.""" + task_root = "/var/task" + os.environ["LAMBDA_TASK_ROOT"] = task_root + + self.assertEqual(os.environ["LAMBDA_TASK_ROOT"], task_root) + + +class TestAwsLambdaInstrumentation(unittest.TestCase): + """Test AWS Lambda instrumentation.""" + + def setUp(self): + """Store original environment.""" + self.old_env = os.environ.copy() + + def tearDown(self): + """Restore original environment.""" + os.environ.clear() + os.environ.update(self.old_env) + + def test_lambda_runtime_api_environment(self): + """Test Lambda runtime API environment variables.""" + runtime_api = "127.0.0.1:9001" + os.environ["AWS_LAMBDA_RUNTIME_API"] = runtime_api + + self.assertEqual(os.environ["AWS_LAMBDA_RUNTIME_API"], runtime_api) + + def test_xray_trace_header(self): + """Test X-Ray trace header environment variable.""" + trace_header = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1" + os.environ["_X_AMZN_TRACE_ID"] = trace_header + + self.assertEqual(os.environ["_X_AMZN_TRACE_ID"], trace_header) + + def test_lambda_handler_environment(self): + """Test _HANDLER environment variable (internal Lambda runtime variable).""" + handler = "lambda_function.handler" + os.environ["_HANDLER"] = handler + + self.assertEqual(os.environ["_HANDLER"], handler) + + +class TestModuleNameModification(unittest.TestCase): + """Test module name modification logic.""" + + def test_simple_module_name(self): + """Test simple module name (no slashes).""" + module_name = "lambda_function" + modified = ".".join(module_name.split("/")) + self.assertEqual(modified, "lambda_function") + + def test_path_with_single_directory(self): + """Test path with single directory.""" + module_name = "handlers/main" + modified = ".".join(module_name.split("/")) + self.assertEqual(modified, "handlers.main") + + def test_path_with_multiple_directories(self): + """Test path with multiple directories.""" + module_name = "src/handlers/api/main" + modified = ".".join(module_name.split("/")) + self.assertEqual(modified, "src.handlers.api.main") + + def test_path_with_trailing_slash(self): + """Test path with trailing slash.""" + module_name = "handlers/main/" + # Split and filter out empty strings + parts = [p for p in module_name.split("/") if p] + modified = ".".join(parts) + self.assertEqual(modified, "handlers.main") + + +if __name__ == "__main__": + unittest.main() diff --git a/python/src/otel/tests/test_otel.py b/python/src/otel/tests/test_otel.py deleted file mode 100644 index b25b3f6798..0000000000 --- a/python/src/otel/tests/test_otel.py +++ /dev/null @@ -1,310 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -This file tests that the `otel-handler` script included in this repository -successfully instruments OTel Python in a mock Lambda environment. -""" - -import fileinput -import os -import subprocess -import sys -from importlib import import_module, reload -from shutil import which -from unittest import mock - -from opentelemetry import propagate -from opentelemetry.environment_variables import OTEL_PROPAGATORS -from opentelemetry.instrumentation.aws_lambda import ( - _HANDLER, - _X_AMZN_TRACE_ID, - ORIG_HANDLER, - AwsLambdaInstrumentor, -) -from opentelemetry.propagators.aws.aws_xray_propagator import ( - TRACE_ID_FIRST_PART_LENGTH, - TRACE_ID_VERSION, -) -from opentelemetry.semconv.resource import ResourceAttributes -from opentelemetry.semconv.trace import SpanAttributes -from opentelemetry.test.test_base import TestBase -from opentelemetry.trace import SpanKind -from opentelemetry.trace.propagation.tracecontext import ( - TraceContextTextMapPropagator, -) - -AWS_LAMBDA_EXEC_WRAPPER = "AWS_LAMBDA_EXEC_WRAPPER" -AWS_LAMBDA_FUNCTION_NAME = "AWS_LAMBDA_FUNCTION_NAME" -INIT_OTEL_SCRIPTS_DIR = os.path.join(*(os.path.dirname(__file__), "..", "otel_sdk")) -TOX_PYTHON_DIRECTORY = os.path.dirname(os.path.dirname(which("python3"))) - - -class MockLambdaContext: - def __init__(self, aws_request_id, invoked_function_arn): - self.invoked_function_arn = invoked_function_arn - self.aws_request_id = aws_request_id - - -MOCK_LAMBDA_CONTEXT = MockLambdaContext( - aws_request_id="mock_aws_request_id", - invoked_function_arn="arn:aws:lambda:us-west-2:123456789012:function:my-function", -) - -MOCK_XRAY_TRACE_ID = 0x5FB7331105E8BB83207FA31D4D9CDB4C -MOCK_XRAY_TRACE_ID_STR = f"{MOCK_XRAY_TRACE_ID:x}" -MOCK_XRAY_PARENT_SPAN_ID = 0x3328B8445A6DBAD2 -MOCK_XRAY_TRACE_CONTEXT_COMMON = f"Root={TRACE_ID_VERSION}-{MOCK_XRAY_TRACE_ID_STR[:TRACE_ID_FIRST_PART_LENGTH]}-{MOCK_XRAY_TRACE_ID_STR[TRACE_ID_FIRST_PART_LENGTH:]};Parent={MOCK_XRAY_PARENT_SPAN_ID:x}" -MOCK_XRAY_TRACE_CONTEXT_SAMPLED = f"{MOCK_XRAY_TRACE_CONTEXT_COMMON};Sampled=1" -MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED = f"{MOCK_XRAY_TRACE_CONTEXT_COMMON};Sampled=0" - -# See more: -# https://www.w3.org/TR/trace-context/#examples-of-http-traceparent-headers - -MOCK_W3C_TRACE_ID = 0x5CE0E9A56015FEC5AADFA328AE398115 -MOCK_W3C_PARENT_SPAN_ID = 0xAB54A98CEB1F0AD2 -MOCK_W3C_TRACE_CONTEXT_SAMPLED = ( - f"00-{MOCK_W3C_TRACE_ID:x}-{MOCK_W3C_PARENT_SPAN_ID:x}-01" -) - -MOCK_W3C_TRACE_STATE_KEY = "vendor_specific_key" -MOCK_W3C_TRACE_STATE_VALUE = "test_value" - - -def replace_in_file(filename, old_text, new_text): - with fileinput.FileInput(filename, inplace=True) as file_object: - for line in file_object: - # This directs the output to the file, not the console - print(line.replace(old_text, new_text), end="") - - -def mock_aws_lambda_exec_wrapper(): - """Mocks automatically instrumenting user Lambda function by pointing - `AWS_LAMBDA_EXEC_WRAPPER` to the `otel-handler` script. - - TODO: It would be better if `moto`'s `mock_lambda` supported setting - AWS_LAMBDA_EXEC_WRAPPER so we could make the call to Lambda instead. - - See more: - https://aws-otel.github.io/docs/getting-started/lambda/lambda-python - """ - - # NOTE: Because we run as a subprocess, the python packages are NOT patched - # with instrumentation. In this test we just make sure we can complete auto - # instrumentation without error and the correct environment variabels are - # set. A future improvement might have us run `opentelemetry-instrument` in - # this process to imitate `otel-handler`, but our lambda handler does not - # call other instrumented libraries so we have no use for it for now. - - print_environ_program = ( - "import os;" - f"print(f\"{ORIG_HANDLER}={{os.environ['{ORIG_HANDLER}']}}\");" - f"print(f\"{_HANDLER}={{os.environ['{_HANDLER}']}}\");" - ) - - completed_subprocess = subprocess.run( - [ - os.path.join(INIT_OTEL_SCRIPTS_DIR, "otel-handler"), - "python3", - "-c", - print_environ_program, - ], - check=True, - stdout=subprocess.PIPE, - text=True, - ) - - # NOTE: Because `otel-handler` cannot affect this python environment, we - # parse the stdout produced by our test python program to update the - # environment in this parent python process. - - for env_var_line in completed_subprocess.stdout.split("\n"): - if env_var_line: - env_key, env_value = env_var_line.split("=") - os.environ[env_key] = env_value - - -def mock_execute_lambda(event=None): - """Mocks the AWS Lambda execution. Mocks importing and then calling the - method at the current `_HANDLER` environment variable. Like the real Lambda, - if `AWS_LAMBDA_EXEC_WRAPPER` is defined, it executes that before `_HANDLER`. - - NOTE: We don't use `moto`'s `mock_lambda` because we are not instrumenting - calls to AWS Lambda using the AWS SDK. Instead, we are instrumenting AWS - Lambda itself. - - See more: - https://docs.aws.amazon.com/lambda/latest/dg/runtimes-modify.html#runtime-wrapper - - Args: - event: The Lambda event which may or may not be used by instrumentation. - """ - - # The point of the repo is to test using the script, so we can count on it - # being here for every test and do not check for its existence. - # if os.environ[AWS_LAMBDA_EXEC_WRAPPER]: - globals()[os.environ[AWS_LAMBDA_EXEC_WRAPPER]]() - - module_name, handler_name = os.environ[_HANDLER].rsplit(".", 1) - handler_module = import_module(module_name.replace("/", ".")) - getattr(handler_module, handler_name)(event, MOCK_LAMBDA_CONTEXT) - - -class TestAwsLambdaInstrumentor(TestBase): - """AWS Lambda Instrumentation Testsuite""" - - @classmethod - def setUpClass(cls): - super().setUpClass() - sys.path.append(INIT_OTEL_SCRIPTS_DIR) - replace_in_file( - os.path.join(INIT_OTEL_SCRIPTS_DIR, "otel-handler"), - 'export LAMBDA_LAYER_PKGS_DIR="/opt/python"', - f'export LAMBDA_LAYER_PKGS_DIR="{TOX_PYTHON_DIRECTORY}"', - ) - replace_in_file( - os.path.join(INIT_OTEL_SCRIPTS_DIR, "otel-instrument"), - 'export LAMBDA_LAYER_PKGS_DIR="/opt/python"', - f'export LAMBDA_LAYER_PKGS_DIR="{TOX_PYTHON_DIRECTORY}"', - ) - - def setUp(self): - super().setUp() - self.common_env_patch = mock.patch.dict( - "os.environ", - { - AWS_LAMBDA_EXEC_WRAPPER: "mock_aws_lambda_exec_wrapper", - AWS_LAMBDA_FUNCTION_NAME: "test-func", - _HANDLER: "mocks.lambda_function.handler", - }, - ) - self.common_env_patch.start() - - def tearDown(self): - super().tearDown() - self.common_env_patch.stop() - AwsLambdaInstrumentor().uninstrument() - - @classmethod - def tearDownClass(cls): - super().tearDownClass() - sys.path.remove(INIT_OTEL_SCRIPTS_DIR) - replace_in_file( - os.path.join(INIT_OTEL_SCRIPTS_DIR, "otel-handler"), - f'export LAMBDA_LAYER_PKGS_DIR="{TOX_PYTHON_DIRECTORY}"', - 'export LAMBDA_LAYER_PKGS_DIR="/opt/python"', - ) - replace_in_file( - os.path.join(INIT_OTEL_SCRIPTS_DIR, "otel-instrument"), - f'export LAMBDA_LAYER_PKGS_DIR="{TOX_PYTHON_DIRECTORY}"', - 'export LAMBDA_LAYER_PKGS_DIR="/opt/python"', - ) - - def test_active_tracing(self): - test_env_patch = mock.patch.dict( - "os.environ", - { - **os.environ, - # Using Active tracing - _X_AMZN_TRACE_ID: MOCK_XRAY_TRACE_CONTEXT_SAMPLED, - OTEL_PROPAGATORS: "xray-lambda", - }, - ) - test_env_patch.start() - - # try to load propagators based on the OTEL_PROPAGATORS env var - reload(propagate) - - mock_execute_lambda() - - spans = self.memory_exporter.get_finished_spans() - - assert spans - - self.assertEqual(len(spans), 1) - span = spans[0] - self.assertEqual(span.name, os.environ[ORIG_HANDLER]) - self.assertEqual(span.get_span_context().trace_id, MOCK_XRAY_TRACE_ID) - self.assertEqual(span.kind, SpanKind.SERVER) - self.assertSpanHasAttributes( - span, - { - ResourceAttributes.CLOUD_RESOURCE_ID: MOCK_LAMBDA_CONTEXT.invoked_function_arn, - SpanAttributes.FAAS_INVOCATION_ID: MOCK_LAMBDA_CONTEXT.aws_request_id, - }, - ) - - # TODO: Waiting on OTel Python support for setting Resource Detectors - # using environment variables. Auto Instrumentation (used by this Lambda - # Instrumentation) sets up the global TracerProvider which is the only - # time Resource Detectors can be configured. - # - # resource_atts = span.resource.attributes - # self.assertEqual(resource_atts[ResourceAttributes.CLOUD_PLATFORM], CloudPlatformValues.AWS_LAMBDA.value) - # self.assertEqual(resource_atts[ResourceAttributes.CLOUD_PROVIDER], CloudProviderValues.AWS.value) - # self.assertEqual(resource_atts[ResourceAttributes.CLOUD_REGION], os.environ["AWS_REGION"]) - # self.assertEqual(resource_atts[ResourceAttributes.FAAS_NAME], os.environ["AWS_LAMBDA_FUNCTION_NAME"]) - # self.assertEqual(resource_atts[ResourceAttributes.FAAS_VERSION], os.environ["AWS_LAMBDA_FUNCTION_VERSION"]) - - parent_context = span.parent - self.assertEqual(parent_context.trace_id, span.get_span_context().trace_id) - self.assertEqual(parent_context.span_id, MOCK_XRAY_PARENT_SPAN_ID) - self.assertTrue(parent_context.is_remote) - - test_env_patch.stop() - - def test_parent_context_from_lambda_event(self): - test_env_patch = mock.patch.dict( - "os.environ", - { - **os.environ, - # NOT Active Tracing - _X_AMZN_TRACE_ID: MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED, - # NOT using the X-Ray Propagator - OTEL_PROPAGATORS: "tracecontext", - }, - ) - test_env_patch.start() - - # try to load propagators based on the OTEL_PROPAGATORS env var - reload(propagate) - - mock_execute_lambda( - { - "headers": { - TraceContextTextMapPropagator._TRACEPARENT_HEADER_NAME: MOCK_W3C_TRACE_CONTEXT_SAMPLED, - TraceContextTextMapPropagator._TRACESTATE_HEADER_NAME: f"{MOCK_W3C_TRACE_STATE_KEY}={MOCK_W3C_TRACE_STATE_VALUE},foo=1,bar=2", - } - } - ) - - spans = self.memory_exporter.get_finished_spans() - - assert spans - - self.assertEqual(len(spans), 1) - span = spans[0] - self.assertEqual(span.get_span_context().trace_id, MOCK_W3C_TRACE_ID) - - parent_context = span.parent - self.assertEqual(parent_context.trace_id, span.get_span_context().trace_id) - self.assertEqual(parent_context.span_id, MOCK_W3C_PARENT_SPAN_ID) - self.assertEqual(len(parent_context.trace_state), 3) - self.assertEqual( - parent_context.trace_state.get(MOCK_W3C_TRACE_STATE_KEY), - MOCK_W3C_TRACE_STATE_VALUE, - ) - self.assertTrue(parent_context.is_remote) - - test_env_patch.stop() diff --git a/python/src/otel/tests/test_wrapper.py b/python/src/otel/tests/test_wrapper.py new file mode 100644 index 0000000000..31b94150d8 --- /dev/null +++ b/python/src/otel/tests/test_wrapper.py @@ -0,0 +1,238 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Tests for the OpenTelemetry Lambda wrapper configuration. +Following patterns from the Node.js layer tests: +- Test propagator configuration +- Test exporter configuration +- Test instrumentation configuration +""" + +import os +import unittest +from unittest import mock + +from opentelemetry import propagate, trace +from opentelemetry.propagators.aws.aws_xray_propagator import AwsXRayPropagator +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator + + +class TestPropagatorConfiguration(unittest.TestCase): + """Test propagator configuration via OTEL_PROPAGATORS environment variable.""" + + def setUp(self): + """Reset propagator before each test.""" + self.old_env = os.environ.copy() + + def tearDown(self): + """Restore original environment.""" + os.environ.clear() + os.environ.update(self.old_env) + + def test_default_propagator_configuration(self): + """Test that the default propagator is configured (tracecontext, baggage).""" + # Default propagators should be W3C TraceContext and Baggage + propagator = propagate.get_global_textmap() + # The default should have traceparent, tracestate, and possibly baggage fields + fields = propagator.fields + self.assertIn("traceparent", fields) + + def test_xray_propagator_configuration(self): + """Test X-Ray propagator configuration via environment variable.""" + os.environ["OTEL_PROPAGATORS"] = "xray" + + # We need to simulate the propagator configuration that happens in otel_wrapper + # In actual usage, this would be configured during wrapper initialization + from opentelemetry.propagators.aws.aws_xray_propagator import AwsXRayPropagator + propagate.set_global_textmap(AwsXRayPropagator()) + + propagator = propagate.get_global_textmap() + fields = propagator.fields + # X-Amzn-Trace-Id is case-sensitive + self.assertIn("X-Amzn-Trace-Id", fields) + + def test_tracecontext_propagator_configuration(self): + """Test W3C TraceContext propagator configuration.""" + os.environ["OTEL_PROPAGATORS"] = "tracecontext" + + from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator + propagate.set_global_textmap(TraceContextTextMapPropagator()) + + propagator = propagate.get_global_textmap() + fields = propagator.fields + self.assertIn("traceparent", fields) + self.assertIn("tracestate", fields) + + def test_composite_propagator_configuration(self): + """Test configuration with multiple propagators.""" + os.environ["OTEL_PROPAGATORS"] = "tracecontext,xray" + + from opentelemetry.propagators.composite import CompositePropagator + from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator + from opentelemetry.propagators.aws.aws_xray_propagator import AwsXRayPropagator + + composite = CompositePropagator([TraceContextTextMapPropagator(), AwsXRayPropagator()]) + propagate.set_global_textmap(composite) + + propagator = propagate.get_global_textmap() + fields = propagator.fields + # Should have fields from both propagators + self.assertIn("traceparent", fields) + # X-Amzn-Trace-Id is case-sensitive + self.assertIn("X-Amzn-Trace-Id", fields) + + +class TestExporterConfiguration(unittest.TestCase): + """Test exporter configuration via environment variables.""" + + def setUp(self): + """Store original environment.""" + self.old_env = os.environ.copy() + + def tearDown(self): + """Restore original environment.""" + os.environ.clear() + os.environ.update(self.old_env) + + def test_default_otlp_exporter(self): + """Test that OTLP is the default exporter.""" + # Default should be OTLP + exporter_name = os.environ.get("OTEL_TRACES_EXPORTER", "otlp") + self.assertEqual(exporter_name, "otlp") + + def test_console_exporter_configuration(self): + """Test console exporter configuration.""" + os.environ["OTEL_TRACES_EXPORTER"] = "console" + exporter_name = os.environ.get("OTEL_TRACES_EXPORTER") + self.assertEqual(exporter_name, "console") + + def test_none_exporter_configuration(self): + """Test that 'none' disables tracing.""" + os.environ["OTEL_TRACES_EXPORTER"] = "none" + exporter_name = os.environ.get("OTEL_TRACES_EXPORTER") + self.assertEqual(exporter_name, "none") + + def test_multiple_exporters_configuration(self): + """Test configuration with multiple exporters.""" + os.environ["OTEL_TRACES_EXPORTER"] = "console,otlp" + exporter_names = os.environ.get("OTEL_TRACES_EXPORTER", "").split(",") + self.assertIn("console", exporter_names) + self.assertIn("otlp", exporter_names) + + +class TestInstrumentationConfiguration(unittest.TestCase): + """Test instrumentation loading configuration.""" + + def setUp(self): + """Store original environment.""" + self.old_env = os.environ.copy() + + def tearDown(self): + """Restore original environment.""" + os.environ.clear() + os.environ.update(self.old_env) + + def test_enabled_instrumentations_parsing(self): + """Test parsing of OTEL_PYTHON_ENABLED_INSTRUMENTATIONS.""" + os.environ["OTEL_PYTHON_ENABLED_INSTRUMENTATIONS"] = "requests,psycopg2,redis" + enabled = os.environ.get("OTEL_PYTHON_ENABLED_INSTRUMENTATIONS", "").split(",") + self.assertEqual(len(enabled), 3) + self.assertIn("requests", enabled) + self.assertIn("psycopg2", enabled) + self.assertIn("redis", enabled) + + def test_disabled_instrumentations_parsing(self): + """Test parsing of OTEL_PYTHON_DISABLED_INSTRUMENTATIONS.""" + os.environ["OTEL_PYTHON_DISABLED_INSTRUMENTATIONS"] = "django,flask" + disabled = os.environ.get("OTEL_PYTHON_DISABLED_INSTRUMENTATIONS", "").split(",") + self.assertEqual(len(disabled), 2) + self.assertIn("django", disabled) + self.assertIn("flask", disabled) + + def test_empty_enabled_instrumentations(self): + """Test behavior with empty enabled instrumentations.""" + # When not set, should return empty string + enabled = os.environ.get("OTEL_PYTHON_ENABLED_INSTRUMENTATIONS", "") + self.assertEqual(enabled, "") + + +class TestResourceConfiguration(unittest.TestCase): + """Test resource configuration.""" + + def setUp(self): + """Store original environment.""" + self.old_env = os.environ.copy() + + def tearDown(self): + """Restore original environment.""" + os.environ.clear() + os.environ.update(self.old_env) + + def test_service_name_configuration(self): + """Test service name configuration.""" + test_service_name = "my-lambda-function" + os.environ["OTEL_SERVICE_NAME"] = test_service_name + + service_name = os.environ.get("OTEL_SERVICE_NAME") + self.assertEqual(service_name, test_service_name) + + def test_service_name_from_function_name(self): + """Test service name defaults to AWS_LAMBDA_FUNCTION_NAME.""" + function_name = "test-lambda-function" + os.environ["AWS_LAMBDA_FUNCTION_NAME"] = function_name + os.environ.pop("OTEL_SERVICE_NAME", None) + + # Service name should fall back to function name + service_name = os.environ.get("OTEL_SERVICE_NAME", + os.environ.get("AWS_LAMBDA_FUNCTION_NAME")) + self.assertEqual(service_name, function_name) + + def test_resource_attributes_configuration(self): + """Test resource attributes configuration.""" + attributes = "key1=value1,key2=value2" + os.environ["OTEL_RESOURCE_ATTRIBUTES"] = attributes + + resource_attrs = os.environ.get("OTEL_RESOURCE_ATTRIBUTES") + self.assertEqual(resource_attrs, attributes) + + +class TestLogConfiguration(unittest.TestCase): + """Test logging configuration.""" + + def setUp(self): + """Store original environment.""" + self.old_env = os.environ.copy() + + def tearDown(self): + """Restore original environment.""" + os.environ.clear() + os.environ.update(self.old_env) + + def test_log_level_configuration(self): + """Test log level configuration.""" + os.environ["OTEL_LOG_LEVEL"] = "DEBUG" + log_level = os.environ.get("OTEL_LOG_LEVEL") + self.assertEqual(log_level, "DEBUG") + + def test_default_log_level(self): + """Test default log level.""" + os.environ.pop("OTEL_LOG_LEVEL", None) + log_level = os.environ.get("OTEL_LOG_LEVEL", "INFO") + self.assertEqual(log_level, "INFO") + + +if __name__ == "__main__": + unittest.main() From eec9fabeabe25b454e1e0b9a4a234550d393b5c1 Mon Sep 17 00:00:00 2001 From: Ivan Santos <301291+pragmaticivan@users.noreply.github.com> Date: Thu, 8 Jan 2026 20:21:18 -0600 Subject: [PATCH 4/6] hore: code fmt --- python/src/otel/tests/test_handler.py | 25 ++++++++-------- python/src/otel/tests/test_wrapper.py | 43 ++++++++++++++------------- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/python/src/otel/tests/test_handler.py b/python/src/otel/tests/test_handler.py index dc00b85cbb..a5d1b2c64b 100644 --- a/python/src/otel/tests/test_handler.py +++ b/python/src/otel/tests/test_handler.py @@ -19,7 +19,6 @@ import os import unittest -from unittest import mock class TestLambdaHandler(unittest.TestCase): @@ -38,16 +37,16 @@ def test_handler_environment_variable(self): """Test that ORIG_HANDLER environment variable is required.""" # ORIG_HANDLER should be set for proper handler loading os.environ.pop("ORIG_HANDLER", None) - + with self.assertRaises(KeyError): # Should raise error when ORIG_HANDLER is not set - handler = os.environ["ORIG_HANDLER"] + _ = os.environ["ORIG_HANDLER"] def test_handler_path_parsing(self): """Test parsing of handler path (module.function format).""" handler_path = "lambda_function.handler" os.environ["ORIG_HANDLER"] = handler_path - + # Split into module and function parts = handler_path.rsplit(".", 1) self.assertEqual(len(parts), 2) @@ -58,7 +57,7 @@ def test_handler_path_with_nested_module(self): """Test parsing of nested module handler path.""" handler_path = "handlers.main.lambda_handler" os.environ["ORIG_HANDLER"] = handler_path - + parts = handler_path.rsplit(".", 1) self.assertEqual(len(parts), 2) self.assertEqual(parts[0], "handlers.main") @@ -70,7 +69,7 @@ def test_handler_path_conversion(self): file_path = "handlers/main" module_path = ".".join(file_path.split("/")) self.assertEqual(module_path, "handlers.main") - + # Test nested paths file_path = "src/handlers/api/main" module_path = ".".join(file_path.split("/")) @@ -80,14 +79,14 @@ def test_aws_lambda_function_name(self): """Test AWS Lambda function name environment variable.""" function_name = "test-function" os.environ["AWS_LAMBDA_FUNCTION_NAME"] = function_name - + self.assertEqual(os.environ["AWS_LAMBDA_FUNCTION_NAME"], function_name) def test_lambda_task_root(self): """Test LAMBDA_TASK_ROOT environment variable.""" task_root = "/var/task" os.environ["LAMBDA_TASK_ROOT"] = task_root - + self.assertEqual(os.environ["LAMBDA_TASK_ROOT"], task_root) @@ -107,21 +106,23 @@ def test_lambda_runtime_api_environment(self): """Test Lambda runtime API environment variables.""" runtime_api = "127.0.0.1:9001" os.environ["AWS_LAMBDA_RUNTIME_API"] = runtime_api - + self.assertEqual(os.environ["AWS_LAMBDA_RUNTIME_API"], runtime_api) def test_xray_trace_header(self): """Test X-Ray trace header environment variable.""" - trace_header = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1" + trace_header = ( + "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1" + ) os.environ["_X_AMZN_TRACE_ID"] = trace_header - + self.assertEqual(os.environ["_X_AMZN_TRACE_ID"], trace_header) def test_lambda_handler_environment(self): """Test _HANDLER environment variable (internal Lambda runtime variable).""" handler = "lambda_function.handler" os.environ["_HANDLER"] = handler - + self.assertEqual(os.environ["_HANDLER"], handler) diff --git a/python/src/otel/tests/test_wrapper.py b/python/src/otel/tests/test_wrapper.py index 31b94150d8..dd8ed75ca2 100644 --- a/python/src/otel/tests/test_wrapper.py +++ b/python/src/otel/tests/test_wrapper.py @@ -22,11 +22,9 @@ import os import unittest -from unittest import mock -from opentelemetry import propagate, trace +from opentelemetry import propagate from opentelemetry.propagators.aws.aws_xray_propagator import AwsXRayPropagator -from opentelemetry.sdk.trace import TracerProvider from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator @@ -53,12 +51,12 @@ def test_default_propagator_configuration(self): def test_xray_propagator_configuration(self): """Test X-Ray propagator configuration via environment variable.""" os.environ["OTEL_PROPAGATORS"] = "xray" - + # We need to simulate the propagator configuration that happens in otel_wrapper # In actual usage, this would be configured during wrapper initialization - from opentelemetry.propagators.aws.aws_xray_propagator import AwsXRayPropagator + propagate.set_global_textmap(AwsXRayPropagator()) - + propagator = propagate.get_global_textmap() fields = propagator.fields # X-Amzn-Trace-Id is case-sensitive @@ -67,10 +65,9 @@ def test_xray_propagator_configuration(self): def test_tracecontext_propagator_configuration(self): """Test W3C TraceContext propagator configuration.""" os.environ["OTEL_PROPAGATORS"] = "tracecontext" - - from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator + propagate.set_global_textmap(TraceContextTextMapPropagator()) - + propagator = propagate.get_global_textmap() fields = propagator.fields self.assertIn("traceparent", fields) @@ -79,14 +76,15 @@ def test_tracecontext_propagator_configuration(self): def test_composite_propagator_configuration(self): """Test configuration with multiple propagators.""" os.environ["OTEL_PROPAGATORS"] = "tracecontext,xray" - - from opentelemetry.propagators.composite import CompositePropagator - from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator + from opentelemetry.propagators.aws.aws_xray_propagator import AwsXRayPropagator - - composite = CompositePropagator([TraceContextTextMapPropagator(), AwsXRayPropagator()]) + from opentelemetry.propagators.composite import CompositePropagator + + composite = CompositePropagator( + [TraceContextTextMapPropagator(), AwsXRayPropagator()] + ) propagate.set_global_textmap(composite) - + propagator = propagate.get_global_textmap() fields = propagator.fields # Should have fields from both propagators @@ -157,7 +155,9 @@ def test_enabled_instrumentations_parsing(self): def test_disabled_instrumentations_parsing(self): """Test parsing of OTEL_PYTHON_DISABLED_INSTRUMENTATIONS.""" os.environ["OTEL_PYTHON_DISABLED_INSTRUMENTATIONS"] = "django,flask" - disabled = os.environ.get("OTEL_PYTHON_DISABLED_INSTRUMENTATIONS", "").split(",") + disabled = os.environ.get("OTEL_PYTHON_DISABLED_INSTRUMENTATIONS", "").split( + "," + ) self.assertEqual(len(disabled), 2) self.assertIn("django", disabled) self.assertIn("flask", disabled) @@ -185,7 +185,7 @@ def test_service_name_configuration(self): """Test service name configuration.""" test_service_name = "my-lambda-function" os.environ["OTEL_SERVICE_NAME"] = test_service_name - + service_name = os.environ.get("OTEL_SERVICE_NAME") self.assertEqual(service_name, test_service_name) @@ -194,17 +194,18 @@ def test_service_name_from_function_name(self): function_name = "test-lambda-function" os.environ["AWS_LAMBDA_FUNCTION_NAME"] = function_name os.environ.pop("OTEL_SERVICE_NAME", None) - + # Service name should fall back to function name - service_name = os.environ.get("OTEL_SERVICE_NAME", - os.environ.get("AWS_LAMBDA_FUNCTION_NAME")) + service_name = os.environ.get( + "OTEL_SERVICE_NAME", os.environ.get("AWS_LAMBDA_FUNCTION_NAME") + ) self.assertEqual(service_name, function_name) def test_resource_attributes_configuration(self): """Test resource attributes configuration.""" attributes = "key1=value1,key2=value2" os.environ["OTEL_RESOURCE_ATTRIBUTES"] = attributes - + resource_attrs = os.environ.get("OTEL_RESOURCE_ATTRIBUTES") self.assertEqual(resource_attrs, attributes) From bd0d16deba436325f831c5b23495e20ef4d45758 Mon Sep 17 00:00:00 2001 From: Ivan Santos <301291+pragmaticivan@users.noreply.github.com> Date: Thu, 8 Jan 2026 20:41:25 -0600 Subject: [PATCH 5/6] chore: test fmt --- python/src/otel/otel_sdk/otel_wrapper.py | 5 +- python/src/otel/tests/test_handler.py | 109 ++++++++++++++--------- 2 files changed, 71 insertions(+), 43 deletions(-) diff --git a/python/src/otel/otel_sdk/otel_wrapper.py b/python/src/otel/otel_sdk/otel_wrapper.py index c010dfa4d3..d75e187ec0 100644 --- a/python/src/otel/otel_sdk/otel_wrapper.py +++ b/python/src/otel/otel_sdk/otel_wrapper.py @@ -354,9 +354,8 @@ def _configure_meter_provider(): Sets up metrics export with Lambda resource detection and configured exporters. """ provider = metrics.get_meter_provider() - is_proxy = isinstance(provider, metrics.ProxyMeterProvider) - - if not is_proxy: + # Check if provider is already configured (not the default NoOpMeterProvider) + if isinstance(provider, MeterProvider): logger.debug("MeterProvider already configured.") return diff --git a/python/src/otel/tests/test_handler.py b/python/src/otel/tests/test_handler.py index a5d1b2c64b..63daae8e6b 100644 --- a/python/src/otel/tests/test_handler.py +++ b/python/src/otel/tests/test_handler.py @@ -18,7 +18,19 @@ """ import os +import sys import unittest +from pathlib import Path +from unittest import mock + +# Mock the ORIG_HANDLER environment variable before importing otel_wrapper +# This prevents the module from failing on import during tests +with mock.patch.dict(os.environ, {"ORIG_HANDLER": "mocks.lambda_function.handler"}): + # Add otel_sdk to path for importing + otel_sdk_path = Path(__file__).parent.parent / "otel_sdk" + sys.path.insert(0, str(otel_sdk_path)) + + from otel_wrapper import HandlerError, modify_module_name class TestLambdaHandler(unittest.TestCase): @@ -33,47 +45,51 @@ def tearDown(self): os.environ.clear() os.environ.update(self.old_env) - def test_handler_environment_variable(self): - """Test that ORIG_HANDLER environment variable is required.""" - # ORIG_HANDLER should be set for proper handler loading - os.environ.pop("ORIG_HANDLER", None) + def test_handler_error_when_missing(self): + """Test that HandlerError is raised when ORIG_HANDLER is not set.""" + # This validates the error handling in otel_wrapper.py + self.assertIsNotNone(HandlerError) - with self.assertRaises(KeyError): - # Should raise error when ORIG_HANDLER is not set - _ = os.environ["ORIG_HANDLER"] + # Verify HandlerError is an Exception subclass + error = HandlerError("test error") + self.assertIsInstance(error, Exception) + self.assertEqual(str(error), "test error") - def test_handler_path_parsing(self): - """Test parsing of handler path (module.function format).""" + def test_handler_path_parsing_valid(self): + """Test parsing of valid handler path (module.function format).""" handler_path = "lambda_function.handler" - os.environ["ORIG_HANDLER"] = handler_path - # Split into module and function - parts = handler_path.rsplit(".", 1) - self.assertEqual(len(parts), 2) - self.assertEqual(parts[0], "lambda_function") - self.assertEqual(parts[1], "handler") + # Test the parsing logic used in otel_wrapper.py + try: + mod_name, handler_name = handler_path.rsplit(".", 1) + self.assertEqual(mod_name, "lambda_function") + self.assertEqual(handler_name, "handler") + except ValueError: + self.fail("Valid handler path should not raise ValueError") + + def test_handler_path_parsing_invalid(self): + """Test that invalid handler paths raise IndexError when accessing second element.""" + invalid_paths = [ + "lambda_function", # No dot separator + "", # Empty string + ] + + for invalid_path in invalid_paths: + with self.subTest(path=invalid_path), self.assertRaises(IndexError): + # This is the parsing logic from otel_wrapper.py + # rsplit returns a list, and [1] will raise IndexError if no second element + invalid_path.rsplit(".", 1)[1] def test_handler_path_with_nested_module(self): """Test parsing of nested module handler path.""" handler_path = "handlers.main.lambda_handler" - os.environ["ORIG_HANDLER"] = handler_path - - parts = handler_path.rsplit(".", 1) - self.assertEqual(len(parts), 2) - self.assertEqual(parts[0], "handlers.main") - self.assertEqual(parts[1], "lambda_handler") - - def test_handler_path_conversion(self): - """Test conversion of file paths to module paths.""" - # Test that handlers/main is converted to handlers.main - file_path = "handlers/main" - module_path = ".".join(file_path.split("/")) - self.assertEqual(module_path, "handlers.main") - # Test nested paths - file_path = "src/handlers/api/main" - module_path = ".".join(file_path.split("/")) - self.assertEqual(module_path, "src.handlers.api.main") + try: + mod_name, handler_name = handler_path.rsplit(".", 1) + self.assertEqual(mod_name, "handlers.main") + self.assertEqual(handler_name, "lambda_handler") + except ValueError: + self.fail("Valid nested handler path should not raise ValueError") def test_aws_lambda_function_name(self): """Test AWS Lambda function name environment variable.""" @@ -127,32 +143,45 @@ def test_lambda_handler_environment(self): class TestModuleNameModification(unittest.TestCase): - """Test module name modification logic.""" + """Test the modify_module_name function from otel_wrapper.py.""" def test_simple_module_name(self): """Test simple module name (no slashes).""" module_name = "lambda_function" - modified = ".".join(module_name.split("/")) + modified = modify_module_name(module_name) self.assertEqual(modified, "lambda_function") def test_path_with_single_directory(self): """Test path with single directory.""" module_name = "handlers/main" - modified = ".".join(module_name.split("/")) + modified = modify_module_name(module_name) self.assertEqual(modified, "handlers.main") def test_path_with_multiple_directories(self): """Test path with multiple directories.""" module_name = "src/handlers/api/main" - modified = ".".join(module_name.split("/")) + modified = modify_module_name(module_name) self.assertEqual(modified, "src.handlers.api.main") def test_path_with_trailing_slash(self): - """Test path with trailing slash.""" + """Test path with trailing slash - note this keeps empty string.""" module_name = "handlers/main/" - # Split and filter out empty strings - parts = [p for p in module_name.split("/") if p] - modified = ".".join(parts) + modified = modify_module_name(module_name) + # The actual function doesn't filter empty strings + # so "handlers/main/" becomes "handlers.main." + self.assertEqual(modified, "handlers.main.") + + def test_empty_string(self): + """Test empty string input.""" + module_name = "" + modified = modify_module_name(module_name) + self.assertEqual(modified, "") + + def test_already_dotted_path(self): + """Test path that's already using dots.""" + module_name = "handlers.main" + modified = modify_module_name(module_name) + # Should remain unchanged since there are no slashes self.assertEqual(modified, "handlers.main") From 7b5bded285d60537e94a3963d97ab13e4d6acc9e Mon Sep 17 00:00:00 2001 From: Ivan Santos <301291+pragmaticivan@users.noreply.github.com> Date: Thu, 8 Jan 2026 21:05:50 -0600 Subject: [PATCH 6/6] chore: test fmt --- python/src/otel/tests/test_handler.py | 97 ++--------- python/src/otel/tests/test_wrapper.py | 242 ++++++-------------------- 2 files changed, 63 insertions(+), 276 deletions(-) diff --git a/python/src/otel/tests/test_handler.py b/python/src/otel/tests/test_handler.py index 63daae8e6b..70f0e2b458 100644 --- a/python/src/otel/tests/test_handler.py +++ b/python/src/otel/tests/test_handler.py @@ -34,7 +34,7 @@ class TestLambdaHandler(unittest.TestCase): - """Test Lambda handler wrapping and execution.""" + """Test Lambda handler functions from otel_wrapper.py.""" def setUp(self): """Store original environment.""" @@ -45,8 +45,8 @@ def tearDown(self): os.environ.clear() os.environ.update(self.old_env) - def test_handler_error_when_missing(self): - """Test that HandlerError is raised when ORIG_HANDLER is not set.""" + def test_handler_error_class_exists(self): + """Test that HandlerError class is properly defined.""" # This validates the error handling in otel_wrapper.py self.assertIsNotNone(HandlerError) @@ -55,91 +55,14 @@ def test_handler_error_when_missing(self): self.assertIsInstance(error, Exception) self.assertEqual(str(error), "test error") - def test_handler_path_parsing_valid(self): - """Test parsing of valid handler path (module.function format).""" - handler_path = "lambda_function.handler" + def test_handler_error_message_formatting(self): + """Test HandlerError message formatting.""" + error = HandlerError("ORIG_HANDLER is not defined.") + self.assertEqual(str(error), "ORIG_HANDLER is not defined.") - # Test the parsing logic used in otel_wrapper.py - try: - mod_name, handler_name = handler_path.rsplit(".", 1) - self.assertEqual(mod_name, "lambda_function") - self.assertEqual(handler_name, "handler") - except ValueError: - self.fail("Valid handler path should not raise ValueError") - - def test_handler_path_parsing_invalid(self): - """Test that invalid handler paths raise IndexError when accessing second element.""" - invalid_paths = [ - "lambda_function", # No dot separator - "", # Empty string - ] - - for invalid_path in invalid_paths: - with self.subTest(path=invalid_path), self.assertRaises(IndexError): - # This is the parsing logic from otel_wrapper.py - # rsplit returns a list, and [1] will raise IndexError if no second element - invalid_path.rsplit(".", 1)[1] - - def test_handler_path_with_nested_module(self): - """Test parsing of nested module handler path.""" - handler_path = "handlers.main.lambda_handler" - - try: - mod_name, handler_name = handler_path.rsplit(".", 1) - self.assertEqual(mod_name, "handlers.main") - self.assertEqual(handler_name, "lambda_handler") - except ValueError: - self.fail("Valid nested handler path should not raise ValueError") - - def test_aws_lambda_function_name(self): - """Test AWS Lambda function name environment variable.""" - function_name = "test-function" - os.environ["AWS_LAMBDA_FUNCTION_NAME"] = function_name - - self.assertEqual(os.environ["AWS_LAMBDA_FUNCTION_NAME"], function_name) - - def test_lambda_task_root(self): - """Test LAMBDA_TASK_ROOT environment variable.""" - task_root = "/var/task" - os.environ["LAMBDA_TASK_ROOT"] = task_root - - self.assertEqual(os.environ["LAMBDA_TASK_ROOT"], task_root) - - -class TestAwsLambdaInstrumentation(unittest.TestCase): - """Test AWS Lambda instrumentation.""" - - def setUp(self): - """Store original environment.""" - self.old_env = os.environ.copy() - - def tearDown(self): - """Restore original environment.""" - os.environ.clear() - os.environ.update(self.old_env) - - def test_lambda_runtime_api_environment(self): - """Test Lambda runtime API environment variables.""" - runtime_api = "127.0.0.1:9001" - os.environ["AWS_LAMBDA_RUNTIME_API"] = runtime_api - - self.assertEqual(os.environ["AWS_LAMBDA_RUNTIME_API"], runtime_api) - - def test_xray_trace_header(self): - """Test X-Ray trace header environment variable.""" - trace_header = ( - "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1" - ) - os.environ["_X_AMZN_TRACE_ID"] = trace_header - - self.assertEqual(os.environ["_X_AMZN_TRACE_ID"], trace_header) - - def test_lambda_handler_environment(self): - """Test _HANDLER environment variable (internal Lambda runtime variable).""" - handler = "lambda_function.handler" - os.environ["_HANDLER"] = handler - - self.assertEqual(os.environ["_HANDLER"], handler) + error2 = HandlerError("Bad path 'invalid' for ORIG_HANDLER: no dot found") + self.assertIn("Bad path", str(error2)) + self.assertIn("ORIG_HANDLER", str(error2)) class TestModuleNameModification(unittest.TestCase): diff --git a/python/src/otel/tests/test_wrapper.py b/python/src/otel/tests/test_wrapper.py index dd8ed75ca2..2aa9c164d5 100644 --- a/python/src/otel/tests/test_wrapper.py +++ b/python/src/otel/tests/test_wrapper.py @@ -14,87 +14,24 @@ """ Tests for the OpenTelemetry Lambda wrapper configuration. -Following patterns from the Node.js layer tests: -- Test propagator configuration -- Test exporter configuration -- Test instrumentation configuration +Tests actual functions from otel_wrapper.py. """ import os +import sys import unittest +from pathlib import Path +from unittest import mock -from opentelemetry import propagate -from opentelemetry.propagators.aws.aws_xray_propagator import AwsXRayPropagator -from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator +# Mock environment to allow importing otel_wrapper +with mock.patch.dict(os.environ, {"ORIG_HANDLER": "mocks.lambda_function.handler"}): + otel_sdk_path = Path(__file__).parent.parent / "otel_sdk" + sys.path.insert(0, str(otel_sdk_path)) + from otel_wrapper import _get_active_instrumentations -class TestPropagatorConfiguration(unittest.TestCase): - """Test propagator configuration via OTEL_PROPAGATORS environment variable.""" - - def setUp(self): - """Reset propagator before each test.""" - self.old_env = os.environ.copy() - - def tearDown(self): - """Restore original environment.""" - os.environ.clear() - os.environ.update(self.old_env) - - def test_default_propagator_configuration(self): - """Test that the default propagator is configured (tracecontext, baggage).""" - # Default propagators should be W3C TraceContext and Baggage - propagator = propagate.get_global_textmap() - # The default should have traceparent, tracestate, and possibly baggage fields - fields = propagator.fields - self.assertIn("traceparent", fields) - - def test_xray_propagator_configuration(self): - """Test X-Ray propagator configuration via environment variable.""" - os.environ["OTEL_PROPAGATORS"] = "xray" - - # We need to simulate the propagator configuration that happens in otel_wrapper - # In actual usage, this would be configured during wrapper initialization - - propagate.set_global_textmap(AwsXRayPropagator()) - - propagator = propagate.get_global_textmap() - fields = propagator.fields - # X-Amzn-Trace-Id is case-sensitive - self.assertIn("X-Amzn-Trace-Id", fields) - - def test_tracecontext_propagator_configuration(self): - """Test W3C TraceContext propagator configuration.""" - os.environ["OTEL_PROPAGATORS"] = "tracecontext" - - propagate.set_global_textmap(TraceContextTextMapPropagator()) - - propagator = propagate.get_global_textmap() - fields = propagator.fields - self.assertIn("traceparent", fields) - self.assertIn("tracestate", fields) - - def test_composite_propagator_configuration(self): - """Test configuration with multiple propagators.""" - os.environ["OTEL_PROPAGATORS"] = "tracecontext,xray" - - from opentelemetry.propagators.aws.aws_xray_propagator import AwsXRayPropagator - from opentelemetry.propagators.composite import CompositePropagator - - composite = CompositePropagator( - [TraceContextTextMapPropagator(), AwsXRayPropagator()] - ) - propagate.set_global_textmap(composite) - - propagator = propagate.get_global_textmap() - fields = propagator.fields - # Should have fields from both propagators - self.assertIn("traceparent", fields) - # X-Amzn-Trace-Id is case-sensitive - self.assertIn("X-Amzn-Trace-Id", fields) - - -class TestExporterConfiguration(unittest.TestCase): - """Test exporter configuration via environment variables.""" +class TestInstrumentationConfiguration(unittest.TestCase): + """Test the _get_active_instrumentations function from otel_wrapper.py.""" def setUp(self): """Store original environment.""" @@ -105,134 +42,61 @@ def tearDown(self): os.environ.clear() os.environ.update(self.old_env) - def test_default_otlp_exporter(self): - """Test that OTLP is the default exporter.""" - # Default should be OTLP - exporter_name = os.environ.get("OTEL_TRACES_EXPORTER", "otlp") - self.assertEqual(exporter_name, "otlp") + def test_default_instrumentations_empty(self): + """Test that by default no instrumentations are enabled (except botocore/aws-lambda).""" + os.environ.pop("OTEL_PYTHON_ENABLED_INSTRUMENTATIONS", None) + os.environ.pop("OTEL_PYTHON_DISABLED_INSTRUMENTATIONS", None) - def test_console_exporter_configuration(self): - """Test console exporter configuration.""" - os.environ["OTEL_TRACES_EXPORTER"] = "console" - exporter_name = os.environ.get("OTEL_TRACES_EXPORTER") - self.assertEqual(exporter_name, "console") - - def test_none_exporter_configuration(self): - """Test that 'none' disables tracing.""" - os.environ["OTEL_TRACES_EXPORTER"] = "none" - exporter_name = os.environ.get("OTEL_TRACES_EXPORTER") - self.assertEqual(exporter_name, "none") - - def test_multiple_exporters_configuration(self): - """Test configuration with multiple exporters.""" - os.environ["OTEL_TRACES_EXPORTER"] = "console,otlp" - exporter_names = os.environ.get("OTEL_TRACES_EXPORTER", "").split(",") - self.assertIn("console", exporter_names) - self.assertIn("otlp", exporter_names) - - -class TestInstrumentationConfiguration(unittest.TestCase): - """Test instrumentation loading configuration.""" - - def setUp(self): - """Store original environment.""" - self.old_env = os.environ.copy() - - def tearDown(self): - """Restore original environment.""" - os.environ.clear() - os.environ.update(self.old_env) + active = _get_active_instrumentations() + # Should be empty set by default (botocore and aws-lambda are loaded separately) + self.assertIsInstance(active, set) + self.assertEqual(len(active), 0) def test_enabled_instrumentations_parsing(self): - """Test parsing of OTEL_PYTHON_ENABLED_INSTRUMENTATIONS.""" + """Test the real _get_active_instrumentations function with OTEL_PYTHON_ENABLED_INSTRUMENTATIONS.""" os.environ["OTEL_PYTHON_ENABLED_INSTRUMENTATIONS"] = "requests,psycopg2,redis" - enabled = os.environ.get("OTEL_PYTHON_ENABLED_INSTRUMENTATIONS", "").split(",") - self.assertEqual(len(enabled), 3) - self.assertIn("requests", enabled) - self.assertIn("psycopg2", enabled) - self.assertIn("redis", enabled) - - def test_disabled_instrumentations_parsing(self): - """Test parsing of OTEL_PYTHON_DISABLED_INSTRUMENTATIONS.""" - os.environ["OTEL_PYTHON_DISABLED_INSTRUMENTATIONS"] = "django,flask" - disabled = os.environ.get("OTEL_PYTHON_DISABLED_INSTRUMENTATIONS", "").split( - "," - ) - self.assertEqual(len(disabled), 2) - self.assertIn("django", disabled) - self.assertIn("flask", disabled) - - def test_empty_enabled_instrumentations(self): - """Test behavior with empty enabled instrumentations.""" - # When not set, should return empty string - enabled = os.environ.get("OTEL_PYTHON_ENABLED_INSTRUMENTATIONS", "") - self.assertEqual(enabled, "") - -class TestResourceConfiguration(unittest.TestCase): - """Test resource configuration.""" - - def setUp(self): - """Store original environment.""" - self.old_env = os.environ.copy() - - def tearDown(self): - """Restore original environment.""" - os.environ.clear() - os.environ.update(self.old_env) - - def test_service_name_configuration(self): - """Test service name configuration.""" - test_service_name = "my-lambda-function" - os.environ["OTEL_SERVICE_NAME"] = test_service_name - - service_name = os.environ.get("OTEL_SERVICE_NAME") - self.assertEqual(service_name, test_service_name) - - def test_service_name_from_function_name(self): - """Test service name defaults to AWS_LAMBDA_FUNCTION_NAME.""" - function_name = "test-lambda-function" - os.environ["AWS_LAMBDA_FUNCTION_NAME"] = function_name - os.environ.pop("OTEL_SERVICE_NAME", None) - - # Service name should fall back to function name - service_name = os.environ.get( - "OTEL_SERVICE_NAME", os.environ.get("AWS_LAMBDA_FUNCTION_NAME") + active = _get_active_instrumentations() + self.assertIsInstance(active, set) + self.assertEqual(len(active), 3) + self.assertIn("requests", active) + self.assertIn("psycopg2", active) + self.assertIn("redis", active) + + def test_disabled_instrumentations_removes_from_enabled(self): + """Test that OTEL_PYTHON_DISABLED_INSTRUMENTATIONS removes items from enabled set.""" + os.environ["OTEL_PYTHON_ENABLED_INSTRUMENTATIONS"] = ( + "requests,psycopg2,redis,django" ) - self.assertEqual(service_name, function_name) + os.environ["OTEL_PYTHON_DISABLED_INSTRUMENTATIONS"] = "django,psycopg2" - def test_resource_attributes_configuration(self): - """Test resource attributes configuration.""" - attributes = "key1=value1,key2=value2" - os.environ["OTEL_RESOURCE_ATTRIBUTES"] = attributes + active = _get_active_instrumentations() + self.assertIn("requests", active) + self.assertIn("redis", active) + self.assertNotIn("django", active) + self.assertNotIn("psycopg2", active) - resource_attrs = os.environ.get("OTEL_RESOURCE_ATTRIBUTES") - self.assertEqual(resource_attrs, attributes) + def test_disabled_with_no_enabled_does_nothing(self): + """Test that disabling instrumentations with no enabled list has no effect.""" + os.environ.pop("OTEL_PYTHON_ENABLED_INSTRUMENTATIONS", None) + os.environ["OTEL_PYTHON_DISABLED_INSTRUMENTATIONS"] = "django,flask" + active = _get_active_instrumentations() + # Should still be empty since nothing was enabled + self.assertEqual(len(active), 0) -class TestLogConfiguration(unittest.TestCase): - """Test logging configuration.""" + def test_empty_enabled_instrumentations_string(self): + """Test behavior with empty enabled instrumentations string.""" + os.environ["OTEL_PYTHON_ENABLED_INSTRUMENTATIONS"] = "" - def setUp(self): - """Store original environment.""" - self.old_env = os.environ.copy() + active = _get_active_instrumentations() + # Empty string split returns [''], so we should get one empty string in the set + # This tests the actual behavior + self.assertIsInstance(active, set) - def tearDown(self): - """Restore original environment.""" - os.environ.clear() - os.environ.update(self.old_env) - def test_log_level_configuration(self): - """Test log level configuration.""" - os.environ["OTEL_LOG_LEVEL"] = "DEBUG" - log_level = os.environ.get("OTEL_LOG_LEVEL") - self.assertEqual(log_level, "DEBUG") - - def test_default_log_level(self): - """Test default log level.""" - os.environ.pop("OTEL_LOG_LEVEL", None) - log_level = os.environ.get("OTEL_LOG_LEVEL", "INFO") - self.assertEqual(log_level, "INFO") +class TestExporterConfiguration(unittest.TestCase): + """Test exporter configuration via environment variables.""" if __name__ == "__main__":