lumera 0.9.9__py3-none-any.whl → 0.10.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/automations.py CHANGED
@@ -57,18 +57,76 @@ __all__ = [
57
57
  "create",
58
58
  "update",
59
59
  "upsert",
60
- # Log streaming and download
60
+ # Log functions
61
61
  "stream_logs",
62
+ "get_logs",
62
63
  "get_log_download_url",
63
64
  # Classes
64
65
  "Run",
65
66
  "Automation",
67
+ "LogsResponse",
66
68
  ]
67
69
 
68
70
  from ._utils import LumeraAPIError, _api_request
69
71
  from .sdk import get_automation_run as _get_automation_run
70
72
  from .sdk import run_automation as _run_automation
71
73
 
74
+ # ============================================================================
75
+ # LogsResponse Class
76
+ # ============================================================================
77
+
78
+
79
+ class LogsResponse:
80
+ """Response from fetching automation run logs.
81
+
82
+ Attributes:
83
+ data: Raw log content as a string (NDJSON format).
84
+ offset: Byte offset where this chunk starts.
85
+ size: Number of bytes in this chunk.
86
+ total_size: Total size of the log file.
87
+ has_more: True if there are more logs after this chunk.
88
+ source: Where logs came from ("live" or "archived").
89
+ truncated: True if logs were truncated at storage time (>50MB).
90
+ """
91
+
92
+ def __init__(self, data: dict[str, Any]) -> None:
93
+ self._data = data
94
+
95
+ @property
96
+ def data(self) -> str:
97
+ return self._data.get("data", "")
98
+
99
+ @property
100
+ def offset(self) -> int:
101
+ return self._data.get("offset", 0)
102
+
103
+ @property
104
+ def size(self) -> int:
105
+ return self._data.get("size", 0)
106
+
107
+ @property
108
+ def total_size(self) -> int:
109
+ return self._data.get("total_size", 0)
110
+
111
+ @property
112
+ def has_more(self) -> bool:
113
+ return self._data.get("has_more", False)
114
+
115
+ @property
116
+ def source(self) -> str:
117
+ return self._data.get("source", "")
118
+
119
+ @property
120
+ def truncated(self) -> bool:
121
+ return self._data.get("truncated", False)
122
+
123
+ def __repr__(self) -> str:
124
+ return (
125
+ f"LogsResponse(offset={self.offset}, size={self.size}, "
126
+ f"total_size={self.total_size}, has_more={self.has_more})"
127
+ )
128
+
129
+
72
130
  # ============================================================================
73
131
  # Run Class
74
132
  # ============================================================================
@@ -246,6 +304,66 @@ class Run:
246
304
  raise ValueError("Cannot get log URL without run id")
247
305
  return get_log_download_url(self.id)
248
306
 
307
+ def logs(
308
+ self,
309
+ *,
310
+ offset: int = 0,
311
+ limit: int = 1024 * 1024,
312
+ all: bool = False,
313
+ ) -> LogsResponse:
314
+ """Fetch logs for this run.
315
+
316
+ Works for both live (running) and archived (completed) runs.
317
+ Returns raw log data as a string (NDJSON format).
318
+
319
+ Args:
320
+ offset: Byte offset to start from. Negative values read from end
321
+ (e.g., -1048576 = last 1MB).
322
+ limit: Maximum bytes to return (default 1MB).
323
+ all: If True, fetch all logs at once. Returns 400 if logs > 10MB.
324
+
325
+ Returns:
326
+ A LogsResponse object with data, offset, size, total_size, has_more,
327
+ source ("live" or "archived"), and truncated flag.
328
+
329
+ Raises:
330
+ ValueError: If the run has no ID.
331
+ LumeraAPIError: If logs are not available or request fails.
332
+
333
+ Example:
334
+ >>> run = automations.get_run("run_id")
335
+ >>> resp = run.logs()
336
+ >>> print(resp.data) # Raw NDJSON log content
337
+ >>> while resp.has_more:
338
+ ... resp = run.logs(offset=resp.offset + resp.size)
339
+ ... print(resp.data)
340
+ """
341
+ if not self.id:
342
+ raise ValueError("Cannot fetch logs without run id")
343
+ return get_logs(self.id, offset=offset, limit=limit, all=all)
344
+
345
+ def stream_logs(self, *, timeout: float = 30) -> Iterator[str]:
346
+ """Stream logs from this run.
347
+
348
+ Works for both live (running) and archived (completed) runs.
349
+ For live runs, streams in real-time as logs are produced.
350
+ For archived runs, streams the entire log from S3.
351
+
352
+ Args:
353
+ timeout: HTTP connection timeout in seconds.
354
+
355
+ Yields:
356
+ Log lines as strings (raw NDJSON lines).
357
+
358
+ Example:
359
+ >>> run = automations.run("automation_id", inputs={})
360
+ >>> for line in run.stream_logs():
361
+ ... print(line)
362
+ """
363
+ if not self.id:
364
+ raise ValueError("Cannot stream logs without run id")
365
+ return stream_logs(self.id, timeout=timeout)
366
+
249
367
  def to_dict(self) -> dict[str, Any]:
250
368
  """Return the underlying data dict."""
251
369
  return self._data.copy()
@@ -795,17 +913,19 @@ def delete(automation_id: str) -> None:
795
913
 
796
914
 
797
915
  def stream_logs(run_id: str, *, timeout: float = 30) -> Iterator[str]:
798
- """Stream live logs from a running automation.
916
+ """Stream logs from an automation run.
799
917
 
918
+ Works for both live (running) and archived (completed) runs.
800
919
  Connects to the server-sent events endpoint and yields log lines
801
- as they arrive. Stops when the run completes.
920
+ as they arrive. For live runs, streams in real-time. For archived
921
+ runs, streams the entire log from storage.
802
922
 
803
923
  Args:
804
924
  run_id: The run ID to stream logs from.
805
925
  timeout: HTTP connection timeout in seconds.
806
926
 
807
927
  Yields:
808
- Log lines as strings.
928
+ Log lines as strings (raw NDJSON lines).
809
929
 
810
930
  Example:
811
931
  >>> for line in automations.stream_logs("run_id"):
@@ -825,7 +945,7 @@ def stream_logs(run_id: str, *, timeout: float = 30) -> Iterator[str]:
825
945
  if not token:
826
946
  raise ValueError("LUMERA_TOKEN environment variable is required")
827
947
 
828
- url = f"{base_url}/automation-runs/{run_id}/logs/live"
948
+ url = f"{base_url}/automation-runs/{run_id}/logs?stream=true"
829
949
  headers = {
830
950
  "Authorization": f"token {token}",
831
951
  "Accept": "text/event-stream",
@@ -902,3 +1022,51 @@ def get_log_download_url(run_id: str) -> str:
902
1022
  if isinstance(result, dict) and "url" in result:
903
1023
  return result["url"]
904
1024
  raise RuntimeError("Unexpected response: no download URL returned")
1025
+
1026
+
1027
+ def get_logs(
1028
+ run_id: str,
1029
+ *,
1030
+ offset: int = 0,
1031
+ limit: int = 1024 * 1024,
1032
+ all: bool = False,
1033
+ ) -> LogsResponse:
1034
+ """Fetch logs for an automation run.
1035
+
1036
+ Works for both live (running) and archived (completed) runs.
1037
+ Returns raw log data as a string (NDJSON format).
1038
+
1039
+ Args:
1040
+ run_id: The run ID to get logs for.
1041
+ offset: Byte offset to start from. Negative values read from end
1042
+ (e.g., -1048576 = last 1MB).
1043
+ limit: Maximum bytes to return (default 1MB).
1044
+ all: If True, fetch all logs at once. Returns 400 if logs > 10MB.
1045
+
1046
+ Returns:
1047
+ A LogsResponse object with data, offset, size, total_size, has_more,
1048
+ source ("live" or "archived"), and truncated flag.
1049
+
1050
+ Raises:
1051
+ ValueError: If run_id is empty.
1052
+ LumeraAPIError: If logs are not available or request fails.
1053
+
1054
+ Example:
1055
+ >>> resp = automations.get_logs("run_id")
1056
+ >>> print(resp.data) # Raw NDJSON log content
1057
+ >>> while resp.has_more:
1058
+ ... resp = automations.get_logs("run_id", offset=resp.offset + resp.size)
1059
+ ... print(resp.data)
1060
+ """
1061
+ run_id = run_id.strip()
1062
+ if not run_id:
1063
+ raise ValueError("run_id is required")
1064
+
1065
+ params: dict[str, Any] = {"offset": offset, "limit": limit}
1066
+ if all:
1067
+ params["all"] = "true"
1068
+
1069
+ result = _api_request("GET", f"automation-runs/{run_id}/logs", params=params)
1070
+ if isinstance(result, dict):
1071
+ return LogsResponse(result)
1072
+ raise RuntimeError("Unexpected response from logs endpoint")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lumera
3
- Version: 0.9.9
3
+ Version: 0.10.0
4
4
  Summary: SDK for building on Lumera platform
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: requests
@@ -1,6 +1,6 @@
1
1
  lumera/__init__.py,sha256=5FlY5dSJ1WNM4ko7wgmcajO8G2voBGn4S19E91_WdqE,2687
2
2
  lumera/_utils.py,sha256=b-l3Ebh4n2pC-9T5mR6h4hPf_Wl48VDlHES0pLo1zKE,25766
3
- lumera/automations.py,sha256=UKKtgmsYWIzJvMnIf0K5ywXCtgIu8kb9DmsAX_upubM,27397
3
+ lumera/automations.py,sha256=KPP_rD7WKmBs865jiKoonZJjdTno-FSAU7hajPFyqs0,32851
4
4
  lumera/email.py,sha256=lk8KUsRw1ZvxgM0FPQXH-jVKUQA5f0zLv88jlc3IWlA,5056
5
5
  lumera/exceptions.py,sha256=bNsx4iYaroAAGsYxErfELC2B5ZJ3w5lVa1kKdIx5s9g,2173
6
6
  lumera/files.py,sha256=xMJmLTSaQQDttM3AMmpOWc6soh4lvCCKBreV0fXWHQw,3159
@@ -13,7 +13,7 @@ lumera/storage.py,sha256=fWkscTvKDzQ-5tsfA1lREO2qgtjJ4Yvxj3hvYNLKiW0,10527
13
13
  lumera/webhooks.py,sha256=L_Q5YHBJKQNpv7G9Nq0QqlGMRch6x9ptlwu1xD2qwUc,8661
14
14
  lumera/integrations/__init__.py,sha256=LnJmAnFB_p3YMKyeGVdDP4LYlJ85XFNQFAxGo6zF7CI,937
15
15
  lumera/integrations/google.py,sha256=QkbBbbDh3I_OToPDFqcivU6sWy2UieHBxZ_TPv5rqK0,11862
16
- lumera-0.9.9.dist-info/METADATA,sha256=NFyzKojtDJrZktYxFL-a5QM85V9XmWwJk3kawbSTR20,1611
17
- lumera-0.9.9.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
18
- lumera-0.9.9.dist-info/top_level.txt,sha256=HgfK4XQkpMTnM2E5iWM4kB711FnYqUY9dglzib3pWlE,7
19
- lumera-0.9.9.dist-info/RECORD,,
16
+ lumera-0.10.0.dist-info/METADATA,sha256=uWvSDuD868zVICFyVUppHMIrWe6A-JKeyxFRurjxieU,1612
17
+ lumera-0.10.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
18
+ lumera-0.10.0.dist-info/top_level.txt,sha256=HgfK4XQkpMTnM2E5iWM4kB711FnYqUY9dglzib3pWlE,7
19
+ lumera-0.10.0.dist-info/RECORD,,