From 6af1ecd948f858add5975ec306d19beb843153c8 Mon Sep 17 00:00:00 2001 From: Lukas Bindreiter Date: Fri, 4 Sep 2026 11:35:43 +0200 Subject: [PATCH] Update clients to latest protobuf changes --- CHANGELOG.md | 9 ++- tilebox-datasets/tests/data/data_access.py | 7 ++- .../tests/data/test_data_access.py | 23 +++++++- .../tilebox/datasets/aio/dataset.py | 6 +- .../tilebox/datasets/data/data_access.py | 20 +++++-- .../datasets/datasets/v1/data_access_pb2.py | 14 ++--- .../datasets/datasets/v1/data_access_pb2.pyi | 6 +- .../tilebox/datasets/sync/dataset.py | 6 +- tilebox-workflows/tests/jobs/test_client.py | 57 +++++++++++++++++++ tilebox-workflows/tilebox/workflows/data.py | 15 ++++- .../tilebox/workflows/jobs/client.py | 40 ++++++++++--- .../workflows/jobs/telemetry_service.py | 24 +++++++- uv.lock | 14 ++--- 13 files changed, 197 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b11e8f..8a798e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.61.0] - 2026-09-04 + ### Added - `tilebox-grpc`: Add runtime and execution environment metadata to all API requests using the `Tilebox-Client` header. +- `tilebox-datasets`: Add explicit `filter_contains_geometry` and `geometry_contains_filter` spatial query modes, + deprecated the legacy `contains` mode which is an alias for `filter_contains_geometry`. +- `tilebox-workflows`: Add task ID filters to job log and span queries and severity filters to job log queries. +- `tilebox-workflows`: Allow querying jobs without specifying a temporal extent. ### Fixed @@ -489,7 +495,8 @@ the first client that does not cache data (since it's already on the local file - Released under the [MIT](https://opensource.org/license/mit) license. - Released packages: `tilebox-datasets`, `tilebox-workflows`, `tilebox-storage`, `tilebox-grpc` -[Unreleased]: https://github.com/tilebox/tilebox-python/compare/v0.60.0...HEAD +[Unreleased]: https://github.com/tilebox/tilebox-python/compare/v0.61.0...HEAD +[0.61.0]: https://github.com/tilebox/tilebox-python/compare/v0.60.0...v0.61.0 [0.60.0]: https://github.com/tilebox/tilebox-python/compare/v0.59.0...v0.60.0 [0.59.0]: https://github.com/tilebox/tilebox-python/compare/v0.58.0...v0.59.0 [0.58.0]: https://github.com/tilebox/tilebox-python/compare/v0.57.0...v0.58.0 diff --git a/tilebox-datasets/tests/data/data_access.py b/tilebox-datasets/tests/data/data_access.py index 872391e..3ea455e 100644 --- a/tilebox-datasets/tests/data/data_access.py +++ b/tilebox-datasets/tests/data/data_access.py @@ -26,8 +26,11 @@ def spatial_filter_likes(draw: DrawFn) -> Geometry | SpatialFilterDict: return geometry # return a dict - mode: SpatialFilterMode | Literal["intersects", "contains"] | None = draw( - sampled_from(["intersects", "contains"]), sampled_from(SpatialFilterMode) | none() + mode: SpatialFilterMode | Literal["intersects", "filter_contains_geometry", "geometry_contains_filter"] | None = ( + draw( + sampled_from(["intersects", "filter_contains_geometry", "geometry_contains_filter"]), + sampled_from(SpatialFilterMode) | none(), + ) ) coordinate_system: SpatialCoordinateSystem | Literal["cartesian", "spherical"] | None = draw( sampled_from(["cartesian", "spherical"]), sampled_from(SpatialCoordinateSystem) | none() diff --git a/tilebox-datasets/tests/data/test_data_access.py b/tilebox-datasets/tests/data/test_data_access.py index afd4fad..ff6089a 100644 --- a/tilebox-datasets/tests/data/test_data_access.py +++ b/tilebox-datasets/tests/data/test_data_access.py @@ -3,10 +3,10 @@ import pytest from hypothesis import given -from shapely import Geometry +from shapely import Geometry, box from tests.data.data_access import query_filters, spatial_filter_likes, spatial_filters -from tilebox.datasets.data.data_access import QueryFilters, SpatialFilter, SpatialFilterDict +from tilebox.datasets.data.data_access import QueryFilters, SpatialFilter, SpatialFilterDict, SpatialFilterMode from tilebox.datasets.datasets.v1 import data_access_pb2 from tilebox.datasets.query import TimeInterval, field from tilebox.datasets.query.id_interval import IDInterval @@ -37,6 +37,25 @@ def test_parse_spatial_filter_like(spatial_filter_like: Geometry | SpatialFilter assert spatial_filter.coordinate_system is not None +def test_contains_spatial_filter_mode_is_deprecated() -> None: + with pytest.warns(DeprecationWarning, match='Use "filter_contains_geometry" instead'): + spatial_filter = SpatialFilter.parse({"geometry": box(0, 0, 1, 1), "mode": "contains"}) + + assert spatial_filter.mode is SpatialFilterMode.FILTER_CONTAINS_GEOMETRY + + +@pytest.mark.parametrize( + ("mode", "wire_mode"), + [ + (SpatialFilterMode.FILTER_CONTAINS_GEOMETRY, data_access_pb2.SPATIAL_FILTER_MODE_FILTER_CONTAINS_GEOMETRY), + (SpatialFilterMode.GEOMETRY_CONTAINS_FILTER, data_access_pb2.SPATIAL_FILTER_MODE_GEOMETRY_CONTAINS_FILTER), + ], +) +def test_directional_spatial_filter_modes(mode: SpatialFilterMode, wire_mode: int) -> None: + message = SpatialFilter(box(0, 0, 1, 1), mode).to_message() + assert message.mode == wire_mode + + @given(query_filters()) def test_query_filters_to_message_and_back(q: QueryFilters) -> None: assert QueryFilters.from_message(q.to_message()) == q diff --git a/tilebox-datasets/tilebox/datasets/aio/dataset.py b/tilebox-datasets/tilebox/datasets/aio/dataset.py index 88d8c97..6037d94 100644 --- a/tilebox-datasets/tilebox/datasets/aio/dataset.py +++ b/tilebox-datasets/tilebox/datasets/aio/dataset.py @@ -464,12 +464,12 @@ async def query( spatial_extent: The spatial extent to query data in. (Optional) Expected to be either a shapely geometry, or a dict with the following keys: - geometry: The geometry to query by. Must be a shapely.Polygon, shapely.MultiPolygon or shapely.Point. - - mode: The spatial filter mode to use. Can be one of "intersects" or "contains". - Defaults to "intersects". + - mode: The spatial filter mode to use. Can be one of "intersects", "filter_contains_geometry", or + "geometry_contains_filter". Defaults to "intersects". - coordinate_system: The coordinate system to use for performing geometry calculations. Can be one of "cartesian" or "spherical". Only supported for spatiotemporal datasets. Will raise an error if used for other dataset types. - All datapoints whose geometry intersects the given spatial extent will be returned. + Datapoints matching the selected spatial filter mode will be returned. skip_data: Whether to skip the actual data of the datapoint. If True, only datapoint metadata is returned. show_progress: Whether to show a progress bar while loading the data. If a callable is specified it is used as callback to report progress percentages. diff --git a/tilebox-datasets/tilebox/datasets/data/data_access.py b/tilebox-datasets/tilebox/datasets/data/data_access.py index b081340..d2a0283 100644 --- a/tilebox-datasets/tilebox/datasets/data/data_access.py +++ b/tilebox-datasets/tilebox/datasets/data/data_access.py @@ -1,6 +1,7 @@ from dataclasses import dataclass from enum import Enum from typing import Literal, TypeAlias, TypedDict +from warnings import warn from shapely import Geometry, from_wkb, to_wkb @@ -15,7 +16,8 @@ class SpatialFilterMode(Enum): INTERSECTS = data_access_pb2.SPATIAL_FILTER_MODE_INTERSECTS - CONTAINS = data_access_pb2.SPATIAL_FILTER_MODE_CONTAINS + FILTER_CONTAINS_GEOMETRY = data_access_pb2.SPATIAL_FILTER_MODE_FILTER_CONTAINS_GEOMETRY + GEOMETRY_CONTAINS_FILTER = data_access_pb2.SPATIAL_FILTER_MODE_GEOMETRY_CONTAINS_FILTER _filter_modes_from_string = {mode.name.lower(): mode for mode in SpatialFilterMode} @@ -33,7 +35,10 @@ class SpatialCoordinateSystem(Enum): class SpatialFilterDict(TypedDict): geometry: Geometry - mode: NotRequired[SpatialFilterMode | Literal["intersects", "contains"]] + # "contains" is deprecated and retained only for backwards compatibility. + mode: NotRequired[ + SpatialFilterMode | Literal["intersects", "filter_contains_geometry", "geometry_contains_filter", "contains"] + ] coordinate_system: NotRequired[SpatialCoordinateSystem | Literal["cartesian", "spherical"]] @@ -49,8 +54,8 @@ class SpatialFilter: Args: geometry: The spatial geometry to filter by (e.g. a polygon) - mode: The spatial filter mode to use. Can be one of "intersects" or "contains". - Defaults to "intersects". + mode: The spatial filter mode to use. Can be one of "intersects", "filter_contains_geometry", or + "geometry_contains_filter". Defaults to "intersects". crs: The coordinate system to use for performing geometry calculations. Can be one of "cartesian" or "spherical". """ @@ -92,6 +97,13 @@ def parse(cls, spatial_filter_like: SpatialFilterLike) -> "SpatialFilter": if isinstance(spatial_filter_like, dict): mode = spatial_filter_like.get("mode", None) if isinstance(mode, str): + if mode.lower() == "contains": + warn( + 'The spatial filter mode "contains" is deprecated. Use "filter_contains_geometry" instead.', + DeprecationWarning, + stacklevel=2, + ) + mode = "filter_contains_geometry" mode = _filter_modes_from_string.get(mode.lower()) coordinate_system = spatial_filter_like.get("coordinate_system", None) if isinstance(coordinate_system, str): diff --git a/tilebox-datasets/tilebox/datasets/datasets/v1/data_access_pb2.py b/tilebox-datasets/tilebox/datasets/datasets/v1/data_access_pb2.py index b06450c..99a5475 100644 --- a/tilebox-datasets/tilebox/datasets/datasets/v1/data_access_pb2.py +++ b/tilebox-datasets/tilebox/datasets/datasets/v1/data_access_pb2.py @@ -30,7 +30,7 @@ from tilebox.datasets.tilebox.v1 import query_pb2 as tilebox_dot_v1_dot_query__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1d\x64\x61tasets/v1/data_access.proto\x12\x0b\x64\x61tasets.v1\x1a\x16\x64\x61tasets/v1/core.proto\x1a\"datasets/v1/well_known_types.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x13tilebox/v1/id.proto\x1a\x16tilebox/v1/query.proto\"\xb5\x01\n\x10QueryByIDRequest\x12-\n\ndataset_id\x18\x04 \x01(\x0b\x32\x0e.tilebox.v1.IDR\tdatasetId\x12\x35\n\x0e\x63ollection_ids\x18\x01 \x03(\x0b\x32\x0e.tilebox.v1.IDR\rcollectionIds\x12\x1e\n\x02id\x18\x02 \x01(\x0b\x32\x0e.tilebox.v1.IDR\x02id\x12\x1b\n\tskip_data\x18\x03 \x01(\x08R\x08skipData\"\x98\x02\n\x0cQueryFilters\x12=\n\rtime_interval\x18\x01 \x01(\x0b\x32\x18.tilebox.v1.TimeIntervalR\x0ctimeInterval\x12\x45\n\x12\x64\x61tapoint_interval\x18\x02 \x01(\x0b\x32\x16.tilebox.v1.IDIntervalR\x11\x64\x61tapointInterval\x12\x41\n\x0espatial_extent\x18\x03 \x01(\x0b\x32\x1a.datasets.v1.SpatialFilterR\rspatialExtent\x12?\n\x0b\x65xpressions\x18\x04 \x03(\x0b\x32\x1d.datasets.v1.FilterExpressionR\x0b\x65xpressions\"\xc0\x01\n\x10\x46ilterExpression\x12\x38\n\x07logical\x18\x01 \x01(\x0b\x32\x1e.datasets.v1.LogicalExpressionR\x07logical\x12<\n\ncomparison\x18\x02 \x01(\x0b\x32\x1c.datasets.v1.FieldComparisonR\ncomparison\x12\x34\n\x07is_null\x18\x03 \x01(\x0b\x32\x1b.datasets.v1.FieldNullCheckR\x06isNull\"\x88\x01\n\x11LogicalExpression\x12\x38\n\x08operator\x18\x01 \x01(\x0e\x32\x1c.datasets.v1.LogicalOperatorR\x08operator\x12\x39\n\x08operands\x18\x02 \x03(\x0b\x32\x1d.datasets.v1.FilterExpressionR\x08operands\"\xa6\x01\n\x0f\x46ieldComparison\x12\x1d\n\nfield_name\x18\x01 \x01(\tR\tfieldName\x12@\n\x08operator\x18\x02 \x01(\x0e\x32$.datasets.v1.FieldComparisonOperatorR\x08operator\x12\x32\n\x05value\x18\x03 \x01(\x0b\x32\x1c.datasets.v1.FieldQueryValueR\x05value\"/\n\x0e\x46ieldNullCheck\x12\x1d\n\nfield_name\x18\x01 \x01(\tR\tfieldName\"\xb0\x03\n\x0f\x46ieldQueryValue\x12$\n\nbool_value\x18\x01 \x01(\x08\x42\x05\xaa\x01\x02\x08\x01R\tboolValue\x12&\n\x0bint64_value\x18\x02 \x01(\x03\x42\x05\xaa\x01\x02\x08\x01R\nint64Value\x12(\n\x0cuint64_value\x18\x03 \x01(\x04\x42\x05\xaa\x01\x02\x08\x01R\x0buint64Value\x12(\n\x0c\x64ouble_value\x18\x04 \x01(\x01\x42\x05\xaa\x01\x02\x08\x01R\x0b\x64oubleValue\x12(\n\x0cstring_value\x18\x05 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x0bstringValue\x12\x43\n\x0ftimestamp_value\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\x0etimestampValue\x12@\n\x0e\x64uration_value\x18\x07 \x01(\x0b\x32\x19.google.protobuf.DurationR\rdurationValue\x12\"\n\tenum_name\x18\x08 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x08\x65numName\x12&\n\x0b\x62ytes_value\x18\t \x01(\x0c\x42\x05\xaa\x01\x02\x08\x01R\nbytesValue\"\xc9\x01\n\rSpatialFilter\x12\x31\n\x08geometry\x18\x01 \x01(\x0b\x32\x15.datasets.v1.GeometryR\x08geometry\x12\x32\n\x04mode\x18\x02 \x01(\x0e\x32\x1e.datasets.v1.SpatialFilterModeR\x04mode\x12Q\n\x11\x63oordinate_system\x18\x03 \x01(\x0e\x32$.datasets.v1.SpatialCoordinateSystemR\x10\x63oordinateSystem\"\xf9\x01\n\x0cQueryRequest\x12-\n\ndataset_id\x18\x05 \x01(\x0b\x32\x0e.tilebox.v1.IDR\tdatasetId\x12\x35\n\x0e\x63ollection_ids\x18\x01 \x03(\x0b\x32\x0e.tilebox.v1.IDR\rcollectionIds\x12\x33\n\x07\x66ilters\x18\x02 \x01(\x0b\x32\x19.datasets.v1.QueryFiltersR\x07\x66ilters\x12\x31\n\x04page\x18\x03 \x01(\x0b\x32\x16.tilebox.v1.PaginationB\x05\xaa\x01\x02\x08\x01R\x04page\x12\x1b\n\tskip_data\x18\x04 \x01(\x08R\x08skipData\"{\n\x0fQueryResultPage\x12,\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32\x18.datasets.v1.RepeatedAnyR\x04\x64\x61ta\x12:\n\tnext_page\x18\x02 \x01(\x0b\x32\x16.tilebox.v1.PaginationB\x05\xaa\x01\x02\x08\x01R\x08nextPage*\x80\x01\n\x0fLogicalOperator\x12 \n\x1cLOGICAL_OPERATOR_UNSPECIFIED\x10\x00\x12\x18\n\x14LOGICAL_OPERATOR_AND\x10\x01\x12\x17\n\x13LOGICAL_OPERATOR_OR\x10\x02\x12\x18\n\x14LOGICAL_OPERATOR_NOT\x10\x03*\xce\x02\n\x17\x46ieldComparisonOperator\x12)\n%FIELD_COMPARISON_OPERATOR_UNSPECIFIED\x10\x00\x12#\n\x1f\x46IELD_COMPARISON_OPERATOR_EQUAL\x10\x01\x12\'\n#FIELD_COMPARISON_OPERATOR_NOT_EQUAL\x10\x02\x12\'\n#FIELD_COMPARISON_OPERATOR_LESS_THAN\x10\x03\x12\x30\n,FIELD_COMPARISON_OPERATOR_LESS_THAN_OR_EQUAL\x10\x04\x12*\n&FIELD_COMPARISON_OPERATOR_GREATER_THAN\x10\x05\x12\x33\n/FIELD_COMPARISON_OPERATOR_GREATER_THAN_OR_EQUAL\x10\x06*~\n\x11SpatialFilterMode\x12#\n\x1fSPATIAL_FILTER_MODE_UNSPECIFIED\x10\x00\x12\"\n\x1eSPATIAL_FILTER_MODE_INTERSECTS\x10\x01\x12 \n\x1cSPATIAL_FILTER_MODE_CONTAINS\x10\x02*\x96\x01\n\x17SpatialCoordinateSystem\x12)\n%SPATIAL_COORDINATE_SYSTEM_UNSPECIFIED\x10\x00\x12\'\n#SPATIAL_COORDINATE_SYSTEM_CARTESIAN\x10\x01\x12\'\n#SPATIAL_COORDINATE_SYSTEM_SPHERICAL\x10\x02\x32\x93\x01\n\x11\x44\x61taAccessService\x12<\n\tQueryByID\x12\x1d.datasets.v1.QueryByIDRequest\x1a\x10.datasets.v1.Any\x12@\n\x05Query\x12\x19.datasets.v1.QueryRequest\x1a\x1c.datasets.v1.QueryResultPageBt\n\x0f\x63om.datasets.v1B\x0f\x44\x61taAccessProtoP\x01\xa2\x02\x03\x44XX\xaa\x02\x0b\x44\x61tasets.V1\xca\x02\x0b\x44\x61tasets\\V1\xe2\x02\x17\x44\x61tasets\\V1\\GPBMetadata\xea\x02\x0c\x44\x61tasets::V1\x92\x03\x02\x08\x02\x62\x08\x65\x64itionsp\xe8\x07') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1d\x64\x61tasets/v1/data_access.proto\x12\x0b\x64\x61tasets.v1\x1a\x16\x64\x61tasets/v1/core.proto\x1a\"datasets/v1/well_known_types.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x13tilebox/v1/id.proto\x1a\x16tilebox/v1/query.proto\"\xb5\x01\n\x10QueryByIDRequest\x12-\n\ndataset_id\x18\x04 \x01(\x0b\x32\x0e.tilebox.v1.IDR\tdatasetId\x12\x35\n\x0e\x63ollection_ids\x18\x01 \x03(\x0b\x32\x0e.tilebox.v1.IDR\rcollectionIds\x12\x1e\n\x02id\x18\x02 \x01(\x0b\x32\x0e.tilebox.v1.IDR\x02id\x12\x1b\n\tskip_data\x18\x03 \x01(\x08R\x08skipData\"\x98\x02\n\x0cQueryFilters\x12=\n\rtime_interval\x18\x01 \x01(\x0b\x32\x18.tilebox.v1.TimeIntervalR\x0ctimeInterval\x12\x45\n\x12\x64\x61tapoint_interval\x18\x02 \x01(\x0b\x32\x16.tilebox.v1.IDIntervalR\x11\x64\x61tapointInterval\x12\x41\n\x0espatial_extent\x18\x03 \x01(\x0b\x32\x1a.datasets.v1.SpatialFilterR\rspatialExtent\x12?\n\x0b\x65xpressions\x18\x04 \x03(\x0b\x32\x1d.datasets.v1.FilterExpressionR\x0b\x65xpressions\"\xc0\x01\n\x10\x46ilterExpression\x12\x38\n\x07logical\x18\x01 \x01(\x0b\x32\x1e.datasets.v1.LogicalExpressionR\x07logical\x12<\n\ncomparison\x18\x02 \x01(\x0b\x32\x1c.datasets.v1.FieldComparisonR\ncomparison\x12\x34\n\x07is_null\x18\x03 \x01(\x0b\x32\x1b.datasets.v1.FieldNullCheckR\x06isNull\"\x88\x01\n\x11LogicalExpression\x12\x38\n\x08operator\x18\x01 \x01(\x0e\x32\x1c.datasets.v1.LogicalOperatorR\x08operator\x12\x39\n\x08operands\x18\x02 \x03(\x0b\x32\x1d.datasets.v1.FilterExpressionR\x08operands\"\xa6\x01\n\x0f\x46ieldComparison\x12\x1d\n\nfield_name\x18\x01 \x01(\tR\tfieldName\x12@\n\x08operator\x18\x02 \x01(\x0e\x32$.datasets.v1.FieldComparisonOperatorR\x08operator\x12\x32\n\x05value\x18\x03 \x01(\x0b\x32\x1c.datasets.v1.FieldQueryValueR\x05value\"/\n\x0e\x46ieldNullCheck\x12\x1d\n\nfield_name\x18\x01 \x01(\tR\tfieldName\"\xb0\x03\n\x0f\x46ieldQueryValue\x12$\n\nbool_value\x18\x01 \x01(\x08\x42\x05\xaa\x01\x02\x08\x01R\tboolValue\x12&\n\x0bint64_value\x18\x02 \x01(\x03\x42\x05\xaa\x01\x02\x08\x01R\nint64Value\x12(\n\x0cuint64_value\x18\x03 \x01(\x04\x42\x05\xaa\x01\x02\x08\x01R\x0buint64Value\x12(\n\x0c\x64ouble_value\x18\x04 \x01(\x01\x42\x05\xaa\x01\x02\x08\x01R\x0b\x64oubleValue\x12(\n\x0cstring_value\x18\x05 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x0bstringValue\x12\x43\n\x0ftimestamp_value\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\x0etimestampValue\x12@\n\x0e\x64uration_value\x18\x07 \x01(\x0b\x32\x19.google.protobuf.DurationR\rdurationValue\x12\"\n\tenum_name\x18\x08 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x08\x65numName\x12&\n\x0b\x62ytes_value\x18\t \x01(\x0c\x42\x05\xaa\x01\x02\x08\x01R\nbytesValue\"\xc9\x01\n\rSpatialFilter\x12\x31\n\x08geometry\x18\x01 \x01(\x0b\x32\x15.datasets.v1.GeometryR\x08geometry\x12\x32\n\x04mode\x18\x02 \x01(\x0e\x32\x1e.datasets.v1.SpatialFilterModeR\x04mode\x12Q\n\x11\x63oordinate_system\x18\x03 \x01(\x0e\x32$.datasets.v1.SpatialCoordinateSystemR\x10\x63oordinateSystem\"\xf9\x01\n\x0cQueryRequest\x12-\n\ndataset_id\x18\x05 \x01(\x0b\x32\x0e.tilebox.v1.IDR\tdatasetId\x12\x35\n\x0e\x63ollection_ids\x18\x01 \x03(\x0b\x32\x0e.tilebox.v1.IDR\rcollectionIds\x12\x33\n\x07\x66ilters\x18\x02 \x01(\x0b\x32\x19.datasets.v1.QueryFiltersR\x07\x66ilters\x12\x31\n\x04page\x18\x03 \x01(\x0b\x32\x16.tilebox.v1.PaginationB\x05\xaa\x01\x02\x08\x01R\x04page\x12\x1b\n\tskip_data\x18\x04 \x01(\x08R\x08skipData\"{\n\x0fQueryResultPage\x12,\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32\x18.datasets.v1.RepeatedAnyR\x04\x64\x61ta\x12:\n\tnext_page\x18\x02 \x01(\x0b\x32\x16.tilebox.v1.PaginationB\x05\xaa\x01\x02\x08\x01R\x08nextPage*\x80\x01\n\x0fLogicalOperator\x12 \n\x1cLOGICAL_OPERATOR_UNSPECIFIED\x10\x00\x12\x18\n\x14LOGICAL_OPERATOR_AND\x10\x01\x12\x17\n\x13LOGICAL_OPERATOR_OR\x10\x02\x12\x18\n\x14LOGICAL_OPERATOR_NOT\x10\x03*\xce\x02\n\x17\x46ieldComparisonOperator\x12)\n%FIELD_COMPARISON_OPERATOR_UNSPECIFIED\x10\x00\x12#\n\x1f\x46IELD_COMPARISON_OPERATOR_EQUAL\x10\x01\x12\'\n#FIELD_COMPARISON_OPERATOR_NOT_EQUAL\x10\x02\x12\'\n#FIELD_COMPARISON_OPERATOR_LESS_THAN\x10\x03\x12\x30\n,FIELD_COMPARISON_OPERATOR_LESS_THAN_OR_EQUAL\x10\x04\x12*\n&FIELD_COMPARISON_OPERATOR_GREATER_THAN\x10\x05\x12\x33\n/FIELD_COMPARISON_OPERATOR_GREATER_THAN_OR_EQUAL\x10\x06*\xc0\x01\n\x11SpatialFilterMode\x12#\n\x1fSPATIAL_FILTER_MODE_UNSPECIFIED\x10\x00\x12\"\n\x1eSPATIAL_FILTER_MODE_INTERSECTS\x10\x01\x12\x30\n,SPATIAL_FILTER_MODE_FILTER_CONTAINS_GEOMETRY\x10\x02\x12\x30\n,SPATIAL_FILTER_MODE_GEOMETRY_CONTAINS_FILTER\x10\x03*\x96\x01\n\x17SpatialCoordinateSystem\x12)\n%SPATIAL_COORDINATE_SYSTEM_UNSPECIFIED\x10\x00\x12\'\n#SPATIAL_COORDINATE_SYSTEM_CARTESIAN\x10\x01\x12\'\n#SPATIAL_COORDINATE_SYSTEM_SPHERICAL\x10\x02\x32\x93\x01\n\x11\x44\x61taAccessService\x12<\n\tQueryByID\x12\x1d.datasets.v1.QueryByIDRequest\x1a\x10.datasets.v1.Any\x12@\n\x05Query\x12\x19.datasets.v1.QueryRequest\x1a\x1c.datasets.v1.QueryResultPageBt\n\x0f\x63om.datasets.v1B\x0f\x44\x61taAccessProtoP\x01\xa2\x02\x03\x44XX\xaa\x02\x0b\x44\x61tasets.V1\xca\x02\x0b\x44\x61tasets\\V1\xe2\x02\x17\x44\x61tasets\\V1\\GPBMetadata\xea\x02\x0c\x44\x61tasets::V1\x92\x03\x02\x08\x02\x62\x08\x65\x64itionsp\xe8\x07') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -60,10 +60,10 @@ _globals['_LOGICALOPERATOR']._serialized_end=2380 _globals['_FIELDCOMPARISONOPERATOR']._serialized_start=2383 _globals['_FIELDCOMPARISONOPERATOR']._serialized_end=2717 - _globals['_SPATIALFILTERMODE']._serialized_start=2719 - _globals['_SPATIALFILTERMODE']._serialized_end=2845 - _globals['_SPATIALCOORDINATESYSTEM']._serialized_start=2848 - _globals['_SPATIALCOORDINATESYSTEM']._serialized_end=2998 + _globals['_SPATIALFILTERMODE']._serialized_start=2720 + _globals['_SPATIALFILTERMODE']._serialized_end=2912 + _globals['_SPATIALCOORDINATESYSTEM']._serialized_start=2915 + _globals['_SPATIALCOORDINATESYSTEM']._serialized_end=3065 _globals['_QUERYBYIDREQUEST']._serialized_start=217 _globals['_QUERYBYIDREQUEST']._serialized_end=398 _globals['_QUERYFILTERS']._serialized_start=401 @@ -84,6 +84,6 @@ _globals['_QUERYREQUEST']._serialized_end=2124 _globals['_QUERYRESULTPAGE']._serialized_start=2126 _globals['_QUERYRESULTPAGE']._serialized_end=2249 - _globals['_DATAACCESSSERVICE']._serialized_start=3001 - _globals['_DATAACCESSSERVICE']._serialized_end=3148 + _globals['_DATAACCESSSERVICE']._serialized_start=3068 + _globals['_DATAACCESSSERVICE']._serialized_end=3215 # @@protoc_insertion_point(module_scope) diff --git a/tilebox-datasets/tilebox/datasets/datasets/v1/data_access_pb2.pyi b/tilebox-datasets/tilebox/datasets/datasets/v1/data_access_pb2.pyi index e62a842..393d435 100644 --- a/tilebox-datasets/tilebox/datasets/datasets/v1/data_access_pb2.pyi +++ b/tilebox-datasets/tilebox/datasets/datasets/v1/data_access_pb2.pyi @@ -34,7 +34,8 @@ class SpatialFilterMode(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): __slots__ = () SPATIAL_FILTER_MODE_UNSPECIFIED: _ClassVar[SpatialFilterMode] SPATIAL_FILTER_MODE_INTERSECTS: _ClassVar[SpatialFilterMode] - SPATIAL_FILTER_MODE_CONTAINS: _ClassVar[SpatialFilterMode] + SPATIAL_FILTER_MODE_FILTER_CONTAINS_GEOMETRY: _ClassVar[SpatialFilterMode] + SPATIAL_FILTER_MODE_GEOMETRY_CONTAINS_FILTER: _ClassVar[SpatialFilterMode] class SpatialCoordinateSystem(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): __slots__ = () @@ -54,7 +55,8 @@ FIELD_COMPARISON_OPERATOR_GREATER_THAN: FieldComparisonOperator FIELD_COMPARISON_OPERATOR_GREATER_THAN_OR_EQUAL: FieldComparisonOperator SPATIAL_FILTER_MODE_UNSPECIFIED: SpatialFilterMode SPATIAL_FILTER_MODE_INTERSECTS: SpatialFilterMode -SPATIAL_FILTER_MODE_CONTAINS: SpatialFilterMode +SPATIAL_FILTER_MODE_FILTER_CONTAINS_GEOMETRY: SpatialFilterMode +SPATIAL_FILTER_MODE_GEOMETRY_CONTAINS_FILTER: SpatialFilterMode SPATIAL_COORDINATE_SYSTEM_UNSPECIFIED: SpatialCoordinateSystem SPATIAL_COORDINATE_SYSTEM_CARTESIAN: SpatialCoordinateSystem SPATIAL_COORDINATE_SYSTEM_SPHERICAL: SpatialCoordinateSystem diff --git a/tilebox-datasets/tilebox/datasets/sync/dataset.py b/tilebox-datasets/tilebox/datasets/sync/dataset.py index 3bc2a03..40eab29 100644 --- a/tilebox-datasets/tilebox/datasets/sync/dataset.py +++ b/tilebox-datasets/tilebox/datasets/sync/dataset.py @@ -457,12 +457,12 @@ def query( spatial_extent: The spatial extent to query data in. (Optional) Expected to be either a shapely geometry, or a dict with the following keys: - geometry: The geometry to query by. Must be a shapely.Polygon, shapely.MultiPolygon or shapely.Point. - - mode: The spatial filter mode to use. Can be one of "intersects" or "contains". - Defaults to "intersects". + - mode: The spatial filter mode to use. Can be one of "intersects", "filter_contains_geometry", or + "geometry_contains_filter". Defaults to "intersects". - coordinate_system: The coordinate system to use for performing geometry calculations. Can be one of "cartesian" or "spherical". Only supported for spatiotemporal datasets. Will raise an error if used for other dataset types. - All datapoints whose geometry intersects the given spatial extent will be returned. + Datapoints matching the selected spatial filter mode will be returned. skip_data: Whether to skip the actual data of the datapoint. If True, only datapoint metadata is returned. show_progress: Whether to show a progress bar while loading the data. If a callable is specified it is used as callback to report progress percentages. diff --git a/tilebox-workflows/tests/jobs/test_client.py b/tilebox-workflows/tests/jobs/test_client.py index 0a55c22..ad642a9 100644 --- a/tilebox-workflows/tests/jobs/test_client.py +++ b/tilebox-workflows/tests/jobs/test_client.py @@ -14,6 +14,8 @@ JobState, LogRecord, LogRecords, + LogSeverity, + LogSeverityLiteral, QueryJobLogsResponse, QueryJobSpansResponse, Span, @@ -23,6 +25,7 @@ ) from tilebox.workflows.jobs.client import JobClient from tilebox.workflows.jobs.service import JobService +from tilebox.workflows.jobs.telemetry_service import TelemetryService from tilebox.workflows.observability.tracing import NoopWorkflowTracer from tilebox.workflows.task import ExecutionContext, Task from tilebox.workflows.workflows.v1.core_pb2 import Job as JobMessage @@ -40,6 +43,7 @@ VisualizeJobRequest, ) from tilebox.workflows.workflows.v1.job_pb2_grpc import JobServiceStub +from tilebox.workflows.workflows.v1.telemetry_pb2 import PaginatedLogsData, PaginatedSpansData class DummyTask(Task): @@ -89,6 +93,32 @@ def test_query_logs_paginates() -> None: ] +@pytest.mark.parametrize( + ("level", "expected"), + [ + (LogSeverity.WARNING, [LogSeverity.WARNING.value]), + ("warning", [LogSeverity.WARNING.value]), + ([LogSeverity.WARNING, LogSeverity.ERROR], [LogSeverity.WARNING.value, LogSeverity.ERROR.value]), + (["warning", "error"], [LogSeverity.WARNING.value, LogSeverity.ERROR.value]), + ], +) +def test_query_logs_filters_by_task_and_severity( + level: LogSeverity | LogSeverityLiteral | list[LogSeverity] | list[LogSeverityLiteral], expected: list[int] +) -> None: + service = MagicMock() + service.QueryJobLogs.return_value = PaginatedLogsData() + telemetry_service = TelemetryService(MagicMock()) + telemetry_service.service = service + job_client = JobClient(MagicMock(), telemetry_service, NoopWorkflowTracer()) + task_id = uuid4() + + job_client.query_logs(uuid4(), task_id=str(task_id), level=level) + + request = service.QueryJobLogs.call_args.args[0] + assert uuid_message_to_uuid(request.task_id) == task_id + assert list(request.filters.severity_levels) == expected + + def test_query_spans_paginates() -> None: next_page_start = uuid4() spans = [ @@ -137,6 +167,20 @@ def test_query_spans_paginates() -> None: ] +def test_query_spans_filters_by_task() -> None: + service = MagicMock() + service.QueryJobSpans.return_value = PaginatedSpansData() + telemetry_service = TelemetryService(MagicMock()) + telemetry_service.service = service + job_client = JobClient(MagicMock(), telemetry_service, NoopWorkflowTracer()) + task_id = uuid4() + + job_client.query_spans(uuid4(), task_id=str(task_id)) + + request = service.QueryJobSpans.call_args.args[0] + assert uuid_message_to_uuid(request.task_id) == task_id + + class MockJobService(JobServiceStub): """A mock implementation of the gRPC job service, that stores jobs in memory as a dict.""" @@ -221,6 +265,19 @@ def test_query_empty_cluster_list_applies_no_cluster_filter() -> None: assert list(mock_service.query_requests[-1].filters.cluster_slugs) == [] +def test_query_without_temporal_extent() -> None: + service = JobService(MagicMock()) + mock_service = MockJobService() + service.service = mock_service + job_client = JobClient(service, MagicMock(), NoopWorkflowTracer()) + + job_client.query(job_states=JobState.RUNNING) + + filters = mock_service.query_requests[-1].filters + assert not filters.HasField("time_interval") + assert not filters.HasField("id_interval") + + def test_query_rejects_empty_cluster_slug() -> None: service = JobService(MagicMock()) service.service = MockJobService() diff --git a/tilebox-workflows/tilebox/workflows/data.py b/tilebox-workflows/tilebox/workflows/data.py index 5b52c6f..261ff38 100644 --- a/tilebox-workflows/tilebox/workflows/data.py +++ b/tilebox-workflows/tilebox/workflows/data.py @@ -6,7 +6,7 @@ from enum import Enum from functools import lru_cache from pathlib import Path -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any, Literal, TypeAlias, cast from uuid import UUID from google.protobuf.duration_pb2 import Duration @@ -37,7 +37,7 @@ WorkflowTracer = Any from tilebox.workflows.workflows.v1 import automation_pb2 as automation_pb -from tilebox.workflows.workflows.v1 import core_pb2, job_pb2, task_pb2, workflows_pb2 +from tilebox.workflows.workflows.v1 import core_pb2, job_pb2, task_pb2, telemetry_pb2, workflows_pb2 _VERSION_PATTERN = re.compile(r"^v(\d+)\.(\d+)$") # matches a version string in the format "v3.2" @@ -701,6 +701,17 @@ def _parse_version(version: str) -> tuple[int, int]: raise ValueError(f"Invalid version string: {version}") +class LogSeverity(Enum): + TRACE = telemetry_pb2.LOG_SEVERITY_GROUP_TRACE + DEBUG = telemetry_pb2.LOG_SEVERITY_GROUP_DEBUG + INFO = telemetry_pb2.LOG_SEVERITY_GROUP_INFO + WARNING = telemetry_pb2.LOG_SEVERITY_GROUP_WARNING + ERROR = telemetry_pb2.LOG_SEVERITY_GROUP_ERROR + + +LogSeverityLiteral: TypeAlias = Literal["trace", "debug", "info", "warning", "error"] + + @dataclass class LogRecord: time: datetime diff --git a/tilebox-workflows/tilebox/workflows/jobs/client.py b/tilebox-workflows/tilebox/workflows/jobs/client.py index 3d675a6..e1c9bc7 100644 --- a/tilebox-workflows/tilebox/workflows/jobs/client.py +++ b/tilebox-workflows/tilebox/workflows/jobs/client.py @@ -11,6 +11,8 @@ Job, JobState, LogRecords, + LogSeverity, + LogSeverityLiteral, QueryFilters, QueryJobLogsResponse, QueryJobSpansResponse, @@ -138,19 +140,36 @@ def find(self, job_id: JobIDLike) -> Job: """ return self._service.get_by_id(_to_uuid(job_id)) - def query_logs(self, job_id: JobIDLike) -> LogRecords: + def query_logs( + self, + job_id: JobIDLike, + *, + task_id: UUID | str | None = None, + level: LogSeverity | LogSeverityLiteral | list[LogSeverity] | list[LogSeverityLiteral] | None = None, + ) -> LogRecords: """Query logs emitted while running a job. Args: job_id: The job or job id to query logs for. + task_id: A task id to filter logs by. + level: A severity level or list of severity levels to filter logs by. Returns: A list of log records for the given job. """ + task_uuid = UUID(task_id) if isinstance(task_id, str) else task_id + levels = level if isinstance(level, list) else [level] if level is not None else [] + try: + severity_levels = [ + LogSeverity[severity.upper()] if isinstance(severity, str) else severity for severity in levels + ] + except KeyError as error: + raise ValueError(f"Invalid log severity level: {error.args[0].lower()}") from None + def request(page: PaginationProtocol) -> QueryJobLogsResponse: query_page = Pagination(page.limit, page.starting_after) - return self._telemetry_service.query_job_logs(_to_uuid(job_id), query_page) + return self._telemetry_service.query_job_logs(_to_uuid(job_id), query_page, task_uuid, severity_levels) pages = paginated_request(request, Pagination()) @@ -159,19 +178,22 @@ def request(page: PaginationProtocol) -> QueryJobLogsResponse: logs.extend(page.logs) return logs - def query_spans(self, job_id: JobIDLike) -> Spans: + def query_spans(self, job_id: JobIDLike, *, task_id: UUID | str | None = None) -> Spans: """Query spans emitted while running a job. Args: job_id: The job or job id to query spans for. + task_id: A task id to filter spans by. Returns: A list of spans for the given job. """ + task_uuid = UUID(task_id) if isinstance(task_id, str) else task_id + def request(page: PaginationProtocol) -> QueryJobSpansResponse: query_page = Pagination(page.limit, page.starting_after) - return self._telemetry_service.query_job_spans(_to_uuid(job_id), query_page) + return self._telemetry_service.query_job_spans(_to_uuid(job_id), query_page, task_uuid) pages = paginated_request(request, Pagination()) @@ -211,18 +233,18 @@ def visualize(self, job: JobIDLike, direction: str = "down", layout: str = "dagr def query( # noqa: PLR0913, PLR0917 self, - temporal_extent: "TimeIntervalLike | IDIntervalLike", + temporal_extent: "TimeIntervalLike | IDIntervalLike | None" = None, automation_ids: UUID | list[UUID] | None = None, job_states: JobState | list[JobState] | None = None, name: str | None = None, task_states: TaskState | list[TaskState] | None = None, clusters: ClusterSlugLike | list[ClusterSlugLike] | None = None, ) -> list[Job]: - """List jobs in the given temporal extent. + """List jobs matching the given filters. Args: - temporal_extent: The temporal extent to filter jobs by. If an IDInterval is given, jobs are filtered by their - job id instead of their creation time. + temporal_extent: The optional temporal extent to filter jobs by. If an IDInterval is given, jobs are filtered + by their job id instead of their creation time. If omitted, jobs are not filtered by time or id. Can be specified in a number of ways: - TimeInterval: interval -> Use the time interval as its given - DatetimeScalar: [time, time] -> Construct a TimeInterval with start and end time set to the given @@ -250,6 +272,8 @@ def query( # noqa: PLR0913, PLR0917 time_interval: TimeInterval | None = None id_interval: IDInterval | None = None match temporal_extent: + case None: + pass case (str(), str()): # ty doesn't narrow types on match statements yet, once it does we can remove this cast str_temporal_extent: tuple[str, str] = temporal_extent # ty: ignore[invalid-assignment] diff --git a/tilebox-workflows/tilebox/workflows/jobs/telemetry_service.py b/tilebox-workflows/tilebox/workflows/jobs/telemetry_service.py index 6f8be1e..e7cebd1 100644 --- a/tilebox-workflows/tilebox/workflows/jobs/telemetry_service.py +++ b/tilebox-workflows/tilebox/workflows/jobs/telemetry_service.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, cast from uuid import UUID from grpc import Channel @@ -6,11 +6,14 @@ from _tilebox.grpc.error import with_pythonic_errors from tilebox.datasets.query.pagination import Pagination from tilebox.workflows.data import ( + LogSeverity, QueryJobLogsResponse, QueryJobSpansResponse, uuid_to_uuid_message, ) +from tilebox.workflows.workflows.v1 import telemetry_pb2 from tilebox.workflows.workflows.v1.telemetry_pb2 import ( + LogQueryFilters, PaginatedLogsData, PaginatedSpansData, QueryJobLogsRequest, @@ -32,18 +35,33 @@ def __init__(self, channel: Channel | Any) -> None: with_pythonic_errors(TelemetryQueryServiceStub(channel)) if hasattr(channel, "unary_unary") else channel ) - def query_job_logs(self, job_id: UUID, page: Pagination | None = None) -> QueryJobLogsResponse: + def query_job_logs( + self, + job_id: UUID, + page: Pagination | None = None, + task_id: UUID | None = None, + severity_levels: list[LogSeverity] | None = None, + ) -> QueryJobLogsResponse: request = QueryJobLogsRequest( job_id=uuid_to_uuid_message(job_id), page=page.to_message() if page is not None else None, + task_id=uuid_to_uuid_message(task_id) if task_id is not None else None, + filters=LogQueryFilters( + severity_levels=[cast(telemetry_pb2.LogSeverityGroup, severity.value) for severity in severity_levels] + ) + if severity_levels + else None, ) response: PaginatedLogsData = self.service.QueryJobLogs(request) return QueryJobLogsResponse.from_message(response) - def query_job_spans(self, job_id: UUID, page: Pagination | None = None) -> QueryJobSpansResponse: + def query_job_spans( + self, job_id: UUID, page: Pagination | None = None, task_id: UUID | None = None + ) -> QueryJobSpansResponse: request = QueryJobSpansRequest( job_id=uuid_to_uuid_message(job_id), page=page.to_message() if page is not None else None, + task_id=uuid_to_uuid_message(task_id) if task_id is not None else None, ) response: PaginatedSpansData = self.service.QueryJobSpans(request) return QueryJobSpansResponse.from_message(response) diff --git a/uv.lock b/uv.lock index 4f5a9f2..d00ac7d 100644 --- a/uv.lock +++ b/uv.lock @@ -1357,7 +1357,7 @@ wheels = [ [[package]] name = "ipython" -version = "9.16.1" +version = "9.17.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", @@ -1383,9 +1383,9 @@ dependencies = [ { name = "traitlets" }, { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/96/b150fe7e25a5a29ae9ac1374e71488639605d39a1ea4abb74c9ce33af235/ipython-9.16.1.tar.gz", hash = "sha256:5a3d1f9a47ff216d6cf9cf863124f6a2c1a198d1354c546a4d24a370a283b64c", size = 4515302, upload-time = "2026-08-03T08:36:15.571Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/bc/e05ae123712ce4e1fde4408eedca1791fc1ff832684565132ea1dc646092/ipython-9.17.0.tar.gz", hash = "sha256:1dc69e6966b270fb259f676c71a21450e63607729b14a672b942914a54e8b730", size = 4538547, upload-time = "2026-08-28T09:00:58.233Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/8e/1239df488393d61076653bfb29f759d0f60cab8e030abdf7c17c31539b51/ipython-9.16.1-py3-none-any.whl", hash = "sha256:4acae635506f6d352d94c4899a19d5f85f8bc4d230932342dca556fdab1c69b4", size = 625974, upload-time = "2026-08-03T08:36:13.654Z" }, + { url = "https://files.pythonhosted.org/packages/18/5f/f992b57e8deb8fa6c2e614b422d80698adb5107902bbc89832e203665bd1/ipython-9.17.0-py3-none-any.whl", hash = "sha256:ce647713be8fef3fab2418c515a0def4d45d6705dd102be2c6d1f3015d7368b0", size = 638698, upload-time = "2026-08-28T09:00:56.174Z" }, ] [[package]] @@ -1407,7 +1407,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "comm" }, { name = "ipython", version = "8.39.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "ipython", version = "9.16.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "ipython", version = "9.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "jupyterlab-widgets" }, { name = "traitlets" }, { name = "widgetsnbextension" }, @@ -3742,16 +3742,16 @@ wheels = [ [[package]] name = "urllib3-future" -version = "2.24.904" +version = "2.24.905" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "h11" }, { name = "jh2" }, { name = "qh3", marker = "(python_full_version < '3.12' and platform_machine == 'AMD64' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'ARM64' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'arm64' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'armv7l' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'i686' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'ppc64' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'ppc64le' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'riscv64' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'riscv64gc' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 's390x' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'x86' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'x86_64' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'AMD64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'ARM64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'arm64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'armv7l' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'i686' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'ppc64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'ppc64le' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'riscv64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'riscv64gc' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 's390x' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'x86' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'x86_64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'AMD64' and platform_python_implementation == 'PyPy' and sys_platform == 'win32') or (python_full_version < '3.12' and platform_machine == 'ARM64' and platform_python_implementation == 'PyPy' and sys_platform == 'win32') or (python_full_version < '3.12' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'win32') or (python_full_version < '3.12' and platform_machine == 'arm64' and platform_python_implementation == 'PyPy' and sys_platform == 'win32') or (python_full_version < '3.12' and platform_machine == 'armv7l' and platform_python_implementation == 'PyPy' and sys_platform == 'win32') or (python_full_version < '3.12' and platform_machine == 'i686' and platform_python_implementation == 'PyPy' and sys_platform == 'win32') or (python_full_version < '3.12' and platform_machine == 'ppc64' and platform_python_implementation == 'PyPy' and sys_platform == 'win32') or (python_full_version < '3.12' and platform_machine == 'ppc64le' and platform_python_implementation == 'PyPy' and sys_platform == 'win32') or (python_full_version < '3.12' and platform_machine == 'riscv64' and platform_python_implementation == 'PyPy' and sys_platform == 'win32') or (python_full_version < '3.12' and platform_machine == 'riscv64gc' and platform_python_implementation == 'PyPy' and sys_platform == 'win32') or (python_full_version < '3.12' and platform_machine == 's390x' and platform_python_implementation == 'PyPy' and sys_platform == 'win32') or (python_full_version < '3.12' and platform_machine == 'x86' and platform_python_implementation == 'PyPy' and sys_platform == 'win32') or (python_full_version < '3.12' and platform_machine == 'x86_64' and platform_python_implementation == 'PyPy' and sys_platform == 'win32') or (platform_machine == 'AMD64' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'ARM64' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'arm64' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'armv7l' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'i686' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'ppc64' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'ppc64le' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'riscv64' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'riscv64gc' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 's390x' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'x86' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'AMD64' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'ARM64' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'arm64' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'armv7l' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'i686' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'ppc64' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'ppc64le' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'riscv64' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'riscv64gc' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 's390x' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'x86' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'x86_64' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'AMD64' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'ARM64' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'arm64' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'armv7l' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'i686' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'ppc64' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'ppc64le' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'riscv64' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'riscv64gc' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 's390x' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'x86' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'x86_64' and platform_python_implementation == 'CPython' and sys_platform == 'win32')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f0/71/13dd9927c0b165ee42f6f04f2a5abe3837a185b4fec9503d39aee939d1e4/urllib3_future-2.24.904.tar.gz", hash = "sha256:0850f6c829b9fa83da83cbf67499a24fa63f5828b68c2c090649b930783d33a7", size = 1384369, upload-time = "2026-08-24T14:46:12.72Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/dd/da4c654c8b14556e029dd20136c28fd0e40b056fed88cbbe55c1ba2edd7b/urllib3_future-2.24.905.tar.gz", hash = "sha256:bb07b332340f81d38f47080242e4ab0ae46fc3a257666e390b65e47b8d2b10a1", size = 1385778, upload-time = "2026-08-28T08:48:21.237Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/a6/32b467808b459ae5d250bd1193f9f04bce19dc8dfd77e0dc1a42850d944f/urllib3_future-2.24.904-py3-none-any.whl", hash = "sha256:b1b3062457ee7fe330a5aeba76cd063e2bfcb9c8c8579213e5c7dad4e069e7e4", size = 817748, upload-time = "2026-08-24T14:46:10.905Z" }, + { url = "https://files.pythonhosted.org/packages/01/f9/518ed3aba95c2e8058369c1bcbc2a49ec21644dd3390ae5506b6b3099d52/urllib3_future-2.24.905-py3-none-any.whl", hash = "sha256:539d5b29e8d56ea6c1142d81066605ee46c5430950e513de58f1da1d9611593d", size = 818281, upload-time = "2026-08-28T08:48:19.512Z" }, ] [[package]]