iii-sdk 0.11.3.dev2__tar.gz → 0.11.4__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.3.dev2 → iii_sdk-0.11.4}/PKG-INFO +8 -8
  2. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/README.md +5 -5
  3. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/pyproject.toml +3 -3
  4. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/src/iii/__init__.py +0 -2
  5. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/src/iii/iii.py +48 -262
  6. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/src/iii/stream.py +60 -4
  7. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/src/iii/types.py +1 -17
  8. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/tests/test_api_triggers.py +46 -10
  9. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/tests/test_async_api.py +0 -18
  10. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/tests/test_bridge.py +24 -18
  11. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/tests/test_context_propagation.py +18 -14
  12. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/tests/test_data_channels.py +37 -29
  13. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/tests/test_healthcheck.py +1 -1
  14. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/tests/test_http_external_functions_integration.py +30 -16
  15. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/tests/test_iii_registration_dedup.py +2 -2
  16. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/tests/test_middleware.py +10 -10
  17. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/tests/test_pubsub.py +3 -3
  18. iii_sdk-0.11.4/tests/test_queue_integration.py +363 -0
  19. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/tests/test_rbac_workers.py +30 -17
  20. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/tests/test_register_function_args.py +57 -71
  21. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/tests/test_state.py +126 -6
  22. iii_sdk-0.11.4/tests/test_stream_models.py +73 -0
  23. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/tests/test_streams.py +175 -7
  24. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/tests/test_sync_api.py +13 -11
  25. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/uv.lock +1 -1
  26. iii_sdk-0.11.3.dev2/tests/test_queue_integration.py +0 -136
  27. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/.gitignore +0 -0
  28. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/src/iii/channels.py +0 -0
  29. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/src/iii/errors.py +0 -0
  30. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/src/iii/format_utils.py +0 -0
  31. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/src/iii/iii_constants.py +0 -0
  32. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/src/iii/iii_types.py +0 -0
  33. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/src/iii/logger.py +0 -0
  34. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/src/iii/otel_worker_gauges.py +0 -0
  35. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/src/iii/state.py +0 -0
  36. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/src/iii/telemetry.py +0 -0
  37. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/src/iii/telemetry_exporters.py +0 -0
  38. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/src/iii/telemetry_types.py +0 -0
  39. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/src/iii/triggers.py +0 -0
  40. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/src/iii/utils.py +0 -0
  41. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/src/iii/worker_metrics.py +0 -0
  42. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/tests/conftest.py +0 -0
  43. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/tests/test_channel_close_delay.py +0 -0
  44. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/tests/test_errors.py +0 -0
  45. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/tests/test_format_utils.py +0 -0
  46. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/tests/test_hold_process.py +0 -0
  47. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/tests/test_init_api.py +0 -0
  48. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/tests/test_invocation_exception.py +0 -0
  49. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/tests/test_logger_function_ids.py +0 -0
  50. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/tests/test_logger_otel.py +0 -0
  51. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/tests/test_streams_runtime_annotations.py +0 -0
  52. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/tests/test_telemetry.py +0 -0
  53. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/tests/test_telemetry_exporters.py +0 -0
  54. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/tests/test_telemetry_types.py +0 -0
  55. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/tests/test_trace_helpers.py +0 -0
  56. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/tests/test_trigger_metadata.py +0 -0
  57. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/tests/test_utils.py +0 -0
  58. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/tests/test_worker_metadata.py +0 -0
  59. {iii_sdk-0.11.3.dev2 → iii_sdk-0.11.4}/tests/test_worker_metrics.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iii-sdk
3
- Version: 0.11.3.dev2
3
+ Version: 0.11.4
4
4
  Summary: III SDK for Python
5
- Project-URL: Homepage, https://github.com/iii-hq/sdk
6
- Project-URL: Repository, https://github.com/iii-hq/sdk
5
+ Project-URL: Homepage, https://github.com/iii-hq/iii
6
+ Project-URL: Repository, https://github.com/iii-hq/iii
7
7
  Author: III
8
8
  License: Apache-2.0
9
9
  Keywords: iii,sdk
@@ -53,7 +53,7 @@ iii = register_worker("ws://localhost:49134")
53
53
  def greet(data):
54
54
  return {"message": f"Hello, {data['name']}!"}
55
55
 
56
- iii.register_function({"id": "greet"}, greet)
56
+ iii.register_function("greet", greet)
57
57
 
58
58
  iii.register_trigger({
59
59
  "type": "http",
@@ -72,7 +72,7 @@ print(result) # {"message": "Hello, world!"}
72
72
  | Operation | Signature | Description |
73
73
  | ------------------------ | ------------------------------------------------- | ------------------------------------------------------ |
74
74
  | Initialize | `register_worker(url, options?)` | Create an SDK instance and auto-connect |
75
- | Register function | `iii.register_function({"id": id}, handler)` | Register a function that can be invoked by name |
75
+ | Register function | `iii.register_function(id, handler)` | Register a function that can be invoked by name |
76
76
  | Register trigger | `iii.register_trigger({"type": ..., "function_id": ..., "config": ...})` | Bind a trigger (HTTP, cron, queue, etc.) to a function |
77
77
  | Invoke (await result) | `iii.trigger({"function_id": id, "payload": data})` | Invoke a function and wait for the result |
78
78
  | Invoke (fire-and-forget) | `iii.trigger({"function_id": id, ..., "action": TriggerAction.Void()})` | Fire-and-forget |
@@ -86,7 +86,7 @@ print(result) # {"message": "Hello, world!"}
86
86
  def create_order(data):
87
87
  return {"status_code": 201, "body": {"id": "123", "item": data["body"]["item"]}}
88
88
 
89
- iii.register_function({"id": "orders.create"}, create_order)
89
+ iii.register_function("orders::create", create_order)
90
90
  ```
91
91
 
92
92
  ### Registering Triggers
@@ -94,7 +94,7 @@ iii.register_function({"id": "orders.create"}, create_order)
94
94
  ```python
95
95
  iii.register_trigger({
96
96
  "type": "http",
97
- "function_id": "orders.create",
97
+ "function_id": "orders::create",
98
98
  "config": {"api_path": "/orders", "http_method": "POST"},
99
99
  })
100
100
  ```
@@ -102,7 +102,7 @@ iii.register_trigger({
102
102
  ### Invoking Functions
103
103
 
104
104
  ```python
105
- result = iii.trigger({"function_id": "orders.create", "payload": {"body": {"item": "widget"}}})
105
+ result = iii.trigger({"function_id": "orders::create", "payload": {"body": {"item": "widget"}}})
106
106
  ```
107
107
 
108
108
  ## Modules
@@ -22,7 +22,7 @@ iii = register_worker("ws://localhost:49134")
22
22
  def greet(data):
23
23
  return {"message": f"Hello, {data['name']}!"}
24
24
 
25
- iii.register_function({"id": "greet"}, greet)
25
+ iii.register_function("greet", greet)
26
26
 
27
27
  iii.register_trigger({
28
28
  "type": "http",
@@ -41,7 +41,7 @@ print(result) # {"message": "Hello, world!"}
41
41
  | Operation | Signature | Description |
42
42
  | ------------------------ | ------------------------------------------------- | ------------------------------------------------------ |
43
43
  | Initialize | `register_worker(url, options?)` | Create an SDK instance and auto-connect |
44
- | Register function | `iii.register_function({"id": id}, handler)` | Register a function that can be invoked by name |
44
+ | Register function | `iii.register_function(id, handler)` | Register a function that can be invoked by name |
45
45
  | Register trigger | `iii.register_trigger({"type": ..., "function_id": ..., "config": ...})` | Bind a trigger (HTTP, cron, queue, etc.) to a function |
46
46
  | Invoke (await result) | `iii.trigger({"function_id": id, "payload": data})` | Invoke a function and wait for the result |
47
47
  | Invoke (fire-and-forget) | `iii.trigger({"function_id": id, ..., "action": TriggerAction.Void()})` | Fire-and-forget |
@@ -55,7 +55,7 @@ print(result) # {"message": "Hello, world!"}
55
55
  def create_order(data):
56
56
  return {"status_code": 201, "body": {"id": "123", "item": data["body"]["item"]}}
57
57
 
58
- iii.register_function({"id": "orders.create"}, create_order)
58
+ iii.register_function("orders::create", create_order)
59
59
  ```
60
60
 
61
61
  ### Registering Triggers
@@ -63,7 +63,7 @@ iii.register_function({"id": "orders.create"}, create_order)
63
63
  ```python
64
64
  iii.register_trigger({
65
65
  "type": "http",
66
- "function_id": "orders.create",
66
+ "function_id": "orders::create",
67
67
  "config": {"api_path": "/orders", "http_method": "POST"},
68
68
  })
69
69
  ```
@@ -71,7 +71,7 @@ iii.register_trigger({
71
71
  ### Invoking Functions
72
72
 
73
73
  ```python
74
- result = iii.trigger({"function_id": "orders.create", "payload": {"body": {"item": "widget"}}})
74
+ result = iii.trigger({"function_id": "orders::create", "payload": {"body": {"item": "widget"}}})
75
75
  ```
76
76
 
77
77
  ## Modules
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "iii-sdk"
7
- version = "0.11.3.dev2"
7
+ version = "0.11.4"
8
8
  description = "III SDK for Python"
9
9
  authors = [{ name = "III" }]
10
10
  license = { text = "Apache-2.0" }
@@ -27,8 +27,8 @@ dependencies = [
27
27
  ]
28
28
 
29
29
  [project.urls]
30
- Homepage = "https://github.com/iii-hq/sdk"
31
- Repository = "https://github.com/iii-hq/sdk"
30
+ Homepage = "https://github.com/iii-hq/iii"
31
+ Repository = "https://github.com/iii-hq/iii"
32
32
 
33
33
  [project.optional-dependencies]
34
34
  dev = [
@@ -21,7 +21,6 @@ from .iii_types import (
21
21
  OnTriggerTypeRegistrationInput,
22
22
  OnTriggerTypeRegistrationResult,
23
23
  RegisterFunctionFormat,
24
- RegisterFunctionInput,
25
24
  RegisterFunctionMessage,
26
25
  RegisterServiceInput,
27
26
  RegisterTriggerInput,
@@ -92,7 +91,6 @@ __all__ = [
92
91
  "HttpInvocationConfig",
93
92
  "MessageType",
94
93
  "RegisterFunctionFormat",
95
- "RegisterFunctionInput",
96
94
  "RegisterFunctionMessage",
97
95
  "RegisterServiceInput",
98
96
  "RegisterTriggerInput",
@@ -27,7 +27,6 @@ from .iii_constants import (
27
27
  InitOptions,
28
28
  )
29
29
  from .iii_types import (
30
- FunctionInfo,
31
30
  HttpInvocationConfig,
32
31
  InvocationResultMessage,
33
32
  InvokeFunctionMessage,
@@ -44,13 +43,10 @@ from .iii_types import (
44
43
  StreamChannelRef,
45
44
  TriggerActionEnqueue,
46
45
  TriggerActionVoid,
47
- TriggerInfo,
48
46
  TriggerRequest,
49
- TriggerTypeInfo,
50
47
  UnregisterFunctionMessage,
51
48
  UnregisterTriggerMessage,
52
49
  UnregisterTriggerTypeMessage,
53
- WorkerInfo,
54
50
  )
55
51
  from .stream import (
56
52
  IStream,
@@ -128,11 +124,6 @@ class III:
128
124
  self._reconnect_task: asyncio.Task[None] | None = None
129
125
  self._running = False
130
126
  self._receiver_task: asyncio.Task[None] | None = None
131
- self._functions_available_callbacks: set[
132
- Callable[[list[FunctionInfo]], None]
133
- ] = set()
134
- self._functions_available_trigger: Trigger | None = None
135
- self._functions_available_function_id: str | None = None
136
127
  self._reconnection_config = (
137
128
  self._options.reconnection_config or DEFAULT_RECONNECTION_CONFIG
138
129
  )
@@ -796,7 +787,7 @@ class III:
796
787
 
797
788
  def register_function(
798
789
  self,
799
- func_or_id: RegisterFunctionInput | dict[str, Any] | str,
790
+ function_id: str,
800
791
  handler_or_invocation: RemoteFunctionHandler | HttpInvocationConfig,
801
792
  *,
802
793
  description: str | None = None,
@@ -814,27 +805,28 @@ class III:
814
805
  block the event loop. Each handler receives a single ``data``
815
806
  argument containing the trigger payload.
816
807
 
817
- When ``func_or_id`` is a ``str``, the simplified API is used:
818
808
  ``request_format`` and ``response_format`` are auto-extracted
819
- from the handler's type hints when not explicitly provided.
809
+ from the handler's type hints when omitted or passed as ``None``
810
+ (the default). To opt out of auto-extraction, pass an explicit
811
+ schema (``RegisterFunctionFormat`` or ``dict``). This behavior
812
+ is Python-specific -- the Node SDK does not auto-extract from TS
813
+ types, because TypeScript types are erased at runtime.
820
814
 
821
815
  Args:
822
- func_or_id: A ``RegisterFunctionInput``, dict with ``id``, or
823
- a plain string function ID. When a string is passed, use
824
- keyword arguments for ``description``, ``metadata``,
825
- ``request_format``, and ``response_format``.
816
+ function_id: Unique string identifier for the function.
826
817
  handler_or_invocation: A callable handler or
827
818
  ``HttpInvocationConfig``. Callable handlers receive one
828
819
  positional argument (``data`` -- the trigger payload) and
829
820
  may return a value.
830
- description: Human-readable description (only with string ID).
831
- metadata: Arbitrary metadata (only with string ID).
832
- request_format: Schema describing expected input (only with
833
- string ID). Auto-extracted from handler type hints when
834
- omitted.
835
- response_format: Schema describing expected output (only with
836
- string ID). Auto-extracted from handler type hints when
837
- omitted.
821
+ description: Human-readable description.
822
+ metadata: Arbitrary metadata.
823
+ request_format: Schema describing expected input. When
824
+ ``None`` (default), auto-extracted from the handler's
825
+ first-parameter type hint. Pass an explicit schema to
826
+ override; there is no way to register with no schema
827
+ when the handler is typed.
828
+ response_format: Schema describing expected output. Same
829
+ auto-extraction semantics as ``request_format``.
838
830
 
839
831
  Returns:
840
832
  A ``FunctionRef`` with an ``id`` attribute and an
@@ -842,14 +834,15 @@ class III:
842
834
  the function from the engine.
843
835
 
844
836
  Raises:
845
- ValueError: If ``id`` is empty or already registered.
846
- TypeError: If ``handler_or_invocation`` is not callable or
837
+ TypeError: If ``function_id`` is not a string, or if
838
+ ``handler_or_invocation`` is not callable or
847
839
  ``HttpInvocationConfig``.
840
+ ValueError: If ``function_id`` is empty or already registered.
848
841
 
849
842
  Examples:
850
843
  >>> def greet(data):
851
844
  ... return {'message': f"Hello, {data['name']}!"}
852
- >>> fn = iii.register_function({"id": "greet", "description": "Greets a user"}, greet)
845
+ >>> fn = iii.register_function("greet", greet, description="Greets a user")
853
846
  >>> fn.unregister()
854
847
 
855
848
  >>> from pydantic import BaseModel
@@ -861,31 +854,29 @@ class III:
861
854
  ... return GreetOutput(message=f"Hello, {data.name}!")
862
855
  >>> fn = iii.register_function("greet", greet, description="Greets a user")
863
856
  """
864
- if isinstance(func_or_id, str):
865
- # Simplified API: auto-extract formats from handler type hints
866
- handler_for_extraction = (
867
- handler_or_invocation if callable(handler_or_invocation) else None
868
- )
869
- if request_format is None and handler_for_extraction is not None:
870
- request_format = extract_request_format(handler_for_extraction)
871
- if response_format is None and handler_for_extraction is not None:
872
- response_format = extract_response_format(handler_for_extraction)
873
- func = RegisterFunctionInput(
874
- id=func_or_id,
875
- description=description,
876
- metadata=metadata,
877
- request_format=request_format,
878
- response_format=response_format,
857
+ if not isinstance(function_id, str):
858
+ raise TypeError(
859
+ f"function_id must be str, got {type(function_id).__name__}"
879
860
  )
880
- elif isinstance(func_or_id, dict):
881
- func = RegisterFunctionInput(**func_or_id)
882
- else:
883
- func = func_or_id
884
-
885
- if not func.id or not func.id.strip():
861
+ if not function_id or not function_id.strip():
886
862
  raise ValueError("id is required")
887
- if func.id in self._functions:
888
- raise ValueError(f"function id '{func.id}' already registered")
863
+ if function_id in self._functions:
864
+ raise ValueError(f"function id '{function_id}' already registered")
865
+
866
+ handler_for_extraction = (
867
+ handler_or_invocation if callable(handler_or_invocation) else None
868
+ )
869
+ if request_format is None and handler_for_extraction is not None:
870
+ request_format = extract_request_format(handler_for_extraction)
871
+ if response_format is None and handler_for_extraction is not None:
872
+ response_format = extract_response_format(handler_for_extraction)
873
+ func = RegisterFunctionInput(
874
+ id=function_id,
875
+ description=description,
876
+ metadata=metadata,
877
+ request_format=request_format,
878
+ response_format=response_format,
879
+ )
889
880
 
890
881
  if isinstance(handler_or_invocation, HttpInvocationConfig):
891
882
  msg = RegisterFunctionMessage(
@@ -1096,148 +1087,6 @@ class III:
1096
1087
  invocation_id=invocation_id,
1097
1088
  )
1098
1089
 
1099
- def list_functions(self) -> list[FunctionInfo]:
1100
- """List all functions registered with the engine across all workers.
1101
-
1102
- Returns:
1103
- A list of ``FunctionInfo`` objects describing each function.
1104
-
1105
- Examples:
1106
- >>> for fn in iii.list_functions():
1107
- ... print(fn.function_id, fn.description)
1108
- """
1109
- return self._run_on_loop(self.list_functions_async())
1110
-
1111
- async def list_functions_async(self) -> list[FunctionInfo]:
1112
- """List all functions registered with the engine across all workers.
1113
-
1114
- Returns:
1115
- A list of ``FunctionInfo`` objects describing each function.
1116
-
1117
- Examples:
1118
- >>> for fn in await iii.list_functions_async():
1119
- ... print(fn.function_id, fn.description)
1120
- """
1121
- result = await self.trigger_async(
1122
- {"function_id": "engine::functions::list", "payload": {}}
1123
- )
1124
- functions_data = result.get("functions", [])
1125
- return [FunctionInfo(**f) for f in functions_data]
1126
-
1127
- def list_workers(self) -> list[WorkerInfo]:
1128
- """List all workers currently connected to the engine.
1129
-
1130
- Returns:
1131
- A list of ``WorkerInfo`` objects with worker metadata.
1132
-
1133
- Examples:
1134
- >>> for w in iii.list_workers():
1135
- ... print(w.name, w.worker_id)
1136
- """
1137
- return self._run_on_loop(self.list_workers_async())
1138
-
1139
- async def list_workers_async(self) -> list[WorkerInfo]:
1140
- """List all workers currently connected to the engine.
1141
-
1142
- Returns:
1143
- A list of ``WorkerInfo`` objects with worker metadata.
1144
-
1145
- Examples:
1146
- >>> for w in await iii.list_workers_async():
1147
- ... print(w.name, w.worker_id)
1148
- """
1149
- result = await self.trigger_async(
1150
- {"function_id": "engine::workers::list", "payload": {}}
1151
- )
1152
- workers_data = result.get("workers", [])
1153
- return [WorkerInfo(**w) for w in workers_data]
1154
-
1155
- def list_triggers(self, include_internal: bool = False) -> list[TriggerInfo]:
1156
- """List all triggers registered with the engine.
1157
-
1158
- Args:
1159
- include_internal: If ``True``, include engine-internal triggers
1160
- (e.g. ``functions-available``). Defaults to ``False``.
1161
-
1162
- Returns:
1163
- A list of ``TriggerInfo`` objects.
1164
-
1165
- Examples:
1166
- >>> triggers = iii.list_triggers()
1167
- >>> internal = iii.list_triggers(include_internal=True)
1168
- """
1169
- return self._run_on_loop(self.list_triggers_async(include_internal))
1170
-
1171
- async def list_triggers_async(
1172
- self, include_internal: bool = False
1173
- ) -> list[TriggerInfo]:
1174
- """List all triggers registered with the engine.
1175
-
1176
- Args:
1177
- include_internal: If ``True``, include engine-internal triggers
1178
- (e.g. ``functions-available``). Defaults to ``False``.
1179
-
1180
- Returns:
1181
- A list of ``TriggerInfo`` objects.
1182
-
1183
- Examples:
1184
- >>> triggers = await iii.list_triggers_async()
1185
- >>> internal = await iii.list_triggers_async(include_internal=True)
1186
- """
1187
- result = await self.trigger_async(
1188
- {
1189
- "function_id": "engine::triggers::list",
1190
- "payload": {"include_internal": include_internal},
1191
- }
1192
- )
1193
- triggers_data = result.get("triggers", [])
1194
- return [TriggerInfo(**t) for t in triggers_data]
1195
-
1196
- def list_trigger_types(
1197
- self, include_internal: bool = False
1198
- ) -> list[TriggerTypeInfo]:
1199
- """List all trigger types registered with the engine.
1200
-
1201
- Args:
1202
- include_internal: If ``True``, include engine-internal trigger
1203
- types (e.g. ``engine::functions-available``). Defaults to ``False``.
1204
-
1205
- Returns:
1206
- A list of ``TriggerTypeInfo`` objects with ``trigger_request_format``
1207
- and ``call_request_format`` schemas.
1208
-
1209
- Examples:
1210
- >>> trigger_types = iii.list_trigger_types()
1211
- >>> for tt in trigger_types:
1212
- ... print(tt.id, tt.trigger_request_format)
1213
- """
1214
- return self._run_on_loop(self.list_trigger_types_async(include_internal))
1215
-
1216
- async def list_trigger_types_async(
1217
- self, include_internal: bool = False
1218
- ) -> list[TriggerTypeInfo]:
1219
- """List all trigger types registered with the engine.
1220
-
1221
- Args:
1222
- include_internal: If ``True``, include engine-internal trigger
1223
- types (e.g. ``engine::functions-available``). Defaults to ``False``.
1224
-
1225
- Returns:
1226
- A list of ``TriggerTypeInfo`` objects with ``trigger_request_format``
1227
- and ``call_request_format`` schemas.
1228
-
1229
- Examples:
1230
- >>> trigger_types = await iii.list_trigger_types_async()
1231
- """
1232
- result = await self.trigger_async(
1233
- {
1234
- "function_id": "engine::trigger-types::list",
1235
- "payload": {"include_internal": include_internal},
1236
- }
1237
- )
1238
- types_data = result.get("trigger_types", [])
1239
- return [TriggerTypeInfo(**t) for t in types_data]
1240
-
1241
1090
  def create_channel(self, buffer_size: int | None = None) -> Channel:
1242
1091
  """Create a streaming channel pair for worker-to-worker data transfer.
1243
1092
 
@@ -1258,7 +1107,7 @@ class III:
1258
1107
 
1259
1108
  Examples:
1260
1109
  >>> ch = iii.create_channel()
1261
- >>> fn = iii.register_function({"id": "producer"}, producer_handler)
1110
+ >>> fn = iii.register_function("producer", producer_handler)
1262
1111
  >>> iii.trigger({"function_id": "producer", "payload": {"output": ch.writer_ref}})
1263
1112
  """
1264
1113
  return self._run_on_loop(self.create_channel_async(buffer_size))
@@ -1279,7 +1128,7 @@ class III:
1279
1128
 
1280
1129
  Examples:
1281
1130
  >>> ch = await iii.create_channel_async()
1282
- >>> fn = iii.register_function({"id": "producer"}, producer_handler)
1131
+ >>> fn = iii.register_function("producer", producer_handler)
1283
1132
  >>> await iii.trigger_async({"function_id": "producer", "payload": {"output": ch.writer_ref}})
1284
1133
  """
1285
1134
  result = await self.trigger_async(
@@ -1341,69 +1190,6 @@ class III:
1341
1190
  )
1342
1191
  asyncio.run_coroutine_threadsafe(self._send(msg), self._loop)
1343
1192
 
1344
- def on_functions_available(
1345
- self, callback: Callable[[list[FunctionInfo]], None]
1346
- ) -> Callable[[], None]:
1347
- """Subscribe to function-availability events from the engine.
1348
-
1349
- The callback fires whenever the set of available functions changes
1350
- (e.g. a new worker connects or a function is unregistered).
1351
-
1352
- Args:
1353
- callback (Callable[[list[FunctionInfo]], None]): Receives the
1354
- current list of ``FunctionInfo`` objects each time
1355
- availability changes.
1356
-
1357
- Returns:
1358
- A callable that unsubscribes when called. Calling the
1359
- returned function removes the callback and, if no callbacks
1360
- remain, tears down the internal trigger.
1361
-
1362
- Examples:
1363
- >>> def on_change(functions):
1364
- ... print("Available:", [f.function_id for f in functions])
1365
- >>> unsub = iii.on_functions_available(on_change)
1366
- >>> # later ...
1367
- >>> unsub()
1368
- """
1369
- self._functions_available_callbacks.add(callback)
1370
-
1371
- if not self._functions_available_trigger:
1372
- if not self._functions_available_function_id:
1373
- self._functions_available_function_id = (
1374
- f"iii.on_functions_available.{uuid.uuid4()}"
1375
- )
1376
-
1377
- function_id = self._functions_available_function_id
1378
- if function_id not in self._functions:
1379
-
1380
- async def handler(data: dict[str, Any]) -> None:
1381
- functions_data = data.get("functions", [])
1382
- functions = [FunctionInfo(**f) for f in functions_data]
1383
- for cb in list(self._functions_available_callbacks):
1384
- cb(functions)
1385
-
1386
- self.register_function({"id": function_id}, handler)
1387
-
1388
- self._functions_available_trigger = self.register_trigger(
1389
- {
1390
- "type": "engine::functions-available",
1391
- "function_id": function_id,
1392
- "config": {},
1393
- }
1394
- )
1395
-
1396
- def unsubscribe() -> None:
1397
- self._functions_available_callbacks.discard(callback)
1398
- if (
1399
- len(self._functions_available_callbacks) == 0
1400
- and self._functions_available_trigger
1401
- ):
1402
- self._functions_available_trigger.unregister()
1403
- self._functions_available_trigger = None
1404
-
1405
- return unsubscribe
1406
-
1407
1193
  def create_stream(self, stream_name: str, stream: IStream[Any]) -> None:
1408
1194
  """Register a custom stream implementation, overriding the engine default.
1409
1195
 
@@ -1451,12 +1237,12 @@ class III:
1451
1237
  )
1452
1238
  return await stream.list_groups(input_data)
1453
1239
 
1454
- self.register_function({"id": f"stream::get({stream_name})"}, get_handler)
1455
- self.register_function({"id": f"stream::set({stream_name})"}, set_handler)
1456
- self.register_function({"id": f"stream::delete({stream_name})"}, delete_handler)
1457
- self.register_function({"id": f"stream::list({stream_name})"}, list_handler)
1240
+ self.register_function(f"stream::get({stream_name})", get_handler)
1241
+ self.register_function(f"stream::set({stream_name})", set_handler)
1242
+ self.register_function(f"stream::delete({stream_name})", delete_handler)
1243
+ self.register_function(f"stream::list({stream_name})", list_handler)
1458
1244
  self.register_function(
1459
- {"id": f"stream::list_groups({stream_name})"}, list_groups_handler
1245
+ f"stream::list_groups({stream_name})", list_groups_handler
1460
1246
  )
1461
1247
 
1462
1248
 
@@ -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):
@@ -135,6 +155,14 @@ class UpdateDecrement(BaseModel):
135
155
  by: int | float
136
156
 
137
157
 
158
+ class UpdateAppend(BaseModel):
159
+ """Append operation for stream update."""
160
+
161
+ type: str = "append"
162
+ path: str
163
+ value: Any
164
+
165
+
138
166
  class UpdateRemove(BaseModel):
139
167
  """Remove operation for stream update."""
140
168
 
@@ -143,14 +171,42 @@ class UpdateRemove(BaseModel):
143
171
 
144
172
 
145
173
  class UpdateMerge(BaseModel):
146
- """Merge operation for stream update."""
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.
199
+ """
147
200
 
148
201
  type: str = "merge"
149
- 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
150
206
  value: Any
151
207
 
152
208
 
153
- UpdateOp = UpdateSet | UpdateIncrement | UpdateDecrement | UpdateRemove | UpdateMerge
209
+ UpdateOp = UpdateSet | UpdateIncrement | UpdateDecrement | UpdateAppend | UpdateRemove | UpdateMerge
154
210
 
155
211
 
156
212
  class StreamTriggerConfig(BaseModel):