hotdata-framework 0.6.1__tar.gz → 0.6.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/CHANGELOG.md +13 -1
  2. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/PKG-INFO +1 -1
  3. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/hotdata_framework/client.py +43 -0
  4. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/hotdata_framework/managed_client.py +2 -0
  5. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/pyproject.toml +1 -1
  6. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/tests/test_errors.py +1 -2
  7. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/tests/test_managed_client.py +1 -2
  8. hotdata_framework-0.6.3/tests/test_request_timeout.py +69 -0
  9. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/uv.lock +1 -1
  10. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/.github/dependabot.yml +0 -0
  11. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/.github/workflows/check-release.yml +0 -0
  12. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/.github/workflows/ci.yml +0 -0
  13. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/.github/workflows/dependabot-automerge.yml +0 -0
  14. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/.github/workflows/publish.yml +0 -0
  15. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/.github/workflows/release.yml +0 -0
  16. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/.gitignore +0 -0
  17. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/CONTRACT.md +0 -0
  18. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/README.md +0 -0
  19. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/RELEASING.md +0 -0
  20. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/examples/basic_usage.py +0 -0
  21. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/hotdata_framework/__init__.py +0 -0
  22. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/hotdata_framework/databases.py +0 -0
  23. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/hotdata_framework/env.py +0 -0
  24. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/hotdata_framework/errors.py +0 -0
  25. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/hotdata_framework/health.py +0 -0
  26. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/hotdata_framework/http.py +0 -0
  27. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/hotdata_framework/py.typed +0 -0
  28. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/hotdata_framework/result.py +0 -0
  29. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/scripts/check-release.py +0 -0
  30. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/scripts/extract-changelog.py +0 -0
  31. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/scripts/publish-workflow.sh +0 -0
  32. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/scripts/release.sh +0 -0
  33. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/scripts/update_changelog.py +0 -0
  34. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/tests/test_client.py +0 -0
  35. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/tests/test_contract.py +0 -0
  36. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/tests/test_databases.py +0 -0
  37. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/tests/test_health.py +0 -0
  38. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/tests/test_result.py +0 -0
  39. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/tests/test_update_changelog.py +0 -0
  40. {hotdata_framework-0.6.1 → hotdata_framework-0.6.3}/tests/test_version.py +0 -0
@@ -8,11 +8,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
  ## [Unreleased]
9
9
 
10
10
 
11
+ ## [0.6.3] - 2026-07-08
12
+
13
+ ### Added
14
+
15
+ - `HotdataClient` and `ManagedDatabaseClient` accept `request_timeout` (seconds, or a `(connect, read)` pair). The generated SDK otherwise issues every HTTP request with urllib3's no-timeout default, so a stalled or unreachable server blocks the calling thread indefinitely; the new parameter applies a socket-level deadline to every call through the client while still honoring an explicit per-call `_request_timeout`. Also exported as `apply_default_request_timeout(api_client, timeout)` for callers holding a raw generated client. Default remains no timeout (behavior unchanged unless opted in).
16
+
17
+ ## [0.6.2] - 2026-07-08
18
+
19
+ ### Changed
20
+
21
+ - Repository text cleanup: the changelog and test docstrings no longer reference external issue trackers. No functional changes; 0.6.2 is byte-identical to 0.6.1 in package code.
22
+
11
23
  ## [0.6.1] - 2026-07-08
12
24
 
13
25
  ### Fixed
14
26
 
15
- - `ManagedDatabaseClient.fetch_table` now carries the `X-Database-Id` scope header on the result poll, the query-run poll, and the Arrow fetch — not only on the query submit. Results of database-scoped queries are themselves database-scoped, so every read against an existing synced table (merge/append loads, dlt state restore) failed with `400: Bad Request` once the table had data (dlthubworker#70).
27
+ - `ManagedDatabaseClient.fetch_table` now carries the `X-Database-Id` scope header on the result poll, the query-run poll, and the Arrow fetch — not only on the query submit. Results of database-scoped queries are themselves database-scoped, so every read against an existing synced table (merge/append loads, dlt state restore) failed with `400: Bad Request` once the table had data.
16
28
  - API error messages now include the response body (flattened, truncated to 500 chars). `400: Bad Request` alone hid the server's actual explanation.
17
29
 
18
30
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hotdata-framework
3
- Version: 0.6.1
3
+ Version: 0.6.3
4
4
  Summary: Python framework for building Hotdata integrations: workspace/session runtime, query execution, and managed databases
5
5
  Project-URL: Homepage, https://www.hotdata.dev
6
6
  Project-URL: Documentation, https://www.hotdata.dev/docs
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import functools
3
4
  import time
4
5
  from collections.abc import Iterator
5
6
  from dataclasses import asdict, dataclass
@@ -71,6 +72,45 @@ class RunHistoryItem:
71
72
  return asdict(self)
72
73
 
73
74
 
75
+
76
+ def apply_default_request_timeout(
77
+ api_client: ApiClient, timeout: float | tuple[float, float]
78
+ ) -> None:
79
+ """Give every request through this client a socket-level deadline.
80
+
81
+ The generated client forwards ``_request_timeout=None`` — urllib3's
82
+ no-timeout — on every call unless the caller passes one explicitly, and
83
+ most helper methods expose no such knob. A stalled or black-holed server
84
+ therefore blocks the calling thread indefinitely. Wrapping the REST seam
85
+ applies ``timeout`` (seconds, or a ``(connect, read)`` pair) as the
86
+ default while still honoring an explicit per-call ``_request_timeout``.
87
+ """
88
+ rest_client = api_client.rest_client
89
+ original = rest_client.request
90
+
91
+ @functools.wraps(original)
92
+ def request_with_default_timeout(
93
+ method,
94
+ url,
95
+ headers=None,
96
+ body=None,
97
+ post_params=None,
98
+ _request_timeout=None,
99
+ ):
100
+ if _request_timeout is None:
101
+ _request_timeout = timeout
102
+ return original(
103
+ method,
104
+ url,
105
+ headers=headers,
106
+ body=body,
107
+ post_params=post_params,
108
+ _request_timeout=_request_timeout,
109
+ )
110
+
111
+ rest_client.request = request_with_default_timeout
112
+
113
+
74
114
  class HotdataClient:
75
115
  """Thin wrapper around the Hotdata Python SDK with query polling helpers."""
76
116
 
@@ -81,6 +121,7 @@ class HotdataClient:
81
121
  *,
82
122
  host: str | None = None,
83
123
  session_id: str | None = None,
124
+ request_timeout: float | tuple[float, float] | None = None,
84
125
  ) -> None:
85
126
  self._host = normalize_host(host) if host else default_host()
86
127
  self._api_key = api_key
@@ -94,6 +135,8 @@ class HotdataClient:
94
135
  retries=default_http_retries(),
95
136
  )
96
137
  self._api = ApiClient(self._config)
138
+ if request_timeout is not None:
139
+ apply_default_request_timeout(self._api, request_timeout)
97
140
 
98
141
  @classmethod
99
142
  def from_env(cls) -> HotdataClient:
@@ -61,6 +61,7 @@ class ManagedDatabaseClient:
61
61
  api_base_url: str,
62
62
  max_retries: int,
63
63
  retry_backoff_seconds: float,
64
+ request_timeout: float | tuple[float, float] | None = None,
64
65
  ) -> None:
65
66
  self._max_retries = max_retries
66
67
  self._retry_backoff_seconds = retry_backoff_seconds
@@ -68,6 +69,7 @@ class ManagedDatabaseClient:
68
69
  api_key,
69
70
  workspace_id,
70
71
  host=api_base_url.rstrip("/"),
72
+ request_timeout=request_timeout,
71
73
  )
72
74
 
73
75
  def close(self) -> None:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hotdata-framework"
7
- version = "0.6.1"
7
+ version = "0.6.3"
8
8
  description = "Python framework for building Hotdata integrations: workspace/session runtime, query execution, and managed databases"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,8 +1,7 @@
1
1
  """Error message construction: the API's response body must survive.
2
2
 
3
3
  "400: Bad Request" alone is undebuggable; the body carries the server's
4
- actual explanation (e.g. which header was missing). Regression for the
5
- opaque load failures in dlthubworker#70.
4
+ actual explanation (e.g. which header was missing).
6
5
  """
7
6
 
8
7
  from __future__ import annotations
@@ -107,8 +107,7 @@ def test_fetch_table_carries_database_scope_on_result_reads(
107
107
  hotdata 0.6.0 SDK exposes ``x_database_id`` on all three.
108
108
 
109
109
  Regression: reruns/append loads against an existing synced table failed
110
- with an opaque ``400: Bad Request`` (dlthubworker#70) because both reads
111
- omitted the scope.
110
+ with an opaque ``400: Bad Request`` because both reads omitted the scope.
112
111
  """
113
112
  result_scopes: list[str | None] = []
114
113
  arrow_scopes: list[str | None] = []
@@ -0,0 +1,69 @@
1
+ """The default request timeout must reach every call through the client.
2
+
3
+ The generated client sends ``_request_timeout=None`` (urllib3 no-timeout)
4
+ unless a caller passes one per call — and the helper methods expose no such
5
+ knob — so a stalled server blocks the calling thread indefinitely.
6
+ ``apply_default_request_timeout`` wraps the REST seam once at construction.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from hotdata_framework.client import HotdataClient, apply_default_request_timeout
12
+ from hotdata_framework.managed_client import ManagedDatabaseClient
13
+
14
+
15
+ class _FakeRest:
16
+ def __init__(self) -> None:
17
+ self.seen: list[object] = []
18
+
19
+ def request(self, method, url, headers=None, body=None, post_params=None, _request_timeout=None):
20
+ self.seen.append(_request_timeout)
21
+ return "resp"
22
+
23
+
24
+ class _FakeApi:
25
+ def __init__(self) -> None:
26
+ self.rest_client = _FakeRest()
27
+
28
+
29
+ def test_default_applied_when_no_per_call_timeout() -> None:
30
+ api = _FakeApi()
31
+ rest = api.rest_client
32
+ apply_default_request_timeout(api, (10.0, 60.0))
33
+
34
+ assert api.rest_client.request("GET", "http://x") == "resp"
35
+ assert rest.seen == [(10.0, 60.0)]
36
+
37
+
38
+ def test_explicit_per_call_timeout_wins() -> None:
39
+ api = _FakeApi()
40
+ rest = api.rest_client
41
+ apply_default_request_timeout(api, 60.0)
42
+
43
+ api.rest_client.request("GET", "http://x", _request_timeout=5.0)
44
+ assert rest.seen == [5.0]
45
+
46
+
47
+ def test_hotdata_client_wraps_its_rest_seam() -> None:
48
+ client = HotdataClient("k", "w", host="https://example.test", request_timeout=7.0)
49
+ # The wrapper is installed in place of the generated bound method.
50
+ assert client.api.rest_client.request.__name__ == "request"
51
+ assert client.api.rest_client.request.__wrapped__ is not None
52
+
53
+
54
+ def test_hotdata_client_without_timeout_is_untouched() -> None:
55
+ client = HotdataClient("k", "w", host="https://example.test")
56
+ assert not hasattr(client.api.rest_client.request, "__wrapped__")
57
+
58
+
59
+ def test_managed_client_threads_timeout_through() -> None:
60
+ client = ManagedDatabaseClient(
61
+ api_key="k",
62
+ workspace_id="w",
63
+ api_base_url="https://example.test",
64
+ max_retries=1,
65
+ retry_backoff_seconds=0.0,
66
+ request_timeout=(5.0, 30.0),
67
+ )
68
+ assert client._runtime.api.rest_client.request.__wrapped__ is not None
69
+ client.close()
@@ -101,7 +101,7 @@ wheels = [
101
101
 
102
102
  [[package]]
103
103
  name = "hotdata-framework"
104
- version = "0.6.1"
104
+ version = "0.6.3"
105
105
  source = { editable = "." }
106
106
  dependencies = [
107
107
  { name = "hotdata" },