iii-sdk 0.11.7.dev1__tar.gz → 0.11.7.dev2__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.
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/PKG-INFO +1 -1
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/pyproject.toml +1 -1
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/src/iii/stream.py +65 -8
- iii_sdk-0.11.7.dev2/tests/test_stream_models.py +134 -0
- iii_sdk-0.11.7.dev1/tests/test_stream_models.py +0 -73
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/.gitignore +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/README.md +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/src/iii/__init__.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/src/iii/channels.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/src/iii/errors.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/src/iii/format_utils.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/src/iii/iii.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/src/iii/iii_constants.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/src/iii/iii_types.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/src/iii/logger.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/src/iii/otel_worker_gauges.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/src/iii/state.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/src/iii/telemetry.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/src/iii/telemetry_exporters.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/src/iii/telemetry_types.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/src/iii/triggers.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/src/iii/types.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/src/iii/utils.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/src/iii/worker_metrics.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/conftest.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_api_triggers.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_async_api.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_bridge.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_channel_close_delay.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_context_propagation.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_data_channels.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_errors.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_format_utils.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_healthcheck.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_hold_process.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_http_external_functions_integration.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_iii_registration_dedup.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_init_api.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_invocation_exception.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_logger_function_ids.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_logger_otel.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_middleware.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_pubsub.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_queue_integration.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_rbac_workers.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_register_function_args.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_state.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_streams.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_streams_runtime_annotations.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_sync_api.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_telemetry.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_telemetry_exporters.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_telemetry_types.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_trace_helpers.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_trigger_metadata.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_utils.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_worker_metadata.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_worker_metrics.py +0 -0
- {iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/uv.lock +0 -0
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
from abc import ABC, abstractmethod
|
|
6
6
|
from typing import Any, Generic, List, Literal, TypeVar
|
|
7
7
|
|
|
8
|
-
from pydantic import BaseModel, Field
|
|
8
|
+
from pydantic import BaseModel, Field, model_serializer
|
|
9
9
|
|
|
10
10
|
TData = TypeVar("TData")
|
|
11
11
|
|
|
@@ -117,11 +117,13 @@ class StreamUpdateResult(BaseModel, Generic[TData]):
|
|
|
117
117
|
|
|
118
118
|
old_value: TData | None = None
|
|
119
119
|
new_value: TData
|
|
120
|
-
# Per-op errors.
|
|
121
|
-
#
|
|
122
|
-
#
|
|
123
|
-
#
|
|
124
|
-
#
|
|
120
|
+
# Per-op errors. Emitted by ``merge`` and ``append`` for validation
|
|
121
|
+
# rejections (path/value bounds, proto-pollution segments) and by
|
|
122
|
+
# ``append`` for the ``append.type_mismatch`` and
|
|
123
|
+
# ``append.target_not_object`` surfaces. Field is omitted from the
|
|
124
|
+
# JSON wire when empty. ``default_factory`` is used (not ``= []``)
|
|
125
|
+
# to keep Pydantic's parameterized-Generic + default handling
|
|
126
|
+
# well-behaved across Python versions.
|
|
125
127
|
errors: list[UpdateOpError] = Field(default_factory=list)
|
|
126
128
|
|
|
127
129
|
|
|
@@ -156,12 +158,57 @@ class UpdateDecrement(BaseModel):
|
|
|
156
158
|
|
|
157
159
|
|
|
158
160
|
class UpdateAppend(BaseModel):
|
|
159
|
-
"""Append
|
|
161
|
+
"""Append an element to an array, concatenate a string, or push at a nested path.
|
|
162
|
+
|
|
163
|
+
The target is the root (when ``path`` is omitted, an empty string,
|
|
164
|
+
or an empty list), a single first-level key (when ``path`` is a
|
|
165
|
+
non-empty string), or an arbitrary nested location (when ``path``
|
|
166
|
+
is a list of literal segments).
|
|
167
|
+
|
|
168
|
+
Path forms accepted (mirrors :class:`UpdateMerge` after #1547):
|
|
169
|
+
- ``None`` / ``""`` / ``[]``: append at the root.
|
|
170
|
+
- ``"foo"``: append at the first-level key ``foo``. A dotted
|
|
171
|
+
string like ``"a.b"`` is the literal key ``"a.b"``, *not*
|
|
172
|
+
traversed as ``a -> b``.
|
|
173
|
+
- ``["a", "b", "c"]``: nested path; each element is a literal
|
|
174
|
+
segment.
|
|
175
|
+
|
|
176
|
+
Engine semantics:
|
|
177
|
+
- Missing/non-object intermediates along a nested path are
|
|
178
|
+
auto-created/replaced with ``{}``.
|
|
179
|
+
- At the leaf:
|
|
180
|
+
- missing/null + nested path -> ``[value]`` (always an array)
|
|
181
|
+
- missing/null + single-string path -> string-as-string for
|
|
182
|
+
the string-concat tier, otherwise ``[value]``
|
|
183
|
+
- existing array -> push
|
|
184
|
+
- existing string + string value -> concatenate
|
|
185
|
+
- existing object/scalar at the leaf -> ``append.type_mismatch``
|
|
186
|
+
|
|
187
|
+
Validation: invalid paths (depth > 32 segments, segment > 256
|
|
188
|
+
bytes, or any ``__proto__`` / ``constructor`` / ``prototype``
|
|
189
|
+
segment) are rejected with a structured error in the ``errors``
|
|
190
|
+
field of the ``state::update`` / ``stream::update`` response. The
|
|
191
|
+
append does not apply when an error is returned for that op.
|
|
192
|
+
"""
|
|
160
193
|
|
|
161
194
|
type: str = "append"
|
|
162
|
-
|
|
195
|
+
# Optional. Accepts a single string (legacy / first-level key) or
|
|
196
|
+
# a list of literal segments (nested append). ``None`` / ``""`` /
|
|
197
|
+
# ``[]`` all route to root append.
|
|
198
|
+
path: str | list[str] | None = None
|
|
163
199
|
value: Any
|
|
164
200
|
|
|
201
|
+
@model_serializer(mode="wrap")
|
|
202
|
+
def _omit_none_path(self, handler): # type: ignore[no-untyped-def]
|
|
203
|
+
# Drop ``path: None`` from the wire so cross-SDK consumers see
|
|
204
|
+
# the field absent rather than ``null``. Mirrors the Rust
|
|
205
|
+
# ``#[serde(skip_serializing_if = "Option::is_none")]`` on
|
|
206
|
+
# ``UpdateOp::Append.path``.
|
|
207
|
+
data = handler(self)
|
|
208
|
+
if data.get("path") is None:
|
|
209
|
+
data.pop("path", None)
|
|
210
|
+
return data
|
|
211
|
+
|
|
165
212
|
|
|
166
213
|
class UpdateRemove(BaseModel):
|
|
167
214
|
"""Remove operation for stream update."""
|
|
@@ -205,6 +252,16 @@ class UpdateMerge(BaseModel):
|
|
|
205
252
|
path: str | list[str] | None = None
|
|
206
253
|
value: Any
|
|
207
254
|
|
|
255
|
+
@model_serializer(mode="wrap")
|
|
256
|
+
def _omit_none_path(self, handler): # type: ignore[no-untyped-def]
|
|
257
|
+
# Mirrors the same skip-when-none rule applied to
|
|
258
|
+
# ``UpdateOp::Merge.path`` in the Rust SDK so cross-SDK wire
|
|
259
|
+
# payloads are byte-identical for root merges.
|
|
260
|
+
data = handler(self)
|
|
261
|
+
if data.get("path") is None:
|
|
262
|
+
data.pop("path", None)
|
|
263
|
+
return data
|
|
264
|
+
|
|
208
265
|
|
|
209
266
|
UpdateOp = UpdateSet | UpdateIncrement | UpdateDecrement | UpdateAppend | UpdateRemove | UpdateMerge
|
|
210
267
|
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Unit tests for stream model serialization."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from iii.stream import StreamUpdateResult, UpdateAppend, UpdateMerge, UpdateOpError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_update_append_model_serializes() -> None:
|
|
9
|
+
op = UpdateAppend(path="chunks", value={"text": "hello"})
|
|
10
|
+
|
|
11
|
+
assert op.model_dump() == {"type": "append", "path": "chunks", "value": {"text": "hello"}}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_update_append_with_array_path_round_trips() -> None:
|
|
15
|
+
"""Closes issue #1552 case 3: nested-path array form is the new happy path."""
|
|
16
|
+
op = UpdateAppend(path=["entityId", "buffer"], value="chunk")
|
|
17
|
+
dumped = op.model_dump()
|
|
18
|
+
assert dumped == {
|
|
19
|
+
"type": "append",
|
|
20
|
+
"path": ["entityId", "buffer"],
|
|
21
|
+
"value": "chunk",
|
|
22
|
+
}
|
|
23
|
+
parsed = UpdateAppend.model_validate(json.loads(json.dumps(dumped)))
|
|
24
|
+
assert parsed.path == ["entityId", "buffer"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_update_append_without_path_round_trips() -> None:
|
|
28
|
+
"""Root append omits ``path`` from the wire (parity with the Rust
|
|
29
|
+
``skip_serializing_if = "Option::is_none"`` on
|
|
30
|
+
``UpdateOp::Append.path`` — surfaced in #1612 review)."""
|
|
31
|
+
op = UpdateAppend(value="first")
|
|
32
|
+
dumped = op.model_dump()
|
|
33
|
+
assert dumped == {"type": "append", "value": "first"}
|
|
34
|
+
assert "path" not in dumped
|
|
35
|
+
parsed = UpdateAppend.model_validate(json.loads(json.dumps(dumped)))
|
|
36
|
+
assert parsed.path is None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_update_append_with_explicit_none_path() -> None:
|
|
40
|
+
"""Explicit ``None`` and missing field both round-trip through a
|
|
41
|
+
wire payload that has no ``path`` key. ``path: null`` payloads
|
|
42
|
+
coming from older clients still deserialize for backward compat."""
|
|
43
|
+
op = UpdateAppend(path=None, value=42)
|
|
44
|
+
assert op.path is None
|
|
45
|
+
assert op.model_dump() == {"type": "append", "value": 42}
|
|
46
|
+
parsed = UpdateAppend.model_validate({"type": "append", "value": 42})
|
|
47
|
+
assert parsed.path is None
|
|
48
|
+
parsed_null = UpdateAppend.model_validate(
|
|
49
|
+
{"type": "append", "path": None, "value": 42}
|
|
50
|
+
)
|
|
51
|
+
assert parsed_null.path is None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_update_append_with_empty_string_path() -> None:
|
|
55
|
+
"""Empty string is preserved as `Single("")` — engine maps it to root append."""
|
|
56
|
+
op = UpdateAppend(path="", value="x")
|
|
57
|
+
dumped = op.model_dump()
|
|
58
|
+
assert dumped["path"] == ""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_update_append_with_dotted_string_keeps_literal_segment() -> None:
|
|
62
|
+
"""`"a.b"` is a single literal key, never traversed as `a -> b`."""
|
|
63
|
+
op = UpdateAppend(path="entityId.buffer", value="literal")
|
|
64
|
+
parsed = UpdateAppend.model_validate(json.loads(json.dumps(op.model_dump())))
|
|
65
|
+
assert parsed.path == "entityId.buffer"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_update_merge_with_string_path_round_trips() -> None:
|
|
69
|
+
op = UpdateMerge(path="session-abc", value={"author": "alice"})
|
|
70
|
+
dumped = op.model_dump()
|
|
71
|
+
assert dumped == {
|
|
72
|
+
"type": "merge",
|
|
73
|
+
"path": "session-abc",
|
|
74
|
+
"value": {"author": "alice"},
|
|
75
|
+
}
|
|
76
|
+
# JSON round-trip preserves the string form.
|
|
77
|
+
parsed = UpdateMerge.model_validate(json.loads(json.dumps(dumped)))
|
|
78
|
+
assert parsed.path == "session-abc"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_update_merge_with_array_path_round_trips() -> None:
|
|
82
|
+
op = UpdateMerge(path=["sessions", "abc"], value={"ts": "chunk"})
|
|
83
|
+
dumped = op.model_dump()
|
|
84
|
+
assert dumped == {
|
|
85
|
+
"type": "merge",
|
|
86
|
+
"path": ["sessions", "abc"],
|
|
87
|
+
"value": {"ts": "chunk"},
|
|
88
|
+
}
|
|
89
|
+
parsed = UpdateMerge.model_validate(json.loads(json.dumps(dumped)))
|
|
90
|
+
assert parsed.path == ["sessions", "abc"]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_update_merge_without_path_round_trips() -> None:
|
|
94
|
+
"""Same wire-format rule as ``UpdateAppend``: root merge omits the
|
|
95
|
+
``path`` key entirely. ``path: null`` payloads still deserialize."""
|
|
96
|
+
op = UpdateMerge(value={"x": 1})
|
|
97
|
+
dumped = op.model_dump()
|
|
98
|
+
assert dumped == {"type": "merge", "value": {"x": 1}}
|
|
99
|
+
assert "path" not in dumped
|
|
100
|
+
parsed = UpdateMerge.model_validate(json.loads(json.dumps(dumped)))
|
|
101
|
+
assert parsed.path is None
|
|
102
|
+
parsed_null = UpdateMerge.model_validate(
|
|
103
|
+
{"type": "merge", "path": None, "value": {"x": 1}}
|
|
104
|
+
)
|
|
105
|
+
assert parsed_null.path is None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_update_op_error_round_trip() -> None:
|
|
109
|
+
err = UpdateOpError(
|
|
110
|
+
op_index=0,
|
|
111
|
+
code="merge.path.too_deep",
|
|
112
|
+
message="Path depth 33 exceeds maximum of 32",
|
|
113
|
+
doc_url="https://iii.dev/docs/workers/iii-state#merge-bounds",
|
|
114
|
+
)
|
|
115
|
+
dumped = err.model_dump()
|
|
116
|
+
assert dumped["code"] == "merge.path.too_deep"
|
|
117
|
+
assert dumped["op_index"] == 0
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_stream_update_result_with_errors_round_trips() -> None:
|
|
121
|
+
result = StreamUpdateResult[dict](
|
|
122
|
+
old_value=None,
|
|
123
|
+
new_value={"a": 1},
|
|
124
|
+
errors=[
|
|
125
|
+
UpdateOpError(
|
|
126
|
+
op_index=0,
|
|
127
|
+
code="merge.path.proto_polluted",
|
|
128
|
+
message='Path segment "__proto__" is a prototype-pollution sink',
|
|
129
|
+
)
|
|
130
|
+
],
|
|
131
|
+
)
|
|
132
|
+
dumped = result.model_dump()
|
|
133
|
+
assert len(dumped["errors"]) == 1
|
|
134
|
+
assert dumped["errors"][0]["code"] == "merge.path.proto_polluted"
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
"""Unit tests for stream model serialization."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
|
|
5
|
-
from iii.stream import StreamUpdateResult, UpdateAppend, UpdateMerge, UpdateOpError
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def test_update_append_model_serializes() -> None:
|
|
9
|
-
op = UpdateAppend(path="chunks", value={"text": "hello"})
|
|
10
|
-
|
|
11
|
-
assert op.model_dump() == {"type": "append", "path": "chunks", "value": {"text": "hello"}}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def test_update_merge_with_string_path_round_trips() -> None:
|
|
15
|
-
op = UpdateMerge(path="session-abc", value={"author": "alice"})
|
|
16
|
-
dumped = op.model_dump()
|
|
17
|
-
assert dumped == {
|
|
18
|
-
"type": "merge",
|
|
19
|
-
"path": "session-abc",
|
|
20
|
-
"value": {"author": "alice"},
|
|
21
|
-
}
|
|
22
|
-
# JSON round-trip preserves the string form.
|
|
23
|
-
parsed = UpdateMerge.model_validate(json.loads(json.dumps(dumped)))
|
|
24
|
-
assert parsed.path == "session-abc"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def test_update_merge_with_array_path_round_trips() -> None:
|
|
28
|
-
op = UpdateMerge(path=["sessions", "abc"], value={"ts": "chunk"})
|
|
29
|
-
dumped = op.model_dump()
|
|
30
|
-
assert dumped == {
|
|
31
|
-
"type": "merge",
|
|
32
|
-
"path": ["sessions", "abc"],
|
|
33
|
-
"value": {"ts": "chunk"},
|
|
34
|
-
}
|
|
35
|
-
parsed = UpdateMerge.model_validate(json.loads(json.dumps(dumped)))
|
|
36
|
-
assert parsed.path == ["sessions", "abc"]
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def test_update_merge_without_path_round_trips() -> None:
|
|
40
|
-
op = UpdateMerge(value={"x": 1})
|
|
41
|
-
dumped = op.model_dump()
|
|
42
|
-
assert dumped == {"type": "merge", "path": None, "value": {"x": 1}}
|
|
43
|
-
parsed = UpdateMerge.model_validate(json.loads(json.dumps(dumped)))
|
|
44
|
-
assert parsed.path is None
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def test_update_op_error_round_trip() -> None:
|
|
48
|
-
err = UpdateOpError(
|
|
49
|
-
op_index=0,
|
|
50
|
-
code="merge.path.too_deep",
|
|
51
|
-
message="Path depth 33 exceeds maximum of 32",
|
|
52
|
-
doc_url="https://iii.dev/docs/workers/iii-state#merge-bounds",
|
|
53
|
-
)
|
|
54
|
-
dumped = err.model_dump()
|
|
55
|
-
assert dumped["code"] == "merge.path.too_deep"
|
|
56
|
-
assert dumped["op_index"] == 0
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def test_stream_update_result_with_errors_round_trips() -> None:
|
|
60
|
-
result = StreamUpdateResult[dict](
|
|
61
|
-
old_value=None,
|
|
62
|
-
new_value={"a": 1},
|
|
63
|
-
errors=[
|
|
64
|
-
UpdateOpError(
|
|
65
|
-
op_index=0,
|
|
66
|
-
code="merge.path.proto_polluted",
|
|
67
|
-
message='Path segment "__proto__" is a prototype-pollution sink',
|
|
68
|
-
)
|
|
69
|
-
],
|
|
70
|
-
)
|
|
71
|
-
dumped = result.model_dump()
|
|
72
|
-
assert len(dumped["errors"]) == 1
|
|
73
|
-
assert dumped["errors"][0]["code"] == "merge.path.proto_polluted"
|
|
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
|
{iii_sdk-0.11.7.dev1 → iii_sdk-0.11.7.dev2}/tests/test_http_external_functions_integration.py
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
|