iii-sdk 0.17.0.dev1__tar.gz → 0.18.0.dev1__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.17.0.dev1 → iii_sdk-0.18.0.dev1}/PKG-INFO +2 -2
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/pyproject.toml +2 -2
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/src/iii/channels.py +47 -0
- iii_sdk-0.18.0.dev1/src/iii/helpers.py +80 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/src/iii/iii.py +16 -58
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/src/iii/triggers.py +1 -1
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/src/iii/types.py +67 -9
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_async_api.py +3 -4
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_data_channels.py +5 -4
- iii_sdk-0.18.0.dev1/tests/test_helpers.py +196 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_streams.py +2 -1
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_sync_api.py +3 -2
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/uv.lock +2 -2
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/.gitignore +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/README.md +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/src/iii/__init__.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/src/iii/errors.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/src/iii/format_utils.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/src/iii/iii_constants.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/src/iii/iii_types.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/src/iii/otel_worker_gauges.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/src/iii/state.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/src/iii/stream.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/src/iii/utils.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/src/iii/worker_metrics.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/conftest.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_api_triggers.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_baggage_span_processor.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_bridge.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_channel_close_delay.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_context_propagation.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_errors.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_format_utils.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_healthcheck.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_hold_process.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_http_external_functions_integration.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_iii_registration_dedup.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_init_api.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_invocation_exception.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_logger_function_ids.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_logger_otel.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_middleware.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_payload.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_pubsub.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_queue_integration.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_rbac_workers.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_register_function_args.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_span_ops.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_state.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_stream_models.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_streams_runtime_annotations.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_telemetry.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_telemetry_exporters.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_telemetry_types.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_trace_helpers.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_trigger_metadata.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_trigger_registration_error.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_utils.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_worker_metadata.py +0 -0
- {iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/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.
|
|
3
|
+
Version: 0.18.0.dev1
|
|
4
4
|
Summary: III SDK for Python
|
|
5
5
|
Project-URL: Homepage, https://github.com/iii-hq/iii
|
|
6
6
|
Project-URL: Repository, https://github.com/iii-hq/iii
|
|
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.12
|
|
16
16
|
Requires-Python: >=3.10
|
|
17
|
-
Requires-Dist: iii-observability==0.
|
|
17
|
+
Requires-Dist: iii-observability==0.18.0.dev1
|
|
18
18
|
Requires-Dist: opentelemetry-api>=1.25
|
|
19
19
|
Requires-Dist: pydantic>=2.0
|
|
20
20
|
Requires-Dist: websockets>=12.0
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "iii-sdk"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.18.0.dev1"
|
|
8
8
|
description = "III SDK for Python"
|
|
9
9
|
authors = [{ name = "III" }]
|
|
10
10
|
license = { text = "Apache-2.0" }
|
|
@@ -23,7 +23,7 @@ dependencies = [
|
|
|
23
23
|
"websockets>=12.0",
|
|
24
24
|
"pydantic>=2.0",
|
|
25
25
|
"opentelemetry-api>=1.25",
|
|
26
|
-
"iii-observability==0.
|
|
26
|
+
"iii-observability==0.18.0.dev1",
|
|
27
27
|
]
|
|
28
28
|
|
|
29
29
|
[project.urls]
|
|
@@ -4,6 +4,8 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import logging
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from enum import Enum
|
|
7
9
|
from typing import Any, AsyncIterator, Callable
|
|
8
10
|
from urllib.parse import quote
|
|
9
11
|
|
|
@@ -17,6 +19,51 @@ log = logging.getLogger("iii.channels")
|
|
|
17
19
|
MAX_FRAME_SIZE = 64 * 1024
|
|
18
20
|
|
|
19
21
|
|
|
22
|
+
class ChannelDirection(str, Enum):
|
|
23
|
+
"""Direction of a streaming channel reference.
|
|
24
|
+
|
|
25
|
+
Mirrors the Rust SDK's ``ChannelDirection`` enum. The string values
|
|
26
|
+
(``"read"`` / ``"write"``) match the wire format used by
|
|
27
|
+
:class:`StreamChannelRef.direction`.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
READ = "read"
|
|
31
|
+
WRITE = "write"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class ChannelItem:
|
|
36
|
+
"""A single frame transferred over a channel.
|
|
37
|
+
|
|
38
|
+
Mirrors the Rust SDK's ``ChannelItem`` enum. Exactly one of ``text`` or
|
|
39
|
+
``binary`` is set; use :meth:`text_item` / :meth:`binary_item` to
|
|
40
|
+
construct instances.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
text: str | None = None
|
|
44
|
+
binary: bytes | None = None
|
|
45
|
+
|
|
46
|
+
def __post_init__(self) -> None:
|
|
47
|
+
if (self.text is None) == (self.binary is None):
|
|
48
|
+
raise ValueError("ChannelItem requires exactly one of `text` or `binary`")
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def text_item(cls, value: str) -> "ChannelItem":
|
|
52
|
+
return cls(text=value, binary=None)
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def binary_item(cls, value: bytes) -> "ChannelItem":
|
|
56
|
+
return cls(text=None, binary=value)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def is_text(self) -> bool:
|
|
60
|
+
return self.text is not None
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def is_binary(self) -> bool:
|
|
64
|
+
return self.binary is not None
|
|
65
|
+
|
|
66
|
+
|
|
20
67
|
def build_channel_url(
|
|
21
68
|
engine_ws_base: str,
|
|
22
69
|
channel_id: str,
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Helper free functions that operate on an :class:`III` client instance.
|
|
2
|
+
|
|
3
|
+
These were previously instance methods on the SDK client. They take the
|
|
4
|
+
``iii`` client as the first argument so the public surface of the client
|
|
5
|
+
stays focused on the core lifecycle and registration methods.
|
|
6
|
+
|
|
7
|
+
Mirrors the Rust ``iii_sdk::helpers`` module and the Node
|
|
8
|
+
``iii-sdk/helpers`` subpath export.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any, Protocol, TypeVar
|
|
14
|
+
|
|
15
|
+
from .channels import ChannelDirection, ChannelItem
|
|
16
|
+
from .stream import IStream
|
|
17
|
+
from .types import Channel, IIIClient, extract_channel_refs, is_channel_ref
|
|
18
|
+
|
|
19
|
+
TData = TypeVar("TData")
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"ChannelDirection",
|
|
23
|
+
"ChannelItem",
|
|
24
|
+
"create_channel",
|
|
25
|
+
"create_channel_async",
|
|
26
|
+
"create_stream",
|
|
27
|
+
"extract_channel_refs",
|
|
28
|
+
"is_channel_ref",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class _IIIWithHelperShims(IIIClient, Protocol):
|
|
33
|
+
"""Internal Protocol that adds the ``_helpers_*`` shim methods.
|
|
34
|
+
|
|
35
|
+
The free functions below delegate to these private methods on the
|
|
36
|
+
concrete :class:`III` instance. Defining the Protocol here mirrors the
|
|
37
|
+
Node SDK's ``IIIWithHelperShims`` intersection type — callers see the
|
|
38
|
+
public :class:`IIIClient` Protocol; helpers see the shims internally.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def _helpers_create_channel(self, buffer_size: int | None = None) -> Channel: ...
|
|
42
|
+
|
|
43
|
+
async def _helpers_create_channel_async(
|
|
44
|
+
self, buffer_size: int | None = None
|
|
45
|
+
) -> Channel: ...
|
|
46
|
+
|
|
47
|
+
def _helpers_create_stream(
|
|
48
|
+
self, stream_name: str, stream: IStream[Any]
|
|
49
|
+
) -> None: ... # noqa: D401 — internal shim, generic erased at the boundary
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def create_channel(iii: IIIClient, buffer_size: int | None = None) -> Channel:
|
|
53
|
+
"""Create a streaming channel pair (sync wrapper).
|
|
54
|
+
|
|
55
|
+
Free-function form of the former ``III.create_channel`` instance method.
|
|
56
|
+
"""
|
|
57
|
+
shim: _IIIWithHelperShims = iii # type: ignore[assignment]
|
|
58
|
+
return shim._helpers_create_channel(buffer_size)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
async def create_channel_async(
|
|
62
|
+
iii: IIIClient, buffer_size: int | None = None
|
|
63
|
+
) -> Channel:
|
|
64
|
+
"""Create a streaming channel pair (async).
|
|
65
|
+
|
|
66
|
+
Free-function form of the former ``III.create_channel_async`` method.
|
|
67
|
+
"""
|
|
68
|
+
shim: _IIIWithHelperShims = iii # type: ignore[assignment]
|
|
69
|
+
return await shim._helpers_create_channel_async(buffer_size)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def create_stream(iii: IIIClient, stream_name: str, stream: IStream[TData]) -> None:
|
|
73
|
+
"""Register a custom stream implementation.
|
|
74
|
+
|
|
75
|
+
Free-function form of the former ``III.create_stream`` instance method.
|
|
76
|
+
The ``IStream`` generic ``TData`` is preserved so type checkers can
|
|
77
|
+
validate the implementor's get/set/delete/list signatures.
|
|
78
|
+
"""
|
|
79
|
+
shim: _IIIWithHelperShims = iii # type: ignore[assignment]
|
|
80
|
+
shim._helpers_create_stream(stream_name, stream)
|
|
@@ -1142,49 +1142,20 @@ class III:
|
|
|
1142
1142
|
invocation_id=invocation_id,
|
|
1143
1143
|
)
|
|
1144
1144
|
|
|
1145
|
-
|
|
1146
|
-
|
|
1145
|
+
# Internal: backing methods for items in the `iii.helpers` submodule.
|
|
1146
|
+
# These are renamed with a leading underscore so they don't appear on the
|
|
1147
|
+
# public `IIIClient` Protocol; callers use `iii.helpers.<name>(iii, ...)`.
|
|
1148
|
+
def _helpers_create_channel(self, buffer_size: int | None = None) -> Channel:
|
|
1149
|
+
"""Internal shim backing :func:`iii.helpers.create_channel`.
|
|
1147
1150
|
|
|
1148
|
-
|
|
1149
|
-
and their serializable refs (``writer_ref``, ``reader_ref``) that
|
|
1150
|
-
can be passed as fields in invocation data to other functions.
|
|
1151
|
-
|
|
1152
|
-
Args:
|
|
1153
|
-
buffer_size: Buffer capacity for the channel. Defaults to ``64``.
|
|
1154
|
-
|
|
1155
|
-
Returns:
|
|
1156
|
-
A ``Channel`` object with ``writer``, ``reader``,
|
|
1157
|
-
``writer_ref``, and ``reader_ref`` attributes. Pass
|
|
1158
|
-
``writer_ref`` or ``reader_ref`` in trigger payloads to
|
|
1159
|
-
share channels across functions -- the receiving function
|
|
1160
|
-
can reconstruct a ``ChannelWriter`` or ``ChannelReader``
|
|
1161
|
-
from the ref.
|
|
1162
|
-
|
|
1163
|
-
Examples:
|
|
1164
|
-
>>> ch = iii.create_channel()
|
|
1165
|
-
>>> fn = iii.register_function("producer", producer_handler)
|
|
1166
|
-
>>> iii.trigger({"function_id": "producer", "payload": {"output": ch.writer_ref}})
|
|
1151
|
+
Public callers must use the free function from ``iii.helpers``.
|
|
1167
1152
|
"""
|
|
1168
|
-
return self._run_on_loop(self.
|
|
1169
|
-
|
|
1170
|
-
async def create_channel_async(self, buffer_size: int | None = None) -> Channel:
|
|
1171
|
-
"""Create a streaming channel pair for worker-to-worker data transfer.
|
|
1172
|
-
|
|
1173
|
-
The returned ``Channel`` contains a local ``writer`` / ``reader``
|
|
1174
|
-
and their serializable refs (``writer_ref``, ``reader_ref``) that
|
|
1175
|
-
can be passed as fields in invocation data to other functions.
|
|
1153
|
+
return self._run_on_loop(self._helpers_create_channel_async(buffer_size))
|
|
1176
1154
|
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
Returns:
|
|
1181
|
-
A ``Channel`` with ``writer``, ``reader``, ``writer_ref``, and
|
|
1182
|
-
``reader_ref`` attributes.
|
|
1155
|
+
async def _helpers_create_channel_async(self, buffer_size: int | None = None) -> Channel:
|
|
1156
|
+
"""Internal shim backing :func:`iii.helpers.create_channel_async`.
|
|
1183
1157
|
|
|
1184
|
-
|
|
1185
|
-
>>> ch = await iii.create_channel_async()
|
|
1186
|
-
>>> fn = iii.register_function("producer", producer_handler)
|
|
1187
|
-
>>> await iii.trigger_async({"function_id": "producer", "payload": {"output": ch.writer_ref}})
|
|
1158
|
+
Public callers must use the free function from ``iii.helpers``.
|
|
1188
1159
|
"""
|
|
1189
1160
|
result = await self.trigger_async(
|
|
1190
1161
|
{
|
|
@@ -1248,27 +1219,14 @@ class III:
|
|
|
1248
1219
|
)
|
|
1249
1220
|
asyncio.run_coroutine_threadsafe(self._send(msg), self._loop)
|
|
1250
1221
|
|
|
1251
|
-
def
|
|
1252
|
-
"""
|
|
1222
|
+
def _helpers_create_stream(self, stream_name: str, stream: IStream[Any]) -> None:
|
|
1223
|
+
"""Internal shim backing :func:`iii.helpers.create_stream`.
|
|
1253
1224
|
|
|
1225
|
+
Public callers must use the free function from ``iii.helpers``.
|
|
1254
1226
|
Registers 5 of the 6 ``IStream`` methods (``get``, ``set``, ``delete``,
|
|
1255
|
-
``list``, ``list_groups``).
|
|
1256
|
-
-- atomic updates are handled by the engine's built-in stream update
|
|
1257
|
-
|
|
1258
|
-
Args:
|
|
1259
|
-
stream_name: Unique name for the stream.
|
|
1260
|
-
stream: An object implementing the ``IStream`` interface.
|
|
1261
|
-
|
|
1262
|
-
Examples:
|
|
1263
|
-
>>> from iii.stream import IStream
|
|
1264
|
-
>>> class MyStream(IStream):
|
|
1265
|
-
... async def get(self, input): ...
|
|
1266
|
-
... async def set(self, input): ...
|
|
1267
|
-
... async def delete(self, input): ...
|
|
1268
|
-
... async def list(self, input): ...
|
|
1269
|
-
... async def list_groups(self, input): ...
|
|
1270
|
-
... async def update(self, input): ...
|
|
1271
|
-
>>> iii.create_stream("my-stream", MyStream())
|
|
1227
|
+
``list``, ``list_groups``). The ``update`` method is **not** registered
|
|
1228
|
+
-- atomic updates are handled by the engine's built-in stream update
|
|
1229
|
+
logic.
|
|
1272
1230
|
"""
|
|
1273
1231
|
|
|
1274
1232
|
async def get_handler(data: Any) -> Any:
|
|
@@ -5,7 +5,18 @@ from __future__ import annotations
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import json
|
|
7
7
|
from dataclasses import dataclass
|
|
8
|
-
from typing import
|
|
8
|
+
from typing import (
|
|
9
|
+
TYPE_CHECKING,
|
|
10
|
+
Any,
|
|
11
|
+
Awaitable,
|
|
12
|
+
Callable,
|
|
13
|
+
Generic,
|
|
14
|
+
Literal,
|
|
15
|
+
Protocol,
|
|
16
|
+
TypedDict,
|
|
17
|
+
TypeGuard,
|
|
18
|
+
TypeVar,
|
|
19
|
+
)
|
|
9
20
|
|
|
10
21
|
from pydantic import BaseModel, ConfigDict, Field
|
|
11
22
|
|
|
@@ -18,7 +29,6 @@ from .iii_types import (
|
|
|
18
29
|
StreamChannelRef,
|
|
19
30
|
TriggerRequest,
|
|
20
31
|
)
|
|
21
|
-
from .stream import IStream
|
|
22
32
|
from .triggers import Trigger, TriggerHandler
|
|
23
33
|
|
|
24
34
|
if TYPE_CHECKING:
|
|
@@ -77,7 +87,11 @@ class RemoteServiceFunctionData(BaseModel):
|
|
|
77
87
|
|
|
78
88
|
|
|
79
89
|
class IIIClient(Protocol):
|
|
80
|
-
"""Protocol for III client implementations.
|
|
90
|
+
"""Protocol for III client implementations.
|
|
91
|
+
|
|
92
|
+
Helper free functions live in :mod:`iii.helpers`. See
|
|
93
|
+
:func:`iii.helpers.create_channel` and :func:`iii.helpers.create_stream`.
|
|
94
|
+
"""
|
|
81
95
|
|
|
82
96
|
def register_trigger(self, trigger: RegisterTriggerInput | dict[str, Any]) -> Trigger: ...
|
|
83
97
|
|
|
@@ -97,10 +111,6 @@ class IIIClient(Protocol):
|
|
|
97
111
|
|
|
98
112
|
def unregister_trigger_type(self, trigger_type: RegisterTriggerTypeInput | dict[str, Any]) -> None: ...
|
|
99
113
|
|
|
100
|
-
def create_channel(self, buffer_size: int | None = None) -> Channel: ...
|
|
101
|
-
|
|
102
|
-
def create_stream(self, stream_name: str, stream: IStream[Any]) -> None: ...
|
|
103
|
-
|
|
104
114
|
def shutdown(self) -> None: ...
|
|
105
115
|
|
|
106
116
|
|
|
@@ -183,11 +193,59 @@ class HttpRequest:
|
|
|
183
193
|
request_body: ChannelReader
|
|
184
194
|
|
|
185
195
|
|
|
186
|
-
|
|
187
|
-
"""
|
|
196
|
+
class StreamChannelRefDict(TypedDict):
|
|
197
|
+
"""Wire-shape of a :class:`StreamChannelRef` as it appears in raw payloads.
|
|
198
|
+
|
|
199
|
+
Used as the narrowed type for :func:`is_channel_ref` so type checkers can
|
|
200
|
+
validate ``value["channel_id"]`` style access after the guard passes.
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
channel_id: str
|
|
204
|
+
access_key: str
|
|
205
|
+
direction: Literal["read", "write"]
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def is_channel_ref(value: Any) -> TypeGuard[StreamChannelRefDict]:
|
|
209
|
+
"""Check if a value looks like a StreamChannelRef.
|
|
210
|
+
|
|
211
|
+
Returns a :class:`typing.TypeGuard` that narrows ``value`` to
|
|
212
|
+
:class:`StreamChannelRefDict`, mirroring the TypeScript SDK's
|
|
213
|
+
``value is StreamChannelRef`` predicate.
|
|
214
|
+
"""
|
|
188
215
|
return (
|
|
189
216
|
isinstance(value, dict)
|
|
190
217
|
and isinstance(value.get("channel_id"), str)
|
|
191
218
|
and isinstance(value.get("access_key"), str)
|
|
192
219
|
and value.get("direction") in {"read", "write"}
|
|
193
220
|
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def extract_channel_refs(data: Any) -> list[tuple[str, StreamChannelRef]]:
|
|
224
|
+
"""Extract all channel references from a nested value, returning ``(path, ref)`` tuples.
|
|
225
|
+
|
|
226
|
+
Recursively walks ``dict`` and ``list`` structures. Dotted paths are
|
|
227
|
+
used for object fields, bracketed indices for list elements (e.g.
|
|
228
|
+
``items[0].writer``). Mirrors the Rust SDK's ``extract_channel_refs``.
|
|
229
|
+
"""
|
|
230
|
+
refs: list[tuple[str, StreamChannelRef]] = []
|
|
231
|
+
_extract_refs_recursive(data, "", refs)
|
|
232
|
+
return refs
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _extract_refs_recursive(
|
|
236
|
+
data: Any, prefix: str, refs: list[tuple[str, StreamChannelRef]]
|
|
237
|
+
) -> None:
|
|
238
|
+
if is_channel_ref(data):
|
|
239
|
+
try:
|
|
240
|
+
refs.append((prefix, StreamChannelRef(**data)))
|
|
241
|
+
except Exception:
|
|
242
|
+
pass
|
|
243
|
+
return
|
|
244
|
+
if isinstance(data, dict):
|
|
245
|
+
for key, value in data.items():
|
|
246
|
+
path = key if not prefix else f"{prefix}.{key}"
|
|
247
|
+
_extract_refs_recursive(value, path, refs)
|
|
248
|
+
elif isinstance(data, list):
|
|
249
|
+
for idx, item in enumerate(data):
|
|
250
|
+
path = f"[{idx}]" if not prefix else f"{prefix}[{idx}]"
|
|
251
|
+
_extract_refs_recursive(item, path, refs)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
4
|
|
|
5
|
+
from iii import helpers
|
|
5
6
|
from iii.iii import III
|
|
6
7
|
|
|
7
8
|
|
|
@@ -10,9 +11,8 @@ def test_trigger_async_is_coroutine_function():
|
|
|
10
11
|
assert inspect.iscoroutinefunction(III.trigger_async)
|
|
11
12
|
|
|
12
13
|
|
|
13
|
-
def
|
|
14
|
-
assert
|
|
15
|
-
assert inspect.iscoroutinefunction(III.create_channel_async)
|
|
14
|
+
def test_create_channel_async_helper_is_coroutine_function():
|
|
15
|
+
assert inspect.iscoroutinefunction(helpers.create_channel_async)
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def test_connect_async_is_coroutine_function():
|
|
@@ -29,7 +29,6 @@ def test_async_methods_have_docstrings():
|
|
|
29
29
|
"""All public async methods must have docstrings."""
|
|
30
30
|
async_methods = [
|
|
31
31
|
"trigger_async",
|
|
32
|
-
"create_channel_async",
|
|
33
32
|
"connect_async",
|
|
34
33
|
"shutdown_async",
|
|
35
34
|
]
|
|
@@ -4,8 +4,9 @@ import asyncio
|
|
|
4
4
|
import json
|
|
5
5
|
import time
|
|
6
6
|
|
|
7
|
-
from iii.iii import III
|
|
8
7
|
from iii.channels import ChannelReader, ChannelWriter
|
|
8
|
+
from iii.helpers import create_channel_async
|
|
9
|
+
from iii.iii import III
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
def test_stream_data_from_sender_to_processor(iii_client: III):
|
|
@@ -35,7 +36,7 @@ def test_stream_data_from_sender_to_processor(iii_client: III):
|
|
|
35
36
|
|
|
36
37
|
async def sender_handler(input_data):
|
|
37
38
|
records = input_data["records"]
|
|
38
|
-
channel = await
|
|
39
|
+
channel = await create_channel_async(iii_client)
|
|
39
40
|
|
|
40
41
|
payload = json.dumps(records).encode("utf-8")
|
|
41
42
|
await channel.writer.write(payload)
|
|
@@ -137,8 +138,8 @@ def test_bidirectional_streaming(iii_client: III):
|
|
|
137
138
|
text = input_data["text"]
|
|
138
139
|
chunk_size = input_data["chunkSize"]
|
|
139
140
|
|
|
140
|
-
input_channel = await
|
|
141
|
-
output_channel = await
|
|
141
|
+
input_channel = await create_channel_async(iii_client)
|
|
142
|
+
output_channel = await create_channel_async(iii_client)
|
|
142
143
|
|
|
143
144
|
messages = []
|
|
144
145
|
output_channel.reader.on_message(lambda msg: messages.append(json.loads(msg)))
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Tests for the public helpers submodule (mirrors Rust/Node helpers parity)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
import json
|
|
7
|
+
from types import SimpleNamespace
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
import iii.iii as iii_module
|
|
13
|
+
from iii import InitOptions
|
|
14
|
+
from iii.iii import III
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FakeWebSocket:
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
self.sent: list[dict[str, Any]] = []
|
|
20
|
+
self.state = SimpleNamespace(name="OPEN")
|
|
21
|
+
|
|
22
|
+
async def send(self, payload: str) -> None:
|
|
23
|
+
self.sent.append(json.loads(payload))
|
|
24
|
+
|
|
25
|
+
async def close(self) -> None:
|
|
26
|
+
self.state = SimpleNamespace(name="CLOSED")
|
|
27
|
+
|
|
28
|
+
def __aiter__(self) -> "FakeWebSocket":
|
|
29
|
+
return self
|
|
30
|
+
|
|
31
|
+
async def __anext__(self) -> Any:
|
|
32
|
+
raise StopAsyncIteration
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _patch_ws(monkeypatch: pytest.MonkeyPatch) -> FakeWebSocket:
|
|
36
|
+
ws = FakeWebSocket()
|
|
37
|
+
|
|
38
|
+
async def fake_connect(_: str, **kwargs: object) -> FakeWebSocket:
|
|
39
|
+
return ws
|
|
40
|
+
|
|
41
|
+
monkeypatch.setattr(iii_module.websockets, "connect", fake_connect)
|
|
42
|
+
monkeypatch.setattr("iii_observability.telemetry.init_otel", lambda **kwargs: None)
|
|
43
|
+
monkeypatch.setattr("iii_observability.telemetry.attach_event_loop", lambda loop: None)
|
|
44
|
+
monkeypatch.setattr(iii_module.III, "_register_worker_metadata", lambda self: None)
|
|
45
|
+
return ws
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_helpers_module_exports_expected_names() -> None:
|
|
49
|
+
from iii import helpers
|
|
50
|
+
|
|
51
|
+
expected = {
|
|
52
|
+
"ChannelDirection",
|
|
53
|
+
"ChannelItem",
|
|
54
|
+
"create_channel",
|
|
55
|
+
"create_channel_async",
|
|
56
|
+
"create_stream",
|
|
57
|
+
"extract_channel_refs",
|
|
58
|
+
"is_channel_ref",
|
|
59
|
+
}
|
|
60
|
+
actual = set(helpers.__all__)
|
|
61
|
+
missing = expected - actual
|
|
62
|
+
assert not missing, f"missing from helpers.__all__: {missing}"
|
|
63
|
+
for name in expected:
|
|
64
|
+
assert hasattr(helpers, name), f"helpers module missing attribute: {name}"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_helpers_free_functions_take_iii_first() -> None:
|
|
68
|
+
from iii import helpers
|
|
69
|
+
|
|
70
|
+
for name in (
|
|
71
|
+
"create_channel",
|
|
72
|
+
"create_channel_async",
|
|
73
|
+
"create_stream",
|
|
74
|
+
):
|
|
75
|
+
sig = inspect.signature(getattr(helpers, name))
|
|
76
|
+
params = list(sig.parameters)
|
|
77
|
+
assert params and params[0] == "iii", f"{name} signature: {sig}"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_is_channel_ref_works_via_helpers() -> None:
|
|
81
|
+
from iii import helpers
|
|
82
|
+
|
|
83
|
+
assert helpers.is_channel_ref({}) is False
|
|
84
|
+
assert helpers.is_channel_ref(
|
|
85
|
+
{"channel_id": "c", "access_key": "k", "direction": "read"}
|
|
86
|
+
) is True
|
|
87
|
+
assert helpers.is_channel_ref(
|
|
88
|
+
{"channel_id": "c", "access_key": "k", "direction": "garbage"}
|
|
89
|
+
) is False
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_extract_channel_refs_walks_nested_structures() -> None:
|
|
93
|
+
from iii import helpers
|
|
94
|
+
|
|
95
|
+
assert helpers.extract_channel_refs({}) == []
|
|
96
|
+
|
|
97
|
+
ref = {"channel_id": "c1", "access_key": "k1", "direction": "read"}
|
|
98
|
+
refs = helpers.extract_channel_refs({"input": ref})
|
|
99
|
+
assert len(refs) == 1
|
|
100
|
+
path, channel_ref = refs[0]
|
|
101
|
+
assert path == "input"
|
|
102
|
+
assert channel_ref.channel_id == "c1"
|
|
103
|
+
|
|
104
|
+
nested = helpers.extract_channel_refs(
|
|
105
|
+
{"items": [{"writer": ref}, {"writer": ref}]}
|
|
106
|
+
)
|
|
107
|
+
assert {p for p, _ in nested} == {"items[0].writer", "items[1].writer"}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_channel_direction_string_values() -> None:
|
|
111
|
+
from iii.helpers import ChannelDirection
|
|
112
|
+
|
|
113
|
+
assert ChannelDirection.READ.value == "read"
|
|
114
|
+
assert ChannelDirection.WRITE.value == "write"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_channel_item_constructors() -> None:
|
|
118
|
+
from iii.helpers import ChannelItem
|
|
119
|
+
|
|
120
|
+
text = ChannelItem.text_item("hi")
|
|
121
|
+
assert text.is_text and not text.is_binary
|
|
122
|
+
assert text.text == "hi"
|
|
123
|
+
|
|
124
|
+
binary = ChannelItem.binary_item(b"\x00\x01")
|
|
125
|
+
assert binary.is_binary and not binary.is_text
|
|
126
|
+
assert binary.binary == b"\x00\x01"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_iii_no_longer_exposes_relocated_methods(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
130
|
+
_patch_ws(monkeypatch)
|
|
131
|
+
client = III("ws://fake", InitOptions())
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
for name in (
|
|
135
|
+
"create_channel",
|
|
136
|
+
"create_channel_async",
|
|
137
|
+
"create_stream",
|
|
138
|
+
):
|
|
139
|
+
assert not hasattr(client, name), f"client still has {name}"
|
|
140
|
+
finally:
|
|
141
|
+
client.shutdown()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_iii_client_protocol_no_longer_declares_relocated_methods() -> None:
|
|
145
|
+
"""The :class:`iii.IIIClient` Protocol must not declare the relocated methods."""
|
|
146
|
+
from iii import IIIClient
|
|
147
|
+
|
|
148
|
+
for name in (
|
|
149
|
+
"create_channel",
|
|
150
|
+
"create_stream",
|
|
151
|
+
):
|
|
152
|
+
assert name not in IIIClient.__dict__, (
|
|
153
|
+
f"IIIClient Protocol still declares {name}"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_init_no_longer_exports_relocated_channel_items() -> None:
|
|
158
|
+
import iii
|
|
159
|
+
|
|
160
|
+
for name in (
|
|
161
|
+
"extract_channel_refs",
|
|
162
|
+
"is_channel_ref",
|
|
163
|
+
"ChannelDirection",
|
|
164
|
+
"ChannelItem",
|
|
165
|
+
):
|
|
166
|
+
assert name not in iii.__all__, f"{name} still in iii.__all__"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_iii_register_and_unregister_trigger_type_round_trip(
|
|
170
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
171
|
+
) -> None:
|
|
172
|
+
from iii import RegisterTriggerTypeInput
|
|
173
|
+
from iii.triggers import TriggerConfig, TriggerHandler
|
|
174
|
+
|
|
175
|
+
class DummyHandler(TriggerHandler[Any]):
|
|
176
|
+
async def register_trigger(self, config: TriggerConfig[Any]) -> None:
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
async def unregister_trigger(self, config: TriggerConfig[Any]) -> None:
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
_patch_ws(monkeypatch)
|
|
183
|
+
client = III("ws://fake", InitOptions())
|
|
184
|
+
try:
|
|
185
|
+
trigger_type = RegisterTriggerTypeInput(
|
|
186
|
+
id="helpers.test", description="from helpers"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
ref = client.register_trigger_type(trigger_type, DummyHandler())
|
|
190
|
+
assert "helpers.test" in client._trigger_types
|
|
191
|
+
assert ref is not None
|
|
192
|
+
|
|
193
|
+
client.unregister_trigger_type(trigger_type)
|
|
194
|
+
assert "helpers.test" not in client._trigger_types
|
|
195
|
+
finally:
|
|
196
|
+
client.shutdown()
|
|
@@ -6,6 +6,7 @@ from typing import Any
|
|
|
6
6
|
|
|
7
7
|
import pytest
|
|
8
8
|
|
|
9
|
+
from iii.helpers import create_stream
|
|
9
10
|
from iii.iii import III
|
|
10
11
|
from iii.stream import (
|
|
11
12
|
IStream,
|
|
@@ -353,7 +354,7 @@ async def test_stream_custom_operations(iii_client: III):
|
|
|
353
354
|
async def update(self, input: StreamUpdateInput) -> StreamUpdateResult | None:
|
|
354
355
|
raise NotImplementedError
|
|
355
356
|
|
|
356
|
-
|
|
357
|
+
create_stream(iii_client, stream_name, InMemoryStream())
|
|
357
358
|
await asyncio.sleep(1.0)
|
|
358
359
|
|
|
359
360
|
test_data = {"name": "Test", "value": 100}
|
|
@@ -9,6 +9,7 @@ import pytest
|
|
|
9
9
|
|
|
10
10
|
import iii.iii as iii_module
|
|
11
11
|
from iii import InitOptions, RegisterTriggerTypeInput
|
|
12
|
+
from iii.helpers import create_channel
|
|
12
13
|
from iii.iii import III
|
|
13
14
|
from iii.triggers import TriggerConfig, TriggerHandler
|
|
14
15
|
|
|
@@ -179,7 +180,7 @@ def test_register_and_unregister_trigger_type_accept_input_object(monkeypatch: p
|
|
|
179
180
|
|
|
180
181
|
|
|
181
182
|
def test_public_methods_are_sync(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
182
|
-
"""trigger and create_channel should be sync."""
|
|
183
|
+
"""trigger and the helpers.create_channel free function should be sync."""
|
|
183
184
|
_patch_ws(monkeypatch)
|
|
184
185
|
client = III("ws://fake", InitOptions())
|
|
185
186
|
time.sleep(0.05)
|
|
@@ -187,7 +188,7 @@ def test_public_methods_are_sync(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
187
188
|
import inspect
|
|
188
189
|
|
|
189
190
|
assert not inspect.iscoroutinefunction(client.trigger)
|
|
190
|
-
assert not inspect.iscoroutinefunction(
|
|
191
|
+
assert not inspect.iscoroutinefunction(create_channel)
|
|
191
192
|
|
|
192
193
|
client.shutdown()
|
|
193
194
|
|
|
@@ -546,7 +546,7 @@ wheels = [
|
|
|
546
546
|
|
|
547
547
|
[[package]]
|
|
548
548
|
name = "iii-observability"
|
|
549
|
-
version = "0.
|
|
549
|
+
version = "0.16.0.dev3"
|
|
550
550
|
source = { editable = "../observability" }
|
|
551
551
|
dependencies = [
|
|
552
552
|
{ name = "httpx" },
|
|
@@ -572,7 +572,7 @@ provides-extras = ["dev"]
|
|
|
572
572
|
|
|
573
573
|
[[package]]
|
|
574
574
|
name = "iii-sdk"
|
|
575
|
-
version = "0.
|
|
575
|
+
version = "0.16.0.dev3"
|
|
576
576
|
source = { editable = "." }
|
|
577
577
|
dependencies = [
|
|
578
578
|
{ name = "iii-observability" },
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{iii_sdk-0.17.0.dev1 → iii_sdk-0.18.0.dev1}/tests/test_http_external_functions_integration.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|