nvent 0.5.15 → 1.0.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/dist/module.d.mts +181 -8
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +1257 -1196
  4. package/dist/runtime/app/composables/useFunctionCall.d.ts +17 -0
  5. package/dist/runtime/app/composables/useFunctionCall.js +25 -0
  6. package/dist/runtime/app/composables/useNventStream.d.ts +23 -0
  7. package/dist/runtime/app/composables/useNventStream.js +58 -0
  8. package/dist/runtime/nitro/plugins/00.iii-lifecycle.d.ts +19 -0
  9. package/dist/runtime/nitro/plugins/00.iii-lifecycle.js +62 -0
  10. package/dist/runtime/nitro/plugins/01.iii-worker.d.ts +15 -0
  11. package/dist/runtime/nitro/plugins/01.iii-worker.js +91 -0
  12. package/dist/runtime/nitro/routes/stream-proxy.d.ts +17 -0
  13. package/dist/runtime/nitro/routes/stream-proxy.js +45 -0
  14. package/dist/runtime/nitro/utils/console.d.ts +32 -0
  15. package/dist/runtime/nitro/utils/console.js +95 -0
  16. package/dist/runtime/nitro/utils/defineFunction.d.ts +378 -3
  17. package/dist/runtime/nitro/utils/defineFunction.js +224 -4
  18. package/dist/runtime/nitro/utils/engine.d.ts +25 -0
  19. package/dist/runtime/nitro/utils/engine.js +130 -0
  20. package/dist/runtime/nitro/utils/nventDir.d.ts +1 -0
  21. package/dist/runtime/nitro/utils/nventDir.js +20 -0
  22. package/dist/runtime/nitro/utils/useIii.d.ts +23 -0
  23. package/dist/runtime/nitro/utils/useIii.js +14 -0
  24. package/dist/runtime/nitro/utils/workers/node.d.ts +24 -0
  25. package/dist/runtime/nitro/utils/workers/node.js +57 -0
  26. package/dist/runtime/nitro/utils/workers/python.d.ts +67 -0
  27. package/dist/runtime/nitro/utils/workers/python.js +220 -0
  28. package/dist/runtime/python/nvent.py +208 -0
  29. package/dist/runtime/python/worker_runtime.py +659 -0
  30. package/dist/runtime/virtual.d.ts +17 -0
  31. package/dist/types.d.mts +5 -1
  32. package/package.json +11 -14
  33. package/dist/runtime/adapters/base/index.d.ts +0 -6
  34. package/dist/runtime/adapters/base/index.js +0 -1
  35. package/dist/runtime/adapters/base/store-validator.d.ts +0 -48
  36. package/dist/runtime/adapters/base/store-validator.js +0 -147
  37. package/dist/runtime/adapters/builtin/file-queue.d.ts +0 -67
  38. package/dist/runtime/adapters/builtin/file-queue.js +0 -499
  39. package/dist/runtime/adapters/builtin/file-store.d.ts +0 -32
  40. package/dist/runtime/adapters/builtin/file-store.js +0 -206
  41. package/dist/runtime/adapters/builtin/file-stream.d.ts +0 -39
  42. package/dist/runtime/adapters/builtin/file-stream.js +0 -56
  43. package/dist/runtime/adapters/builtin/index.d.ts +0 -10
  44. package/dist/runtime/adapters/builtin/index.js +0 -5
  45. package/dist/runtime/adapters/builtin/memory-queue.d.ts +0 -52
  46. package/dist/runtime/adapters/builtin/memory-queue.js +0 -243
  47. package/dist/runtime/adapters/builtin/memory-store.d.ts +0 -69
  48. package/dist/runtime/adapters/builtin/memory-store.js +0 -357
  49. package/dist/runtime/adapters/builtin/memory-stream.d.ts +0 -21
  50. package/dist/runtime/adapters/builtin/memory-stream.js +0 -56
  51. package/dist/runtime/adapters/factory.d.ts +0 -31
  52. package/dist/runtime/adapters/factory.js +0 -135
  53. package/dist/runtime/adapters/index.d.ts +0 -8
  54. package/dist/runtime/adapters/index.js +0 -3
  55. package/dist/runtime/adapters/interfaces/index.d.ts +0 -11
  56. package/dist/runtime/adapters/interfaces/index.js +0 -3
  57. package/dist/runtime/adapters/interfaces/queue.d.ts +0 -155
  58. package/dist/runtime/adapters/interfaces/queue.js +0 -0
  59. package/dist/runtime/adapters/interfaces/store.d.ts +0 -299
  60. package/dist/runtime/adapters/interfaces/store.js +0 -0
  61. package/dist/runtime/adapters/interfaces/stream.d.ts +0 -62
  62. package/dist/runtime/adapters/interfaces/stream.js +0 -0
  63. package/dist/runtime/adapters/registry.d.ts +0 -85
  64. package/dist/runtime/adapters/registry.js +0 -161
  65. package/dist/runtime/config/index.d.ts +0 -23
  66. package/dist/runtime/config/index.js +0 -200
  67. package/dist/runtime/config/types.d.ts +0 -439
  68. package/dist/runtime/config/types.js +0 -0
  69. package/dist/runtime/events/eventBus.d.ts +0 -20
  70. package/dist/runtime/events/eventBus.js +0 -35
  71. package/dist/runtime/events/types.d.ts +0 -144
  72. package/dist/runtime/events/types.js +0 -0
  73. package/dist/runtime/events/utils/scheduleTrigger.d.ts +0 -8
  74. package/dist/runtime/events/utils/scheduleTrigger.js +0 -71
  75. package/dist/runtime/events/utils/stallDetector.d.ts +0 -83
  76. package/dist/runtime/events/utils/stallDetector.js +0 -318
  77. package/dist/runtime/events/utils/triggerRuntime.d.ts +0 -58
  78. package/dist/runtime/events/utils/triggerRuntime.js +0 -212
  79. package/dist/runtime/events/wiring/flowWiring.d.ts +0 -34
  80. package/dist/runtime/events/wiring/flowWiring.js +0 -1258
  81. package/dist/runtime/events/wiring/registry.d.ts +0 -19
  82. package/dist/runtime/events/wiring/registry.js +0 -43
  83. package/dist/runtime/events/wiring/stateWiring.d.ts +0 -37
  84. package/dist/runtime/events/wiring/stateWiring.js +0 -92
  85. package/dist/runtime/events/wiring/streamWiring.d.ts +0 -36
  86. package/dist/runtime/events/wiring/streamWiring.js +0 -156
  87. package/dist/runtime/events/wiring/triggerWiring.d.ts +0 -21
  88. package/dist/runtime/events/wiring/triggerWiring.js +0 -422
  89. package/dist/runtime/nitro/plugins/00.adapters.d.ts +0 -14
  90. package/dist/runtime/nitro/plugins/00.adapters.js +0 -73
  91. package/dist/runtime/nitro/plugins/01.ws-lifecycle.d.ts +0 -5
  92. package/dist/runtime/nitro/plugins/01.ws-lifecycle.js +0 -69
  93. package/dist/runtime/nitro/plugins/02.workers.d.ts +0 -2
  94. package/dist/runtime/nitro/plugins/02.workers.js +0 -92
  95. package/dist/runtime/nitro/plugins/03.triggers.d.ts +0 -12
  96. package/dist/runtime/nitro/plugins/03.triggers.js +0 -55
  97. package/dist/runtime/nitro/routes/webhook.await.d.ts +0 -23
  98. package/dist/runtime/nitro/routes/webhook.await.js +0 -112
  99. package/dist/runtime/nitro/routes/webhook.trigger.d.ts +0 -86
  100. package/dist/runtime/nitro/routes/webhook.trigger.js +0 -73
  101. package/dist/runtime/nitro/utils/adapters.d.ts +0 -66
  102. package/dist/runtime/nitro/utils/adapters.js +0 -51
  103. package/dist/runtime/nitro/utils/awaitPatterns/event.d.ts +0 -15
  104. package/dist/runtime/nitro/utils/awaitPatterns/event.js +0 -128
  105. package/dist/runtime/nitro/utils/awaitPatterns/index.d.ts +0 -28
  106. package/dist/runtime/nitro/utils/awaitPatterns/index.js +0 -55
  107. package/dist/runtime/nitro/utils/awaitPatterns/schedule.d.ts +0 -16
  108. package/dist/runtime/nitro/utils/awaitPatterns/schedule.js +0 -83
  109. package/dist/runtime/nitro/utils/awaitPatterns/time.d.ts +0 -15
  110. package/dist/runtime/nitro/utils/awaitPatterns/time.js +0 -71
  111. package/dist/runtime/nitro/utils/awaitPatterns/webhook.d.ts +0 -15
  112. package/dist/runtime/nitro/utils/awaitPatterns/webhook.js +0 -128
  113. package/dist/runtime/nitro/utils/defineFunctionConfig.d.ts +0 -378
  114. package/dist/runtime/nitro/utils/defineFunctionConfig.js +0 -3
  115. package/dist/runtime/nitro/utils/defineHooks.d.ts +0 -95
  116. package/dist/runtime/nitro/utils/defineHooks.js +0 -9
  117. package/dist/runtime/nitro/utils/registerAdapter.d.ts +0 -59
  118. package/dist/runtime/nitro/utils/registerAdapter.js +0 -13
  119. package/dist/runtime/nitro/utils/useAwait.d.ts +0 -83
  120. package/dist/runtime/nitro/utils/useAwait.js +0 -169
  121. package/dist/runtime/nitro/utils/useEventManager.d.ts +0 -15
  122. package/dist/runtime/nitro/utils/useEventManager.js +0 -26
  123. package/dist/runtime/nitro/utils/useFlow.d.ts +0 -66
  124. package/dist/runtime/nitro/utils/useFlow.js +0 -310
  125. package/dist/runtime/nitro/utils/useHookRegistry.d.ts +0 -40
  126. package/dist/runtime/nitro/utils/useHookRegistry.js +0 -25
  127. package/dist/runtime/nitro/utils/useNventLogger.d.ts +0 -42
  128. package/dist/runtime/nitro/utils/useNventLogger.js +0 -54
  129. package/dist/runtime/nitro/utils/useRunContext.d.ts +0 -6
  130. package/dist/runtime/nitro/utils/useRunContext.js +0 -102
  131. package/dist/runtime/nitro/utils/useStreamTopics.d.ts +0 -83
  132. package/dist/runtime/nitro/utils/useStreamTopics.js +0 -94
  133. package/dist/runtime/nitro/utils/useTrigger.d.ts +0 -150
  134. package/dist/runtime/nitro/utils/useTrigger.js +0 -311
  135. package/dist/runtime/nitro/utils/wsPeerManager.d.ts +0 -44
  136. package/dist/runtime/nitro/utils/wsPeerManager.js +0 -32
  137. package/dist/runtime/scheduler/index.d.ts +0 -33
  138. package/dist/runtime/scheduler/index.js +0 -42
  139. package/dist/runtime/scheduler/scheduler.d.ts +0 -132
  140. package/dist/runtime/scheduler/scheduler.js +0 -799
  141. package/dist/runtime/scheduler/types.d.ts +0 -122
  142. package/dist/runtime/scheduler/types.js +0 -0
  143. package/dist/runtime/worker/node/runner.d.ts +0 -95
  144. package/dist/runtime/worker/node/runner.js +0 -272
  145. package/dist/runtime/worker/python/get_config.py +0 -64
  146. package/dist/runtime/worker/system/awaitHandlers.d.ts +0 -27
  147. package/dist/runtime/worker/system/awaitHandlers.js +0 -230
  148. package/dist/runtime/worker/system/index.d.ts +0 -24
  149. package/dist/runtime/worker/system/index.js +0 -39
@@ -0,0 +1,659 @@
1
+ # nvent Python worker runtime
2
+ #
3
+ # This file is copied to .nuxt/iii-workers/_runtime.py at dev startup.
4
+ # Generated worker entry scripts (e.g. __shared__.py, <fn-id>.py) import from it
5
+ # so all shared logic lives here as real, editable Python — not embedded strings.
6
+ #
7
+ # Use in your step files:
8
+ # from nvent import http, queue, cron, FlowContext, ApiRequest, ApiResponse
9
+ #
10
+ # Responsibilities:
11
+ # http/queue/cron — trigger config helpers (same API as motia)
12
+ # FlowContext — ctx object passed to handlers (ctx.logger, ctx.state, ctx.enqueue, ctx.match)
13
+ # _load(path, name) — load a .py module from an absolute path
14
+ # _register(client, mod, default_id) — register a module's handler + triggers
15
+ # _iii_sdk — the iii Python SDK (re-exported for entry scripts)
16
+
17
+ import sys
18
+ import re
19
+ import inspect
20
+ import importlib.util
21
+ import logging as _logging
22
+
23
+ try:
24
+ import iii as _iii_sdk
25
+ except ImportError:
26
+ print("[nvent] iii package not found — install it: pip install iii-sdk[otel]", flush=True)
27
+ sys.exit(1)
28
+
29
+ # Direct Python logging to stdout so nvent captures it alongside other output.
30
+ # DEBUG and above are collected; the JS side filters by logLevel.
31
+ _logging.basicConfig(
32
+ level=_logging.DEBUG,
33
+ stream=sys.stdout,
34
+ format="[%(levelname)s] %(name)s: %(message)s",
35
+ force=True,
36
+ )
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # OpenTelemetry tracing helpers — gracefully disabled when otel is not available
40
+ # ---------------------------------------------------------------------------
41
+
42
+ try:
43
+ from opentelemetry import trace as _otel_trace
44
+ from opentelemetry.trace import SpanKind as _SpanKind, StatusCode as _StatusCode
45
+ _HAS_OTEL = True
46
+ except ImportError:
47
+ _HAS_OTEL = False
48
+
49
+ import contextlib
50
+
51
+
52
+ def _get_tracer():
53
+ """Return the nvent OTel tracer, or None when OTel is not available."""
54
+ if not _HAS_OTEL:
55
+ return None
56
+ return _otel_trace.get_tracer("nvent")
57
+
58
+
59
+ @contextlib.contextmanager
60
+ def operation_span(operation: str, **attributes):
61
+ """Context manager that creates an OTel child span for an operation.
62
+
63
+ When OTel is not installed (``opentelemetry-api`` not on sys.path),
64
+ this is a no-op — ``span`` is ``None`` inside the block.
65
+
66
+ Usage::
67
+
68
+ with operation_span("state::get", **{"nvent.state.scope": scope, "nvent.state.key": key}) as span:
69
+ try:
70
+ result = await client._async_trigger(...)
71
+ set_span_ok(span)
72
+ return result
73
+ except Exception as exc:
74
+ record_exception(span, exc)
75
+ raise
76
+ """
77
+ tracer = _get_tracer()
78
+ if not tracer:
79
+ yield None
80
+ return
81
+ with tracer.start_as_current_span(
82
+ operation,
83
+ kind=_SpanKind.CLIENT,
84
+ attributes={k: v for k, v in attributes.items() if v is not None},
85
+ ) as span:
86
+ yield span
87
+
88
+
89
+ def record_exception(span, exc: Exception) -> None:
90
+ """Record an exception on a span and mark it as errored."""
91
+ if _HAS_OTEL and span is not None:
92
+ span.set_status(_StatusCode.ERROR, str(exc))
93
+ span.record_exception(exc)
94
+
95
+
96
+ def set_span_ok(span) -> None:
97
+ """Mark a span as successfully completed."""
98
+ if _HAS_OTEL and span is not None:
99
+ span.set_status(_StatusCode.OK)
100
+
101
+
102
+ # Internal key used to propagate the stream channel through queue messages.
103
+ _NVENT_STREAM_KEY = '__nventStream'
104
+
105
+
106
+ # ---------------------------------------------------------------------------
107
+ # Trigger config helpers — use these in config["triggers"]
108
+ # ---------------------------------------------------------------------------
109
+
110
+ def http(method: str, path: str) -> dict:
111
+ """HTTP trigger: http("POST", "/my-endpoint")"""
112
+ return {"type": "http", "method": method.upper(), "path": path}
113
+
114
+
115
+ def queue(topic: str) -> dict:
116
+ """Queue trigger: queue("order.created")"""
117
+ return {"type": "queue", "topic": topic}
118
+
119
+
120
+ def cron(expression: str) -> dict:
121
+ """Cron trigger: cron("0 0 * * * * *") (7-field: sec min hour dom month dow year)"""
122
+ return {"type": "cron", "expression": expression}
123
+
124
+
125
+ class _Stream:
126
+ """Access to the iii Stream module.
127
+
128
+ **Implicit API** (recommended): ``set(item_id, data)`` automatically uses
129
+ the function's flow/stream name and the current OTel trace ID as the group
130
+ ID. All steps that run in the same request share the same trace → same
131
+ group, so they write to a consistent WebSocket channel without manual
132
+ coordination.
133
+
134
+ **Explicit API**: ``set_in(name, group, item_id, data)`` targets any
135
+ specific stream and group.
136
+
137
+ Available as ``ctx.stream``.
138
+ """
139
+
140
+ def __init__(self, client, stream_name: str, get_group_id):
141
+ self._client = client
142
+ self._stream_name = stream_name
143
+ self._get_group_id = get_group_id
144
+
145
+ # ------------------------------------------------------------------
146
+ # Implicit helpers — use flow stream + trace ID automatically
147
+ # ------------------------------------------------------------------
148
+
149
+ async def set(self, item_id: str, data: dict) -> None:
150
+ """Set (create/update) an item in the implicit stream channel."""
151
+ group = self._get_group_id()
152
+ with operation_span("stream::set", **{
153
+ "nvent.stream.name": self._stream_name,
154
+ "nvent.stream.group_id": group,
155
+ "nvent.stream.item_id": item_id,
156
+ }) as span:
157
+ try:
158
+ await self._client._async_trigger({"function_id": "stream::set", "payload": {
159
+ "stream_name": self._stream_name,
160
+ "group_id": group,
161
+ "item_id": item_id,
162
+ "data": data,
163
+ }})
164
+ set_span_ok(span)
165
+ except Exception as exc:
166
+ record_exception(span, exc)
167
+ raise
168
+
169
+ async def send(self, data: dict) -> None:
170
+ """Send a custom event to all subscribers of the implicit stream channel."""
171
+ group = self._get_group_id()
172
+ with operation_span("stream::send", **{
173
+ "nvent.stream.name": self._stream_name,
174
+ "nvent.stream.group_id": group,
175
+ }) as span:
176
+ try:
177
+ await self._client._async_trigger({"function_id": "stream::send", "payload": {
178
+ "stream_name": self._stream_name,
179
+ "group_id": group,
180
+ "data": data,
181
+ }})
182
+ set_span_ok(span)
183
+ except Exception as exc:
184
+ record_exception(span, exc)
185
+ raise
186
+
187
+ def subscription(self) -> dict:
188
+ """Return ``{"streamName": ..., "groupId": ...}`` for the implicit channel.
189
+
190
+ Return this dict from an HTTP handler so the client knows where to subscribe::
191
+
192
+ return ApiResponse(statusCode=200, body=ctx.stream.subscription())
193
+ """
194
+ return {"streamName": self._stream_name, "groupId": self._get_group_id()}
195
+
196
+ # ------------------------------------------------------------------
197
+ # Explicit helpers — target any stream + group
198
+ # ------------------------------------------------------------------
199
+
200
+ async def set_in(self, name: str, group: str, item_id: str, data: dict) -> None:
201
+ """Set (create/update) an item in an explicit stream group."""
202
+ with operation_span("stream::set", **{
203
+ "nvent.stream.name": name,
204
+ "nvent.stream.group_id": group,
205
+ "nvent.stream.item_id": item_id,
206
+ }) as span:
207
+ try:
208
+ await self._client._async_trigger({"function_id": "stream::set", "payload": {
209
+ "stream_name": name, "group_id": group, "item_id": item_id, "data": data,
210
+ }})
211
+ set_span_ok(span)
212
+ except Exception as exc:
213
+ record_exception(span, exc)
214
+ raise
215
+
216
+ async def get_item(self, name: str, group: str, item_id: str) -> dict:
217
+ """Get a single item from a stream group."""
218
+ with operation_span("stream::get", **{
219
+ "nvent.stream.name": name,
220
+ "nvent.stream.group_id": group,
221
+ "nvent.stream.item_id": item_id,
222
+ }) as span:
223
+ try:
224
+ result = await self._client._async_trigger({"function_id": "stream::get", "payload": {
225
+ "stream_name": name, "group_id": group, "item_id": item_id,
226
+ }})
227
+ set_span_ok(span)
228
+ return result
229
+ except Exception as exc:
230
+ record_exception(span, exc)
231
+ raise
232
+
233
+ async def delete_item(self, name: str, group: str, item_id: str) -> None:
234
+ """Delete an item from a stream group."""
235
+ with operation_span("stream::delete", **{
236
+ "nvent.stream.name": name,
237
+ "nvent.stream.group_id": group,
238
+ "nvent.stream.item_id": item_id,
239
+ }) as span:
240
+ try:
241
+ await self._client._async_trigger({"function_id": "stream::delete", "payload": {
242
+ "stream_name": name, "group_id": group, "item_id": item_id,
243
+ }})
244
+ set_span_ok(span)
245
+ except Exception as exc:
246
+ record_exception(span, exc)
247
+ raise
248
+
249
+ async def list_items(self, name: str, group: str) -> list:
250
+ """List all items in a stream group."""
251
+ with operation_span("stream::list", **{
252
+ "nvent.stream.name": name,
253
+ "nvent.stream.group_id": group,
254
+ }) as span:
255
+ try:
256
+ result = await self._client._async_trigger({"function_id": "stream::list", "payload": {"stream_name": name, "group_id": group}})
257
+ set_span_ok(span)
258
+ return result
259
+ except Exception as exc:
260
+ record_exception(span, exc)
261
+ raise
262
+
263
+ async def send_to(self, name: str, group: str, data: dict) -> None:
264
+ """Send a custom event to all subscribers of an explicit stream group."""
265
+ with operation_span("stream::send", **{
266
+ "nvent.stream.name": name,
267
+ "nvent.stream.group_id": group,
268
+ }) as span:
269
+ try:
270
+ await self._client._async_trigger({"function_id": "stream::send", "payload": {
271
+ "stream_name": name, "group_id": group, "data": data,
272
+ }})
273
+ set_span_ok(span)
274
+ except Exception as exc:
275
+ record_exception(span, exc)
276
+ raise
277
+
278
+
279
+ # ---------------------------------------------------------------------------
280
+ # State manager sub-object — accessible as ctx.state
281
+ # ---------------------------------------------------------------------------
282
+
283
+ class _State:
284
+ """Provides ctx.state.get/set/delete/list — scope is auto-bound to the function ID."""
285
+
286
+ def __init__(self, client, fn_id: str):
287
+ self._client = client
288
+ self._fn_id = fn_id
289
+
290
+ async def get(self, key: str):
291
+ with operation_span("state::get", **{"nvent.state.scope": self._fn_id, "nvent.state.key": key}) as span:
292
+ try:
293
+ result = await self._client._async_trigger({"function_id": "state::get", "payload": {"scope": self._fn_id, "key": key}})
294
+ set_span_ok(span)
295
+ return result
296
+ except Exception as exc:
297
+ record_exception(span, exc)
298
+ raise
299
+
300
+ async def set(self, key: str, value) -> None:
301
+ with operation_span("state::set", **{"nvent.state.scope": self._fn_id, "nvent.state.key": key}) as span:
302
+ try:
303
+ result = await self._client._async_trigger({"function_id": "state::set", "payload": {"scope": self._fn_id, "key": key, "value": value}})
304
+ set_span_ok(span)
305
+ return result
306
+ except Exception as exc:
307
+ record_exception(span, exc)
308
+ raise
309
+
310
+ async def delete(self, key: str) -> None:
311
+ with operation_span("state::delete", **{"nvent.state.scope": self._fn_id, "nvent.state.key": key}) as span:
312
+ try:
313
+ await self._client._async_trigger({"function_id": "state::delete", "payload": {"scope": self._fn_id, "key": key}})
314
+ set_span_ok(span)
315
+ except Exception as exc:
316
+ record_exception(span, exc)
317
+ raise
318
+
319
+ async def list(self):
320
+ with operation_span("state::list", **{"nvent.state.scope": self._fn_id}) as span:
321
+ try:
322
+ result = await self._client._async_trigger({"function_id": "state::list", "payload": {"scope": self._fn_id}})
323
+ set_span_ok(span)
324
+ return result
325
+ except Exception as exc:
326
+ record_exception(span, exc)
327
+ raise
328
+
329
+
330
+ # ---------------------------------------------------------------------------
331
+ # FlowContext — execution context passed to handler(input, ctx)
332
+ # ---------------------------------------------------------------------------
333
+
334
+ class FlowContext:
335
+ """Execution context passed as the second argument to Python step handlers.
336
+
337
+ ctx.logger — structured logger (OTel-backed when available, else stdout)
338
+ ctx.state — key-value store (ctx.state.get / .set / .delete / .list)
339
+ ctx.stream — stream channel (ctx.stream.set / .send / .subscription)
340
+ ctx.trace_id — current OTel trace ID (shared across all steps in the same request)
341
+ ctx.stream_name — stream name used by implicit ctx.stream ops
342
+ ctx.enqueue() — emit a message to a topic
343
+ ctx.match() — route to a trigger-type-specific sub-handler
344
+
345
+ Implicit stream usage (recommended)::
346
+
347
+ # All steps in the same trace write to the same channel automatically:
348
+ await ctx.stream.set("step-1", {"label": "Tokenizing", "step": 1})
349
+
350
+ # HTTP handler: return subscription info for the client
351
+ return ApiResponse(statusCode=200, body=ctx.stream.subscription())
352
+
353
+ Explicit stream (cross-flow or specific group)::
354
+
355
+ await ctx.stream.set_in("other-flow", "group-123", "item", data)
356
+ """
357
+
358
+ def __init__(self, client, fn_id: str, trigger_type: str, input_data, stream_name: str = "", inherited_group_id: str = None):
359
+ self._client = client
360
+ self._fn_id = fn_id
361
+ self._trigger_type = trigger_type
362
+ self._input = input_data
363
+ self._stream_name = stream_name or fn_id.split("::")[0]
364
+ self._stream_group_id = inherited_group_id
365
+ self.logger = _iii_sdk.Logger(fn_id)
366
+ self.state = _State(client, fn_id)
367
+ self.stream = _Stream(client, self._stream_name, self._get_or_create_group_id)
368
+
369
+ def _get_or_create_group_id(self) -> str:
370
+ """Return stable group ID for this invocation, generating one lazily if needed."""
371
+ if not self._stream_group_id:
372
+ import uuid
373
+ self._stream_group_id = str(uuid.uuid4())
374
+ return self._stream_group_id
375
+
376
+ @property
377
+ def stream_name(self) -> str:
378
+ """The stream name used by implicit ``ctx.stream.set()`` / ``ctx.stream.send()``."""
379
+ return self._stream_name
380
+
381
+ async def enqueue(self, payload: dict) -> None:
382
+ """Emit a message to a queue topic.
383
+ If a stream channel is active, its context is propagated automatically.
384
+ Usage: await ctx.enqueue({"topic": "order.created", "data": {"id": "x"}})
385
+ """
386
+ topic = payload.get("topic")
387
+ data = dict(payload.get("data") or {})
388
+ if self._stream_group_id:
389
+ data[_NVENT_STREAM_KEY] = {"name": self._stream_name, "groupId": self._stream_group_id}
390
+ await self._client._async_trigger({
391
+ "function_id": "enqueue",
392
+ "payload": {"topic": topic, "data": data},
393
+ })
394
+
395
+ async def enqueue_named(self, payload: dict) -> dict:
396
+ """Dispatch directly to a function via a named queue.
397
+
398
+ Unlike ``enqueue()``, which publishes to a topic, this targets a
399
+ specific function and routes the call through the named queue for
400
+ FIFO ordering, concurrency control, or custom retry behaviour.
401
+ Requires the queue to be declared in nvent's ``queue_configs``.
402
+
403
+ Returns ``{"messageReceiptId": "..."}`` immediately — the function
404
+ runs asynchronously.
405
+
406
+ Usage::
407
+
408
+ result = await ctx.enqueue_named({
409
+ "queue": "orders",
410
+ "function_id": "orders::process",
411
+ "data": {"orderId": "123"},
412
+ })
413
+ """
414
+ queue = payload["queue"]
415
+ function_id = payload["function_id"]
416
+ data = payload.get("data")
417
+ return await self._client._async_trigger({
418
+ "function_id": function_id,
419
+ "payload": data,
420
+ "action": {"type": "enqueue", "queue": queue},
421
+ })
422
+
423
+ async def match(self, handlers: dict):
424
+ """Route to a sub-handler based on the trigger type that activated this step.
425
+
426
+ Each value in the dict is an async callable:
427
+ "http" → receives ApiRequest
428
+ "queue" → receives the queue data dict
429
+ "cron" → receives no arguments
430
+ "default" → fallback for unmatched trigger types
431
+ """
432
+ handler = handlers.get(self._trigger_type) or handlers.get("default")
433
+ if handler is None:
434
+ raise ValueError(
435
+ f"[nvent] ctx.match(): no handler for trigger type '{self._trigger_type}'. "
436
+ f"Available: {list(handlers)}"
437
+ )
438
+ sig = inspect.signature(handler)
439
+ # Count required positional params (ignores *args, **kwargs, params with defaults)
440
+ required = sum(
441
+ 1 for p in sig.parameters.values()
442
+ if p.default is inspect.Parameter.empty
443
+ and p.kind not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)
444
+ )
445
+ if required == 0:
446
+ return await handler()
447
+ return await handler(self._input)
448
+
449
+
450
+ # ---------------------------------------------------------------------------
451
+ # Module loader
452
+ # ---------------------------------------------------------------------------
453
+
454
+ def _load(path: str, name: str):
455
+ """Load a Python module from an absolute file path."""
456
+ spec = importlib.util.spec_from_file_location(name, path)
457
+ mod = importlib.util.module_from_spec(spec)
458
+ spec.loader.exec_module(mod)
459
+ return mod
460
+
461
+
462
+ # ---------------------------------------------------------------------------
463
+ # Registration — reads config dict (motia-style) or meta+triggers (legacy)
464
+ # ---------------------------------------------------------------------------
465
+
466
+ def _normalize_trigger_meta(t: dict) -> dict:
467
+ """Return a flat {type, path/topic/expression} dict for console metadata."""
468
+ t_type = t.get("type", "")
469
+ if t_type == "http":
470
+ path = t.get("path") or (t.get("config") or {}).get("api_path")
471
+ method = t.get("method") or (t.get("config") or {}).get("http_method")
472
+ return {"type": "http", "path": path, "method": method}
473
+ if t_type == "queue":
474
+ topic = t.get("topic") or (t.get("config") or {}).get("topic")
475
+ return {"type": "queue", "topic": topic}
476
+ if t_type == "cron":
477
+ expr = t.get("expression") or (t.get("config") or {}).get("expression")
478
+ return {"type": "cron", "expression": expr}
479
+ cfg = dict(t.get("config") or {})
480
+ return {"type": t_type, **cfg}
481
+
482
+
483
+ def _trigger_to_iii_cfg(t: dict) -> dict:
484
+ """Convert a trigger dict to the engine-facing config (api_path/http_method for HTTP)."""
485
+ t_type = t.get("type", "")
486
+ if t_type == "http":
487
+ path = t.get("path") or (t.get("config") or {}).get("api_path") or ""
488
+ method = t.get("method") or (t.get("config") or {}).get("http_method") or "GET"
489
+ return {"api_path": path, "http_method": method}
490
+ if t_type == "queue":
491
+ topic = t.get("topic") or (t.get("config") or {}).get("topic") or ""
492
+ return {"topic": topic}
493
+ if t_type == "cron":
494
+ expr = t.get("expression") or (t.get("config") or {}).get("expression") or ""
495
+ return {"expression": expr}
496
+ return dict(t.get("config") or {k: v for k, v in t.items() if k != "type"})
497
+
498
+
499
+ def _trigger_suffix(trigger: dict) -> str:
500
+ """Build a descriptive trigger suffix (no whitespace for valid function_id)."""
501
+ t_type = trigger.get("type", "")
502
+ if t_type == "http":
503
+ method = trigger.get("method") or (trigger.get("config") or {}).get("http_method", "GET")
504
+ path = trigger.get("path") or (trigger.get("config") or {}).get("api_path", "/")
505
+ return f"http({method}_{path})"
506
+ if t_type == "queue":
507
+ topic = trigger.get("topic") or (trigger.get("config") or {}).get("topic", "")
508
+ return f"queue({topic})"
509
+ if t_type == "cron":
510
+ expr = trigger.get("expression") or (trigger.get("config") or {}).get("expression", "")
511
+ return f"cron({expr})"
512
+ return t_type
513
+
514
+
515
+ def _register(client, mod, default_id: str) -> None:
516
+ """Register a loaded module's handler and triggers with the iii client.
517
+
518
+ Supports two step file formats:
519
+
520
+ **New motia-compatible format** (preferred):
521
+ config = {
522
+ "name": "my-step",
523
+ "description": "...",
524
+ "triggers": [http("POST", "/path"), queue("topic"), cron("0 * * * * * *")],
525
+ "enqueues": ["topic.done"],
526
+ "flows": ["my-flow"],
527
+ }
528
+ async def handler(input, ctx: FlowContext): ...
529
+
530
+ **Legacy nvent format** (still supported):
531
+ meta = {"id": "my-step", "description": "...", "flows": [...]}
532
+ triggers = [{"type": "http", "config": {"api_path": "path", "http_method": "POST"}}]
533
+ async def handler(input, ctx): ...
534
+
535
+ One function per trigger is registered (mirrors JS worker pattern).
536
+ """
537
+ config_dict = getattr(mod, "config", None)
538
+
539
+ if config_dict is not None:
540
+ # New motia-compatible format: config = { name, description, triggers, flows, enqueues, stream }
541
+ fn_id = config_dict.get("name") or default_id
542
+ if not config_dict.get("name"):
543
+ print(f"[nvent] WARNING: Python step at {getattr(mod, '__file__', '?')} has no 'name' in config — "
544
+ f"falling back to file-path-derived name {fn_id!r}. "
545
+ "Add `config['name'] = '<your-step-name>'` to fix this.", flush=True)
546
+ description = config_dict.get("description")
547
+ triggers = list(config_dict.get("triggers") or [])
548
+ flows = list(config_dict.get("flows") or [])
549
+ enqueues = list(config_dict.get("enqueues") or [])
550
+ stream_name = config_dict.get("stream") or (flows[0] if flows else fn_id.split("::")[0])
551
+ else:
552
+ # Legacy format: meta = {...}, triggers = [...]
553
+ meta = dict(getattr(mod, "meta", {}) or {})
554
+ fn_id = meta.get("name") or default_id
555
+ if not meta.get("name"):
556
+ print(f"[nvent] WARNING: Python step at {getattr(mod, '__file__', '?')} has no 'name' in meta — "
557
+ f"falling back to file-path-derived name {fn_id!r}. "
558
+ "Add `meta['name'] = '<your-step-name>'` to fix this.", flush=True)
559
+ description = meta.get("description")
560
+ triggers = list(getattr(mod, "triggers", []) or [])
561
+ flows = list(meta.get("flows") or [])
562
+ enqueues = list(meta.get("enqueues") or [])
563
+ stream_name = meta.get("stream") or (flows[0] if flows else fn_id.split("::")[0])
564
+
565
+ handler_fn = getattr(mod, "handler", None)
566
+ file_path = getattr(mod, "__file__", None)
567
+
568
+ if not handler_fn:
569
+ print(f"[nvent] Python function {fn_id!r} has no handler — skipping", flush=True)
570
+ return
571
+
572
+ sig = inspect.signature(handler_fn)
573
+ has_ctx = len(list(sig.parameters)) >= 2
574
+
575
+ metadata = {
576
+ "name": fn_id,
577
+ "description": description,
578
+ "filePath": file_path,
579
+ "triggers": [_normalize_trigger_meta(t) for t in triggers],
580
+ "flows": flows,
581
+ "enqueues": enqueues,
582
+ }
583
+
584
+ seen_suffixes: set = set()
585
+ for i, trigger in enumerate(triggers):
586
+ t_type = trigger.get("type", "")
587
+ suffix = _trigger_suffix(trigger)
588
+ if suffix in seen_suffixes:
589
+ suffix = f"{suffix}::{i}"
590
+ seen_suffixes.add(suffix)
591
+
592
+ # Motia-style function_id: steps::<name>::trigger::<descriptive-suffix>
593
+ function_id = f"steps::{fn_id}::trigger::{suffix}"
594
+ iii_cfg = _trigger_to_iii_cfg(trigger)
595
+ is_http = t_type == "http"
596
+
597
+ # Create a per-trigger wrapper that captures trigger type for ctx.match()
598
+ def _make_wrapper(_h, _is_http, _t_type, _fn_id, _hc, _stream_name):
599
+ async def _wrapped(data):
600
+ # Extract and strip inherited stream context from queue messages
601
+ inherited_group = None
602
+ effective_stream = _stream_name
603
+ clean_data = data
604
+ if isinstance(data, dict) and _NVENT_STREAM_KEY in data:
605
+ nvent_stream = data[_NVENT_STREAM_KEY]
606
+ if isinstance(nvent_stream, dict):
607
+ inherited_group = nvent_stream.get('groupId')
608
+ effective_stream = nvent_stream.get('name') or _stream_name
609
+ clean_data = {k: v for k, v in data.items() if k != _NVENT_STREAM_KEY}
610
+ input_data = _iii_sdk.ApiRequest(**clean_data) if (_is_http and isinstance(clean_data, dict)) else clean_data
611
+ if not _hc:
612
+ return await _h(input_data)
613
+ ctx = FlowContext(client, _fn_id, _t_type, input_data, effective_stream, inherited_group)
614
+ return await _h(input_data, ctx)
615
+ return _wrapped
616
+
617
+ wrapped = _make_wrapper(handler_fn, is_http, t_type, fn_id, has_ctx, stream_name)
618
+ client.register_function(
619
+ {"id": function_id, "description": description, "metadata": metadata},
620
+ wrapped,
621
+ )
622
+ client.register_trigger({
623
+ "type": t_type,
624
+ "function_id": function_id,
625
+ "config": {**iii_cfg, "metadata": metadata},
626
+ })
627
+
628
+ print(f"[nvent] registered {fn_id!r} ({len(triggers)} trigger(s))", flush=True)
629
+
630
+
631
+ # ---------------------------------------------------------------------------
632
+ # CLI entry point — invoked by the TypeScript worker manager:
633
+ # python3 _runtime.py <ws_url> <worker_name> <path1> <id1> [<path2> <id2> ...]
634
+ # ---------------------------------------------------------------------------
635
+
636
+ if __name__ == "__main__":
637
+ if len(sys.argv) < 4 or (len(sys.argv) - 3) % 2 != 0:
638
+ print(
639
+ "Usage: _runtime.py <ws_url> <worker_name> <path1> <id1> [<path2> <id2> ...]",
640
+ file=sys.stderr,
641
+ )
642
+ sys.exit(1)
643
+
644
+ _ws_url = sys.argv[1]
645
+ _worker_name = sys.argv[2]
646
+ _fn_pairs = [(sys.argv[i], sys.argv[i + 1]) for i in range(3, len(sys.argv), 2)]
647
+
648
+ options = _iii_sdk.InitOptions(
649
+ worker_name=_worker_name,
650
+ otel={"enabled": True, "service_name": "nvent", "metrics_enabled": False},
651
+ telemetry=_iii_sdk.TelemetryOptions(framework="nvent", project_name=_worker_name),
652
+ )
653
+ client = _iii_sdk.register_worker(_ws_url, options)
654
+ for _path, _fn_id in _fn_pairs:
655
+ _mod = _load(_path, _fn_id)
656
+ _register(client, _mod, _fn_id)
657
+ # Keep process alive — the SDK's background thread is daemon=True so we must
658
+ # block the main thread until the process is externally killed.
659
+ client._thread.join()
@@ -0,0 +1,17 @@
1
+ declare module '#nvent/iii-registry' {
2
+ interface NodeFnInfo {
3
+ name: string
4
+ description?: string
5
+ handler: (input: unknown, ctx: unknown) => Promise<unknown>
6
+ triggers: Array<{ type: string; function_id: string; config?: Record<string, unknown> }>
7
+ enqueues: string[]
8
+ flows: string[]
9
+ stream?: string
10
+ filePath?: string
11
+ }
12
+ export const registry: {
13
+ functions: NodeFnInfo[]
14
+ triggers: Array<{ type: string; function_id: string; config?: Record<string, unknown> }>
15
+ }
16
+ export const pythonFunctions: Array<{ id: string; absPath: string; standalone: boolean }>
17
+ }
package/dist/types.d.mts CHANGED
@@ -1,3 +1,7 @@
1
- export { type ModuleOptions } from '../dist/runtime/config/types.js'
1
+ import type { NuxtModule } from '@nuxt/schema'
2
+
3
+ import type { default as Module } from './module.mjs'
4
+
5
+ export type ModuleOptions = typeof Module extends NuxtModule<infer O> ? Partial<O> : Record<string, any>
2
6
 
3
7
  export { default } from './module.mjs'