Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- `tilebox-datasets`, `tilebox-workflows`: Use `TILEBOX_API_URL` as the default API URL when no explicit client URL is provided, falling back to production when the environment variable is unset or empty.

## [0.61.0] - 2026-09-04

### Added
Expand Down
14 changes: 4 additions & 10 deletions prek.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ hooks = [

[[repos]]
repo = "https://github.com/charliermarsh/ruff-pre-commit"
rev = "v0.16.5"
rev = "v0.16.7"
hooks = [
{
id = "ruff-check",
Expand All @@ -32,14 +32,8 @@ hooks = [
]

[[repos]]
repo = "local"
repo = "https://github.com/astral-sh/ty-pre-commit"
rev = "v0.0.79"
hooks = [
{
id = "ty",
name = "ty-check",
entry = "uv run ty check",
language = "python",
types = ["python"],
pass_filenames = true
}
{ id = "ty" },
]
31 changes: 31 additions & 0 deletions tilebox-datasets/tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,42 @@
from _tilebox.grpc.error import NotFoundError
from _tilebox.grpc.replay import open_recording_channel, open_replay_channel
from tilebox.datasets import Client, DatasetClient
from tilebox.datasets.aio.client import Client as AsyncClient
from tilebox.datasets.client import _TILEBOX_API_URL, _TILEBOX_DEV_API_URL
from tilebox.datasets.data.datapoint import QueryResultPage
from tilebox.datasets.query.time_interval import us_to_datetime


@pytest.mark.parametrize("client_type", [Client, AsyncClient])
@pytest.mark.parametrize(
("environment_url", "explicit_url", "expected_url"),
[
(None, None, "https://api.tilebox.com"),
("", None, "https://api.tilebox.com"),
("https://runner.example.com/", None, "https://runner.example.com"),
("https://runner.example.com", "https://explicit.example.com", "https://explicit.example.com"),
("https://runner.example.com", "https://api.tilebox.com", "https://api.tilebox.com"),
],
)
def test_client_url_environment(
monkeypatch: pytest.MonkeyPatch,
client_type: type[Client] | type[AsyncClient],
environment_url: str | None,
explicit_url: str | None,
expected_url: str,
) -> None:
monkeypatch.delenv("TILEBOX_API_URL", raising=False)
if environment_url is not None:
monkeypatch.setenv("TILEBOX_API_URL", environment_url)
monkeypatch.setenv("TILEBOX_API_KEY", "runner-key")
with patch(f"{client_type.__module__}.open_channel") as open_channel_mock:
if explicit_url is None:
client_type()
else:
client_type(url=explicit_url)
open_channel_mock.assert_called_once_with(expected_url, "runner-key", rpc_method_prefix=None)


def test_heavy_imports_are_lazy() -> None:
code = (
"import sys\n"
Expand Down
7 changes: 5 additions & 2 deletions tilebox-datasets/tilebox/datasets/aio/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class Client:
def __init__(
self,
*,
url: str = _TILEBOX_API_URL,
url: str | None = None,
token: str | None = None,
warn_if_unauthenticated: bool = True,
transport: Transport = "grpc",
Expand All @@ -37,14 +37,17 @@ def __init__(
Create a Tilebox datasets client.

Args:
url: Tilebox API Url. Defaults to "https://api.tilebox.com".
url: Tilebox API URL. If not set, uses the `TILEBOX_API_URL` environment variable,
or defaulting to "https://api.tilebox.com".
token: The API Key to authenticate with. If not set the `TILEBOX_API_KEY` environment variable will be used.
If no token is provided or found, anonymous open data access will be used.
warn_if_unauthenticated: Whether to warn if no API key is provided and the client is used with the default
Tilebox API URL. Defaults to True.
transport: Network transport to use for API requests. Defaults to "grpc". Use "http1" to force
the Connect protocol over HTTP/1.1 for networks that do not support gRPC over HTTP/2 correctly.
"""
if url is None:
url = os.environ.get("TILEBOX_API_URL") or _TILEBOX_API_URL
url = url.removesuffix("/")

if token is None:
Expand Down
7 changes: 5 additions & 2 deletions tilebox-datasets/tilebox/datasets/sync/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class Client:
def __init__(
self,
*,
url: str = _TILEBOX_API_URL,
url: str | None = None,
token: str | None = None,
warn_if_unauthenticated: bool = True,
transport: Transport = "grpc",
Expand All @@ -36,14 +36,17 @@ def __init__(
Create a Tilebox datasets client.

Args:
url: Tilebox API Url. Defaults to "https://api.tilebox.com".
url: Tilebox API URL. If not set, uses the `TILEBOX_API_URL` environment variable,
or defaulting to "https://api.tilebox.com".
token: The API Key to authenticate with. If not set the `TILEBOX_API_KEY` environment variable will be used.
If no token is provided or found, anonymous open data access will be used.
warn_if_unauthenticated: Whether to warn if no API key is provided and the client is used with the default
Tilebox API URL. Defaults to True.
transport: Network transport to use for API requests. Defaults to "grpc". Use "http1" to force
the Connect protocol over HTTP/1.1 for networks that do not support gRPC over HTTP/2 correctly.
"""
if url is None:
url = os.environ.get("TILEBOX_API_URL") or _TILEBOX_API_URL
url = url.removesuffix("/")

if token is None:
Expand Down
16 changes: 14 additions & 2 deletions tilebox-storage/tests/test_geotiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@
from tilebox.storage.geotiff import window_from_bounds # noqa: E402


def _geotiff() -> GeoTIFF:
def _geotiff(transform: Affine = Affine(1, 0, 0, 0, -1, 10)) -> GeoTIFF:
return cast(
GeoTIFF,
SimpleNamespace(
crs="EPSG:4326",
transform=Affine(1, 0, 0, 0, -1, 10),
transform=transform,
width=10,
height=10,
),
Expand All @@ -33,6 +33,18 @@ def test_window_clips_partial_overlap() -> None:
assert window_from_bounds(_geotiff(), (-3, 8, 3, 12), crs="EPSG:4326") == Window(0, 0, 3, 2)


def test_window_from_sheared_transform() -> None:
# x = 2 * col + row, y = 10 - row: rows span [3, 7], columns [-1.5, 3.5].
geotiff = _geotiff(Affine(2, 1, 0, 0, -1, 10))
assert window_from_bounds(geotiff, (4, 3, 10, 7), crs="EPSG:4326") == Window(0, 3, 4, 4)


def test_window_rejects_noninvertible_transform() -> None:
geotiff = _geotiff(Affine(1, 2, 0, 2, 4, 10))
with pytest.raises(ValueError, match="GeoTIFF transform is not invertible"):
window_from_bounds(geotiff, (1, 2, 3, 5), crs="EPSG:4326")


def test_window_requires_full_containment() -> None:
with pytest.raises(ValueError, match="not fully contained"):
window_from_bounds(_geotiff(), (-1, 2, 3, 5), crs="EPSG:4326", require_fully_contained=True)
Expand Down
6 changes: 4 additions & 2 deletions tilebox-storage/tilebox/storage/geotiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from pyproj import CRS, Proj, Transformer

try:
from affine import TransformNotInvertibleError
from async_geotiff import GeoTIFF, Window
except ImportError:
if sys.version_info < (3, 11):
Expand Down Expand Up @@ -78,9 +79,10 @@ def window_from_bounds( # noqa: C901
left, bottom, right, top = transformed
try:
inverse = ~geotiff.transform
pixels = [inverse * (x, y) for x in (left, right) for y in (bottom, top)]
except Exception as error:
except TransformNotInvertibleError as error:
raise ValueError("GeoTIFF transform is not invertible") from error
pixels = [(x, y) for x in (left, right) for y in (bottom, top)]
inverse.itransform(pixels)
col_start = math.floor(min(point[0] for point in pixels))
col_stop = math.ceil(max(point[0] for point in pixels))
row_start = math.floor(min(point[1] for point in pixels))
Expand Down
37 changes: 37 additions & 0 deletions tilebox-workflows/tests/test_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from unittest.mock import patch

import pytest

from tilebox.workflows import Client


@pytest.mark.parametrize(
("environment_url", "explicit_url", "expected_url"),
[
(None, None, "https://api.tilebox.com"),
("", None, "https://api.tilebox.com"),
("https://runner.example.com", None, "https://runner.example.com"),
("https://runner.example.com", "https://explicit.example.com", "https://explicit.example.com"),
("https://runner.example.com", "https://api.tilebox.com", "https://api.tilebox.com"),
],
)
def test_client_url_environment(
monkeypatch: pytest.MonkeyPatch,
environment_url: str | None,
explicit_url: str | None,
expected_url: str,
) -> None:
monkeypatch.delenv("TILEBOX_API_URL", raising=False)
if environment_url is not None:
monkeypatch.setenv("TILEBOX_API_URL", environment_url)
monkeypatch.setenv("TILEBOX_API_KEY", "runner-key")
with (
patch("tilebox.workflows.client.open_channel") as open_channel_mock,
patch("tilebox.workflows.client._create_tilebox_logger_provider") as logger_provider_mock,
patch("tilebox.workflows.client.WorkflowTracer") as tracer_mock,
):
client = Client() if explicit_url is None else Client(url=explicit_url)
open_channel_mock.assert_called_once_with(expected_url, "runner-key")
logger_provider_mock.assert_called_once_with(service=None, url=expected_url, token="runner-key") # noqa: S106
tracer_mock.assert_called_once_with(service=None, url=expected_url, token="runner-key") # noqa: S106
assert client._auth == {"url": expected_url, "token": "runner-key"}
7 changes: 5 additions & 2 deletions tilebox-workflows/tilebox/workflows/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class Client:
def __init__(
self,
*,
url: str = "https://api.tilebox.com",
url: str | None = None,
token: str | None = None,
name: str | None = None,
client_id: UUID | None = None,
Expand All @@ -43,7 +43,8 @@ def __init__(
Create a Tilebox workflows client.

Args:
url: Tilebox API Url. Defaults to "https://api.tilebox.com".
url: Tilebox API URL. If not set, uses the `TILEBOX_API_URL` environment variable,
or defaulting to "https://api.tilebox.com".
token: The API Key to authenticate with. If not set the `TILEBOX_API_KEY` environment variable will be used.
name: An optional name of the client, used as service.name for telemetry. If not set, defaults to
the service name provided by `tilebox.workflows.observability.tracing.configure_otel_tracing`,
Expand All @@ -52,6 +53,8 @@ def __init__(
transport: Network transport to use for API requests. Defaults to "grpc". Use "http1" to force
the Connect protocol over HTTP/1.1 for networks that do not support gRPC over HTTP/2 correctly.
"""
if url is None:
url = os.environ.get("TILEBOX_API_URL") or "https://api.tilebox.com"
token = _token_from_env(url, token)
self._auth: dict[str, str] = {"token": token, "url": url}
match transport:
Expand Down
8 changes: 4 additions & 4 deletions tools/generate_protobuf.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
uv run generate-protobuf <path-to-tilebox-python-repo>
"""

import os
import subprocess
import sys
from pathlib import Path

Expand All @@ -26,9 +26,9 @@ def main() -> None:
# The Buf templates deliberately exclude buf.validate: these clients do not perform
# client-side validation, and validation-blind messages avoid global descriptor conflicts.
print("Running buf generate") # noqa: T201
os.system("buf generate --template buf.gen.datasets.yaml") # noqa: S605, S607
os.system("buf generate --template buf.gen.datasets-bufpy.yaml") # noqa: S605, S607
os.system("buf generate --template buf.gen.workflows.yaml") # noqa: S605, S607
subprocess.run(["buf", "generate", "--template", "buf.gen.datasets.yaml"], check=True) # noqa: S607
subprocess.run(["buf", "generate", "--template", "buf.gen.datasets-bufpy.yaml"], check=True) # noqa: S607
subprocess.run(["buf", "generate", "--template", "buf.gen.workflows.yaml"], check=True) # noqa: S607

package_mapping = {
"from datasets.v1 import": "from tilebox.datasets.datasets.v1 import",
Expand Down
Loading
Loading