diff --git a/AGENTS.md b/AGENTS.md index 3af4169bc..f6da1226a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -422,3 +422,32 @@ columns). Do not silently rewrite either historical form. The SHACL shapes graph (`docs/ontology/lineageweave-kg-shapes.ttl`) is the closed-world data-validation boundary for DB-to-RDF projections and is published beside the ontology. + +## Bounded telemetry maintenance + +When replacing an OpenTelemetry handler, preserve explicit OTLP opt-in and +attach it only to the product's bounded logger. Do not enable global automatic +instrumentation to silence a deprecation warning. Verify the root logger and +LogRecord factory are unchanged, retain the intended severity threshold, and +check warning absence after real provider setup and teardown. Backend diagnostic +tests require both the dev and backend extras in the isolated uv environment. + +Register provider ownership immediately after allocation, before attaching +processors or handlers. A later optional-telemetry setup failure must still +leave the provider reachable by normal shutdown; test that failure path using +a real provider and verify shutdown rather than only catching the exception. + +Optional telemetry failure boundaries must cover instrument acquisition as well +as recording. A failed meter/counter constructor must not replace the original +application failure or leak its exception message into bounded diagnostic logs. + +Telemetry privacy tests must use allowed attribute names with disallowed values; +unlisted names are rejected earlier and cannot prove scalar-value filtering. +Inspect structured LogRecord fields as well as formatted text when checking +content exclusion. Preserve the full coverage denominator and record harness +failures separately from product assertions. + +When testing degraded telemetry support, verify that the original application +exception survives and that exported events plus structured logs omit its value. +A coverage percentage alone does not prove either invariant; retain the same +statement/branch denominator when comparing improvements. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index b5d31877b..b745962fe 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -932,3 +932,109 @@ The ONET rows stacked into base branches (#743/#745/#746/#740/#732) reached `main` together through the #759 promotion; their per-base merge records are historical evidence only. The job-architecture artifact ship originally via #749 is now re-verified on `main` from the promotion. + + +## OpenTelemetry log-handler compatibility (2026-09-07) + +Hosted Tests run 34082387676 reported a deprecated SDK LoggingHandler during +provider configuration. The existing provider test was strengthened to capture +DeprecationWarning, then assert that none occurred after shutdown; it failed +on the SDK handler. It also checks the WARNING threshold, dedicated logger +attachment, unchanged root handlers, and unchanged global LogRecord factory. + +The adapter now imports LoggingHandler from the official +opentelemetry-instrumentation-logging package and retains manual construction +with its existing LoggerProvider. The lock pins 0.65b0 alongside SDK 1.44.0 +and semantic conventions 0.65b0. Only the new instrumentation package, its base +instrumentation dependency, and wrapt are added; no existing locked versions +were changed. No LoggingInstrumentor, global record-factory patch, root handler, +or new exporter setting is enabled. ADR 0122 remains the governing opt-in and +bounded-content contract. The replacement's default omits optional code-location +attributes; the ADR-defined operation/session/error evidence remains unchanged. + +Final local verification used the frozen dev and backend extras on Python 3.14.6: +30 observability and server-diagnostic tests passed without warnings. Module +statement/branch coverage is 92%, not 100%; uncovered paths remain a tracked +gap. The first combined run lacked backend extras and stopped during collection +on missing asyncpg; its 15% coverage report is not test acceptance evidence. +No warning suppression, real-record fixture, collector deployment, or protected +merge is claimed. + +References: + +OpenTelemetry Authors. (n.d.). *OpenTelemetry logging instrumentation*. +https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/logging/logging.html + +OpenTelemetry Authors. (2026). *LoggingHandler implementation (v0.65b0)*. +https://github.com/open-telemetry/opentelemetry-python-contrib/blob/v0.65b0/instrumentation/opentelemetry-instrumentation-logging/src/opentelemetry/instrumentation/logging/handler.py + + +### Partial telemetry initialization ownership (2026-09-07) + +Handler initialization could fail after the log provider acquired a batch worker +but before the module saved the provider for shutdown. A regression with a real +LoggerProvider and a deliberately failing handler proved normal shutdown called +the provider zero times. Test cleanup explicitly closed the orphan afterward. + +The provider is now registered with module ownership immediately after creation. +The same failure remains fail-open, while normal shutdown closes the partially +initialized provider exactly once and clears module state. This moves one +existing assignment and introduces no new lifecycle abstraction. + +All 31 observability/server-diagnostic tests passed in 1.97 s without warnings. +The same module coverage increased from 92% to 95% (197 statements, 68 branches); +100% remains unmet. This is local failure-path evidence, not live collector or +protected-merge acceptance. + + +### Metric initialization failure boundary (2026-09-07) + +The counter add operation was guarded, but acquiring the counter was outside +the guard. A synthetic metrics-provider failure escaped record_server_failure +and replaced the application's original failure. The regression failed with +the injected RuntimeError before the repair. + +Counter acquisition now belongs to the existing metric-recording exception +boundary. The original ValueError classification and provider-unavailable +outcome still reach bounded logs, while neither the injected metric exception +message nor the original request exception message appears. No fallback metric +value, alternate provider, or additional wrapper is introduced. + +The complete three-file observability/server-diagnostic selection passed 32 +tests in 34.74 s without warnings. Module statement/branch coverage increased +from 95% to 96%; the remaining paths are not claimed covered. This is local +failure-isolation evidence, not protected deployment or full-goal completion. + +### Telemetry attribute privacy coverage follow-up (2026-09-07) + +Hosted run 34087122183 actually executed 32 tests successfully and reported 96% +statement/branch coverage for observability; earlier harness failures were not +coverage measurements. The container-value test had used only unlisted keys, +which bypassed the allowed-key scalar filter it purported to test. It now checks +an allowed key with dictionary, list, tuple, set, null, and unsupported-object +values. A separate unavailable-metric test verifies unknown operation content and +exception payload stay out of structured log fields. + +The corrected focused suite passes 38 tests in 2.43 s and reports 97% (197 +statements, 68 branches, 2 missed statements and 5 partial branches). No exclusion +or denominator change was made. Initial new-test failures were harness mistakes +(keyword-only outcome called positionally, then checking formatted text instead +of structured LogRecord attributes); neither is claimed as a product bug. +The 100% target, current-head hosted tests, independent review, and deployment +remain incomplete; keep the PR Draft. + +### Telemetry measured coverage completion (2026-09-07; local module scope) + +The remaining telemetry regressions exercise absent/incomplete span context, +unavailable propagation without header mutation, rejected outcome classification +before metric creation, and partial status support. The latter uses the existing +in-memory SDK exporter and verifies original-exception identity, retained bounded +exception type, unset status, and absence of exception content in both events and +structured logs. No production guard or coverage exclusion was removed. + +The focused suite passed 44 tests in 34.20 s. `coverage report --fail-under=100` +passed with 197 statements, 68 branches, zero missed statements and zero partial +branches: **100% for lineageweave/observability.py**, on the same denominator as +96% and 97% observations above. This supersedes the local module coverage gap; +it does not establish repository-wide 100% coverage, current-head hosted tests, +independent review, collector deployment, or real-source runtime acceptance. diff --git a/lineageweave/observability.py b/lineageweave/observability.py index 9262a4135..c5d92aefc 100644 --- a/lineageweave/observability.py +++ b/lineageweave/observability.py @@ -205,13 +205,15 @@ def configure_telemetry(service_name: str = "lineageweave") -> None: from opentelemetry.exporter.otlp.proto.http._log_exporter import ( OTLPLogExporter, ) - from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler + from opentelemetry.instrumentation.logging.handler import LoggingHandler + from opentelemetry.sdk._logs import LoggerProvider from opentelemetry.sdk._logs.export import BatchLogRecordProcessor except ImportError: # pragma: no cover - guarded by the runtime extra _LOGGER.warning("OpenTelemetry log SDK/exporter is unavailable") return try: log_provider = LoggerProvider(resource=resource) + _LOG_PROVIDER = log_provider log_provider.add_log_record_processor( BatchLogRecordProcessor( OTLPLogExporter(endpoint=_otlp_log_endpoint(endpoint)) @@ -220,7 +222,6 @@ def configure_telemetry(service_name: str = "lineageweave") -> None: set_logger_provider(log_provider) handler = LoggingHandler(level=logging.WARNING, logger_provider=log_provider) _LOGGER.addHandler(handler) - _LOG_PROVIDER = log_provider _LOG_HANDLER = handler except Exception: # noqa: BLE001 - export must stay fail-open _LOGGER.warning("OpenTelemetry log exporter is unavailable") @@ -316,9 +317,9 @@ def record_server_failure( bounded_operation = "unknown" error_type = type(exc).__name__[:128] session_id = current_session_id() or "" - counter = _failure_counter() - if counter is not None: - try: + try: + counter = _failure_counter() + if counter is not None: counter.add( 1, { @@ -326,8 +327,8 @@ def record_server_failure( "lineageweave.failure_outcome": outcome, }, ) - except Exception: # noqa: BLE001 # telemetry failure must not mask API failure - _LOGGER.warning("telemetry.metric_recording_failed") + except Exception: # noqa: BLE001 # telemetry failure must not mask API failure + _LOGGER.warning("telemetry.metric_recording_failed") stack_trace = ( _stack_trace_without_exception(exc) if outcome == "internal_error" else "" diff --git a/pyproject.toml b/pyproject.toml index 7744aef87..1eb558a74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ dependencies = [ "opentelemetry-api>=1.30.0", "opentelemetry-sdk>=1.30.0", "opentelemetry-exporter-otlp-proto-http>=1.30.0", + "opentelemetry-instrumentation-logging>=0.65b0", ] [build-system] diff --git a/tests/test_observability.py b/tests/test_observability.py index 16a29a2c6..67ccb68da 100644 --- a/tests/test_observability.py +++ b/tests/test_observability.py @@ -33,6 +33,25 @@ def attach_inmemory_tracer(monkeypatch: pytest.MonkeyPatch) -> InMemorySpanExpor return exporter +@pytest.mark.parametrize("missing_symbol", ["Status", "StatusCode"]) +def test_missing_status_support_preserves_failure_and_safe_evidence(monkeypatch, caplog, missing_symbol): + """Partial status support must neither replace application errors nor leak content.""" + exporter = attach_inmemory_tracer(monkeypatch) + monkeypatch.setattr(observability, missing_symbol, None) + original = ValueError("synthetic private payload") + with caplog.at_level(logging.WARNING), pytest.raises(ValueError) as raised: + with traced("lineageweave.test.partial_status"): + record_server_failure("post_chat", original, outcome="provider_unavailable") + raise original + assert raised.value is original + spans = exporter.get_finished_spans() + assert len(spans) == 1 + assert spans[0].status.status_code == StatusCode.UNSET + assert spans[0].events[0].attributes["exception.type"] == "ValueError" + assert "synthetic private payload" not in str(spans[0].events) + assert "synthetic private payload" not in str([vars(record) for record in caplog.records]) + + def test_post_json_sends_post_session_header(monkeypatch): """One post session reaches the orchestrator as a transport header.""" captured = {} diff --git a/tests/test_observability_telemetry.py b/tests/test_observability_telemetry.py index b99622fca..c7901e947 100644 --- a/tests/test_observability_telemetry.py +++ b/tests/test_observability_telemetry.py @@ -10,12 +10,52 @@ from __future__ import annotations import logging +from types import SimpleNamespace import pytest import lineageweave.observability as observability +@pytest.mark.parametrize("current_span", [None, object()]) +def test_missing_span_context_does_not_invent_correlation(monkeypatch, current_span) -> None: + """An absent or incomplete span has no valid correlation identifiers.""" + monkeypatch.setattr(observability, "trace", SimpleNamespace(get_current_span=lambda: current_span)) + assert observability._current_trace_ids() == ("", "") + + +def test_unavailable_propagator_preserves_request_headers(monkeypatch) -> None: + """Missing propagation support must not change existing transport headers.""" + monkeypatch.setattr(observability, "_otel_inject", None) + carrier = {"content-type": "application/json"} + observability.inject_trace_context(carrier) + assert carrier == {"content-type": "application/json"} + + +def test_unlisted_failure_outcome_is_rejected_before_metric_creation(monkeypatch) -> None: + """Unlisted outcomes cannot create a new metric classification.""" + def unexpected_counter(): + pytest.fail("invalid classification reached metric creation") + + monkeypatch.setattr(observability, "_failure_counter", unexpected_counter) + with pytest.raises(ValueError, match="unsupported server failure outcome"): + observability.record_server_failure("post_chat", ValueError(), outcome="invented") + + +def test_failure_log_drops_unknown_operation_without_a_metric_counter(monkeypatch, caplog) -> None: + """An unavailable metric channel must not admit unlisted operation content.""" + monkeypatch.setattr(observability, "_failure_counter", lambda: None) + monkeypatch.setattr(observability, "trace", None) + with caplog.at_level(logging.WARNING, logger=observability.__name__): + observability.record_server_failure( + "synthetic-private-operation", ValueError("private payload"), outcome="provider_unavailable" + ) + record = next(record for record in caplog.records if record.message == "lineageweave.server_failure") + assert record.operation_code == "unknown" + assert "synthetic-private-operation" not in str(vars(record)) + assert "private payload" not in str(vars(record)) + + def _signal_endpoint(endpoint: str, signal: str) -> str: """Thin wrapper so callers pass one helper under test.""" return observability._otlp_signal_endpoint(endpoint, signal) @@ -68,7 +108,8 @@ def test_metric_and_log_endpoint_helpers_route_to_their_signals() -> None: ) -def test_safe_attributes_skips_container_values_and_unknown_keys() -> None: +@pytest.mark.parametrize("invalid_value", [{"private": "content"}, ["content"], ("content",), {"content"}, None, object()]) +def test_safe_attributes_skips_container_values_and_unknown_keys(invalid_value) -> None: """Composite and unlisted attribute values never reach a span.""" sanitized = observability._safe_attributes( { @@ -77,11 +118,13 @@ def test_safe_attributes_skips_container_values_and_unknown_keys() -> None: "nested": {"a": 1}, "items": [1, 2, 3], "unlisted_key": "should-not-appear", + "http.response.status_code": invalid_value, } ) assert sanitized["lineageweave.operation_code"] == "http_post_json" assert sanitized["lineageweave.session_id"] == "post-123" assert "nested" not in sanitized + assert "http.response.status_code" not in sanitized assert "items" not in sanitized assert "unlisted_key" not in sanitized @@ -107,6 +150,10 @@ def test_configure_telemetry_success_installs_providers( import opentelemetry.metrics as otel_metrics import opentelemetry.trace as otel_trace + import warnings + + root_handlers = list(logging.getLogger().handlers) + record_factory = logging.getLogRecordFactory() trace_providers: list[object] = [] metric_providers: list[object] = [] log_providers: list[object] = [] @@ -118,7 +165,9 @@ def test_configure_telemetry_success_installs_providers( monkeypatch.setattr(otel_metrics, "set_meter_provider", metric_providers.append) monkeypatch.setattr(otel_logs, "set_logger_provider", log_providers.append) - observability.configure_telemetry("services/synthetic") + with warnings.catch_warnings(record=True) as notices: + warnings.simplefilter("always", DeprecationWarning) + observability.configure_telemetry("services/synthetic") assert observability._CONFIGURED is True assert observability._TRACE_PROVIDER is not None @@ -126,6 +175,10 @@ def test_configure_telemetry_success_installs_providers( assert metric_providers == [observability._METER_PROVIDER] assert log_providers == [observability._LOG_PROVIDER] assert isinstance(observability._LOG_HANDLER, logging.Handler) + assert observability._LOG_HANDLER.level == logging.WARNING + assert observability._LOG_HANDLER in observability._LOGGER.handlers + assert logging.getLogger().handlers == root_handlers + assert logging.getLogRecordFactory() is record_factory # Restore the module to a clean, unconfigured state for the rest of the suite. observability.shutdown_telemetry() @@ -134,6 +187,7 @@ def test_configure_telemetry_success_installs_providers( monkeypatch.setattr(observability, "_METER_PROVIDER", None) monkeypatch.setattr(observability, "_LOG_PROVIDER", None) monkeypatch.setattr(observability, "_LOG_HANDLER", None) + assert not [notice for notice in notices if issubclass(notice.category, DeprecationWarning)] def test_configure_telemetry_returns_when_sdk_disabled( @@ -180,4 +234,60 @@ def test_shutdown_telemetry_removes_handler_and_nulls_providers( assert observability._TRACE_PROVIDER is None assert observability._METER_PROVIDER is None assert observability._LOG_PROVIDER is None - assert fake_handler not in logging.getLogger().handlers \ No newline at end of file + assert fake_handler not in logging.getLogger().handlers + +def test_failed_handler_keeps_provider_owned_for_shutdown(monkeypatch: pytest.MonkeyPatch) -> None: + """A handler failure must not orphan the log provider's batch worker.""" + from unittest.mock import Mock + + import opentelemetry._logs as otel_logs + import opentelemetry.instrumentation.logging.handler as handler_module + import opentelemetry.sdk._logs as sdk_logs + import opentelemetry.trace as otel_trace + + provider = sdk_logs.LoggerProvider() + shutdown = Mock(wraps=provider.shutdown) + monkeypatch.setattr(provider, "shutdown", shutdown) + monkeypatch.setattr(sdk_logs, "LoggerProvider", lambda **kwargs: provider) + monkeypatch.setattr(handler_module, "LoggingHandler", Mock(side_effect=RuntimeError("synthetic handler failure"))) + monkeypatch.setattr(otel_logs, "set_logger_provider", lambda provider: None) + monkeypatch.setattr(otel_trace, "set_tracer_provider", lambda provider: None) + monkeypatch.setattr(observability, "metrics", None) + monkeypatch.setattr(observability, "_CONFIGURED", False) + monkeypatch.setattr(observability, "_LOG_PROVIDER", None) + monkeypatch.setattr(observability, "_LOG_HANDLER", None) + monkeypatch.delenv("OTEL_SDK_DISABLED", raising=False) + monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://127.0.0.1:9") + try: + observability.configure_telemetry() + observability.shutdown_telemetry() + shutdown.assert_called_once_with() + assert observability._LOG_PROVIDER is None + assert observability._LOG_HANDLER is None + finally: + observability.shutdown_telemetry() + if not shutdown.called: + provider.shutdown() + + +def test_metric_initialization_failure_preserves_bounded_failure_log( + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, +) -> None: + """A broken metric provider cannot replace the original application failure.""" + from types import SimpleNamespace + from unittest.mock import Mock + + monkeypatch.setattr(observability, "_FAILURE_COUNTER", None) + monkeypatch.setattr(observability, "metrics", SimpleNamespace( + get_meter=Mock(side_effect=RuntimeError("synthetic private metric detail")), + )) + monkeypatch.setattr(observability, "trace", None) + with caplog.at_level(logging.WARNING, logger=observability.__name__): + observability.record_server_failure( + "global_ask", ValueError("synthetic private request detail"), + outcome="provider_unavailable", + ) + failure = next(record for record in caplog.records if record.msg == "lineageweave.server_failure") + assert failure.error_type == "ValueError" + assert failure.failure_outcome == "provider_unavailable" + assert "synthetic private" not in caplog.text diff --git a/uv.lock b/uv.lock index f94e79cf4..6239a78bd 100644 --- a/uv.lock +++ b/uv.lock @@ -692,6 +692,7 @@ dependencies = [ { name = "cryptography" }, { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-instrumentation-logging" }, { name = "opentelemetry-sdk" }, { name = "pillow" }, { name = "rankweave" }, @@ -732,6 +733,7 @@ requires-dist = [ { name = "mcp", marker = "extra == 'backend'", specifier = "==2.0.0" }, { name = "opentelemetry-api", specifier = ">=1.30.0" }, { name = "opentelemetry-exporter-otlp-proto-http", specifier = ">=1.30.0" }, + { name = "opentelemetry-instrumentation-logging", specifier = ">=0.65b0" }, { name = "opentelemetry-sdk", specifier = ">=1.30.0" }, { name = "pillow", specifier = ">=12.3.0" }, { name = "psycopg2-binary", marker = "extra == 'dev'", specifier = ">=2.9.12" }, @@ -900,6 +902,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cd/d0/fdeb1a98d8d3a6205f5f297c51b4a9bfe65126ab60339669bbe3dd54c2e2/opentelemetry_exporter_otlp_proto_http-1.44.0-py3-none-any.whl", hash = "sha256:838592fce774c1c8bb7b9a0a7facbfa82e17be5a8a4e94cef10cb84ae026bae3", size = 21850, upload-time = "2026-07-16T15:25:20.006Z" }, ] +[[package]] +name = "opentelemetry-instrumentation" +version = "0.65b0" +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/13/91/3c58961cb0360cd60509064734f0be4275383c8681d73c580a40ca83ddce/opentelemetry_instrumentation-0.65b0.tar.gz", hash = "sha256:071d9d9eced9bd6460444ec3b0c77229870ed05a881c22c84fdede58e4eed09b", size = 42689, upload-time = "2026-07-16T15:25:50.275Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/7b/85eab1215f72adf0e68d3dc4a679b9bff993fa679ff34cd8dd378e2659fd/opentelemetry_instrumentation-0.65b0-py3-none-any.whl", hash = "sha256:ea967a72b9939b5fcfdad572753b4306c59dcb99e3f382d95dae04286805e137", size = 36717, upload-time = "2026-07-16T15:24:51.424Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-logging" +version = "0.65b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/0a/b70a9cddbc7b314a783e62739dbb1184f8538c1f85e8ded6d340142b9b54/opentelemetry_instrumentation_logging-0.65b0.tar.gz", hash = "sha256:c0a50cade5d54db6c6af12e2c69227ecd26f2b3b779e99ff850561d3d8dd77e3", size = 19783, upload-time = "2026-07-16T15:26:09.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/8e/7577914681d77b180f8d6dcbac435be8e4ca6add6315da2d01ac4289eaa3/opentelemetry_instrumentation_logging-0.65b0-py3-none-any.whl", hash = "sha256:68365b31755c844f1e85f07dcd217839ff92f2d278a214bdf02d4dc806f9d915", size = 15727, upload-time = "2026-07-16T15:25:18.774Z" }, +] + [[package]] name = "opentelemetry-proto" version = "1.44.0" @@ -1821,3 +1852,78 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/24/a585e7573e128070605d003b5544729bcd58d9756c7e99d550818ca4b916/websockets-17.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:dbfae8e75b342e31fc6fd1a8bbb393b7cbb91d6cfd581650300a94381e7b7e2b", size = 213009, upload-time = "2026-07-31T11:31:16.776Z" }, { url = "https://files.pythonhosted.org/packages/09/ce/3929538b2b9918f5eee623fbf3346893973191f6df93f19bbda097bd7bb7/websockets-17.0.1-py3-none-any.whl", hash = "sha256:c6be9cba65c65cc76dfa3d4619e359ff02a4476c74e179b215236c11a0b32345", size = 206718, upload-time = "2026-07-31T11:31:26.037Z" }, ] + +[[package]] +name = "wrapt" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ba/8dc25478ed234dacc7d83c671634f347d0bdfb65bf0502f41879cf2f15a9/wrapt-2.4.0.tar.gz", hash = "sha256:7082fc1f94b020ac275870c4af71b09cff22876fe6e9c4c0ad01ea21d217b288", size = 161179, upload-time = "2026-08-30T04:41:51.424Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/22/581a0b44349d5babe526c958f365b8126e0fbd8fc2810e80446c47358050/wrapt-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef4e2d6e399ce6eecc80179a6b9ef6544f121288f95fc132bc36c9d9503903af", size = 96374, upload-time = "2026-08-30T04:39:42.335Z" }, + { url = "https://files.pythonhosted.org/packages/5d/90/095984648cec62a786bb27c0b50f6cfa5856d1e073ba1006fe148d190084/wrapt-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b9b32d5e4f0a179cef5075cc79b79d6d3482c44c434c12969e48c6719e06d95", size = 96178, upload-time = "2026-08-30T04:39:43.789Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fd/b20e3cb3cab35131b515edf18e8cd777dff680fc76fc00919481f4e536af/wrapt-2.4.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d7dbbdbfdacb85c2d962fa52db791c77943fd777d600d74c95af2d53b32f5a94", size = 227806, upload-time = "2026-08-30T04:39:45.264Z" }, + { url = "https://files.pythonhosted.org/packages/08/75/c8dfba5e0caf17cd0718a0cbbe76cb85e637a2d65183fb728232419f6fca/wrapt-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39cd68df4dff79f5336f9c745c06259d204bcb42d504040c9c91eac9e2abb39c", size = 229004, upload-time = "2026-08-30T04:39:47.068Z" }, + { url = "https://files.pythonhosted.org/packages/42/05/d4853fbd33e5860b10d5aec690f563547a92a82e61fb8bb2d4ece1ce3570/wrapt-2.4.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2a9f1a2f75bb95257cc5744e255e10a5a86e923f328b40ad3dbf9d8d03430013", size = 208934, upload-time = "2026-08-30T04:39:48.73Z" }, + { url = "https://files.pythonhosted.org/packages/a3/66/23d0e8de9b411fd198af5121627587563657370c8d509fbe5ea8adb3df79/wrapt-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8763ad01e3725b7751a4575f38bbcc19c0aa0822fec91c5c5bd21ce3ce7e1d2b", size = 225709, upload-time = "2026-08-30T04:39:50.287Z" }, + { url = "https://files.pythonhosted.org/packages/01/37/3b357bc90530d510ae59ae7ac48265c482ae899e47637ca4436645688b40/wrapt-2.4.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9125c6dbe8b88c00dd8ef4fc1e55757e8eb4720b6b2b2cc610a45bd32bd28c57", size = 207090, upload-time = "2026-08-30T04:39:51.78Z" }, + { url = "https://files.pythonhosted.org/packages/6d/0c/d8a5c6dbcc2d221308223bcea4130c6332454a855cb4dbd5dcb2360b13b2/wrapt-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:28f5de1526831b8f173889a436e289fe181ede8c66c9feb669d1aca8fd602eaf", size = 216269, upload-time = "2026-08-30T04:39:53.641Z" }, + { url = "https://files.pythonhosted.org/packages/92/93/cc9fc8fef1d3d25edaa1c2dc2337b556dc1d0613ddc1c4a6fe9ee08ad705/wrapt-2.4.0-cp312-cp312-win32.whl", hash = "sha256:a9ca1cdb3f7facb4990c7739ea5afbaceeb6728d066feedde03a4cfe83b29b03", size = 91187, upload-time = "2026-08-30T04:39:55.38Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ec/a7b10705172bdb669b9687a8ff68bbe5f566437d2a49ad6d976af48b6d10/wrapt-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:8b464316489fb2fca0669ea0f8f07290054a0f26fc72982d3e4cf95469628ba9", size = 96423, upload-time = "2026-08-30T04:39:56.81Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/e838ac6463a1a1a1817b2f184ee2aa20c54692b80368c5063403c8d2461c/wrapt-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:db1285071ea09a7767fac608e7b5c7b03c09833b06186875a359905fbc659d29", size = 93003, upload-time = "2026-08-30T04:39:58.237Z" }, + { url = "https://files.pythonhosted.org/packages/19/86/f9de4e11582ff96ad2199eeeceaa17faa27bbdc599243f520070c4f3de07/wrapt-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5c5c4c728cd22a36e4b8bb5df4a7d3bccaa865d27725b36eeb3b6f18fb2e1bc2", size = 96041, upload-time = "2026-08-30T04:39:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/c3/ab/1dbf50802bea3b46192fd0dc39bb0eb2e77a064c813b2bbd88d2888ad49f/wrapt-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7de5b8d94417e55c02be50cc226e0ae1209bbc73813bf691dff3979c94438115", size = 96269, upload-time = "2026-08-30T04:40:01.182Z" }, + { url = "https://files.pythonhosted.org/packages/cb/a3/a3b5cde1cd06e04b6e95134eb3187a0a7da607a530e7795b221d4e4fa819/wrapt-2.4.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6436e2bda993a3eb69a1b317fc831c8ebcafb5704c390859ebd49f81218c4bbb", size = 225787, upload-time = "2026-08-30T04:40:02.715Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f7/d100f6c348b7669f19119cf890dcd4764623e2233af065586d110e0cd99e/wrapt-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e084558fbd112d2e1e34b0f5c71e45a3405bdad51a17150368a959bcf6697964", size = 226649, upload-time = "2026-08-30T04:40:04.647Z" }, + { url = "https://files.pythonhosted.org/packages/52/c6/3af8df515d5d7e92306957536f3468c6bdfecbe3659f99dbf09a468c2c4c/wrapt-2.4.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e78c947e18fadfd690c9420c30a96d221feeb93fc8f1cc00509b370ac16c3114", size = 206760, upload-time = "2026-08-30T04:40:06.332Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c5/40d355552bd3eb6c5186e26051c19b573d24d7896de42caa7937d6b5ca9f/wrapt-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:08d8378c4514ac8dcc0ace76044cf87a873e6a52b5e6109834c8fb9037f4441b", size = 223467, upload-time = "2026-08-30T04:40:07.829Z" }, + { url = "https://files.pythonhosted.org/packages/40/ab/d198eebdb39f0d7e182e771e590a36673489cd58cebdad8aa273dcf28e04/wrapt-2.4.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:93180c2199784dd6a1075b33f9ed636bd0966821edbece6b3d5379b1c4f0bb7d", size = 205358, upload-time = "2026-08-30T04:40:09.344Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0e/974a60672ad507d39a3d8a1c6351ef37fe65b07240d000ceba5d2b83e9e9/wrapt-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d5e5eb76fb87e62752af751d2dcd9d1cd986b12037d2e1363d109ba716029e8", size = 214654, upload-time = "2026-08-30T04:40:10.923Z" }, + { url = "https://files.pythonhosted.org/packages/cc/5a/8b2db70206db0a4246758e0472ce344cb9636217113ef70640fc8d2ce874/wrapt-2.4.0-cp313-cp313-win32.whl", hash = "sha256:49bb5a572469e0e18163a8ec2aa972135a0929899ecbe627665f274506e1b5b4", size = 91171, upload-time = "2026-08-30T04:40:12.895Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1e/e782b511c680dbe7369c92e7d981484aacca0cda584da1f28a84cd9a8e1a/wrapt-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:b1737f46b1e4a81eb93500a7f2854319e1c7a86e8863fb050b7b4daadd5a4178", size = 96178, upload-time = "2026-08-30T04:40:14.336Z" }, + { url = "https://files.pythonhosted.org/packages/9f/62/095ba31123fa5dd482d6183c05200b061314aabbd5442c010aba4b03ff1c/wrapt-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:f1e9e088094f4895f84ab043e7d59401df137d663efbf1e80c82144882960830", size = 92949, upload-time = "2026-08-30T04:40:15.935Z" }, + { url = "https://files.pythonhosted.org/packages/1f/dd/1f269e4daf0c992f675e1ca2de6b1683b761c6d0aeb6c7b4b412486823ea/wrapt-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:788e473d1a6786d29d577b1e2bd95e214c09cdafde84907c522c31069c9acfac", size = 96386, upload-time = "2026-08-30T04:40:17.584Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/7ecef06d33c0121c68d66a8a695efe67ebaa57218c1c61c585eca2a6117a/wrapt-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:947bd4b3438167b3638bf5477cb83a068a586ffb6d331ac427f39839c2b93b3c", size = 96532, upload-time = "2026-08-30T04:40:19.116Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e3/8fdc9eba0e6cbbfe8303e1e807d734691309a27970b2ea458d099f1a46b0/wrapt-2.4.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3a69161cae7f0dca44c89c1d14146b4a0508a0c3cad98b3f2db1f4e9016c94ba", size = 228775, upload-time = "2026-08-30T04:40:20.604Z" }, + { url = "https://files.pythonhosted.org/packages/f4/77/4ac5882abfb29bf9821c5fa5cf9f30241a194e0f47faa2682b9b29765278/wrapt-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0536f5d85ff6a157ebe7e0fe08c5479943742cf1ce59569075a66159efcbc495", size = 229029, upload-time = "2026-08-30T04:40:22.186Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c5/8a3608311a02faf3e5c072da38d06a7c623150fc258e29f18fe377d91703/wrapt-2.4.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5f041ed6a4d571010944bd6cfad9072db463e1851877b6d3227467a44af37456", size = 210436, upload-time = "2026-08-30T04:40:23.953Z" }, + { url = "https://files.pythonhosted.org/packages/de/90/e0cbc43f435fd39df25460e9f173e7b96f3dac5c7f66be41c7227166f021/wrapt-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f7fed45dbadf5d98a52bfff9624d3cca00affeb9543d493c9632b7a53cdd35c9", size = 226586, upload-time = "2026-08-30T04:40:25.507Z" }, + { url = "https://files.pythonhosted.org/packages/81/6c/7e5f2143228635ec139ef6df733dc477049f7d96a0c49deb23944a73ed6a/wrapt-2.4.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5cc2e7c7b6032e11a2b367a9baadaf0c5241feff2d8205260d87f1aa6dbdf84b", size = 208880, upload-time = "2026-08-30T04:40:27.128Z" }, + { url = "https://files.pythonhosted.org/packages/10/16/1de84402bb7a0916e10739bf6586e031244172b299e87c8cff2a04baf9ff/wrapt-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:72826910a1cf5a081234720fd43011304b899acfee219af49148155b4d795533", size = 216689, upload-time = "2026-08-30T04:40:28.844Z" }, + { url = "https://files.pythonhosted.org/packages/20/19/cd6bd5050381a541b44be97c4e0994eed60c5f439f4314f95eb5777d6c1a/wrapt-2.4.0-cp314-cp314-win32.whl", hash = "sha256:0eca69c9e93518240abe8801fb9b2726116a6e48172e4564c2651a2e14521747", size = 91581, upload-time = "2026-08-30T04:40:30.592Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f8/b642f3184619adde676ad449030bcbeae6cc78ea07a92f0b5fddeec4c4e6/wrapt-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:63b94f401d7ae3a9a3027472fd3a3ff38afd2ed293b2f0b3b84a6d133a9f99a3", size = 96510, upload-time = "2026-08-30T04:40:32.1Z" }, + { url = "https://files.pythonhosted.org/packages/4d/3b/3415a18b91221261eeac85bf8ee23dfb0e2a39d76b9703a797efca177439/wrapt-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:6b3e082d43f592fcd381aee46354a11ce887a813ce5bbcedd9766fd681723c09", size = 93648, upload-time = "2026-08-30T04:40:33.563Z" }, + { url = "https://files.pythonhosted.org/packages/ac/90/80cf6a09e9599a11249775928df9bb790b82471e4312b847a861ffb2c2ed/wrapt-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:09064c7be688c38c3ff125ce86bc26b69b5d78dd56062c3ddd9c814b2a25f1e1", size = 99615, upload-time = "2026-08-30T04:40:35.134Z" }, + { url = "https://files.pythonhosted.org/packages/b2/da/c1d3245abb911a42584f8f7e9781995bdc41345c7affba75cf7e376c85ac/wrapt-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4f8ddff4bbb75916be36da5169b8b9d475b59a1bd24acdb7551bb2c71be9aaac", size = 100031, upload-time = "2026-08-30T04:40:36.641Z" }, + { url = "https://files.pythonhosted.org/packages/84/46/8ec4941d0abbb010df7caf0a34840ca0128177389843b0f5ef2f9ee48ac5/wrapt-2.4.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e9f8017443595870aa31f46125553a5c55ce95a26a267b96261baee6ba566d83", size = 269389, upload-time = "2026-08-30T04:40:38.212Z" }, + { url = "https://files.pythonhosted.org/packages/14/b5/a0ae1b431cc1f49a545d32b8b678a5788c50583ecf0ecb85dc0c7f95b4f6/wrapt-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:328eb2d978ca3a6ae25f8d8fe560bf8f4bc9778b5932e7b142664eef05b92e8f", size = 281081, upload-time = "2026-08-30T04:40:40.045Z" }, + { url = "https://files.pythonhosted.org/packages/c7/24/dfaf53dd3bdb0703524a9367b48e2a64ea86433fcc854b5f14be6a8e0e39/wrapt-2.4.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7a057d376d994da6bd1bbf955ecfda699aa7353826f98847f5605e1801abdfd4", size = 249637, upload-time = "2026-08-30T04:40:41.657Z" }, + { url = "https://files.pythonhosted.org/packages/3e/27/bdd82044d7503c2bfa78afcc89881f82a1b82b5d2013aabab853d339ce2a/wrapt-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3367a5212212c9393e0d3ca6ae029b3a8fa40c5896e4a985d43fe8a4b8322f0d", size = 275322, upload-time = "2026-08-30T04:40:43.408Z" }, + { url = "https://files.pythonhosted.org/packages/c4/82/04f4228eb3fb348d660dd1ea7225e53665b1809df2273ff4861d4d33b741/wrapt-2.4.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c4fca1e63af6675af3df7cdfcd5a0c878b5e655c7e48611ced9dc8d62183a11d", size = 247292, upload-time = "2026-08-30T04:40:45.457Z" }, + { url = "https://files.pythonhosted.org/packages/a2/20/67b2968fa9200458446c51b36a435adb6906083428b70fafb4caf92d4dc2/wrapt-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:694005fdc3002ade0f21641408c588028abde03c85961f3ba7727d8bead3ed6b", size = 264586, upload-time = "2026-08-30T04:40:47.079Z" }, + { url = "https://files.pythonhosted.org/packages/d7/fd/0db9ba03e08a7663f52455e95520c723f567bc037bffc6699950fcc456c4/wrapt-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:332d9bad7e9b718974bb2a576504c4956f45b4a0fcd7b3bb7827279167550464", size = 93752, upload-time = "2026-08-30T04:40:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/3f/87/ced171220935c696b157207385fa6be5675558a74655479f071d95a00f1d/wrapt-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6d57264c9dfcf37d2bf0b0fbec68d0f6184fc5617267619ada04d03e8b0231f3", size = 99890, upload-time = "2026-08-30T04:40:50.407Z" }, + { url = "https://files.pythonhosted.org/packages/a3/af/4a10c9a6d3b7ae41f830978c28d33a59ceb29537bd6875d2abfe78db4b41/wrapt-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f43af38a642c3d6062e9740d8f5cc0feb5dbe0da516702df892147393b8cb14d", size = 96033, upload-time = "2026-08-30T04:40:51.933Z" }, + { url = "https://files.pythonhosted.org/packages/a0/df/3a0b6225ab88bd47090df70391c059a3308057638f8fc0ae32e8ac9d1886/wrapt-2.4.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:430fde1a116df3ceb5c29035de1da6609b70e680d9b8ce3ee624422f3fe0978c", size = 96389, upload-time = "2026-08-30T04:40:53.555Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6f/803b0d0e14de11781f0e938e6f7d6e29e79652139fe70d7513460357ac78/wrapt-2.4.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:7d28f8f35a02d49f75f57fa4e755db4ba33f65841c0de64cd65b253916f5bf06", size = 96557, upload-time = "2026-08-30T04:40:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e8/46571e1218d0494604a7aadc4c898c738c4b179052327ee1e57e278cebd6/wrapt-2.4.0-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efd9a4be6785295e471f71efdf5682bd11d5b822b9665e6e1b4844917cf2f7ac", size = 229230, upload-time = "2026-08-30T04:40:56.703Z" }, + { url = "https://files.pythonhosted.org/packages/78/2e/0cab15fcaec56096a5734feace3620bc01edc885653be04bd756f84a6784/wrapt-2.4.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75529a2fb569a671cf162f762c1b576f569f571b55ec7f3481258ca842ba507f", size = 229444, upload-time = "2026-08-30T04:40:58.51Z" }, + { url = "https://files.pythonhosted.org/packages/e7/9e/a92c049371a2675f98a0381ab2951f984866d1ba4de0e0771d6a31fdaa2b/wrapt-2.4.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66e7512c0d324cc37bba1def2be1fc365cbb685d3aa393a8f6f4d2d00202881d", size = 212482, upload-time = "2026-08-30T04:41:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/ee/3b/8b5b57d0ff24edcd3421dbaeb4e94c89be3616824e47708f4e13f25ae3d7/wrapt-2.4.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:5f3bdfc35c83b562fcaebc0f24593045e5ed9f3b633adafd35222718a0ec38fa", size = 227017, upload-time = "2026-08-30T04:41:01.918Z" }, + { url = "https://files.pythonhosted.org/packages/0e/20/124b40bfd9585848db5a5aa6741d0c8dbf378dd995c6c2d95f090d9cf540/wrapt-2.4.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:d5f45bead708e2c0014be5e98531ce7202916b098a208c7be83c6ceb0a2559fa", size = 210498, upload-time = "2026-08-30T04:41:03.617Z" }, + { url = "https://files.pythonhosted.org/packages/4b/bf/89db9d5a80a9f2af52b24bdfdb5392be80bc0f0fd39fc39d1aab72afd0bd/wrapt-2.4.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:d294576fddac636589e4deccfe782e8f429da10f167c1985c4d51071de3672b7", size = 217046, upload-time = "2026-08-30T04:41:05.473Z" }, + { url = "https://files.pythonhosted.org/packages/3b/0b/021c9d6ce64c639894bffdaa7a895ddd4187abfefb2873ce55e536cd9d56/wrapt-2.4.0-cp315-cp315-win32.whl", hash = "sha256:0191d717dfbb8e519e7bfd4775e5b9bd57e359b3a09ab5db1ea47f6025b4d845", size = 91591, upload-time = "2026-08-30T04:41:07.086Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d3/6ebd944041cea0ac4a108a4739510ed2dc891a3f3216e4f7bf0650f5b5a6/wrapt-2.4.0-cp315-cp315-win_amd64.whl", hash = "sha256:e8df31a126a0a247c1aa379e30873839de03912dea09ca360c680f3625d815df", size = 96517, upload-time = "2026-08-30T04:41:08.671Z" }, + { url = "https://files.pythonhosted.org/packages/96/84/7c5e52e450f80ba76fd0282dccf7c79cd004ebd8ccabd0903064d3d2c56e/wrapt-2.4.0-cp315-cp315-win_arm64.whl", hash = "sha256:e9e7e94472f0e3f1447caf27e1939eb384d0e87972a35a05f5c2e0968e9c01af", size = 93652, upload-time = "2026-08-30T04:41:10.258Z" }, + { url = "https://files.pythonhosted.org/packages/35/89/f08ff45d7646de29750932805cc3b1e86b6ac3128015b293ed45fa8efe86/wrapt-2.4.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:8828369b7d3e93c547cc8ad931b5a57b4e8d174035c82762fb1091e7d05ac9f5", size = 99610, upload-time = "2026-08-30T04:41:11.933Z" }, + { url = "https://files.pythonhosted.org/packages/4b/c2/f9a3c40901a36c6bb7ecaff8e1e54af78fa7fa0b95a0e54d13d3a24c8a0a/wrapt-2.4.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:413e757dce7a43fcda8bb8441994b1127492ffac6a5803af777d44516df8c6e2", size = 100064, upload-time = "2026-08-30T04:41:13.492Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e1/e2437f17f2a1ec292056e2fcafe1248269ebc39502f2ffe79424bf86f8a6/wrapt-2.4.0-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:75944792cf6b99262d649d55710bf5901f7013fbb212c7a1d736b97a20517607", size = 269421, upload-time = "2026-08-30T04:41:15.238Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d0/c98d6548dc4c7d12ab9baa192234ca1a57e141afd283252b448faddbd9ef/wrapt-2.4.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:648d1d4f94e8a0a1656675c755f40d2f0ee5fe92c449ab45326f4ecc2738cbe8", size = 281452, upload-time = "2026-08-30T04:41:16.939Z" }, + { url = "https://files.pythonhosted.org/packages/a3/57/673168e00aa03725148ce621ed201b75df4e787a57acd48fecefd2725600/wrapt-2.4.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a112a1bfdd2621e4344cb0a32dbaab80636b32dac1b055d03fbb2a67d806d1db", size = 250358, upload-time = "2026-08-30T04:41:18.716Z" }, + { url = "https://files.pythonhosted.org/packages/78/0b/f2e576de5bf53ef5b578470104ea93f33e273a704c825131bc1719fffc42/wrapt-2.4.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0972cd025f4c86fa2d8abd953d9f875779935343af58b4ce019ff89573fc65bd", size = 275654, upload-time = "2026-08-30T04:41:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/9347b2e236346b1ba4cb28b82b205b8a377bb2da9417cb81bbe3d25816d7/wrapt-2.4.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:c246aaed719dcdb62eeb7b8d9306a6237777226ef3baad35919c4ae134c91ce7", size = 248662, upload-time = "2026-08-30T04:41:22.371Z" }, + { url = "https://files.pythonhosted.org/packages/a5/36/3b84d9e1ac8393bf2c94272760a2d361dc394ac30301e6d6dbd6583ade2d/wrapt-2.4.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:1656de3835f760781c9b974bce07d8c04edb9c9ad7ad67264aee69cd68a1db09", size = 264813, upload-time = "2026-08-30T04:41:24.116Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a2/de7b1de1702667b4a048318e301e26887268c17b07c8b9797cea06b10aee/wrapt-2.4.0-cp315-cp315t-win32.whl", hash = "sha256:d8e6e1e5dc684dfce7c33fc8b67a08ba2af94f3a45cfc70d5c1d6a839d2caf97", size = 93753, upload-time = "2026-08-30T04:41:25.793Z" }, + { url = "https://files.pythonhosted.org/packages/09/50/4e7ef58c4eb058861ceddc0d1f94a6ed87f62e1cb27783c60b2897ef7e58/wrapt-2.4.0-cp315-cp315t-win_amd64.whl", hash = "sha256:85ed3c67fd39e8d9a36c224758cb6f2f4eb277d07ea677930caa0008c18ec002", size = 99888, upload-time = "2026-08-30T04:41:27.305Z" }, + { url = "https://files.pythonhosted.org/packages/68/64/d15740c763dd0ddea2338ad42e3bd4a84f8702e16083e7ff61674c504a13/wrapt-2.4.0-cp315-cp315t-win_arm64.whl", hash = "sha256:36b56a4fba13b34ed8ff307557325fff215de0a58b5dbaef2c50e4d8aa39dbd1", size = 96039, upload-time = "2026-08-30T04:41:29.062Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/fafe0002f572ced999c792cfe8b05d39269c63d8193d15d25bd828bcad7a/wrapt-2.4.0-py3-none-any.whl", hash = "sha256:18aabd9301d06026f5900538051773d6f87f65ae02cdc60de482df978513dc0a", size = 73713, upload-time = "2026-08-30T04:41:49.805Z" }, +]