lumera 0.6.0__py3-none-any.whl → 0.7.3__py3-none-any.whl
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.
- lumera/__init__.py +9 -6
- lumera/_utils.py +13 -13
- lumera/automations.py +835 -0
- lumera/pb.py +12 -12
- lumera/sdk.py +39 -39
- lumera/storage.py +3 -3
- {lumera-0.6.0.dist-info → lumera-0.7.3.dist-info}/METADATA +1 -1
- lumera-0.7.3.dist-info/RECORD +14 -0
- lumera-0.6.0.dist-info/RECORD +0 -13
- {lumera-0.6.0.dist-info → lumera-0.7.3.dist-info}/WHEEL +0 -0
- {lumera-0.6.0.dist-info → lumera-0.7.3.dist-info}/top_level.txt +0 -0
lumera/__init__.py
CHANGED
|
@@ -5,10 +5,10 @@ This SDK provides helpers for agents running within the Lumera Notebook environm
|
|
|
5
5
|
to interact with the Lumera API and define dynamic user interfaces.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
__version__ = "0.
|
|
8
|
+
__version__ = "0.7.0"
|
|
9
9
|
|
|
10
10
|
# Import new modules (as modules, not individual functions)
|
|
11
|
-
from . import exceptions, llm, locks, pb, storage
|
|
11
|
+
from . import automations, exceptions, llm, locks, pb, storage
|
|
12
12
|
from ._utils import (
|
|
13
13
|
LumeraAPIError,
|
|
14
14
|
RecordNotUniqueError,
|
|
@@ -34,7 +34,7 @@ from .sdk import (
|
|
|
34
34
|
create_record,
|
|
35
35
|
delete_collection,
|
|
36
36
|
delete_record,
|
|
37
|
-
|
|
37
|
+
get_automation_run,
|
|
38
38
|
get_collection,
|
|
39
39
|
get_record,
|
|
40
40
|
get_record_by_external_id,
|
|
@@ -42,8 +42,9 @@ from .sdk import (
|
|
|
42
42
|
list_records,
|
|
43
43
|
query_sql,
|
|
44
44
|
replay_hook,
|
|
45
|
-
|
|
45
|
+
run_automation,
|
|
46
46
|
save_to_lumera,
|
|
47
|
+
update_automation_run,
|
|
47
48
|
update_collection,
|
|
48
49
|
update_record,
|
|
49
50
|
upload_lumera_file,
|
|
@@ -73,8 +74,9 @@ __all__ = [
|
|
|
73
74
|
# Other operations
|
|
74
75
|
"replay_hook",
|
|
75
76
|
"query_sql",
|
|
76
|
-
"
|
|
77
|
-
"
|
|
77
|
+
"run_automation",
|
|
78
|
+
"get_automation_run",
|
|
79
|
+
"update_automation_run",
|
|
78
80
|
"upload_lumera_file",
|
|
79
81
|
"save_to_lumera",
|
|
80
82
|
# Type definitions
|
|
@@ -89,6 +91,7 @@ __all__ = [
|
|
|
89
91
|
"RecordNotFoundError",
|
|
90
92
|
"LockHeldError",
|
|
91
93
|
# New modules (use as lumera.pb, lumera.storage, etc.)
|
|
94
|
+
"automations",
|
|
92
95
|
"pb",
|
|
93
96
|
"storage",
|
|
94
97
|
"llm",
|
lumera/_utils.py
CHANGED
|
@@ -117,10 +117,10 @@ def _utcnow_iso() -> str:
|
|
|
117
117
|
)
|
|
118
118
|
|
|
119
119
|
|
|
120
|
-
def _default_provenance(
|
|
120
|
+
def _default_provenance(automation_id: str, run_id: str | None) -> dict[str, Any]:
|
|
121
121
|
recorded_at = _utcnow_iso()
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
env_automation_id = os.getenv("LUMERA_AUTOMATION_ID", "").strip()
|
|
123
|
+
automation_id = (automation_id or "").strip() or env_automation_id
|
|
124
124
|
|
|
125
125
|
run_id = (run_id or "").strip() or os.getenv("LUMERA_RUN_ID", "").strip()
|
|
126
126
|
|
|
@@ -132,8 +132,8 @@ def _default_provenance(agent_id: str, run_id: str | None) -> dict[str, Any]:
|
|
|
132
132
|
"recorded_at": recorded_at,
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
if
|
|
136
|
-
payload["agent"] = {"id":
|
|
135
|
+
if automation_id:
|
|
136
|
+
payload["agent"] = {"id": automation_id} # Backend still expects "agent" key
|
|
137
137
|
|
|
138
138
|
if run_id:
|
|
139
139
|
payload["agent_run"] = {"id": run_id}
|
|
@@ -284,7 +284,7 @@ def _ensure_mapping(payload: Mapping[str, Any] | None, *, name: str) -> dict[str
|
|
|
284
284
|
return dict(payload)
|
|
285
285
|
|
|
286
286
|
|
|
287
|
-
def
|
|
287
|
+
def _prepare_automation_inputs(
|
|
288
288
|
inputs: Mapping[str, Any] | str | None,
|
|
289
289
|
) -> dict[str, Any] | None:
|
|
290
290
|
if inputs is None:
|
|
@@ -345,7 +345,7 @@ def _upload_file_to_presigned(upload_url: str, path: pathlib.Path, content_type:
|
|
|
345
345
|
resp.raise_for_status()
|
|
346
346
|
|
|
347
347
|
|
|
348
|
-
def
|
|
348
|
+
def _upload_automation_files(
|
|
349
349
|
run_id: str | None,
|
|
350
350
|
files: Mapping[str, str | os.PathLike[str] | Sequence[str | os.PathLike[str]]],
|
|
351
351
|
*,
|
|
@@ -370,7 +370,7 @@ def _upload_agent_files(
|
|
|
370
370
|
size = file_path.stat().st_size
|
|
371
371
|
|
|
372
372
|
body: dict[str, Any] = {
|
|
373
|
-
"scope": "
|
|
373
|
+
"scope": "automation_run",
|
|
374
374
|
"filename": filename,
|
|
375
375
|
"content_type": content_type,
|
|
376
376
|
"size": size,
|
|
@@ -631,7 +631,7 @@ def _upload_session_file(file_path: str, session_id: str) -> dict:
|
|
|
631
631
|
return {"name": filename, "notebook_path": notebook_path}
|
|
632
632
|
|
|
633
633
|
|
|
634
|
-
def
|
|
634
|
+
def _upload_automation_run_file(file_path: str, run_id: str) -> dict:
|
|
635
635
|
token = get_lumera_token()
|
|
636
636
|
path = pathlib.Path(file_path).expanduser().resolve()
|
|
637
637
|
if not path.is_file():
|
|
@@ -644,7 +644,7 @@ def _upload_agent_run_file(file_path: str, run_id: str) -> dict:
|
|
|
644
644
|
headers = {"Authorization": f"token {token}", "Content-Type": "application/json"}
|
|
645
645
|
|
|
646
646
|
resp = requests.post(
|
|
647
|
-
f"{API_BASE}/
|
|
647
|
+
f"{API_BASE}/automation-runs/{run_id}/files/upload-url",
|
|
648
648
|
json={"filename": filename, "content_type": mimetype, "size": size},
|
|
649
649
|
headers=headers,
|
|
650
650
|
timeout=30,
|
|
@@ -742,10 +742,10 @@ __all__ = [
|
|
|
742
742
|
"_is_sequence",
|
|
743
743
|
"_normalize_mount_path",
|
|
744
744
|
"_pretty_size",
|
|
745
|
-
"
|
|
745
|
+
"_prepare_automation_inputs",
|
|
746
746
|
"_record_mutation",
|
|
747
|
-
"
|
|
748
|
-
"
|
|
747
|
+
"_upload_automation_files",
|
|
748
|
+
"_upload_automation_run_file",
|
|
749
749
|
"_upload_file_to_presigned",
|
|
750
750
|
"_upload_document",
|
|
751
751
|
"_upload_session_file",
|
lumera/automations.py
ADDED
|
@@ -0,0 +1,835 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Automation execution and management for Lumera.
|
|
3
|
+
|
|
4
|
+
This module provides a high-level interface for running and managing Lumera
|
|
5
|
+
automations (background Python scripts).
|
|
6
|
+
|
|
7
|
+
Run functions:
|
|
8
|
+
run() - Run an automation by ID
|
|
9
|
+
run_by_external_id() - Run an automation by external_id
|
|
10
|
+
get_run() - Get a run by ID
|
|
11
|
+
list_runs() - List runs for an automation
|
|
12
|
+
|
|
13
|
+
Automation management:
|
|
14
|
+
list() - List all automations
|
|
15
|
+
get() - Get automation by ID
|
|
16
|
+
get_by_external_id() - Get automation by external_id
|
|
17
|
+
create() - Create a new automation
|
|
18
|
+
update() - Update an automation
|
|
19
|
+
upsert() - Create or update by external_id
|
|
20
|
+
|
|
21
|
+
Log streaming:
|
|
22
|
+
stream_logs() - Stream live logs from a running automation
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
>>> from lumera import automations
|
|
26
|
+
>>>
|
|
27
|
+
>>> # Run an automation
|
|
28
|
+
>>> run = automations.run("agent_id", inputs={"limit": 100})
|
|
29
|
+
>>> print(run.id, run.status)
|
|
30
|
+
>>>
|
|
31
|
+
>>> # Wait for completion
|
|
32
|
+
>>> result = run.wait(timeout=300)
|
|
33
|
+
>>>
|
|
34
|
+
>>> # Or poll manually
|
|
35
|
+
>>> while run.status in ["queued", "running"]:
|
|
36
|
+
... time.sleep(5)
|
|
37
|
+
... run.refresh()
|
|
38
|
+
>>> print(run.result)
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import json
|
|
44
|
+
import time
|
|
45
|
+
from typing import Any, Iterator, Mapping
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
# Run operations
|
|
49
|
+
"run",
|
|
50
|
+
"run_by_external_id",
|
|
51
|
+
"get_run",
|
|
52
|
+
"list_runs",
|
|
53
|
+
# Automation management
|
|
54
|
+
"list",
|
|
55
|
+
"get",
|
|
56
|
+
"get_by_external_id",
|
|
57
|
+
"create",
|
|
58
|
+
"update",
|
|
59
|
+
"upsert",
|
|
60
|
+
# Log streaming
|
|
61
|
+
"stream_logs",
|
|
62
|
+
# Classes
|
|
63
|
+
"Run",
|
|
64
|
+
"Automation",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
from ._utils import LumeraAPIError, _api_request
|
|
68
|
+
from .sdk import get_automation_run as _get_automation_run
|
|
69
|
+
from .sdk import run_automation as _run_automation
|
|
70
|
+
|
|
71
|
+
# ============================================================================
|
|
72
|
+
# Run Class
|
|
73
|
+
# ============================================================================
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class Run:
|
|
77
|
+
"""Represents an automation run with methods for polling and waiting.
|
|
78
|
+
|
|
79
|
+
Attributes:
|
|
80
|
+
id: The run ID.
|
|
81
|
+
automation_id: The automation/agent ID.
|
|
82
|
+
status: Current status (queued, running, succeeded, failed, cancelled, timeout).
|
|
83
|
+
inputs: The inputs passed to the run.
|
|
84
|
+
result: The result returned by the automation (when succeeded).
|
|
85
|
+
error: Error message (when failed).
|
|
86
|
+
created: Creation timestamp.
|
|
87
|
+
started_at: Execution start timestamp.
|
|
88
|
+
finished_at: Completion timestamp.
|
|
89
|
+
external_id: Optional correlation ID.
|
|
90
|
+
trigger: How the run was initiated (manual, webhook, schedule, assistant).
|
|
91
|
+
log_pointer: S3 location of archived logs (after completion).
|
|
92
|
+
artifacts: List of output files created during execution.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(self, data: dict[str, Any]) -> None:
|
|
96
|
+
self._data = data
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def id(self) -> str:
|
|
100
|
+
return self._data.get("id", "")
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def automation_id(self) -> str:
|
|
104
|
+
return self._data.get("automation_id", "")
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def status(self) -> str:
|
|
108
|
+
return self._data.get("status", "")
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def inputs(self) -> dict[str, Any]:
|
|
112
|
+
raw = self._data.get("inputs", "{}")
|
|
113
|
+
if isinstance(raw, str):
|
|
114
|
+
try:
|
|
115
|
+
return json.loads(raw)
|
|
116
|
+
except json.JSONDecodeError:
|
|
117
|
+
return {}
|
|
118
|
+
return raw if isinstance(raw, dict) else {}
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def result(self) -> dict[str, Any] | None:
|
|
122
|
+
return self._data.get("result")
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def error(self) -> str | None:
|
|
126
|
+
return self._data.get("error")
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def created(self) -> str | None:
|
|
130
|
+
return self._data.get("created")
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def started_at(self) -> str | None:
|
|
134
|
+
return self._data.get("started_at")
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def finished_at(self) -> str | None:
|
|
138
|
+
return self._data.get("finished_at")
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def external_id(self) -> str | None:
|
|
142
|
+
return self._data.get("external_id")
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def trigger(self) -> str | None:
|
|
146
|
+
return self._data.get("trigger")
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def log_pointer(self) -> dict[str, Any] | None:
|
|
150
|
+
return self._data.get("log_pointer")
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def artifacts(self) -> list[dict[str, Any]]:
|
|
154
|
+
return self._data.get("artifacts") or []
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def is_terminal(self) -> bool:
|
|
158
|
+
"""Returns True if the run has completed (success, failure, or cancelled)."""
|
|
159
|
+
return self.status in ("succeeded", "failed", "cancelled", "timeout")
|
|
160
|
+
|
|
161
|
+
def refresh(self) -> Run:
|
|
162
|
+
"""Refresh run status from the server.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
self (for chaining).
|
|
166
|
+
"""
|
|
167
|
+
if not self.id:
|
|
168
|
+
raise ValueError("Cannot refresh run without id")
|
|
169
|
+
updated = _get_automation_run(run_id=self.id)
|
|
170
|
+
self._data = updated
|
|
171
|
+
return self
|
|
172
|
+
|
|
173
|
+
def wait(
|
|
174
|
+
self,
|
|
175
|
+
timeout: float = 300,
|
|
176
|
+
poll_interval: float = 2.0,
|
|
177
|
+
) -> dict[str, Any] | None:
|
|
178
|
+
"""Block until the run completes or timeout is reached.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
timeout: Maximum seconds to wait (default 300 = 5 minutes).
|
|
182
|
+
poll_interval: Seconds between status checks (default 2).
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
The result dict if succeeded, None otherwise.
|
|
186
|
+
|
|
187
|
+
Raises:
|
|
188
|
+
TimeoutError: If timeout is reached before completion.
|
|
189
|
+
RuntimeError: If the run failed.
|
|
190
|
+
"""
|
|
191
|
+
start = time.monotonic()
|
|
192
|
+
while not self.is_terminal:
|
|
193
|
+
elapsed = time.monotonic() - start
|
|
194
|
+
if elapsed >= timeout:
|
|
195
|
+
raise TimeoutError(
|
|
196
|
+
f"Run {self.id} did not complete within {timeout}s (status: {self.status})"
|
|
197
|
+
)
|
|
198
|
+
time.sleep(poll_interval)
|
|
199
|
+
self.refresh()
|
|
200
|
+
|
|
201
|
+
if self.status == "failed":
|
|
202
|
+
raise RuntimeError(f"Run {self.id} failed: {self.error}")
|
|
203
|
+
if self.status in ("cancelled", "timeout"):
|
|
204
|
+
raise RuntimeError(f"Run {self.id} was {self.status}")
|
|
205
|
+
|
|
206
|
+
return self.result
|
|
207
|
+
|
|
208
|
+
def cancel(self) -> Run:
|
|
209
|
+
"""Cancel the run if it's still running.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
self (for chaining).
|
|
213
|
+
"""
|
|
214
|
+
if not self.id:
|
|
215
|
+
raise ValueError("Cannot cancel run without id")
|
|
216
|
+
result = _api_request("POST", f"automation-runs/{self.id}/cancel")
|
|
217
|
+
if isinstance(result, dict):
|
|
218
|
+
self._data = result
|
|
219
|
+
return self
|
|
220
|
+
|
|
221
|
+
def to_dict(self) -> dict[str, Any]:
|
|
222
|
+
"""Return the underlying data dict."""
|
|
223
|
+
return self._data.copy()
|
|
224
|
+
|
|
225
|
+
def __repr__(self) -> str:
|
|
226
|
+
return f"Run(id={self.id!r}, status={self.status!r}, automation_id={self.automation_id!r})"
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# ============================================================================
|
|
230
|
+
# Automation Class
|
|
231
|
+
# ============================================================================
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class Automation:
|
|
235
|
+
"""Represents an automation definition.
|
|
236
|
+
|
|
237
|
+
Attributes:
|
|
238
|
+
id: The automation ID.
|
|
239
|
+
name: Display name.
|
|
240
|
+
description: Human-readable description.
|
|
241
|
+
external_id: Stable identifier for programmatic access.
|
|
242
|
+
code: Python source code.
|
|
243
|
+
input_schema: JSON schema defining inputs.
|
|
244
|
+
status: Automation status (active, inactive, archived).
|
|
245
|
+
schedule: Cron schedule (if scheduled).
|
|
246
|
+
last_run_status: Status of the most recent run.
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
def __init__(self, data: dict[str, Any]) -> None:
|
|
250
|
+
self._data = data
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def id(self) -> str:
|
|
254
|
+
return self._data.get("id", "")
|
|
255
|
+
|
|
256
|
+
@property
|
|
257
|
+
def name(self) -> str:
|
|
258
|
+
return self._data.get("name", "")
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def description(self) -> str | None:
|
|
262
|
+
return self._data.get("description")
|
|
263
|
+
|
|
264
|
+
@property
|
|
265
|
+
def external_id(self) -> str | None:
|
|
266
|
+
return self._data.get("external_id")
|
|
267
|
+
|
|
268
|
+
@property
|
|
269
|
+
def code(self) -> str:
|
|
270
|
+
return self._data.get("code", "")
|
|
271
|
+
|
|
272
|
+
@property
|
|
273
|
+
def input_schema(self) -> dict[str, Any] | None:
|
|
274
|
+
"""Return the input schema as a dict. API always returns JSON object."""
|
|
275
|
+
return self._data.get("input_schema")
|
|
276
|
+
|
|
277
|
+
@property
|
|
278
|
+
def status(self) -> str | None:
|
|
279
|
+
return self._data.get("status")
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def schedule(self) -> str | None:
|
|
283
|
+
return self._data.get("schedule")
|
|
284
|
+
|
|
285
|
+
@property
|
|
286
|
+
def last_run_status(self) -> str | None:
|
|
287
|
+
return self._data.get("last_run_status")
|
|
288
|
+
|
|
289
|
+
def run(
|
|
290
|
+
self,
|
|
291
|
+
inputs: Mapping[str, Any] | None = None,
|
|
292
|
+
*,
|
|
293
|
+
external_id: str | None = None,
|
|
294
|
+
metadata: Mapping[str, Any] | None = None,
|
|
295
|
+
) -> Run:
|
|
296
|
+
"""Run this automation.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
inputs: Input parameters for the run.
|
|
300
|
+
external_id: Optional correlation ID for idempotency.
|
|
301
|
+
metadata: Optional metadata to attach to the run.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
A Run object representing the execution.
|
|
305
|
+
"""
|
|
306
|
+
return run(self.id, inputs=inputs, external_id=external_id, metadata=metadata)
|
|
307
|
+
|
|
308
|
+
def to_dict(self) -> dict[str, Any]:
|
|
309
|
+
"""Return the underlying data dict."""
|
|
310
|
+
return self._data.copy()
|
|
311
|
+
|
|
312
|
+
def __repr__(self) -> str:
|
|
313
|
+
return f"Automation(id={self.id!r}, name={self.name!r})"
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
# ============================================================================
|
|
317
|
+
# Run Functions
|
|
318
|
+
# ============================================================================
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def run(
|
|
322
|
+
automation_id: str,
|
|
323
|
+
inputs: Mapping[str, Any] | None = None,
|
|
324
|
+
*,
|
|
325
|
+
files: Mapping[str, Any] | None = None,
|
|
326
|
+
external_id: str | None = None,
|
|
327
|
+
metadata: Mapping[str, Any] | None = None,
|
|
328
|
+
) -> Run:
|
|
329
|
+
"""Run an automation by ID.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
automation_id: The automation/agent ID to run.
|
|
333
|
+
inputs: Input parameters (dict). Types are coerced based on input_schema.
|
|
334
|
+
files: File inputs to upload (mapping of input key to file path(s)).
|
|
335
|
+
external_id: Optional correlation ID for idempotency. Repeated calls
|
|
336
|
+
with the same external_id return the existing run.
|
|
337
|
+
metadata: Optional metadata to attach to the run.
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
A Run object for tracking execution status.
|
|
341
|
+
|
|
342
|
+
Example:
|
|
343
|
+
>>> run = automations.run("abc123", inputs={"limit": 100})
|
|
344
|
+
>>> print(run.id, run.status)
|
|
345
|
+
>>> result = run.wait(timeout=300)
|
|
346
|
+
"""
|
|
347
|
+
result = _run_automation(
|
|
348
|
+
automation_id,
|
|
349
|
+
inputs=inputs,
|
|
350
|
+
files=files,
|
|
351
|
+
external_id=external_id,
|
|
352
|
+
metadata=metadata,
|
|
353
|
+
)
|
|
354
|
+
return Run(result)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def run_by_external_id(
|
|
358
|
+
external_id: str,
|
|
359
|
+
inputs: Mapping[str, Any] | None = None,
|
|
360
|
+
*,
|
|
361
|
+
files: Mapping[str, Any] | None = None,
|
|
362
|
+
run_external_id: str | None = None,
|
|
363
|
+
metadata: Mapping[str, Any] | None = None,
|
|
364
|
+
) -> Run:
|
|
365
|
+
"""Run an automation by its external_id (more stable than internal ID).
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
external_id: The automation's external_id (e.g., "deposit_matching:step1").
|
|
369
|
+
inputs: Input parameters (dict).
|
|
370
|
+
files: File inputs to upload.
|
|
371
|
+
run_external_id: Optional correlation ID for the run itself.
|
|
372
|
+
metadata: Optional metadata to attach to the run.
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
A Run object for tracking execution status.
|
|
376
|
+
|
|
377
|
+
Example:
|
|
378
|
+
>>> run = automations.run_by_external_id(
|
|
379
|
+
... "deposit_matching:step1",
|
|
380
|
+
... inputs={"limit": 100}
|
|
381
|
+
... )
|
|
382
|
+
"""
|
|
383
|
+
automation = get_by_external_id(external_id)
|
|
384
|
+
return run(
|
|
385
|
+
automation.id,
|
|
386
|
+
inputs=inputs,
|
|
387
|
+
files=files,
|
|
388
|
+
external_id=run_external_id,
|
|
389
|
+
metadata=metadata,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def get_run(run_id: str) -> Run:
|
|
394
|
+
"""Get a run by its ID.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
run_id: The run ID.
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
A Run object with current status.
|
|
401
|
+
|
|
402
|
+
Example:
|
|
403
|
+
>>> run = automations.get_run("run_abc123")
|
|
404
|
+
>>> print(run.status, run.result)
|
|
405
|
+
"""
|
|
406
|
+
result = _get_automation_run(run_id=run_id)
|
|
407
|
+
return Run(result)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def list_runs(
|
|
411
|
+
automation_id: str | None = None,
|
|
412
|
+
*,
|
|
413
|
+
status: str | None = None,
|
|
414
|
+
limit: int = 100,
|
|
415
|
+
offset: int = 0,
|
|
416
|
+
sort: str = "created",
|
|
417
|
+
dir: str = "desc",
|
|
418
|
+
) -> list[Run]:
|
|
419
|
+
"""List runs, optionally filtered by automation and status.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
automation_id: Filter by automation ID (optional).
|
|
423
|
+
status: Filter by status (queued, running, succeeded, failed, etc.).
|
|
424
|
+
limit: Maximum number of results (default 100).
|
|
425
|
+
offset: Pagination offset.
|
|
426
|
+
sort: Sort field (default "created").
|
|
427
|
+
dir: Sort direction ("asc" or "desc", default "desc").
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
List of Run objects.
|
|
431
|
+
|
|
432
|
+
Example:
|
|
433
|
+
>>> runs = automations.list_runs("agent_id", status="succeeded", limit=10)
|
|
434
|
+
>>> for r in runs:
|
|
435
|
+
... print(r.id, r.created, r.status)
|
|
436
|
+
"""
|
|
437
|
+
params: dict[str, Any] = {
|
|
438
|
+
"limit": limit,
|
|
439
|
+
"offset": offset,
|
|
440
|
+
"sort": sort,
|
|
441
|
+
"dir": dir,
|
|
442
|
+
}
|
|
443
|
+
if automation_id:
|
|
444
|
+
params["automation_id"] = automation_id
|
|
445
|
+
if status:
|
|
446
|
+
params["status"] = status
|
|
447
|
+
|
|
448
|
+
result = _api_request("GET", "automation-runs", params=params)
|
|
449
|
+
items = []
|
|
450
|
+
if isinstance(result, dict):
|
|
451
|
+
# API returns runs in different keys depending on context
|
|
452
|
+
items = result.get("automation_runs") or result.get("data") or []
|
|
453
|
+
return [Run(item) for item in items if isinstance(item, dict)]
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
# ============================================================================
|
|
457
|
+
# Automation Management Functions
|
|
458
|
+
# ============================================================================
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def list(
|
|
462
|
+
*,
|
|
463
|
+
q: str | None = None,
|
|
464
|
+
limit: int = 50,
|
|
465
|
+
offset: int = 0,
|
|
466
|
+
sort: str = "created",
|
|
467
|
+
dir: str = "desc",
|
|
468
|
+
) -> list[Automation]:
|
|
469
|
+
"""List all automations.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
q: Search query (case-insensitive name/description search).
|
|
473
|
+
limit: Maximum number of results (default 50).
|
|
474
|
+
offset: Pagination offset.
|
|
475
|
+
sort: Sort field (default "created").
|
|
476
|
+
dir: Sort direction ("asc" or "desc", default "desc").
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
List of Automation objects.
|
|
480
|
+
|
|
481
|
+
Example:
|
|
482
|
+
>>> all_automations = automations.list()
|
|
483
|
+
>>> for a in all_automations:
|
|
484
|
+
... print(a.id, a.name)
|
|
485
|
+
"""
|
|
486
|
+
params: dict[str, Any] = {
|
|
487
|
+
"limit": limit,
|
|
488
|
+
"offset": offset,
|
|
489
|
+
"sort": sort,
|
|
490
|
+
"dir": dir,
|
|
491
|
+
}
|
|
492
|
+
if q:
|
|
493
|
+
params["q"] = q
|
|
494
|
+
|
|
495
|
+
result = _api_request("GET", "automations", params=params)
|
|
496
|
+
items = []
|
|
497
|
+
if isinstance(result, dict):
|
|
498
|
+
items = result.get("automations") or []
|
|
499
|
+
return [Automation(item) for item in items if isinstance(item, dict)]
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def get(automation_id: str) -> Automation:
|
|
503
|
+
"""Get an automation by ID.
|
|
504
|
+
|
|
505
|
+
Args:
|
|
506
|
+
automation_id: The automation ID.
|
|
507
|
+
|
|
508
|
+
Returns:
|
|
509
|
+
An Automation object.
|
|
510
|
+
|
|
511
|
+
Example:
|
|
512
|
+
>>> agent = automations.get("abc123")
|
|
513
|
+
>>> print(agent.name, agent.input_schema)
|
|
514
|
+
"""
|
|
515
|
+
automation_id = automation_id.strip()
|
|
516
|
+
if not automation_id:
|
|
517
|
+
raise ValueError("automation_id is required")
|
|
518
|
+
|
|
519
|
+
result = _api_request("GET", f"automations/{automation_id}")
|
|
520
|
+
if not isinstance(result, dict):
|
|
521
|
+
raise RuntimeError("Unexpected response")
|
|
522
|
+
return Automation(result)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def get_by_external_id(external_id: str) -> Automation:
|
|
526
|
+
"""Get an automation by its external_id.
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
external_id: The automation's external_id.
|
|
530
|
+
|
|
531
|
+
Returns:
|
|
532
|
+
An Automation object.
|
|
533
|
+
|
|
534
|
+
Raises:
|
|
535
|
+
LumeraAPIError: If no automation with that external_id exists.
|
|
536
|
+
|
|
537
|
+
Example:
|
|
538
|
+
>>> agent = automations.get_by_external_id("deposit_matching:step1")
|
|
539
|
+
>>> print(agent.id, agent.name)
|
|
540
|
+
"""
|
|
541
|
+
external_id = external_id.strip()
|
|
542
|
+
if not external_id:
|
|
543
|
+
raise ValueError("external_id is required")
|
|
544
|
+
|
|
545
|
+
result = _api_request("GET", "automations", params={"external_id": external_id, "limit": 1})
|
|
546
|
+
if isinstance(result, dict):
|
|
547
|
+
items = result.get("automations") or []
|
|
548
|
+
if items and isinstance(items[0], dict):
|
|
549
|
+
return Automation(items[0])
|
|
550
|
+
|
|
551
|
+
raise LumeraAPIError(
|
|
552
|
+
404,
|
|
553
|
+
f"Automation with external_id '{external_id}' not found",
|
|
554
|
+
url="automations",
|
|
555
|
+
payload=None,
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def create(
|
|
560
|
+
name: str,
|
|
561
|
+
code: str,
|
|
562
|
+
*,
|
|
563
|
+
input_schema: Mapping[str, Any] | None = None,
|
|
564
|
+
description: str | None = None,
|
|
565
|
+
external_id: str | None = None,
|
|
566
|
+
schedule: str | None = None,
|
|
567
|
+
schedule_tz: str | None = None,
|
|
568
|
+
) -> Automation:
|
|
569
|
+
"""Create a new automation.
|
|
570
|
+
|
|
571
|
+
Args:
|
|
572
|
+
name: Display name for the automation.
|
|
573
|
+
code: Python code with the entrypoint function.
|
|
574
|
+
input_schema: JSON schema defining inputs. Should include
|
|
575
|
+
``function.name`` and ``function.parameters``.
|
|
576
|
+
description: Human-readable description.
|
|
577
|
+
external_id: Stable identifier for programmatic access.
|
|
578
|
+
schedule: Cron expression for scheduled runs.
|
|
579
|
+
schedule_tz: Timezone for schedule (e.g., "America/New_York").
|
|
580
|
+
|
|
581
|
+
Returns:
|
|
582
|
+
The created Automation object.
|
|
583
|
+
|
|
584
|
+
Example:
|
|
585
|
+
>>> agent = automations.create(
|
|
586
|
+
... name="My Automation",
|
|
587
|
+
... code="def main(x): return {'result': x * 2}",
|
|
588
|
+
... input_schema={
|
|
589
|
+
... "type": "function",
|
|
590
|
+
... "function": {
|
|
591
|
+
... "name": "main",
|
|
592
|
+
... "parameters": {
|
|
593
|
+
... "type": "object",
|
|
594
|
+
... "properties": {"x": {"type": "integer"}},
|
|
595
|
+
... "required": ["x"]
|
|
596
|
+
... }
|
|
597
|
+
... }
|
|
598
|
+
... }
|
|
599
|
+
... )
|
|
600
|
+
"""
|
|
601
|
+
payload: dict[str, Any] = {
|
|
602
|
+
"name": name,
|
|
603
|
+
"code": code,
|
|
604
|
+
}
|
|
605
|
+
if input_schema is not None:
|
|
606
|
+
payload["input_schema"] = input_schema
|
|
607
|
+
if description is not None:
|
|
608
|
+
payload["description"] = description
|
|
609
|
+
if external_id is not None:
|
|
610
|
+
payload["external_id"] = external_id
|
|
611
|
+
if schedule is not None:
|
|
612
|
+
payload["schedule"] = schedule
|
|
613
|
+
if schedule_tz is not None:
|
|
614
|
+
payload["schedule_tz"] = schedule_tz
|
|
615
|
+
|
|
616
|
+
result = _api_request("POST", "automations", json_body=payload)
|
|
617
|
+
if not isinstance(result, dict):
|
|
618
|
+
raise RuntimeError("Unexpected response")
|
|
619
|
+
return Automation(result)
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def update(
|
|
623
|
+
automation_id: str,
|
|
624
|
+
*,
|
|
625
|
+
name: str | None = None,
|
|
626
|
+
code: str | None = None,
|
|
627
|
+
input_schema: Mapping[str, Any] | None = None,
|
|
628
|
+
description: str | None = None,
|
|
629
|
+
external_id: str | None = None,
|
|
630
|
+
schedule: str | None = None,
|
|
631
|
+
schedule_tz: str | None = None,
|
|
632
|
+
) -> Automation:
|
|
633
|
+
"""Update an existing automation.
|
|
634
|
+
|
|
635
|
+
Args:
|
|
636
|
+
automation_id: The automation ID to update.
|
|
637
|
+
name: New display name.
|
|
638
|
+
code: New Python code.
|
|
639
|
+
input_schema: New input schema.
|
|
640
|
+
description: New description.
|
|
641
|
+
external_id: New external_id.
|
|
642
|
+
schedule: New cron schedule (empty string to clear).
|
|
643
|
+
schedule_tz: New schedule timezone.
|
|
644
|
+
|
|
645
|
+
Returns:
|
|
646
|
+
The updated Automation object.
|
|
647
|
+
|
|
648
|
+
Example:
|
|
649
|
+
>>> automations.update("abc123", code=new_code)
|
|
650
|
+
"""
|
|
651
|
+
automation_id = automation_id.strip()
|
|
652
|
+
if not automation_id:
|
|
653
|
+
raise ValueError("automation_id is required")
|
|
654
|
+
|
|
655
|
+
payload: dict[str, Any] = {}
|
|
656
|
+
if name is not None:
|
|
657
|
+
payload["name"] = name
|
|
658
|
+
if code is not None:
|
|
659
|
+
payload["code"] = code
|
|
660
|
+
if input_schema is not None:
|
|
661
|
+
payload["input_schema"] = input_schema
|
|
662
|
+
if description is not None:
|
|
663
|
+
payload["description"] = description
|
|
664
|
+
if external_id is not None:
|
|
665
|
+
payload["external_id"] = external_id
|
|
666
|
+
if schedule is not None:
|
|
667
|
+
payload["schedule"] = schedule
|
|
668
|
+
if schedule_tz is not None:
|
|
669
|
+
payload["schedule_tz"] = schedule_tz
|
|
670
|
+
|
|
671
|
+
if not payload:
|
|
672
|
+
raise ValueError("At least one field to update is required")
|
|
673
|
+
|
|
674
|
+
result = _api_request("PATCH", f"automations/{automation_id}", json_body=payload)
|
|
675
|
+
if not isinstance(result, dict):
|
|
676
|
+
raise RuntimeError("Unexpected response")
|
|
677
|
+
return Automation(result)
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def upsert(
|
|
681
|
+
external_id: str,
|
|
682
|
+
*,
|
|
683
|
+
name: str,
|
|
684
|
+
code: str,
|
|
685
|
+
input_schema: Mapping[str, Any] | None = None,
|
|
686
|
+
description: str | None = None,
|
|
687
|
+
schedule: str | None = None,
|
|
688
|
+
schedule_tz: str | None = None,
|
|
689
|
+
) -> Automation:
|
|
690
|
+
"""Create or update an automation by external_id (idempotent deploy).
|
|
691
|
+
|
|
692
|
+
If an automation with the given external_id exists, it will be updated.
|
|
693
|
+
Otherwise, a new automation will be created.
|
|
694
|
+
|
|
695
|
+
Args:
|
|
696
|
+
external_id: Stable identifier (required for upsert).
|
|
697
|
+
name: Display name.
|
|
698
|
+
code: Python code.
|
|
699
|
+
input_schema: JSON schema defining inputs.
|
|
700
|
+
description: Human-readable description.
|
|
701
|
+
schedule: Cron expression for scheduled runs.
|
|
702
|
+
schedule_tz: Timezone for schedule.
|
|
703
|
+
|
|
704
|
+
Returns:
|
|
705
|
+
The created or updated Automation object.
|
|
706
|
+
|
|
707
|
+
Example:
|
|
708
|
+
>>> # Idempotent deployment
|
|
709
|
+
>>> automations.upsert(
|
|
710
|
+
... external_id="my_app:my_automation",
|
|
711
|
+
... name="My Automation",
|
|
712
|
+
... code=open("my_script.py").read(),
|
|
713
|
+
... input_schema=schema
|
|
714
|
+
... )
|
|
715
|
+
"""
|
|
716
|
+
external_id = external_id.strip()
|
|
717
|
+
if not external_id:
|
|
718
|
+
raise ValueError("external_id is required for upsert")
|
|
719
|
+
|
|
720
|
+
# Try to find existing
|
|
721
|
+
try:
|
|
722
|
+
existing = get_by_external_id(external_id)
|
|
723
|
+
# Update existing
|
|
724
|
+
return update(
|
|
725
|
+
existing.id,
|
|
726
|
+
name=name,
|
|
727
|
+
code=code,
|
|
728
|
+
input_schema=input_schema,
|
|
729
|
+
description=description,
|
|
730
|
+
schedule=schedule,
|
|
731
|
+
schedule_tz=schedule_tz,
|
|
732
|
+
)
|
|
733
|
+
except LumeraAPIError as e:
|
|
734
|
+
if e.status_code != 404:
|
|
735
|
+
raise
|
|
736
|
+
# Create new
|
|
737
|
+
return create(
|
|
738
|
+
name=name,
|
|
739
|
+
code=code,
|
|
740
|
+
input_schema=input_schema,
|
|
741
|
+
description=description,
|
|
742
|
+
external_id=external_id,
|
|
743
|
+
schedule=schedule,
|
|
744
|
+
schedule_tz=schedule_tz,
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def delete(automation_id: str) -> None:
|
|
749
|
+
"""Delete an automation.
|
|
750
|
+
|
|
751
|
+
Args:
|
|
752
|
+
automation_id: The automation ID to delete.
|
|
753
|
+
|
|
754
|
+
Example:
|
|
755
|
+
>>> automations.delete("abc123")
|
|
756
|
+
"""
|
|
757
|
+
automation_id = automation_id.strip()
|
|
758
|
+
if not automation_id:
|
|
759
|
+
raise ValueError("automation_id is required")
|
|
760
|
+
|
|
761
|
+
_api_request("DELETE", f"automations/{automation_id}")
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
# ============================================================================
|
|
765
|
+
# Log Streaming
|
|
766
|
+
# ============================================================================
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def stream_logs(run_id: str, *, timeout: float = 30) -> Iterator[str]:
|
|
770
|
+
"""Stream live logs from a running automation.
|
|
771
|
+
|
|
772
|
+
Connects to the server-sent events endpoint and yields log lines
|
|
773
|
+
as they arrive. Stops when the run completes.
|
|
774
|
+
|
|
775
|
+
Args:
|
|
776
|
+
run_id: The run ID to stream logs from.
|
|
777
|
+
timeout: HTTP connection timeout in seconds.
|
|
778
|
+
|
|
779
|
+
Yields:
|
|
780
|
+
Log lines as strings.
|
|
781
|
+
|
|
782
|
+
Example:
|
|
783
|
+
>>> for line in automations.stream_logs("run_id"):
|
|
784
|
+
... print(line)
|
|
785
|
+
"""
|
|
786
|
+
import base64
|
|
787
|
+
import os
|
|
788
|
+
|
|
789
|
+
import requests
|
|
790
|
+
|
|
791
|
+
run_id = run_id.strip()
|
|
792
|
+
if not run_id:
|
|
793
|
+
raise ValueError("run_id is required")
|
|
794
|
+
|
|
795
|
+
base_url = os.environ.get("LUMERA_BASE_URL", "https://app.lumerahq.com/api").rstrip("/")
|
|
796
|
+
token = os.environ.get("LUMERA_TOKEN", "")
|
|
797
|
+
if not token:
|
|
798
|
+
raise ValueError("LUMERA_TOKEN environment variable is required")
|
|
799
|
+
|
|
800
|
+
url = f"{base_url}/automation-runs/{run_id}/logs/live"
|
|
801
|
+
headers = {
|
|
802
|
+
"Authorization": f"token {token}",
|
|
803
|
+
"Accept": "text/event-stream",
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
with requests.get(url, headers=headers, stream=True, timeout=timeout) as resp:
|
|
807
|
+
resp.raise_for_status()
|
|
808
|
+
|
|
809
|
+
current_event = ""
|
|
810
|
+
current_data = ""
|
|
811
|
+
|
|
812
|
+
for line in resp.iter_lines(decode_unicode=True):
|
|
813
|
+
if line is None:
|
|
814
|
+
continue
|
|
815
|
+
|
|
816
|
+
if line.startswith("event:"):
|
|
817
|
+
current_event = line[6:].strip()
|
|
818
|
+
elif line.startswith("data:"):
|
|
819
|
+
current_data = line[5:].strip()
|
|
820
|
+
elif line == "":
|
|
821
|
+
# End of event
|
|
822
|
+
if current_event == "chunk" and current_data:
|
|
823
|
+
try:
|
|
824
|
+
data = json.loads(current_data)
|
|
825
|
+
if "data" in data:
|
|
826
|
+
# Data is base64-encoded
|
|
827
|
+
raw = base64.b64decode(data["data"])
|
|
828
|
+
decoded = raw.decode("utf-8", errors="replace")
|
|
829
|
+
yield from decoded.splitlines()
|
|
830
|
+
except (json.JSONDecodeError, KeyError):
|
|
831
|
+
pass
|
|
832
|
+
elif current_event == "complete":
|
|
833
|
+
return
|
|
834
|
+
current_event = ""
|
|
835
|
+
current_data = ""
|
lumera/pb.py
CHANGED
|
@@ -76,6 +76,18 @@ __all__ = [
|
|
|
76
76
|
|
|
77
77
|
# Import underlying SDK functions (prefixed with _ to indicate internal use)
|
|
78
78
|
from ._utils import LumeraAPIError
|
|
79
|
+
from .sdk import (
|
|
80
|
+
bulk_delete_records as _bulk_delete_records,
|
|
81
|
+
)
|
|
82
|
+
from .sdk import (
|
|
83
|
+
bulk_insert_records as _bulk_insert_records,
|
|
84
|
+
)
|
|
85
|
+
from .sdk import (
|
|
86
|
+
bulk_update_records as _bulk_update_records,
|
|
87
|
+
)
|
|
88
|
+
from .sdk import (
|
|
89
|
+
bulk_upsert_records as _bulk_upsert_records,
|
|
90
|
+
)
|
|
79
91
|
from .sdk import (
|
|
80
92
|
create_collection as _create_collection,
|
|
81
93
|
)
|
|
@@ -112,18 +124,6 @@ from .sdk import (
|
|
|
112
124
|
from .sdk import (
|
|
113
125
|
upsert_record as _upsert_record,
|
|
114
126
|
)
|
|
115
|
-
from .sdk import (
|
|
116
|
-
bulk_delete_records as _bulk_delete_records,
|
|
117
|
-
)
|
|
118
|
-
from .sdk import (
|
|
119
|
-
bulk_update_records as _bulk_update_records,
|
|
120
|
-
)
|
|
121
|
-
from .sdk import (
|
|
122
|
-
bulk_upsert_records as _bulk_upsert_records,
|
|
123
|
-
)
|
|
124
|
-
from .sdk import (
|
|
125
|
-
bulk_insert_records as _bulk_insert_records,
|
|
126
|
-
)
|
|
127
127
|
|
|
128
128
|
|
|
129
129
|
def search(
|
lumera/sdk.py
CHANGED
|
@@ -51,10 +51,10 @@ from ._utils import (
|
|
|
51
51
|
_default_provenance,
|
|
52
52
|
_ensure_mapping,
|
|
53
53
|
_is_sequence,
|
|
54
|
-
|
|
54
|
+
_prepare_automation_inputs,
|
|
55
55
|
_record_mutation,
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
_upload_automation_files,
|
|
57
|
+
_upload_automation_run_file,
|
|
58
58
|
_upload_document,
|
|
59
59
|
_upload_lumera_file,
|
|
60
60
|
_upload_session_file,
|
|
@@ -388,8 +388,8 @@ def get_record_by_external_id(collection_id_or_name: str, external_id: str) -> d
|
|
|
388
388
|
return first
|
|
389
389
|
|
|
390
390
|
|
|
391
|
-
def
|
|
392
|
-
|
|
391
|
+
def run_automation(
|
|
392
|
+
automation_id: str,
|
|
393
393
|
*,
|
|
394
394
|
inputs: Mapping[str, Any] | str | None = None,
|
|
395
395
|
files: Mapping[str, str | os.PathLike[str] | Sequence[str | os.PathLike[str]]] | None = None,
|
|
@@ -402,7 +402,7 @@ def run_agent(
|
|
|
402
402
|
"""Create an agent run and optionally upload files for file inputs.
|
|
403
403
|
|
|
404
404
|
Args:
|
|
405
|
-
|
|
405
|
+
automation_id: The automation/agent to run. Required.
|
|
406
406
|
inputs: Inputs payload (dict or JSON string). File refs are resolved automatically.
|
|
407
407
|
files: Mapping of input key -> path(s) to upload before run creation.
|
|
408
408
|
status: Optional initial status (defaults to ``queued``).
|
|
@@ -413,16 +413,18 @@ def run_agent(
|
|
|
413
413
|
metadata: Arbitrary JSON metadata to persist with the run (e.g., callback_url).
|
|
414
414
|
"""
|
|
415
415
|
|
|
416
|
-
|
|
417
|
-
if not
|
|
418
|
-
raise ValueError("
|
|
416
|
+
automation_id = automation_id.strip()
|
|
417
|
+
if not automation_id:
|
|
418
|
+
raise ValueError("automation_id is required")
|
|
419
419
|
|
|
420
420
|
run_id: str | None = None
|
|
421
421
|
|
|
422
|
-
prepared_inputs =
|
|
422
|
+
prepared_inputs = _prepare_automation_inputs(inputs) or {}
|
|
423
423
|
|
|
424
424
|
file_map = files or {}
|
|
425
|
-
run_id, upload_descriptors =
|
|
425
|
+
run_id, upload_descriptors = _upload_automation_files(
|
|
426
|
+
run_id, file_map, api_request=_api_request
|
|
427
|
+
)
|
|
426
428
|
|
|
427
429
|
final_inputs = json.loads(json.dumps(prepared_inputs)) if prepared_inputs else {}
|
|
428
430
|
for key, descriptors in upload_descriptors.items():
|
|
@@ -433,7 +435,7 @@ def run_agent(
|
|
|
433
435
|
|
|
434
436
|
cleaned_status = status.strip() if isinstance(status, str) else ""
|
|
435
437
|
payload: dict[str, Any] = {
|
|
436
|
-
"
|
|
438
|
+
"automation_id": automation_id,
|
|
437
439
|
"inputs": json.dumps(final_inputs),
|
|
438
440
|
"status": cleaned_status or "queued",
|
|
439
441
|
}
|
|
@@ -447,24 +449,24 @@ def run_agent(
|
|
|
447
449
|
payload["metadata"] = _ensure_mapping(metadata, name="metadata")
|
|
448
450
|
payload["lm_provenance"] = _ensure_mapping(
|
|
449
451
|
provenance, name="provenance"
|
|
450
|
-
) or _default_provenance(
|
|
452
|
+
) or _default_provenance(automation_id, run_id)
|
|
451
453
|
|
|
452
|
-
run = _api_request("POST", "
|
|
454
|
+
run = _api_request("POST", "automation-runs", json_body=payload)
|
|
453
455
|
if not isinstance(run, dict):
|
|
454
456
|
raise RuntimeError("unexpected response payload")
|
|
455
457
|
return run
|
|
456
458
|
|
|
457
459
|
|
|
458
|
-
def
|
|
459
|
-
|
|
460
|
+
def get_automation_run(
|
|
461
|
+
automation_id: str | None = None,
|
|
460
462
|
*,
|
|
461
463
|
run_id: str | None = None,
|
|
462
464
|
external_id: str | None = None,
|
|
463
465
|
) -> dict[str, Any]:
|
|
464
|
-
"""Fetch an agent run by id or by
|
|
466
|
+
"""Fetch an agent run by id or by automation_id + external_id idempotency key.
|
|
465
467
|
|
|
466
468
|
Args:
|
|
467
|
-
|
|
469
|
+
automation_id: Agent id for external_id lookup. Required when ``run_id`` is not provided.
|
|
468
470
|
run_id: Optional run id. When provided, this takes precedence over external_id lookup.
|
|
469
471
|
external_id: Optional idempotency key to look up the latest run for the agent.
|
|
470
472
|
|
|
@@ -474,29 +476,33 @@ def get_agent_run(
|
|
|
474
476
|
"""
|
|
475
477
|
|
|
476
478
|
if run_id:
|
|
477
|
-
return _api_request("GET", f"
|
|
479
|
+
return _api_request("GET", f"automation-runs/{run_id}")
|
|
478
480
|
|
|
479
|
-
|
|
481
|
+
automation_id = automation_id.strip() if isinstance(automation_id, str) else ""
|
|
480
482
|
external_id = external_id.strip() if isinstance(external_id, str) else ""
|
|
481
|
-
if not
|
|
482
|
-
raise ValueError("
|
|
483
|
+
if not automation_id:
|
|
484
|
+
raise ValueError("automation_id is required when run_id is not provided")
|
|
483
485
|
if not external_id:
|
|
484
486
|
raise ValueError("external_id is required when run_id is not provided")
|
|
485
487
|
|
|
486
488
|
resp = _api_request(
|
|
487
489
|
"GET",
|
|
488
|
-
"
|
|
489
|
-
params={
|
|
490
|
+
"automation-runs",
|
|
491
|
+
params={
|
|
492
|
+
"automation_id": automation_id,
|
|
493
|
+
"external_id": external_id,
|
|
494
|
+
"limit": 1,
|
|
495
|
+
},
|
|
490
496
|
)
|
|
491
|
-
runs = resp.get("
|
|
497
|
+
runs = resp.get("data") if isinstance(resp, dict) else None # Backend returns "data" key
|
|
492
498
|
if runs and isinstance(runs, list) and runs and isinstance(runs[0], dict):
|
|
493
499
|
return runs[0]
|
|
494
500
|
|
|
495
|
-
url = _api_url("
|
|
496
|
-
raise _LumeraAPIError(404, "
|
|
501
|
+
url = _api_url("automation-runs")
|
|
502
|
+
raise _LumeraAPIError(404, "automation run not found", url=url, payload=None)
|
|
497
503
|
|
|
498
504
|
|
|
499
|
-
def
|
|
505
|
+
def update_automation_run(
|
|
500
506
|
run_id: str,
|
|
501
507
|
*,
|
|
502
508
|
result: Mapping[str, Any] | None = None,
|
|
@@ -533,7 +539,7 @@ def update_agent_run(
|
|
|
533
539
|
if not payload:
|
|
534
540
|
raise ValueError("at least one field to update is required")
|
|
535
541
|
|
|
536
|
-
response = _api_request("PATCH", f"
|
|
542
|
+
response = _api_request("PATCH", f"automation-runs/{run_id}", json_body=payload)
|
|
537
543
|
if not isinstance(response, dict):
|
|
538
544
|
raise RuntimeError("unexpected response payload")
|
|
539
545
|
return response
|
|
@@ -627,9 +633,7 @@ def bulk_update_records(
|
|
|
627
633
|
raise ValueError("data is required")
|
|
628
634
|
|
|
629
635
|
path = f"collections/{collection_id_or_name}/records/bulk/update"
|
|
630
|
-
result = _api_request(
|
|
631
|
-
"POST", path, json_body={"ids": list(record_ids), "data": dict(data)}
|
|
632
|
-
)
|
|
636
|
+
result = _api_request("POST", path, json_body={"ids": list(record_ids), "data": dict(data)})
|
|
633
637
|
return result if isinstance(result, dict) else {}
|
|
634
638
|
|
|
635
639
|
|
|
@@ -652,9 +656,7 @@ def bulk_upsert_records(
|
|
|
652
656
|
raise ValueError("records is required")
|
|
653
657
|
|
|
654
658
|
path = f"collections/{collection_id_or_name}/records/bulk/upsert"
|
|
655
|
-
result = _api_request(
|
|
656
|
-
"POST", path, json_body={"records": [dict(r) for r in records]}
|
|
657
|
-
)
|
|
659
|
+
result = _api_request("POST", path, json_body={"records": [dict(r) for r in records]})
|
|
658
660
|
return result if isinstance(result, dict) else {}
|
|
659
661
|
|
|
660
662
|
|
|
@@ -677,9 +679,7 @@ def bulk_insert_records(
|
|
|
677
679
|
raise ValueError("records is required")
|
|
678
680
|
|
|
679
681
|
path = f"collections/{collection_id_or_name}/records/bulk/insert"
|
|
680
|
-
result = _api_request(
|
|
681
|
-
"POST", path, json_body={"records": [dict(r) for r in records]}
|
|
682
|
-
)
|
|
682
|
+
result = _api_request("POST", path, json_body={"records": [dict(r) for r in records]})
|
|
683
683
|
return result if isinstance(result, dict) else {}
|
|
684
684
|
|
|
685
685
|
|
|
@@ -912,7 +912,7 @@ def save_to_lumera(file_path: str) -> dict:
|
|
|
912
912
|
|
|
913
913
|
run_id = os.getenv("LUMERA_RUN_ID", "").strip()
|
|
914
914
|
if run_id:
|
|
915
|
-
return
|
|
915
|
+
return _upload_automation_run_file(file_path, run_id)
|
|
916
916
|
|
|
917
917
|
session_id = os.getenv("LUMERA_SESSION_ID", "").strip()
|
|
918
918
|
if session_id:
|
lumera/storage.py
CHANGED
|
@@ -116,7 +116,7 @@ def upload(
|
|
|
116
116
|
|
|
117
117
|
# Request presigned upload URL
|
|
118
118
|
resp = requests.post(
|
|
119
|
-
f"{API_BASE}/
|
|
119
|
+
f"{API_BASE}/automation-runs/{run_id}/files/upload-url",
|
|
120
120
|
json={"filename": filename, "content_type": content_type, "size": size},
|
|
121
121
|
headers=headers,
|
|
122
122
|
timeout=30,
|
|
@@ -215,7 +215,7 @@ def download_url(path: str) -> str:
|
|
|
215
215
|
headers = {"Authorization": f"token {token}"}
|
|
216
216
|
|
|
217
217
|
resp = requests.get(
|
|
218
|
-
f"{API_BASE}/
|
|
218
|
+
f"{API_BASE}/automation-runs/{run_id}/files/download-url",
|
|
219
219
|
params={"name": filename},
|
|
220
220
|
headers=headers,
|
|
221
221
|
timeout=30,
|
|
@@ -257,7 +257,7 @@ def list_files(prefix: str | None = None) -> list[dict[str, Any]]:
|
|
|
257
257
|
token = get_lumera_token()
|
|
258
258
|
headers = {"Authorization": f"token {token}"}
|
|
259
259
|
|
|
260
|
-
resp = requests.get(f"{API_BASE}/
|
|
260
|
+
resp = requests.get(f"{API_BASE}/automation-runs/{run_id}/files", headers=headers, timeout=30)
|
|
261
261
|
resp.raise_for_status()
|
|
262
262
|
|
|
263
263
|
data = resp.json()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
lumera/__init__.py,sha256=NuZgCTtxQNdj1HKltc76XIcx8kUuE4PKY7mUy5XCXiU,2296
|
|
2
|
+
lumera/_utils.py,sha256=Ur4ZEf-khH3zFzX-aY4Y5uDqgYSWz9kJEkoR9WKy5yc,23727
|
|
3
|
+
lumera/automations.py,sha256=ZHN4m1pIO7py_F3DkUTTDTsr3dbJy7Mly_-gXt3h31o,24780
|
|
4
|
+
lumera/exceptions.py,sha256=bNsx4iYaroAAGsYxErfELC2B5ZJ3w5lVa1kKdIx5s9g,2173
|
|
5
|
+
lumera/google.py,sha256=3IVNL1HaOtsTmunl0alnGFuUAkzQQRyCEA3CKjlPqO0,10183
|
|
6
|
+
lumera/llm.py,sha256=pUTZK7t3GTK0vfxMI1PJgJwNendyuiJc5MB1pUj2vxE,14412
|
|
7
|
+
lumera/locks.py,sha256=8l_qxb8nrxge7YJ-ApUTJ5MeYpIdxDeEa94Eim9O-YM,6806
|
|
8
|
+
lumera/pb.py,sha256=EZABRPVbrSJ8_9Yh-BLt9cp4omkwo5r10Qs8v7wc1qc,18236
|
|
9
|
+
lumera/sdk.py,sha256=GVu1Pe_r7qNPDJ5g7qeACAfW5w5TDOyK8LN3VvDwk0s,30446
|
|
10
|
+
lumera/storage.py,sha256=b0W6JNSGfmhJIcmK3vrATXAwxIr_bfrj-hPuQRVLTYU,8206
|
|
11
|
+
lumera-0.7.3.dist-info/METADATA,sha256=G0fM5Y0DkPMr6QsNOuUaUZPnpKiqpo25_hEMlB7T1yg,1611
|
|
12
|
+
lumera-0.7.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
13
|
+
lumera-0.7.3.dist-info/top_level.txt,sha256=HgfK4XQkpMTnM2E5iWM4kB711FnYqUY9dglzib3pWlE,7
|
|
14
|
+
lumera-0.7.3.dist-info/RECORD,,
|
lumera-0.6.0.dist-info/RECORD
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
lumera/__init__.py,sha256=AtbfmFoPuLEIcjBS4L-XWL-ARpjUIfR370-A7PAfsC8,2188
|
|
2
|
-
lumera/_utils.py,sha256=QyAaphxXGEK8XNPO0ghKLgTOYhAxcF_j3W0T8StzjxA,23610
|
|
3
|
-
lumera/exceptions.py,sha256=bNsx4iYaroAAGsYxErfELC2B5ZJ3w5lVa1kKdIx5s9g,2173
|
|
4
|
-
lumera/google.py,sha256=3IVNL1HaOtsTmunl0alnGFuUAkzQQRyCEA3CKjlPqO0,10183
|
|
5
|
-
lumera/llm.py,sha256=pUTZK7t3GTK0vfxMI1PJgJwNendyuiJc5MB1pUj2vxE,14412
|
|
6
|
-
lumera/locks.py,sha256=8l_qxb8nrxge7YJ-ApUTJ5MeYpIdxDeEa94Eim9O-YM,6806
|
|
7
|
-
lumera/pb.py,sha256=oJf37awGIoJ25VA1eSA3lMUXorOtNynVC7yG30X4-lU,18236
|
|
8
|
-
lumera/sdk.py,sha256=XnzVtAGonhhQ2pDMtNKEOG5ZVH1sPLK-gsyTfCfBLSA,30233
|
|
9
|
-
lumera/storage.py,sha256=kQQJYVRnxcpDZQ_gB2iZy6anb3hyZN7b8a_oNuE2yYE,8191
|
|
10
|
-
lumera-0.6.0.dist-info/METADATA,sha256=4rhyTGFp6Iw1Zv-ZOtDcyThUvMSljTTtyLp-EKnEyvE,1611
|
|
11
|
-
lumera-0.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
-
lumera-0.6.0.dist-info/top_level.txt,sha256=HgfK4XQkpMTnM2E5iWM4kB711FnYqUY9dglzib3pWlE,7
|
|
13
|
-
lumera-0.6.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|