Skip to content

Commit 4ed8f6e

Browse files
committed
feat: named environment option (production default, staging switch)
Adds environment="production"|"staging" to Client/AsyncClient with resolution precedence base_url > environment > TOPOLAB_BASE_URL > TOPOLAB_ENV > production. Production ships as the default; staging is one keyword away. +6 tests (30 pass).
1 parent f74fbc8 commit 4ed8f6e

4 files changed

Lines changed: 93 additions & 8 deletions

File tree

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,19 @@ from topolab import Client
5151
tl = Client(api_key=os.environ["TOPOLAB_API_KEY"])
5252
```
5353

54+
## Staging vs production
55+
56+
The client targets **production** (`https://api.topolab.nl`) by default. Point it
57+
at staging with the `environment` argument:
58+
59+
```python
60+
tl = Client(api_key="tlb_staging_...", environment="staging") # https://api-staging.topolab.nl
61+
```
62+
63+
Or set `TOPOLAB_ENV=staging` in the environment. An explicit `base_url=` always
64+
wins (for self-hosting or tests). Precedence: `base_url``environment`
65+
`TOPOLAB_BASE_URL``TOPOLAB_ENV` → production.
66+
5467
## What you can do
5568

5669
### Browse the catalog

src/topolab/async_client.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from .errors import ConfigurationError
66
from ._transport import Transport
77
from .models import DatasetSummary, DatasetPage
8-
from .client import DEFAULT_BASE_URL
8+
from .client import resolve_base_url
99

1010

1111
def _clean(d):
@@ -75,13 +75,14 @@ async def list(self, *, page=None, limit=None, search=None, theme=None,
7575

7676
class AsyncClient:
7777
def __init__(self, api_key: str | None = None, *, base_url: str | None = None,
78-
timeout: float = 60.0, max_retries: int = 3,
79-
proxy_url: str | None = None, user_agent: str | None = None):
78+
environment: str | None = None, timeout: float = 60.0,
79+
max_retries: int = 3, proxy_url: str | None = None,
80+
user_agent: str | None = None):
8081
key = api_key if api_key is not None else os.environ.get("TOPOLAB_API_KEY")
8182
if not key:
8283
raise ConfigurationError("No API key. Pass api_key= or set TOPOLAB_API_KEY.")
8384
self.api_key = key
84-
self.base_url = base_url or os.environ.get("TOPOLAB_BASE_URL") or DEFAULT_BASE_URL
85+
self.base_url = resolve_base_url(base_url, environment)
8586
self._t = Transport(api_key=key, base_url=self.base_url, timeout=timeout,
8687
max_retries=max_retries, proxy_url=proxy_url, user_agent=user_agent)
8788
self.datasets = AsyncDatasetsNamespace(self._t)

src/topolab/client.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,51 @@
66
from .dataset import Dataset
77
from .datasets import DatasetsNamespace
88

9-
DEFAULT_BASE_URL = "https://api.topolab.nl"
9+
# Named API environments. Production is the shipped default; staging is one
10+
# keyword away. Self-hosting / tests can still pass an explicit base_url.
11+
ENVIRONMENTS = {
12+
"production": "https://api.topolab.nl",
13+
"staging": "https://api-staging.topolab.nl",
14+
}
15+
DEFAULT_BASE_URL = ENVIRONMENTS["production"]
16+
17+
18+
def _environment_url(name: str) -> str:
19+
try:
20+
return ENVIRONMENTS[name.lower()]
21+
except KeyError:
22+
raise ConfigurationError(
23+
f"Unknown environment {name!r}. Use one of {sorted(ENVIRONMENTS)}."
24+
) from None
25+
26+
27+
def resolve_base_url(base_url: str | None, environment: str | None) -> str:
28+
"""Resolve the API base URL. Precedence (most specific first):
29+
explicit base_url > environment arg > TOPOLAB_BASE_URL > TOPOLAB_ENV > production.
30+
"""
31+
if base_url:
32+
return base_url.rstrip("/")
33+
if environment:
34+
return _environment_url(environment)
35+
env_base = os.environ.get("TOPOLAB_BASE_URL")
36+
if env_base:
37+
return env_base.rstrip("/")
38+
env_name = os.environ.get("TOPOLAB_ENV")
39+
if env_name:
40+
return _environment_url(env_name)
41+
return DEFAULT_BASE_URL
1042

1143

1244
class Client:
1345
def __init__(self, api_key: str | None = None, *, base_url: str | None = None,
14-
timeout: float = 60.0, max_retries: int = 3,
15-
proxy_url: str | None = None, user_agent: str | None = None):
46+
environment: str | None = None, timeout: float = 60.0,
47+
max_retries: int = 3, proxy_url: str | None = None,
48+
user_agent: str | None = None):
1649
key = api_key if api_key is not None else os.environ.get("TOPOLAB_API_KEY")
1750
if not key:
1851
raise ConfigurationError("No API key. Pass api_key= or set TOPOLAB_API_KEY.")
1952
self.api_key = key
20-
self.base_url = base_url or os.environ.get("TOPOLAB_BASE_URL") or DEFAULT_BASE_URL
53+
self.base_url = resolve_base_url(base_url, environment)
2154
self._t = Transport(api_key=key, base_url=self.base_url, timeout=timeout,
2255
max_retries=max_retries, proxy_url=proxy_url, user_agent=user_agent)
2356
self.datasets = DatasetsNamespace(self._t)

tests/test_client.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,41 @@ def test_missing_key_raises(monkeypatch):
2525
monkeypatch.delenv("TOPOLAB_API_KEY", raising=False)
2626
with pytest.raises(ConfigurationError):
2727
Client(api_key=None, base_url=BASE)
28+
29+
30+
def test_default_environment_is_production(monkeypatch):
31+
monkeypatch.delenv("TOPOLAB_BASE_URL", raising=False)
32+
monkeypatch.delenv("TOPOLAB_ENV", raising=False)
33+
assert Client(api_key="k").base_url == "https://api.topolab.nl"
34+
35+
36+
def test_environment_staging(monkeypatch):
37+
monkeypatch.delenv("TOPOLAB_BASE_URL", raising=False)
38+
monkeypatch.delenv("TOPOLAB_ENV", raising=False)
39+
assert Client(api_key="k", environment="staging").base_url == "https://api-staging.topolab.nl"
40+
41+
42+
def test_unknown_environment_raises():
43+
with pytest.raises(ConfigurationError):
44+
Client(api_key="k", environment="dev")
45+
46+
47+
def test_topolab_env_var(monkeypatch):
48+
monkeypatch.delenv("TOPOLAB_BASE_URL", raising=False)
49+
monkeypatch.setenv("TOPOLAB_ENV", "staging")
50+
assert Client(api_key="k").base_url == "https://api-staging.topolab.nl"
51+
52+
53+
def test_base_url_beats_environment(monkeypatch):
54+
monkeypatch.delenv("TOPOLAB_BASE_URL", raising=False)
55+
monkeypatch.delenv("TOPOLAB_ENV", raising=False)
56+
# explicit base_url wins over environment
57+
c = Client(api_key="k", base_url="https://self.example/api", environment="staging")
58+
assert c.base_url == "https://self.example/api"
59+
60+
61+
def test_async_environment_staging(monkeypatch):
62+
from topolab import AsyncClient
63+
monkeypatch.delenv("TOPOLAB_BASE_URL", raising=False)
64+
monkeypatch.delenv("TOPOLAB_ENV", raising=False)
65+
assert AsyncClient(api_key="k", environment="staging").base_url == "https://api-staging.topolab.nl"

0 commit comments

Comments
 (0)