lumera 0.7.0__py3-none-any.whl → 0.8.0__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 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.7.0"
8
+ __version__ = "0.8.0"
9
9
 
10
10
  # Import new modules (as modules, not individual functions)
11
- from . import automations, exceptions, llm, locks, pb, storage
11
+ from . import automations, exceptions, llm, locks, pb, storage, webhooks
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
- get_agent_run,
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
- run_agent,
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
- "run_agent",
77
- "get_agent_run",
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
@@ -95,4 +97,5 @@ __all__ = [
95
97
  "llm",
96
98
  "locks",
97
99
  "exceptions",
100
+ "webhooks",
98
101
  ]
lumera/_utils.py CHANGED
@@ -117,10 +117,10 @@ def _utcnow_iso() -> str:
117
117
  )
118
118
 
119
119
 
120
- def _default_provenance(agent_id: str, run_id: str | None) -> dict[str, Any]:
120
+ def _default_provenance(automation_id: str, run_id: str | None) -> dict[str, Any]:
121
121
  recorded_at = _utcnow_iso()
122
- env_agent_id = os.getenv("LUMERA_AGENT_ID", "").strip()
123
- agent_id = (agent_id or "").strip() or env_agent_id
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 agent_id:
136
- payload["agent"] = {"id": 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 _prepare_agent_inputs(
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 _upload_agent_files(
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": "agent_run",
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 _upload_agent_run_file(file_path: str, run_id: str) -> dict:
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}/agent-runs/{run_id}/files/upload-url",
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
- "_prepare_agent_inputs",
745
+ "_prepare_automation_inputs",
746
746
  "_record_mutation",
747
- "_upload_agent_files",
748
- "_upload_agent_run_file",
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 CHANGED
@@ -65,8 +65,8 @@ __all__ = [
65
65
  ]
66
66
 
67
67
  from ._utils import LumeraAPIError, _api_request
68
- from .sdk import get_agent_run as _get_agent_run
69
- from .sdk import run_agent as _run_agent
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("agent_id", "")
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 = _get_agent_run(run_id=self.id)
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"agent-runs/{self.id}/cancel")
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
- raw = self._data.get("input_schema")
275
- if isinstance(raw, str):
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 = _run_agent(
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 = _get_agent_run(run_id=run_id)
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["agent_id"] = automation_id
444
+ params["automation_id"] = automation_id
450
445
  if status:
451
446
  params["status"] = status
452
447
 
453
- result = _api_request("GET", "agent-runs", params=params)
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("agent_runs") or result.get("data") or []
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", "agents", params=params)
495
+ result = _api_request("GET", "automations", params=params)
501
496
  items = []
502
497
  if isinstance(result, dict):
503
- items = result.get("agents") or []
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"agents/{automation_id}")
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", "agents", params={"external_id": external_id, "limit": 1})
545
+ result = _api_request("GET", "automations", params={"external_id": external_id, "limit": 1})
551
546
  if isinstance(result, dict):
552
- items = result.get("agents") or []
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, f"Automation with external_id '{external_id}' not found", url="agents", payload=None
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", "agents", json_body=payload)
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"agents/{automation_id}", json_body=payload)
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"agents/{automation_id}")
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}/agent-runs/{run_id}/logs/live"
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",
lumera/sdk.py CHANGED
@@ -51,10 +51,10 @@ from ._utils import (
51
51
  _default_provenance,
52
52
  _ensure_mapping,
53
53
  _is_sequence,
54
- _prepare_agent_inputs,
54
+ _prepare_automation_inputs,
55
55
  _record_mutation,
56
- _upload_agent_files,
57
- _upload_agent_run_file,
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 run_agent(
392
- agent_id: str,
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
- agent_id: The automation/agent to run. Required.
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
- agent_id = agent_id.strip()
417
- if not agent_id:
418
- raise ValueError("agent_id is required")
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 = _prepare_agent_inputs(inputs) or {}
422
+ prepared_inputs = _prepare_automation_inputs(inputs) or {}
423
423
 
424
424
  file_map = files or {}
425
- run_id, upload_descriptors = _upload_agent_files(run_id, file_map, api_request=_api_request)
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
- "agent_id": agent_id,
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(agent_id, run_id)
452
+ ) or _default_provenance(automation_id, run_id)
451
453
 
452
- run = _api_request("POST", "agent-runs", json_body=payload)
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 get_agent_run(
459
- agent_id: str | None = None,
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 agent_id + external_id idempotency key.
466
+ """Fetch an agent run by id or by automation_id + external_id idempotency key.
465
467
 
466
468
  Args:
467
- agent_id: Agent id for external_id lookup. Required when ``run_id`` is not provided.
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"agent-runs/{run_id}")
479
+ return _api_request("GET", f"automation-runs/{run_id}")
478
480
 
479
- agent_id = agent_id.strip() if isinstance(agent_id, str) else ""
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 agent_id:
482
- raise ValueError("agent_id is required when run_id is not provided")
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
- "agent-runs",
489
- params={"agent_id": agent_id, "external_id": external_id, "limit": 1},
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("agent_runs") if isinstance(resp, dict) else None
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("agent-runs")
496
- raise _LumeraAPIError(404, "agent run not found", url=url, payload=None)
501
+ url = _api_url("automation-runs")
502
+ raise _LumeraAPIError(404, "automation run not found", url=url, payload=None)
497
503
 
498
504
 
499
- def update_agent_run(
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"agent-runs/{run_id}", json_body=payload)
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 _upload_agent_run_file(file_path, run_id)
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}/agent-runs/{run_id}/files/upload-url",
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}/agent-runs/{run_id}/files/download-url",
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}/agent-runs/{run_id}/files", headers=headers, timeout=30)
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()
lumera/webhooks.py ADDED
@@ -0,0 +1,295 @@
1
+ """
2
+ Webhook endpoint management for Lumera.
3
+
4
+ This module provides functions for managing webhook endpoints that receive
5
+ external events from third-party services (Stripe, GitHub, etc.).
6
+
7
+ Functions:
8
+ create() - Create a new webhook endpoint
9
+ list() - List all webhook endpoints
10
+ get() - Get endpoint by external_id
11
+ update() - Update an existing endpoint
12
+ delete() - Delete an endpoint
13
+ url() - Get the public webhook URL for an endpoint
14
+
15
+ Example:
16
+ >>> from lumera import webhooks
17
+ >>>
18
+ >>> # Create a webhook endpoint
19
+ >>> endpoint = webhooks.create(
20
+ ... name="Stripe Events",
21
+ ... external_id="stripe-events",
22
+ ... description="Receives Stripe payment webhooks"
23
+ ... )
24
+ >>>
25
+ >>> # Get the public URL to configure in Stripe
26
+ >>> webhook_url = webhooks.url("stripe-events")
27
+ >>> print(webhook_url)
28
+ https://app.lumerahq.com/webhooks/acme/stripe-events
29
+ >>>
30
+ >>> # List all endpoints
31
+ >>> for ep in webhooks.list():
32
+ ... print(ep["name"], ep["external_id"])
33
+
34
+ External ID Format:
35
+ The external_id is used as the URL slug and must follow these rules:
36
+ - 3-50 characters
37
+ - Lowercase alphanumeric and hyphens only
38
+ - Must start with a letter
39
+ - Cannot end with a hyphen
40
+ - No consecutive hyphens
41
+
42
+ Valid examples: "stripe-events", "github-webhooks", "acme-orders"
43
+ Invalid examples: "1-start", "end-", "double--hyphen"
44
+ """
45
+
46
+ import os
47
+ from typing import Any
48
+
49
+ __all__ = [
50
+ "create",
51
+ "list",
52
+ "get",
53
+ "update",
54
+ "delete",
55
+ "url",
56
+ ]
57
+
58
+ from ._utils import API_BASE, LumeraAPIError, _api_request
59
+
60
+ # Collection name for webhook endpoints
61
+ _COLLECTION = "lm_webhook_endpoints"
62
+
63
+
64
+ def create(
65
+ name: str,
66
+ external_id: str,
67
+ *,
68
+ description: str | None = None,
69
+ ) -> dict[str, Any]:
70
+ """Create a new webhook endpoint.
71
+
72
+ Args:
73
+ name: Human-readable name for the endpoint (e.g., "Stripe Events")
74
+ external_id: URL-safe identifier used in the webhook URL.
75
+ Must be 3-50 chars, lowercase alphanumeric with hyphens,
76
+ start with a letter. Example: "stripe-events"
77
+ description: Optional description of what this webhook receives
78
+
79
+ Returns:
80
+ Created endpoint record with id, external_id, name, etc.
81
+
82
+ Raises:
83
+ ValueError: If name or external_id is empty
84
+ LumeraAPIError: If external_id format is invalid or already exists
85
+
86
+ Example:
87
+ >>> endpoint = webhooks.create(
88
+ ... name="Stripe Events",
89
+ ... external_id="stripe-events",
90
+ ... description="Payment and subscription events from Stripe"
91
+ ... )
92
+ >>> print(endpoint["id"])
93
+ """
94
+ name = (name or "").strip()
95
+ external_id = (external_id or "").strip()
96
+
97
+ if not name:
98
+ raise ValueError("name is required")
99
+ if not external_id:
100
+ raise ValueError("external_id is required")
101
+
102
+ payload: dict[str, Any] = {
103
+ "name": name,
104
+ "external_id": external_id,
105
+ }
106
+ if description is not None:
107
+ payload["description"] = description.strip()
108
+
109
+ result = _api_request("POST", f"collections/{_COLLECTION}/records", json_body=payload)
110
+ if not isinstance(result, dict):
111
+ raise RuntimeError("unexpected response payload")
112
+ return result
113
+
114
+
115
+ def list(
116
+ *,
117
+ per_page: int = 100,
118
+ page: int = 1,
119
+ ) -> list[dict[str, Any]]:
120
+ """List all webhook endpoints.
121
+
122
+ Args:
123
+ per_page: Number of results per page (default 100, max 500)
124
+ page: Page number, 1-indexed (default 1)
125
+
126
+ Returns:
127
+ List of endpoint records
128
+
129
+ Example:
130
+ >>> endpoints = webhooks.list()
131
+ >>> for ep in endpoints:
132
+ ... print(f"{ep['name']}: {ep['external_id']}")
133
+ """
134
+ params: dict[str, Any] = {
135
+ "perPage": per_page,
136
+ "page": page,
137
+ "sort": "-created",
138
+ }
139
+
140
+ result = _api_request("GET", f"collections/{_COLLECTION}/records", params=params)
141
+ if not isinstance(result, dict):
142
+ return []
143
+ return result.get("items", [])
144
+
145
+
146
+ def get(external_id: str) -> dict[str, Any]:
147
+ """Get a webhook endpoint by its external_id.
148
+
149
+ Args:
150
+ external_id: The external_id of the endpoint (e.g., "stripe-events")
151
+
152
+ Returns:
153
+ Endpoint record
154
+
155
+ Raises:
156
+ ValueError: If external_id is empty
157
+ LumeraAPIError: If endpoint not found (404)
158
+
159
+ Example:
160
+ >>> endpoint = webhooks.get("stripe-events")
161
+ >>> print(endpoint["name"])
162
+ Stripe Events
163
+ """
164
+ external_id = (external_id or "").strip()
165
+ if not external_id:
166
+ raise ValueError("external_id is required")
167
+
168
+ import json
169
+
170
+ params = {"filter": json.dumps({"external_id": external_id}), "perPage": 1}
171
+ result = _api_request("GET", f"collections/{_COLLECTION}/records", params=params)
172
+
173
+ if not isinstance(result, dict):
174
+ raise LumeraAPIError(404, "endpoint not found", url=f"collections/{_COLLECTION}/records", payload=None)
175
+
176
+ items = result.get("items", [])
177
+ if not items:
178
+ raise LumeraAPIError(404, f"webhook endpoint '{external_id}' not found", url=f"collections/{_COLLECTION}/records", payload=None)
179
+
180
+ return items[0]
181
+
182
+
183
+ def update(
184
+ external_id: str,
185
+ *,
186
+ name: str | None = None,
187
+ description: str | None = None,
188
+ ) -> dict[str, Any]:
189
+ """Update a webhook endpoint.
190
+
191
+ Args:
192
+ external_id: The external_id of the endpoint to update
193
+ name: New name (optional)
194
+ description: New description (optional)
195
+
196
+ Returns:
197
+ Updated endpoint record
198
+
199
+ Raises:
200
+ ValueError: If external_id is empty or no fields to update
201
+ LumeraAPIError: If endpoint not found
202
+
203
+ Example:
204
+ >>> endpoint = webhooks.update(
205
+ ... "stripe-events",
206
+ ... description="Updated: Now includes refund events"
207
+ ... )
208
+ """
209
+ external_id = (external_id or "").strip()
210
+ if not external_id:
211
+ raise ValueError("external_id is required")
212
+
213
+ # First, find the endpoint to get its record ID
214
+ endpoint = get(external_id)
215
+ record_id = endpoint["id"]
216
+
217
+ payload: dict[str, Any] = {}
218
+ if name is not None:
219
+ payload["name"] = name.strip()
220
+ if description is not None:
221
+ payload["description"] = description.strip()
222
+
223
+ if not payload:
224
+ raise ValueError("at least one field (name or description) must be provided")
225
+
226
+ result = _api_request("PATCH", f"collections/{_COLLECTION}/records/{record_id}", json_body=payload)
227
+ if not isinstance(result, dict):
228
+ raise RuntimeError("unexpected response payload")
229
+ return result
230
+
231
+
232
+ def delete(external_id: str) -> None:
233
+ """Delete a webhook endpoint.
234
+
235
+ Args:
236
+ external_id: The external_id of the endpoint to delete
237
+
238
+ Raises:
239
+ ValueError: If external_id is empty
240
+ LumeraAPIError: If endpoint not found
241
+
242
+ Example:
243
+ >>> webhooks.delete("old-stripe-endpoint")
244
+ """
245
+ external_id = (external_id or "").strip()
246
+ if not external_id:
247
+ raise ValueError("external_id is required")
248
+
249
+ # First, find the endpoint to get its record ID
250
+ endpoint = get(external_id)
251
+ record_id = endpoint["id"]
252
+
253
+ _api_request("DELETE", f"collections/{_COLLECTION}/records/{record_id}")
254
+
255
+
256
+ def url(external_id: str) -> str:
257
+ """Get the public webhook URL for an endpoint.
258
+
259
+ This returns the full URL that external services should send webhooks to.
260
+ The URL format is: https://{base}/webhooks/{company_api}/{external_id}
261
+
262
+ Args:
263
+ external_id: The external_id of the endpoint
264
+
265
+ Returns:
266
+ Full public webhook URL
267
+
268
+ Raises:
269
+ ValueError: If external_id is empty
270
+ RuntimeError: If COMPANY_API_NAME environment variable is not set
271
+
272
+ Example:
273
+ >>> url = webhooks.url("stripe-events")
274
+ >>> print(url)
275
+ https://app.lumerahq.com/webhooks/acme/stripe-events
276
+
277
+ # Use this URL when configuring webhooks in Stripe, GitHub, etc.
278
+ """
279
+ external_id = (external_id or "").strip()
280
+ if not external_id:
281
+ raise ValueError("external_id is required")
282
+
283
+ company_api = os.getenv("COMPANY_API_NAME", "").strip()
284
+ if not company_api:
285
+ raise RuntimeError(
286
+ "COMPANY_API_NAME environment variable not set. "
287
+ "This is required to construct the webhook URL."
288
+ )
289
+
290
+ # API_BASE is like "https://app.lumerahq.com/api" - we need the base without /api
291
+ base_url = API_BASE.rstrip("/")
292
+ if base_url.endswith("/api"):
293
+ base_url = base_url[:-4]
294
+
295
+ return f"{base_url}/webhooks/{company_api}/{external_id}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lumera
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: SDK for building on Lumera platform
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: requests
@@ -0,0 +1,15 @@
1
+ lumera/__init__.py,sha256=waGigk2pMakKwoYDLMjKGai5kmL-2KA6hDKouWdq1Z8,2322
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/webhooks.py,sha256=wOAhqpC6mDgi7wK1LNJEx4sGMR0ZFdCHz2y_W6R9Ajg,8576
12
+ lumera-0.8.0.dist-info/METADATA,sha256=9vmayvlbC9SJ_w1cGEHBqXFo8PtlGimgmh5SkG69XaE,1611
13
+ lumera-0.8.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ lumera-0.8.0.dist-info/top_level.txt,sha256=HgfK4XQkpMTnM2E5iWM4kB711FnYqUY9dglzib3pWlE,7
15
+ lumera-0.8.0.dist-info/RECORD,,
@@ -1,14 +0,0 @@
1
- lumera/__init__.py,sha256=uZMw2gAa7bVn6bOykeCP_19BP0tQquHie3teypVyCkY,2220
2
- lumera/_utils.py,sha256=QyAaphxXGEK8XNPO0ghKLgTOYhAxcF_j3W0T8StzjxA,23610
3
- lumera/automations.py,sha256=6Y2USVktrFrueTNNxn8rnBi4-2_eeFpeVbI2gB90AP8,25244
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=XnzVtAGonhhQ2pDMtNKEOG5ZVH1sPLK-gsyTfCfBLSA,30233
10
- lumera/storage.py,sha256=kQQJYVRnxcpDZQ_gB2iZy6anb3hyZN7b8a_oNuE2yYE,8191
11
- lumera-0.7.0.dist-info/METADATA,sha256=BDmiGKUcRGZg585Gfc9Yq8cUP8myD0SA5EZXfdciC_4,1611
12
- lumera-0.7.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
13
- lumera-0.7.0.dist-info/top_level.txt,sha256=HgfK4XQkpMTnM2E5iWM4kB711FnYqUY9dglzib3pWlE,7
14
- lumera-0.7.0.dist-info/RECORD,,
File without changes