chtypes 2.0.0.dev7__tar.gz → 2.0.0.dev9__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 (101) hide show
  1. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/CHANGELOG.md +13 -0
  2. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/PKG-INFO +1 -1
  3. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/pyproject.toml +1 -1
  4. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/_decode.py +8 -1
  5. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/_input.py +37 -2
  6. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/library.py +1 -1
  7. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/api/test_decode.py +22 -0
  8. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/api/test_input.py +38 -0
  9. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/uv.lock +1 -1
  10. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/.gitignore +0 -0
  11. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/LICENSE +0 -0
  12. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/README.md +0 -0
  13. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/__init__.py +0 -0
  14. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/__main__.py +0 -0
  15. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/_abi2/__init__.py +0 -0
  16. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/_abi2/_decls.py +0 -0
  17. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/_abi2/_errmap.py +0 -0
  18. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/_abi2/_errors.py +0 -0
  19. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/_abi2/_loader.py +0 -0
  20. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/_abi2/_vocab.py +0 -0
  21. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/_codes.py +0 -0
  22. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/_ed25519.py +0 -0
  23. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/_guard.py +0 -0
  24. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/_ocifetch/__init__.py +0 -0
  25. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/_ocifetch/_channel.py +0 -0
  26. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/_ocifetch/_constants.py +0 -0
  27. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/_ocifetch/_dsse.py +0 -0
  28. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/_ocifetch/_ensure.py +0 -0
  29. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/_ocifetch/_errors.py +0 -0
  30. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/_ocifetch/_faults.py +0 -0
  31. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/_ocifetch/_goldens.py +0 -0
  32. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/_ocifetch/_hold.py +0 -0
  33. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/_ocifetch/_http.py +0 -0
  34. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/_ocifetch/_layout.py +0 -0
  35. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/_ocifetch/_lock.py +0 -0
  36. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/_ocifetch/_oci.py +0 -0
  37. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/_ocifetch/_prune.py +0 -0
  38. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/_ocifetch/_referrers.py +0 -0
  39. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/_ocifetch/_resolve.py +0 -0
  40. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/_ocifetch/_retired.py +0 -0
  41. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/_ocifetch/_unpack.py +0 -0
  42. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/_setup.py +0 -0
  43. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/errors.py +0 -0
  44. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/py.typed +0 -0
  45. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/registry.py +0 -0
  46. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/src/chtypes/results.py +0 -0
  47. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/abi2/__init__.py +0 -0
  48. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/abi2/_stub_tree.py +0 -0
  49. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/abi2/conftest.py +0 -0
  50. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/abi2/test_conformance.py +0 -0
  51. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/abi2/test_dev_fingerprint.py +0 -0
  52. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/abi2/test_goldens_runner_stub.py +0 -0
  53. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/abi2/test_loader.py +0 -0
  54. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/abi2/test_public_library.py +0 -0
  55. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/abi2/test_public_open.py +0 -0
  56. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/abi2/test_public_registry.py +0 -0
  57. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/abi2/test_public_server.py +0 -0
  58. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/abi2/test_public_setup_cases.py +0 -0
  59. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/abi2/test_reader_rules.py +0 -0
  60. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/abi2/test_registry_fetch.py +0 -0
  61. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/abi2/test_registry_flight.py +0 -0
  62. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/api/__init__.py +0 -0
  63. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/api/test_trusted_keys.py +0 -0
  64. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/api/test_vocab.py +0 -0
  65. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/cli/__init__.py +0 -0
  66. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/cli/conftest.py +0 -0
  67. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/cli/test_devchannel.py +0 -0
  68. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/cli/test_main.py +0 -0
  69. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/cli/test_resolve_prune.py +0 -0
  70. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/conftest.py +0 -0
  71. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/goldens_v1/__init__.py +0 -0
  72. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/goldens_v1/data/stub-goldens.json +0 -0
  73. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/goldens_v1/test_runner.py +0 -0
  74. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/ocifetch/__init__.py +0 -0
  75. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/ocifetch/_bundle_support.py +0 -0
  76. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/ocifetch/_registry_support.py +0 -0
  77. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/ocifetch/_sign_support.py +0 -0
  78. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/ocifetch/conftest.py +0 -0
  79. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/ocifetch/test_cache_roots.py +0 -0
  80. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/ocifetch/test_cache_strict.py +0 -0
  81. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/ocifetch/test_conformance.py +0 -0
  82. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/ocifetch/test_devchannel.py +0 -0
  83. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/ocifetch/test_dsse.py +0 -0
  84. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/ocifetch/test_ensure.py +0 -0
  85. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/ocifetch/test_goldens.py +0 -0
  86. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/ocifetch/test_http.py +0 -0
  87. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/ocifetch/test_layout.py +0 -0
  88. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/ocifetch/test_lock.py +0 -0
  89. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/ocifetch/test_oci.py +0 -0
  90. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/ocifetch/test_prodv2_smoke.py +0 -0
  91. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/ocifetch/test_prodv2channel.py +0 -0
  92. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/ocifetch/test_retired.py +0 -0
  93. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/ocifetch/test_seam_gate.py +0 -0
  94. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/ocifetch/test_unpack.py +0 -0
  95. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/test_abi_identity.py +0 -0
  96. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/test_cache_roots_api.py +0 -0
  97. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/test_devchannel_registry.py +0 -0
  98. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/test_ed25519.py +0 -0
  99. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/test_examples_tour.py +0 -0
  100. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/test_fetch_options.py +0 -0
  101. {chtypes-2.0.0.dev7 → chtypes-2.0.0.dev9}/tests/test_support_v2_gen.py +0 -0
@@ -54,6 +54,17 @@ The four bindings in this repository are released together and give one answer,
54
54
  - **Enforcement gate restored** for `26.7` on `darwin-arm64`.
55
55
  - **Enforcement gate restored** for `26.8` on `darwin-arm64`.
56
56
  - **Enforcement gate restored** for `26.9` on `darwin-arm64`.
57
+ - **Enforcement gate lifted** for `26.3` on `linux-amd64`.
58
+ - **Enforcement gate lifted** for `26.7` on `linux-amd64`.
59
+ - **Enforcement gate lifted** for `26.8` on `linux-amd64`.
60
+ - **Enforcement gate lifted** for `26.9` on `linux-amd64`.
61
+ - **Enforcement gate lifted** for `26.3` on `linux-arm64`.
62
+ - **Enforcement gate lifted** for `26.7` on `linux-arm64`.
63
+ - **Enforcement gate lifted** for `26.8` on `linux-arm64`.
64
+ - **Enforcement gate lifted** for `26.9` on `linux-arm64`.
65
+ - **Enforcement gate lifted** for `26.7` on `darwin-arm64`.
66
+ - **Enforcement gate lifted** for `26.8` on `darwin-arm64`.
67
+ - **Enforcement gate lifted** for `26.9` on `darwin-arm64`.
57
68
 
58
69
  ### Removed
59
70
 
@@ -61,6 +72,8 @@ The four bindings in this repository are released together and give one answer,
61
72
 
62
73
  ### Fixed
63
74
 
75
+ - A settings or query-parameter key or value that is not valid UTF-8 is refused as a usage error; it was silently replaced with U+FFFD, so the library saw a different value. A string holding a lone surrogate (what `surrogateescape` makes of undecodable bytes) was sent as a `\udXXX` escape; the same inputs are refused now, for the per-call zone, the setup defaults and the server profile too. Valid UTF-8 serializes as before.
76
+ - A declined export's payload is absent, as documented: it was an empty payload when the library returned a zero-length buffer for a declined export. `BatchResult.payload` and `spans` are `None` for a declined export. Whether an export was declined is decided from the document's `export_declined`, never from the buffer's length.
64
77
  - The layout's `index.json` never gains a second entry for one manifest (#487): concurrent installs of one build each appended an entry (7 or 8 entries for the same digest after 8 concurrent installs, measured on 1.0.2). The merge now deduplicates by digest, as Go and TypeScript do.
65
78
  - `--frozen` with a warm, verified cache makes zero requests, and `--frozen --offline` verifies the installed build against the lock with zero network (#414). An installed build whose `verified.json` is for the platform and records exactly the lock's pinned manifest, layer and bundle digests is used as it is. `--frozen --offline` is `CHTYPES_ARTIFACT_MISSING` when the pinned build is not installed, and `CHTYPES_ARTIFACT_PINNED` when there is no lock entry or the installed record does not match the pin; `--offline` without `--frozen` is unchanged. The `frozen-warm-cache-zero-requests`, `frozen-offline-lock-match`, `frozen-offline-lock-mismatch` and `frozen-offline-lock-not-installed` conformance cases hold all four bindings to it, under both corpora. The Python frozen path also compares the record's layer and bundle digests to the pin, and looks in the system directories too.
66
79
  - A 2.0.0-dev SDK whose own-fingerprint alias has expired, with a cached build of that fingerprint for the request, now says so in the `CHTYPES_ARTIFACT_UNPUBLISHED` message of `fetch` (#581): `; a cached build for this fingerprint (build <id>) is still usable by open; upgrade the SDK to get newer builds` is appended. The code, the exit status (4) and the cache are unchanged, and all four bindings give the same message (`fp-ahead-cached-own-fp`).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: chtypes
3
- Version: 2.0.0.dev7
3
+ Version: 2.0.0.dev9
4
4
  Summary: Python binding for the chtypes artifacts: ClickHouse's own type system, per version, behind the frozen chs_* C ABI
5
5
  Project-URL: Homepage, https://github.com/wave-rf/chtypes
6
6
  Project-URL: Repository, https://github.com/wave-rf/chtypes
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "chtypes"
3
- version = "2.0.0.dev7"
3
+ version = "2.0.0.dev9"
4
4
  description = "Python binding for the chtypes artifacts: ClickHouse's own type system, per version, behind the frozen chs_* C ABI"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -428,6 +428,13 @@ def decode_batch(raw: bytes, export: bytes | None) -> BatchResult:
428
428
  framing = obj.get("framing")
429
429
  engine_rows = None if obj.get("engine_rows") is None else _engine_rows(obj, where)
430
430
  spans = None if obj.get("row_spans") is None else _spans(obj, "row_spans", where)
431
+ export_declined = _bytes(obj, "export_declined", where)
432
+ if export_declined:
433
+ # A declined export has no payload and no spans, whatever buffer the call
434
+ # returned: the library returns a NULL out_export for it (builds up to
435
+ # 20261011.105019 returned a zero-length buffer), and the document's
436
+ # export_declined decides, never the buffer.
437
+ export, spans = None, None
431
438
  return BatchResult(
432
439
  outcome=Outcome.of(_text(obj, "outcome", where)),
433
440
  err_code=_int(obj, "code", where),
@@ -439,7 +446,7 @@ def decode_batch(raw: bytes, export: bytes | None) -> BatchResult:
439
446
  engine_rows=engine_rows,
440
447
  payload=export,
441
448
  spans=spans,
442
- export_declined=_bytes(obj, "export_declined", where),
449
+ export_declined=export_declined,
443
450
  rows_passed=_u64(obj, "rows_passed", where),
444
451
  rows_cut=_u64(obj, "rows_cut", where),
445
452
  partition_count=_opt_u64(obj, "partition_count", where),
@@ -45,6 +45,31 @@ def body_bytes(value: bytes, what: str = "body") -> bytes:
45
45
  raise TypeError(f"{what} must be bytes, not {type(value).__name__}")
46
46
 
47
47
 
48
+ _NON_UTF8_HINT = (
49
+ "this ABI generation cannot carry a non-UTF-8 value in a JSON input; "
50
+ "write the bytes as unhex('<hex>') in the expression instead"
51
+ )
52
+
53
+
54
+ def _is_utf8(text: str) -> bool:
55
+ """False for a `str` that cannot be UTF-8 encoded (a lone surrogate, as
56
+ `surrogateescape` makes of undecodable bytes)."""
57
+ try:
58
+ text.encode("utf-8")
59
+ except UnicodeEncodeError:
60
+ return False
61
+ return True
62
+
63
+
64
+ def _refuse_non_utf8(what: str, part: str, key: str) -> Exception:
65
+ # The key is shown escaped (ASCII only), never as the raw text.
66
+ return misuse(
67
+ f"{what}: the key {json.dumps(key)} is not valid UTF-8, and {_NON_UTF8_HINT}"
68
+ if part == "key"
69
+ else f"{what}: the value for key {json.dumps(key)} is not valid UTF-8, and {_NON_UTF8_HINT}"
70
+ )
71
+
72
+
48
73
  def _string_map(mapping: Mapping[str, str], what: str) -> dict[str, str]:
49
74
  out: dict[str, str] = {}
50
75
  for key, value in mapping.items():
@@ -53,6 +78,12 @@ def _string_map(mapping: Mapping[str, str], what: str) -> dict[str, str]:
53
78
  f"{what} maps strings to strings only; got {type(key).__name__} -> "
54
79
  f"{type(value).__name__} for {key!r} (a value is never rewritten)"
55
80
  )
81
+ # json.dumps would write a lone surrogate as a \udXXX escape, a value
82
+ # the library does not read as the bytes the caller meant: refuse.
83
+ if not _is_utf8(key):
84
+ raise _refuse_non_utf8(what, "key", key)
85
+ if not _is_utf8(value):
86
+ raise _refuse_non_utf8(what, "value", key)
56
87
  out[key] = value
57
88
  return out
58
89
 
@@ -75,6 +106,8 @@ def settings_json(
75
106
  if session_timezone is not None:
76
107
  if not isinstance(session_timezone, str):
77
108
  raise TypeError(f"session_timezone must be str, not {type(session_timezone).__name__}")
109
+ if not _is_utf8(session_timezone):
110
+ raise misuse(f"the zone: the value is not valid UTF-8, and {_NON_UTF8_HINT}")
78
111
  if _ZONE_KEY in merged:
79
112
  raise misuse(
80
113
  f"the {_ZONE_KEY} option and a {_ZONE_KEY} settings key were both given; "
@@ -141,10 +174,12 @@ def server_profile_json(profile: ServerProfile) -> bytes:
141
174
  if profile.timezone is not None:
142
175
  if not isinstance(profile.timezone, str):
143
176
  raise TypeError(f"timezone must be str, not {type(profile.timezone).__name__}")
177
+ if not _is_utf8(profile.timezone):
178
+ raise misuse(f"server profile: the timezone is not valid UTF-8, and {_NON_UTF8_HINT}")
144
179
  if profile.timezone != "":
145
180
  doc["timezone"] = profile.timezone
146
181
  if profile.settings is not None:
147
- doc["settings"] = _string_map(profile.settings, "settings")
182
+ doc["settings"] = _string_map(profile.settings, "server profile settings")
148
183
  if profile.macros is not None:
149
- doc["macros"] = _string_map(profile.macros, "macros")
184
+ doc["macros"] = _string_map(profile.macros, "server profile macros")
150
185
  return _dump(doc)
@@ -296,7 +296,7 @@ class Schema(_Handle):
296
296
  zone is its own, fixed here: its WHERE runs in the session this call's
297
297
  `session_timezone` (or `settings`) names, on every evaluation."""
298
298
  expr_bytes = to_bytes(expr, "expr")
299
- params_bytes = string_map_json(params, "params")
299
+ params_bytes = string_map_json(params, "query parameters")
300
300
  settings_bytes = settings_json(settings, session_timezone)
301
301
  with _hold(self._guard):
302
302
  handle = self._library._api.filter_create(
@@ -177,6 +177,28 @@ BATCH = {
177
177
  }
178
178
 
179
179
 
180
+ def test_a_declined_exports_payload_is_absent_whatever_the_buffer() -> None:
181
+ # The library returns a NULL out_export for a declined export (builds up to
182
+ # 20261011.105019 returned a non-NULL, zero-length buffer): the document's
183
+ # export_declined decides, never the buffer.
184
+ for member in (
185
+ {"export_declined": "only accepted batches export"},
186
+ {"export_declined_b64": b64(b"\xff")},
187
+ ):
188
+ doc = {"outcome": "rejected", "row_spans": [{"off": 0, "len": 1}], **member}
189
+ for buffer in (b"", b"x"):
190
+ batch = decode_batch(dumps(doc), buffer)
191
+ assert batch.payload is None and batch.spans is None and batch.export_declined
192
+
193
+
194
+ def test_an_accepted_zero_row_export_is_present_and_empty() -> None:
195
+ batch = decode_batch(
196
+ dumps({"outcome": "accepted", "row_spans": [], "export_declined": ""}), b""
197
+ )
198
+ assert batch.payload == b"" and batch.spans == () and batch.export_declined == b""
199
+ assert decode_batch(dumps({"outcome": "accepted"}), None).payload is None
200
+
201
+
180
202
  def test_a_batch_decodes_rows_payload_framing_and_spans() -> None:
181
203
  batch = decode_batch(dumps(BATCH), b"export bytes")
182
204
  assert batch.outcome is Outcome.ACCEPTED_POISONED
@@ -64,3 +64,41 @@ def test_text_inputs_are_utf8_and_bodies_never_accept_text() -> None:
64
64
  assert body_bytes(memoryview(b"ab")) == b"ab"
65
65
  with pytest.raises(TypeError):
66
66
  body_bytes("text") # type: ignore[arg-type]
67
+
68
+
69
+ _LONE = "\udcff" # what surrogateescape makes of the byte 0xff
70
+
71
+
72
+ @pytest.mark.parametrize(
73
+ ("call", "words"),
74
+ [
75
+ (lambda: settings_json({"k": _LONE}), ("settings", 'the value for key "k"')),
76
+ (lambda: settings_json({"k" + _LONE: "v"}), ("settings", 'the key "k\\udcff"')),
77
+ (lambda: settings_json({"k": _LONE}, "UTC"), ("settings", 'the value for key "k"')),
78
+ (lambda: settings_json(None, _LONE), ("the zone",)),
79
+ (
80
+ lambda: string_map_json({"p": _LONE}, "query parameters"),
81
+ ("query parameters", 'the value for key "p"'),
82
+ ),
83
+ (
84
+ lambda: string_map_json({_LONE: "v"}, "query parameters"),
85
+ ("query parameters", 'the key "\\udcff"'),
86
+ ),
87
+ ],
88
+ )
89
+ def test_a_non_utf8_key_or_value_is_a_usage_error_naming_the_input_and_the_key(call, words) -> None:
90
+ with pytest.raises(UsageError) as info:
91
+ call()
92
+ message = info.value.message.decode("utf-8")
93
+ for word in (*words, "not valid UTF-8", "unhex('<hex>')"):
94
+ assert word in message
95
+ assert info.value.status == 3 and info.value.ch_code == 0
96
+ assert message.isascii() # the key is escaped, never raw
97
+
98
+
99
+ def test_valid_non_ascii_serializes_byte_for_byte_as_before() -> None:
100
+ # Measured before the change on the same inputs: json.dumps, ASCII escapes.
101
+ assert string_map_json({"p": "é日本"}, "query parameters") == b'{"p":"\\u00e9\\u65e5\\u672c"}'
102
+ assert settings_json({"é": "\ufffd"}, "Asia/Tokyo") == (
103
+ b'{"\\u00e9":"\\ufffd","session_timezone":"Asia/Tokyo"}'
104
+ )
@@ -74,7 +74,7 @@ wheels = [
74
74
 
75
75
  [[package]]
76
76
  name = "chtypes"
77
- version = "2.0.0.dev7"
77
+ version = "2.0.0.dev9"
78
78
  source = { editable = "." }
79
79
  dependencies = [
80
80
  { name = "backports-zstd", marker = "python_full_version < '3.14'" },
File without changes
File without changes
File without changes