iii-sdk 0.11.4.dev3__tar.gz → 0.11.4.dev5__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 (59) hide show
  1. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/PKG-INFO +1 -1
  2. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/pyproject.toml +1 -1
  3. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/src/iii/stream.py +50 -5
  4. iii_sdk-0.11.4.dev5/tests/test_stream_models.py +73 -0
  5. iii_sdk-0.11.4.dev3/tests/test_stream_models.py +0 -9
  6. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/.gitignore +0 -0
  7. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/README.md +0 -0
  8. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/src/iii/__init__.py +0 -0
  9. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/src/iii/channels.py +0 -0
  10. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/src/iii/errors.py +0 -0
  11. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/src/iii/format_utils.py +0 -0
  12. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/src/iii/iii.py +0 -0
  13. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/src/iii/iii_constants.py +0 -0
  14. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/src/iii/iii_types.py +0 -0
  15. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/src/iii/logger.py +0 -0
  16. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/src/iii/otel_worker_gauges.py +0 -0
  17. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/src/iii/state.py +0 -0
  18. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/src/iii/telemetry.py +0 -0
  19. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/src/iii/telemetry_exporters.py +0 -0
  20. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/src/iii/telemetry_types.py +0 -0
  21. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/src/iii/triggers.py +0 -0
  22. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/src/iii/types.py +0 -0
  23. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/src/iii/utils.py +0 -0
  24. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/src/iii/worker_metrics.py +0 -0
  25. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/conftest.py +0 -0
  26. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/test_api_triggers.py +0 -0
  27. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/test_async_api.py +0 -0
  28. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/test_bridge.py +0 -0
  29. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/test_channel_close_delay.py +0 -0
  30. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/test_context_propagation.py +0 -0
  31. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/test_data_channels.py +0 -0
  32. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/test_errors.py +0 -0
  33. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/test_format_utils.py +0 -0
  34. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/test_healthcheck.py +0 -0
  35. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/test_hold_process.py +0 -0
  36. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/test_http_external_functions_integration.py +0 -0
  37. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/test_iii_registration_dedup.py +0 -0
  38. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/test_init_api.py +0 -0
  39. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/test_invocation_exception.py +0 -0
  40. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/test_logger_function_ids.py +0 -0
  41. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/test_logger_otel.py +0 -0
  42. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/test_middleware.py +0 -0
  43. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/test_pubsub.py +0 -0
  44. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/test_queue_integration.py +0 -0
  45. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/test_rbac_workers.py +0 -0
  46. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/test_register_function_args.py +0 -0
  47. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/test_state.py +0 -0
  48. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/test_streams.py +0 -0
  49. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/test_streams_runtime_annotations.py +0 -0
  50. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/test_sync_api.py +0 -0
  51. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/test_telemetry.py +0 -0
  52. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/test_telemetry_exporters.py +0 -0
  53. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/test_telemetry_types.py +0 -0
  54. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/test_trace_helpers.py +0 -0
  55. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/test_trigger_metadata.py +0 -0
  56. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/test_utils.py +0 -0
  57. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/test_worker_metadata.py +0 -0
  58. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/tests/test_worker_metrics.py +0 -0
  59. {iii_sdk-0.11.4.dev3 → iii_sdk-0.11.4.dev5}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iii-sdk
3
- Version: 0.11.4.dev3
3
+ Version: 0.11.4.dev5
4
4
  Summary: III SDK for Python
5
5
  Project-URL: Homepage, https://github.com/iii-hq/sdk
6
6
  Project-URL: Repository, https://github.com/iii-hq/sdk
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "iii-sdk"
7
- version = "0.11.4.dev3"
7
+ version = "0.11.4.dev5"
8
8
  description = "III SDK for Python"
9
9
  authors = [{ name = "III" }]
10
10
  license = { text = "Apache-2.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
8
+ from pydantic import BaseModel, Field
9
9
 
10
10
  TData = TypeVar("TData")
11
11
 
@@ -91,6 +91,20 @@ class StreamUpdateInput(BaseModel):
91
91
  ops: list["UpdateOp"]
92
92
 
93
93
 
94
+ class UpdateOpError(BaseModel):
95
+ """Per-op error returned by ``state::update`` / ``stream::update``.
96
+
97
+ Currently emitted only by the ``merge`` op when input violates the
98
+ new validation bounds. Successfully applied ops are still
99
+ reflected in the response's ``new_value``.
100
+ """
101
+
102
+ op_index: int
103
+ code: str
104
+ message: str
105
+ doc_url: str | None = None
106
+
107
+
94
108
  class StreamSetResult(BaseModel, Generic[TData]):
95
109
  """Result of stream set operation."""
96
110
 
@@ -103,6 +117,12 @@ class StreamUpdateResult(BaseModel, Generic[TData]):
103
117
 
104
118
  old_value: TData | None = None
105
119
  new_value: TData
120
+ # Per-op errors. Currently emitted only by the ``merge`` op for
121
+ # validation rejections. Field is omitted from the JSON wire when
122
+ # empty. ``default_factory`` is used (not ``= []``) to keep
123
+ # Pydantic's parameterized-Generic + default handling well-behaved
124
+ # across Python versions.
125
+ errors: list[UpdateOpError] = Field(default_factory=list)
106
126
 
107
127
 
108
128
  class StreamDeleteResult(BaseModel):
@@ -151,13 +171,38 @@ class UpdateRemove(BaseModel):
151
171
 
152
172
 
153
173
  class UpdateMerge(BaseModel):
154
- """Shallow root-level merge operation for stream update.
155
-
156
- Only an empty path is supported. Non-empty paths are ignored by the engine.
174
+ """Shallow merge an object into the target.
175
+
176
+ The target is the root (when ``path`` is omitted, an empty string,
177
+ or an empty list) or an arbitrary nested location specified by an
178
+ array of literal segments.
179
+
180
+ Path forms accepted:
181
+ - ``None`` / ``""`` / ``[]``: merge at the root.
182
+ - ``"foo"``: equivalent to ``["foo"]`` -- single first-level key.
183
+ - ``["a", "b", "c"]``: nested path. Each element is a *literal*
184
+ key. ``["a.b"]`` writes a single key named ``"a.b"``, not
185
+ ``a -> b``.
186
+
187
+ Engine semantics:
188
+ - Missing or non-object intermediates along the path are
189
+ auto-replaced with ``{}``.
190
+ - The merge is shallow at the target node (top-level keys of
191
+ ``value`` overwrite same-named keys; siblings preserved).
192
+
193
+ Validation: invalid paths/values (depth > 32 segments, segment >
194
+ 256 bytes, value depth > 16, > 1024 top-level keys, or any
195
+ ``__proto__`` / ``constructor`` / ``prototype`` segment or
196
+ top-level key) are rejected with a structured error in the
197
+ ``errors`` array of the ``state::update`` / ``stream::update``
198
+ response. The merge does not apply when an error is returned.
157
199
  """
158
200
 
159
201
  type: str = "merge"
160
- path: str
202
+ # Optional. Accepts a single string or a list of literal segments.
203
+ # Pydantic resolves ``str | list[str]`` via smart-union: string
204
+ # input -> str, array input -> list[str].
205
+ path: str | list[str] | None = None
161
206
  value: Any
162
207
 
163
208
 
@@ -0,0 +1,73 @@
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://docs.iii.dev/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"
@@ -1,9 +0,0 @@
1
- """Unit tests for stream model serialization."""
2
-
3
- from iii.stream import UpdateAppend
4
-
5
-
6
- def test_update_append_model_serializes() -> None:
7
- op = UpdateAppend(path="chunks", value={"text": "hello"})
8
-
9
- assert op.model_dump() == {"type": "append", "path": "chunks", "value": {"text": "hello"}}
File without changes
File without changes
File without changes