iii-sdk 0.21.4.dev2__tar.gz → 0.21.5.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.21.4.dev2 → iii_sdk-0.21.5.dev1}/PKG-INFO +2 -2
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/pyproject.toml +2 -2
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/src/iii/iii.py +9 -5
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/src/iii/iii_types.py +3 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_http_external_functions_integration.py +46 -76
- iii_sdk-0.21.5.dev1/tests/test_trigger_action.py +19 -0
- iii_sdk-0.21.4.dev2/tests/test_queue_integration.py +0 -363
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/.gitignore +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/README.md +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/src/iii/__init__.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/src/iii/channel.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/src/iii/channels.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/src/iii/engine.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/src/iii/errors.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/src/iii/format_utils.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/src/iii/helpers.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/src/iii/iii_constants.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/src/iii/internal.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/src/iii/otel_worker_gauges.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/src/iii/protocol.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/src/iii/runtime.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/src/iii/state.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/src/iii/stream.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/src/iii/trigger.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/src/iii/triggers.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/src/iii/types.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/src/iii/utils.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/src/iii/worker_metrics.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/conftest.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_api_triggers.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_async_api.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_baggage_span_processor.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_bridge.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_channel_close_delay.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_channel_submodule.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_context_propagation.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_data_channels.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_engine_constants.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_errors.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_format_utils.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_healthcheck.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_helpers.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_hold_process.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_iii_registration_dedup.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_init_api.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_invocation_exception.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_invocation_metadata.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_logger_function_ids.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_logger_otel.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_middleware.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_payload.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_pubsub.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_rbac_workers.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_register_function_args.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_runtime_submodule.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_span_ops.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_state.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_stream_models.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_stream_types.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_streams.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_streams_runtime_annotations.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_sync_api.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_telemetry.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_telemetry_exporters.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_telemetry_types.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_trace_helpers.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_trigger_metadata.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_trigger_registration_error.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_trigger_submodule.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_trigger_type_lifecycle.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_utils.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_worker_metadata.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_worker_metrics.py +0 -0
- {iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: iii-sdk
|
|
3
|
-
Version: 0.21.
|
|
3
|
+
Version: 0.21.5.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-helpers==0.21.
|
|
17
|
+
Requires-Dist: iii-helpers==0.21.5.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.21.
|
|
7
|
+
version = "0.21.5.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-helpers==0.21.
|
|
26
|
+
"iii-helpers==0.21.5.dev1",
|
|
27
27
|
]
|
|
28
28
|
|
|
29
29
|
[project.urls]
|
|
@@ -1130,7 +1130,8 @@ class III:
|
|
|
1130
1130
|
The routing behavior and return type depend on the ``action`` field:
|
|
1131
1131
|
|
|
1132
1132
|
- No action: synchronous -- waits for the function to return.
|
|
1133
|
-
- ``TriggerAction.Enqueue(...)``: async via named queue -- returns
|
|
1133
|
+
- ``TriggerAction.Enqueue(...)``: async via named queue -- returns a dict
|
|
1134
|
+
with ``messageReceiptId``.
|
|
1134
1135
|
- ``TriggerAction.Void()``: fire-and-forget -- returns ``None``.
|
|
1135
1136
|
|
|
1136
1137
|
Args:
|
|
@@ -1139,8 +1140,8 @@ class III:
|
|
|
1139
1140
|
|
|
1140
1141
|
Returns:
|
|
1141
1142
|
The function's return value for synchronous (no-action) calls,
|
|
1142
|
-
|
|
1143
|
-
actions.
|
|
1143
|
+
a ``{"messageReceiptId": ...}`` dict for enqueue actions, or
|
|
1144
|
+
``None`` for void actions.
|
|
1144
1145
|
|
|
1145
1146
|
Raises:
|
|
1146
1147
|
InvocationError: For any engine rejection. Inspect ``code``:
|
|
@@ -1159,7 +1160,8 @@ class III:
|
|
|
1159
1160
|
The routing behavior and return type depend on the ``action`` field:
|
|
1160
1161
|
|
|
1161
1162
|
- No action: synchronous -- waits for the function to return.
|
|
1162
|
-
- ``TriggerAction.Enqueue(...)``: async via named queue -- returns
|
|
1163
|
+
- ``TriggerAction.Enqueue(...)``: async via named queue -- returns a dict
|
|
1164
|
+
with ``messageReceiptId``.
|
|
1163
1165
|
- ``TriggerAction.Void()``: fire-and-forget -- returns ``None``.
|
|
1164
1166
|
|
|
1165
1167
|
Args:
|
|
@@ -1167,7 +1169,9 @@ class III:
|
|
|
1167
1169
|
and optional ``action`` / ``timeout_ms``.
|
|
1168
1170
|
|
|
1169
1171
|
Returns:
|
|
1170
|
-
The
|
|
1172
|
+
The function's return value for synchronous (no-action) calls,
|
|
1173
|
+
a ``{"messageReceiptId": ...}`` dict for enqueue actions, or
|
|
1174
|
+
``None`` for void actions.
|
|
1171
1175
|
|
|
1172
1176
|
Raises:
|
|
1173
1177
|
InvocationError: For any engine rejection. Inspect ``code``:
|
|
@@ -170,6 +170,9 @@ class RegisterFunctionMessage(BaseModel):
|
|
|
170
170
|
class TriggerActionEnqueue(BaseModel):
|
|
171
171
|
"""Routes the invocation through a named queue for async processing.
|
|
172
172
|
|
|
173
|
+
Requires a queue worker in the project — run ``iii worker add queue``.
|
|
174
|
+
Without it the trigger rejects with ``enqueue_error`` (no queue provider).
|
|
175
|
+
|
|
173
176
|
Attributes:
|
|
174
177
|
type: Always ``'enqueue'``.
|
|
175
178
|
queue: Name of the target queue.
|
{iii_sdk-0.21.4.dev2 → iii_sdk-0.21.5.dev1}/tests/test_http_external_functions_integration.py
RENAMED
|
@@ -21,10 +21,6 @@ def _unique_function_id(prefix: str) -> str:
|
|
|
21
21
|
return f"{prefix}::{int(time.time())}::{random.random():.10f}".replace(".", "")
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
def _unique_topic(prefix: str) -> str:
|
|
25
|
-
return f"{prefix}.{int(time.time())}.{random.random():.10f}".replace(".", "")
|
|
26
|
-
|
|
27
|
-
|
|
28
24
|
class WebhookProbe:
|
|
29
25
|
def __init__(self) -> None:
|
|
30
26
|
self._received: list[dict[str, Any]] = []
|
|
@@ -191,6 +187,21 @@ def _make_integration_client() -> III:
|
|
|
191
187
|
return client
|
|
192
188
|
|
|
193
189
|
|
|
190
|
+
async def _invoke(client: III, function_id: str, payload: dict[str, Any]) -> Any:
|
|
191
|
+
"""Directly invoke an HTTP external function without blocking the test loop.
|
|
192
|
+
|
|
193
|
+
``client.trigger`` is synchronous and blocks the caller until the engine's
|
|
194
|
+
webhook POST round-trips. Since the WebhookProbe server runs on this same
|
|
195
|
+
event loop, calling it inline would deadlock -- so we run it on a worker
|
|
196
|
+
thread and let the loop keep serving the probe.
|
|
197
|
+
"""
|
|
198
|
+
loop = asyncio.get_running_loop()
|
|
199
|
+
return await loop.run_in_executor(
|
|
200
|
+
None,
|
|
201
|
+
lambda: client.trigger({"function_id": function_id, "payload": payload}),
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
194
205
|
# ===========================================================================
|
|
195
206
|
# Unit tests (FakeWs)
|
|
196
207
|
# ===========================================================================
|
|
@@ -282,16 +293,14 @@ def test_unregister_removes_function_from_sent_messages(monkeypatch: pytest.Monk
|
|
|
282
293
|
|
|
283
294
|
|
|
284
295
|
@pytest.mark.asyncio
|
|
285
|
-
async def
|
|
296
|
+
async def test_delivers_events_to_external_http_function() -> None:
|
|
286
297
|
client = _make_integration_client()
|
|
287
298
|
|
|
288
299
|
probe = WebhookProbe()
|
|
289
300
|
await probe.start()
|
|
290
301
|
|
|
291
302
|
function_id = _unique_function_id("test::http_external::target")
|
|
292
|
-
topic = _unique_topic("test.http_external.topic")
|
|
293
303
|
payload = {"hello": "world", "count": 1}
|
|
294
|
-
trigger = None
|
|
295
304
|
http_fn = None
|
|
296
305
|
|
|
297
306
|
try:
|
|
@@ -301,21 +310,16 @@ async def test_delivers_queue_events_to_external_http_function() -> None:
|
|
|
301
310
|
)
|
|
302
311
|
time.sleep(0.5)
|
|
303
312
|
|
|
304
|
-
|
|
305
|
-
{"type": "durable:subscriber", "function_id": function_id, "config": {"topic": topic}}
|
|
306
|
-
)
|
|
307
|
-
time.sleep(0.5)
|
|
308
|
-
|
|
309
|
-
client.trigger({"function_id": "iii::durable::publish", "payload": {"topic": topic, "data": payload}})
|
|
313
|
+
await _invoke(client, function_id, payload)
|
|
310
314
|
|
|
311
315
|
webhook = await probe.wait_for_webhook(7.0)
|
|
312
316
|
|
|
313
317
|
assert webhook["method"] == "POST"
|
|
314
318
|
assert webhook["url"] == "/webhook"
|
|
315
|
-
|
|
319
|
+
# Direct invocation injects caller metadata (e.g. _caller_worker_id) into
|
|
320
|
+
# the POSTed body, so assert the expected payload is a subset.
|
|
321
|
+
assert payload.items() <= webhook["body"].items()
|
|
316
322
|
finally:
|
|
317
|
-
if trigger:
|
|
318
|
-
trigger.unregister()
|
|
319
323
|
if http_fn:
|
|
320
324
|
http_fn.unregister()
|
|
321
325
|
await probe.close()
|
|
@@ -381,9 +385,7 @@ async def test_delivers_events_with_custom_headers() -> None:
|
|
|
381
385
|
await probe.start()
|
|
382
386
|
|
|
383
387
|
function_id = _unique_function_id("test::http_external::custom_headers")
|
|
384
|
-
topic = _unique_topic("test.http_external.headers")
|
|
385
388
|
payload = {"event": "custom_header_test"}
|
|
386
|
-
trigger = None
|
|
387
389
|
http_fn = None
|
|
388
390
|
|
|
389
391
|
try:
|
|
@@ -398,17 +400,14 @@ async def test_delivers_events_with_custom_headers() -> None:
|
|
|
398
400
|
)
|
|
399
401
|
time.sleep(0.5)
|
|
400
402
|
|
|
401
|
-
|
|
402
|
-
{"type": "durable:subscriber", "function_id": function_id, "config": {"topic": topic}}
|
|
403
|
-
)
|
|
404
|
-
time.sleep(0.5)
|
|
405
|
-
|
|
406
|
-
client.trigger({"function_id": "iii::durable::publish", "payload": {"topic": topic, "data": payload}})
|
|
403
|
+
await _invoke(client, function_id, payload)
|
|
407
404
|
|
|
408
405
|
webhook = await probe.wait_for_webhook(7.0)
|
|
409
406
|
|
|
410
407
|
assert webhook["method"] == "POST"
|
|
411
|
-
|
|
408
|
+
# Direct invocation injects caller metadata (e.g. _caller_worker_id) into
|
|
409
|
+
# the POSTed body, so assert the expected payload is a subset.
|
|
410
|
+
assert payload.items() <= webhook["body"].items()
|
|
412
411
|
|
|
413
412
|
# Verify custom headers were forwarded by the engine.
|
|
414
413
|
# Header keys may be lowercased by the HTTP client.
|
|
@@ -418,8 +417,6 @@ async def test_delivers_events_with_custom_headers() -> None:
|
|
|
418
417
|
)
|
|
419
418
|
assert received_headers.get("x-another") == "123", f"Expected x-another=123, got headers: {received_headers}"
|
|
420
419
|
finally:
|
|
421
|
-
if trigger:
|
|
422
|
-
trigger.unregister()
|
|
423
420
|
if http_fn:
|
|
424
421
|
http_fn.unregister()
|
|
425
422
|
await probe.close()
|
|
@@ -437,15 +434,11 @@ async def test_delivers_events_to_multiple_external_functions() -> None:
|
|
|
437
434
|
|
|
438
435
|
fn_id_a = _unique_function_id("test::http_external::multi_a")
|
|
439
436
|
fn_id_b = _unique_function_id("test::http_external::multi_b")
|
|
440
|
-
topic_a = _unique_topic("test.http_external.multi_a")
|
|
441
|
-
topic_b = _unique_topic("test.http_external.multi_b")
|
|
442
437
|
payload_a = {"source": "topic_a", "value": 1}
|
|
443
438
|
payload_b = {"source": "topic_b", "value": 2}
|
|
444
439
|
|
|
445
440
|
http_fn_a = None
|
|
446
441
|
http_fn_b = None
|
|
447
|
-
trigger_a = None
|
|
448
|
-
trigger_b = None
|
|
449
442
|
|
|
450
443
|
try:
|
|
451
444
|
http_fn_a = client.register_function(
|
|
@@ -458,31 +451,20 @@ async def test_delivers_events_to_multiple_external_functions() -> None:
|
|
|
458
451
|
)
|
|
459
452
|
time.sleep(0.5)
|
|
460
453
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
)
|
|
464
|
-
trigger_b = client.register_trigger(
|
|
465
|
-
{"type": "durable:subscriber", "function_id": fn_id_b, "config": {"topic": topic_b}}
|
|
466
|
-
)
|
|
467
|
-
time.sleep(0.5)
|
|
468
|
-
|
|
469
|
-
client.trigger({"function_id": "iii::durable::publish", "payload": {"topic": topic_a, "data": payload_a}})
|
|
470
|
-
client.trigger({"function_id": "iii::durable::publish", "payload": {"topic": topic_b, "data": payload_b}})
|
|
454
|
+
await _invoke(client, fn_id_a, payload_a)
|
|
455
|
+
await _invoke(client, fn_id_b, payload_b)
|
|
471
456
|
|
|
472
457
|
webhook_a = await probe_a.wait_for_webhook(7.0)
|
|
473
458
|
webhook_b = await probe_b.wait_for_webhook(7.0)
|
|
474
459
|
|
|
475
|
-
# Each probe should receive only its own
|
|
476
|
-
|
|
460
|
+
# Each probe should receive only its own function's event. Direct
|
|
461
|
+
# invocation injects caller metadata into the body, so assert subset.
|
|
462
|
+
assert payload_a.items() <= webhook_a["body"].items(), f"probe_a got wrong body: {webhook_a['body']}"
|
|
477
463
|
assert webhook_a["url"] == "/hook_a"
|
|
478
464
|
|
|
479
|
-
assert webhook_b["body"]
|
|
465
|
+
assert payload_b.items() <= webhook_b["body"].items(), f"probe_b got wrong body: {webhook_b['body']}"
|
|
480
466
|
assert webhook_b["url"] == "/hook_b"
|
|
481
467
|
finally:
|
|
482
|
-
if trigger_a:
|
|
483
|
-
trigger_a.unregister()
|
|
484
|
-
if trigger_b:
|
|
485
|
-
trigger_b.unregister()
|
|
486
468
|
if http_fn_a:
|
|
487
469
|
http_fn_a.unregister()
|
|
488
470
|
if http_fn_b:
|
|
@@ -500,10 +482,8 @@ async def test_stops_delivering_after_unregister() -> None:
|
|
|
500
482
|
await probe.start()
|
|
501
483
|
|
|
502
484
|
function_id = _unique_function_id("test::http_external::stop_deliver")
|
|
503
|
-
topic = _unique_topic("test.http_external.stop_deliver")
|
|
504
485
|
payload_before = {"phase": "before_unregister"}
|
|
505
486
|
payload_after = {"phase": "after_unregister"}
|
|
506
|
-
trigger = None
|
|
507
487
|
http_fn = None
|
|
508
488
|
|
|
509
489
|
try:
|
|
@@ -513,30 +493,27 @@ async def test_stops_delivering_after_unregister() -> None:
|
|
|
513
493
|
)
|
|
514
494
|
time.sleep(0.5)
|
|
515
495
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
)
|
|
519
|
-
time.sleep(0.5)
|
|
520
|
-
|
|
521
|
-
# First enqueue -- should be delivered.
|
|
522
|
-
client.trigger({"function_id": "iii::durable::publish", "payload": {"topic": topic, "data": payload_before}})
|
|
496
|
+
# First invocation -- should be delivered.
|
|
497
|
+
await _invoke(client, function_id, payload_before)
|
|
523
498
|
webhook = await probe.wait_for_webhook(7.0)
|
|
524
|
-
|
|
499
|
+
# Direct invocation injects caller metadata (e.g. _caller_worker_id) into
|
|
500
|
+
# the POSTed body, so assert the expected payload is a subset.
|
|
501
|
+
assert payload_before.items() <= webhook["body"].items()
|
|
525
502
|
|
|
526
|
-
# Unregister
|
|
527
|
-
trigger.unregister()
|
|
528
|
-
trigger = None
|
|
503
|
+
# Unregister the function.
|
|
529
504
|
http_fn.unregister()
|
|
530
505
|
http_fn = None
|
|
531
506
|
time.sleep(0.5)
|
|
532
507
|
|
|
533
|
-
# Second
|
|
534
|
-
|
|
508
|
+
# Second invocation -- the function is gone, so the trigger is rejected
|
|
509
|
+
# and nothing reaches the webhook.
|
|
510
|
+
try:
|
|
511
|
+
await _invoke(client, function_id, payload_after)
|
|
512
|
+
except Exception:
|
|
513
|
+
pass
|
|
535
514
|
no_delivery = await probe.wait_for_webhook_or_none(timeout=2.0)
|
|
536
515
|
assert no_delivery is None, f"Expected no delivery after unregister, but got: {no_delivery}"
|
|
537
516
|
finally:
|
|
538
|
-
if trigger:
|
|
539
|
-
trigger.unregister()
|
|
540
517
|
if http_fn:
|
|
541
518
|
http_fn.unregister()
|
|
542
519
|
await probe.close()
|
|
@@ -551,9 +528,7 @@ async def test_delivers_with_put_method() -> None:
|
|
|
551
528
|
await probe.start()
|
|
552
529
|
|
|
553
530
|
function_id = _unique_function_id("test::http_external::put_method")
|
|
554
|
-
topic = _unique_topic("test.http_external.put_method")
|
|
555
531
|
payload = {"action": "update", "id": 42}
|
|
556
|
-
trigger = None
|
|
557
532
|
http_fn = None
|
|
558
533
|
|
|
559
534
|
try:
|
|
@@ -563,20 +538,15 @@ async def test_delivers_with_put_method() -> None:
|
|
|
563
538
|
)
|
|
564
539
|
time.sleep(0.5)
|
|
565
540
|
|
|
566
|
-
|
|
567
|
-
{"type": "durable:subscriber", "function_id": function_id, "config": {"topic": topic}}
|
|
568
|
-
)
|
|
569
|
-
time.sleep(0.5)
|
|
570
|
-
|
|
571
|
-
client.trigger({"function_id": "iii::durable::publish", "payload": {"topic": topic, "data": payload}})
|
|
541
|
+
await _invoke(client, function_id, payload)
|
|
572
542
|
|
|
573
543
|
webhook = await probe.wait_for_webhook(7.0)
|
|
574
544
|
|
|
575
545
|
assert webhook["method"] == "PUT", f"Expected PUT, got {webhook['method']}"
|
|
576
|
-
|
|
546
|
+
# Direct invocation injects caller metadata (e.g. _caller_worker_id) into
|
|
547
|
+
# the POSTed body, so assert the expected payload is a subset.
|
|
548
|
+
assert payload.items() <= webhook["body"].items()
|
|
577
549
|
finally:
|
|
578
|
-
if trigger:
|
|
579
|
-
trigger.unregister()
|
|
580
550
|
if http_fn:
|
|
581
551
|
http_fn.unregister()
|
|
582
552
|
await probe.close()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Contract test for the SDK-owned TriggerAction builders.
|
|
2
|
+
|
|
3
|
+
The queue *behavior* (delivery, ordering, DLQ, durable fan-out) is exercised by
|
|
4
|
+
the standalone queue worker's own suite; here we only pin the wire shape the
|
|
5
|
+
engine's ``TriggerAction`` deserialization depends on, so it needs no live engine.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from iii.iii_types import TriggerActionEnqueue, TriggerActionVoid
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_enqueue_serializes_to_type_and_queue() -> None:
|
|
12
|
+
assert TriggerActionEnqueue(queue="orders").model_dump() == {
|
|
13
|
+
"type": "enqueue",
|
|
14
|
+
"queue": "orders",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_void_serializes_to_type() -> None:
|
|
19
|
+
assert TriggerActionVoid().model_dump() == {"type": "void"}
|
|
@@ -1,363 +0,0 @@
|
|
|
1
|
-
"""Integration tests for queue enqueue and void trigger actions."""
|
|
2
|
-
|
|
3
|
-
import time
|
|
4
|
-
|
|
5
|
-
from iii import TriggerAction
|
|
6
|
-
from iii.iii import III
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def wait_for(condition, timeout=5.0, interval=0.1):
|
|
10
|
-
"""Poll until condition() is truthy or timeout."""
|
|
11
|
-
deadline = time.monotonic() + timeout
|
|
12
|
-
while time.monotonic() < deadline:
|
|
13
|
-
if condition():
|
|
14
|
-
return
|
|
15
|
-
time.sleep(interval)
|
|
16
|
-
raise TimeoutError(f"Condition not met within {timeout}s")
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _wait_for_registration(iii_client: III, function_id: str, timeout: float = 5.0) -> None:
|
|
20
|
-
"""Poll engine::functions::list until function_id is registered, or TimeoutError."""
|
|
21
|
-
deadline = time.monotonic() + timeout
|
|
22
|
-
while time.monotonic() < deadline:
|
|
23
|
-
try:
|
|
24
|
-
result = iii_client.trigger({"function_id": "engine::functions::list", "payload": {}})
|
|
25
|
-
functions = result.get("functions", []) if isinstance(result, dict) else []
|
|
26
|
-
ids = [f.get("function_id") for f in functions if isinstance(f, dict)]
|
|
27
|
-
if function_id in ids:
|
|
28
|
-
return
|
|
29
|
-
except Exception:
|
|
30
|
-
pass
|
|
31
|
-
time.sleep(0.1)
|
|
32
|
-
raise TimeoutError(f"Function {function_id} was not registered within {timeout}s")
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def test_enqueue_delivers_message_to_function(iii_client: III):
|
|
36
|
-
"""Enqueue action delivers payload to the registered consumer function."""
|
|
37
|
-
received = []
|
|
38
|
-
|
|
39
|
-
def consumer_handler(input_data):
|
|
40
|
-
received.append(input_data)
|
|
41
|
-
return None
|
|
42
|
-
|
|
43
|
-
ref = iii_client.register_function("test.queue.py.consumer", consumer_handler)
|
|
44
|
-
time.sleep(0.3)
|
|
45
|
-
|
|
46
|
-
try:
|
|
47
|
-
result = iii_client.trigger(
|
|
48
|
-
{
|
|
49
|
-
"function_id": "test.queue.py.consumer",
|
|
50
|
-
"payload": {"order": "pizza"},
|
|
51
|
-
"action": TriggerAction.Enqueue(queue="test-orders"),
|
|
52
|
-
}
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
assert isinstance(result, dict)
|
|
56
|
-
assert isinstance(result["messageReceiptId"], str)
|
|
57
|
-
|
|
58
|
-
wait_for(lambda: len(received) > 0, timeout=5.0)
|
|
59
|
-
|
|
60
|
-
assert received[0]["order"] == "pizza"
|
|
61
|
-
finally:
|
|
62
|
-
ref.unregister()
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def test_void_trigger_returns_none(iii_client: III):
|
|
66
|
-
"""Void trigger action fires without waiting and returns None."""
|
|
67
|
-
calls = []
|
|
68
|
-
|
|
69
|
-
def void_consumer_handler(input_data):
|
|
70
|
-
calls.append(input_data)
|
|
71
|
-
return None
|
|
72
|
-
|
|
73
|
-
ref = iii_client.register_function("test.queue.py.void-consumer", void_consumer_handler)
|
|
74
|
-
time.sleep(0.3)
|
|
75
|
-
|
|
76
|
-
try:
|
|
77
|
-
result = iii_client.trigger(
|
|
78
|
-
{
|
|
79
|
-
"function_id": "test.queue.py.void-consumer",
|
|
80
|
-
"payload": {"msg": "fire"},
|
|
81
|
-
"action": TriggerAction.Void(),
|
|
82
|
-
}
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
assert result is None
|
|
86
|
-
|
|
87
|
-
wait_for(lambda: len(calls) > 0, timeout=5.0)
|
|
88
|
-
|
|
89
|
-
assert calls[0]["msg"] == "fire"
|
|
90
|
-
finally:
|
|
91
|
-
ref.unregister()
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def test_enqueue_multiple_messages(iii_client: III):
|
|
95
|
-
"""Multiple enqueued messages are all delivered to the consumer."""
|
|
96
|
-
received = []
|
|
97
|
-
|
|
98
|
-
def multi_handler(input_data):
|
|
99
|
-
received.append(input_data)
|
|
100
|
-
return None
|
|
101
|
-
|
|
102
|
-
ref = iii_client.register_function("test.queue.py.multi", multi_handler)
|
|
103
|
-
time.sleep(0.3)
|
|
104
|
-
|
|
105
|
-
try:
|
|
106
|
-
for i in range(5):
|
|
107
|
-
iii_client.trigger(
|
|
108
|
-
{
|
|
109
|
-
"function_id": "test.queue.py.multi",
|
|
110
|
-
"payload": {"index": i},
|
|
111
|
-
"action": TriggerAction.Enqueue(queue="test-multi"),
|
|
112
|
-
}
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
wait_for(lambda: len(received) == 5, timeout=10.0)
|
|
116
|
-
|
|
117
|
-
assert len(received) == 5
|
|
118
|
-
received_indices = sorted(item["index"] for item in received)
|
|
119
|
-
assert received_indices == [0, 1, 2, 3, 4]
|
|
120
|
-
finally:
|
|
121
|
-
ref.unregister()
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def test_chained_enqueue(iii_client: III):
|
|
125
|
-
"""Function A enqueues a message to function B, forming a chain."""
|
|
126
|
-
chain_received = []
|
|
127
|
-
|
|
128
|
-
def chain_a_handler(input_data):
|
|
129
|
-
iii_client.trigger(
|
|
130
|
-
{
|
|
131
|
-
"function_id": "test.queue.py.chain-b",
|
|
132
|
-
"payload": {**input_data, "chained": True},
|
|
133
|
-
"action": TriggerAction.Enqueue(queue="test-chain"),
|
|
134
|
-
}
|
|
135
|
-
)
|
|
136
|
-
return input_data
|
|
137
|
-
|
|
138
|
-
def chain_b_handler(input_data):
|
|
139
|
-
chain_received.append(input_data)
|
|
140
|
-
return None
|
|
141
|
-
|
|
142
|
-
ref_a = iii_client.register_function("test.queue.py.chain-a", chain_a_handler)
|
|
143
|
-
ref_b = iii_client.register_function("test.queue.py.chain-b", chain_b_handler)
|
|
144
|
-
time.sleep(0.3)
|
|
145
|
-
|
|
146
|
-
try:
|
|
147
|
-
iii_client.trigger(
|
|
148
|
-
{
|
|
149
|
-
"function_id": "test.queue.py.chain-a",
|
|
150
|
-
"payload": {"origin": "test"},
|
|
151
|
-
"action": TriggerAction.Enqueue(queue="test-chain"),
|
|
152
|
-
}
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
wait_for(lambda: len(chain_received) > 0, timeout=5.0)
|
|
156
|
-
|
|
157
|
-
assert chain_received[0]["chained"] is True
|
|
158
|
-
assert chain_received[0]["origin"] == "test"
|
|
159
|
-
finally:
|
|
160
|
-
ref_b.unregister()
|
|
161
|
-
ref_a.unregister()
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
# ---------------------------------------------------------------------------
|
|
165
|
-
# Durable subscriber / publisher scenarios (ported from motia-py, MOT-3109).
|
|
166
|
-
# These exercise the `durable:subscriber` trigger + `iii::durable::publish`
|
|
167
|
-
# path, complementing the TriggerAction.Enqueue tests above.
|
|
168
|
-
# ---------------------------------------------------------------------------
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
def test_durable_publish_delivers_to_subscribed_handler(iii_client: III):
|
|
172
|
-
"""Publishing to a topic delivers the message to a subscribed handler."""
|
|
173
|
-
function_id = f"test.queue.py.durable.basic.{int(time.time() * 1000)}"
|
|
174
|
-
topic = f"test-topic-{int(time.time() * 1000)}"
|
|
175
|
-
received: list = []
|
|
176
|
-
|
|
177
|
-
def handler(data):
|
|
178
|
-
received.append(data)
|
|
179
|
-
return None
|
|
180
|
-
|
|
181
|
-
fn_ref = iii_client.register_function(function_id, handler)
|
|
182
|
-
trigger = iii_client.register_trigger(
|
|
183
|
-
{"type": "durable:subscriber", "function_id": function_id, "config": {"topic": topic}}
|
|
184
|
-
)
|
|
185
|
-
time.sleep(0.5)
|
|
186
|
-
|
|
187
|
-
try:
|
|
188
|
-
iii_client.trigger(
|
|
189
|
-
{
|
|
190
|
-
"function_id": "iii::durable::publish",
|
|
191
|
-
"payload": {"topic": topic, "data": {"order": "abc"}},
|
|
192
|
-
}
|
|
193
|
-
)
|
|
194
|
-
wait_for(lambda: len(received) > 0, timeout=5.0)
|
|
195
|
-
assert received == [{"order": "abc"}]
|
|
196
|
-
finally:
|
|
197
|
-
trigger.unregister()
|
|
198
|
-
fn_ref.unregister()
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
def test_durable_publish_delivers_exact_payload(iii_client: III):
|
|
202
|
-
"""Handler receives the exact published payload without mutation."""
|
|
203
|
-
function_id = f"test.queue.py.durable.payload.{int(time.time() * 1000)}"
|
|
204
|
-
topic = f"test-topic-payload-{int(time.time() * 1000)}"
|
|
205
|
-
payload = {"id": "x1", "count": 42, "nested": {"a": 1}}
|
|
206
|
-
received: list = []
|
|
207
|
-
|
|
208
|
-
def handler(data):
|
|
209
|
-
received.append(data)
|
|
210
|
-
return None
|
|
211
|
-
|
|
212
|
-
fn_ref = iii_client.register_function(function_id, handler)
|
|
213
|
-
trigger = iii_client.register_trigger(
|
|
214
|
-
{"type": "durable:subscriber", "function_id": function_id, "config": {"topic": topic}}
|
|
215
|
-
)
|
|
216
|
-
time.sleep(0.5)
|
|
217
|
-
|
|
218
|
-
try:
|
|
219
|
-
iii_client.trigger(
|
|
220
|
-
{
|
|
221
|
-
"function_id": "iii::durable::publish",
|
|
222
|
-
"payload": {"topic": topic, "data": payload},
|
|
223
|
-
}
|
|
224
|
-
)
|
|
225
|
-
wait_for(lambda: len(received) > 0, timeout=5.0)
|
|
226
|
-
assert received == [payload]
|
|
227
|
-
finally:
|
|
228
|
-
trigger.unregister()
|
|
229
|
-
fn_ref.unregister()
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
def test_durable_subscriber_with_queue_config(iii_client: III):
|
|
233
|
-
"""Subscription with queue_config (maxRetries, concurrency, type) still delivers."""
|
|
234
|
-
function_id = f"test.queue.py.durable.infra.{int(time.time() * 1000)}"
|
|
235
|
-
topic = f"test-topic-infra-{int(time.time() * 1000)}"
|
|
236
|
-
received: list = []
|
|
237
|
-
|
|
238
|
-
def handler(data):
|
|
239
|
-
received.append(data)
|
|
240
|
-
return None
|
|
241
|
-
|
|
242
|
-
fn_ref = iii_client.register_function(function_id, handler)
|
|
243
|
-
trigger = iii_client.register_trigger(
|
|
244
|
-
{
|
|
245
|
-
"type": "durable:subscriber",
|
|
246
|
-
"function_id": function_id,
|
|
247
|
-
"config": {
|
|
248
|
-
"topic": topic,
|
|
249
|
-
"queue_config": {
|
|
250
|
-
"maxRetries": 5,
|
|
251
|
-
"type": "standard",
|
|
252
|
-
"concurrency": 2,
|
|
253
|
-
},
|
|
254
|
-
},
|
|
255
|
-
}
|
|
256
|
-
)
|
|
257
|
-
time.sleep(0.5)
|
|
258
|
-
|
|
259
|
-
try:
|
|
260
|
-
iii_client.trigger(
|
|
261
|
-
{
|
|
262
|
-
"function_id": "iii::durable::publish",
|
|
263
|
-
"payload": {"topic": topic, "data": {"infra": True}},
|
|
264
|
-
}
|
|
265
|
-
)
|
|
266
|
-
wait_for(lambda: len(received) > 0, timeout=5.0)
|
|
267
|
-
assert received == [{"infra": True}]
|
|
268
|
-
finally:
|
|
269
|
-
trigger.unregister()
|
|
270
|
-
fn_ref.unregister()
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
def test_durable_multiple_subscribers_fanout(iii_client: III):
|
|
274
|
-
"""Multiple subscribers on the same topic each receive every message."""
|
|
275
|
-
topic = f"test-topic-multi-{int(time.time() * 1000)}"
|
|
276
|
-
function_id1 = f"test.queue.py.durable.multi1.{int(time.time() * 1000)}"
|
|
277
|
-
function_id2 = f"test.queue.py.durable.multi2.{int(time.time() * 1000)}"
|
|
278
|
-
received1: list = []
|
|
279
|
-
received2: list = []
|
|
280
|
-
|
|
281
|
-
def handler1(data):
|
|
282
|
-
received1.append(data)
|
|
283
|
-
return None
|
|
284
|
-
|
|
285
|
-
def handler2(data):
|
|
286
|
-
received2.append(data)
|
|
287
|
-
return None
|
|
288
|
-
|
|
289
|
-
fn1 = iii_client.register_function(function_id1, handler1)
|
|
290
|
-
fn2 = iii_client.register_function(function_id2, handler2)
|
|
291
|
-
trigger1 = iii_client.register_trigger(
|
|
292
|
-
{"type": "durable:subscriber", "function_id": function_id1, "config": {"topic": topic}}
|
|
293
|
-
)
|
|
294
|
-
trigger2 = iii_client.register_trigger(
|
|
295
|
-
{"type": "durable:subscriber", "function_id": function_id2, "config": {"topic": topic}}
|
|
296
|
-
)
|
|
297
|
-
time.sleep(0.5)
|
|
298
|
-
|
|
299
|
-
try:
|
|
300
|
-
iii_client.trigger({"function_id": "iii::durable::publish", "payload": {"topic": topic, "data": {"msg": 1}}})
|
|
301
|
-
iii_client.trigger({"function_id": "iii::durable::publish", "payload": {"topic": topic, "data": {"msg": 2}}})
|
|
302
|
-
wait_for(lambda: len(received1) >= 2 and len(received2) >= 2, timeout=8.0)
|
|
303
|
-
|
|
304
|
-
assert len(received1) == 2
|
|
305
|
-
assert len(received2) == 2
|
|
306
|
-
assert {"msg": 1} in received1
|
|
307
|
-
assert {"msg": 2} in received1
|
|
308
|
-
assert {"msg": 1} in received2
|
|
309
|
-
assert {"msg": 2} in received2
|
|
310
|
-
finally:
|
|
311
|
-
trigger1.unregister()
|
|
312
|
-
trigger2.unregister()
|
|
313
|
-
fn1.unregister()
|
|
314
|
-
fn2.unregister()
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
def test_durable_condition_function_filters_messages(iii_client: III):
|
|
318
|
-
"""Registering a condition_function_id filters out non-matching messages."""
|
|
319
|
-
topic = f"test-topic-cond-{int(time.time() * 1000)}"
|
|
320
|
-
function_id = f"test.queue.py.durable.cond.{int(time.time() * 1000)}"
|
|
321
|
-
condition_path = f"{function_id}::conditions::0"
|
|
322
|
-
handler_calls: list = []
|
|
323
|
-
|
|
324
|
-
def handler(data):
|
|
325
|
-
handler_calls.append(data)
|
|
326
|
-
return None
|
|
327
|
-
|
|
328
|
-
def condition(input_data):
|
|
329
|
-
return bool(input_data.get("accept"))
|
|
330
|
-
|
|
331
|
-
fn_ref = iii_client.register_function(function_id, handler)
|
|
332
|
-
cond_ref = iii_client.register_function(condition_path, condition)
|
|
333
|
-
trigger = iii_client.register_trigger(
|
|
334
|
-
{
|
|
335
|
-
"type": "durable:subscriber",
|
|
336
|
-
"function_id": function_id,
|
|
337
|
-
"config": {
|
|
338
|
-
"topic": topic,
|
|
339
|
-
"condition_function_id": condition_path,
|
|
340
|
-
},
|
|
341
|
-
}
|
|
342
|
-
)
|
|
343
|
-
|
|
344
|
-
try:
|
|
345
|
-
_wait_for_registration(iii_client, function_id)
|
|
346
|
-
_wait_for_registration(iii_client, condition_path)
|
|
347
|
-
|
|
348
|
-
iii_client.trigger(
|
|
349
|
-
{"function_id": "iii::durable::publish", "payload": {"topic": topic, "data": {"accept": False}}}
|
|
350
|
-
)
|
|
351
|
-
iii_client.trigger(
|
|
352
|
-
{"function_id": "iii::durable::publish", "payload": {"topic": topic, "data": {"accept": True}}}
|
|
353
|
-
)
|
|
354
|
-
wait_for(lambda: len(handler_calls) >= 1, timeout=8.0)
|
|
355
|
-
# Give the rejected message a moment to ensure it does NOT arrive.
|
|
356
|
-
time.sleep(1.0)
|
|
357
|
-
|
|
358
|
-
assert len(handler_calls) == 1
|
|
359
|
-
assert handler_calls[0] == {"accept": True}
|
|
360
|
-
finally:
|
|
361
|
-
trigger.unregister()
|
|
362
|
-
cond_ref.unregister()
|
|
363
|
-
fn_ref.unregister()
|
|
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
|
|
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
|
|
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
|