Skip to content

Commit db85746

Browse files
committed
feat(config): default api_key, workspace_id and host from the environment
Configuration() now reads HOTDATA_API_KEY, HOTDATA_WORKSPACE and HOTDATA_API_URL when the matching argument is None, the same variables the CLI honours. Explicit arguments win; an empty variable is unset. The change lives in the mustache template the generator uses for configuration.py, so a regen keeps it.
1 parent e2ae326 commit db85746

5 files changed

Lines changed: 143 additions & 5 deletions

File tree

‎.openapi-generator-templates/configuration.mustache‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import logging
99
from logging import FileHandler
1010
{{^async}}
1111
import multiprocessing
12+
import os
1213
{{/async}}
1314
import sys
1415
from importlib.metadata import PackageNotFoundError, version as _pkg_version
@@ -156,13 +157,16 @@ class HostSetting(TypedDict):
156157
class Configuration:
157158
"""This class contains various settings of the API client.
158159

159-
:param host: Base url.
160+
:param host: Base url. Defaults to the `HOTDATA_API_URL` environment
161+
variable when set, else `https://api.hotdata.dev`.
160162
:param ignore_operation_servers
161163
Boolean to ignore operation servers for the API client.
162164
Config will use `host` as the base url regardless of the operation servers.
163165
:param api_key: Hotdata API key, sent as `Authorization: Bearer <key>`.
166+
Defaults to the `HOTDATA_API_KEY` environment variable when not given.
164167
:param workspace_id: Public id of the target workspace, sent as
165-
`X-Workspace-Id`.
168+
`X-Workspace-Id`. Defaults to the `HOTDATA_WORKSPACE` environment
169+
variable when not given.
166170
:param api_keys: Escape hatch for raw apiKey-scheme values, keyed by
167171
security scheme name. Prefer `workspace_id`.
168172
:param api_key_prefix: Dict to store API prefix (e.g. Bearer).
@@ -299,6 +303,14 @@ conf = {{packageName}}.Configuration(
299303
) -> None:
300304
"""Constructor
301305
"""
306+
# Environment defaults, matching the Hotdata CLI: an explicit argument
307+
# always wins; an unset or empty variable leaves the default in place.
308+
if api_key is None:
309+
api_key = os.environ.get("HOTDATA_API_KEY") or None
310+
if workspace_id is None:
311+
workspace_id = os.environ.get("HOTDATA_WORKSPACE") or None
312+
if host is None:
313+
host = os.environ.get("HOTDATA_API_URL") or None
302314
self._base_path = "{{basePath}}" if host is None else host
303315
"""Default Base url
304316
"""

‎CHANGELOG.md‎

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

1010
### Added
1111

12+
- `hotdata.Configuration` reads `HOTDATA_API_KEY`, `HOTDATA_WORKSPACE` and
13+
`HOTDATA_API_URL` from the environment when `api_key`, `workspace_id` or
14+
`host` is not passed, matching the CLI. Explicit arguments always win and an
15+
empty variable counts as unset.
1216
- `DatabasesApi.load_database_table` and `ConnectionsApi.load_managed_table`
1317
(the classes exported from `hotdata`) retry automatically while another load
1418
holds the table: HTTP 409 with error code `RESOURCE_LOCKED` is retried,

‎README.md‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,13 @@ configuration = hotdata.Configuration(
3737
)
3838
```
3939

40-
`host` defaults to `https://api.hotdata.dev`. Override it if you target another environment.
40+
Each of these can come from the environment instead, the same variables the
41+
CLI uses: `HOTDATA_API_KEY`, `HOTDATA_WORKSPACE` and `HOTDATA_API_URL` fill in
42+
`api_key`, `workspace_id` and `host` when the argument is not passed. An
43+
explicit argument always wins.
44+
45+
`host` defaults to `https://api.hotdata.dev`. Override it (or set
46+
`HOTDATA_API_URL`) if you target another environment.
4147

4248
## Usage
4349

‎hotdata/configuration.py‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import logging
1717
from logging import FileHandler
1818
import multiprocessing
19+
import os
1920
import sys
2021
from importlib.metadata import PackageNotFoundError, version as _pkg_version
2122
from typing import Any, ClassVar, Dict, List, Literal, Optional, TypedDict, Union
@@ -135,13 +136,16 @@ class HostSetting(TypedDict):
135136
class Configuration:
136137
"""This class contains various settings of the API client.
137138
138-
:param host: Base url.
139+
:param host: Base url. Defaults to the `HOTDATA_API_URL` environment
140+
variable when set, else `https://api.hotdata.dev`.
139141
:param ignore_operation_servers
140142
Boolean to ignore operation servers for the API client.
141143
Config will use `host` as the base url regardless of the operation servers.
142144
:param api_key: Hotdata API key, sent as `Authorization: Bearer <key>`.
145+
Defaults to the `HOTDATA_API_KEY` environment variable when not given.
143146
:param workspace_id: Public id of the target workspace, sent as
144-
`X-Workspace-Id`.
147+
`X-Workspace-Id`. Defaults to the `HOTDATA_WORKSPACE` environment
148+
variable when not given.
145149
:param api_keys: Escape hatch for raw apiKey-scheme values, keyed by
146150
security scheme name. Prefer `workspace_id`.
147151
:param api_key_prefix: Dict to store API prefix (e.g. Bearer).
@@ -203,6 +207,14 @@ def __init__(
203207
) -> None:
204208
"""Constructor
205209
"""
210+
# Environment defaults, matching the Hotdata CLI: an explicit argument
211+
# always wins; an unset or empty variable leaves the default in place.
212+
if api_key is None:
213+
api_key = os.environ.get("HOTDATA_API_KEY") or None
214+
if workspace_id is None:
215+
workspace_id = os.environ.get("HOTDATA_WORKSPACE") or None
216+
if host is None:
217+
host = os.environ.get("HOTDATA_API_URL") or None
206218
self._base_path = "https://api.hotdata.dev" if host is None else host
207219
"""Default Base url
208220
"""

‎tests/test_config_env.py‎

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""`Configuration` reads credentials and host from the environment.
2+
3+
The CLI already honours `HOTDATA_API_KEY`, `HOTDATA_WORKSPACE` and
4+
`HOTDATA_API_URL`; the SDK now does the same when the matching constructor
5+
argument is `None`. Explicit arguments always win, and an empty variable
6+
counts as unset. `Configuration` is generated from the in-repo mustache
7+
template, so the template is checked for the same wiring.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
from pathlib import Path
13+
14+
import pytest
15+
16+
from hotdata import ApiClient, Configuration
17+
from hotdata.api.workspaces_api import WorkspacesApi
18+
19+
REPO_ROOT = Path(__file__).resolve().parents[1]
20+
TEMPLATE = REPO_ROOT / ".openapi-generator-templates" / "configuration.mustache"
21+
22+
ENV_VARS = ("HOTDATA_API_KEY", "HOTDATA_WORKSPACE", "HOTDATA_API_URL")
23+
24+
25+
@pytest.fixture(autouse=True)
26+
def _clean_env(monkeypatch: pytest.MonkeyPatch) -> None:
27+
for name in ENV_VARS:
28+
monkeypatch.delenv(name, raising=False)
29+
30+
31+
def test_defaults_without_env() -> None:
32+
cfg = Configuration()
33+
assert cfg.api_key is None
34+
assert cfg.workspace_id is None
35+
assert cfg.host == "https://api.hotdata.dev"
36+
37+
38+
def test_env_fills_in_missing_args(monkeypatch: pytest.MonkeyPatch) -> None:
39+
monkeypatch.setenv("HOTDATA_API_KEY", "hd_env_key")
40+
monkeypatch.setenv("HOTDATA_WORKSPACE", "ws_env")
41+
monkeypatch.setenv("HOTDATA_API_URL", "https://api.example.test")
42+
cfg = Configuration()
43+
assert cfg.api_key == "hd_env_key"
44+
assert cfg.workspace_id == "ws_env"
45+
assert cfg.host == "https://api.example.test"
46+
# The env-provided values reach the wire exactly like explicit ones.
47+
auth = cfg.auth_settings()
48+
assert auth["BearerAuth"]["value"] == "Bearer hd_env_key"
49+
assert auth["WorkspaceId"]["value"] == "ws_env"
50+
51+
52+
def test_explicit_args_win_over_env(monkeypatch: pytest.MonkeyPatch) -> None:
53+
monkeypatch.setenv("HOTDATA_API_KEY", "hd_env_key")
54+
monkeypatch.setenv("HOTDATA_WORKSPACE", "ws_env")
55+
monkeypatch.setenv("HOTDATA_API_URL", "https://env.example.test")
56+
cfg = Configuration(
57+
api_key="hd_arg_key", workspace_id="ws_arg", host="https://arg.example.test"
58+
)
59+
assert cfg.api_key == "hd_arg_key"
60+
assert cfg.workspace_id == "ws_arg"
61+
assert cfg.host == "https://arg.example.test"
62+
63+
64+
def test_empty_env_counts_as_unset(monkeypatch: pytest.MonkeyPatch) -> None:
65+
for name in ENV_VARS:
66+
monkeypatch.setenv(name, "")
67+
cfg = Configuration()
68+
assert cfg.api_key is None
69+
assert cfg.workspace_id is None
70+
assert cfg.host == "https://api.hotdata.dev"
71+
72+
73+
def test_env_host_is_used_for_requests(monkeypatch: pytest.MonkeyPatch) -> None:
74+
monkeypatch.setenv("HOTDATA_API_URL", "https://env.example.test")
75+
monkeypatch.setenv("HOTDATA_API_KEY", "hd_env_key")
76+
client = ApiClient(Configuration())
77+
method, url, headers, _body, _post = WorkspacesApi(client)._list_workspaces_serialize(
78+
organization_public_id=None,
79+
_request_auth=None,
80+
_content_type=None,
81+
_headers=None,
82+
_host_index=0,
83+
)
84+
assert method == "GET"
85+
assert url.startswith("https://env.example.test/v1/workspaces")
86+
assert headers["Authorization"] == "Bearer hd_env_key"
87+
88+
89+
def test_env_is_read_per_instance(monkeypatch: pytest.MonkeyPatch) -> None:
90+
# Resolved at construction, not at import: a later change to the
91+
# environment affects only configurations built after it.
92+
monkeypatch.setenv("HOTDATA_API_KEY", "first")
93+
first = Configuration()
94+
monkeypatch.setenv("HOTDATA_API_KEY", "second")
95+
second = Configuration()
96+
assert (first.api_key, second.api_key) == ("first", "second")
97+
98+
99+
def test_template_carries_the_same_env_defaults() -> None:
100+
# configuration.py is regenerated from this template; the env wiring must
101+
# live there too or the next regen silently drops it.
102+
src = TEMPLATE.read_text()
103+
for name in ENV_VARS:
104+
assert f'os.environ.get("{name}")' in src

0 commit comments

Comments
 (0)