lumera 0.7.0__tar.gz → 0.7.3__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.
- {lumera-0.7.0 → lumera-0.7.3}/PKG-INFO +1 -1
- {lumera-0.7.0 → lumera-0.7.3}/lumera/__init__.py +6 -4
- {lumera-0.7.0 → lumera-0.7.3}/lumera/_utils.py +13 -13
- {lumera-0.7.0 → lumera-0.7.3}/lumera/automations.py +25 -39
- {lumera-0.7.0 → lumera-0.7.3}/lumera/sdk.py +39 -39
- {lumera-0.7.0 → lumera-0.7.3}/lumera/storage.py +3 -3
- {lumera-0.7.0 → lumera-0.7.3}/lumera.egg-info/PKG-INFO +1 -1
- {lumera-0.7.0 → lumera-0.7.3}/pyproject.toml +1 -1
- {lumera-0.7.0 → lumera-0.7.3}/tests/test_sdk.py +27 -27
- {lumera-0.7.0 → lumera-0.7.3}/lumera/exceptions.py +0 -0
- {lumera-0.7.0 → lumera-0.7.3}/lumera/google.py +0 -0
- {lumera-0.7.0 → lumera-0.7.3}/lumera/llm.py +0 -0
- {lumera-0.7.0 → lumera-0.7.3}/lumera/locks.py +0 -0
- {lumera-0.7.0 → lumera-0.7.3}/lumera/pb.py +0 -0
- {lumera-0.7.0 → lumera-0.7.3}/lumera.egg-info/SOURCES.txt +0 -0
- {lumera-0.7.0 → lumera-0.7.3}/lumera.egg-info/dependency_links.txt +0 -0
- {lumera-0.7.0 → lumera-0.7.3}/lumera.egg-info/requires.txt +0 -0
- {lumera-0.7.0 → lumera-0.7.3}/lumera.egg-info/top_level.txt +0 -0
- {lumera-0.7.0 → lumera-0.7.3}/setup.cfg +0 -0
|
@@ -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
|
|
@@ -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",
|
|
@@ -65,8 +65,8 @@ __all__ = [
|
|
|
65
65
|
]
|
|
66
66
|
|
|
67
67
|
from ._utils import LumeraAPIError, _api_request
|
|
68
|
-
from .sdk import
|
|
69
|
-
from .sdk import
|
|
68
|
+
from .sdk import get_automation_run as _get_automation_run
|
|
69
|
+
from .sdk import run_automation as _run_automation
|
|
70
70
|
|
|
71
71
|
# ============================================================================
|
|
72
72
|
# Run Class
|
|
@@ -101,7 +101,7 @@ class Run:
|
|
|
101
101
|
|
|
102
102
|
@property
|
|
103
103
|
def automation_id(self) -> str:
|
|
104
|
-
return self._data.get("
|
|
104
|
+
return self._data.get("automation_id", "")
|
|
105
105
|
|
|
106
106
|
@property
|
|
107
107
|
def status(self) -> str:
|
|
@@ -166,7 +166,7 @@ class Run:
|
|
|
166
166
|
"""
|
|
167
167
|
if not self.id:
|
|
168
168
|
raise ValueError("Cannot refresh run without id")
|
|
169
|
-
updated =
|
|
169
|
+
updated = _get_automation_run(run_id=self.id)
|
|
170
170
|
self._data = updated
|
|
171
171
|
return self
|
|
172
172
|
|
|
@@ -213,7 +213,7 @@ class Run:
|
|
|
213
213
|
"""
|
|
214
214
|
if not self.id:
|
|
215
215
|
raise ValueError("Cannot cancel run without id")
|
|
216
|
-
result = _api_request("POST", f"
|
|
216
|
+
result = _api_request("POST", f"automation-runs/{self.id}/cancel")
|
|
217
217
|
if isinstance(result, dict):
|
|
218
218
|
self._data = result
|
|
219
219
|
return self
|
|
@@ -271,13 +271,8 @@ class Automation:
|
|
|
271
271
|
|
|
272
272
|
@property
|
|
273
273
|
def input_schema(self) -> dict[str, Any] | None:
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
try:
|
|
277
|
-
return json.loads(raw)
|
|
278
|
-
except json.JSONDecodeError:
|
|
279
|
-
return None
|
|
280
|
-
return raw
|
|
274
|
+
"""Return the input schema as a dict. API always returns JSON object."""
|
|
275
|
+
return self._data.get("input_schema")
|
|
281
276
|
|
|
282
277
|
@property
|
|
283
278
|
def status(self) -> str | None:
|
|
@@ -349,7 +344,7 @@ def run(
|
|
|
349
344
|
>>> print(run.id, run.status)
|
|
350
345
|
>>> result = run.wait(timeout=300)
|
|
351
346
|
"""
|
|
352
|
-
result =
|
|
347
|
+
result = _run_automation(
|
|
353
348
|
automation_id,
|
|
354
349
|
inputs=inputs,
|
|
355
350
|
files=files,
|
|
@@ -408,7 +403,7 @@ def get_run(run_id: str) -> Run:
|
|
|
408
403
|
>>> run = automations.get_run("run_abc123")
|
|
409
404
|
>>> print(run.status, run.result)
|
|
410
405
|
"""
|
|
411
|
-
result =
|
|
406
|
+
result = _get_automation_run(run_id=run_id)
|
|
412
407
|
return Run(result)
|
|
413
408
|
|
|
414
409
|
|
|
@@ -446,15 +441,15 @@ def list_runs(
|
|
|
446
441
|
"dir": dir,
|
|
447
442
|
}
|
|
448
443
|
if automation_id:
|
|
449
|
-
params["
|
|
444
|
+
params["automation_id"] = automation_id
|
|
450
445
|
if status:
|
|
451
446
|
params["status"] = status
|
|
452
447
|
|
|
453
|
-
result = _api_request("GET", "
|
|
448
|
+
result = _api_request("GET", "automation-runs", params=params)
|
|
454
449
|
items = []
|
|
455
450
|
if isinstance(result, dict):
|
|
456
451
|
# API returns runs in different keys depending on context
|
|
457
|
-
items = result.get("
|
|
452
|
+
items = result.get("automation_runs") or result.get("data") or []
|
|
458
453
|
return [Run(item) for item in items if isinstance(item, dict)]
|
|
459
454
|
|
|
460
455
|
|
|
@@ -497,10 +492,10 @@ def list(
|
|
|
497
492
|
if q:
|
|
498
493
|
params["q"] = q
|
|
499
494
|
|
|
500
|
-
result = _api_request("GET", "
|
|
495
|
+
result = _api_request("GET", "automations", params=params)
|
|
501
496
|
items = []
|
|
502
497
|
if isinstance(result, dict):
|
|
503
|
-
items = result.get("
|
|
498
|
+
items = result.get("automations") or []
|
|
504
499
|
return [Automation(item) for item in items if isinstance(item, dict)]
|
|
505
500
|
|
|
506
501
|
|
|
@@ -521,7 +516,7 @@ def get(automation_id: str) -> Automation:
|
|
|
521
516
|
if not automation_id:
|
|
522
517
|
raise ValueError("automation_id is required")
|
|
523
518
|
|
|
524
|
-
result = _api_request("GET", f"
|
|
519
|
+
result = _api_request("GET", f"automations/{automation_id}")
|
|
525
520
|
if not isinstance(result, dict):
|
|
526
521
|
raise RuntimeError("Unexpected response")
|
|
527
522
|
return Automation(result)
|
|
@@ -547,14 +542,17 @@ def get_by_external_id(external_id: str) -> Automation:
|
|
|
547
542
|
if not external_id:
|
|
548
543
|
raise ValueError("external_id is required")
|
|
549
544
|
|
|
550
|
-
result = _api_request("GET", "
|
|
545
|
+
result = _api_request("GET", "automations", params={"external_id": external_id, "limit": 1})
|
|
551
546
|
if isinstance(result, dict):
|
|
552
|
-
items = result.get("
|
|
547
|
+
items = result.get("automations") or []
|
|
553
548
|
if items and isinstance(items[0], dict):
|
|
554
549
|
return Automation(items[0])
|
|
555
550
|
|
|
556
551
|
raise LumeraAPIError(
|
|
557
|
-
404,
|
|
552
|
+
404,
|
|
553
|
+
f"Automation with external_id '{external_id}' not found",
|
|
554
|
+
url="automations",
|
|
555
|
+
payload=None,
|
|
558
556
|
)
|
|
559
557
|
|
|
560
558
|
|
|
@@ -565,7 +563,6 @@ def create(
|
|
|
565
563
|
input_schema: Mapping[str, Any] | None = None,
|
|
566
564
|
description: str | None = None,
|
|
567
565
|
external_id: str | None = None,
|
|
568
|
-
expose_to_ai: bool = False,
|
|
569
566
|
schedule: str | None = None,
|
|
570
567
|
schedule_tz: str | None = None,
|
|
571
568
|
) -> Automation:
|
|
@@ -578,7 +575,6 @@ def create(
|
|
|
578
575
|
``function.name`` and ``function.parameters``.
|
|
579
576
|
description: Human-readable description.
|
|
580
577
|
external_id: Stable identifier for programmatic access.
|
|
581
|
-
expose_to_ai: If True, makes this callable as an AI tool.
|
|
582
578
|
schedule: Cron expression for scheduled runs.
|
|
583
579
|
schedule_tz: Timezone for schedule (e.g., "America/New_York").
|
|
584
580
|
|
|
@@ -612,14 +608,12 @@ def create(
|
|
|
612
608
|
payload["description"] = description
|
|
613
609
|
if external_id is not None:
|
|
614
610
|
payload["external_id"] = external_id
|
|
615
|
-
if expose_to_ai:
|
|
616
|
-
payload["expose_to_ai"] = True
|
|
617
611
|
if schedule is not None:
|
|
618
612
|
payload["schedule"] = schedule
|
|
619
613
|
if schedule_tz is not None:
|
|
620
614
|
payload["schedule_tz"] = schedule_tz
|
|
621
615
|
|
|
622
|
-
result = _api_request("POST", "
|
|
616
|
+
result = _api_request("POST", "automations", json_body=payload)
|
|
623
617
|
if not isinstance(result, dict):
|
|
624
618
|
raise RuntimeError("Unexpected response")
|
|
625
619
|
return Automation(result)
|
|
@@ -633,7 +627,6 @@ def update(
|
|
|
633
627
|
input_schema: Mapping[str, Any] | None = None,
|
|
634
628
|
description: str | None = None,
|
|
635
629
|
external_id: str | None = None,
|
|
636
|
-
expose_to_ai: bool | None = None,
|
|
637
630
|
schedule: str | None = None,
|
|
638
631
|
schedule_tz: str | None = None,
|
|
639
632
|
) -> Automation:
|
|
@@ -646,7 +639,6 @@ def update(
|
|
|
646
639
|
input_schema: New input schema.
|
|
647
640
|
description: New description.
|
|
648
641
|
external_id: New external_id.
|
|
649
|
-
expose_to_ai: Whether to expose as AI tool.
|
|
650
642
|
schedule: New cron schedule (empty string to clear).
|
|
651
643
|
schedule_tz: New schedule timezone.
|
|
652
644
|
|
|
@@ -671,8 +663,6 @@ def update(
|
|
|
671
663
|
payload["description"] = description
|
|
672
664
|
if external_id is not None:
|
|
673
665
|
payload["external_id"] = external_id
|
|
674
|
-
if expose_to_ai is not None:
|
|
675
|
-
payload["expose_to_ai"] = expose_to_ai
|
|
676
666
|
if schedule is not None:
|
|
677
667
|
payload["schedule"] = schedule
|
|
678
668
|
if schedule_tz is not None:
|
|
@@ -681,7 +671,7 @@ def update(
|
|
|
681
671
|
if not payload:
|
|
682
672
|
raise ValueError("At least one field to update is required")
|
|
683
673
|
|
|
684
|
-
result = _api_request("PATCH", f"
|
|
674
|
+
result = _api_request("PATCH", f"automations/{automation_id}", json_body=payload)
|
|
685
675
|
if not isinstance(result, dict):
|
|
686
676
|
raise RuntimeError("Unexpected response")
|
|
687
677
|
return Automation(result)
|
|
@@ -694,7 +684,6 @@ def upsert(
|
|
|
694
684
|
code: str,
|
|
695
685
|
input_schema: Mapping[str, Any] | None = None,
|
|
696
686
|
description: str | None = None,
|
|
697
|
-
expose_to_ai: bool = False,
|
|
698
687
|
schedule: str | None = None,
|
|
699
688
|
schedule_tz: str | None = None,
|
|
700
689
|
) -> Automation:
|
|
@@ -709,7 +698,6 @@ def upsert(
|
|
|
709
698
|
code: Python code.
|
|
710
699
|
input_schema: JSON schema defining inputs.
|
|
711
700
|
description: Human-readable description.
|
|
712
|
-
expose_to_ai: If True, makes this callable as an AI tool.
|
|
713
701
|
schedule: Cron expression for scheduled runs.
|
|
714
702
|
schedule_tz: Timezone for schedule.
|
|
715
703
|
|
|
@@ -739,7 +727,6 @@ def upsert(
|
|
|
739
727
|
code=code,
|
|
740
728
|
input_schema=input_schema,
|
|
741
729
|
description=description,
|
|
742
|
-
expose_to_ai=expose_to_ai,
|
|
743
730
|
schedule=schedule,
|
|
744
731
|
schedule_tz=schedule_tz,
|
|
745
732
|
)
|
|
@@ -753,7 +740,6 @@ def upsert(
|
|
|
753
740
|
input_schema=input_schema,
|
|
754
741
|
description=description,
|
|
755
742
|
external_id=external_id,
|
|
756
|
-
expose_to_ai=expose_to_ai,
|
|
757
743
|
schedule=schedule,
|
|
758
744
|
schedule_tz=schedule_tz,
|
|
759
745
|
)
|
|
@@ -772,7 +758,7 @@ def delete(automation_id: str) -> None:
|
|
|
772
758
|
if not automation_id:
|
|
773
759
|
raise ValueError("automation_id is required")
|
|
774
760
|
|
|
775
|
-
_api_request("DELETE", f"
|
|
761
|
+
_api_request("DELETE", f"automations/{automation_id}")
|
|
776
762
|
|
|
777
763
|
|
|
778
764
|
# ============================================================================
|
|
@@ -811,7 +797,7 @@ def stream_logs(run_id: str, *, timeout: float = 30) -> Iterator[str]:
|
|
|
811
797
|
if not token:
|
|
812
798
|
raise ValueError("LUMERA_TOKEN environment variable is required")
|
|
813
799
|
|
|
814
|
-
url = f"{base_url}/
|
|
800
|
+
url = f"{base_url}/automation-runs/{run_id}/logs/live"
|
|
815
801
|
headers = {
|
|
816
802
|
"Authorization": f"token {token}",
|
|
817
803
|
"Accept": "text/event-stream",
|
|
@@ -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:
|
|
@@ -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()
|
|
@@ -14,14 +14,14 @@ from lumera.sdk import (
|
|
|
14
14
|
claim_locks,
|
|
15
15
|
create_collection,
|
|
16
16
|
create_record,
|
|
17
|
-
|
|
17
|
+
get_automation_run,
|
|
18
18
|
get_collection,
|
|
19
19
|
get_record_by_external_id,
|
|
20
20
|
list_collections,
|
|
21
21
|
query_sql,
|
|
22
22
|
replay_hook,
|
|
23
23
|
resolve_path,
|
|
24
|
-
|
|
24
|
+
run_automation,
|
|
25
25
|
to_filerefs,
|
|
26
26
|
upload_lumera_file,
|
|
27
27
|
upsert_record,
|
|
@@ -462,14 +462,14 @@ def test_upsert_record_json(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
462
462
|
assert body["name"] == "value"
|
|
463
463
|
|
|
464
464
|
|
|
465
|
-
def
|
|
465
|
+
def test_run_automation_without_files(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
466
466
|
monkeypatch.setenv(sdk.TOKEN_ENV, "tok")
|
|
467
467
|
|
|
468
468
|
calls: list[tuple[str, str, dict[str, object]]] = []
|
|
469
469
|
|
|
470
470
|
def fake_api(method: str, path: str, **kwargs: object) -> dict[str, object]:
|
|
471
471
|
calls.append((method, path, kwargs))
|
|
472
|
-
if path == "
|
|
472
|
+
if path == "automation-runs" and method == "POST":
|
|
473
473
|
body = kwargs.get("json_body", {})
|
|
474
474
|
assert isinstance(body, dict)
|
|
475
475
|
return {"id": "pb1234567890abc", "status": body.get("status")}
|
|
@@ -477,16 +477,16 @@ def test_run_agent_without_files(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
477
477
|
|
|
478
478
|
monkeypatch.setattr(sdk, "_api_request", fake_api)
|
|
479
479
|
|
|
480
|
-
run =
|
|
480
|
+
run = run_automation("agent123", inputs={"foo": "bar"})
|
|
481
481
|
assert run["id"] == "pb1234567890abc"
|
|
482
482
|
|
|
483
483
|
assert len(calls) == 1
|
|
484
484
|
method, path, kwargs = calls[0]
|
|
485
485
|
assert method == "POST"
|
|
486
|
-
assert path == "
|
|
486
|
+
assert path == "automation-runs"
|
|
487
487
|
payload = kwargs.get("json_body")
|
|
488
488
|
assert isinstance(payload, dict)
|
|
489
|
-
assert payload["
|
|
489
|
+
assert payload["automation_id"] == "agent123"
|
|
490
490
|
assert "id" not in payload
|
|
491
491
|
assert payload["status"] == "queued"
|
|
492
492
|
assert json.loads(payload["inputs"]) == {"foo": "bar"}
|
|
@@ -498,7 +498,7 @@ def test_run_agent_without_files(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
498
498
|
assert "agent_run" not in provenance
|
|
499
499
|
|
|
500
500
|
|
|
501
|
-
def
|
|
501
|
+
def test_run_automation_with_files(monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> None:
|
|
502
502
|
monkeypatch.setenv(sdk.TOKEN_ENV, "tok")
|
|
503
503
|
|
|
504
504
|
file_a = tmp_path / "a.txt"
|
|
@@ -521,7 +521,7 @@ def test_run_agent_with_files(monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib
|
|
|
521
521
|
"run_path": f"{ROOT}/agent_runs/{resource_id}/{filename}",
|
|
522
522
|
"run_id": resource_id,
|
|
523
523
|
}
|
|
524
|
-
if path == "
|
|
524
|
+
if path == "automation-runs" and method == "POST":
|
|
525
525
|
body = kwargs.get("json_body", {})
|
|
526
526
|
return {
|
|
527
527
|
"id": body.get("id", "run42"),
|
|
@@ -548,7 +548,7 @@ def test_run_agent_with_files(monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib
|
|
|
548
548
|
monkeypatch.setattr(sdk, "_api_request", fake_api)
|
|
549
549
|
monkeypatch.setattr(sdk.requests, "put", fake_put)
|
|
550
550
|
|
|
551
|
-
run =
|
|
551
|
+
run = run_automation(
|
|
552
552
|
"agent-xyz",
|
|
553
553
|
inputs={"foo": "bar"},
|
|
554
554
|
files={"report": file_a, "images": [file_a, file_b]},
|
|
@@ -582,7 +582,7 @@ def test_run_agent_with_files(monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib
|
|
|
582
582
|
assert provenance["agent_run"] == {"id": "run42"}
|
|
583
583
|
|
|
584
584
|
|
|
585
|
-
def
|
|
585
|
+
def test_run_automation_with_external_id_and_metadata(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
586
586
|
monkeypatch.setenv(sdk.TOKEN_ENV, "tok")
|
|
587
587
|
|
|
588
588
|
captured: dict[str, object] = {}
|
|
@@ -595,7 +595,7 @@ def test_run_agent_with_external_id_and_metadata(monkeypatch: pytest.MonkeyPatch
|
|
|
595
595
|
|
|
596
596
|
monkeypatch.setattr(sdk, "_api_request", fake_api)
|
|
597
597
|
|
|
598
|
-
run =
|
|
598
|
+
run = run_automation(
|
|
599
599
|
"agent-abc",
|
|
600
600
|
inputs={"foo": "bar"},
|
|
601
601
|
external_id=" ext-123 ",
|
|
@@ -605,12 +605,12 @@ def test_run_agent_with_external_id_and_metadata(monkeypatch: pytest.MonkeyPatch
|
|
|
605
605
|
|
|
606
606
|
body = captured["kwargs"].get("json_body")
|
|
607
607
|
assert isinstance(body, dict)
|
|
608
|
-
assert body["
|
|
608
|
+
assert body["automation_id"] == "agent-abc"
|
|
609
609
|
assert body["external_id"] == "ext-123"
|
|
610
610
|
assert body["metadata"] == {"callback_url": "https://example.com/hook", "source": "tests"}
|
|
611
611
|
|
|
612
612
|
|
|
613
|
-
def
|
|
613
|
+
def test_run_automation_default_provenance_uses_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
614
614
|
monkeypatch.setenv(sdk.TOKEN_ENV, "tok")
|
|
615
615
|
monkeypatch.setenv("LUMERA_RUN_ID", "env-run")
|
|
616
616
|
monkeypatch.setenv("COMPANY_ID", "co-1")
|
|
@@ -619,14 +619,14 @@ def test_run_agent_default_provenance_uses_env(monkeypatch: pytest.MonkeyPatch)
|
|
|
619
619
|
captured: dict[str, object] = {}
|
|
620
620
|
|
|
621
621
|
def fake_api(method: str, path: str, **kwargs: object) -> dict[str, object]:
|
|
622
|
-
if path == "
|
|
622
|
+
if path == "automation-runs" and method == "POST":
|
|
623
623
|
captured.update(kwargs)
|
|
624
624
|
return {"id": "env-run", "status": "queued"}
|
|
625
625
|
raise AssertionError("unexpected call")
|
|
626
626
|
|
|
627
627
|
monkeypatch.setattr(sdk, "_api_request", fake_api)
|
|
628
628
|
|
|
629
|
-
run =
|
|
629
|
+
run = run_automation("agent123")
|
|
630
630
|
assert run["id"] == "env-run"
|
|
631
631
|
|
|
632
632
|
payload = captured.get("json_body")
|
|
@@ -637,13 +637,13 @@ def test_run_agent_default_provenance_uses_env(monkeypatch: pytest.MonkeyPatch)
|
|
|
637
637
|
assert prov["company"] == {"id": "co-1", "api_name": "acme"}
|
|
638
638
|
|
|
639
639
|
|
|
640
|
-
def
|
|
640
|
+
def test_run_automation_custom_provenance(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
641
641
|
monkeypatch.setenv(sdk.TOKEN_ENV, "tok")
|
|
642
642
|
|
|
643
643
|
captured: dict[str, object] = {}
|
|
644
644
|
|
|
645
645
|
def fake_api(method: str, path: str, **kwargs: object) -> dict[str, object]:
|
|
646
|
-
if path == "
|
|
646
|
+
if path == "automation-runs" and method == "POST":
|
|
647
647
|
captured.update(kwargs)
|
|
648
648
|
return {"id": "custom", "status": kwargs.get("json_body", {}).get("status")}
|
|
649
649
|
raise AssertionError("unexpected call")
|
|
@@ -656,7 +656,7 @@ def test_run_agent_custom_provenance(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
656
656
|
"scheduler": {"agent_id": "agent123", "scheduled_at": "2024-05-01T12:00:00Z"},
|
|
657
657
|
}
|
|
658
658
|
|
|
659
|
-
run =
|
|
659
|
+
run = run_automation("agent123", provenance=custom_prov)
|
|
660
660
|
assert run["id"] == "custom"
|
|
661
661
|
|
|
662
662
|
payload = captured.get("json_body")
|
|
@@ -664,7 +664,7 @@ def test_run_agent_custom_provenance(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
664
664
|
assert payload["lm_provenance"] == custom_prov
|
|
665
665
|
|
|
666
666
|
|
|
667
|
-
def
|
|
667
|
+
def test_get_automation_run_by_id(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
668
668
|
captured: dict[str, object] = {}
|
|
669
669
|
|
|
670
670
|
def fake_api(method: str, path: str, **kwargs: object) -> Mapping[str, Any]:
|
|
@@ -675,30 +675,30 @@ def test_get_agent_run_by_id(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
675
675
|
|
|
676
676
|
monkeypatch.setattr(sdk, "_api_request", fake_api)
|
|
677
677
|
|
|
678
|
-
run =
|
|
678
|
+
run = get_automation_run(run_id="run-1")
|
|
679
679
|
assert run["id"] == "run-1"
|
|
680
680
|
assert captured["method"] == "GET"
|
|
681
|
-
assert captured["path"] == "
|
|
681
|
+
assert captured["path"] == "automation-runs/run-1"
|
|
682
682
|
|
|
683
683
|
|
|
684
|
-
def
|
|
684
|
+
def test_get_automation_run_by_external_id(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
685
685
|
captured: dict[str, object] = {}
|
|
686
686
|
|
|
687
687
|
def fake_api(method: str, path: str, **kwargs: object) -> Mapping[str, Any]:
|
|
688
688
|
captured["method"] = method
|
|
689
689
|
captured["path"] = path
|
|
690
690
|
captured["kwargs"] = kwargs
|
|
691
|
-
return {"
|
|
691
|
+
return {"data": [{"id": "run-2", "external_id": "ext-xyz"}]}
|
|
692
692
|
|
|
693
693
|
monkeypatch.setattr(sdk, "_api_request", fake_api)
|
|
694
694
|
|
|
695
|
-
run =
|
|
695
|
+
run = get_automation_run(automation_id="agent-xyz", external_id=" ext-xyz ")
|
|
696
696
|
assert run["id"] == "run-2"
|
|
697
697
|
|
|
698
698
|
assert captured["method"] == "GET"
|
|
699
|
-
assert captured["path"] == "
|
|
699
|
+
assert captured["path"] == "automation-runs"
|
|
700
700
|
params = captured["kwargs"].get("params")
|
|
701
|
-
assert params == {"
|
|
701
|
+
assert params == {"automation_id": "agent-xyz", "external_id": "ext-xyz", "limit": 1}
|
|
702
702
|
|
|
703
703
|
|
|
704
704
|
def test_claim_locks_default_provenance_uses_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
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
|