iii-sdk 0.19.0.dev2__tar.gz → 0.19.0.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.19.0.dev2 → iii_sdk-0.19.0.dev3}/PKG-INFO +2 -2
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/pyproject.toml +2 -2
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/src/iii/__init__.py +1 -56
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/src/iii/errors.py +10 -26
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/src/iii/iii.py +19 -11
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_errors.py +17 -32
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_invocation_exception.py +2 -2
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_rbac_workers.py +5 -7
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/uv.lock +2 -2
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/.gitignore +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/README.md +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/src/iii/channels.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/src/iii/format_utils.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/src/iii/helpers.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/src/iii/iii_constants.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/src/iii/iii_types.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/src/iii/otel_worker_gauges.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/src/iii/state.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/src/iii/stream.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/src/iii/triggers.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/src/iii/types.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/src/iii/utils.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/src/iii/worker_metrics.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/conftest.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_api_triggers.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_async_api.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_baggage_span_processor.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_bridge.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_channel_close_delay.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_context_propagation.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_data_channels.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_format_utils.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_healthcheck.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_helpers.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_hold_process.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_http_external_functions_integration.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_iii_registration_dedup.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_init_api.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_logger_function_ids.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_logger_otel.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_middleware.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_payload.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_pubsub.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_queue_integration.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_register_function_args.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_span_ops.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_state.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_stream_models.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_streams.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_streams_runtime_annotations.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_sync_api.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_telemetry.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_telemetry_exporters.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_telemetry_types.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_trace_helpers.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_trigger_metadata.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_trigger_registration_error.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_trigger_type_lifecycle.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_utils.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/tests/test_worker_metadata.py +0 -0
- {iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.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.19.0.
|
|
3
|
+
Version: 0.19.0.dev3
|
|
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.19.0.
|
|
17
|
+
Requires-Dist: iii-observability==0.19.0.dev3
|
|
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.19.0.
|
|
7
|
+
version = "0.19.0.dev3"
|
|
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.19.0.
|
|
26
|
+
"iii-observability==0.19.0.dev3",
|
|
27
27
|
]
|
|
28
28
|
|
|
29
29
|
[project.urls]
|
|
@@ -1,34 +1,7 @@
|
|
|
1
1
|
"""III SDK for Python."""
|
|
2
2
|
|
|
3
|
-
from iii_observability import (
|
|
4
|
-
DEFAULT_ALLOWLIST,
|
|
5
|
-
REDACTED_PLACEHOLDER,
|
|
6
|
-
BaggageSpanProcessor,
|
|
7
|
-
Logger,
|
|
8
|
-
OtelConfig,
|
|
9
|
-
ReconnectionConfig,
|
|
10
|
-
current_span_id,
|
|
11
|
-
current_span_is_recording,
|
|
12
|
-
current_trace_id,
|
|
13
|
-
execute_traced_request,
|
|
14
|
-
extract_baggage,
|
|
15
|
-
extract_traceparent,
|
|
16
|
-
flush_otel,
|
|
17
|
-
init_otel,
|
|
18
|
-
inject_baggage,
|
|
19
|
-
inject_traceparent,
|
|
20
|
-
record_span_event,
|
|
21
|
-
redact,
|
|
22
|
-
redact_and_truncate,
|
|
23
|
-
resolve_max_bytes_from_env,
|
|
24
|
-
set_current_span_attribute,
|
|
25
|
-
set_current_span_error,
|
|
26
|
-
shutdown_otel,
|
|
27
|
-
with_span,
|
|
28
|
-
)
|
|
29
|
-
|
|
30
3
|
from .channels import ChannelReader, ChannelWriter
|
|
31
|
-
from .errors import
|
|
4
|
+
from .errors import IIIInvocationError
|
|
32
5
|
from .format_utils import extract_request_format, extract_response_format, python_type_to_format
|
|
33
6
|
from .iii import TriggerAction, register_worker
|
|
34
7
|
from .iii_constants import FunctionRef, InitOptions, TelemetryOptions
|
|
@@ -80,40 +53,14 @@ from .types import (
|
|
|
80
53
|
from .utils import http
|
|
81
54
|
|
|
82
55
|
__all__ = [
|
|
83
|
-
# Telemetry helpers
|
|
84
|
-
"BaggageSpanProcessor",
|
|
85
|
-
"DEFAULT_ALLOWLIST",
|
|
86
|
-
"REDACTED_PLACEHOLDER",
|
|
87
|
-
"current_span_id",
|
|
88
|
-
"current_span_is_recording",
|
|
89
|
-
"current_trace_id",
|
|
90
|
-
"execute_traced_request",
|
|
91
|
-
"extract_baggage",
|
|
92
|
-
"extract_traceparent",
|
|
93
|
-
"flush_otel",
|
|
94
|
-
"init_otel",
|
|
95
|
-
"inject_baggage",
|
|
96
|
-
"inject_traceparent",
|
|
97
|
-
"record_span_event",
|
|
98
|
-
"redact",
|
|
99
|
-
"redact_and_truncate",
|
|
100
|
-
"resolve_max_bytes_from_env",
|
|
101
|
-
"set_current_span_attribute",
|
|
102
|
-
"set_current_span_error",
|
|
103
|
-
"shutdown_otel",
|
|
104
|
-
"with_span",
|
|
105
56
|
# Channels
|
|
106
57
|
"ChannelReader",
|
|
107
58
|
"ChannelWriter",
|
|
108
59
|
# Errors
|
|
109
|
-
"IIIForbiddenError",
|
|
110
60
|
"IIIInvocationError",
|
|
111
|
-
"IIITimeoutError",
|
|
112
61
|
# Core
|
|
113
62
|
"FunctionRef",
|
|
114
63
|
"InitOptions",
|
|
115
|
-
"OtelConfig",
|
|
116
|
-
"ReconnectionConfig",
|
|
117
64
|
"register_worker",
|
|
118
65
|
"TelemetryOptions",
|
|
119
66
|
"TriggerAction",
|
|
@@ -142,8 +89,6 @@ __all__ = [
|
|
|
142
89
|
"TriggerActionEnqueue",
|
|
143
90
|
"TriggerActionVoid",
|
|
144
91
|
"TriggerRequest",
|
|
145
|
-
# Logger
|
|
146
|
-
"Logger",
|
|
147
92
|
# Triggers
|
|
148
93
|
"Trigger",
|
|
149
94
|
"TriggerConfig",
|
|
@@ -6,13 +6,10 @@ from typing import Any
|
|
|
6
6
|
class IIIInvocationError(Exception):
|
|
7
7
|
"""Raised when an invocation dispatched by the SDK fails.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
Catch the base to handle every rejection; catch a subclass to react to
|
|
14
|
-
a specific category. ``except Exception`` continues to work because
|
|
15
|
-
``IIIInvocationError`` inherits from ``Exception``.
|
|
9
|
+
Inspect ``err.code`` to react to a specific category (e.g.
|
|
10
|
+
``'FORBIDDEN'`` for RBAC denials, ``'TIMEOUT'`` for timeouts). Catch
|
|
11
|
+
this class to handle every rejection. ``except Exception`` continues to
|
|
12
|
+
work because ``IIIInvocationError`` inherits from ``Exception``.
|
|
16
13
|
|
|
17
14
|
Attributes are read-only after construction. ``stacktrace`` is the
|
|
18
15
|
engine-side trace when the remote handler raised; it may include
|
|
@@ -36,26 +33,18 @@ class IIIInvocationError(Exception):
|
|
|
36
33
|
self.invocation_id = invocation_id
|
|
37
34
|
|
|
38
35
|
|
|
39
|
-
class IIIForbiddenError(IIIInvocationError):
|
|
40
|
-
"""Raised when RBAC denies an invocation. ``code == 'FORBIDDEN'``."""
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class IIITimeoutError(IIIInvocationError):
|
|
44
|
-
"""Raised when an invocation exceeds its timeout. ``code == 'TIMEOUT'``."""
|
|
45
|
-
|
|
46
|
-
|
|
47
36
|
def _wrap_wire_error(
|
|
48
37
|
error: Any,
|
|
49
38
|
*,
|
|
50
39
|
function_id: str | None,
|
|
51
40
|
invocation_id: str | None,
|
|
52
41
|
) -> IIIInvocationError:
|
|
53
|
-
"""Convert a wire ``ErrorBody``-shaped dict into
|
|
42
|
+
"""Convert a wire ``ErrorBody``-shaped dict into an ``IIIInvocationError``.
|
|
54
43
|
|
|
55
|
-
|
|
56
|
-
``
|
|
57
|
-
values) fall back to ``
|
|
58
|
-
|
|
44
|
+
The ``code`` field distinguishes categories (e.g. ``'FORBIDDEN'``,
|
|
45
|
+
``'TIMEOUT'``). Malformed shapes (non-dict, missing fields, non-string
|
|
46
|
+
values) fall back to ``code='UNKNOWN'`` so no rejection path prints as a
|
|
47
|
+
raw dict repr.
|
|
59
48
|
"""
|
|
60
49
|
if isinstance(error, dict):
|
|
61
50
|
raw_code = error.get("code")
|
|
@@ -67,12 +56,7 @@ def _wrap_wire_error(
|
|
|
67
56
|
raw_stacktrace = error.get("stacktrace")
|
|
68
57
|
stacktrace = raw_stacktrace if isinstance(raw_stacktrace, str) else None
|
|
69
58
|
|
|
70
|
-
|
|
71
|
-
"FORBIDDEN": IIIForbiddenError,
|
|
72
|
-
"TIMEOUT": IIITimeoutError,
|
|
73
|
-
}.get(code, IIIInvocationError)
|
|
74
|
-
|
|
75
|
-
return cls(
|
|
59
|
+
return IIIInvocationError(
|
|
76
60
|
code=code,
|
|
77
61
|
message=message,
|
|
78
62
|
function_id=function_id,
|
|
@@ -18,7 +18,7 @@ from iii_observability import OtelConfig
|
|
|
18
18
|
from websockets.asyncio.client import ClientConnection
|
|
19
19
|
|
|
20
20
|
from .channels import ChannelReader, ChannelWriter
|
|
21
|
-
from .errors import IIIInvocationError,
|
|
21
|
+
from .errors import IIIInvocationError, _wrap_wire_error
|
|
22
22
|
from .format_utils import extract_request_format, extract_response_format
|
|
23
23
|
from .iii_constants import (
|
|
24
24
|
DEFAULT_RECONNECTION_CONFIG,
|
|
@@ -482,6 +482,7 @@ class III:
|
|
|
482
482
|
|
|
483
483
|
async def _invoke_with_otel_context(
|
|
484
484
|
self,
|
|
485
|
+
function_id: str,
|
|
485
486
|
handler: Callable[[Any], Awaitable[Any]],
|
|
486
487
|
data: Any,
|
|
487
488
|
traceparent: str | None,
|
|
@@ -509,10 +510,16 @@ class III:
|
|
|
509
510
|
)
|
|
510
511
|
payload_max_bytes = resolve_max_bytes_from_env()
|
|
511
512
|
|
|
513
|
+
# INTERNAL and named `execute` (not `call`/`trigger`): the engine
|
|
514
|
+
# already emits the SERVER `call <fn>` span for this hop AND a
|
|
515
|
+
# `trigger <fn>` span from fire_triggers. Reusing either name would
|
|
516
|
+
# duplicate an engine span under the worker's service. `execute` is
|
|
517
|
+
# unique, so the worker handler span reads as a clean internal child
|
|
518
|
+
# of the engine's call span (and is collapsible by a single rule).
|
|
512
519
|
with tracer.start_as_current_span(
|
|
513
|
-
f"
|
|
520
|
+
f"execute {function_id}",
|
|
514
521
|
context=parent_ctx,
|
|
515
|
-
kind=trace.SpanKind.
|
|
522
|
+
kind=trace.SpanKind.INTERNAL,
|
|
516
523
|
) as span:
|
|
517
524
|
if trace_payloads and span.is_recording():
|
|
518
525
|
input_json, input_truncated = redact_and_truncate(data, payload_max_bytes)
|
|
@@ -620,7 +627,7 @@ class III:
|
|
|
620
627
|
if not invocation_id:
|
|
621
628
|
task = asyncio.create_task(
|
|
622
629
|
self._invoke_with_otel_context(
|
|
623
|
-
func.handler, resolved_data, traceparent, baggage
|
|
630
|
+
path, func.handler, resolved_data, traceparent, baggage
|
|
624
631
|
)
|
|
625
632
|
)
|
|
626
633
|
task.add_done_callback(self._log_task_exception)
|
|
@@ -628,6 +635,7 @@ class III:
|
|
|
628
635
|
|
|
629
636
|
try:
|
|
630
637
|
result, response_traceparent = await self._invoke_with_otel_context(
|
|
638
|
+
path,
|
|
631
639
|
func.handler,
|
|
632
640
|
resolved_data,
|
|
633
641
|
traceparent,
|
|
@@ -1073,9 +1081,9 @@ class III:
|
|
|
1073
1081
|
actions.
|
|
1074
1082
|
|
|
1075
1083
|
Raises:
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1084
|
+
IIIInvocationError: For any engine rejection. Inspect ``code``:
|
|
1085
|
+
``'TIMEOUT'`` if the invocation timed out, ``'FORBIDDEN'`` if
|
|
1086
|
+
RBAC denied it.
|
|
1079
1087
|
|
|
1080
1088
|
Examples:
|
|
1081
1089
|
>>> result = iii.trigger({'function_id': 'greet', 'payload': {'name': 'World'}})
|
|
@@ -1100,9 +1108,9 @@ class III:
|
|
|
1100
1108
|
The result of the function invocation, or ``None`` for void calls.
|
|
1101
1109
|
|
|
1102
1110
|
Raises:
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1111
|
+
IIIInvocationError: For any engine rejection. Inspect ``code``:
|
|
1112
|
+
``'TIMEOUT'`` if the invocation timed out, ``'FORBIDDEN'`` if
|
|
1113
|
+
RBAC denied it.
|
|
1106
1114
|
|
|
1107
1115
|
Examples:
|
|
1108
1116
|
>>> result = await iii.trigger_async({'function_id': 'greet', 'payload': {'name': 'World'}})
|
|
@@ -1163,7 +1171,7 @@ class III:
|
|
|
1163
1171
|
return await asyncio.wait_for(future, timeout=timeout_secs)
|
|
1164
1172
|
except asyncio.TimeoutError:
|
|
1165
1173
|
self._pending.pop(invocation_id, None)
|
|
1166
|
-
raise
|
|
1174
|
+
raise IIIInvocationError(
|
|
1167
1175
|
code="TIMEOUT",
|
|
1168
1176
|
message=f"invocation timed out after {timeout_ms}ms",
|
|
1169
1177
|
function_id=function_id,
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
6
|
|
|
7
|
-
from iii import
|
|
7
|
+
from iii import IIIInvocationError
|
|
8
8
|
from iii.errors import _wrap_wire_error
|
|
9
9
|
|
|
10
10
|
|
|
@@ -57,34 +57,19 @@ class TestIIIInvocationError:
|
|
|
57
57
|
assert str(err) == "TIMEOUT: gone"
|
|
58
58
|
|
|
59
59
|
|
|
60
|
-
class
|
|
61
|
-
def
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
err = IIITimeoutError(code="TIMEOUT", message="x")
|
|
69
|
-
assert isinstance(err, IIIInvocationError)
|
|
70
|
-
assert isinstance(err, IIITimeoutError)
|
|
71
|
-
assert isinstance(err, Exception)
|
|
60
|
+
class TestCodeDiscrimination:
|
|
61
|
+
def test_categories_discriminated_by_code(self) -> None:
|
|
62
|
+
"""Categories are distinguished by ``code``, not by subclass."""
|
|
63
|
+
for code in ("FORBIDDEN", "TIMEOUT", "UNKNOWN"):
|
|
64
|
+
err = IIIInvocationError(code=code, message="x")
|
|
65
|
+
assert isinstance(err, IIIInvocationError)
|
|
66
|
+
assert isinstance(err, Exception)
|
|
67
|
+
assert err.code == code
|
|
72
68
|
|
|
73
|
-
def
|
|
74
|
-
"""`except IIIForbiddenError` fires before `except IIIInvocationError`."""
|
|
75
|
-
caught: str | None = None
|
|
76
|
-
try:
|
|
77
|
-
raise IIIForbiddenError(code="FORBIDDEN", message="x")
|
|
78
|
-
except IIIForbiddenError:
|
|
79
|
-
caught = "forbidden"
|
|
80
|
-
except IIIInvocationError:
|
|
81
|
-
caught = "base"
|
|
82
|
-
assert caught == "forbidden"
|
|
83
|
-
|
|
84
|
-
def test_base_catches_every_subclass(self) -> None:
|
|
69
|
+
def test_base_catches_every_category(self) -> None:
|
|
85
70
|
for err in (
|
|
86
|
-
|
|
87
|
-
|
|
71
|
+
IIIInvocationError(code="FORBIDDEN", message="x"),
|
|
72
|
+
IIIInvocationError(code="TIMEOUT", message="x"),
|
|
88
73
|
IIIInvocationError(code="UNKNOWN", message="x"),
|
|
89
74
|
):
|
|
90
75
|
try:
|
|
@@ -95,30 +80,30 @@ class TestSubclassHierarchy:
|
|
|
95
80
|
def test_except_exception_still_works(self) -> None:
|
|
96
81
|
"""Migration guarantee: existing `except Exception:` handlers still catch."""
|
|
97
82
|
try:
|
|
98
|
-
raise
|
|
83
|
+
raise IIIInvocationError(code="FORBIDDEN", message="x")
|
|
99
84
|
except Exception as got:
|
|
100
85
|
assert isinstance(got, IIIInvocationError)
|
|
101
86
|
|
|
102
87
|
|
|
103
88
|
class TestWrapWireError:
|
|
104
|
-
def
|
|
89
|
+
def test_forbidden_dict_sets_forbidden_code(self) -> None:
|
|
105
90
|
err = _wrap_wire_error(
|
|
106
91
|
{"code": "FORBIDDEN", "message": "not allowed"},
|
|
107
92
|
function_id="engine::functions::list",
|
|
108
93
|
invocation_id="inv-1",
|
|
109
94
|
)
|
|
110
|
-
assert
|
|
95
|
+
assert type(err) is IIIInvocationError
|
|
111
96
|
assert err.code == "FORBIDDEN"
|
|
112
97
|
assert err.function_id == "engine::functions::list"
|
|
113
98
|
assert err.invocation_id == "inv-1"
|
|
114
99
|
|
|
115
|
-
def
|
|
100
|
+
def test_timeout_dict_sets_timeout_code(self) -> None:
|
|
116
101
|
err = _wrap_wire_error(
|
|
117
102
|
{"code": "TIMEOUT", "message": "gone"},
|
|
118
103
|
function_id="api::slow",
|
|
119
104
|
invocation_id=None,
|
|
120
105
|
)
|
|
121
|
-
assert
|
|
106
|
+
assert type(err) is IIIInvocationError
|
|
122
107
|
assert err.code == "TIMEOUT"
|
|
123
108
|
|
|
124
109
|
def test_unknown_code_falls_back_to_base(self) -> None:
|
|
@@ -34,7 +34,7 @@ async def test_invoke_with_otel_context_records_exception_with_stacktrace():
|
|
|
34
34
|
raise ValueError("test invocation error")
|
|
35
35
|
|
|
36
36
|
with pytest.raises(_TraceContextError) as exc_info:
|
|
37
|
-
await client._invoke_with_otel_context(failing_handler, {"key": "value"}, None, None)
|
|
37
|
+
await client._invoke_with_otel_context("test.fn", failing_handler, {"key": "value"}, None, None)
|
|
38
38
|
assert isinstance(exc_info.value.__cause__, ValueError)
|
|
39
39
|
assert "test invocation error" in str(exc_info.value.__cause__)
|
|
40
40
|
|
|
@@ -81,7 +81,7 @@ async def test_invoke_with_otel_context_success_no_exception():
|
|
|
81
81
|
async def success_handler(data):
|
|
82
82
|
return {"result": "ok"}
|
|
83
83
|
|
|
84
|
-
result, traceparent = await client._invoke_with_otel_context(success_handler, {"key": "value"}, None, None)
|
|
84
|
+
result, traceparent = await client._invoke_with_otel_context("test.fn", success_handler, {"key": "value"}, None, None)
|
|
85
85
|
|
|
86
86
|
assert result == {"result": "ok"}
|
|
87
87
|
|
|
@@ -8,7 +8,6 @@ import pytest
|
|
|
8
8
|
from iii import (
|
|
9
9
|
AuthInput,
|
|
10
10
|
AuthResult,
|
|
11
|
-
IIIForbiddenError,
|
|
12
11
|
IIIInvocationError,
|
|
13
12
|
InitOptions,
|
|
14
13
|
MiddlewareFunctionInput,
|
|
@@ -350,22 +349,21 @@ class TestRbacWorkers:
|
|
|
350
349
|
iii_client.shutdown()
|
|
351
350
|
|
|
352
351
|
def test_forbidden_wrapped_as_typed_error(self, iii_server):
|
|
353
|
-
"""FORBIDDEN rejections surface as
|
|
354
|
-
and the engine's remediation phrase in the message."""
|
|
352
|
+
"""FORBIDDEN rejections surface as IIIInvocationError (code='FORBIDDEN')
|
|
353
|
+
with function_id set and the engine's remediation phrase in the message."""
|
|
355
354
|
iii_client = register_worker(
|
|
356
355
|
EW_URL,
|
|
357
356
|
InitOptions(otel={"enabled": False}, headers={"x-test-token": "valid-token"}),
|
|
358
357
|
)
|
|
359
358
|
|
|
360
359
|
try:
|
|
361
|
-
with pytest.raises(
|
|
360
|
+
with pytest.raises(IIIInvocationError) as excinfo:
|
|
362
361
|
iii_client.trigger({
|
|
363
362
|
"function_id": "test::ew::private",
|
|
364
363
|
"payload": {},
|
|
365
364
|
})
|
|
366
365
|
|
|
367
366
|
err = excinfo.value
|
|
368
|
-
assert isinstance(err, IIIInvocationError) # base class
|
|
369
367
|
assert err.code == "FORBIDDEN"
|
|
370
368
|
assert err.function_id == "test::ew::private"
|
|
371
369
|
assert "FORBIDDEN" in str(err)
|
|
@@ -432,7 +430,7 @@ class TestRbacWorkers:
|
|
|
432
430
|
try:
|
|
433
431
|
def handler(data: dict) -> dict:
|
|
434
432
|
# If the carve-out regresses, this nested trigger surfaces
|
|
435
|
-
#
|
|
433
|
+
# IIIInvocationError (code='FORBIDDEN') and the handler propagates it as a failure.
|
|
436
434
|
iii_client.trigger({
|
|
437
435
|
"function_id": "engine::log::info",
|
|
438
436
|
"payload": {
|
|
@@ -473,7 +471,7 @@ class TestRbacWorkers:
|
|
|
473
471
|
|
|
474
472
|
try:
|
|
475
473
|
# No exception == carve-out is working. If this raises
|
|
476
|
-
#
|
|
474
|
+
# IIIInvocationError (code='FORBIDDEN'), the carve-out regressed.
|
|
477
475
|
iii_client.trigger({
|
|
478
476
|
"function_id": "engine::log::info",
|
|
479
477
|
"payload": {"message": "carve-out direct invocation"},
|
|
@@ -546,7 +546,7 @@ wheels = [
|
|
|
546
546
|
|
|
547
547
|
[[package]]
|
|
548
548
|
name = "iii-observability"
|
|
549
|
-
version = "0.
|
|
549
|
+
version = "0.18.0"
|
|
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.18.0"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{iii_sdk-0.19.0.dev2 → iii_sdk-0.19.0.dev3}/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
|
|
File without changes
|