hotdata-framework 0.7.3__tar.gz → 0.8.0__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.7.3 → hotdata_framework-0.8.0}/CHANGELOG.md +9 -0
  2. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/PKG-INFO +2 -2
  3. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/hotdata_framework/client.py +2 -0
  4. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/hotdata_framework/managed_client.py +6 -0
  5. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/pyproject.toml +2 -2
  6. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/tests/test_databases.py +10 -0
  7. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/tests/test_managed_client.py +38 -1
  8. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/uv.lock +5 -5
  9. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/.github/dependabot.yml +0 -0
  10. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/.github/workflows/check-release.yml +0 -0
  11. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/.github/workflows/ci.yml +0 -0
  12. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/.github/workflows/dependabot-automerge.yml +0 -0
  13. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/.github/workflows/publish.yml +0 -0
  14. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/.github/workflows/release.yml +0 -0
  15. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/.gitignore +0 -0
  16. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/CONTRACT.md +0 -0
  17. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/README.md +0 -0
  18. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/RELEASING.md +0 -0
  19. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/examples/basic_usage.py +0 -0
  20. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/hotdata_framework/__init__.py +0 -0
  21. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/hotdata_framework/databases.py +0 -0
  22. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/hotdata_framework/env.py +0 -0
  23. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/hotdata_framework/errors.py +0 -0
  24. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/hotdata_framework/health.py +0 -0
  25. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/hotdata_framework/http.py +0 -0
  26. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/hotdata_framework/py.typed +0 -0
  27. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/hotdata_framework/result.py +0 -0
  28. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/scripts/check-release.py +0 -0
  29. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/scripts/extract-changelog.py +0 -0
  30. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/scripts/publish-workflow.sh +0 -0
  31. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/scripts/release.sh +0 -0
  32. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/scripts/update_changelog.py +0 -0
  33. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/tests/test_client.py +0 -0
  34. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/tests/test_contract.py +0 -0
  35. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/tests/test_errors.py +0 -0
  36. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/tests/test_health.py +0 -0
  37. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/tests/test_request_timeout.py +0 -0
  38. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/tests/test_result.py +0 -0
  39. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/tests/test_update_changelog.py +0 -0
  40. {hotdata_framework-0.7.3 → hotdata_framework-0.8.0}/tests/test_version.py +0 -0
@@ -8,6 +8,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
  ## [Unreleased]
9
9
 
10
10
 
11
+ ## [0.8.0] - 2026-07-20
12
+
13
+ ### Changed
14
+
15
+ - `load_managed_table` accepts a `key` argument — the merge key columns for
16
+ `delete`/`update`/`upsert` loads, matched per-load instead of requiring a key
17
+ declared at table creation. Omit it to use the table's declared key; ignored
18
+ for `replace`/`append`. Requires `hotdata>=0.8.0`.
19
+
11
20
  ## [0.7.3] - 2026-07-16
12
21
 
13
22
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hotdata-framework
3
- Version: 0.7.3
3
+ Version: 0.8.0
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
@@ -21,7 +21,7 @@ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
21
21
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
22
  Classifier: Typing :: Typed
23
23
  Requires-Python: >=3.10
24
- Requires-Dist: hotdata>=0.7.0
24
+ Requires-Dist: hotdata>=0.8.0
25
25
  Requires-Dist: pandas>=2.0
26
26
  Requires-Dist: pyarrow>=14.0
27
27
  Description-Content-Type: text/markdown
@@ -340,6 +340,7 @@ class HotdataClient:
340
340
  upload_id: str | None = None,
341
341
  file: str | None = None,
342
342
  mode: ManagedLoadMode = "replace",
343
+ key: list[str] | None = None,
343
344
  ) -> LoadManagedTableResult:
344
345
  if (upload_id is None) == (file is None):
345
346
  raise ValueError("Exactly one of upload_id or file is required")
@@ -352,6 +353,7 @@ class HotdataClient:
352
353
  request = LoadManagedTableRequest(
353
354
  mode=mode,
354
355
  upload_id=resolved_upload_id,
356
+ key=key,
355
357
  )
356
358
  try:
357
359
  loaded = self.connections().load_managed_table(
@@ -205,10 +205,15 @@ class ManagedDatabaseClient:
205
205
  schema: str,
206
206
  upload_id: str,
207
207
  mode: ManagedLoadMode = "replace",
208
+ key: list[str] | None = None,
208
209
  ) -> LoadManagedTableResult:
209
210
  # append is the only non-idempotent mode: if the server commits the load
210
211
  # but the response is lost, a retry re-appends the same rows. Run it
211
212
  # at-most-once; every other mode is safe to retry.
213
+ #
214
+ # `key` is the merge key for delete/update/upsert loads: when set it is
215
+ # matched per-load instead of a key declared at table creation. Omit it
216
+ # to use the table's declared key. Ignored for replace/append.
212
217
  return self._request_with_retry(
213
218
  lambda: self._runtime.load_managed_table(
214
219
  database,
@@ -216,6 +221,7 @@ class ManagedDatabaseClient:
216
221
  schema=schema,
217
222
  upload_id=upload_id,
218
223
  mode=mode,
224
+ key=key,
219
225
  ),
220
226
  retryable=(mode != "append"),
221
227
  )
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hotdata-framework"
7
- version = "0.7.3"
7
+ version = "0.8.0"
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"
@@ -27,7 +27,7 @@ classifiers = [
27
27
  ]
28
28
  dependencies = [
29
29
  # 0.7.0 adds `key` to table decls (create_managed_database(keys=) / add_managed_table(key=))
30
- "hotdata>=0.7.0",
30
+ "hotdata>=0.8.0",
31
31
  "pandas>=2.0",
32
32
  "pyarrow>=14.0",
33
33
  ]
@@ -308,6 +308,16 @@ def test_load_managed_table_defaults_to_replace():
308
308
  assert _load_and_capture_request(_client()).mode == "replace"
309
309
 
310
310
 
311
+ def test_load_managed_table_passes_key():
312
+ request = _load_and_capture_request(_client(), mode="delete", key=["id"])
313
+ assert request.mode == "delete"
314
+ assert request.key == ["id"]
315
+
316
+
317
+ def test_load_managed_table_key_defaults_to_none():
318
+ assert _load_and_capture_request(_client()).key is None
319
+
320
+
311
321
  @pytest.mark.parametrize("mode", ["append", "delete", "update", "upsert"])
312
322
  def test_load_managed_table_forwards_mode(mode: str):
313
323
  assert _load_and_capture_request(_client(), mode=mode).mode == mode
@@ -163,7 +163,13 @@ def _load_recording_runtime(calls: list[str]) -> SimpleNamespace:
163
163
  with a transient error, so retry behaviour is observable via ``calls``."""
164
164
 
165
165
  def load_managed_table(
166
- database: str, table: str, *, schema: str, upload_id: str, mode: str
166
+ database: str,
167
+ table: str,
168
+ *,
169
+ schema: str,
170
+ upload_id: str,
171
+ mode: str,
172
+ key: list[str] | None = None,
167
173
  ) -> SimpleNamespace:
168
174
  calls.append(mode)
169
175
  raise TimeoutError("commit succeeded but response was lost")
@@ -209,3 +215,34 @@ def test_idempotent_load_retries_on_transient(monkeypatch: pytest.MonkeyPatch) -
209
215
  client.load_managed_table("db", "orders", schema="public", upload_id="u1", mode="replace")
210
216
 
211
217
  assert calls == ["replace", "replace", "replace"] # retried up to max_retries
218
+
219
+
220
+ def test_load_managed_table_forwards_key(monkeypatch: pytest.MonkeyPatch) -> None:
221
+ """A per-load ``key`` is passed straight through to the runtime client."""
222
+ monkeypatch.setattr(mc.time, "sleep", lambda _seconds: None)
223
+ captured: dict[str, Any] = {}
224
+
225
+ def load_managed_table(
226
+ database: str,
227
+ table: str,
228
+ *,
229
+ schema: str,
230
+ upload_id: str,
231
+ mode: str,
232
+ key: list[str] | None = None,
233
+ ) -> SimpleNamespace:
234
+ captured["mode"] = mode
235
+ captured["key"] = key
236
+ return SimpleNamespace(
237
+ connection_id="c", schema_name=schema, table_name=table, row_count=0
238
+ )
239
+
240
+ client = _managed_client(max_retries=1)
241
+ runtime = _fake_runtime()
242
+ runtime.load_managed_table = load_managed_table
243
+ client._runtime = runtime
244
+
245
+ client.load_managed_table(
246
+ "db", "orders", schema="public", upload_id="u1", mode="delete", key=["id"]
247
+ )
248
+ assert captured == {"mode": "delete", "key": ["id"]}
@@ -86,7 +86,7 @@ wheels = [
86
86
 
87
87
  [[package]]
88
88
  name = "hotdata"
89
- version = "0.7.0"
89
+ version = "0.8.0"
90
90
  source = { registry = "https://pypi.org/simple" }
91
91
  dependencies = [
92
92
  { name = "pydantic" },
@@ -94,14 +94,14 @@ dependencies = [
94
94
  { name = "typing-extensions" },
95
95
  { name = "urllib3" },
96
96
  ]
97
- sdist = { url = "https://files.pythonhosted.org/packages/61/30/08681132e019f6c9ddb566dc38be6a302763c4b4848b2d97e3d7ace8d257/hotdata-0.7.0.tar.gz", hash = "sha256:ef4484a74c0f33ee543b0a0dbfb5a6c04e7b5812b277f42a6448000d41c2405d", size = 215130, upload-time = "2026-07-14T12:21:33.936Z" }
97
+ sdist = { url = "https://files.pythonhosted.org/packages/8a/38/30ed3d1d99413e7684672fa424baef85a347db465b90c774443320cf1cea/hotdata-0.8.0.tar.gz", hash = "sha256:cdac515ffa193ed028491e4b7abcd1b6404d4e3f96986c9c0a2b2f407db42eb6", size = 216848, upload-time = "2026-07-20T06:30:37.659Z" }
98
98
  wheels = [
99
- { url = "https://files.pythonhosted.org/packages/dc/ef/6c4236640629688074892759b1a7593c08f30e9d916b6951c2bbfec19d7f/hotdata-0.7.0-py3-none-any.whl", hash = "sha256:d9e3008e3084d22bcc27b7bf1d08d8c2c255fe3726c252c777bb614a3cae0576", size = 314103, upload-time = "2026-07-14T12:21:32.154Z" },
99
+ { url = "https://files.pythonhosted.org/packages/bc/0f/f2ccaeb0f910c3f8cd84a911d934704277f0d6e5a20f8b49cf0eedb2a8aa/hotdata-0.8.0-py3-none-any.whl", hash = "sha256:5d64bfc185a0e7e2bf34ebdcea50787d3418fb250dacc53e00dc74d64e99f753", size = 316923, upload-time = "2026-07-20T06:30:35.778Z" },
100
100
  ]
101
101
 
102
102
  [[package]]
103
103
  name = "hotdata-framework"
104
- version = "0.7.3"
104
+ version = "0.8.0"
105
105
  source = { editable = "." }
106
106
  dependencies = [
107
107
  { name = "hotdata" },
@@ -120,7 +120,7 @@ dev = [
120
120
 
121
121
  [package.metadata]
122
122
  requires-dist = [
123
- { name = "hotdata", specifier = ">=0.7.0" },
123
+ { name = "hotdata", specifier = ">=0.8.0" },
124
124
  { name = "pandas", specifier = ">=2.0" },
125
125
  { name = "pyarrow", specifier = ">=14.0" },
126
126
  ]