From 766365e9f41735d5c87ec9da2094d5a2d7839f88 Mon Sep 17 00:00:00 2001 From: Rodion Mostovoi <36400912+rodion-m@users.noreply.github.com> Date: Wed, 22 Jul 2026 21:58:36 +0500 Subject: [PATCH 1/3] Add privacy-safe OpenTelemetry tracing --- pyproject.toml | 1 + src/core/logging.py | 15 +- src/core/observability.py | 138 +++++++++++++-- src/middleware/observability_middleware.py | 26 ++- src/tests/test_logging.py | 30 ++++ src/tests/test_observability.py | 193 ++++++++++++++++----- src/tests/test_observability_middleware.py | 46 ++++- uv.lock | 43 +++++ 8 files changed, 428 insertions(+), 64 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f1875fa..844fb55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ "opentelemetry-sdk==1.43.0", "opentelemetry-exporter-otlp-proto-http==1.43.0", "opentelemetry-instrumentation-httpx==0.64b0", + "opentelemetry-instrumentation-starlette==0.64b0", "opentelemetry-semantic-conventions==0.64b0", ] diff --git a/src/core/logging.py b/src/core/logging.py index e026733..54dc308 100644 --- a/src/core/logging.py +++ b/src/core/logging.py @@ -6,6 +6,7 @@ """ import logging +import os import sys import uuid from typing import Any, Dict, List, Optional, Tuple, Union @@ -115,13 +116,23 @@ def setup_logging(debug: bool = False) -> None: # Intercept stdlib logging logging.basicConfig(handlers=[_InterceptHandler()], level=0, force=True) + # FastMCP validation and exception records can contain rejected argument + # values. In production its logger is disabled in favor of the privacy-safe + # CodeAlive middleware logs. Stop propagation explicitly because FastMCP's + # disabled setting otherwise leaves child loggers attached to the root. + fastmcp_logger = logging.getLogger("fastmcp") + if os.environ.get("FASTMCP_LOG_ENABLED", "").lower() in {"false", "0", "no"}: + fastmcp_logger.handlers.clear() + fastmcp_logger.addHandler(logging.NullHandler()) + fastmcp_logger.propagate = False + else: + fastmcp_logger.propagate = True + logger.info("Logging initialized at {level} level", level=_current_level) def setup_debug_logging() -> bool: """Backward-compatible helper: enable debug logging if ``DEBUG_MODE`` env is set.""" - import os - if os.environ.get("DEBUG_MODE", "").lower() in ["true", "1", "yes"]: setup_logging(debug=True) return True diff --git a/src/core/observability.py b/src/core/observability.py index 838b571..0ab6d8a 100644 --- a/src/core/observability.py +++ b/src/core/observability.py @@ -1,47 +1,150 @@ """OpenTelemetry setup for CodeAlive MCP server. -Initialises a ``TracerProvider`` with an OTLP/HTTP exporter when the -``OTEL_EXPORTER_OTLP_ENDPOINT`` env var is set. Otherwise tracing is -configured as a no-op so the rest of the code can call ``trace.get_tracer()`` -unconditionally. +Initialises a ``TracerProvider`` with an OTLP/HTTP exporter when either the +generic or traces-specific OTLP endpoint is configured. Otherwise tracing is +configured without an exporter so the rest of the code can call +``trace.get_tracer()`` unconditionally. -HTTPX client instrumentation is always enabled so outbound HTTP calls -automatically get ``traceparent`` headers injected. +Starlette and HTTPX instrumentation connect inbound MCP requests to outbound +CodeAlive API calls without recording request or response bodies. """ import atexit import os +from collections.abc import Sequence from loguru import logger from opentelemetry import trace from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor +from opentelemetry.instrumentation.starlette import StarletteInstrumentor from opentelemetry.sdk.resources import Resource -from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace import Event, ReadableSpan, TracerProvider +from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult +from opentelemetry.trace import Status _SERVICE_NAME = "codealive-mcp" +_SENSITIVE_ATTRIBUTE_PREFIXES = ( + "enduser.", + "http.request.header.", + "http.response.header.", +) +_SENSITIVE_ATTRIBUTES = { + "client.address", + "http.url", + "mcp.session.id", + "mcp.resource.uri", + "network.peer.address", + "url.full", + "url.query", + "user_agent.original", +} + + +class _SanitizingSpanExporter(SpanExporter): + """Remove client data added by framework auto-instrumentation before export.""" + + def __init__(self, delegate: SpanExporter) -> None: + self._delegate = delegate + + def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: + return self._delegate.export(tuple(self._sanitize(span) for span in spans)) + + def shutdown(self) -> None: + self._delegate.shutdown() + + def force_flush(self, timeout_millis: int = 30000) -> bool: + return self._delegate.force_flush(timeout_millis) + + @staticmethod + def _sanitize(span: ReadableSpan) -> ReadableSpan: + attributes = { + key: value + for key, value in span.attributes.items() + if key not in _SENSITIVE_ATTRIBUTES + and not key.startswith(_SENSITIVE_ATTRIBUTE_PREFIXES) + } + events = tuple( + Event( + event.name, + {"exception.type": event.attributes["exception.type"]} + if event.name == "exception" + and event.attributes + and "exception.type" in event.attributes + else {}, + event.timestamp, + ) + for event in span.events + ) + return ReadableSpan( + name=span.name, + context=span.context, + parent=span.parent, + resource=span.resource, + attributes=attributes, + events=events, + links=span.links, + kind=span.kind, + status=Status(span.status.status_code), + start_time=span.start_time, + end_time=span.end_time, + instrumentation_scope=span.instrumentation_scope, + ) + + +def _resource_attributes() -> dict[str, str]: + """Build low-cardinality resource identity from deployment metadata only.""" + attributes = { + "service.name": os.environ.get("OTEL_SERVICE_NAME", _SERVICE_NAME), + "k8s.container.name": "mcp-server", + } + optional_attributes = { + "service.version": os.environ.get("CODEALIVE_MCP_VERSION"), + "service.instance.id": os.environ.get("POD_NAME") + or os.environ.get("HOSTNAME"), + "deployment.environment.name": os.environ.get("DEPLOYMENT_ENVIRONMENT") + or os.environ.get("ENVIRONMENT"), + "k8s.namespace.name": os.environ.get("POD_NAMESPACE"), + "k8s.pod.name": os.environ.get("POD_NAME"), + "k8s.node.name": os.environ.get("NODE_NAME"), + } + attributes.update( + {key: value for key, value in optional_attributes.items() if value} + ) + return attributes + def init_tracing() -> None: """Bootstrap OpenTelemetry tracing. - * If ``OTEL_EXPORTER_OTLP_ENDPOINT`` is set, traces are exported via - OTLP/HTTP (protobuf) to that endpoint. - * Otherwise a no-op provider is configured (zero overhead). + * If a generic or traces-specific OTLP endpoint is set, traces are exported + via OTLP/HTTP (protobuf). The exporter reads the standard OTel env vars so + it can apply the correct ``/v1/traces`` path semantics. + * Otherwise a provider without an exporter is configured (no network I/O). * HTTPX client instrumentation is always enabled so that ``traceparent`` propagates to the CodeAlive backend regardless of whether traces are exported. """ - otlp_endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT") + otlp_endpoint = os.environ.get( + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT" + ) or os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT") - resource = Resource.create({"service.name": _SERVICE_NAME}) + resource = Resource.create(_resource_attributes()) if otlp_endpoint: - from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter + from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( + OTLPSpanExporter, + ) from opentelemetry.sdk.trace.export import BatchSpanProcessor - exporter = OTLPSpanExporter(endpoint=otlp_endpoint) + # Do not pass the endpoint explicitly. The exporter distinguishes the + # signal-specific URL from the generic base URL and appends /v1/traces + # only where the OTel environment-variable contract requires it. + exporter = OTLPSpanExporter() provider = TracerProvider(resource=resource) - provider.add_span_processor(BatchSpanProcessor(exporter)) + provider.add_span_processor( + BatchSpanProcessor(_SanitizingSpanExporter(exporter)) + ) trace.set_tracer_provider(provider) logger.info( @@ -59,5 +162,8 @@ def init_tracing() -> None: # Flush pending spans on process exit atexit.register(provider.shutdown) - # Auto-instrument httpx so outbound requests carry traceparent + # Instrument before FastMCP creates its Starlette app. Neither integration + # captures bodies by default; health endpoints are excluded through the + # standard OTEL_PYTHON_STARLETTE_EXCLUDED_URLS deployment setting. + StarletteInstrumentor().instrument() HTTPXClientInstrumentor().instrument() diff --git a/src/middleware/observability_middleware.py b/src/middleware/observability_middleware.py index e2eab12..a8b1d04 100644 --- a/src/middleware/observability_middleware.py +++ b/src/middleware/observability_middleware.py @@ -4,7 +4,7 @@ - ``gen_ai.operation.name`` = ``"execute_tool"`` - ``gen_ai.tool.name`` = tool name - ``mcp.tool.name`` = tool name (MCP-specific alias) - - ``mcp.method`` = ``"tools/call"`` + - ``mcp.method.name`` = ``"tools/call"`` The middleware also injects ``trace_id`` into loguru context via ``logger.contextualize`` so that every log emitted during the tool @@ -60,7 +60,27 @@ def _extract_tool_arguments(context: "MiddlewareContext") -> dict[str, Any]: class ObservabilityMiddleware(Middleware): - """Wrap each ``tools/call`` in an OTel span and log its outcome.""" + """Trace MCP requests and nested tool execution without recording payloads.""" + + async def on_request(self, context: "MiddlewareContext", call_next: "CallNext"): + method = context.method or "unknown" + with _tracer.start_as_current_span( + f"mcp {method}", + record_exception=False, + set_status_on_exception=False, + attributes={"mcp.method.name": method}, + ) as span: + try: + result = await call_next(context) + except Exception as exc: + error_type = type(exc).__name__ + span.set_attribute("error.type", error_type) + span.set_status(StatusCode.ERROR, error_type) + span.add_event("exception", {"exception.type": error_type}) + raise + + span.set_status(StatusCode.OK) + return result async def on_call_tool(self, context: "MiddlewareContext", call_next: "CallNext"): tool_name = getattr(context.message, "name", "unknown") @@ -75,7 +95,7 @@ async def on_call_tool(self, context: "MiddlewareContext", call_next: "CallNext" "gen_ai.operation.name": "execute_tool", "gen_ai.tool.name": tool_name, "mcp.tool.name": tool_name, - "mcp.method": "tools/call", + "mcp.method.name": "tools/call", }, ) as span: # Inject trace_id into loguru so every log inside the tool carries it diff --git a/src/tests/test_logging.py b/src/tests/test_logging.py index bbe063e..6aeebca 100644 --- a/src/tests/test_logging.py +++ b/src/tests/test_logging.py @@ -2,6 +2,7 @@ import io import json +import logging import sys from unittest.mock import MagicMock @@ -106,6 +107,35 @@ def test_setup_logging_sets_level(self): logger.remove(handler_id) + def test_fastmcp_logs_do_not_propagate_when_disabled(self, monkeypatch, capsys): + monkeypatch.setenv("FASTMCP_LOG_ENABLED", "false") + sink = io.StringIO() + setup_logging() + logger.remove() + handler_id = logger.add(sink, level="DEBUG", serialize=True) + + logging.getLogger("fastmcp.server.server").warning( + "Invalid arguments: secret query text" + ) + + assert "secret query text" not in sink.getvalue() + assert "secret query text" not in capsys.readouterr().err + logger.remove(handler_id) + + def test_fastmcp_logs_propagate_by_default_for_self_hosted(self, monkeypatch): + monkeypatch.delenv("FASTMCP_LOG_ENABLED", raising=False) + sink = io.StringIO() + setup_logging() + logger.remove() + handler_id = logger.add(sink, level="DEBUG", serialize=True) + + logging.getLogger("fastmcp.server.server").warning( + "Self-hosted framework diagnostic" + ) + + assert "Self-hosted framework diagnostic" in sink.getvalue() + logger.remove(handler_id) + def test_setup_debug_logging_env_var(self, monkeypatch): monkeypatch.setenv("DEBUG_MODE", "true") assert setup_debug_logging() is True diff --git a/src/tests/test_observability.py b/src/tests/test_observability.py index 9144ff7..8005419 100644 --- a/src/tests/test_observability.py +++ b/src/tests/test_observability.py @@ -1,67 +1,176 @@ """Tests for core.observability — OTel TracerProvider bootstrap.""" import sys -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch import pytest -from opentelemetry import trace +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import Event, ReadableSpan +from opentelemetry.trace import SpanContext, SpanKind, Status, StatusCode, TraceFlags sys.path.insert(0, str(__import__("pathlib").Path(__file__).parent.parent)) -from core.observability import init_tracing, _SERVICE_NAME +from core.observability import _SERVICE_NAME, _SanitizingSpanExporter, init_tracing class TestInitTracing: def test_no_endpoint_creates_provider_without_exporter(self, monkeypatch): monkeypatch.delenv("OTEL_EXPORTER_OTLP_ENDPOINT", raising=False) - - with patch("core.observability.HTTPXClientInstrumentor") as mock_instrumentor: - with patch("core.observability.trace.set_tracer_provider") as mock_set: - init_tracing() - - mock_set.assert_called_once() - provider = mock_set.call_args[0][0] - from opentelemetry.sdk.trace import TracerProvider - assert isinstance(provider, TracerProvider) - mock_instrumentor.return_value.instrument.assert_called_once() - - def test_with_endpoint_creates_otlp_exporter(self, monkeypatch): - monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318") + monkeypatch.delenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", raising=False) + + with patch("core.observability.HTTPXClientInstrumentor") as mock_httpx: + with patch("core.observability.StarletteInstrumentor") as mock_starlette: + with patch("core.observability.trace.set_tracer_provider") as mock_set: + init_tracing() + + mock_set.assert_called_once() + provider = mock_set.call_args[0][0] + from opentelemetry.sdk.trace import TracerProvider + assert isinstance(provider, TracerProvider) + mock_httpx.return_value.instrument.assert_called_once_with() + mock_starlette.return_value.instrument.assert_called_once_with() + + @pytest.mark.parametrize( + "variable", + ["OTEL_EXPORTER_OTLP_ENDPOINT", "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"], + ) + def test_with_endpoint_creates_environment_configured_otlp_exporter( + self, monkeypatch, variable + ): + monkeypatch.delenv("OTEL_EXPORTER_OTLP_ENDPOINT", raising=False) + monkeypatch.delenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", raising=False) + monkeypatch.setenv(variable, "http://localhost:4318") mock_exporter = MagicMock() mock_processor = MagicMock() - with patch("core.observability.HTTPXClientInstrumentor") as mock_instrumentor: - with patch("core.observability.trace.set_tracer_provider"): - with patch( - "opentelemetry.exporter.otlp.proto.http.trace_exporter.OTLPSpanExporter", - return_value=mock_exporter, - ) as mock_exporter_cls: + with patch("core.observability.HTTPXClientInstrumentor"): + with patch("core.observability.StarletteInstrumentor"): + with patch("core.observability.trace.set_tracer_provider"): with patch( - "opentelemetry.sdk.trace.export.BatchSpanProcessor", - return_value=mock_processor, - ) as mock_processor_cls: - init_tracing() - - mock_exporter_cls.assert_called_once_with(endpoint="http://localhost:4318") - mock_processor_cls.assert_called_once_with(mock_exporter) - mock_instrumentor.return_value.instrument.assert_called_once() - - def test_httpx_instrumentor_always_called(self, monkeypatch): + "opentelemetry.exporter.otlp.proto.http.trace_exporter.OTLPSpanExporter", + return_value=mock_exporter, + ) as mock_exporter_cls: + with patch( + "opentelemetry.sdk.trace.export.BatchSpanProcessor", + return_value=mock_processor, + ) as mock_processor_cls: + init_tracing() + + # Let the exporter implement the standard OTel env contract, + # including appending /v1/traces to the generic endpoint. + mock_exporter_cls.assert_called_once_with() + sanitized_exporter = mock_processor_cls.call_args[0][0] + assert isinstance(sanitized_exporter, _SanitizingSpanExporter) + assert sanitized_exporter._delegate is mock_exporter + + def test_transport_instrumentors_always_called(self, monkeypatch): monkeypatch.delenv("OTEL_EXPORTER_OTLP_ENDPOINT", raising=False) + monkeypatch.delenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", raising=False) - with patch("core.observability.HTTPXClientInstrumentor") as mock_instrumentor: - with patch("core.observability.trace.set_tracer_provider"): - init_tracing() - mock_instrumentor.return_value.instrument.assert_called_once() + with patch("core.observability.HTTPXClientInstrumentor") as mock_httpx: + with patch("core.observability.StarletteInstrumentor") as mock_starlette: + with patch("core.observability.trace.set_tracer_provider"): + init_tracing() + mock_httpx.return_value.instrument.assert_called_once_with() + mock_starlette.return_value.instrument.assert_called_once_with() - def test_service_name_in_resource(self, monkeypatch): + def test_resource_uses_safe_environment_metadata(self, monkeypatch): monkeypatch.delenv("OTEL_EXPORTER_OTLP_ENDPOINT", raising=False) + monkeypatch.delenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", raising=False) + monkeypatch.setenv("OTEL_SERVICE_NAME", "custom-mcp") + monkeypatch.setenv("CODEALIVE_MCP_VERSION", "sha-deadbeef") + monkeypatch.setenv("POD_NAME", "mcp-abc") + monkeypatch.setenv("POD_NAMESPACE", "codealive") + monkeypatch.setenv("NODE_NAME", "gke-node-1") + monkeypatch.setenv("ENVIRONMENT", "production") with patch("core.observability.HTTPXClientInstrumentor"): - with patch("core.observability.trace.set_tracer_provider") as mock_set: - init_tracing() + with patch("core.observability.StarletteInstrumentor"): + with patch("core.observability.trace.set_tracer_provider") as mock_set: + init_tracing() + + attrs = dict(mock_set.call_args[0][0].resource.attributes) + assert attrs["service.name"] == "custom-mcp" + assert attrs["service.version"] == "sha-deadbeef" + assert attrs["service.instance.id"] == "mcp-abc" + assert attrs["deployment.environment.name"] == "production" + assert attrs["k8s.namespace.name"] == "codealive" + assert attrs["k8s.pod.name"] == "mcp-abc" + assert attrs["k8s.node.name"] == "gke-node-1" + assert attrs["k8s.container.name"] == "mcp-server" + + def test_service_name_defaults_when_metadata_is_absent(self, monkeypatch): + for variable in ( + "OTEL_EXPORTER_OTLP_ENDPOINT", + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", + "OTEL_SERVICE_NAME", + "CODEALIVE_MCP_VERSION", + "POD_NAME", + "POD_NAMESPACE", + "NODE_NAME", + "ENVIRONMENT", + "DEPLOYMENT_ENVIRONMENT", + ): + monkeypatch.delenv(variable, raising=False) - provider = mock_set.call_args[0][0] - resource_attrs = dict(provider.resource.attributes) - assert resource_attrs["service.name"] == _SERVICE_NAME + with patch("core.observability.HTTPXClientInstrumentor"): + with patch("core.observability.StarletteInstrumentor"): + with patch("core.observability.trace.set_tracer_provider") as mock_set: + init_tracing() + + resource_attrs = dict(mock_set.call_args[0][0].resource.attributes) + assert resource_attrs["service.name"] == _SERVICE_NAME + + +def test_exporter_removes_sensitive_framework_telemetry(): + delegate = MagicMock() + delegate.export.return_value = MagicMock() + exporter = _SanitizingSpanExporter(delegate) + context = SpanContext( + trace_id=1, + span_id=2, + is_remote=False, + trace_flags=TraceFlags.SAMPLED, + ) + span = ReadableSpan( + name="tools/call semantic_search", + context=context, + resource=Resource.create({"service.name": "codealive-mcp"}), + kind=SpanKind.SERVER, + attributes={ + "mcp.method.name": "tools/call", + "gen_ai.tool.name": "semantic_search", + "enduser.id": "client-secret-id", + "mcp.session.id": "session-secret", + "mcp.resource.uri": "repo://secret/path", + "url.full": "https://mcp.example/api?code=oauth-secret", + "url.query": "code=oauth-secret", + "http.request.header.authorization": ("Bearer secret",), + }, + events=( + Event( + "exception", + { + "exception.type": "ValueError", + "exception.message": "secret query text", + "exception.stacktrace": "secret stack", + }, + 1, + ), + ), + status=Status(StatusCode.ERROR, "secret query text"), + ) + + exporter.export((span,)) + + exported = delegate.export.call_args[0][0][0] + assert exported.context == context + assert exported.attributes == { + "mcp.method.name": "tools/call", + "gen_ai.tool.name": "semantic_search", + } + assert exported.status.status_code == StatusCode.ERROR + assert exported.status.description is None + assert exported.events[0].attributes == {"exception.type": "ValueError"} + assert "secret" not in exported.to_json() diff --git a/src/tests/test_observability_middleware.py b/src/tests/test_observability_middleware.py index 890b697..494dec2 100644 --- a/src/tests/test_observability_middleware.py +++ b/src/tests/test_observability_middleware.py @@ -88,7 +88,7 @@ async def test_creates_span_with_correct_attributes(self, otel_setup): assert span.attributes["gen_ai.operation.name"] == "execute_tool" assert span.attributes["gen_ai.tool.name"] == "get_data_sources" assert span.attributes["mcp.tool.name"] == "get_data_sources" - assert span.attributes["mcp.method"] == "tools/call" + assert span.attributes["mcp.method.name"] == "tools/call" @pytest.mark.asyncio async def test_span_status_ok_on_success(self, otel_setup): @@ -143,6 +143,50 @@ async def test_lifecycle_logs_only_include_tool_argument_shape(self, otel_setup) assert tool_arguments["identifier"] not in str(lifecycle) +class TestMcpRequest: + @pytest.mark.asyncio + async def test_request_span_wraps_nested_tool_span(self, otel_setup): + middleware = ObservabilityMiddleware() + context = _make_context("get_data_sources") + context.method = "tools/call" + + async def call_tool(inner_context): + return await middleware.on_call_tool( + inner_context, + AsyncMock(return_value="ok"), + ) + + assert await middleware.on_request(context, call_tool) == "ok" + + spans = {span.name: span for span in otel_setup.get_finished_spans()} + request_span = spans["mcp tools/call"] + tool_span = spans["tool get_data_sources"] + assert request_span.attributes == {"mcp.method.name": "tools/call"} + assert request_span.status.status_code == trace.StatusCode.OK + assert tool_span.parent.span_id == request_span.context.span_id + + @pytest.mark.asyncio + async def test_request_failure_records_type_without_message(self, otel_setup): + middleware = ObservabilityMiddleware() + context = _make_context() + context.method = "tools/call" + + with pytest.raises(ValueError, match="secret query text"): + await middleware.on_request( + context, + AsyncMock(side_effect=ValueError("secret query text")), + ) + + span = otel_setup.get_finished_spans()[0] + assert span.status.status_code == trace.StatusCode.ERROR + assert span.status.description == "ValueError" + assert span.attributes == { + "mcp.method.name": "tools/call", + "error.type": "ValueError", + } + assert "secret query text" not in str(span.events) + + # --------------------------------------------------------------------------- # Failed tool call # --------------------------------------------------------------------------- diff --git a/uv.lock b/uv.lock index 747804b..5166e3e 100644 --- a/uv.lock +++ b/uv.lock @@ -55,6 +55,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875, upload-time = "2026-06-24T20:56:04.413Z" }, ] +[[package]] +name = "asgiref" +version = "3.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/26/3b59f2bdae5f640389becb1f673cded775287f5fc4f816309d9ca9a3f93d/asgiref-3.12.1.tar.gz", hash = "sha256:59dcb51c272ad209d59bed5708a64a333083e86017d7fcdd67498eeab7784340", size = 42378, upload-time = "2026-07-14T09:56:18.087Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/1b/54f4ad77cd8a584fa70746c47df988e002cf1ee1eba43364d46f87803647/asgiref-3.12.1-py3-none-any.whl", hash = "sha256:fe386d1c2bff7259ea95929266d12a8cf9a8b5a1c2598402967d8792e7a7c094", size = 25478, upload-time = "2026-07-14T09:56:16.926Z" }, +] + [[package]] name = "attrs" version = "26.1.0" @@ -332,6 +341,7 @@ dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp-proto-http" }, { name = "opentelemetry-instrumentation-httpx" }, + { name = "opentelemetry-instrumentation-starlette" }, { name = "opentelemetry-sdk" }, { name = "opentelemetry-semantic-conventions" }, { name = "python-dotenv" }, @@ -357,6 +367,7 @@ requires-dist = [ { name = "opentelemetry-api", specifier = "==1.43.0" }, { name = "opentelemetry-exporter-otlp-proto-http", specifier = "==1.43.0" }, { name = "opentelemetry-instrumentation-httpx", specifier = "==0.64b0" }, + { name = "opentelemetry-instrumentation-starlette", specifier = "==0.64b0" }, { name = "opentelemetry-sdk", specifier = "==1.43.0" }, { name = "opentelemetry-semantic-conventions", specifier = "==0.64b0" }, { name = "pytest", marker = "extra == 'test'", specifier = "==9.1.1" }, @@ -1002,6 +1013,22 @@ 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-httpx" version = "0.64b0" @@ -1018,6 +1045,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/06/29/a20309bd3f5a8051b61ca475e78623410c3b077e3deccae19aa4f8b5b9a2/opentelemetry_instrumentation_httpx-0.64b0-py3-none-any.whl", hash = "sha256:04829e5723941b5ceb0c88b44d63983e226b5c75b2b2e34a57739fdd0e060608", size = 16336, upload-time = "2026-06-24T15:18:41.412Z" }, ] +[[package]] +name = "opentelemetry-instrumentation-starlette" +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/91/49/e4621a8bd31f744543024242a9d5d77d203dcb2322749d8f0bec3d4dabad/opentelemetry_instrumentation_starlette-0.64b0.tar.gz", hash = "sha256:bdcc4ec3cbb9173539674bd151628332091f8c079afe86c4f98036e91d7fcd6c", size = 14384, upload-time = "2026-06-24T15:19:41.874Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/11/e353e0a10ba092342795ea3f3b999b7883d0a720a9f3f32bb85a682108ef/opentelemetry_instrumentation_starlette-0.64b0-py3-none-any.whl", hash = "sha256:900d2e377c62e9c213da03ede901eb6714644db99751d22fad7a1e5d74b43377", size = 11106, upload-time = "2026-06-24T15:18:59.039Z" }, +] + [[package]] name = "opentelemetry-proto" version = "1.43.0" From fcc2fcfc1ccaa34f5e2e238de7d65bead95ced26 Mon Sep 17 00:00:00 2001 From: Rodion Mostovoi <36400912+rodion-m@users.noreply.github.com> Date: Wed, 22 Jul 2026 22:29:21 +0500 Subject: [PATCH 2/3] Make MCP access telemetry configurable --- src/codealive_mcp_server.py | 14 ++++++++++++++ src/tests/test_http_transport_security.py | 14 ++++++++++++++ src/tests/test_observability.py | 20 ++++++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/src/codealive_mcp_server.py b/src/codealive_mcp_server.py index 73007bf..a46d231 100644 --- a/src/codealive_mcp_server.py +++ b/src/codealive_mcp_server.py @@ -53,6 +53,13 @@ def _package_version() -> str: return "unknown" +def _environment_flag(name: str, *, default: bool) -> bool: + value = os.getenv(name) + if value is None: + return default + return value.strip().lower() not in {"false", "0", "no", "off"} + + # Initialize FastMCP server with lifespan and enhanced system instructions mcp = FastMCP( name="CodeAlive MCP Server", @@ -301,6 +308,13 @@ def main(): allowed_origins=allowed_origins or None, uvicorn_config={ "forwarded_allow_ips": "*", + # Access logs scale linearly with unauthenticated traffic. Keep + # the upstream default for self-hosted operators, while allowing + # hardened deployments to rely on sampled traces and safe events. + "access_log": _environment_flag( + "CODEALIVE_MCP_ACCESS_LOG_ENABLED", + default=True, + ), }, ) else: diff --git a/src/tests/test_http_transport_security.py b/src/tests/test_http_transport_security.py index 1169927..cb363d5 100644 --- a/src/tests/test_http_transport_security.py +++ b/src/tests/test_http_transport_security.py @@ -97,6 +97,20 @@ def test_http_main_enables_guard_and_reads_environment_allowlists(monkeypatch): assert options["host_origin_protection"] is True assert options["allowed_hosts"] == ["mcp.codealive.ai", "codealive-mcp-server"] assert options["allowed_origins"] == ["https://mcp.codealive.ai"] + assert options["uvicorn_config"]["access_log"] is True + + +def test_http_main_can_disable_per_request_access_logs(monkeypatch): + run = MagicMock() + monkeypatch.setattr(server.mcp, "run", run) + monkeypatch.setattr(server, "setup_logging", MagicMock()) + monkeypatch.setattr(server, "init_tracing", MagicMock()) + monkeypatch.setenv("CODEALIVE_MCP_ACCESS_LOG_ENABLED", "false") + monkeypatch.setattr(sys, "argv", ["codealive-mcp", "--transport", "http"]) + + server.main() + + assert run.call_args.kwargs["uvicorn_config"]["access_log"] is False def test_http_main_fails_closed_when_oauth_exchange_secret_is_missing(monkeypatch): diff --git a/src/tests/test_observability.py b/src/tests/test_observability.py index 8005419..e758866 100644 --- a/src/tests/test_observability.py +++ b/src/tests/test_observability.py @@ -6,6 +6,7 @@ import pytest from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import Event, ReadableSpan +from opentelemetry.sdk.trace.sampling import Decision from opentelemetry.trace import SpanContext, SpanKind, Status, StatusCode, TraceFlags sys.path.insert(0, str(__import__("pathlib").Path(__file__).parent.parent)) @@ -14,6 +15,25 @@ class TestInitTracing: + def test_standard_sampler_environment_controls_root_sampling(self, monkeypatch): + monkeypatch.delenv("OTEL_EXPORTER_OTLP_ENDPOINT", raising=False) + monkeypatch.delenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", raising=False) + monkeypatch.setenv("OTEL_TRACES_SAMPLER", "parentbased_traceidratio") + monkeypatch.setenv("OTEL_TRACES_SAMPLER_ARG", "0.05") + + with patch("core.observability.HTTPXClientInstrumentor"): + with patch("core.observability.StarletteInstrumentor"): + with patch("core.observability.trace.set_tracer_provider") as mock_set: + init_tracing() + + sampler = mock_set.call_args[0][0].sampler + assert sampler.should_sample(None, 1, "sampled").decision == Decision.RECORD_AND_SAMPLE + assert sampler.should_sample( + None, + (1 << 128) - 1, + "dropped", + ).decision == Decision.DROP + def test_no_endpoint_creates_provider_without_exporter(self, monkeypatch): monkeypatch.delenv("OTEL_EXPORTER_OTLP_ENDPOINT", raising=False) monkeypatch.delenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", raising=False) From 80b77f53008e4108bcf776c5622c32b34fd80477 Mon Sep 17 00:00:00 2001 From: Rodion Mostovoi <36400912+rodion-m@users.noreply.github.com> Date: Tue, 4 Aug 2026 21:19:11 +0500 Subject: [PATCH 3/3] Fix cryptography security advisory --- pyproject.toml | 4 ++ uv.lock | 99 ++++++++++++++++++++++++++------------------------ 2 files changed, 55 insertions(+), 48 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 844fb55..a145f4c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,10 @@ fallback_version = "3.0.2" # Older versions silently ignore the whole [tool.uv] section on parse error. required-version = "==0.11.28" exclude-newer = "7 days" +# Security exception: cryptography 50.0.0 fixes GHSA-g6cj-pr64-35w5 and was +# released inside the normal quarantine window. Keep the exception package- +# scoped so all unrelated dependencies remain subject to the seven-day delay. +exclude-newer-package = { cryptography = "2026-08-01T00:00:00Z" } # Security minimums are older than the global seven-day quarantine. Exact direct # pins plus exclude-newer keep every future lock update inside the same policy. constraint-dependencies = [ diff --git a/uv.lock b/uv.lock index 5166e3e..4edf517 100644 --- a/uv.lock +++ b/uv.lock @@ -12,6 +12,9 @@ resolution-markers = [ exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. exclude-newer-span = "P7D" +[options.exclude-newer-package] +cryptography = "2026-08-01T00:00:00Z" + [manifest] constraints = [ { name = "cryptography", specifier = ">=48.0.1" }, @@ -478,58 +481,58 @@ toml = [ [[package]] name = "cryptography" -version = "49.0.0" +version = "50.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" }, - { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, - { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, - { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, - { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, - { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, - { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, - { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, - { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, - { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, - { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, - { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, - { url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" }, - { url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" }, - { url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" }, - { url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" }, - { url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" }, - { url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" }, - { url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" }, - { url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" }, - { url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" }, - { url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" }, - { url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" }, - { url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" }, - { url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" }, - { url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" }, - { url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" }, - { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, - { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, - { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, - { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, - { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, - { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, - { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, - { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, - { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, - { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, - { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, - { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, - { url = "https://files.pythonhosted.org/packages/63/d3/4a83af35d65e3fad632c926fad684c193ea4398569ccb0bbbc7fe8f5dc9a/cryptography-49.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b", size = 3993685, upload-time = "2026-06-12T20:02:14.883Z" }, - { url = "https://files.pythonhosted.org/packages/d6/a7/f9dac0ab7f80368c56993a7bf638ef9935f825c91902798481fac0898138/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838", size = 4676239, upload-time = "2026-06-12T20:02:28.793Z" }, - { url = "https://files.pythonhosted.org/packages/d7/70/2ba3769dd0ae167e2f33dfa9592d45db6ff9a61d62ca1a5b3d1bdd09068f/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5", size = 4715584, upload-time = "2026-06-12T20:01:27.495Z" }, - { url = "https://files.pythonhosted.org/packages/94/64/2923570ac1c0bd3a737aa366ac3abbbbde273042308b8cde95e2364a6e6a/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615", size = 4675885, upload-time = "2026-06-12T20:01:55.49Z" }, - { url = "https://files.pythonhosted.org/packages/ab/f8/614dc7e051418cfe53d55173c1e24c6b0085e89996fe90508c2fdf769aef/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6", size = 4715449, upload-time = "2026-06-12T20:02:05.469Z" }, - { url = "https://files.pythonhosted.org/packages/aa/50/a9caea39ad19c431c1a3f8a31114df65b260cdfe67786b6c7e7c040c4c44/cryptography-49.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6", size = 3783731, upload-time = "2026-06-12T20:02:43.319Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" }, + { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" }, + { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" }, + { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" }, + { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" }, + { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" }, + { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" }, + { url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" }, + { url = "https://files.pythonhosted.org/packages/c3/fb/951032a3bf22a5697c83183fb6294a4843772947a70e616c57b3ff5f522e/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41", size = 3989258, upload-time = "2026-07-31T14:23:58.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" }, + { url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" }, + { url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" }, + { url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" }, + { url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" }, + { url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" }, + { url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" }, + { url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" }, + { url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" }, + { url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" }, + { url = "https://files.pythonhosted.org/packages/74/9a/02ffe35b2853d121689871eb5dce862092562b3a1ed5cc98f1aaed441506/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269", size = 3816291, upload-time = "2026-07-31T14:24:22.125Z" }, + { url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" }, + { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" }, + { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" }, + { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" }, + { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" }, + { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" }, + { url = "https://files.pythonhosted.org/packages/9d/3e/e54cde8c01631a5a8226ccd617eab9e57fd5cfdad90f1a9e6bb570794631/cryptography-50.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c", size = 3963170, upload-time = "2026-07-31T14:24:51.968Z" }, + { url = "https://files.pythonhosted.org/packages/01/b6/0b9e125e90f3d2dcf599a218a899cda7326a3158cfa258723f0b398b08f6/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a", size = 4692441, upload-time = "2026-07-31T14:24:53.743Z" }, + { url = "https://files.pythonhosted.org/packages/53/c9/a5151588710785a96d7bc4de27d4cd62f263bbbcb203cfe29df537eb6505/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e", size = 4699810, upload-time = "2026-07-31T14:24:55.746Z" }, + { url = "https://files.pythonhosted.org/packages/c7/1a/15b92b25eb6ce3089cd49377ae990a0f3ad485a510f968aed1f19dbdcdf2/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d", size = 4691924, upload-time = "2026-07-31T14:24:58.082Z" }, + { url = "https://files.pythonhosted.org/packages/62/15/219075012ab13e8905f3cd572204f4acb4b111df787104346b9bc0cea789/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437", size = 4699593, upload-time = "2026-07-31T14:24:59.951Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b5/c2c5fce26f0ee40d21bafe7f191d29a34b35a65ac4fe8a1191d1983612e9/cryptography-50.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9", size = 3813796, upload-time = "2026-07-31T14:25:02.298Z" }, ] [[package]]