hotdata-framework 0.6.2__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.2 → hotdata_framework-0.6.3}/CHANGELOG.md +6 -0
  2. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/PKG-INFO +1 -1
  3. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/hotdata_framework/client.py +43 -0
  4. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/hotdata_framework/managed_client.py +2 -0
  5. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/pyproject.toml +1 -1
  6. hotdata_framework-0.6.3/tests/test_request_timeout.py +69 -0
  7. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/uv.lock +1 -1
  8. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/.github/dependabot.yml +0 -0
  9. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/.github/workflows/check-release.yml +0 -0
  10. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/.github/workflows/ci.yml +0 -0
  11. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/.github/workflows/dependabot-automerge.yml +0 -0
  12. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/.github/workflows/publish.yml +0 -0
  13. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/.github/workflows/release.yml +0 -0
  14. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/.gitignore +0 -0
  15. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/CONTRACT.md +0 -0
  16. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/README.md +0 -0
  17. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/RELEASING.md +0 -0
  18. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/examples/basic_usage.py +0 -0
  19. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/hotdata_framework/__init__.py +0 -0
  20. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/hotdata_framework/databases.py +0 -0
  21. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/hotdata_framework/env.py +0 -0
  22. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/hotdata_framework/errors.py +0 -0
  23. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/hotdata_framework/health.py +0 -0
  24. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/hotdata_framework/http.py +0 -0
  25. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/hotdata_framework/py.typed +0 -0
  26. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/hotdata_framework/result.py +0 -0
  27. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/scripts/check-release.py +0 -0
  28. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/scripts/extract-changelog.py +0 -0
  29. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/scripts/publish-workflow.sh +0 -0
  30. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/scripts/release.sh +0 -0
  31. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/scripts/update_changelog.py +0 -0
  32. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/tests/test_client.py +0 -0
  33. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/tests/test_contract.py +0 -0
  34. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/tests/test_databases.py +0 -0
  35. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/tests/test_errors.py +0 -0
  36. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/tests/test_health.py +0 -0
  37. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/tests/test_managed_client.py +0 -0
  38. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/tests/test_result.py +0 -0
  39. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/tests/test_update_changelog.py +0 -0
  40. {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/tests/test_version.py +0 -0
@@ -8,6 +8,12 @@ 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
+
11
17
  ## [0.6.2] - 2026-07-08
12
18
 
13
19
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hotdata-framework
3
- Version: 0.6.2
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.2"
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"
@@ -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.2"
104
+ version = "0.6.3"
105
105
  source = { editable = "." }
106
106
  dependencies = [
107
107
  { name = "hotdata" },