iii-sdk 0.11.4.dev1__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.
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/PKG-INFO +6 -6
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/README.md +5 -5
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/pyproject.toml +1 -1
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/src/iii/__init__.py +0 -2
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/src/iii/iii.py +48 -48
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/src/iii/stream.py +13 -2
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/src/iii/types.py +1 -2
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/tests/test_api_triggers.py +11 -11
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/tests/test_bridge.py +20 -20
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/tests/test_context_propagation.py +18 -14
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/tests/test_data_channels.py +37 -29
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/tests/test_healthcheck.py +1 -1
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/tests/test_http_external_functions_integration.py +30 -16
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/tests/test_iii_registration_dedup.py +2 -2
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/tests/test_middleware.py +10 -10
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/tests/test_pubsub.py +3 -3
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/tests/test_queue_integration.py +49 -44
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/tests/test_rbac_workers.py +12 -11
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/tests/test_register_function_args.py +57 -71
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/tests/test_state.py +55 -6
- iii_sdk-0.11.4.dev3/tests/test_stream_models.py +9 -0
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/tests/test_streams.py +61 -1
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/tests/test_sync_api.py +11 -8
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/uv.lock +1 -1
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/.gitignore +0 -0
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/src/iii/channels.py +0 -0
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/src/iii/errors.py +0 -0
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/src/iii/format_utils.py +0 -0
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/src/iii/iii_constants.py +0 -0
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/src/iii/iii_types.py +0 -0
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/src/iii/logger.py +0 -0
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/src/iii/otel_worker_gauges.py +0 -0
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/src/iii/state.py +0 -0
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/src/iii/telemetry.py +0 -0
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/src/iii/telemetry_exporters.py +0 -0
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/src/iii/telemetry_types.py +0 -0
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/src/iii/triggers.py +0 -0
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/src/iii/utils.py +0 -0
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/src/iii/worker_metrics.py +0 -0
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/tests/conftest.py +0 -0
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/tests/test_async_api.py +0 -0
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/tests/test_channel_close_delay.py +0 -0
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/tests/test_errors.py +0 -0
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/tests/test_format_utils.py +0 -0
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/tests/test_hold_process.py +0 -0
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/tests/test_init_api.py +0 -0
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/tests/test_invocation_exception.py +0 -0
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/tests/test_logger_function_ids.py +0 -0
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/tests/test_logger_otel.py +0 -0
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/tests/test_streams_runtime_annotations.py +0 -0
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/tests/test_telemetry.py +0 -0
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/tests/test_telemetry_exporters.py +0 -0
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/tests/test_telemetry_types.py +0 -0
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/tests/test_trace_helpers.py +0 -0
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/tests/test_trigger_metadata.py +0 -0
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/tests/test_utils.py +0 -0
- {iii_sdk-0.11.4.dev1 → iii_sdk-0.11.4.dev3}/tests/test_worker_metadata.py +0 -0
- {iii_sdk-0.11.4.dev1 → 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.
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
74
|
+
result = iii.trigger({"function_id": "orders::create", "payload": {"body": {"item": "widget"}}})
|
|
75
75
|
```
|
|
76
76
|
|
|
77
77
|
## Modules
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
822
|
-
metadata: Arbitrary metadata
|
|
823
|
-
request_format: Schema describing expected input
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
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
|
-
|
|
837
|
-
|
|
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(
|
|
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(
|
|
856
|
-
|
|
857
|
-
|
|
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
|
-
|
|
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
|
|
879
|
-
raise ValueError(f"function id '{
|
|
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(
|
|
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(
|
|
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(
|
|
1241
|
-
self.register_function(
|
|
1242
|
-
self.register_function(
|
|
1243
|
-
self.register_function(
|
|
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
|
-
|
|
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
|
-
"""
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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(
|
|
91
|
-
fn2 = iii_client.register_function(
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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(
|
|
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(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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(
|
|
95
|
+
client.register_function("test::fn", handler)
|
|
94
96
|
|
|
95
97
|
with patch.object(client, "_send", new_callable=AsyncMock):
|
|
96
|
-
client._run_on_loop(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|