From 1a346bbf534be0b756d286a3892462a41f53ec40 Mon Sep 17 00:00:00 2001 From: "Michael A. Neilson" Date: Thu, 30 Jul 2026 18:15:36 +0000 Subject: [PATCH 01/14] Initial setup of Otel Spans and w3c trace context. --- cwms/api.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/cwms/api.py b/cwms/api.py index 4f82099..84cef97 100644 --- a/cwms/api.py +++ b/cwms/api.py @@ -44,6 +44,26 @@ from cwms.cwms_types import JSON, RequestParams +from opentelemetry.instrumentation.requests import RequestsInstrumentor + + +from opentelemetry import trace, baggage +from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator +#from opentelemetry.baggage.propagation import W3CBaggagePropagator +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ConsoleSpanExporter, BatchSpanProcessor + +trace.set_tracer_provider(TracerProvider()) +trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(ConsoleSpanExporter())) + +tracer = trace.get_tracer(__name__) + +# Propagate the current W3CTraceContext to the request. +def request_hook(span, request): + TraceContextTextMapPropagator().inject(request.headers, None) + +RequestsInstrumentor().instrument(tracer = tracer, tracer_provider = trace.get_tracer_provider(), request_hook = request_hook) + # Specify the default API root URL and version. API_ROOT = "https://cwms-data.usace.army.mil/cwms-data/" API_VERSION = 2 From 07e73f278e46c449f81faa807917003d12eb24c7 Mon Sep 17 00:00:00 2001 From: Adam Korynta <47677856+adamkorynta@users.noreply.github.com> Date: Wed, 5 Aug 2026 15:32:01 -0700 Subject: [PATCH 02/14] don't retry time series retrieval on 404 errors (#303) * don't retry time series retrieval on 404 errors * add test for handling 404 errors on missing time series chunks --- cwms/timeseries/timeseries.py | 3 +++ tests/cda/timeseries/timeseries_CDA_test.py | 29 +++++++++++++++++++++ tests/mock/timeseries/timeseries_test.py | 21 +++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/cwms/timeseries/timeseries.py b/cwms/timeseries/timeseries.py index 17a3294..9ce3dfe 100644 --- a/cwms/timeseries/timeseries.py +++ b/cwms/timeseries/timeseries.py @@ -166,6 +166,9 @@ def _call_with_retry(fn: Any, *args: Any, attempts: int = _CHUNK_ATTEMPTS) -> An try: return fn(*args) except Exception as e: + status_code = getattr(getattr(e, "response", None), "status_code", None) + if status_code == 404: + raise if i == attempts - 1: raise logging.warning(f"chunk attempt {i + 1}/{attempts} failed: {e}") diff --git a/tests/cda/timeseries/timeseries_CDA_test.py b/tests/cda/timeseries/timeseries_CDA_test.py index 83abc05..4e8a8c9 100644 --- a/tests/cda/timeseries/timeseries_CDA_test.py +++ b/tests/cda/timeseries/timeseries_CDA_test.py @@ -22,6 +22,7 @@ TEST_TSID_COPY_NULLS = f"{TEST_LOCATION_ID}.Stage.Inst.15Minutes.0.Raw-Copy-Nulls" TS_ID_REV_TEST = TEST_TSID_MULTI.replace("Raw-Multi", "Raw-Rev-Test") TEST_TSID_CHUNK_PARTIAL = f"{TEST_LOCATION_ID}.Stage.Inst.15Minutes.0.Raw-Multi-Partial" +TEST_TSID_MISSING = f"{TEST_LOCATION_ID}.Stage.Inst.15Minutes.0.Raw-Missing-404" # Generate 15-minute interval timestamps START_DATE_CHUNK_MULTI = datetime(2025, 7, 31, 0, 0, tzinfo=timezone.utc) END_DATE_CHUNK_MULTI = datetime(2025, 9, 30, 23, 45, tzinfo=timezone.utc) @@ -37,6 +38,7 @@ TEST_TSID_CHUNK_NULLS, TEST_TSID_COPY_NULLS, TEST_TSID_CHUNK_PARTIAL, + TEST_TSID_MISSING, TEST_TSID_DELETE, ] @@ -390,6 +392,33 @@ def sabotaged(selector, endpoint, param, begin, end): assert "simulated CDA failure" in error_msg +def test_get_timeseries_missing_chunk_404_real_api(): + """A missing time series should fail once with a 404, not retry.""" + + max_days = 14 + chunks = ts.chunk_timeseries_time_range( + START_DATE_CHUNK_MULTI, + END_DATE_CHUNK_MULTI.replace(tzinfo=timezone.utc), + timedelta(days=max_days), + ) + assert len(chunks) > 1, "Test requires multiple chunks to exercise retry scope" + + with pytest.raises(RuntimeError) as exc_info: + ts.get_timeseries( + ts_id=TEST_TSID_MISSING, + office_id=TEST_OFFICE, + begin=START_DATE_CHUNK_MULTI, + end=END_DATE_CHUNK_MULTI, + max_days_per_chunk=max_days, + unit="SI", + ) + + error_msg = str(exc_info.value) + assert "chunk(s) failed to fetch" in error_msg + assert "Failed to fetch data from" in error_msg + assert "Not Found" in error_msg or "404" in error_msg + + def test_store_timesereis_chunk_to_with_null_values(): # Define parameters ts_id = TEST_TSID_CHUNK_NULLS diff --git a/tests/mock/timeseries/timeseries_test.py b/tests/mock/timeseries/timeseries_test.py index 03e64c9..e5c906b 100644 --- a/tests/mock/timeseries/timeseries_test.py +++ b/tests/mock/timeseries/timeseries_test.py @@ -251,6 +251,27 @@ def test_get_timeseries_paging(requests_mock): assert data.df.shape == (30, 3) +def test_call_with_retry_does_not_retry_404(): + class ResponseStub: + url = "https://mockwebserver.cwms.gov/timeseries" + status_code = 404 + reason = "Not Found" + content = b"" + + call_count = 0 + + def failing_call(): + nonlocal call_count + call_count += 1 + raise cwms.api.ApiError(ResponseStub()) + + with pytest.raises(cwms.api.ApiError) as exc_info: + timeseries._call_with_retry(failing_call) + + assert exc_info.value.response.status_code == 404 + assert call_count == 1 + + def test_get_timeseries_group_default(requests_mock): group_id = "USGS TS Data Acquisition" category_id = "Data Acquisition" From bcde948f4f446494e0169613e5baa0505efd78cf Mon Sep 17 00:00:00 2001 From: Zack Olson Date: Fri, 14 Aug 2026 14:13:13 -0700 Subject: [PATCH 03/14] Initial implementation for properties endpoint support (#301) * Initial implementation for properties endpoint support --- cwms/__init__.py | 1 + cwms/properties/properties.py | 168 ++++++++++++++++++++ tests/cda/properties/properties_CDA_test.py | 114 +++++++++++++ tests/resources/properties.json | 16 ++ tests/resources/property.json | 7 + 5 files changed, 306 insertions(+) create mode 100644 cwms/properties/properties.py create mode 100644 tests/cda/properties/properties_CDA_test.py create mode 100644 tests/resources/properties.json create mode 100644 tests/resources/property.json diff --git a/cwms/__init__.py b/cwms/__init__.py index 8d75b4d..d743cc2 100644 --- a/cwms/__init__.py +++ b/cwms/__init__.py @@ -18,6 +18,7 @@ from cwms.projects.project_locks import * from cwms.projects.projects import * from cwms.projects.water_supply.accounting import * +from cwms.properties.properties import * from cwms.ratings.ratings import * from cwms.ratings.ratings_spec import * from cwms.ratings.ratings_template import * diff --git a/cwms/properties/properties.py b/cwms/properties/properties.py new file mode 100644 index 0000000..14f5bf6 --- /dev/null +++ b/cwms/properties/properties.py @@ -0,0 +1,168 @@ +# Copyright (c) 2026 +# United States Army Corps of Engineers - Hydrologic Engineering Center (USACE/HEC) +# All Rights Reserved. USACE PROPRIETARY/CONFIDENTIAL. +# Source may not be released without written approval from HEC + +from typing import Optional + +import cwms.api as api +from cwms.cwms_types import JSON, Data + + +def get_properties( + office_mask: Optional[str] = None, + category_id_mask: Optional[str] = None, + name_mask: Optional[str] = None, +) -> Data: + """ + Returns matching CWMS Property Data. + + Parameters + ---------- + office_mask: string, optional + Filters properties to the specified office mask + category_id_mask: string, optional + Filters properties to the specified category mask + name_mask: string, optional + Filters properties to the specified name mask + + Returns + ------- + cwms data type + """ + + endpoint = "properties" + params = { + "office-mask": office_mask, + "category-id-mask": category_id_mask, + "name-mask": name_mask, + } + + response = api.get(endpoint, params, api_version=1) + return Data(response) + + +def get_property( + name: str, office: str, category_id: str, default_value: Optional[str] = None +) -> Data: + """ + Returns CWMS Property Data. + + Parameters + ---------- + name: string + Specifies the name of the property to be retrieved. + office: string + Specifies the owning office of the property to be retrieved. + category_id: string + Specifies the category id of the property to be retrieved. + default_value: string, optional + Specifies the default value if the property does not exist. + + Returns + ------- + cwms data type + """ + + endpoint = f"properties/{name}" + params = { + "office": office, + "category-id": category_id, + "default-value": default_value, + } + + response = api.get(endpoint, params, api_version=1) + return Data(response) + + +def create_property(data: JSON) -> None: + """ + Create CWMS Property. + + Parameters + ---------- + data: JSON dictionary + Property data to be stored. + Example: + { + "office-id": "string", + "name": "string", + "category": "string", + "value": "string", + "comment": "string" + } + + Returns + ------- + None + """ + + endpoint = "properties" + + if data is None: + raise ValueError("Cannot store a property without JSON data") + + return api.post(endpoint, data, api_version=1) + + +def update_property(name: str, data: JSON) -> None: + """ + Update CWMS Property. + + Parameters + ---------- + name: string + Specifies the name of the property to be updated. + data: JSON dictionary + Property data to be updated. + Example: + { + "office-id": "string", + "name": "string", + "category": "string", + "value": "string", + "comment": "string" + } + + Returns + ------- + None + """ + + endpoint = f"properties/{name}" + + if name is None: + raise ValueError("Must specify a property name to update") + + if data is None: + raise ValueError("Cannot update a property without JSON data") + + return api.patch(endpoint, data, api_version=1) + + +def delete_property(name: str, office: str, category_id: str) -> None: + """ + Delete CWMS Property. + + Parameters + ---------- + name: string + Specifies the name of the property to be deleted. + office: string + Specifies the owning office of the property to be deleted. + category_id: string + Specifies the category id of the property to be deleted. + + Returns + ------- + None + """ + + endpoint = f"properties/{name}" + + params = { + "office": office, + "category-id": category_id, + } + + return api.delete(endpoint, params, api_version=1) diff --git a/tests/cda/properties/properties_CDA_test.py b/tests/cda/properties/properties_CDA_test.py new file mode 100644 index 0000000..474a734 --- /dev/null +++ b/tests/cda/properties/properties_CDA_test.py @@ -0,0 +1,114 @@ +# Copyright (c) 2026 +# United States Army Corps of Engineers - Hydrologic Engineering Center (USACE/HEC) +# All Rights Reserved. USACE PROPRIETARY/CONFIDENTIAL. +# Source may not be released without written approval from HEC + +import pytest + +import cwms.api as api +import cwms.properties.properties as properties +from cwms.api import ApiError + +TEST_OFFICE = "SPK" +TEST_CATEGORY = "PytestCategory" +TEST_NAME = "PytestProperty" +TEST_VALUE = "PytestValue" +TEST_COMMENT = "PytestComment" + +TEST_PROPERTY_DATA = { + "office-id": TEST_OFFICE, + "name": TEST_NAME, + "category": TEST_CATEGORY, + "value": TEST_VALUE, + "comment": TEST_COMMENT, +} + + +@pytest.fixture(scope="module", autouse=True) +def setup_data(): + # Clean up any leftover state from a prior aborted run before starting. + try: + properties.delete_property(TEST_NAME, TEST_OFFICE, TEST_CATEGORY) + except Exception: + pass + + properties.create_property(TEST_PROPERTY_DATA) + yield + try: + properties.delete_property(TEST_NAME, TEST_OFFICE, TEST_CATEGORY) + except Exception: + pass + + +@pytest.fixture(autouse=True) +def init_session(): + # Session initialization is handled by environment or global config in CDA tests + print("Initializing CWMS API session for properties tests...") + + +def test_create_property(): + properties.create_property(TEST_PROPERTY_DATA) + props = properties.get_property(TEST_NAME, TEST_OFFICE, TEST_CATEGORY) + assert props.json.get("name") == TEST_NAME + assert props.json.get("office-id") == TEST_OFFICE + assert props.json.get("category") == TEST_CATEGORY + assert props.json.get("value") == TEST_VALUE + assert props.json.get("comment") == TEST_COMMENT + + +def test_get_properties(): + data = properties.get_properties( + office_mask=TEST_OFFICE, category_id_mask=TEST_CATEGORY + ) + assert data is not None + # Check if our test property is in the returned list + found = False + for prop in data.json: + if prop.get("name") == TEST_NAME and prop.get("office-id") == TEST_OFFICE: + found = True + break + assert found + + +def test_get_property(): + data = properties.get_property(TEST_NAME, TEST_OFFICE, TEST_CATEGORY) + assert data is not None + assert data.json.get("name") == TEST_NAME + assert data.json.get("office-id") == TEST_OFFICE + assert data.json.get("category") == TEST_CATEGORY + assert data.json.get("value") == TEST_VALUE + + +def test_update_property(): + updated_value = "UpdatedPytestValue" + updated_data = TEST_PROPERTY_DATA.copy() + updated_data["value"] = updated_value + + properties.update_property(TEST_NAME, updated_data) + + data = properties.get_property(TEST_NAME, TEST_OFFICE, TEST_CATEGORY) + assert data.json.get("value") == updated_value + + +def test_delete_property(): + # Create a temporary property to delete + temp_name = "TempDeleteProperty" + temp_data = TEST_PROPERTY_DATA.copy() + temp_data["name"] = temp_name + + properties.create_property(temp_data) + + # Verify it was created + data = properties.get_property(temp_name, TEST_OFFICE, TEST_CATEGORY) + assert data.json.get("name") == temp_name + + # Delete it + properties.delete_property(temp_name, TEST_OFFICE, TEST_CATEGORY) + + # Verify it was deleted + data = properties.get_property(temp_name, TEST_OFFICE, TEST_CATEGORY) + assert data.json.get("name") == temp_name + assert data.json.get("value") is None + assert data.json.get("comment") is None + assert data.json.get("category") == TEST_CATEGORY + assert data.json.get("office-id") == TEST_OFFICE diff --git a/tests/resources/properties.json b/tests/resources/properties.json new file mode 100644 index 0000000..1a94603 --- /dev/null +++ b/tests/resources/properties.json @@ -0,0 +1,16 @@ +[ + { + "name": "TestProperty1", + "office-id": "SWT", + "category-id": "TestCategory", + "value": "Value1", + "comment": "Comment1" + }, + { + "name": "TestProperty2", + "office-id": "SWT", + "category-id": "TestCategory", + "value": "Value2", + "comment": "Comment2" + } +] diff --git a/tests/resources/property.json b/tests/resources/property.json new file mode 100644 index 0000000..f127c64 --- /dev/null +++ b/tests/resources/property.json @@ -0,0 +1,7 @@ +{ + "name": "TestProperty1", + "office-id": "SWT", + "category": "TestCategory", + "value": "Value1", + "comment": "Comment1" +} From 99e31852fbe2ef7d5010de49c8e5945f3979a211 Mon Sep 17 00:00:00 2001 From: "Michael A. Neilson" Date: Mon, 17 Aug 2026 21:43:41 +0000 Subject: [PATCH 04/14] Correct image reference. --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 03b7892..690e152 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -52,7 +52,7 @@ services: condition: service_completed_successfully traefik: condition: service_healthy - image: ${CWMS_DATA_API_IMAGE:-ghcr.io/usace/cwms-data-api:latest} + image: ${CWMS_DATA_API_IMAGE:-ghcr.io/usace/cwms-data-api:develop-nightly} restart: unless-stopped volumes: - ./compose_files/pki/certs:/conf/ From 4d1e1e3bc4697bc996b51665f6f438718f93a0da Mon Sep 17 00:00:00 2001 From: "Michael A. Neilson" Date: Mon, 17 Aug 2026 22:39:13 +0000 Subject: [PATCH 05/14] Got the propogator not to crash --- cwms/api.py | 20 +++++++++++++------- pyproject.toml | 7 +++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/cwms/api.py b/cwms/api.py index 84cef97..ff60cca 100644 --- a/cwms/api.py +++ b/cwms/api.py @@ -36,7 +36,7 @@ from json import JSONDecodeError from typing import Any, Optional, cast -from requests import Response, adapters +from requests import Request, Response, adapters from requests.exceptions import RetryError as RequestsRetryError from requests_toolbelt import sessions # type: ignore from requests_toolbelt.sessions import BaseUrlSession # type: ignore @@ -45,22 +45,28 @@ from cwms.cwms_types import JSON, RequestParams from opentelemetry.instrumentation.requests import RequestsInstrumentor - - -from opentelemetry import trace, baggage +from opentelemetry import propagate, trace, baggage from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator #from opentelemetry.baggage.propagation import W3CBaggagePropagator from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ConsoleSpanExporter, BatchSpanProcessor +from opentelemetry.instrumentation.logging import LoggingInstrumentor + +LOGGER = logging.getLogger(__name__) trace.set_tracer_provider(TracerProvider()) -trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(ConsoleSpanExporter())) +#trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(ConsoleSpanExporter())) + +LoggingInstrumentor().instrument(inject_trace_context=True) tracer = trace.get_tracer(__name__) +propagate.set_global_textmap(TraceContextTextMapPropagator()) + # Propagate the current W3CTraceContext to the request. -def request_hook(span, request): - TraceContextTextMapPropagator().inject(request.headers, None) +def request_hook(span: trace.Span, request: Request): + ctx = trace.set_span_in_context(span) + propagate.get_global_textmap().inject(request.headers, ctx) RequestsInstrumentor().instrument(tracer = tracer, tracer_provider = trace.get_tracer_provider(), request_hook = request_hook) diff --git a/pyproject.toml b/pyproject.toml index 45e7ed9..71b8d69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,3 +43,10 @@ comments_min_spaces_from_content = 1 whitelines = 1 explicit_start = false preserve_quotes = true + + +[tool.pytest.ini_options] +log_cli = true +log_format = "%(levelname)s:%(name)s:%(message)s - %(otelSpanID)s %(otelTraceID)s %(otelServiceName)s %(otelTraceSampled)s" +log_file = "tests/run.log" +log_file_level = "DEBUG" \ No newline at end of file From 8a9d90b0d07b086fd1e8c602401f48ca0f206470 Mon Sep 17 00:00:00 2001 From: "Michael A. Neilson" Date: Tue, 18 Aug 2026 15:03:38 +0000 Subject: [PATCH 06/14] cwms-python side of things appears to be behaving. --- cwms/api.py | 33 ++++++++++++++++++--------------- cwms/timeseries/timeseries.py | 15 ++++++++------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/cwms/api.py b/cwms/api.py index ff60cca..c0eda66 100644 --- a/cwms/api.py +++ b/cwms/api.py @@ -34,7 +34,7 @@ import logging from http import HTTPStatus from json import JSONDecodeError -from typing import Any, Optional, cast +from typing import Any, Optional, cast, Final from requests import Request, Response, adapters from requests.exceptions import RetryError as RequestsRetryError @@ -44,29 +44,30 @@ from cwms.cwms_types import JSON, RequestParams +LOGGER: Final[logging.Logger] = logging.getLogger(__name__) + + + +# Setup telemetry from opentelemetry.instrumentation.requests import RequestsInstrumentor -from opentelemetry import propagate, trace, baggage +from opentelemetry import propagate, trace from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator -#from opentelemetry.baggage.propagation import W3CBaggagePropagator +from opentelemetry.sdk.resources import Resource, SERVICE_NAME from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import ConsoleSpanExporter, BatchSpanProcessor from opentelemetry.instrumentation.logging import LoggingInstrumentor -LOGGER = logging.getLogger(__name__) - -trace.set_tracer_provider(TracerProvider()) -#trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(ConsoleSpanExporter())) +# Propagate the current W3CTraceContext to the request. +def request_hook(span: trace.Span, request: Request): + ctx = trace.set_span_in_context(span) + propagate.get_global_textmap().inject(request.headers, ctx) +trace.set_tracer_provider(TracerProvider(resource=Resource.create(attributes = {SERVICE_NAME:"cwms-python"}))) LoggingInstrumentor().instrument(inject_trace_context=True) tracer = trace.get_tracer(__name__) propagate.set_global_textmap(TraceContextTextMapPropagator()) -# Propagate the current W3CTraceContext to the request. -def request_hook(span: trace.Span, request: Request): - ctx = trace.set_span_in_context(span) - propagate.get_global_textmap().inject(request.headers, ctx) RequestsInstrumentor().instrument(tracer = tracer, tracer_provider = trace.get_tracer_provider(), request_hook = request_hook) @@ -216,7 +217,7 @@ def init_session( if api_root: # Ensure the API_ROOT ends with a single slash api_root = api_root.rstrip("/") + "/" - logging.debug(f"Initializing root URL: api_root={api_root}") + LOGGER.debug(f"Initializing root URL: api_root={api_root}") SESSION = sessions.BaseUrlSession(base_url=api_root) adapter = adapters.HTTPAdapter( pool_connections=pool_connections, @@ -226,7 +227,7 @@ def init_session( SESSION.mount("https://", adapter) if token: if api_key: - logging.warning( + LOGGER.warning( "Both token and api_key were provided to init_session(); using token for Authorization." ) # Ensure we don't provide the bearer text twice @@ -326,7 +327,7 @@ def _process_response(response: Response) -> Any: # Fallback for remaining content types return response.content.decode("utf-8") except JSONDecodeError as error: - logging.error( + LOGGER.error( f"Error decoding CDA response as JSON: {error} on line {error.lineno}\n\tFalling back to text" ) return response.text @@ -358,6 +359,8 @@ def get( headers = {"Accept": api_version_text(api_version)} try: with SESSION.get(endpoint, params=params, headers=headers) as response: + sentHeaders = repr(response.request.headers) + LOGGER.debug(f"request headers ={sentHeaders}") if not response.ok: logging.error(f"CDA Error: response={response}") raise ApiError(response) diff --git a/cwms/timeseries/timeseries.py b/cwms/timeseries/timeseries.py index 9ce3dfe..6340f79 100644 --- a/cwms/timeseries/timeseries.py +++ b/cwms/timeseries/timeseries.py @@ -1,7 +1,7 @@ import concurrent.futures import logging from datetime import datetime, timedelta, timezone -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple, Final import pandas as pd from pandas import DataFrame @@ -10,6 +10,7 @@ from cwms.catalog.catalog import get_ts_extents from cwms.cwms_types import JSON, Data +LOGGER: Final[logging.Logger] = logging.getLogger(__name__) def get_multi_timeseries_df( ts_ids: list[str], @@ -83,7 +84,7 @@ def get_ts_ids(ts_id: str) -> Any: } return result_dict except Exception as e: - logging.error(f"Error processing {ts_id}: {e}") + LOGGER.error(f"Error processing {ts_id}: {e}") return None with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: @@ -372,12 +373,12 @@ def get_timeseries( # replace begin with begin extent if outside extents if begin < begin_extent: begin = begin_extent - logging.debug( + LOGGER.debug( f"Requested begin was before any data in this timeseries. Reseting to {begin}" ) except Exception as e: # If getting extents fails, fall back to single-threaded mode - logging.debug( + LOGGER.debug( f"Could not retrieve time series extents ({e}). Falling back to single-threaded mode." ) @@ -399,7 +400,7 @@ def get_timeseries( ) return Data(response, selector=selector) else: - logging.debug( + LOGGER.debug( f"Fetching {len(chunks)} chunks of timeseries data with {max_workers} threads" ) # fetch the data @@ -687,7 +688,7 @@ def store_timeseries( return api.post(endpoint, data, params) actual_workers = min(max_workers, len(chunks)) - logging.debug( + LOGGER.debug( f"Storing {len(chunks)} chunks of timeseries data with {actual_workers} threads" ) @@ -709,7 +710,7 @@ def store_timeseries( start_time = chunk["values"][0][0] end_time = chunk["values"][-1][0] error_msg = f"Error storing chunk from {start_time} to {end_time}: {e}" - logging.error(error_msg) + LOGGER.error(error_msg) errors.append(error_msg) responses.append({"error": error_msg}) From 9b14d2e3d3042b90ae0fe6afa578d3d1adbc387e Mon Sep 17 00:00:00 2001 From: "Michael A. Neilson" Date: Tue, 18 Aug 2026 15:26:53 +0000 Subject: [PATCH 07/14] validated propagation of trace id to CDA instance. --- cwms/api.py | 64 ++++++++++++++++++++++++++++++++++++-------------- pyproject.toml | 2 +- 2 files changed, 48 insertions(+), 18 deletions(-) diff --git a/cwms/api.py b/cwms/api.py index c0eda66..3c36f2b 100644 --- a/cwms/api.py +++ b/cwms/api.py @@ -54,6 +54,8 @@ from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator from opentelemetry.sdk.resources import Resource, SERVICE_NAME from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter + from opentelemetry.instrumentation.logging import LoggingInstrumentor # Propagate the current W3CTraceContext to the request. @@ -61,7 +63,9 @@ def request_hook(span: trace.Span, request: Request): ctx = trace.set_span_in_context(span) propagate.get_global_textmap().inject(request.headers, ctx) -trace.set_tracer_provider(TracerProvider(resource=Resource.create(attributes = {SERVICE_NAME:"cwms-python"}))) +provider = TracerProvider(resource=Resource.create(attributes = {SERVICE_NAME:"cwms-python"})) +provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter())) +trace.set_tracer_provider(provider) LoggingInstrumentor().instrument(inject_trace_context=True) tracer = trace.get_tracer(__name__) @@ -332,7 +336,7 @@ def _process_response(response: Response) -> Any: ) return response.text - +@tracer.start_as_current_span("get") def get( endpoint: str, params: Optional[RequestParams] = None, @@ -355,20 +359,24 @@ def get( Raises: ApiError: If an error response is return by the API. """ - + span = trace.get_current_span() + span.set_attribute("endpoint", endpoint) headers = {"Accept": api_version_text(api_version)} try: with SESSION.get(endpoint, params=params, headers=headers) as response: - sentHeaders = repr(response.request.headers) - LOGGER.debug(f"request headers ={sentHeaders}") if not response.ok: logging.error(f"CDA Error: response={response}") - raise ApiError(response) + error = ApiError(response) + span.set_status(status = trace.StatusCode.ERROR) + span.record_exception(error) + raise error return _process_response(response) except RequestsRetryError as error: + span.set_status(status = trace.StatusCode.ERROR) + span.record_exception(error) raise _unwrap_retry_error(error) from None - +@tracer.start_as_current_span("get-paging") def get_with_paging( selector: str, endpoint: str, @@ -395,6 +403,8 @@ def get_with_paging( """ first_pass = True + current_span = trace.get_current_span() + current_span.set_attribute("page", "first") while (params["page"] is not None) or first_pass: temp = get(endpoint, params, api_version=api_version) if first_pass: @@ -403,12 +413,14 @@ def get_with_paging( response[selector] = response[selector] + temp[selector] if "next-page" in temp.keys(): params["page"] = temp["next-page"] + current_span.set_attribute("page", params["page"]) else: params["page"] = None + current_span.set_attribute("page", "last") first_pass = False return response - +@tracer.start_as_current_span("post") def _post_function( endpoint: str, data: Any, @@ -416,7 +428,8 @@ def _post_function( *, api_version: int = API_VERSION, ) -> Any: - + span = trace.get_current_span() + span.set_attribute("endpoint", endpoint) # post requires different headers than get for headers = {"accept": "*/*", "Content-Type": api_version_text(api_version)} if isinstance(data, dict) or isinstance(data, list): @@ -427,9 +440,14 @@ def _post_function( ) as response: if not response.ok: logging.error(f"CDA Error: response={response}") - raise ApiError(response) + error = ApiError(response) + span.set_status(status = trace.StatusCode.ERROR) + span.record_exception(error) + raise error return response except RequestsRetryError as error: + span.set_status(status = trace.StatusCode.ERROR) + span.record_exception(error) raise _unwrap_retry_error(error) from None @@ -459,7 +477,7 @@ def post( """ _post_function(endpoint=endpoint, data=data, params=params, api_version=api_version) - +@tracer.start_as_current_span("post_with_return") def post_with_returned_data( endpoint: str, data: Any, @@ -490,7 +508,7 @@ def post_with_returned_data( ) return _process_response(response) - +@tracer.start_as_current_span("patch") def patch( endpoint: str, data: Optional[Any] = None, @@ -515,7 +533,8 @@ def patch( Raises: ApiError: If an error response is return by the API. """ - + span = trace.get_current_span() + span.set_attribute("endpoint", endpoint) headers = {"accept": "*/*", "Content-Type": api_version_text(api_version)} if data and isinstance(data, dict) or isinstance(data, list): @@ -526,11 +545,16 @@ def patch( ) as response: if not response.ok: logging.error(f"CDA Error: response={response}") - raise ApiError(response) + error = ApiError(response) + span.set_status(status = trace.StatusCode.ERROR) + span.record_exception(error) + raise error except RequestsRetryError as error: + span.set_status(status = trace.StatusCode.ERROR) + span.record_exception(error) raise _unwrap_retry_error(error) from None - +@tracer.start_as_current_span("delete") def delete( endpoint: str, params: Optional[RequestParams] = None, @@ -550,12 +574,18 @@ def delete( Raises: ApiError: If an error response is return by the API. """ - + span = trace.get_current_span() + span.set_attribute("endpoint", endpoint) headers = {"Accept": api_version_text(api_version)} try: with SESSION.delete(endpoint, params=params, headers=headers) as response: if not response.ok: logging.error(f"CDA Error: response={response}") - raise ApiError(response) + error = ApiError(response) + span.set_status(status = trace.StatusCode.ERROR) + span.record_exception(error) + raise error except RequestsRetryError as error: + span.set_status(status = trace.StatusCode.ERROR) + span.record_exception(error) raise _unwrap_retry_error(error) from None diff --git a/pyproject.toml b/pyproject.toml index 71b8d69..5ff85dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,6 @@ preserve_quotes = true [tool.pytest.ini_options] log_cli = true -log_format = "%(levelname)s:%(name)s:%(message)s - %(otelSpanID)s %(otelTraceID)s %(otelServiceName)s %(otelTraceSampled)s" +log_format = "%(levelname)s:%(name)s:%(message)s - %(otelTraceID)s %(otelSpanID)s %(otelServiceName)s %(otelTraceSampled)s" log_file = "tests/run.log" log_file_level = "DEBUG" \ No newline at end of file From e3edb0f17131b577dd27e43c5e552851fc9481e4 Mon Sep 17 00:00:00 2001 From: "Michael A. Neilson" Date: Tue, 18 Aug 2026 15:49:15 +0000 Subject: [PATCH 08/14] Move OTEL configuration to test setup as cwms-python is a library. --- cwms/api.py | 18 ------------------ tests/__init__.py | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/cwms/api.py b/cwms/api.py index 3c36f2b..c88e6f9 100644 --- a/cwms/api.py +++ b/cwms/api.py @@ -51,28 +51,14 @@ # Setup telemetry from opentelemetry.instrumentation.requests import RequestsInstrumentor from opentelemetry import propagate, trace -from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator -from opentelemetry.sdk.resources import Resource, SERVICE_NAME -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter - -from opentelemetry.instrumentation.logging import LoggingInstrumentor # Propagate the current W3CTraceContext to the request. def request_hook(span: trace.Span, request: Request): ctx = trace.set_span_in_context(span) propagate.get_global_textmap().inject(request.headers, ctx) -provider = TracerProvider(resource=Resource.create(attributes = {SERVICE_NAME:"cwms-python"})) -provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter())) -trace.set_tracer_provider(provider) -LoggingInstrumentor().instrument(inject_trace_context=True) - tracer = trace.get_tracer(__name__) -propagate.set_global_textmap(TraceContextTextMapPropagator()) - - RequestsInstrumentor().instrument(tracer = tracer, tracer_provider = trace.get_tracer_provider(), request_hook = request_hook) # Specify the default API root URL and version. @@ -360,7 +346,6 @@ def get( ApiError: If an error response is return by the API. """ span = trace.get_current_span() - span.set_attribute("endpoint", endpoint) headers = {"Accept": api_version_text(api_version)} try: with SESSION.get(endpoint, params=params, headers=headers) as response: @@ -429,7 +414,6 @@ def _post_function( api_version: int = API_VERSION, ) -> Any: span = trace.get_current_span() - span.set_attribute("endpoint", endpoint) # post requires different headers than get for headers = {"accept": "*/*", "Content-Type": api_version_text(api_version)} if isinstance(data, dict) or isinstance(data, list): @@ -534,7 +518,6 @@ def patch( ApiError: If an error response is return by the API. """ span = trace.get_current_span() - span.set_attribute("endpoint", endpoint) headers = {"accept": "*/*", "Content-Type": api_version_text(api_version)} if data and isinstance(data, dict) or isinstance(data, list): @@ -575,7 +558,6 @@ def delete( ApiError: If an error response is return by the API. """ span = trace.get_current_span() - span.set_attribute("endpoint", endpoint) headers = {"Accept": api_version_text(api_version)} try: with SESSION.delete(endpoint, params=params, headers=headers) as response: diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..705d8b1 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,16 @@ +# Setup telemetry +from opentelemetry.instrumentation.requests import RequestsInstrumentor +from opentelemetry import propagate, trace +from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator +from opentelemetry.sdk.resources import Resource, SERVICE_NAME +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter + +from opentelemetry.instrumentation.logging import LoggingInstrumentor + + +provider = TracerProvider(resource=Resource.create(attributes = {SERVICE_NAME:"cwms-python-tests"})) +provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter())) +trace.set_tracer_provider(provider) +LoggingInstrumentor().instrument(inject_trace_context=True) +propagate.set_global_textmap(TraceContextTextMapPropagator()) \ No newline at end of file From 88ba566459ad56895a0b655165aee5dd3fafc6f1 Mon Sep 17 00:00:00 2001 From: "Michael A. Neilson" Date: Tue, 18 Aug 2026 16:03:35 +0000 Subject: [PATCH 09/14] Formatting. --- .gitignore | 3 +++ cwms/api.py | 34 +++++++++++++++++++++++----------- cwms/timeseries/timeseries.py | 3 ++- docker-compose.yml | 1 - tests/__init__.py | 16 ++++++++-------- 5 files changed, 36 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index 8bc2be7..8ac8f06 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,6 @@ bin/ gradle.properties .coverage + +# Don't commit test run logs. +tests/run.log diff --git a/cwms/api.py b/cwms/api.py index c88e6f9..edbba89 100644 --- a/cwms/api.py +++ b/cwms/api.py @@ -34,7 +34,7 @@ import logging from http import HTTPStatus from json import JSONDecodeError -from typing import Any, Optional, cast, Final +from typing import Any, Final, Optional, cast from requests import Request, Response, adapters from requests.exceptions import RetryError as RequestsRetryError @@ -47,19 +47,25 @@ LOGGER: Final[logging.Logger] = logging.getLogger(__name__) +from opentelemetry import propagate, trace # Setup telemetry from opentelemetry.instrumentation.requests import RequestsInstrumentor -from opentelemetry import propagate, trace + # Propagate the current W3CTraceContext to the request. def request_hook(span: trace.Span, request: Request): ctx = trace.set_span_in_context(span) propagate.get_global_textmap().inject(request.headers, ctx) + tracer = trace.get_tracer(__name__) -RequestsInstrumentor().instrument(tracer = tracer, tracer_provider = trace.get_tracer_provider(), request_hook = request_hook) +RequestsInstrumentor().instrument( + tracer=tracer, + tracer_provider=trace.get_tracer_provider(), + request_hook=request_hook, +) # Specify the default API root URL and version. API_ROOT = "https://cwms-data.usace.army.mil/cwms-data/" @@ -322,6 +328,7 @@ def _process_response(response: Response) -> Any: ) return response.text + @tracer.start_as_current_span("get") def get( endpoint: str, @@ -352,15 +359,16 @@ def get( if not response.ok: logging.error(f"CDA Error: response={response}") error = ApiError(response) - span.set_status(status = trace.StatusCode.ERROR) + span.set_status(status=trace.StatusCode.ERROR) span.record_exception(error) raise error return _process_response(response) except RequestsRetryError as error: - span.set_status(status = trace.StatusCode.ERROR) + span.set_status(status=trace.StatusCode.ERROR) span.record_exception(error) raise _unwrap_retry_error(error) from None + @tracer.start_as_current_span("get-paging") def get_with_paging( selector: str, @@ -405,6 +413,7 @@ def get_with_paging( first_pass = False return response + @tracer.start_as_current_span("post") def _post_function( endpoint: str, @@ -425,12 +434,12 @@ def _post_function( if not response.ok: logging.error(f"CDA Error: response={response}") error = ApiError(response) - span.set_status(status = trace.StatusCode.ERROR) + span.set_status(status=trace.StatusCode.ERROR) span.record_exception(error) raise error return response except RequestsRetryError as error: - span.set_status(status = trace.StatusCode.ERROR) + span.set_status(status=trace.StatusCode.ERROR) span.record_exception(error) raise _unwrap_retry_error(error) from None @@ -461,6 +470,7 @@ def post( """ _post_function(endpoint=endpoint, data=data, params=params, api_version=api_version) + @tracer.start_as_current_span("post_with_return") def post_with_returned_data( endpoint: str, @@ -492,6 +502,7 @@ def post_with_returned_data( ) return _process_response(response) + @tracer.start_as_current_span("patch") def patch( endpoint: str, @@ -529,14 +540,15 @@ def patch( if not response.ok: logging.error(f"CDA Error: response={response}") error = ApiError(response) - span.set_status(status = trace.StatusCode.ERROR) + span.set_status(status=trace.StatusCode.ERROR) span.record_exception(error) raise error except RequestsRetryError as error: - span.set_status(status = trace.StatusCode.ERROR) + span.set_status(status=trace.StatusCode.ERROR) span.record_exception(error) raise _unwrap_retry_error(error) from None + @tracer.start_as_current_span("delete") def delete( endpoint: str, @@ -564,10 +576,10 @@ def delete( if not response.ok: logging.error(f"CDA Error: response={response}") error = ApiError(response) - span.set_status(status = trace.StatusCode.ERROR) + span.set_status(status=trace.StatusCode.ERROR) span.record_exception(error) raise error except RequestsRetryError as error: - span.set_status(status = trace.StatusCode.ERROR) + span.set_status(status=trace.StatusCode.ERROR) span.record_exception(error) raise _unwrap_retry_error(error) from None diff --git a/cwms/timeseries/timeseries.py b/cwms/timeseries/timeseries.py index 6340f79..16382c2 100644 --- a/cwms/timeseries/timeseries.py +++ b/cwms/timeseries/timeseries.py @@ -1,7 +1,7 @@ import concurrent.futures import logging from datetime import datetime, timedelta, timezone -from typing import Any, Dict, List, Optional, Tuple, Final +from typing import Any, Dict, Final, List, Optional, Tuple import pandas as pd from pandas import DataFrame @@ -12,6 +12,7 @@ LOGGER: Final[logging.Logger] = logging.getLogger(__name__) + def get_multi_timeseries_df( ts_ids: list[str], office_id: str, diff --git a/docker-compose.yml b/docker-compose.yml index 690e152..4984316 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -140,4 +140,3 @@ services: - "traefik.enable=true" - "traefik.http.routers.traefik.rule=PathPrefix(`/traefik`)" - "traefik.http.routers.traefik.service=api@internal" - diff --git a/tests/__init__.py b/tests/__init__.py index 705d8b1..11dddac 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,16 +1,16 @@ # Setup telemetry -from opentelemetry.instrumentation.requests import RequestsInstrumentor from opentelemetry import propagate, trace -from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator -from opentelemetry.sdk.resources import Resource, SERVICE_NAME +from opentelemetry.instrumentation.logging import LoggingInstrumentor +from opentelemetry.instrumentation.requests import RequestsInstrumentor +from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter +from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator -from opentelemetry.instrumentation.logging import LoggingInstrumentor - - -provider = TracerProvider(resource=Resource.create(attributes = {SERVICE_NAME:"cwms-python-tests"})) +provider = TracerProvider( + resource=Resource.create(attributes={SERVICE_NAME: "cwms-python-tests"}) +) provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter())) trace.set_tracer_provider(provider) LoggingInstrumentor().instrument(inject_trace_context=True) -propagate.set_global_textmap(TraceContextTextMapPropagator()) \ No newline at end of file +propagate.set_global_textmap(TraceContextTextMapPropagator()) From 38ca1212c16194f623dad4ee5ea95226acf02add Mon Sep 17 00:00:00 2001 From: "Michael A. Neilson" Date: Tue, 18 Aug 2026 16:09:43 +0000 Subject: [PATCH 10/14] Put Otel deps in project toml. --- pyproject.toml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 5ff85dc..1293493 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,14 @@ python = "^3.9" pandas = "^2.1.3" requests-toolbelt = "^1.0.0" requests = "^2.31.0" +opentelemetry-api = "^1.44.0" +opentelemetry-distro = "^0.65b0" +opentelemetry-instrumentation = "^0.65b0" +opentelemetry-instrumentation-logging = "^0.65b0" +opentelemetry-instrumentation-requests= "^0.65b0" +opentelemetry-sdk = "^1.44.0" +opentelemetry-semantic-conventions = "^0.65b0" +opentelemetry-util-http= "^0.65b0" [tool.poetry.group.dev.dependencies] black = "^24.2.0" From 9047cb3387d9dabbd528ebd20bb3c521aae545c8 Mon Sep 17 00:00:00 2001 From: "Michael A. Neilson" Date: Tue, 18 Aug 2026 16:10:43 +0000 Subject: [PATCH 11/14] reduce test execution duplication. --- .github/workflows/code-check.yml | 6 +++++- .github/workflows/testing.yml | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/code-check.yml b/.github/workflows/code-check.yml index 306f94f..0f42eb1 100644 --- a/.github/workflows/code-check.yml +++ b/.github/workflows/code-check.yml @@ -1,7 +1,11 @@ name: Code Check # Run the workflow on all branches. -on: [push, pull_request] +on: + push: + branches: [main] + pull_request: + branches: [main] jobs: # Run basic code quality checks. diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 01dd641..9aefafb 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -1,7 +1,11 @@ name: Testing # Run the workflow on all branches. -on: [push, pull_request] +on: + push: + branches: [main] + pull_request: + branches: [main] jobs: # Run tests and generate code coverage report. From f9eaf3c8cec3ff607d9378fabda7be882def28e6 Mon Sep 17 00:00:00 2001 From: "Michael A. Neilson" Date: Tue, 18 Aug 2026 16:14:41 +0000 Subject: [PATCH 12/14] Increase minimum python. --- poetry.lock | 371 ++++++++++++++++++++++++++++++++++++++++--------- pyproject.toml | 2 +- 2 files changed, 306 insertions(+), 67 deletions(-) diff --git a/poetry.lock b/poetry.lock index a1fd39d..b50e1b8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. [[package]] name = "black" @@ -6,6 +6,7 @@ version = "24.10.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, @@ -52,6 +53,7 @@ version = "2024.8.30" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["main", "dev"] files = [ {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, @@ -63,6 +65,7 @@ version = "3.4.0" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, @@ -74,6 +77,7 @@ version = "3.4.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" +groups = ["main", "dev"] files = [ {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, @@ -188,6 +192,7 @@ version = "8.1.7" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, @@ -202,6 +207,8 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "platform_system == \"Windows\" or sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -213,6 +220,7 @@ version = "7.6.3" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "coverage-7.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6da42bbcec130b188169107ecb6ee7bd7b4c849d24c9370a0c884cf728d8e976"}, {file = "coverage-7.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c222958f59b0ae091f4535851cbb24eb57fc0baea07ba675af718fb5302dddb2"}, @@ -282,7 +290,7 @@ files = [ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] -toml = ["tomli"] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "distlib" @@ -290,6 +298,7 @@ version = "0.3.9" description = "Distribution utilities" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, @@ -301,6 +310,7 @@ version = "1.9.0" description = "Distro - an OS platform information API" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, @@ -312,6 +322,8 @@ version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version == \"3.10\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -326,6 +338,7 @@ version = "3.16.1" description = "A platform independent file lock." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, @@ -334,7 +347,7 @@ files = [ [package.extras] docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] -typing = ["typing-extensions (>=4.12.2)"] +typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] [[package]] name = "identify" @@ -342,6 +355,7 @@ version = "2.6.1" description = "File identification library for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"}, {file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"}, @@ -356,6 +370,7 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" +groups = ["main", "dev"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -370,6 +385,7 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -381,6 +397,7 @@ version = "5.13.2" description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.8.0" +groups = ["dev"] files = [ {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, @@ -395,6 +412,7 @@ version = "1.4.2" description = "Read settings from config files" optional = false python-versions = ">=3.7.1,<4.0.0" +groups = ["dev"] files = [ {file = "maison-1.4.2-py3-none-any.whl", hash = "sha256:b63fe6751494935fc453dfb76319af223e4cb8bab32ac5464c2a9ca0edda8765"}, {file = "maison-1.4.2.tar.gz", hash = "sha256:d2abac30a5c6a0749526d70ae95a63c6acf43461a1c10e51410b36734e053ec7"}, @@ -411,6 +429,7 @@ version = "1.12.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "mypy-1.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4397081e620dc4dc18e2f124d5e1d2c288194c2c08df6bdb1db31c38cd1fe1ed"}, {file = "mypy-1.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:684a9c508a283f324804fea3f0effeb7858eb03f85c4402a967d187f64562469"}, @@ -463,6 +482,7 @@ version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, @@ -474,71 +494,19 @@ version = "1.9.1" description = "Node.js virtual environment builder" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] files = [ {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, ] -[[package]] -name = "numpy" -version = "2.0.2" -description = "Fundamental package for array computing in Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece"}, - {file = "numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04"}, - {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66"}, - {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b"}, - {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd"}, - {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318"}, - {file = "numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8"}, - {file = "numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326"}, - {file = "numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97"}, - {file = "numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a"}, - {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669"}, - {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951"}, - {file = "numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9"}, - {file = "numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15"}, - {file = "numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4"}, - {file = "numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c"}, - {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692"}, - {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a"}, - {file = "numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c"}, - {file = "numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded"}, - {file = "numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5"}, - {file = "numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729"}, - {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1"}, - {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd"}, - {file = "numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d"}, - {file = "numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d"}, - {file = "numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa"}, - {file = "numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385"}, - {file = "numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78"}, -] - [[package]] name = "numpy" version = "2.1.2" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.10" +groups = ["main", "dev"] files = [ {file = "numpy-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:30d53720b726ec36a7f88dc873f0eec8447fbc93d93a8f079dfac2629598d6ee"}, {file = "numpy-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8d3ca0a72dd8846eb6f7dfe8f19088060fcb76931ed592d29128e0219652884"}, @@ -595,12 +563,152 @@ files = [ {file = "numpy-2.1.2.tar.gz", hash = "sha256:13532a088217fa624c99b843eeb54640de23b3414b14aa66d023805eb731066c"}, ] +[[package]] +name = "opentelemetry-api" +version = "1.44.0" +description = "OpenTelemetry Python API" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef"}, + {file = "opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a"}, +] + +[package.dependencies] +typing-extensions = ">=4.5.0" + +[[package]] +name = "opentelemetry-distro" +version = "0.65b0" +description = "OpenTelemetry Python Distro" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "opentelemetry_distro-0.65b0-py3-none-any.whl", hash = "sha256:0e15bb1a54c638e6c361bc66bc43e9de9ead442e1ae06f8099a2c1d27b6ef845"}, + {file = "opentelemetry_distro-0.65b0.tar.gz", hash = "sha256:e2fef26fdf72978ab172530c13338aff367edae41ae8d90b7f6133efedde10ab"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.65b0" +opentelemetry-sdk = ">=1.13,<2.0" + +[package.extras] +otlp = ["opentelemetry-exporter-otlp (==1.44.0)"] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.65b0" +description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "opentelemetry_instrumentation-0.65b0-py3-none-any.whl", hash = "sha256:ea967a72b9939b5fcfdad572753b4306c59dcb99e3f382d95dae04286805e137"}, + {file = "opentelemetry_instrumentation-0.65b0.tar.gz", hash = "sha256:071d9d9eced9bd6460444ec3b0c77229870ed05a881c22c84fdede58e4eed09b"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.4,<2.0" +opentelemetry-semantic-conventions = "0.65b0" +packaging = ">=18.0" +wrapt = ">=1.0.0,<3.0.0" + +[[package]] +name = "opentelemetry-instrumentation-logging" +version = "0.65b0" +description = "OpenTelemetry Logging instrumentation" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "opentelemetry_instrumentation_logging-0.65b0-py3-none-any.whl", hash = "sha256:68365b31755c844f1e85f07dcd217839ff92f2d278a214bdf02d4dc806f9d915"}, + {file = "opentelemetry_instrumentation_logging-0.65b0.tar.gz", hash = "sha256:c0a50cade5d54db6c6af12e2c69227ecd26f2b3b779e99ff850561d3d8dd77e3"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.65b0" +opentelemetry-semantic-conventions = "0.65b0" + +[[package]] +name = "opentelemetry-instrumentation-requests" +version = "0.65b0" +description = "OpenTelemetry requests instrumentation" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "opentelemetry_instrumentation_requests-0.65b0-py3-none-any.whl", hash = "sha256:91688ec0d4d1fed75ea8d026ef2c66274ed9868c22b6be211ef85d832d16f957"}, + {file = "opentelemetry_instrumentation_requests-0.65b0.tar.gz", hash = "sha256:1d601548f89236d5ab373c7208a2e1e162a8d6462b5b972f9ad8fb0ed82d7438"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.65b0" +opentelemetry-semantic-conventions = "0.65b0" +opentelemetry-util-http = "0.65b0" + +[package.extras] +instruments = ["requests (>=2.0,<3.0)"] + +[[package]] +name = "opentelemetry-sdk" +version = "1.44.0" +description = "OpenTelemetry Python SDK" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "opentelemetry_sdk-1.44.0-py3-none-any.whl", hash = "sha256:df081c4c6bcfdb1211e3e86140376792643128a25f8d72d1d27675936e7e96ad"}, + {file = "opentelemetry_sdk-1.44.0.tar.gz", hash = "sha256:cebe7f65dc12f26ead75c6064de12fd2a9052e5060c0272d402cfa203aae123b"}, +] + +[package.dependencies] +opentelemetry-api = "1.44.0" +opentelemetry-semantic-conventions = "0.65b0" +typing-extensions = ">=4.5.0" + +[package.extras] +file-configuration = ["opentelemetry-configuration (==0.65b0)"] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.65b0" +description = "OpenTelemetry Semantic Conventions" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "opentelemetry_semantic_conventions-0.65b0-py3-none-any.whl", hash = "sha256:1cacde7b0ad306f84c5ef08c3dbe1bbaf20165bba6f8bff43b670e555a086bcb"}, + {file = "opentelemetry_semantic_conventions-0.65b0.tar.gz", hash = "sha256:f9b2b81e9d5b64f11bc952075e7e9c7fb0aab075c7fd1c46d597f1b919852d60"}, +] + +[package.dependencies] +opentelemetry-api = "1.44.0" +typing-extensions = ">=4.5.0" + +[[package]] +name = "opentelemetry-util-http" +version = "0.65b0" +description = "Web util for OpenTelemetry" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "opentelemetry_util_http-0.65b0-py3-none-any.whl", hash = "sha256:7553b606f963097cb190536dc30556cce85090692e471a422fff30ca29b04348"}, + {file = "opentelemetry_util_http-0.65b0.tar.gz", hash = "sha256:84f82d826978bba416ab453460ff6a7391cdc3534c93a786595e4068680016b7"}, +] + [[package]] name = "packaging" version = "24.1" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, @@ -612,6 +720,7 @@ version = "2.2.3" description = "Powerful data structures for data analysis, time series, and statistics" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"}, {file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"}, @@ -698,6 +807,7 @@ version = "2.2.2.240807" description = "Type annotations for pandas" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "pandas_stubs-2.2.2.240807-py3-none-any.whl", hash = "sha256:893919ad82be4275f0d07bb47a95d08bae580d3fdea308a7acfcb3f02e76186e"}, {file = "pandas_stubs-2.2.2.240807.tar.gz", hash = "sha256:64a559725a57a449f46225fbafc422520b7410bff9252b661a225b5559192a93"}, @@ -713,6 +823,7 @@ version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -724,6 +835,7 @@ version = "4.3.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, @@ -740,6 +852,7 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -755,6 +868,7 @@ version = "3.8.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, @@ -773,6 +887,7 @@ version = "1.10.18" description = "Data validation and settings management using python type hints" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pydantic-1.10.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e405ffcc1254d76bb0e760db101ee8916b620893e6edfbfee563b3c6f7a67c02"}, {file = "pydantic-1.10.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e306e280ebebc65040034bff1a0a81fd86b2f4f05daac0131f29541cafd80b80"}, @@ -832,6 +947,7 @@ version = "8.3.3" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, @@ -854,6 +970,7 @@ version = "4.1.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, @@ -872,6 +989,7 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -886,6 +1004,7 @@ version = "2024.2" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, @@ -897,6 +1016,7 @@ version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -959,6 +1079,7 @@ version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, @@ -980,6 +1101,7 @@ version = "1.12.1" description = "Mock out responses from the requests package" optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {file = "requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401"}, {file = "requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563"}, @@ -997,6 +1119,7 @@ version = "1.0.0" description = "A utility belt for advanced users of python-requests" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] files = [ {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, @@ -1011,6 +1134,7 @@ version = "0.91.0" description = "ruyaml is a fork of ruamel.yaml" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "ruyaml-0.91.0-py3-none-any.whl", hash = "sha256:50e0ee3389c77ad340e209472e0effd41ae0275246df00cdad0a067532171755"}, {file = "ruyaml-0.91.0.tar.gz", hash = "sha256:6ce9de9f4d082d696d3bde264664d1bcdca8f5a9dff9d1a1f1a127969ab871ab"}, @@ -1029,19 +1153,20 @@ version = "75.1.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "setuptools-75.1.0-py3-none-any.whl", hash = "sha256:35ab7fd3bcd95e6b7fd704e4a1539513edad446c097797f2985e0e4b960772f2"}, {file = "setuptools-75.1.0.tar.gz", hash = "sha256:d59a21b17a275fb872a9c3dae73963160ae079f1049ed956880cd7c09b120538"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] -core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.5.2) ; sys_platform != \"cygwin\""] +core = ["importlib-metadata (>=6) ; python_version < \"3.10\"", "importlib-resources (>=5.10.2) ; python_version < \"3.9\"", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.11.*)", "pytest-mypy"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib-metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.11.*)", "pytest-mypy"] [[package]] name = "six" @@ -1049,6 +1174,7 @@ version = "1.16.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["main"] files = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -1060,6 +1186,7 @@ version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["dev"] files = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, @@ -1071,6 +1198,8 @@ version = "2.0.2" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version == \"3.10\"" files = [ {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, @@ -1082,6 +1211,7 @@ version = "2024.2.0.20241003" description = "Typing stubs for pytz" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "types-pytz-2024.2.0.20241003.tar.gz", hash = "sha256:575dc38f385a922a212bac00a7d6d2e16e141132a3c955078f4a4fd13ed6cb44"}, {file = "types_pytz-2024.2.0.20241003-py3-none-any.whl", hash = "sha256:3e22df1336c0c6ad1d29163c8fda82736909eb977281cb823c57f8bae07118b7"}, @@ -1093,6 +1223,7 @@ version = "2.32.0.20240914" description = "Typing stubs for requests" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "types-requests-2.32.0.20240914.tar.gz", hash = "sha256:2850e178db3919d9bf809e434eef65ba49d0e7e33ac92d588f4a5e295fffd405"}, {file = "types_requests-2.32.0.20240914-py3-none-any.whl", hash = "sha256:59c2f673eb55f32a99b2894faf6020e1a9f4a402ad0f192bfee0b64469054310"}, @@ -1107,6 +1238,7 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, @@ -1118,6 +1250,7 @@ version = "2024.2" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" +groups = ["main"] files = [ {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, @@ -1129,13 +1262,14 @@ version = "2.2.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -1146,6 +1280,7 @@ version = "20.26.6" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2"}, {file = "virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48"}, @@ -1158,7 +1293,110 @@ platformdirs = ">=3.9.1,<5" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] + +[[package]] +name = "wrapt" +version = "2.3.0" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "wrapt-2.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0bb2797048db0956348cb3058c33bc4184614f13231389cfbccc16a5d32780a7"}, + {file = "wrapt-2.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce9f398f868d2b3b27aa2ea4de79645ef9077aeeac8dfc2814b0d542c6a2b87f"}, + {file = "wrapt-2.3.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ad71df7a04dd3497e9302e81f4a7c91bd401ea0e15a9df9029527900f94bee43"}, + {file = "wrapt-2.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc82c2ccc8e234c844f5303d9f2984b346dcdd53e94823ce8420d2c75b4b9023"}, + {file = "wrapt-2.3.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6e19531ae33c508cea7d84a7edfda01fa86e51b8d1a93a77712c55e6e469152"}, + {file = "wrapt-2.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:df4ce31150bcd5d9f36f816aac3010ab4f4bf8672ac1d3b0ac7d539ec61c7c02"}, + {file = "wrapt-2.3.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e2e692bc0d63f881cf7006730a56bd4e0c2fab5dc318466942805d692b166276"}, + {file = "wrapt-2.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c8388ba7faf5dbf9ee106bb70d66f257629b1bd98091123e19e8a4553a319199"}, + {file = "wrapt-2.3.0-cp310-cp310-win32.whl", hash = "sha256:e045ff75d7d94900fc32896ed93c45ce2d2cac28c9dead582ff9a5a49d446e35"}, + {file = "wrapt-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:b4fc96b159af0a3e0faa72475a69d66292bea72a5bed1e1aca1bffbddc3cb2b0"}, + {file = "wrapt-2.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:1236fa25173ca964c97422470482e9011b9e3c7ed0d75798b40b3da3b0e0e760"}, + {file = "wrapt-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ab559e1b2551d23d54db2a0001c6d73bad022a254639561c5f6c382a9d6c2fe"}, + {file = "wrapt-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bff9a671bc00709cab5a7f745c592b5671873449db0ee2a569af994f16b29a4d"}, + {file = "wrapt-2.3.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fc648a335d7e01adb3640b25f02fd0ea05886cf04d0af7f4ee902bc7b5e466e8"}, + {file = "wrapt-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0077f3d65541925fa83002f967b22ad6550d24813ac64cb905f717194128d9c"}, + {file = "wrapt-2.3.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9790ea25190a4e0fe4cdf4eeb868e9d75f8a024a70a5b6bf9c348a3a2b72e731"}, + {file = "wrapt-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:816877aa749253149f9ecfd2635d4d948ecfa338e1a0311d187b1acb1bb8a3eb"}, + {file = "wrapt-2.3.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d1c2c1b808600d2ea808e6360910a60ed5f409a4011655e10f9164ba0a414a6"}, + {file = "wrapt-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5ba1e5e08ddc46130e9682b2c249f2d1dd39bda9106ed4bd401b7519f18f41bd"}, + {file = "wrapt-2.3.0-cp311-cp311-win32.whl", hash = "sha256:45c9279b373d15649dfa2c2077cb3408ea1a6d3125afbdab9d6b809a66f68e14"}, + {file = "wrapt-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:195b1842b4122fb54e3cd3dd5b2b4aa49302a5a61da901df0481f5c97aedde84"}, + {file = "wrapt-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:6db604ef0c67bdb2042ecdfd7b7f037cf09733557ca42360d1018285634f7b98"}, + {file = "wrapt-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0a45ffae742ce91a16e11cb6c7cd71e7f9994f3cbd283b962ab093f5c6dcf525"}, + {file = "wrapt-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:69e477046f2237ef0bc6547544ee73008dc764ca26eff44f09e976d221b34d5d"}, + {file = "wrapt-2.3.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d221a6e6ddd302b8397433184e96b59f259f50024b854db1c411a881586b6b8"}, + {file = "wrapt-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:392158c9a7f2ab1b8699418bfc0fe6f83548788c418b27d7bf2019ad3405cebb"}, + {file = "wrapt-2.3.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e5301c35cf75655eb33498f2bd6ae8703ca19940e3167dc9cdf740c712a39c60"}, + {file = "wrapt-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:418f54bb09d1762db02c7009b4051149893af3153a87f92d70356703c11eea02"}, + {file = "wrapt-2.3.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1598becd30f8f2777d18564064eb4f4dbe1ab0e05a8f09786d0ef505ac782bf3"}, + {file = "wrapt-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3da470536bf9645143323dd41b32db55c6f4304ad382094c1a1da8a92061e10d"}, + {file = "wrapt-2.3.0-cp312-cp312-win32.whl", hash = "sha256:fb8e2e6704a1e0b1b989546c69e2688371ef4a07fa5f61bde3eb6211186f5ac1"}, + {file = "wrapt-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:cdc021cb0b62471d6aac7f2bd92f3b4658073775f9ee7fcd325c511129e7bcc8"}, + {file = "wrapt-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:67bfe2485f50368c3fcd2275fc1fd100e350d601e0058921a7c82678a465aeab"}, + {file = "wrapt-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d3fb71e65b001adfc42684522eeccd9c21d8ba679945abc993439567b66e59f"}, + {file = "wrapt-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51a7a4181c1295774812271fbcd7c909df372bc25579d4ed9eb875caaf0ae86f"}, + {file = "wrapt-2.3.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9045917809c63fdf7abe3a2ceaed3d670b8ee4500ddd9291192d30aeb34467c5"}, + {file = "wrapt-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54ca1d5573f69b5fe1d74f1f65799c68015e82f685efec9fd8cfa40a094c44d0"}, + {file = "wrapt-2.3.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:242b60c21e30866e6a2fa606c612b47c553fa60c0eaeeeb7797fb842ac0ce609"}, + {file = "wrapt-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3f3d7ec0a51fbfe00d3aef047641ff2c58b25565b4717fc1f90e050be01cba8"}, + {file = "wrapt-2.3.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:261f53870cd4fb2bf38f9f972c56c728fd224cb7c65721307de59d9e7e6741ae"}, + {file = "wrapt-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8159ec0b0cb7608175eb150de94c19e34f4d47ac655f5ca9baf45df6b688ffd3"}, + {file = "wrapt-2.3.0-cp313-cp313-win32.whl", hash = "sha256:10461884b3014fbfc8eb7d09a93c5f246363e6711d9d881f95eb8c27fdef049f"}, + {file = "wrapt-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:ac870cc97b73bb00ac353329e9559a4bebc47c4c86792ed9b23b58c15b6ad838"}, + {file = "wrapt-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:a65e8db2b4e90c2e7ade931086351c98ef420bf7a94ee08c95ac8a3cbbc43579"}, + {file = "wrapt-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:fd1f2f557dd3491fe75905e578f4db967393d40d1a8f468edc4d40ac7f2d5944"}, + {file = "wrapt-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9f5d2aec29dfc76c37e23897dee92766a3fd4f3bff3ae7fc9c6b4bf37d8c1360"}, + {file = "wrapt-2.3.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:646d20d413ffcd1b0a2f700076e2d0252d872dcb7754860a73e45a59ea883614"}, + {file = "wrapt-2.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:379f670f45b7bb8993edd9f6fc36c6cc65edb81cffa0b504be34acb0303fff0a"}, + {file = "wrapt-2.3.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6208f302f110295d64b22a7ac96500c791bf492dce4366e622e4912b077c9687"}, + {file = "wrapt-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ed635a9ca4f3a5a2b900c10c69e823373bc00ebc114b459383596d3487da3570"}, + {file = "wrapt-2.3.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:e3b9eaa742ae7a0aaaaad4ca4b69469d757af2d6e6663ef1dadc47adec0aeb41"}, + {file = "wrapt-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d0f7284f88f4833705132d06d3b425a43095c2cbd07c58166aac3ab646ba12a4"}, + {file = "wrapt-2.3.0-cp313-cp313t-win32.whl", hash = "sha256:7ebb274aba688b043429eb1500ff8a76ce0cb8ac0812ca3e301f06247b8722b3"}, + {file = "wrapt-2.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:c4bded758ad6f03b965830944a2f0bc5b2eb3767fe5a7310134315d1a6610e98"}, + {file = "wrapt-2.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:d2cc64539da63e39ffb9c7ede849b6e8ddaaf7b3876b5cfb04efd85a5f3f4eb6"}, + {file = "wrapt-2.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ea52a0d0f08c584943d5764be0e84efa912c8da23c23e1e285ff2f5641c18fcc"}, + {file = "wrapt-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd85b0aa88efdb189d6ae2f35f4526943a8f091c38599c9c31478241c819e6a1"}, + {file = "wrapt-2.3.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:141ed6211286a9660d8d6702de598b43f0934b4f0eda16393f100a80f501d945"}, + {file = "wrapt-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e49885a62ec4ee854d1b9e6371fda6afd219917225752abf729a3f36d4df9a5"}, + {file = "wrapt-2.3.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d6159c9b2fefec02314e1332dbbbfaf960e369dfd26bcf7f8b258b5732065b3"}, + {file = "wrapt-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24da48596326ef8e448cfa837b454f638713d3531262375f00e5a9681682fc07"}, + {file = "wrapt-2.3.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cd3a2edf0427013736b8127955cec62608c56e53ea47e82812ea32059cda407f"}, + {file = "wrapt-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4fa0df3bff4e7ce45759f33fd39335fe2f60477bb9ecf7b8aa41e7d07ee36a23"}, + {file = "wrapt-2.3.0-cp314-cp314-win32.whl", hash = "sha256:2935d5454b3f179a29b12cf390ee47246740ba2c3a7545b1b46ba31a5f2a4a0b"}, + {file = "wrapt-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:cc2cea812e5cb179a796b766747e7d3b21088760d8deb95676d482b8c8e6fa7d"}, + {file = "wrapt-2.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:22cc5c0a717bd4da87018ae0bffd4c19c6fb679d3ff357216ba566ab26c76cab"}, + {file = "wrapt-2.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a6b5984cd65dd639546f0eb4b8eacf1c31cb2fe9fb5c27bffe240987cdb2cf84"}, + {file = "wrapt-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c88abcf53daef80e01a75c7530e727fa6e2c1888fe83e3dcdba4c96216a1f5c7"}, + {file = "wrapt-2.3.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:85de890ff968196e92dd1ae73a9fb8970495e7650a457b1c9ef0ac3dd550bce2"}, + {file = "wrapt-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50f416b74d092bb9f41b424e90dd457f365f7ba4b11de62a23679769a21bd85c"}, + {file = "wrapt-2.3.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39febbee6d77301d31da6996b152ce52452da7c7ef72aba10c2fa976dff9c295"}, + {file = "wrapt-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:93513bec052c6cd987f9f580c3df068c8bc4ebae6543736be3ca7ec5959cafcd"}, + {file = "wrapt-2.3.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:729126e667da34d251b8ebf8a45ef0c5ddadc21542b3d6e1abf4259ece6508df"}, + {file = "wrapt-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:626b69db2021aa01671ec7bbc9740e558522bd44c18cf2ce69bf3d666a014109"}, + {file = "wrapt-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:629d73378082c00a8173031f9fb30a3ac6abbc894a5bfdfae71fabc60642d501"}, + {file = "wrapt-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:42869085687f0aefd57c0f636c3f9354f8ffb321a8ba9cb52d19beb796e561c5"}, + {file = "wrapt-2.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b1e5aa486e269b00ed35e64771c7d0ab8096cfd2643405ca8cd60ebedc099a51"}, + {file = "wrapt-2.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c3b476ae63b4a3b4da681aafcb25ff3542d289fbda8b5da7caf76aaffafafdbb"}, + {file = "wrapt-2.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:932dced0a7b2950ed58a3325536a1dcb7b58e7330af54e8552d2e566b5328b99"}, + {file = "wrapt-2.3.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0db083387d6e75ec0be8173ecbf0e811cf60bae1cc75a815feb104167ea10d4d"}, + {file = "wrapt-2.3.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abc71504669d126d91f89fc0e388c6295d8fbd2439be884f175133fda8aa403c"}, + {file = "wrapt-2.3.0-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b767a9566f165dd14decf8f4194c6bb0ce3a8420cec213824e05a99400c9260a"}, + {file = "wrapt-2.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:73d0b10b64620a2cf4bc3d31775c4d9527e309a5549e4379e3bf71e8d2dc193e"}, + {file = "wrapt-2.3.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:e31734c5077f29f892b2565eee5106d610278151ad49fc6a9d69a647cd5730e2"}, + {file = "wrapt-2.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:628f3ba8ec793a5b10a6cd8c6c6b7b55eb552abd1f3bd301336acb74c7a82dfe"}, + {file = "wrapt-2.3.0-cp39-cp39-win32.whl", hash = "sha256:3873c3c5ca9f4ef91f693602eca19d1f1e7c410338df82a4ff11d826b5896a8f"}, + {file = "wrapt-2.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:8f8a1c6472675956cece9a8f403f43c3594f1681319eed2dd56f60877397c636"}, + {file = "wrapt-2.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:c8858d8ff9822a081e3cc49ae1b3b22f0f789c14001cdac8f94564010d9c9d66"}, + {file = "wrapt-2.3.0-py3-none-any.whl", hash = "sha256:d8c7ed08477429752b8c44991f40ad7838b18332a160698740a6bfbc10d998a2"}, + {file = "wrapt-2.3.0.tar.gz", hash = "sha256:681a2d0eefd721998f90642762b8e75c2159ec531b20ad5e437245ea7b06a107"}, +] + +[package.extras] +dev = ["pytest", "setuptools"] [[package]] name = "yamlfix" @@ -1166,6 +1404,7 @@ version = "1.16.1" description = "A simple opionated yaml formatter that keeps your comments!" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "yamlfix-1.16.1-py3-none-any.whl", hash = "sha256:8c505ca27cf19181ca8943101b56b8e4ad58f47aa792fbab01339ededaddb7d2"}, {file = "yamlfix-1.16.1.tar.gz", hash = "sha256:f49ba70e457a1add6724a6859505d22f7f222f56f7e31f37822c530fc2e7ec94"}, @@ -1177,6 +1416,6 @@ maison = ">=1.4.0,<1.4.3" ruyaml = ">=0.91.0" [metadata] -lock-version = "2.0" -python-versions = "^3.9" -content-hash = "56893da3e5c4e664c7f2d8bd0ca69a996b85d028096cf412bbda217440e10936" +lock-version = "2.1" +python-versions = "^3.10" +content-hash = "a7a03a3a21372d6acfff98c9eb4a7e1b347c15cd25e641bcbc5d27328a78b019" diff --git a/pyproject.toml b/pyproject.toml index 1293493..8b32a0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ keywords = ["USACE", "water data", "CWMS"] authors = ["Eric Novotny "] [tool.poetry.dependencies] -python = "^3.9" +python = "^3.10" pandas = "^2.1.3" requests-toolbelt = "^1.0.0" requests = "^2.31.0" From cfd30f4ebcc13951fe146358bbaf1fed89e6089c Mon Sep 17 00:00:00 2001 From: "Michael A. Neilson" Date: Tue, 18 Aug 2026 16:17:03 +0000 Subject: [PATCH 13/14] Add return annotation. --- cwms/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cwms/api.py b/cwms/api.py index edbba89..6c19b35 100644 --- a/cwms/api.py +++ b/cwms/api.py @@ -54,7 +54,7 @@ # Propagate the current W3CTraceContext to the request. -def request_hook(span: trace.Span, request: Request): +def request_hook(span: trace.Span, request: Request) -> None: ctx = trace.set_span_in_context(span) propagate.get_global_textmap().inject(request.headers, ctx) From 33a396590563f9e779d8a0eb6f4b7b3847b2d61b Mon Sep 17 00:00:00 2001 From: "Michael A. Neilson" Date: Tue, 18 Aug 2026 17:05:13 +0000 Subject: [PATCH 14/14] Update matrix. --- .github/workflows/CDA-testing.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/CDA-testing.yml b/.github/workflows/CDA-testing.yml index fb6ade0..60085ad 100644 --- a/.github/workflows/CDA-testing.yml +++ b/.github/workflows/CDA-testing.yml @@ -14,7 +14,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.9', '3.13'] + python-version: ['3.10', '3.13'] steps: - uses: actions/checkout@v7 @@ -50,8 +50,7 @@ jobs: id: cache-poetry-venv with: path: .venv - key: ${{ runner.os }}-py${{ matrix.python-version }}-poetry-${{ hashFiles('poetry.lock') - }} + key: ${{ runner.os }}-py${{ matrix.python-version }}-poetry-${{ hashFiles('poetry.lock')}} # Install dependencies only if cache is missed - name: Install dependencies