Repository navigation
fix: wire the documented HOTDATA_* env vars into the destination #96
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,17 @@ Changelog style — terse "bullet + brief why": | |
| **Breaking:**. See the released entries below for the target density. | ||
| --> | ||
|
|
||
| ### Fixed | ||
|
|
||
| - The documented `HOTDATA_*` env vars now reach the destination (previously only | ||
| `HOTDATA_API_KEY` did); explicit params still win, and the plain names take | ||
| precedence over `DESTINATION__HOTDATA__*` / `config.toml` (#95). | ||
|
|
||
| ### Changed | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. super nit: move The style note at |
||
|
|
||
| - Diagnostic CLI retry defaults aligned with the destination's: `max_retries` 8 | ||
| (was 5), `retry_backoff_seconds` 1.5 (was 1.0). | ||
|
|
||
| ## [0.16.0] - 2026-09-01 | ||
|
|
||
| ### Added | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||||||||||||||||
| from dataclasses import dataclass | ||||||||||||||||||||||||||||||||||||||
| from typing import Any | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def _parse_max_retries(value: str) -> int: | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -24,6 +25,46 @@ def _parse_backoff(value: str) -> float: | |||||||||||||||||||||||||||||||||||||
| return n | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def plain_env_overrides() -> dict[str, Any]: | ||||||||||||||||||||||||||||||||||||||
| """Settings read from the plain ``HOTDATA_*`` environment variables. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| This is the documented environment contract, shared by the destination | ||||||||||||||||||||||||||||||||||||||
| factory and the diagnostic CLI. Only variables that are set — and, for the | ||||||||||||||||||||||||||||||||||||||
| string-valued ones, non-empty — appear in the result, so an absent key | ||||||||||||||||||||||||||||||||||||||
| means "not configured this way" and the caller's default stands. | ||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||
| overrides: dict[str, Any] = {} | ||||||||||||||||||||||||||||||||||||||
| for key, name in ( | ||||||||||||||||||||||||||||||||||||||
| ("database_id", "HOTDATA_DATABASE_ID"), | ||||||||||||||||||||||||||||||||||||||
| ("database_name", "HOTDATA_DATABASE"), | ||||||||||||||||||||||||||||||||||||||
| ("schema", "HOTDATA_SCHEMA"), | ||||||||||||||||||||||||||||||||||||||
| ("write_disposition", "HOTDATA_WRITE_DISPOSITION"), | ||||||||||||||||||||||||||||||||||||||
| ("api_base_url", "HOTDATA_API_BASE_URL"), | ||||||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||||||
| value = os.environ.get(name) | ||||||||||||||||||||||||||||||||||||||
| if value: | ||||||||||||||||||||||||||||||||||||||
| overrides[key] = value | ||||||||||||||||||||||||||||||||||||||
| declared = os.environ.get("HOTDATA_DECLARED_TABLES") | ||||||||||||||||||||||||||||||||||||||
| if declared is not None: | ||||||||||||||||||||||||||||||||||||||
| tables = [table.strip() for table in declared.split(",") if table.strip()] | ||||||||||||||||||||||||||||||||||||||
| if tables: | ||||||||||||||||||||||||||||||||||||||
| overrides["declared_tables"] = tables | ||||||||||||||||||||||||||||||||||||||
| # Truthiness, not `is not None`: a set-but-blank variable means unset here, | ||||||||||||||||||||||||||||||||||||||
| # the same convention the string settings above follow and .env.example ships | ||||||||||||||||||||||||||||||||||||||
| # (`HOTDATA_DATABASE_ID=`). An `is not None` check would raise out of the | ||||||||||||||||||||||||||||||||||||||
| # hotdata(...) constructor — or silently flip the create flag — on a blank. | ||||||||||||||||||||||||||||||||||||||
| flag = os.environ.get("HOTDATA_CREATE_DATABASE_IF_MISSING") | ||||||||||||||||||||||||||||||||||||||
| if flag: | ||||||||||||||||||||||||||||||||||||||
| overrides["create_database_if_missing"] = flag.lower() in {"1", "true", "yes"} | ||||||||||||||||||||||||||||||||||||||
| retries = os.environ.get("HOTDATA_MAX_RETRIES") | ||||||||||||||||||||||||||||||||||||||
| if retries: | ||||||||||||||||||||||||||||||||||||||
| overrides["max_retries"] = _parse_max_retries(retries) | ||||||||||||||||||||||||||||||||||||||
| backoff = os.environ.get("HOTDATA_RETRY_BACKOFF_SECONDS") | ||||||||||||||||||||||||||||||||||||||
| if backoff: | ||||||||||||||||||||||||||||||||||||||
| overrides["retry_backoff_seconds"] = _parse_backoff(backoff) | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+56
to
+64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A set-but-empty
The string loop above already uses truthiness, so a blank Fix: treat blank as unset for these three settings too.
Suggested change
Please also add a test that sets these three names to the empty string and asserts the defaults survive.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 9b0f728 — the three non-string settings now use truthiness like the string loop, with a comment naming the .env.example convention; added test_blank_plain_env_vars_are_treated_as_unset covering all three. Also took the nit: .env.example retry values now match the destination defaults (8 / 1.5). |
||||||||||||||||||||||||||||||||||||||
| return overrides | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| @dataclass(frozen=True) | ||||||||||||||||||||||||||||||||||||||
| class HotdataDestinationConfig: | ||||||||||||||||||||||||||||||||||||||
| api_key: str | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -34,27 +75,16 @@ class HotdataDestinationConfig: | |||||||||||||||||||||||||||||||||||||
| write_disposition: str = "append" | ||||||||||||||||||||||||||||||||||||||
| create_database_if_missing: bool = True | ||||||||||||||||||||||||||||||||||||||
| declared_tables: tuple[str, ...] = () | ||||||||||||||||||||||||||||||||||||||
| max_retries: int = 5 | ||||||||||||||||||||||||||||||||||||||
| retry_backoff_seconds: float = 1.0 | ||||||||||||||||||||||||||||||||||||||
| max_retries: int = 8 | ||||||||||||||||||||||||||||||||||||||
| retry_backoff_seconds: float = 1.5 | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| @classmethod | ||||||||||||||||||||||||||||||||||||||
| def from_env(cls) -> HotdataDestinationConfig: | ||||||||||||||||||||||||||||||||||||||
| declared = os.environ.get("HOTDATA_DECLARED_TABLES", "") | ||||||||||||||||||||||||||||||||||||||
| declared_tables = tuple(table.strip() for table in declared.split(",") if table.strip()) | ||||||||||||||||||||||||||||||||||||||
| overrides = plain_env_overrides() | ||||||||||||||||||||||||||||||||||||||
| if "declared_tables" in overrides: | ||||||||||||||||||||||||||||||||||||||
| overrides["declared_tables"] = tuple(overrides["declared_tables"]) | ||||||||||||||||||||||||||||||||||||||
| return cls( | ||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: add a test for
|
||||||||||||||||||||||||||||||||||||||
| api_key=os.environ["HOTDATA_API_KEY"], | ||||||||||||||||||||||||||||||||||||||
| database_name=os.environ.get("HOTDATA_DATABASE", "dlt"), | ||||||||||||||||||||||||||||||||||||||
| database_id=os.environ.get("HOTDATA_DATABASE_ID") or None, | ||||||||||||||||||||||||||||||||||||||
| api_base_url=os.environ.get("HOTDATA_API_BASE_URL", "https://api.hotdata.dev"), | ||||||||||||||||||||||||||||||||||||||
| schema=os.environ.get("HOTDATA_SCHEMA", "public"), | ||||||||||||||||||||||||||||||||||||||
| write_disposition=os.environ.get("HOTDATA_WRITE_DISPOSITION", "append"), | ||||||||||||||||||||||||||||||||||||||
| create_database_if_missing=os.environ.get( | ||||||||||||||||||||||||||||||||||||||
| "HOTDATA_CREATE_DATABASE_IF_MISSING", "true" | ||||||||||||||||||||||||||||||||||||||
| ).lower() | ||||||||||||||||||||||||||||||||||||||
| in {"1", "true", "yes"}, | ||||||||||||||||||||||||||||||||||||||
| declared_tables=declared_tables, | ||||||||||||||||||||||||||||||||||||||
| max_retries=_parse_max_retries(os.environ.get("HOTDATA_MAX_RETRIES", "5")), | ||||||||||||||||||||||||||||||||||||||
| retry_backoff_seconds=_parse_backoff( | ||||||||||||||||||||||||||||||||||||||
| os.environ.get("HOTDATA_RETRY_BACKOFF_SECONDS", "1.0") | ||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||
| database_name=overrides.pop("database_name", "dlt"), | ||||||||||||||||||||||||||||||||||||||
| **overrides, | ||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: blank the illustrative values in
.env.example, or comment them out. (not blocking)docs/runbook.md:19tells a user to copy.env.exampleto.envfor a shell loader. After this PR that copy declarescustomersandorderson every pipeline, andinitialize_storagecreates a managed table per declared name. The remaining sample values match the destination defaults, but now outrankconfig.toml, so aconfig.tomlschemais silently replaced bypublic.