diff --git a/mise.lock b/mise.lock index c72c077..1144569 100644 --- a/mise.lock +++ b/mise.lock @@ -7,30 +7,37 @@ backend = "aqua:nektos/act" [tools.act."platforms.linux-arm64"] checksum = "sha256:94d87738f7ea6650782c8505366c758c99db54cc67bd8c711583478c93305d78" url = "https://github.com/nektos/act/releases/download/v0.2.88/act_Linux_arm64.tar.gz" +url_api = "https://api.github.com/repos/nektos/act/releases/assets/409583068" [tools.act."platforms.linux-arm64-musl"] checksum = "sha256:94d87738f7ea6650782c8505366c758c99db54cc67bd8c711583478c93305d78" url = "https://github.com/nektos/act/releases/download/v0.2.88/act_Linux_arm64.tar.gz" +url_api = "https://api.github.com/repos/nektos/act/releases/assets/409583068" [tools.act."platforms.linux-x64"] checksum = "sha256:1eb9996682dfcc053ac8f3f90f2ec50376f0cdfc229712d82da03d673c63a2b3" url = "https://github.com/nektos/act/releases/download/v0.2.88/act_Linux_x86_64.tar.gz" +url_api = "https://api.github.com/repos/nektos/act/releases/assets/409583042" [tools.act."platforms.linux-x64-musl"] checksum = "sha256:1eb9996682dfcc053ac8f3f90f2ec50376f0cdfc229712d82da03d673c63a2b3" url = "https://github.com/nektos/act/releases/download/v0.2.88/act_Linux_x86_64.tar.gz" +url_api = "https://api.github.com/repos/nektos/act/releases/assets/409583042" [tools.act."platforms.macos-arm64"] checksum = "sha256:5f52aa4151c1cc762b246534a7f5633ae2b4b89aa036fbba7d9ccf04ca37acd9" url = "https://github.com/nektos/act/releases/download/v0.2.88/act_Darwin_arm64.tar.gz" +url_api = "https://api.github.com/repos/nektos/act/releases/assets/409583003" [tools.act."platforms.macos-x64"] checksum = "sha256:887cd13013fdd866f80872ef2b473b7d34af6c2e366cc77542c9b75dceea7a82" url = "https://github.com/nektos/act/releases/download/v0.2.88/act_Darwin_x86_64.tar.gz" +url_api = "https://api.github.com/repos/nektos/act/releases/assets/409583040" [tools.act."platforms.windows-x64"] checksum = "sha256:fb501a109f40193dbccf7124d3098e0802276ace86948507eedd5aee2b3bc18b" url = "https://github.com/nektos/act/releases/download/v0.2.88/act_Windows_x86_64.zip" +url_api = "https://api.github.com/repos/nektos/act/releases/assets/409583039" [[tools.adr-tools]] version = "3.0.0" @@ -65,36 +72,43 @@ backend = "aqua:cli/cli" [tools.gh."platforms.linux-arm64"] checksum = "sha256:c2248526dd0160c08d3fccca2332c3c1a07c15a78b23978e77735f1b5a18cfee" url = "https://github.com/cli/cli/releases/download/v2.92.0/gh_2.92.0_linux_arm64.tar.gz" +url_api = "https://api.github.com/repos/cli/cli/releases/assets/407353515" provenance = "github-attestations" [tools.gh."platforms.linux-arm64-musl"] checksum = "sha256:c2248526dd0160c08d3fccca2332c3c1a07c15a78b23978e77735f1b5a18cfee" url = "https://github.com/cli/cli/releases/download/v2.92.0/gh_2.92.0_linux_arm64.tar.gz" +url_api = "https://api.github.com/repos/cli/cli/releases/assets/407353515" provenance = "github-attestations" [tools.gh."platforms.linux-x64"] checksum = "sha256:b57848131bdf0c229cd35e1f2a51aa718199858b2e728410b37e89a428943ec4" url = "https://github.com/cli/cli/releases/download/v2.92.0/gh_2.92.0_linux_amd64.tar.gz" +url_api = "https://api.github.com/repos/cli/cli/releases/assets/407353509" provenance = "github-attestations" [tools.gh."platforms.linux-x64-musl"] checksum = "sha256:b57848131bdf0c229cd35e1f2a51aa718199858b2e728410b37e89a428943ec4" url = "https://github.com/cli/cli/releases/download/v2.92.0/gh_2.92.0_linux_amd64.tar.gz" +url_api = "https://api.github.com/repos/cli/cli/releases/assets/407353509" provenance = "github-attestations" [tools.gh."platforms.macos-arm64"] checksum = "sha256:b11c54f6bd7d15ed6590475079e5b2fcf36f45d3991a80041b29c9d0cc1f1d07" url = "https://github.com/cli/cli/releases/download/v2.92.0/gh_2.92.0_macOS_arm64.zip" +url_api = "https://api.github.com/repos/cli/cli/releases/assets/407353533" provenance = "github-attestations" [tools.gh."platforms.macos-x64"] checksum = "sha256:ae9bb327ab0d91071bdada79f8f14034a2a0f19b0e001835a782eafa519d2af0" url = "https://github.com/cli/cli/releases/download/v2.92.0/gh_2.92.0_macOS_amd64.zip" +url_api = "https://api.github.com/repos/cli/cli/releases/assets/407353532" provenance = "github-attestations" [tools.gh."platforms.windows-x64"] checksum = "sha256:b6a8df3c8c6b9c80f290906387673bc4d272840f3789c5650e0e4e6e75522785" url = "https://github.com/cli/cli/releases/download/v2.92.0/gh_2.92.0_windows_amd64.zip" +url_api = "https://api.github.com/repos/cli/cli/releases/assets/407353546" provenance = "github-attestations" [[tools."github:minamijoyo/hcledit"]] @@ -150,36 +164,43 @@ backend = "aqua:jqlang/jq" [tools.jq."platforms.linux-arm64"] checksum = "sha256:6bc62f25981328edd3cfcfe6fe51b073f2d7e7710d7ef7fcdac28d4e384fc3d4" url = "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-linux-arm64" +url_api = "https://api.github.com/repos/jqlang/jq/releases/assets/268981481" provenance = "github-attestations" [tools.jq."platforms.linux-arm64-musl"] checksum = "sha256:6bc62f25981328edd3cfcfe6fe51b073f2d7e7710d7ef7fcdac28d4e384fc3d4" url = "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-linux-arm64" +url_api = "https://api.github.com/repos/jqlang/jq/releases/assets/268981481" provenance = "github-attestations" [tools.jq."platforms.linux-x64"] checksum = "sha256:020468de7539ce70ef1bceaf7cde2e8c4f2ca6c3afb84642aabc5c97d9fc2a0d" url = "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-linux-amd64" +url_api = "https://api.github.com/repos/jqlang/jq/releases/assets/268981484" provenance = "github-attestations" [tools.jq."platforms.linux-x64-musl"] checksum = "sha256:020468de7539ce70ef1bceaf7cde2e8c4f2ca6c3afb84642aabc5c97d9fc2a0d" url = "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-linux-amd64" +url_api = "https://api.github.com/repos/jqlang/jq/releases/assets/268981484" provenance = "github-attestations" [tools.jq."platforms.macos-arm64"] checksum = "sha256:a9fe3ea2f86dfc72f6728417521ec9067b343277152b114f4e98d8cb0e263603" url = "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-macos-arm64" +url_api = "https://api.github.com/repos/jqlang/jq/releases/assets/268981506" provenance = "github-attestations" [tools.jq."platforms.macos-x64"] checksum = "sha256:e80dbe0d2a2597e3c11c404f03337b981d74b4a8504b70586c354b7697a7c27f" url = "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-macos-amd64" +url_api = "https://api.github.com/repos/jqlang/jq/releases/assets/268981505" provenance = "github-attestations" [tools.jq."platforms.windows-x64"] checksum = "sha256:23cb60a1354eed6bcc8d9b9735e8c7b388cd1fdcb75726b93bc299ef22dd9334" url = "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-windows-amd64.exe" +url_api = "https://api.github.com/repos/jqlang/jq/releases/assets/268981509" provenance = "github-attestations" [[tools."pipx:keyring"]] @@ -235,36 +256,43 @@ backend = "aqua:aquasecurity/trivy" [tools.trivy."platforms.linux-arm64"] checksum = "sha256:2f6bb988b553a1bbac6bdd1ce890f5e412439564e17522b88a4541b4f364fc8d" url = "https://github.com/aquasecurity/trivy/releases/download/v0.70.0/trivy_0.70.0_Linux-ARM64.tar.gz" +url_api = "https://api.github.com/repos/aquasecurity/trivy/releases/assets/398381978" provenance = "cosign" [tools.trivy."platforms.linux-arm64-musl"] checksum = "sha256:2f6bb988b553a1bbac6bdd1ce890f5e412439564e17522b88a4541b4f364fc8d" url = "https://github.com/aquasecurity/trivy/releases/download/v0.70.0/trivy_0.70.0_Linux-ARM64.tar.gz" +url_api = "https://api.github.com/repos/aquasecurity/trivy/releases/assets/398381978" provenance = "cosign" [tools.trivy."platforms.linux-x64"] checksum = "sha256:8b4376d5d6befe5c24d503f10ff136d9e0c49f9127a4279fd110b727929a5aa9" url = "https://github.com/aquasecurity/trivy/releases/download/v0.70.0/trivy_0.70.0_Linux-64bit.tar.gz" +url_api = "https://api.github.com/repos/aquasecurity/trivy/releases/assets/398381981" provenance = "cosign" [tools.trivy."platforms.linux-x64-musl"] checksum = "sha256:8b4376d5d6befe5c24d503f10ff136d9e0c49f9127a4279fd110b727929a5aa9" url = "https://github.com/aquasecurity/trivy/releases/download/v0.70.0/trivy_0.70.0_Linux-64bit.tar.gz" +url_api = "https://api.github.com/repos/aquasecurity/trivy/releases/assets/398381981" provenance = "cosign" [tools.trivy."platforms.macos-arm64"] checksum = "sha256:68e543c51dcc96e1c344053a4fde9660cf602c25565d9f09dc17dd41e13b838a" url = "https://github.com/aquasecurity/trivy/releases/download/v0.70.0/trivy_0.70.0_macOS-ARM64.tar.gz" +url_api = "https://api.github.com/repos/aquasecurity/trivy/releases/assets/398381999" provenance = "cosign" [tools.trivy."platforms.macos-x64"] checksum = "sha256:52d531452b19e7593da29366007d02a810e1e0080d02f9cf6a1afb46c35aaa93" url = "https://github.com/aquasecurity/trivy/releases/download/v0.70.0/trivy_0.70.0_macOS-64bit.tar.gz" +url_api = "https://api.github.com/repos/aquasecurity/trivy/releases/assets/398382001" provenance = "cosign" [tools.trivy."platforms.windows-x64"] checksum = "sha256:eea5442eab86f9e26cd718d7618d43899e72a83767619e8bee47911bddbfb825" url = "https://github.com/aquasecurity/trivy/releases/download/v0.70.0/trivy_0.70.0_windows-64bit.zip" +url_api = "https://api.github.com/repos/aquasecurity/trivy/releases/assets/398382020" provenance = "cosign" [[tools.uv]] @@ -274,34 +302,41 @@ backend = "aqua:astral-sh/uv" [tools.uv."platforms.linux-arm64"] checksum = "sha256:d7d3966e46915c5f6932692aaf152a2473eecb1d2517ca4f8e88a07484b380b6" url = "https://github.com/astral-sh/uv/releases/download/0.11.14/uv-aarch64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/418588773" provenance = "github-attestations" [tools.uv."platforms.linux-arm64-musl"] checksum = "sha256:d7d3966e46915c5f6932692aaf152a2473eecb1d2517ca4f8e88a07484b380b6" url = "https://github.com/astral-sh/uv/releases/download/0.11.14/uv-aarch64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/418588773" provenance = "github-attestations" [tools.uv."platforms.linux-x64"] checksum = "sha256:077d36f45a0cc6d440b653b2d5c53e7731121e99e54b0221267eec5d1cae76ce" url = "https://github.com/astral-sh/uv/releases/download/0.11.14/uv-x86_64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/418588872" provenance = "github-attestations" [tools.uv."platforms.linux-x64-musl"] checksum = "sha256:077d36f45a0cc6d440b653b2d5c53e7731121e99e54b0221267eec5d1cae76ce" url = "https://github.com/astral-sh/uv/releases/download/0.11.14/uv-x86_64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/418588872" provenance = "github-attestations" [tools.uv."platforms.macos-arm64"] checksum = "sha256:4333af5c0730d94323a7819bbdf87ce92dd07fc857d67fff0059e0fca31b5c02" url = "https://github.com/astral-sh/uv/releases/download/0.11.14/uv-aarch64-apple-darwin.tar.gz" +url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/418588752" provenance = "github-attestations" [tools.uv."platforms.macos-x64"] checksum = "sha256:9836c1440b0bd6aa5f81793648a339bd01d593b7b8f575de3b855dae4ab64654" url = "https://github.com/astral-sh/uv/releases/download/0.11.14/uv-x86_64-apple-darwin.tar.gz" +url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/418588859" provenance = "github-attestations" [tools.uv."platforms.windows-x64"] checksum = "sha256:52ba5d19409aaa688a8a1a6ec8dfb6a4817230d20186e75f4006105c3e39a846" url = "https://github.com/astral-sh/uv/releases/download/0.11.14/uv-x86_64-pc-windows-msvc.zip" +url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/418588864" provenance = "github-attestations" diff --git a/pyproject.toml b/pyproject.toml index 32a4a10..f29d312 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,9 @@ dependencies = [ "fastapi>=0.110,<1", "httpx2>=2.2.0,<3", "loguru>=0.7,<1", + "opentelemetry-sdk>=1.27,<2", + "opentelemetry-exporter-otlp-proto-grpc>=1.27,<2", + "opentelemetry-instrumentation-fastapi>=0.48b0,<1", "PyJWT>=2.10,<3", "platformdirs>=4,<5", "psutil>=6", diff --git a/src/aignostics_foundry_core/boot.py b/src/aignostics_foundry_core/boot.py index 191d000..4620c06 100644 --- a/src/aignostics_foundry_core/boot.py +++ b/src/aignostics_foundry_core/boot.py @@ -19,6 +19,7 @@ from aignostics_foundry_core.foundry import get_context from aignostics_foundry_core.log import logging_initialize +from aignostics_foundry_core.otel import otel_initialize from aignostics_foundry_core.process import get_process_info from aignostics_foundry_core.sentry import sentry_initialize @@ -61,8 +62,9 @@ def boot( 2. Initialise loguru logging via :func:`~aignostics_foundry_core.log.logging_initialize`. 3. Amend the SSL trust chain with *truststore* and *certifi*. 4. Initialise Sentry via :func:`~aignostics_foundry_core.sentry.sentry_initialize`. - 5. Log a boot message with version, PID, and process information. - 6. Register an atexit shutdown message. + 5. Initialise OpenTelemetry via :func:`~aignostics_foundry_core.otel.otel_initialize`. + 6. Log a boot message with version, PID, and process information. + 7. Register an atexit shutdown message. Args: context: :class:`~aignostics_foundry_core.foundry.FoundryContext` providing @@ -90,6 +92,7 @@ def boot( integrations=sentry_integrations, context=ctx, ) + otel_initialize(context=ctx) _log_boot_message(context=ctx, show_cmdline=show_cmdline) _register_shutdown_message(project_name=ctx.name, version=ctx.version) logger.trace("Boot sequence completed successfully.") diff --git a/src/aignostics_foundry_core/otel.py b/src/aignostics_foundry_core/otel.py new file mode 100644 index 0000000..da63de0 --- /dev/null +++ b/src/aignostics_foundry_core/otel.py @@ -0,0 +1,269 @@ +"""OpenTelemetry integration for application telemetry. + +Mirrors the pattern used by :mod:`aignostics_foundry_core.sentry`: a +pydantic-settings-driven ``enabled`` flag, one initialisation entry-point +called from :func:`~aignostics_foundry_core.boot.boot`, and graceful +degradation when the optional SDK or an OTLP endpoint isn't configured. + +Endpoint, service name, and all other exporter behaviour are read directly +from the standard OpenTelemetry environment variables (``OTEL_EXPORTER_OTLP_ENDPOINT``, +``OTEL_SERVICE_NAME``, ``OTEL_RESOURCE_ATTRIBUTES``, etc. — see +https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/) +rather than project-prefixed settings, so that services already familiar with +the OTel spec don't have to learn a second set of variable names. + +``boot()`` sets up the process-wide TracerProvider/MeterProvider/LoggerProvider +and FastAPI/ASGI-independent instrumentation, including bridging loguru +records into OTLP logs. Request-level FastAPI tracing requires calling +:func:`instrument_fastapi` once the app instance exists (``boot()`` runs +before any FastAPI app is constructed) — see the module docstring on +``Service.api_serve()`` in the ``foundry-python`` template. +""" + +from __future__ import annotations + +import logging +import os +from importlib.util import find_spec +from typing import TYPE_CHECKING, Annotated, Any + +from loguru import logger +from pydantic import Field +from pydantic_settings import SettingsConfigDict + +from aignostics_foundry_core.foundry import get_context +from aignostics_foundry_core.settings import OpaqueSettings + +if TYPE_CHECKING: + from collections.abc import Callable + + import fastapi + from loguru import Message + from opentelemetry.sdk._logs import LoggingHandler # pyright: ignore[reportPrivateImportUsage] + from opentelemetry.sdk.resources import Resource + + from aignostics_foundry_core.foundry import FoundryContext + +_OTEL_EXPORTER_OTLP_ENDPOINT = "OTEL_EXPORTER_OTLP_ENDPOINT" +_OTEL_SERVICE_NAME = "OTEL_SERVICE_NAME" + +# Loguru level names are a superset of stdlib logging's (e.g. TRACE, SUCCESS). +# Map them to the closest stdlib numeric level so OTel's LoggingHandler (which +# expects a stdlib logging.LogRecord) records a sensible severity. +_LOGURU_TO_STDLIB_LEVEL = { + "TRACE": logging.DEBUG, + "DEBUG": logging.DEBUG, + "INFO": logging.INFO, + "SUCCESS": logging.INFO, + "WARNING": logging.WARNING, + "ERROR": logging.ERROR, + "CRITICAL": logging.CRITICAL, +} + + +class OTelSettings(OpaqueSettings): + """Configuration settings for OpenTelemetry integration. + + Reads environment variables using the prefix derived from the active + :class:`~aignostics_foundry_core.foundry.FoundryContext` (e.g. + ``MYPROJECT_OTEL_`` when the context's ``env_prefix`` is ``MYPROJECT_``). + + Only carries the project-scoped ``enabled`` switch — exporter endpoint, + service name, and sampling are configured via the standard unprefixed + ``OTEL_*`` environment variables the OpenTelemetry SDK reads itself. + """ + + model_config = SettingsConfigDict( + env_file_encoding="utf-8", + extra="ignore", + ) + + def __init__(self, **kwargs: Any) -> None: # noqa: ANN401 + """Initialise settings, deriving env_prefix from the active FoundryContext.""" + super().__init__(_env_prefix=f"{get_context().env_prefix}OTEL_", **kwargs) # pyright: ignore[reportCallIssue] + + enabled: Annotated[ + bool, + Field( + description="Enable OpenTelemetry trace and metric export via OTLP", + default=False, + ), + ] + + logs_enabled: Annotated[ + bool, + Field( + description=( + "Additionally bridge loguru records into OTLP log export. Off by default even " + "when 'enabled' is set, since log volume/cost characteristics differ from traces/metrics." + ), + default=False, + ), + ] + + +def otel_initialize( + *, + context: FoundryContext | None = None, +) -> bool: + """Initialize OpenTelemetry tracing, metrics, and logs. + + Sets up a process-wide ``TracerProvider``, ``MeterProvider``, and + ``LoggerProvider`` exporting via OTLP/gRPC, using ``OTEL_EXPORTER_OTLP_ENDPOINT`` + as configured target. Does nothing (returns ``False``) unless explicitly + enabled *and* an OTLP endpoint is configured, so that services which don't + opt in never pay the cost of provider setup. + + Logs are bridged from loguru (which this project uses instead of stdlib + ``logging``) via a loguru sink that forwards each record into OTel's own + ``LoggingHandler`` — loguru has no first-party OTel integration, so this + is the simplest way to reuse the SDK's own record conversion and trace + correlation instead of duplicating it. + + Args: + context: :class:`~aignostics_foundry_core.foundry.FoundryContext` providing + the project name (used as the ``OTEL_SERVICE_NAME`` fallback) and + version. Falls back to the global context set via + :func:`~aignostics_foundry_core.foundry.set_context`. + + Returns: + bool: ``True`` if OpenTelemetry was initialised successfully, ``False`` otherwise. + """ + ctx = context or get_context() + + settings = OTelSettings( + _env_file=ctx.env_file, # pyright: ignore[reportCallIssue] + ) + + if not find_spec("opentelemetry.sdk") or not settings.enabled: + logger.trace("OpenTelemetry integration is disabled or opentelemetry.sdk not found, initialization skipped.") + return False + + if not os.environ.get(_OTEL_EXPORTER_OTLP_ENDPOINT): + logger.warning( + "OpenTelemetry integration is enabled but {} is not set, initialization skipped.", + _OTEL_EXPORTER_OTLP_ENDPOINT, + ) + return False + + os.environ.setdefault(_OTEL_SERVICE_NAME, ctx.name) + + from opentelemetry import metrics, trace # noqa: PLC0415 + from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter # noqa: PLC0415 + from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter # noqa: PLC0415 + from opentelemetry.sdk.metrics import MeterProvider # noqa: PLC0415 + from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader # noqa: PLC0415 + from opentelemetry.sdk.resources import Resource # noqa: PLC0415 + from opentelemetry.sdk.trace import TracerProvider # noqa: PLC0415 + from opentelemetry.sdk.trace.export import BatchSpanProcessor # noqa: PLC0415 + + resource = Resource.create({"service.version": ctx.version_full}) + + tracer_provider = TracerProvider(resource=resource) + tracer_provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter())) + trace.set_tracer_provider(tracer_provider) + + meter_provider = MeterProvider( + resource=resource, + metric_readers=[PeriodicExportingMetricReader(OTLPMetricExporter())], + ) + metrics.set_meter_provider(meter_provider) + + if settings.logs_enabled: + _otel_logs_initialize(resource) + + logger.trace("OpenTelemetry integration initialized, exporting to {}.", os.environ[_OTEL_EXPORTER_OTLP_ENDPOINT]) + + return True + + +def _otel_logs_initialize(resource: Resource) -> None: + """Set up the OTLP ``LoggerProvider`` and bridge loguru records into it. + + Split out of :func:`otel_initialize` since logs export is opt-in via + :attr:`OTelSettings.logs_enabled`, off by default even when traces/metrics + are enabled. + + Args: + resource: The shared OTel ``Resource`` used by the tracer/meter providers. + """ + # opentelemetry-python's logs API is still under a leading-underscore module path + # even in stable releases (see open-telemetry/opentelemetry-python#3565) — noqa/ + # pyright-ignore below are for that upstream naming, not our own code. + from opentelemetry import _logs as logs # noqa: PLC0415, PLC2701 + from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter # noqa: PLC0415, PLC2701 + from opentelemetry.sdk._logs import ( # noqa: PLC0415, PLC2701 + LoggerProvider, # pyright: ignore[reportPrivateImportUsage] + LoggingHandler, # pyright: ignore[reportPrivateImportUsage] + ) + from opentelemetry.sdk._logs.export import ( # noqa: PLC0415, PLC2701 + BatchLogRecordProcessor, # pyright: ignore[reportPrivateImportUsage] + ) + + logger_provider = LoggerProvider(resource=resource) + logger_provider.add_log_record_processor(BatchLogRecordProcessor(OTLPLogExporter())) + logs.set_logger_provider(logger_provider) + logger.add(_make_otel_log_sink(LoggingHandler(level=logging.NOTSET, logger_provider=logger_provider))) + + +def _make_otel_log_sink(handler: LoggingHandler) -> Callable[[Message], None]: + """Build a loguru sink that forwards records to an OTel ``LoggingHandler``. + + Loguru has no first-party OTel integration, so this converts each loguru + record into the stdlib ``logging.LogRecord`` shape ``LoggingHandler.emit`` + expects, reusing the SDK's own conversion/trace-correlation logic instead + of reimplementing it. + + Args: + handler: The OTel ``LoggingHandler`` to forward converted records to. + + Returns: + A callable suitable for passing to ``loguru.logger.add()``. + """ + + def sink(message: Message) -> None: + record = message.record + exc_info = None + if record["exception"] is not None: + exc = record["exception"] + exc_info = (exc.type, exc.value, exc.traceback) + handler.emit( + logging.LogRecord( + name=record["name"] or "", + level=_LOGURU_TO_STDLIB_LEVEL.get(record["level"].name, logging.INFO), + pathname=record["file"].path, + lineno=record["line"], + msg=record["message"], + args=None, + exc_info=exc_info, # pyright: ignore[reportArgumentType] + func=record["function"], + ) + ) + + return sink + + +def instrument_fastapi(app: fastapi.FastAPI) -> bool: + """Instrument a FastAPI app instance for request-level tracing. + + Must be called once, after the app is constructed and after + :func:`otel_initialize` has run (so the TracerProvider it sets up is + already in place) — typically from the same place the app is created, + e.g. ``Service.api_serve()``. + + Args: + app: The FastAPI application instance to instrument. + + Returns: + bool: ``True`` if instrumentation was applied, ``False`` if the + ``opentelemetry-instrumentation-fastapi`` package isn't installed. + """ + if not find_spec("opentelemetry.instrumentation.fastapi"): + logger.trace("opentelemetry-instrumentation-fastapi not found, FastAPI instrumentation skipped.") + return False + + from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor # noqa: PLC0415 + + FastAPIInstrumentor.instrument_app(app) + logger.trace("FastAPI instrumentation applied.") + return True diff --git a/tests/aignostics_foundry_core/otel_test.py b/tests/aignostics_foundry_core/otel_test.py new file mode 100644 index 0000000..34e37c4 --- /dev/null +++ b/tests/aignostics_foundry_core/otel_test.py @@ -0,0 +1,237 @@ +"""Tests for aignostics_foundry_core.otel.""" + +import logging +import os +import sys +from unittest.mock import MagicMock, patch + +import pytest + +from aignostics_foundry_core.foundry import set_context +from aignostics_foundry_core.otel import OTelSettings, _make_otel_log_sink, instrument_fastapi, otel_initialize +from tests.conftest import TEST_PROJECT_NAME, TEST_PROJECT_PREFIX, make_context + +_OTEL_PREFIX = f"{TEST_PROJECT_PREFIX}OTEL_" +_OTLP_ENDPOINT = "https://otel-gateway.example.com:4317" +_OTEL_EXPORTER_OTLP_ENDPOINT = "OTEL_EXPORTER_OTLP_ENDPOINT" +_OTEL_SERVICE_NAME = "OTEL_SERVICE_NAME" +_TRACE_SET_TRACER_PROVIDER = "opentelemetry.trace.set_tracer_provider" +_METRICS_SET_METER_PROVIDER = "opentelemetry.metrics.set_meter_provider" +_LOGS_SET_LOGGER_PROVIDER = "opentelemetry._logs.set_logger_provider" +_LOGURU_LOGGER_ADD = "aignostics_foundry_core.otel.logger.add" + + +@pytest.mark.integration +class TestOtelInitialize: + """Behavioural tests for otel_initialize().""" + + def test_otel_initialize_returns_false_when_disabled(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Returns False when TESTPROJECT_OTEL_ENABLED is not set (default False).""" + monkeypatch.delenv(f"{_OTEL_PREFIX}ENABLED", raising=False) + result = otel_initialize() + assert result is False + + def test_otel_initialize_returns_false_when_sdk_absent(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Returns False when opentelemetry.sdk is not importable (find_spec returns None).""" + monkeypatch.setenv(f"{_OTEL_PREFIX}ENABLED", "true") + monkeypatch.setenv(_OTEL_EXPORTER_OTLP_ENDPOINT, _OTLP_ENDPOINT) + with patch("aignostics_foundry_core.otel.find_spec", return_value=None): + result = otel_initialize() + assert result is False + + def test_otel_initialize_returns_false_when_enabled_but_endpoint_unset( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Returns False when enabled but no OTLP endpoint is configured.""" + monkeypatch.setenv(f"{_OTEL_PREFIX}ENABLED", "true") + monkeypatch.delenv(_OTEL_EXPORTER_OTLP_ENDPOINT, raising=False) + result = otel_initialize() + assert result is False + + def test_otel_initialize_returns_true_and_sets_providers_when_enabled( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Returns True and installs tracer/meter providers when enabled with an endpoint.""" + monkeypatch.setenv(f"{_OTEL_PREFIX}ENABLED", "true") + monkeypatch.setenv(_OTEL_EXPORTER_OTLP_ENDPOINT, _OTLP_ENDPOINT) + monkeypatch.delenv(_OTEL_SERVICE_NAME, raising=False) + with ( + patch(_TRACE_SET_TRACER_PROVIDER) as mock_set_tracer_provider, + patch(_METRICS_SET_METER_PROVIDER) as mock_set_meter_provider, + ): + result = otel_initialize() + assert result is True + mock_set_tracer_provider.assert_called_once() + mock_set_meter_provider.assert_called_once() + + def test_otel_initialize_skips_logs_setup_by_default(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Logs bridge is not installed when only 'enabled' (not 'logs_enabled') is set.""" + monkeypatch.setenv(f"{_OTEL_PREFIX}ENABLED", "true") + monkeypatch.delenv(f"{_OTEL_PREFIX}LOGS_ENABLED", raising=False) + monkeypatch.setenv(_OTEL_EXPORTER_OTLP_ENDPOINT, _OTLP_ENDPOINT) + with ( + patch(_TRACE_SET_TRACER_PROVIDER), + patch(_METRICS_SET_METER_PROVIDER), + patch(_LOGS_SET_LOGGER_PROVIDER) as mock_set_logger_provider, + patch(_LOGURU_LOGGER_ADD) as mock_logger_add, + ): + result = otel_initialize() + assert result is True + mock_set_logger_provider.assert_not_called() + mock_logger_add.assert_not_called() + + def test_otel_initialize_installs_logs_bridge_when_logs_enabled(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Logs bridge is installed when both 'enabled' and 'logs_enabled' are set.""" + monkeypatch.setenv(f"{_OTEL_PREFIX}ENABLED", "true") + monkeypatch.setenv(f"{_OTEL_PREFIX}LOGS_ENABLED", "true") + monkeypatch.setenv(_OTEL_EXPORTER_OTLP_ENDPOINT, _OTLP_ENDPOINT) + with ( + patch(_TRACE_SET_TRACER_PROVIDER), + patch(_METRICS_SET_METER_PROVIDER), + patch(_LOGS_SET_LOGGER_PROVIDER) as mock_set_logger_provider, + patch(_LOGURU_LOGGER_ADD) as mock_logger_add, + ): + result = otel_initialize() + assert result is True + mock_set_logger_provider.assert_called_once() + mock_logger_add.assert_called_once() + + def test_otel_initialize_defaults_service_name_to_context_name(self, monkeypatch: pytest.MonkeyPatch) -> None: + """OTEL_SERVICE_NAME defaults to the context project name when unset.""" + monkeypatch.setenv(f"{_OTEL_PREFIX}ENABLED", "true") + monkeypatch.setenv(_OTEL_EXPORTER_OTLP_ENDPOINT, _OTLP_ENDPOINT) + monkeypatch.delenv(_OTEL_SERVICE_NAME, raising=False) + ctx = make_context() + with ( + patch(_TRACE_SET_TRACER_PROVIDER), + patch(_METRICS_SET_METER_PROVIDER), + ): + otel_initialize(context=ctx) + assert os.environ[_OTEL_SERVICE_NAME] == TEST_PROJECT_NAME + + def test_otel_initialize_does_not_override_explicit_service_name(self, monkeypatch: pytest.MonkeyPatch) -> None: + """An explicitly set OTEL_SERVICE_NAME is left untouched.""" + monkeypatch.setenv(f"{_OTEL_PREFIX}ENABLED", "true") + monkeypatch.setenv(_OTEL_EXPORTER_OTLP_ENDPOINT, _OTLP_ENDPOINT) + monkeypatch.setenv(_OTEL_SERVICE_NAME, "explicit-service") + with ( + patch(_TRACE_SET_TRACER_PROVIDER), + patch(_METRICS_SET_METER_PROVIDER), + ): + otel_initialize() + assert os.environ[_OTEL_SERVICE_NAME] == "explicit-service" + + +@pytest.mark.integration +class TestOtelSettings: + """Behavioural tests for OTelSettings.""" + + def test_otel_settings_default_disabled(self) -> None: + """OpenTelemetry is disabled by default (no env vars set).""" + settings = OTelSettings() # pyright: ignore[reportCallIssue] + assert settings.enabled is False + + def test_otel_settings_logs_default_disabled(self) -> None: + """Logs bridging is disabled by default, independent of 'enabled'.""" + settings = OTelSettings() # pyright: ignore[reportCallIssue] + assert settings.logs_enabled is False + + def test_otel_settings_uses_context_env_prefix(self, monkeypatch: pytest.MonkeyPatch) -> None: + """OTelSettings reads env vars from the prefix supplied by FoundryContext.""" + set_context(make_context()) + monkeypatch.setenv(f"{TEST_PROJECT_PREFIX}OTEL_ENABLED", "true") + settings = OTelSettings() # pyright: ignore[reportCallIssue] + assert settings.enabled is True + + +def _make_loguru_message( + *, + level_name: str = "INFO", + message: str = "hello", + exception: object = None, +) -> MagicMock: + """Build a minimal fake loguru Message with a `.record` dict for sink tests.""" + level = MagicMock() + level.name = level_name + file_ = MagicMock() + file_.path = "app.py" + msg = MagicMock() + msg.record = { + "name": "my_module", + "level": level, + "file": file_, + "line": 42, + "message": message, + "function": "my_func", + "exception": exception, + } + return msg + + +@pytest.mark.unit +class TestMakeOtelLogSink: + """Behavioural tests for _make_otel_log_sink().""" + + def test_sink_emits_stdlib_log_record_from_loguru_message(self) -> None: + """Sink converts a loguru message into a stdlib LogRecord and emits it via the handler.""" + handler = MagicMock() + sink = _make_otel_log_sink(handler) + sink(_make_loguru_message(level_name="WARNING", message="disk almost full")) + handler.emit.assert_called_once() + record = handler.emit.call_args.args[0] + assert isinstance(record, logging.LogRecord) + assert record.name == "my_module" + assert record.levelno == logging.WARNING + assert record.getMessage() == "disk almost full" + assert record.funcName == "my_func" + assert record.exc_info is None + + def test_sink_maps_loguru_only_levels_to_stdlib(self) -> None: + """TRACE and SUCCESS (loguru-only levels) map to the closest stdlib level.""" + handler = MagicMock() + sink = _make_otel_log_sink(handler) + + sink(_make_loguru_message(level_name="TRACE")) + assert handler.emit.call_args.args[0].levelno == logging.DEBUG + + sink(_make_loguru_message(level_name="SUCCESS")) + assert handler.emit.call_args.args[0].levelno == logging.INFO + + def test_sink_includes_exc_info_when_exception_present(self) -> None: + """An exception on the loguru record is forwarded as exc_info. + + Raises: + ValueError: deliberately, to obtain a real exc_info tuple. + """ + handler = MagicMock() + sink = _make_otel_log_sink(handler) + error_message = "boom" + try: + raise ValueError(error_message) # noqa: TRY301 + except ValueError: + exc_type, exc_value, exc_tb = sys.exc_info() + exception = MagicMock(type=exc_type, value=exc_value, traceback=exc_tb) + + sink(_make_loguru_message(exception=exception)) + record = handler.emit.call_args.args[0] + assert record.exc_info is not None + assert record.exc_info[1] is exc_value + + +@pytest.mark.unit +class TestInstrumentFastapi: + """Behavioural tests for instrument_fastapi().""" + + def test_instrument_fastapi_returns_false_when_package_absent(self) -> None: + """Returns False when opentelemetry-instrumentation-fastapi is not importable.""" + with patch("aignostics_foundry_core.otel.find_spec", return_value=None): + result = instrument_fastapi(MagicMock()) + assert result is False + + def test_instrument_fastapi_returns_true_and_instruments_app(self) -> None: + """Returns True and calls FastAPIInstrumentor.instrument_app with the given app.""" + mock_app = MagicMock() + with patch("opentelemetry.instrumentation.fastapi.FastAPIInstrumentor.instrument_app") as mock_instrument: + result = instrument_fastapi(mock_app) + assert result is True + mock_instrument.assert_called_once_with(mock_app) diff --git a/uv.lock b/uv.lock index afac137..947ba53 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,11 @@ version = 1 revision = 3 requires-python = ">=3.11, <3.15" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version < '3.13'", +] [manifest] overrides = [ @@ -21,6 +26,9 @@ dependencies = [ { name = "httpx2" }, { name = "loguru" }, { name = "nicegui" }, + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-instrumentation-fastapi" }, + { name = "opentelemetry-sdk" }, { name = "platformdirs" }, { name = "psutil" }, { name = "pydantic" }, @@ -78,6 +86,9 @@ requires-dist = [ { name = "httpx2", specifier = ">=2.2.0,<3" }, { name = "loguru", specifier = ">=0.7,<1" }, { name = "nicegui", specifier = ">=3,<4" }, + { name = "opentelemetry-exporter-otlp-proto-grpc", specifier = ">=1.27,<2" }, + { name = "opentelemetry-instrumentation-fastapi", specifier = ">=0.48b0,<1" }, + { name = "opentelemetry-sdk", specifier = ">=1.27,<2" }, { name = "platformdirs", specifier = ">=4,<5" }, { name = "psutil", specifier = ">=6" }, { name = "pydantic", specifier = ">=2,<3" }, @@ -336,6 +347,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", size = 68797, upload-time = "2025-10-18T17:46:45.663Z" }, ] +[[package]] +name = "asgiref" +version = "3.11.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/40/f03da1264ae8f7cfdbf9146542e5e7e8100a4c66ab48e791df9a03d3f6c0/asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce", size = 38550, upload-time = "2026-02-03T13:30:14.33Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" }, +] + [[package]] name = "asyncpg" version = "0.31.0" @@ -1232,6 +1252,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] +[[package]] +name = "googleapis-common-protos" +version = "1.75.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, +] + [[package]] name = "greenlet" version = "3.5.1" @@ -1289,6 +1321,57 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/57/f3/97dfcf4a6eb5077f8a672234216fb5923eb89f2cab7081cb10b2cf75b605/greenlet-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:45718441607f9325d948db98cbc691276059316d0358c188c246da4e1d4d23d2", size = 245246, upload-time = "2026-05-20T13:12:22.646Z" }, ] +[[package]] +name = "grpcio" +version = "1.82.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/19/e29d3979b420b92d516ee97f0bff4fb88cbb3d724791318f50beb55db549/grpcio-1.82.0.tar.gz", hash = "sha256:bfe3247e0bb598585ce26565730970d21bccf3c9dc547dc6703a1a3c7888e8e1", size = 13184479, upload-time = "2026-07-06T04:22:54.421Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ed/dfba8843c9e0cbf5b3ad33998fab400a6290e29432c5a2c94e684ebbfadf/grpcio-1.82.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:64be5bea5f32b1b13c81d8f322bd49ab22d388b87b5c146050c4faf2769c2036", size = 6181469, upload-time = "2026-07-06T04:21:04.208Z" }, + { url = "https://files.pythonhosted.org/packages/ce/41/fe3cc2de8ccdf0b6a8a9939d731c8bdab37f80c96581237de359baa37bbe/grpcio-1.82.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:03c0f3085e30e51aa4592a2a0b8ee518c9fb1593eb0368f57d7ccfdda2a1cff1", size = 11971108, upload-time = "2026-07-06T04:21:06.634Z" }, + { url = "https://files.pythonhosted.org/packages/27/49/896d665121bd0a806d62a5bacf93c0db2f7097b29bc52bfab76141589f68/grpcio-1.82.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bc428d9825f1f83e61c65c8e6fb662384105022231938d6775e1b6cfb2bf517a", size = 6760133, upload-time = "2026-07-06T04:21:09.779Z" }, + { url = "https://files.pythonhosted.org/packages/78/1a/49739ab1be3f66106f54af46da599f2be9fdb79d4fc52e4d5a43a73cfc92/grpcio-1.82.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4a9389c1b3568b1d6b8c1d85d43eea701814e59280dd955ea0a2d2d65b90cb24", size = 7484373, upload-time = "2026-07-06T04:21:11.942Z" }, + { url = "https://files.pythonhosted.org/packages/d4/a9/53cc88d792b318ec8ed924791cde109096a4174e6e2c115231ece5b69a9a/grpcio-1.82.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:848774370f0783f88e1fa9888f972fa61ab1ec985fffe2668f53bccfa51ef6cb", size = 6924263, upload-time = "2026-07-06T04:21:14.864Z" }, + { url = "https://files.pythonhosted.org/packages/f3/23/d4ea910fcfbd62ad4a7efde95f0fbfec9fe99595be9b58a1a6d67c57c4ff/grpcio-1.82.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:743390c02fb9b565351fc4429b3385c84d851626df11d91a537636b02eaa67df", size = 7531845, upload-time = "2026-07-06T04:21:17.674Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a6/bc6503929c332ae9c74ccf74c57fb67c6b2d9e6ed27bfddb0888d812df52/grpcio-1.82.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b53959e9af6618b10b46dba4ac82706c4ee90443e95a28004c014c2532d7e053", size = 8568207, upload-time = "2026-07-06T04:21:19.868Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a5/7529b811efd1cb3181bc789d22ec9403b27aa3d3d8acb0b122987036143a/grpcio-1.82.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8045b89d863b5a271e65413361f75dddd70eeab6bf79f2b0e0eaeeb0900d8d24", size = 7938767, upload-time = "2026-07-06T04:21:22.597Z" }, + { url = "https://files.pythonhosted.org/packages/5f/b3/1fa12730f95458fba4319b7935fc4c02a604404ea27f74756c51bf73a263/grpcio-1.82.0-cp311-cp311-win32.whl", hash = "sha256:24310aaf1f96a5327fc0d8e00fc98f0e9f90be8e09cc51becd2553d4b823d286", size = 4256371, upload-time = "2026-07-06T04:21:25.157Z" }, + { url = "https://files.pythonhosted.org/packages/60/10/71202ae4c1cade358a07436c010c145210bc2a730a731e5d297a2dac4f6e/grpcio-1.82.0-cp311-cp311-win_amd64.whl", hash = "sha256:ddcd5f8db890c5c822e4c0b89d641b1db6a0c7255fff02535cbe77c4dabf450f", size = 5009634, upload-time = "2026-07-06T04:21:27.282Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e6/9ce68bc431224177b6f506cfb4896a742119c51255143069970955b6510b/grpcio-1.82.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:03cce11532da292215cd8e5f444de91982e39dfb41a916e0e0f91a5c128588ea", size = 6144680, upload-time = "2026-07-06T04:21:29.808Z" }, + { url = "https://files.pythonhosted.org/packages/59/93/00ecf28e1be7a70b4b4d148d3a551b6c9a37024a669c3650a2c374c64026/grpcio-1.82.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:01336fe31d1ea5d5154b0d803eb3584e69fb96e0e657acc87bd4d48d6abc9587", size = 11952213, upload-time = "2026-07-06T04:21:32.347Z" }, + { url = "https://files.pythonhosted.org/packages/38/05/9be6bf4cddb5b5f39c0748cdf5639525cd4116a157c8f3802c9217e54882/grpcio-1.82.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe56f1bea709ddb2531fbd510a2dd6040e993985507c3b2bf3404507aee989b1", size = 6710770, upload-time = "2026-07-06T04:21:35.382Z" }, + { url = "https://files.pythonhosted.org/packages/00/97/62c0969e993673ebef16d526db2504f38bc4c73cf2593c28746791ab7956/grpcio-1.82.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:77a5d8fe331376c7aaa4e06e903a43bd144de4f98c6e414e13cbc5d64e5aa61e", size = 7450671, upload-time = "2026-07-06T04:21:37.923Z" }, + { url = "https://files.pythonhosted.org/packages/67/1f/d83d9fbaaef2b6ebbf70a6e467000f13f92b4cf0b84c561c162b43128439/grpcio-1.82.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9ce80ea66c803398fde9c5b1fd925920c9742da7f10f8b85acac0adac3e4d7e0", size = 6886855, upload-time = "2026-07-06T04:21:40.527Z" }, + { url = "https://files.pythonhosted.org/packages/c5/36/5ed2e28b8c5f00599c7d5e94d5f82c32cf73a6fa42d13c2900cb37c861ec/grpcio-1.82.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4665dc8c9ec30acff455e2b15c47b825f18647c555483aa1ccf670adead8adcc", size = 7501322, upload-time = "2026-07-06T04:21:43.78Z" }, + { url = "https://files.pythonhosted.org/packages/89/4e/69113709e14e24289940d6aef067b797c73c8c96f580683af7d5f09b22b0/grpcio-1.82.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4405d19ecfc7a8caa9043ff5a651e1871b54fc620f917de5dede7e6fb78e9e14", size = 8536896, upload-time = "2026-07-06T04:21:46.387Z" }, + { url = "https://files.pythonhosted.org/packages/30/f6/08bd6eb89a558f0f59dacaef747afda7c21e2c2ecf138ae2ec533dea8aeb/grpcio-1.82.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:30e580c19178609799660f52b1e1eb09248969be20dc44caaa4dcaa3e8450dd9", size = 7913890, upload-time = "2026-07-06T04:21:49.733Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a1/04b467e2ffc93f8e50b660bc6f47c3c8e9ba7603104a741e3d5663a261d4/grpcio-1.82.0-cp312-cp312-win32.whl", hash = "sha256:dbbd83dbaa856b387a4eba74ec3add7f594f090d3b4d390d829dd5834816aae4", size = 4240969, upload-time = "2026-07-06T04:21:52.281Z" }, + { url = "https://files.pythonhosted.org/packages/25/3c/15d3601384d5937e8d9fbe6d8e7b8171008d649744dd7b6c864932937ec9/grpcio-1.82.0-cp312-cp312-win_amd64.whl", hash = "sha256:73176d270699d76a9dc3753f25e01c19fb98f830f826a506e917d83629f1b39b", size = 5001575, upload-time = "2026-07-06T04:21:54.839Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c8/7aace81e7cd12c6ffccf0d47ac389a3f19e5280e9ab04d301d02855ecd25/grpcio-1.82.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:841bcaa24882921d41cd7a82118f9a766edcf8807c2f486e7deeb0ff5ea36181", size = 6146066, upload-time = "2026-07-06T04:21:57.527Z" }, + { url = "https://files.pythonhosted.org/packages/10/17/b448f2265e4927188425c0b09fb943c39b50f7f53fbead644b44ccdf834b/grpcio-1.82.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:d4bb7219c39c735e6573ae3c33aeafb2a4e1f1cc93a762f7899c283defaed365", size = 11948617, upload-time = "2026-07-06T04:22:00.066Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a3/fba54e50fffc9f74b1ffd5eb4e47310e6650557ef136a165f1fe7df03a65/grpcio-1.82.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:254230d2cb773a87380858d0fc74653b4761bf2013c83fe1c5d77ed8f241fc9d", size = 6714591, upload-time = "2026-07-06T04:22:04.04Z" }, + { url = "https://files.pythonhosted.org/packages/29/6f/f0ec3f1a61a746dc7037a737ba4cbe60959645311ac8542bb1ad4e72ae8b/grpcio-1.82.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:16d4f215344057eb21038319b05a0c531306230c17b075fa8461a944581006dd", size = 7454986, upload-time = "2026-07-06T04:22:06.776Z" }, + { url = "https://files.pythonhosted.org/packages/b6/11/9d6ce94465d6a7f92c413430b3e77f0879b39427a217ffb3d23585df4bb3/grpcio-1.82.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a7c283b459151a2e84df32b28c31562ab3e7f624bccffce176c5608565b0212f", size = 6888623, upload-time = "2026-07-06T04:22:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/51/1a/b887adb7abb69081c4f24bc66c3612975e87caf5f7c6aa3916bb82d42e5f/grpcio-1.82.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:96a3be4d2a482ff004c036f6391f33553b36a7627138d1ca3ecc1e70d55d8af5", size = 7505067, upload-time = "2026-07-06T04:22:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/60/94/90f11c0f227ac653cff769e05c7f279da945e134c9351a1c37d475714686/grpcio-1.82.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bc481e6c7e6c8721797f2ab764092c843fd5cea06d4c7e9f4c3e3b7ecf331eb0", size = 8535382, upload-time = "2026-07-06T04:22:14.453Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3d/de8569386685213135dfbaab58e221add11858485524cf7d5987efc6d70b/grpcio-1.82.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:680f633bc788652222cb90a568ec374edcd91e1fe7715a7c248ece8e1032c2bd", size = 7910708, upload-time = "2026-07-06T04:22:17.994Z" }, + { url = "https://files.pythonhosted.org/packages/42/ab/3950b32369763818ae89ef76d52e84bd034d3a0ab46bf315669c913b32e2/grpcio-1.82.0-cp313-cp313-win32.whl", hash = "sha256:640b4715b9c206e5176e46601aa1422c47c779f642a869dfd27062e8fde13dfb", size = 4240351, upload-time = "2026-07-06T04:22:20.626Z" }, + { url = "https://files.pythonhosted.org/packages/a7/5c/c7b9a48efe973883f5707a311f87772a4c307a22ec80d6f3c851846a0a02/grpcio-1.82.0-cp313-cp313-win_amd64.whl", hash = "sha256:8523e81bd23f83e607aed28fbd8244b2be4aeb34e084fcef1bd1867709085c2a", size = 5000985, upload-time = "2026-07-06T04:22:23.365Z" }, + { url = "https://files.pythonhosted.org/packages/bd/6e/abc5efe11b73eefa0183531032eef2a53f33aeced6484b0d9da559c12ed6/grpcio-1.82.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:136969867c14fcc743fa39b12d7da133cae7da4f8e0e7767b6b8b7e75a8c24fd", size = 6146898, upload-time = "2026-07-06T04:22:26.082Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e4/d01ee88ceac221436dce1584ae1f046bd44d0dffc5a92e95aa34857c4516/grpcio-1.82.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:a7e246cd216662f513ae79ea3afadde6afe8a659b48a4170bae9a896e69ac254", size = 11954897, upload-time = "2026-07-06T04:22:29.114Z" }, + { url = "https://files.pythonhosted.org/packages/99/b9/2a35540ec08afc08859c35afd1a8a51e2585f39b12cf4eee68dcb1fa973d/grpcio-1.82.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:85c2a4e9e8a30d73d41e547a5101e57a1ed2093ec4b2daf6847c344adba00523", size = 6723102, upload-time = "2026-07-06T04:22:32.146Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ba/af71af59943b5d08879617a1d97495d45c53e43f3c6ef16f820c64e107f9/grpcio-1.82.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:a3a7802328e0d20e6ac458ff1974a8096cfaa3ca84dea0903235f641f1d703c1", size = 7454545, upload-time = "2026-07-06T04:22:34.776Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7e/258aae12b68910140725873657469ff166f8968bdb0674e12d6452b1812c/grpcio-1.82.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9331e6b91b26cffd63fb70a545f3c2cac8dc1b434b4436f43915c23e1e22f265", size = 6889585, upload-time = "2026-07-06T04:22:37.411Z" }, + { url = "https://files.pythonhosted.org/packages/5e/04/cfada8930306b9101cfb405b33ecb3b72abce51d725f900f167ffbc3146b/grpcio-1.82.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:670ab93a0059e707de53b9e715304b9f566b6defee80e5490cdd15820ce032f6", size = 7514162, upload-time = "2026-07-06T04:22:40.076Z" }, + { url = "https://files.pythonhosted.org/packages/2e/57/955f95989338857a6d73ee838b4d9e8198d50972a70ca83ec22322396f4c/grpcio-1.82.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbfe2e321bb30b84799677d6e8469896e487b494e9e4fbc9b8cc4425fe054d44", size = 8536163, upload-time = "2026-07-06T04:22:42.801Z" }, + { url = "https://files.pythonhosted.org/packages/06/62/826da0b0f01c7ba3f7ea0185968f551290deb28968d98b1edf604c895db8/grpcio-1.82.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2472b72a55da8570e089f1ca22f34a350d86c109be2755f6ee1cca5ebbdd9b54", size = 7912573, upload-time = "2026-07-06T04:22:46.187Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b4/d8824a66ac1ac7fbcdf63806f1a8725d95426654e859afa4606070c78740/grpcio-1.82.0-cp314-cp314-win32.whl", hash = "sha256:c9feb986151c2726789599b3672c1c9faa1d0824da921dc3f33a8cb1f93e2861", size = 4321824, upload-time = "2026-07-06T04:22:48.866Z" }, + { url = "https://files.pythonhosted.org/packages/da/08/02e581cc0bbb4c7b842f85e66a945b46d5bcc5927575c1086edfbc3360e7/grpcio-1.82.0-cp314-cp314-win_amd64.whl", hash = "sha256:b1eeb0480d86965263f8c8ae7ee5360c284d22176a196aab02fce7fba8d89dd8", size = 5141112, upload-time = "2026-07-06T04:22:51.443Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -2047,6 +2130,143 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/95/4df134a100b5a9a12378d5301b934366686ef6fbdaffcd21211d5654970e/nox-2026.4.10-py3-none-any.whl", hash = "sha256:082c117627590d9b90aa21f86df89b310b07c5842539524203bcb3c719f116c1", size = 75536, upload-time = "2026-04-10T17:42:40.664Z" }, ] +[[package]] +name = "opentelemetry-api" +version = "1.43.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/cc/e4c9584181f86494df0f6bdec1a4f3280c50db44704dc2a407e994fc87bb/opentelemetry_api-1.43.0.tar.gz", hash = "sha256:107d0d03857ea8fc7c5fcbbbd83f800c281f0d560553d61c1d675fccfd1761c1", size = 73476, upload-time = "2026-06-24T15:19:55.323Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/83/6dba32b85f31868400440dc7ad2ca1eab94cbbf3a7b0459ed39f8311a9e2/opentelemetry_api-1.43.0-py3-none-any.whl", hash = "sha256:20acf45e9b21851926835292e4045d290acade1edd2ff3de86d2f069687ba1fd", size = 61912, upload-time = "2026-06-24T15:19:35.434Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.43.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/c1/e8098490ab15abf116dcaf9fa89ededcb35547c7d08d4b5a62f573dc1e63/opentelemetry_exporter_otlp_proto_common-1.43.0.tar.gz", hash = "sha256:c4e32ba6d6b13bdb2b8f6764c4fd28d00192826561aa04f6d14eedfce7ac076f", size = 20197, upload-time = "2026-06-24T15:20:00.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/b2/41ebc74ae1d5859901f1b69305de58724bf043381103d6ef413521cbc35a/opentelemetry_exporter_otlp_proto_common-1.43.0-py3-none-any.whl", hash = "sha256:123c3f9cc87218562490c63b36f497bf3a722faf174a515d1443f31ababa6264", size = 17048, upload-time = "2026-06-24T15:19:41.264Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.43.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/1d/6336453716ca0a240d4417d19e6d5b77a5e7163e5670ec4f7ec4d3ede7bf/opentelemetry_exporter_otlp_proto_grpc-1.43.0.tar.gz", hash = "sha256:1b3e0627daa9bc21884d4a13946807c255eb558bfe5bdd543dffb6f4c9faee0d", size = 27213, upload-time = "2026-06-24T15:20:00.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/74/2700b5d5c946bf2dba87073fce3dfc198c46bc92ea3d5693f54bc51c90b1/opentelemetry_exporter_otlp_proto_grpc-1.43.0-py3-none-any.whl", hash = "sha256:6a10d1feacffffda19acacbf277b736094b1e2f4dbb98c90ccb2c6e1962e2ec6", size = 19626, upload-time = "2026-06-24T15:19:42.233Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.64b0" +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/7e/97/02fe6e1c8b1ffac42d0b429c18080edb24e0e0d18c86612edf72b5752382/opentelemetry_instrumentation-0.64b0.tar.gz", hash = "sha256:b47d528dead6271d7743114417eb67fc915bd9258111c48dbf9a4951d2efa88d", size = 41935, upload-time = "2026-06-24T15:19:12.951Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/0c/cb9fe342de5299c7af24582eb7d788661cc53a1c4b904da92309caaa9417/opentelemetry_instrumentation-0.64b0-py3-none-any.whl", hash = "sha256:133ab7ffca796557aec059bf6be3190a34b6dea987f25be3d9409e230cbdad8b", size = 35880, upload-time = "2026-06-24T15:18:17.277Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-asgi" +version = "0.64b0" +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/85/0c/71c696fccb86d37af383ea1604af4729fabad0af2fabaf203e4c79c1e859/opentelemetry_instrumentation_asgi-0.64b0.tar.gz", hash = "sha256:4dd3eee566a4303f8e6b9b84f2a0a7abc57a6640df768926c68a3868bf5b2090", size = 26136, upload-time = "2026-06-24T15:19:17.003Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/20/218b65a63d847a7ed28d1bea84c39234689160b74480b8702272e37f4240/opentelemetry_instrumentation_asgi-0.64b0-py3-none-any.whl", hash = "sha256:e0840b66e15303a9254b0540946010bd008aa0504f4d89b8e1b7fb63490a36f0", size = 15906, upload-time = "2026-06-24T15:18:23.107Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-fastapi" +version = "0.64b0" +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/db/a1/52282e2cc5c08f4df13b087896d9907258fe2ff4f34035d3b21aa92684c3/opentelemetry_instrumentation_fastapi-0.64b0.tar.gz", hash = "sha256:05f75149929e433c1630de381688e650bf651c1e1cce7f9a7b649a807dac8a98", size = 26236, upload-time = "2026-06-24T15:19:27.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/2d/4f48dc2d6f289f2febc5b871940dbea9f3b04ab9186d23342581b49cb984/opentelemetry_instrumentation_fastapi-0.64b0-py3-none-any.whl", hash = "sha256:43cbbfb2d3079dc81104478a2950ae93ac6d0e90a5020fa3987a236f8f2bdef1", size = 13263, upload-time = "2026-06-24T15:18:38.553Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.43.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/b9/d357faefb40bda1d4799913e6af611171ff22a2dedcb93576bc92242d056/opentelemetry_proto-1.43.0.tar.gz", hash = "sha256:224778df17e1f3fafeaaa21d874236ca5f6ffc2f86e0899298ec7351aac27924", size = 46481, upload-time = "2026-06-24T15:20:07.625Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/a7/3e5308cf548b8f72529c7db1afdb3a404211982376a12927fd7759f77bf3/opentelemetry_proto-1.43.0-py3-none-any.whl", hash = "sha256:c58f1f7ef84bc7dc2834016c0c37fe0081dde7ca9f6339be1970fbf9cdaaa90d", size = 72489, upload-time = "2026-06-24T15:19:51.164Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.43.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/3e/eb/5041074274ac0956b03637cc039d434569112468e875eddfcc9a0674ce06/opentelemetry_sdk-1.43.0.tar.gz", hash = "sha256:d8187c81c162df9913e4003dd6485f7390d9a24fc17026ec7387b8b8218b08e9", size = 254744, upload-time = "2026-06-24T15:20:08.467Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/e3/b17be23af124201c9f52eececd4cc8ddfed1597d37b4ee771895d325805c/opentelemetry_sdk-1.43.0-py3-none-any.whl", hash = "sha256:d1323a547c1ce69d6a069a17a44b7da82bb8b332051ecb074041f87642c86823", size = 178852, upload-time = "2026-06-24T15:19:52.169Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.64b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/30/5f26df29509eccd86b99b481ac9ffa39da49ba9577cc69071c552ae30447/opentelemetry_semantic_conventions-0.64b0.tar.gz", hash = "sha256:72f76fb2d1582d9d033dd1fcd84532e961e6ff3d90d24ba6fabc72975a83864c", size = 148340, upload-time = "2026-06-24T15:20:09.267Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/ca/23ba87a221b574a7c5a99d48849d80bfe8b047624681357e2b002e566187/opentelemetry_semantic_conventions-0.64b0-py3-none-any.whl", hash = "sha256:ea77e85e354b8f604ddbe5f3d9135216f982fa4d77e5859ac30f6d8a50505aa6", size = 203713, upload-time = "2026-06-24T15:19:53.339Z" }, +] + +[[package]] +name = "opentelemetry-util-http" +version = "0.64b0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/1b/1029a805fd7242f7dfce91633b244c3b14a94d703232878f71e01ce862b1/opentelemetry_util_http-0.64b0.tar.gz", hash = "sha256:8a86a220dbfc56d736f47f1e5c4e7932a21fcf69052312e1bcf166444dc79322", size = 11102, upload-time = "2026-06-24T15:19:48.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/c7/5f8ec5b30546f2dc22cd5fc5759bce2ab5be6e89a2e710a405ac9ef64ed3/opentelemetry_util_http-0.64b0-py3-none-any.whl", hash = "sha256:c1e5350d25507c1afcd6076cf9ac062485a0a4f79cd9971366996fd3056bacdb", size = 8204, upload-time = "2026-06-24T15:19:09.02Z" }, +] + [[package]] name = "orjson" version = "3.11.9" @@ -2375,6 +2595,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, ] +[[package]] +name = "protobuf" +version = "7.35.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a", size = 458717, upload-time = "2026-06-11T21:55:40.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6", size = 433226, upload-time = "2026-06-11T21:55:31.719Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799", size = 328847, upload-time = "2026-06-11T21:55:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4", size = 344030, upload-time = "2026-06-11T21:55:34.941Z" }, + { url = "https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4", size = 327130, upload-time = "2026-06-11T21:55:35.921Z" }, + { url = "https://files.pythonhosted.org/packages/d8/bc/6d6c7ba8709c85f8f2c390b2b118d6fb08a783676a572271851bf45a7d22/protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30", size = 428945, upload-time = "2026-06-11T21:55:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87", size = 439996, upload-time = "2026-06-11T21:55:38.123Z" }, + { url = "https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659, upload-time = "2026-06-11T21:55:39.155Z" }, +] + [[package]] name = "psutil" version = "7.2.2"