Skip to content

Commit 695fc46

Browse files
Fix token required check for api.tilebox.com
1 parent fec218d commit 695fc46

8 files changed

Lines changed: 82 additions & 47 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- `tilebox-grpc`: More robust parsing of GRPC channel URLs.
13+
1014
## [0.37.0] - 2025-06-06
1115

1216
### Changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ area_of_interest = shape({
7171
)
7272
s2a_l1c = sentinel2_msi.collection("S2A_S2MSI1C")
7373
results = s2a_l1c.query(
74-
temporal_extent=("2022-07-13", "2022-07-13T02:00"),
74+
temporal_extent=("2025-03-01", "2025-06-01"),
7575
spatial_extent=area_of_interest,
7676
show_progress=True
7777
)

tilebox-datasets/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ Query data:
6666
```python
6767
s2a_l1c = sentinel2_msi.collection("S2A_S2MSI1C")
6868
results = s2a_l1c.query(
69-
temporal_extent=("2017-01-01", "2023-01-01"),
69+
temporal_extent=("2025-03-01", "2025-06-01"),
7070
show_progress=True
7171
)
7272
print(f"Found {results.sizes['time']} datapoints") # Found 220542 datapoints
@@ -83,7 +83,7 @@ area_of_interest = shape({
8383
)
8484
s2a_l1c = sentinel2_msi.collection("S2A_S2MSI1C")
8585
results = s2a_l1c.query(
86-
temporal_extent=("2022-07-13", "2022-07-13T02:00"),
86+
temporal_extent=("2025-03-01", "2025-06-01"),
8787
spatial_extent=area_of_interest,
8888
show_progress=True
8989
)

tilebox-datasets/tilebox/datasets/client.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import os
22
import sys
3-
from urllib.parse import urlparse
43
from typing import Any, Protocol, TypeVar
54
from uuid import UUID
65

76
from loguru import logger
87
from promise import Promise
98

9+
from _tilebox.grpc.channel import parse_channel_info
1010
from tilebox.datasets.data.datasets import Dataset, DatasetGroup, ListDatasetsResponse
1111
from tilebox.datasets.data.uuid import as_uuid
1212
from tilebox.datasets.group import Group
@@ -67,13 +67,13 @@ def _dataset_by_id(self, dataset_id: str | UUID, dataset_type: type[T]) -> Promi
6767
def token_from_env(url: str, token: str | None) -> str | None:
6868
if token is None: # if no token is provided, try to get it from the environment
6969
token = os.environ.get("TILEBOX_API_KEY", None)
70-
from urllib.parse import urlparse
71-
parsed_url = urlparse(url)
72-
if parsed_url.hostname == "api.tilebox.com" and token is None:
70+
71+
if token is None and parse_channel_info(url).address == "api.tilebox.com":
7372
raise ValueError(
7473
"No API key provided and no TILEBOX_API_KEY environment variable set. Please specify an API key using "
7574
"the token argument. For example: `Client(token='YOUR_TILEBOX_API_KEY')`"
7675
)
76+
7777
return token
7878

7979

tilebox-grpc/_tilebox/grpc/aio/channel.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ def open_channel(url: str, auth_token: str | None = None) -> Channel:
3636
def _open_channel(channel_info: ChannelInfo, interceptors: Sequence[ClientInterceptor]) -> Channel:
3737
if channel_info.use_ssl:
3838
return secure_channel(
39-
channel_info.url_without_protocol, ssl_channel_credentials(), CHANNEL_OPTIONS, interceptors=interceptors
39+
channel_info.address, ssl_channel_credentials(), CHANNEL_OPTIONS, interceptors=interceptors
4040
)
41-
return insecure_channel(channel_info.url_without_protocol, CHANNEL_OPTIONS, interceptors=interceptors)
41+
return insecure_channel(channel_info.address, CHANNEL_OPTIONS, interceptors=interceptors)
4242

4343

4444
RequestType = TypeVar("RequestType")

tilebox-grpc/_tilebox/grpc/channel.py

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import re
33
from collections.abc import Callable
44
from dataclasses import dataclass
5+
from enum import Enum
56
from typing import TypeVar
67

78
from grpc import (
@@ -42,11 +43,21 @@
4243
("grpc.service_config", json.dumps(_SERVICE_CONFIG)),
4344
]
4445

46+
class ChannelProtocol(Enum):
47+
HTTPS = 1
48+
HTTP = 2
49+
UNIX = 3
50+
4551

4652
@dataclass
4753
class ChannelInfo:
48-
url_without_protocol: str
49-
use_ssl: bool
54+
address: str
55+
"""GRPC target address. For http(s) connections this is the host[:port] format without a scheme. For unix sockets
56+
this is the unix socket path including the `unix://` prefix for absolute paths or `unix:` for relative paths."""
57+
port: int
58+
"""Port number for http(s) connections. For unix sockets this is always 0."""
59+
protocol: ChannelProtocol
60+
"""The protocol to use for the channel."""
5061

5162

5263
def open_channel(url: str, auth_token: str | None = None) -> Channel:
@@ -69,14 +80,22 @@ def open_channel(url: str, auth_token: str | None = None) -> Channel:
6980

7081

7182
def _open_channel(channel_info: ChannelInfo) -> Channel:
72-
if channel_info.use_ssl:
73-
return secure_channel(
74-
channel_info.url_without_protocol,
75-
ssl_channel_credentials(),
76-
CHANNEL_OPTIONS,
77-
compression=Compression.Gzip,
78-
)
79-
return insecure_channel(channel_info.url_without_protocol, CHANNEL_OPTIONS, compression=Compression.NoCompression)
83+
match channel_info.protocol:
84+
case ChannelProtocol.HTTPS:
85+
return secure_channel(
86+
f"{channel_info.address}:{channel_info.port}",
87+
ssl_channel_credentials(),
88+
CHANNEL_OPTIONS,
89+
compression=Compression.Gzip,
90+
)
91+
case ChannelProtocol.HTTP:
92+
return insecure_channel(
93+
f"{channel_info.address}:{channel_info.port}", CHANNEL_OPTIONS, compression=Compression.NoCompression
94+
)
95+
case ChannelProtocol.UNIX:
96+
return insecure_channel(channel_info.address, CHANNEL_OPTIONS, compression=Compression.NoCompression)
97+
case _:
98+
raise ValueError(f"Unsupported channel protocol: {channel_info.protocol}")
8099

81100

82101
_URL_SCHEME = re.compile(r"^(https?://)?([^: ]+)(:\d+)?/?$")
@@ -98,27 +117,28 @@ def parse_channel_info(url: str) -> ChannelInfo:
98117
A ChannelInfo object that can be used to create a gRPC channel.
99118
"""
100119
# See https://github.com/grpc/grpc/blob/master/doc/naming.md
101-
if url.startswith("unix:"):
102-
return ChannelInfo(url, False)
120+
if url.startswith("unix:"): ## unix:///absolute/path or unix://path
121+
return ChannelInfo(url, 0, ChannelProtocol.UNIX)
103122

104123
# `urllib.parse.urlparse` behaves a bit weird with URLs that don't have a scheme but a port number, so regex it is
105124
if (match := _URL_SCHEME.match(url)) is None:
106125
raise ValueError(f"Invalid URL: {url}")
107126
scheme, netloc, port = match.groups()
108127
netloc = netloc.rstrip("/")
109-
use_ssl = True
128+
protocol = ChannelProtocol.HTTPS
110129

111130
if scheme == "http://": # explicitly set http -> require a port
112131
if port is None:
113132
raise ValueError("Explicit port required for insecure HTTP channel")
114-
use_ssl = False
133+
protocol = ChannelProtocol.HTTP
134+
135+
# no scheme, but a port that looks like a dev port -> insecure
136+
if scheme is None and port is not None and port != ":443":
137+
protocol = ChannelProtocol.HTTP
115138

116-
if scheme is None and port is not None: # no scheme, but a port that looks like a dev port -> insecure
117-
use_ssl = port == ":443"
139+
port_number = 443 if port is None else int(port.removeprefix(":"))
118140

119-
if use_ssl:
120-
return ChannelInfo(netloc + (port or ":443"), True)
121-
return ChannelInfo(netloc + port, False)
141+
return ChannelInfo(netloc, port_number, protocol)
122142

123143

124144
RequestType = TypeVar("RequestType")

tilebox-grpc/tests/test_channel.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from _tilebox.grpc.channel import (
66
CHANNEL_OPTIONS,
7+
ChannelProtocol,
78
open_channel,
89
parse_channel_info,
910
)
@@ -50,24 +51,26 @@ def test_open_authenticated_channel(open_func: MagicMock, intercept_func: MagicM
5051
)
5152
def test_parse_channel_info_secure(url: str) -> None:
5253
channel_info = parse_channel_info(url)
53-
assert channel_info.url_without_protocol == "api.tilebox.com:443"
54-
assert channel_info.use_ssl
54+
assert channel_info.address == "api.tilebox.com"
55+
assert channel_info.port == 443
56+
assert channel_info.protocol == ChannelProtocol.HTTPS
5557

5658

5759
@pytest.mark.parametrize(
5860
("url", "expected_url_without_protocol"),
5961
[
60-
("0.0.0.0:8083", "0.0.0.0:8083"),
61-
("http://0.0.0.0:8083", "0.0.0.0:8083"),
62-
("http://localhost:8083", "localhost:8083"),
63-
("localhost:8083", "localhost:8083"),
64-
("http://some.insecure.url:1234", "some.insecure.url:1234"),
62+
("0.0.0.0:8083", "0.0.0.0"), # noqa: S104
63+
("http://0.0.0.0:8083", "0.0.0.0"), # noqa: S104
64+
("http://localhost:8083", "localhost"),
65+
("localhost:8083", "localhost"),
66+
("http://some.insecure.url:8083", "some.insecure.url"),
6567
],
6668
)
6769
def test_parse_channel_info_insecure(url: str, expected_url_without_protocol: str) -> None:
6870
channel_info = parse_channel_info(url)
69-
assert channel_info.url_without_protocol == expected_url_without_protocol
70-
assert not channel_info.use_ssl
71+
assert channel_info.address == expected_url_without_protocol
72+
assert channel_info.port == 8083
73+
assert channel_info.protocol == ChannelProtocol.HTTP
7174

7275

7376
@pytest.mark.parametrize(
@@ -79,8 +82,9 @@ def test_parse_channel_info_insecure(url: str, expected_url_without_protocol: st
7982
)
8083
def test_parse_channel_info_unix(url: str) -> None:
8184
channel_info = parse_channel_info(url)
82-
assert channel_info.url_without_protocol == url
83-
assert not channel_info.use_ssl
85+
assert channel_info.address == url
86+
assert channel_info.port == 0
87+
assert channel_info.protocol == ChannelProtocol.UNIX
8488

8589

8690
def test_parse_channel_invalid() -> None:

tilebox-workflows/tilebox/workflows/client.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import logging
22
import os
33

4-
from _tilebox.grpc.channel import open_channel
4+
from _tilebox.grpc.channel import open_channel, parse_channel_info
55
from tilebox.datasets.sync.client import Client as DatasetsClient
66
from tilebox.workflows.automations.client import AutomationClient, AutomationService
77
from tilebox.workflows.cache import JobCache, NoCache
@@ -28,13 +28,7 @@ def __init__(self, *, url: str = "https://api.tilebox.com", token: str | None =
2828
url: Tilebox API Url. Defaults to "https://api.tilebox.com".
2929
token: The API Key to authenticate with. If not set the `TILEBOX_API_KEY` environment variable will be used.
3030
"""
31-
if token is None: # if no token is provided, try to get it from the environment
32-
token = os.environ.get("TILEBOX_API_KEY", None)
33-
if url == "https://api.tilebox.com" and token is None:
34-
raise ValueError(
35-
"No API key provided and no TILEBOX_API_KEY environment variable set. Please specify an API key using "
36-
"the token argument. For example: `Client(token='YOUR_TILEBOX_API_KEY')`"
37-
)
31+
token = _token_from_env(url, token)
3832
self._auth = {"token": token, "url": url}
3933
self._channel = open_channel(url, token)
4034

@@ -147,3 +141,16 @@ def automations(self) -> AutomationClient:
147141
A client for the automations service.
148142
"""
149143
return AutomationClient(AutomationService(self._channel))
144+
145+
146+
def _token_from_env(url: str, token: str | None) -> str | None:
147+
if token is None: # if no token is provided, try to get it from the environment
148+
token = os.environ.get("TILEBOX_API_KEY", None)
149+
150+
if token is None and parse_channel_info(url).address == "api.tilebox.com":
151+
raise ValueError(
152+
"No API key provided and no TILEBOX_API_KEY environment variable set. Please specify an API key using "
153+
"the token argument. For example: `Client(token='YOUR_TILEBOX_API_KEY')`"
154+
)
155+
156+
return token

0 commit comments

Comments
 (0)