Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,16 @@ jobs:
- name: Ruff
run: uv run ruff format --check . && uv run ruff check .
- name: Ty
run: uv run ty check
run: |
uv run ty check --python-version 3.11
uv run ty check --python-version 3.14

tests:
name: Run tests
strategy:
matrix:
os: [Ubuntu]
python-version: ["3.10", "3.11", "3.12", "3.13"]
python-version: ["3.11", "3.12", "3.13", "3.14"]
runs-on: ${{ matrix.os }}-latest
steps:
- uses: actions/checkout@v7
Expand All @@ -57,3 +59,26 @@ jobs:
with:
paths: "test-report.xml"
if: always()

minimum-dependencies:
name: Test minimum dependencies
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
lfs: true
- name: Set up uv
uses: astral-sh/setup-uv@v8.3.2
with:
python-version: "3.11"
- name: Resolve lowest compatible direct dependencies
# Select the SDK packages, not the root's unbounded lint/build tooling.
run: >-
uv sync --resolution lowest-direct --upgrade
--package tilebox-grpc --package tilebox-datasets
--package tilebox-storage --package tilebox-workflows
- name: Run tests without replacing the minimum-version environment
run: |
for package in tilebox-grpc tilebox-datasets tilebox-storage tilebox-workflows; do
uv run --no-sync pytest "$package" -Wall -Werror --import-mode=importlib
done
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Pre-commit hooks include YAML checks, EOF fixer, `sync-with-uv`, Ruff, and `ty`.

## Code Style And Paradigms

- Python target: `>=3.10`.
- Python target: `>=3.11`; supported versions are 3.11 through 3.14.
- Linting/formatting is Ruff-based and fairly strict (`select = ["ALL"]`) with explicit ignores configured in root `pyproject.toml`.
- Type hints are used broadly across public APIs and internals.
- Prefer dataclasses and explicit domain objects for request/response translation.
Expand Down
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Support Python 3.14 across all packages.
- Test the lowest compatible direct dependencies on Python 3.11 in CI.

### Changed

- Require Python 3.11 or newer across all packages, removing Python 3.10 compatibility code and typing backfills.
- `tilebox-grpc`: Use `nest-asyncio2` for nested event-loop support on Python 3.14. Restart notebook kernels already
patched by `nest-asyncio` and apply `nest-asyncio2` before the old library to use the updated patch.
- Raise dependency minimums to remove obsolete compatibility workarounds: boto3 1.40.2, OpenTelemetry 1.43.0
(logging instrumentation 0.64b0), grpcio 1.84.0, and pyqwest 0.7.0.
- `tilebox-datasets`: Require NumPy 1.25, pandas 2.2.2, xarray 2024.6, and Shapely 2.0.6 or newer.
- Require protobuf 6.31.0 for repeated-field descriptors and protobuf-py 0.2.0 for integer-to-float serialization.
- `tilebox-workflows`: Make interactive notebook progress an optional `notebook` extra; without it, jobs display
as plain text. Move S3 type stubs to development dependencies and use standard-library local timezone conversion.
- Remove unused runtime dependencies: `grpcio-status`, `cftime`, and storage's `folium` and redundant `shapely`
requirement. Install `cftime` separately if your own xarray processing uses non-standard calendars.

### Fixed

- Explicitly trust system certificates for Connect HTTP/1 transports with pyqwest 0.7 and newer.

## [0.62.0] - 2026-09-16

### Added
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ Python library for [Tilebox](https://tilebox.com), a lightweight space data mana

## Install

Requires Python 3.11 or newer. Python 3.11 through 3.14 are supported and tested.

```bash
pip install tilebox
```
Expand Down
27 changes: 13 additions & 14 deletions matrix.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@

[[scenarios]]
name = "datasets-pandas2"
python = ["3.10", "3.11", "3.12", "3.13"]
python = ["3.11", "3.12", "3.13", "3.14"]
working-dir = "tilebox-datasets"
test-command = "pytest"
test-args = ["-v"]

[scenarios.packages]
pandas = ["2.2.3"]
pandas = ["2.3.3"] # Python 3.14 support

[[scenarios]]
name = "datasets-pandas3"
python = ["3.11", "3.12", "3.13"] # pandas 3.0 requires Python 3.11+
python = ["3.11", "3.12", "3.13", "3.14"]
working-dir = "tilebox-datasets"
test-command = "pytest"
test-args = ["-v"]
Expand All @@ -26,17 +26,17 @@ pandas = ["3.0.0"]

[[scenarios]]
name = "storage-pandas2"
python = ["3.10", "3.11", "3.12", "3.13"]
python = ["3.11", "3.12", "3.13", "3.14"]
working-dir = "tilebox-storage"
test-command = "pytest"
test-args = ["-v"]

[scenarios.packages]
pandas = ["2.2.3"]
pandas = ["2.3.3"]

[[scenarios]]
name = "storage-pandas3"
python = ["3.11", "3.12", "3.13"] # pandas 3.0 requires Python 3.11+
python = ["3.11", "3.12", "3.13", "3.14"]
working-dir = "tilebox-storage"
test-command = "pytest"
test-args = ["-v"]
Expand All @@ -46,17 +46,17 @@ pandas = ["3.0.0"]

[[scenarios]]
name = "grpc-pandas2"
python = ["3.10", "3.11", "3.12", "3.13"]
python = ["3.11", "3.12", "3.13", "3.14"]
working-dir = "tilebox-grpc"
test-command = "pytest"
test-args = ["-v"]

[scenarios.packages]
pandas = ["2.2.3"]
pandas = ["2.3.3"]

[[scenarios]]
name = "grpc-pandas3"
python = ["3.11", "3.12", "3.13"] # pandas 3.0 requires Python 3.11+
python = ["3.11", "3.12", "3.13", "3.14"]
working-dir = "tilebox-grpc"
test-command = "pytest"
test-args = ["-v"]
Expand All @@ -66,18 +66,17 @@ pandas = ["3.0.0"]

[[scenarios]]
name = "workflows-pandas2"
python = ["3.10", "3.11", "3.12", "3.13"]
python = ["3.11", "3.12", "3.13", "3.14"]
working-dir = "tilebox-workflows"
test-command = "pytest"
# Ignore FutureWarning: google-cloud-storage raises deprecation warning on Python 3.10
test-args = ["-v", "-W", "ignore::FutureWarning"]
test-args = ["-v"]

[scenarios.packages]
pandas = ["2.2.3"]
pandas = ["2.3.3"]

[[scenarios]]
name = "workflows-pandas3"
python = ["3.11", "3.12", "3.13"] # pandas 3.0 requires Python 3.11+
python = ["3.11", "3.12", "3.13", "3.14"]
working-dir = "tilebox-workflows"
test-command = "pytest"
test-args = ["-v"]
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ dynamic = ["version"]
description = "Tilebox Python packages"
authors = [{ name = "Tilebox, Inc.", email = "support@tilebox.com" }]
readme = "README.md"
requires-python = ">=3.10"
requires-python = ">=3.11"
dependencies = [
"tilebox-datasets>=0.56.0",
"tilebox-grpc",
Expand Down Expand Up @@ -63,7 +63,7 @@ source = "uv-dynamic-versioning"
[tool.ruff]
# settings applying to all ruff subcommands, such as `ruff check` (lint) and `ruff format`
line-length = 120
target-version = "py310"
target-version = "py311"
exclude = [
"*/.venv/*",
# it's auto generated, don't lint it
Expand Down
9 changes: 1 addition & 8 deletions tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,7 @@ for package in $packages; do
module=_tilebox
fi

PYTHON_VERSION=$(uv run python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null || true)

if [ "$PYTHON_VERSION" = "3.10" ]; then
# ignore: FutureWarning: You are using a Python version (3.10.11) which Google will stop supporting in new releases of google.api_core once it reaches its end of life (2026-10-04).
uv run --all-packages pytest -Wall -Werror -W "ignore::FutureWarning" --cov=$module --cov-branch -v --junitxml=test-report.xml . || exit 1
else
uv run --all-packages pytest -Wall -Werror --cov=$module --cov-branch -v --junitxml=test-report.xml . || exit 1
fi
uv run --all-packages pytest -Wall -Werror --cov=$module --cov-branch -v --junitxml=test-report.xml . || exit 1

cd .. || exit 1 # cd back to the root of the monorepo
done
Expand Down
17 changes: 7 additions & 10 deletions tilebox-datasets/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,24 @@ classifiers = [
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Scientific/Engineering",
"Topic :: Software Development",
]
requires-python = ">=3.10"
requires-python = ">=3.11"
dependencies = [
"tilebox-grpc>=0.28.0",
# cftime is a dependency of xarray, and we need this version at least to support numpy v2.0
"cftime>=1.6.4",
# below version pins are some minimum sane versions that we tested against to make sure they are working
"numpy>=1.24",
"xarray>=2023.11",
"numpy>=1.25",
"xarray>=2024.6",
"tqdm>=4.65",
"pandas>=2.1",
"shapely>=2",
"pandas>=2.2.2",
"shapely>=2.0.6",
"promise>=2.3",
"typing-extensions>=4.5",
"protobuf-py>=0.1.1",
"protobuf-py>=0.2.0", # supports integer values in floating-point fields
]

[dependency-groups]
Expand Down
4 changes: 2 additions & 2 deletions tilebox-datasets/tests/data/collection.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import string
from datetime import timezone
from datetime import UTC

from hypothesis.strategies import DrawFn, composite, integers, just, none, text, uuids

Expand Down Expand Up @@ -28,6 +28,6 @@ def collections(draw: DrawFn) -> Collection:
def collection_infos(draw: DrawFn) -> CollectionInfo:
"""A hypothesis strategy for generating random collection infos"""
collection = draw(collections())
interval = draw(none() | just(_EMPTY_TIME_INTERVAL) | time_intervals(tzinfo=timezone.utc))
interval = draw(none() | just(_EMPTY_TIME_INTERVAL) | time_intervals(tzinfo=UTC))
count = draw(none() | uint64s)
return CollectionInfo(collection, interval, count)
18 changes: 9 additions & 9 deletions tilebox-datasets/tests/data/test_data_access.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from uuid import uuid4

import pytest
Expand Down Expand Up @@ -64,8 +64,8 @@ def test_query_filters_to_message_and_back(q: QueryFilters) -> None:
def test_query_filters_expression_to_message_and_back() -> None:
query_filter = QueryFilters(
temporal_extent=TimeInterval(
datetime(2026, 7, 27, tzinfo=timezone.utc),
datetime(2026, 7, 28, tzinfo=timezone.utc),
datetime(2026, 7, 27, tzinfo=UTC),
datetime(2026, 7, 28, tzinfo=UTC),
),
filter=(field("cloud_cover") < 20) | field("cloud_cover").is_null(),
)
Expand All @@ -78,8 +78,8 @@ def test_query_filters_expression_to_message_and_back() -> None:
def test_multiple_wire_expressions_are_combined_with_and() -> None:
query_filter = QueryFilters(
TimeInterval(
datetime.now(tz=timezone.utc),
datetime.now(tz=timezone.utc) + timedelta(days=1),
datetime.now(tz=UTC),
datetime.now(tz=UTC) + timedelta(days=1),
)
)
message = query_filter.to_message()
Expand All @@ -100,8 +100,8 @@ def test_query_filters_reject_invalid_filter() -> None:
with pytest.raises(TypeError, match="Expected a query expression"):
QueryFilters(
TimeInterval(
datetime(2026, 7, 27, tzinfo=timezone.utc),
datetime(2026, 7, 28, tzinfo=timezone.utc),
datetime(2026, 7, 27, tzinfo=UTC),
datetime(2026, 7, 28, tzinfo=UTC),
),
filter="quality > 80", # ty: ignore[invalid-argument-type]
)
Expand All @@ -113,8 +113,8 @@ def test_query_filters_reject_invalid_interval_variants() -> None:

message = QueryFilters(
TimeInterval(
datetime(2026, 7, 27, tzinfo=timezone.utc),
datetime(2026, 7, 28, tzinfo=timezone.utc),
datetime(2026, 7, 27, tzinfo=UTC),
datetime(2026, 7, 28, tzinfo=UTC),
)
).to_message()
message.datapoint_interval.CopyFrom(IDInterval(uuid4(), uuid4(), False, False).to_message())
Expand Down
12 changes: 6 additions & 6 deletions tilebox-datasets/tests/protobuf_conversion/test_to_protobuf.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import datetime, timezone
from datetime import UTC, datetime

import numpy as np
import pandas as pd
Expand Down Expand Up @@ -46,7 +46,7 @@ def test_pandas_to_protobuf_messages(datapoints: list[pd.DataFrame]) -> None:


def test_record_oriented_data_preserves_absent_fields_and_filters_missing_values() -> None:
time = datetime(2026, 7, 31, tzinfo=timezone.utc)
time = datetime(2026, 7, 31, tzinfo=UTC)
converted = to_messages(
[
{
Expand All @@ -71,7 +71,7 @@ def test_record_oriented_data_preserves_absent_fields_and_filters_missing_values
def test_record_oriented_data_requires_every_record_to_have_required_fields() -> None:
with pytest.raises(ValueError, match=r"Record 1: Missing required field.*time"):
to_messages(
[{"time": datetime(2026, 7, 31, tzinfo=timezone.utc)}, {"some_int": 1}],
[{"time": datetime(2026, 7, 31, tzinfo=UTC)}, {"some_int": 1}],
ExampleDatapoint,
required_fields=["time"],
)
Expand All @@ -87,7 +87,7 @@ def test_record_oriented_data_rejects_required_values_that_convert_to_unset() ->


def test_dataframe_missing_values_leave_optional_fields_unset() -> None:
time = datetime(2026, 7, 31, tzinfo=timezone.utc)
time = datetime(2026, 7, 31, tzinfo=UTC)
dataframe = pd.DataFrame([{"time": time, "some_bool": True}, {"time": time}])

converted = to_messages(dataframe, ExampleDatapoint, required_fields=["time"])
Expand All @@ -98,12 +98,12 @@ def test_dataframe_missing_values_leave_optional_fields_unset() -> None:

def test_iterable_of_column_tuples_is_rejected_as_invalid_records() -> None:
with pytest.raises(TypeError, match="record 0 is tuple"):
to_messages([("time", [datetime(2026, 7, 31, tzinfo=timezone.utc)])], ExampleDatapoint) # ty: ignore[invalid-argument-type]
to_messages([("time", [datetime(2026, 7, 31, tzinfo=UTC)])], ExampleDatapoint) # ty: ignore[invalid-argument-type]


def test_ignored_columns_do_not_participate_in_shape_validation() -> None:
converted = to_messages(
{"time": [datetime(2026, 7, 31, tzinfo=timezone.utc)], "id": []},
{"time": [datetime(2026, 7, 31, tzinfo=UTC)], "id": []},
ExampleDatapoint,
required_fields=["time"],
ignore_fields=["id"],
Expand Down
4 changes: 2 additions & 2 deletions tilebox-datasets/tests/query/test_expression.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from enum import Enum, IntEnum

import numpy as np
Expand Down Expand Up @@ -38,7 +38,7 @@ def test_query_value_types(value: object, kind: str, expected: object) -> None:
@pytest.mark.parametrize(
"value",
[
datetime(2026, 7, 27, 12, 30, tzinfo=timezone.utc),
datetime(2026, 7, 27, 12, 30, tzinfo=UTC),
timedelta(days=-1, microseconds=123),
np.datetime64("2026-07-27T12:30:00.123456789"),
np.timedelta64(-123456789, "ns"),
Expand Down
Loading
Loading