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.
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/CHANGELOG.md +6 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/PKG-INFO +1 -1
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/hotdata_framework/client.py +43 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/hotdata_framework/managed_client.py +2 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/pyproject.toml +1 -1
- hotdata_framework-0.6.3/tests/test_request_timeout.py +69 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/uv.lock +1 -1
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/.github/dependabot.yml +0 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/.github/workflows/check-release.yml +0 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/.github/workflows/ci.yml +0 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/.github/workflows/dependabot-automerge.yml +0 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/.github/workflows/publish.yml +0 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/.github/workflows/release.yml +0 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/.gitignore +0 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/CONTRACT.md +0 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/README.md +0 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/RELEASING.md +0 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/examples/basic_usage.py +0 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/hotdata_framework/__init__.py +0 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/hotdata_framework/databases.py +0 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/hotdata_framework/env.py +0 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/hotdata_framework/errors.py +0 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/hotdata_framework/health.py +0 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/hotdata_framework/http.py +0 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/hotdata_framework/py.typed +0 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/hotdata_framework/result.py +0 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/scripts/check-release.py +0 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/scripts/extract-changelog.py +0 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/scripts/publish-workflow.sh +0 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/scripts/release.sh +0 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/scripts/update_changelog.py +0 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/tests/test_client.py +0 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/tests/test_contract.py +0 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/tests/test_databases.py +0 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/tests/test_errors.py +0 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/tests/test_health.py +0 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/tests/test_managed_client.py +0 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/tests/test_result.py +0 -0
- {hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/tests/test_update_changelog.py +0 -0
- {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.
|
|
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.
|
|
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()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hotdata_framework-0.6.2 → hotdata_framework-0.6.3}/.github/workflows/dependabot-automerge.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|