iii-sdk 0.11.4.dev2__tar.gz → 0.11.4.dev3__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 (58) hide show
  1. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/PKG-INFO +6 -6
  2. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/README.md +5 -5
  3. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/pyproject.toml +1 -1
  4. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/src/iii/__init__.py +0 -2
  5. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/src/iii/iii.py +48 -48
  6. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/src/iii/stream.py +13 -2
  7. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/src/iii/types.py +1 -2
  8. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/test_api_triggers.py +11 -11
  9. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/test_bridge.py +20 -20
  10. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/test_context_propagation.py +18 -14
  11. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/test_data_channels.py +37 -29
  12. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/test_healthcheck.py +1 -1
  13. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/test_http_external_functions_integration.py +30 -16
  14. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/test_iii_registration_dedup.py +2 -2
  15. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/test_middleware.py +10 -10
  16. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/test_pubsub.py +3 -3
  17. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/test_queue_integration.py +49 -44
  18. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/test_rbac_workers.py +12 -11
  19. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/test_register_function_args.py +57 -71
  20. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/test_state.py +55 -6
  21. iii_sdk-0.11.4.dev3/tests/test_stream_models.py +9 -0
  22. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/test_streams.py +61 -1
  23. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/test_sync_api.py +11 -8
  24. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/uv.lock +1 -1
  25. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/.gitignore +0 -0
  26. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/src/iii/channels.py +0 -0
  27. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/src/iii/errors.py +0 -0
  28. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/src/iii/format_utils.py +0 -0
  29. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/src/iii/iii_constants.py +0 -0
  30. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/src/iii/iii_types.py +0 -0
  31. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/src/iii/logger.py +0 -0
  32. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/src/iii/otel_worker_gauges.py +0 -0
  33. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/src/iii/state.py +0 -0
  34. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/src/iii/telemetry.py +0 -0
  35. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/src/iii/telemetry_exporters.py +0 -0
  36. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/src/iii/telemetry_types.py +0 -0
  37. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/src/iii/triggers.py +0 -0
  38. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/src/iii/utils.py +0 -0
  39. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/src/iii/worker_metrics.py +0 -0
  40. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/conftest.py +0 -0
  41. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/test_async_api.py +0 -0
  42. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/test_channel_close_delay.py +0 -0
  43. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/test_errors.py +0 -0
  44. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/test_format_utils.py +0 -0
  45. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/test_hold_process.py +0 -0
  46. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/test_init_api.py +0 -0
  47. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/test_invocation_exception.py +0 -0
  48. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/test_logger_function_ids.py +0 -0
  49. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/test_logger_otel.py +0 -0
  50. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/test_streams_runtime_annotations.py +0 -0
  51. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/test_telemetry.py +0 -0
  52. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/test_telemetry_exporters.py +0 -0
  53. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/test_telemetry_types.py +0 -0
  54. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/test_trace_helpers.py +0 -0
  55. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/test_trigger_metadata.py +0 -0
  56. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/test_utils.py +0 -0
  57. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/test_worker_metadata.py +0 -0
  58. {iii_sdk-0.11.4.dev2 → iii_sdk-0.11.4.dev3}/tests/test_worker_metrics.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iii-sdk
3
- Version: 0.11.4.dev2
3
+ Version: 0.11.4.dev3
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
@@ -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.4.dev2"
7
+ version = "0.11.4.dev3"
8
8
  description = "III SDK for Python"
9
9
  authors = [{ name = "III" }]
10
10
  license = { text = "Apache-2.0" }
@@ -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",
@@ -787,7 +787,7 @@ class III:
787
787
 
788
788
  def register_function(
789
789
  self,
790
- func_or_id: RegisterFunctionInput | dict[str, Any] | str,
790
+ function_id: str,
791
791
  handler_or_invocation: RemoteFunctionHandler | HttpInvocationConfig,
792
792
  *,
793
793
  description: str | None = None,
@@ -805,27 +805,28 @@ class III:
805
805
  block the event loop. Each handler receives a single ``data``
806
806
  argument containing the trigger payload.
807
807
 
808
- When ``func_or_id`` is a ``str``, the simplified API is used:
809
808
  ``request_format`` and ``response_format`` are auto-extracted
810
- 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.
811
814
 
812
815
  Args:
813
- func_or_id: A ``RegisterFunctionInput``, dict with ``id``, or
814
- a plain string function ID. When a string is passed, use
815
- keyword arguments for ``description``, ``metadata``,
816
- ``request_format``, and ``response_format``.
816
+ function_id: Unique string identifier for the function.
817
817
  handler_or_invocation: A callable handler or
818
818
  ``HttpInvocationConfig``. Callable handlers receive one
819
819
  positional argument (``data`` -- the trigger payload) and
820
820
  may return a value.
821
- description: Human-readable description (only with string ID).
822
- metadata: Arbitrary metadata (only with string ID).
823
- request_format: Schema describing expected input (only with
824
- string ID). Auto-extracted from handler type hints when
825
- omitted.
826
- response_format: Schema describing expected output (only with
827
- string ID). Auto-extracted from handler type hints when
828
- 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``.
829
830
 
830
831
  Returns:
831
832
  A ``FunctionRef`` with an ``id`` attribute and an
@@ -833,14 +834,15 @@ class III:
833
834
  the function from the engine.
834
835
 
835
836
  Raises:
836
- ValueError: If ``id`` is empty or already registered.
837
- 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
838
839
  ``HttpInvocationConfig``.
840
+ ValueError: If ``function_id`` is empty or already registered.
839
841
 
840
842
  Examples:
841
843
  >>> def greet(data):
842
844
  ... return {'message': f"Hello, {data['name']}!"}
843
- >>> fn = iii.register_function({"id": "greet", "description": "Greets a user"}, greet)
845
+ >>> fn = iii.register_function("greet", greet, description="Greets a user")
844
846
  >>> fn.unregister()
845
847
 
846
848
  >>> from pydantic import BaseModel
@@ -852,31 +854,29 @@ class III:
852
854
  ... return GreetOutput(message=f"Hello, {data.name}!")
853
855
  >>> fn = iii.register_function("greet", greet, description="Greets a user")
854
856
  """
855
- if isinstance(func_or_id, str):
856
- # Simplified API: auto-extract formats from handler type hints
857
- handler_for_extraction = (
858
- handler_or_invocation if callable(handler_or_invocation) else None
859
- )
860
- if request_format is None and handler_for_extraction is not None:
861
- request_format = extract_request_format(handler_for_extraction)
862
- if response_format is None and handler_for_extraction is not None:
863
- response_format = extract_response_format(handler_for_extraction)
864
- func = RegisterFunctionInput(
865
- id=func_or_id,
866
- description=description,
867
- metadata=metadata,
868
- request_format=request_format,
869
- 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__}"
870
860
  )
871
- elif isinstance(func_or_id, dict):
872
- func = RegisterFunctionInput(**func_or_id)
873
- else:
874
- func = func_or_id
875
-
876
- if not func.id or not func.id.strip():
861
+ if not function_id or not function_id.strip():
877
862
  raise ValueError("id is required")
878
- if func.id in self._functions:
879
- 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
+ )
880
880
 
881
881
  if isinstance(handler_or_invocation, HttpInvocationConfig):
882
882
  msg = RegisterFunctionMessage(
@@ -1107,7 +1107,7 @@ class III:
1107
1107
 
1108
1108
  Examples:
1109
1109
  >>> ch = iii.create_channel()
1110
- >>> fn = iii.register_function({"id": "producer"}, producer_handler)
1110
+ >>> fn = iii.register_function("producer", producer_handler)
1111
1111
  >>> iii.trigger({"function_id": "producer", "payload": {"output": ch.writer_ref}})
1112
1112
  """
1113
1113
  return self._run_on_loop(self.create_channel_async(buffer_size))
@@ -1128,7 +1128,7 @@ class III:
1128
1128
 
1129
1129
  Examples:
1130
1130
  >>> ch = await iii.create_channel_async()
1131
- >>> fn = iii.register_function({"id": "producer"}, producer_handler)
1131
+ >>> fn = iii.register_function("producer", producer_handler)
1132
1132
  >>> await iii.trigger_async({"function_id": "producer", "payload": {"output": ch.writer_ref}})
1133
1133
  """
1134
1134
  result = await self.trigger_async(
@@ -1237,12 +1237,12 @@ class III:
1237
1237
  )
1238
1238
  return await stream.list_groups(input_data)
1239
1239
 
1240
- self.register_function({"id": f"stream::get({stream_name})"}, get_handler)
1241
- self.register_function({"id": f"stream::set({stream_name})"}, set_handler)
1242
- self.register_function({"id": f"stream::delete({stream_name})"}, delete_handler)
1243
- 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)
1244
1244
  self.register_function(
1245
- {"id": f"stream::list_groups({stream_name})"}, list_groups_handler
1245
+ f"stream::list_groups({stream_name})", list_groups_handler
1246
1246
  )
1247
1247
 
1248
1248
 
@@ -135,6 +135,14 @@ class UpdateDecrement(BaseModel):
135
135
  by: int | float
136
136
 
137
137
 
138
+ class UpdateAppend(BaseModel):
139
+ """Append operation for stream update."""
140
+
141
+ type: str = "append"
142
+ path: str
143
+ value: Any
144
+
145
+
138
146
  class UpdateRemove(BaseModel):
139
147
  """Remove operation for stream update."""
140
148
 
@@ -143,14 +151,17 @@ class UpdateRemove(BaseModel):
143
151
 
144
152
 
145
153
  class UpdateMerge(BaseModel):
146
- """Merge operation for stream update."""
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.
157
+ """
147
158
 
148
159
  type: str = "merge"
149
160
  path: str
150
161
  value: Any
151
162
 
152
163
 
153
- UpdateOp = UpdateSet | UpdateIncrement | UpdateDecrement | UpdateRemove | UpdateMerge
164
+ UpdateOp = UpdateSet | UpdateIncrement | UpdateDecrement | UpdateAppend | UpdateRemove | UpdateMerge
154
165
 
155
166
 
156
167
  class StreamTriggerConfig(BaseModel):
@@ -11,7 +11,6 @@ from pydantic import BaseModel, ConfigDict, Field
11
11
 
12
12
  from .iii_types import (
13
13
  HttpInvocationConfig,
14
- RegisterFunctionInput,
15
14
  RegisterFunctionMessage,
16
15
  RegisterServiceInput,
17
16
  RegisterTriggerInput,
@@ -87,7 +86,7 @@ class IIIClient(Protocol):
87
86
 
88
87
  def register_function(
89
88
  self,
90
- func: RegisterFunctionInput | dict[str, Any],
89
+ function_id: str,
91
90
  handler_or_invocation: RemoteFunctionHandler | HttpInvocationConfig,
92
91
  ) -> Any: ...
93
92
 
@@ -24,7 +24,7 @@ async def test_get_endpoint(engine_http_url, iii_client: III):
24
24
  def handler(input_data):
25
25
  return {"status_code": 200, "body": {"message": "Hello from GET"}}
26
26
 
27
- fn_ref = iii_client.register_function({"id": "test.api.get.py"}, handler)
27
+ fn_ref = iii_client.register_function("test.api.get.py", handler)
28
28
  trigger = iii_client.register_trigger(
29
29
  {
30
30
  "type": "http",
@@ -59,7 +59,7 @@ async def test_post_endpoint_with_body(engine_http_url, iii_client: III):
59
59
  "body": {"received": body, "created": True},
60
60
  }
61
61
 
62
- fn_ref = iii_client.register_function({"id": "test.api.post.py"}, handler)
62
+ fn_ref = iii_client.register_function("test.api.post.py", handler)
63
63
  trigger = iii_client.register_trigger(
64
64
  {
65
65
  "type": "http",
@@ -97,7 +97,7 @@ async def test_path_parameters(engine_http_url, iii_client: III):
97
97
  "body": {"id": input_data.get("path_params", {}).get("id")},
98
98
  }
99
99
 
100
- fn_ref = iii_client.register_function({"id": "test.api.getbyid.py"}, handler)
100
+ fn_ref = iii_client.register_function("test.api.getbyid.py", handler)
101
101
  trigger = iii_client.register_trigger(
102
102
  {
103
103
  "type": "http",
@@ -138,7 +138,7 @@ async def test_query_parameters(engine_http_url, iii_client: III):
138
138
  "body": {"query": q, "limit": limit},
139
139
  }
140
140
 
141
- fn_ref = iii_client.register_function({"id": "test.api.search.py"}, handler)
141
+ fn_ref = iii_client.register_function("test.api.search.py", handler)
142
142
  trigger = iii_client.register_trigger(
143
143
  {
144
144
  "type": "http",
@@ -170,7 +170,7 @@ async def test_custom_status_code(engine_http_url, iii_client: III):
170
170
  def handler(input_data):
171
171
  return {"status_code": 404, "body": {"error": "Not found"}}
172
172
 
173
- fn_ref = iii_client.register_function({"id": "test.api.notfound.py"}, handler)
173
+ fn_ref = iii_client.register_function("test.api.notfound.py", handler)
174
174
  trigger = iii_client.register_trigger(
175
175
  {
176
176
  "type": "http",
@@ -206,7 +206,7 @@ async def test_content_type_on_api_response_return(engine_http_url, iii_client:
206
206
  "body": xml_body,
207
207
  }
208
208
 
209
- fn_ref = iii_client.register_function({"id": "test.api.xml.return.py"}, handler)
209
+ fn_ref = iii_client.register_function("test.api.xml.return.py", handler)
210
210
  trigger = iii_client.register_trigger(
211
211
  {
212
212
  "type": "http",
@@ -245,7 +245,7 @@ async def test_download_pdf_streaming(engine_http_url, iii_client: III):
245
245
  await response.writer.write(original_pdf)
246
246
  await response.writer.close_async()
247
247
 
248
- fn_ref = iii_client.register_function({"id": "test.api.download.pdf.py"}, handler)
248
+ fn_ref = iii_client.register_function("test.api.download.pdf.py", handler)
249
249
  trigger = iii_client.register_trigger(
250
250
  {
251
251
  "type": "http",
@@ -296,7 +296,7 @@ async def test_upload_pdf_streaming(engine_http_url, iii_client: III):
296
296
  await response.writer.write(body)
297
297
  await response.writer.close_async()
298
298
 
299
- fn_ref = iii_client.register_function({"id": "test.api.upload.pdf.py"}, handler)
299
+ fn_ref = iii_client.register_function("test.api.upload.pdf.py", handler)
300
300
  trigger = iii_client.register_trigger(
301
301
  {
302
302
  "type": "http",
@@ -359,7 +359,7 @@ async def test_sse_streaming(engine_http_url, iii_client: III):
359
359
 
360
360
  await response.writer.close_async()
361
361
 
362
- fn_ref = iii_client.register_function({"id": "test.api.sse.py"}, handler)
362
+ fn_ref = iii_client.register_function("test.api.sse.py", handler)
363
363
  trigger = iii_client.register_trigger(
364
364
  {
365
365
  "type": "http",
@@ -434,7 +434,7 @@ async def test_urlencoded_form_data(engine_http_url, iii_client: III):
434
434
  await response.writer.write(result)
435
435
  await response.writer.close_async()
436
436
 
437
- fn_ref = iii_client.register_function({"id": "test.api.form.urlencoded.py"}, handler)
437
+ fn_ref = iii_client.register_function("test.api.form.urlencoded.py", handler)
438
438
  trigger = iii_client.register_trigger(
439
439
  {
440
440
  "type": "http",
@@ -504,7 +504,7 @@ async def test_multipart_form_data(engine_http_url, iii_client: III):
504
504
  await response.writer.write(result)
505
505
  await response.writer.close_async()
506
506
 
507
- fn_ref = iii_client.register_function({"id": "test.api.form.multipart.py"}, handler)
507
+ fn_ref = iii_client.register_function("test.api.form.multipart.py", handler)
508
508
  trigger = iii_client.register_trigger(
509
509
  {
510
510
  "type": "http",
@@ -21,9 +21,7 @@ async def wait_for(condition, timeout=5.0, interval=0.1):
21
21
  @pytest.mark.asyncio
22
22
  async def test_connect_successfully(iii_client: III):
23
23
  """SDK connects to the engine and can list functions."""
24
- result = iii_client.trigger(
25
- {"function_id": "engine::functions::list", "payload": {}}
26
- )
24
+ result = iii_client.trigger({"function_id": "engine::functions::list", "payload": {}})
27
25
  functions = [FunctionInfo(**f) for f in result.get("functions", [])]
28
26
  assert isinstance(functions, list)
29
27
 
@@ -37,7 +35,7 @@ async def test_register_and_invoke_function(iii_client: III):
37
35
  received.append(data)
38
36
  return {"echoed": data}
39
37
 
40
- fn = iii_client.register_function({"id": "test.bridge.py.echo"}, echo_handler)
38
+ fn = iii_client.register_function("test.bridge.py.echo", echo_handler)
41
39
  await asyncio.sleep(0.3)
42
40
 
43
41
  try:
@@ -66,15 +64,17 @@ async def test_invoke_function_fire_and_forget(iii_client: III):
66
64
  received_event.set()
67
65
  return {}
68
66
 
69
- fn = iii_client.register_function({"id": "test.bridge.py.receiver"}, receiver_handler)
67
+ fn = iii_client.register_function("test.bridge.py.receiver", receiver_handler)
70
68
  await asyncio.sleep(0.3)
71
69
 
72
70
  try:
73
- result = iii_client.trigger({
74
- "function_id": "test.bridge.py.receiver",
75
- "payload": {"value": 42},
76
- "action": TriggerAction.Void(),
77
- })
71
+ result = iii_client.trigger(
72
+ {
73
+ "function_id": "test.bridge.py.receiver",
74
+ "payload": {"value": 42},
75
+ "action": TriggerAction.Void(),
76
+ }
77
+ )
78
78
 
79
79
  assert result is None
80
80
 
@@ -87,14 +87,12 @@ async def test_invoke_function_fire_and_forget(iii_client: III):
87
87
  @pytest.mark.asyncio
88
88
  async def test_list_registered_functions(iii_client: III):
89
89
  """Registered function IDs appear in the engine functions list."""
90
- fn1 = iii_client.register_function({"id": "test.bridge.py.list.func1"}, lambda _: {})
91
- fn2 = iii_client.register_function({"id": "test.bridge.py.list.func2"}, lambda _: {})
90
+ fn1 = iii_client.register_function("test.bridge.py.list.func1", lambda _: {})
91
+ fn2 = iii_client.register_function("test.bridge.py.list.func2", lambda _: {})
92
92
  await asyncio.sleep(0.3)
93
93
 
94
94
  try:
95
- result = iii_client.trigger(
96
- {"function_id": "engine::functions::list", "payload": {}}
97
- )
95
+ result = iii_client.trigger({"function_id": "engine::functions::list", "payload": {}})
98
96
  functions = [FunctionInfo(**f) for f in result.get("functions", [])]
99
97
  function_ids = [f.function_id for f in functions]
100
98
 
@@ -109,8 +107,10 @@ async def test_list_registered_functions(iii_client: III):
109
107
  async def test_reject_non_existent_function(iii_client: III):
110
108
  """Triggering a non-existent function raises an error."""
111
109
  with pytest.raises(Exception):
112
- iii_client.trigger({
113
- "function_id": "nonexistent.function.py",
114
- "payload": {},
115
- "timeout_ms": 2000,
116
- })
110
+ iii_client.trigger(
111
+ {
112
+ "function_id": "nonexistent.function.py",
113
+ "payload": {},
114
+ "timeout_ms": 2000,
115
+ }
116
+ )
@@ -60,19 +60,21 @@ def test_handle_invoke_restores_trace_context_from_traceparent():
60
60
  return {"ok": True}
61
61
 
62
62
  client = III(address="ws://localhost:9999", options=InitOptions(worker_name="test"))
63
- client.register_function({"id": "test::fn"}, handler)
63
+ client.register_function("test::fn", handler)
64
64
 
65
65
  # Real W3C traceparent: trace_id = 4bf92f3577b34da6a3ce929d0e0e4736
66
66
  fake_traceparent = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
67
67
 
68
68
  # Use a non-None invocation_id with mocked _send so _invoke_with_otel_context is awaited
69
69
  with patch.object(client, "_send", new_callable=AsyncMock):
70
- client._run_on_loop(client._handle_invoke(
71
- invocation_id="test-invocation-id",
72
- path="test::fn",
73
- data={},
74
- traceparent=fake_traceparent,
75
- ))
70
+ client._run_on_loop(
71
+ client._handle_invoke(
72
+ invocation_id="test-invocation-id",
73
+ path="test::fn",
74
+ data={},
75
+ traceparent=fake_traceparent,
76
+ )
77
+ )
76
78
 
77
79
  expected_trace_id = 0x4BF92F3577B34DA6A3CE929D0E0E4736
78
80
  assert captured_trace_id, "handler did not capture an active span"
@@ -90,15 +92,17 @@ def test_handle_invoke_without_traceparent_runs_normally():
90
92
  return {"ok": True}
91
93
 
92
94
  client = III(address="ws://localhost:9999", options=InitOptions(worker_name="test"))
93
- client.register_function({"id": "test::fn"}, handler)
95
+ client.register_function("test::fn", handler)
94
96
 
95
97
  with patch.object(client, "_send", new_callable=AsyncMock):
96
- client._run_on_loop(client._handle_invoke(
97
- invocation_id="test-invocation-id",
98
- path="test::fn",
99
- data={},
100
- traceparent=None,
101
- ))
98
+ client._run_on_loop(
99
+ client._handle_invoke(
100
+ invocation_id="test-invocation-id",
101
+ path="test::fn",
102
+ data={},
103
+ traceparent=None,
104
+ )
105
+ )
102
106
 
103
107
  assert called
104
108