lumera 0.8.0__py3-none-any.whl → 0.8.2__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 +3 -2
- lumera/_utils.py +22 -1
- lumera/automations.py +9 -9
- lumera/google.py +47 -270
- lumera/sdk.py +9 -8
- lumera/webhooks.py +20 -11
- {lumera-0.8.0.dist-info → lumera-0.8.2.dist-info}/METADATA +1 -1
- lumera-0.8.2.dist-info/RECORD +15 -0
- lumera-0.8.0.dist-info/RECORD +0 -15
- {lumera-0.8.0.dist-info → lumera-0.8.2.dist-info}/WHEEL +0 -0
- {lumera-0.8.0.dist-info → lumera-0.8.2.dist-info}/top_level.txt +0 -0
lumera/__init__.py
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Lumera Agent SDK
|
|
3
3
|
|
|
4
|
-
This SDK provides helpers for
|
|
4
|
+
This SDK provides helpers for automations running within the Lumera environment
|
|
5
5
|
to interact with the Lumera API and define dynamic user interfaces.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
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, webhooks
|
|
11
|
+
from . import automations, exceptions, integrations, llm, locks, pb, storage, webhooks
|
|
12
12
|
from ._utils import (
|
|
13
13
|
LumeraAPIError,
|
|
14
14
|
RecordNotUniqueError,
|
|
@@ -98,4 +98,5 @@ __all__ = [
|
|
|
98
98
|
"locks",
|
|
99
99
|
"exceptions",
|
|
100
100
|
"webhooks",
|
|
101
|
+
"integrations",
|
|
101
102
|
]
|
lumera/_utils.py
CHANGED
|
@@ -13,6 +13,7 @@ from functools import wraps as _wraps
|
|
|
13
13
|
from typing import IO, Any, Callable, Iterable, Mapping, MutableMapping, Sequence, TypeVar
|
|
14
14
|
|
|
15
15
|
import requests
|
|
16
|
+
import requests.adapters
|
|
16
17
|
from dotenv import load_dotenv
|
|
17
18
|
|
|
18
19
|
TOKEN_ENV = "LUMERA_TOKEN"
|
|
@@ -35,6 +36,25 @@ MOUNT_ROOT = _mount_env
|
|
|
35
36
|
|
|
36
37
|
_token_cache: dict[str, tuple[str, float]] = {}
|
|
37
38
|
|
|
39
|
+
# Connection pooling for better performance
|
|
40
|
+
_http_session: requests.Session | None = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _get_session() -> requests.Session:
|
|
44
|
+
"""Get or create a shared requests Session with connection pooling."""
|
|
45
|
+
global _http_session
|
|
46
|
+
if _http_session is None:
|
|
47
|
+
_http_session = requests.Session()
|
|
48
|
+
# Configure connection pooling
|
|
49
|
+
adapter = requests.adapters.HTTPAdapter(
|
|
50
|
+
pool_connections=10,
|
|
51
|
+
pool_maxsize=20,
|
|
52
|
+
max_retries=0, # Don't retry automatically, let caller handle
|
|
53
|
+
)
|
|
54
|
+
_http_session.mount("https://", adapter)
|
|
55
|
+
_http_session.mount("http://", adapter)
|
|
56
|
+
return _http_session
|
|
57
|
+
|
|
38
58
|
|
|
39
59
|
def get_lumera_token() -> str:
|
|
40
60
|
token = os.getenv(TOKEN_ENV)
|
|
@@ -251,7 +271,8 @@ def _api_request(
|
|
|
251
271
|
"Accept": "application/json",
|
|
252
272
|
}
|
|
253
273
|
|
|
254
|
-
|
|
274
|
+
session = _get_session()
|
|
275
|
+
resp = session.request(
|
|
255
276
|
method,
|
|
256
277
|
url,
|
|
257
278
|
params=params,
|
lumera/automations.py
CHANGED
|
@@ -25,7 +25,7 @@ Example:
|
|
|
25
25
|
>>> from lumera import automations
|
|
26
26
|
>>>
|
|
27
27
|
>>> # Run an automation
|
|
28
|
-
>>> run = automations.run("
|
|
28
|
+
>>> run = automations.run("automation_id", inputs={"limit": 100})
|
|
29
29
|
>>> print(run.id, run.status)
|
|
30
30
|
>>>
|
|
31
31
|
>>> # Wait for completion
|
|
@@ -78,7 +78,7 @@ class Run:
|
|
|
78
78
|
|
|
79
79
|
Attributes:
|
|
80
80
|
id: The run ID.
|
|
81
|
-
automation_id: The automation
|
|
81
|
+
automation_id: The automation ID.
|
|
82
82
|
status: Current status (queued, running, succeeded, failed, cancelled, timeout).
|
|
83
83
|
inputs: The inputs passed to the run.
|
|
84
84
|
result: The result returned by the automation (when succeeded).
|
|
@@ -329,7 +329,7 @@ def run(
|
|
|
329
329
|
"""Run an automation by ID.
|
|
330
330
|
|
|
331
331
|
Args:
|
|
332
|
-
automation_id: The automation
|
|
332
|
+
automation_id: The automation ID to run.
|
|
333
333
|
inputs: Input parameters (dict). Types are coerced based on input_schema.
|
|
334
334
|
files: File inputs to upload (mapping of input key to file path(s)).
|
|
335
335
|
external_id: Optional correlation ID for idempotency. Repeated calls
|
|
@@ -430,7 +430,7 @@ def list_runs(
|
|
|
430
430
|
List of Run objects.
|
|
431
431
|
|
|
432
432
|
Example:
|
|
433
|
-
>>> runs = automations.list_runs("
|
|
433
|
+
>>> runs = automations.list_runs("automation_id", status="succeeded", limit=10)
|
|
434
434
|
>>> for r in runs:
|
|
435
435
|
... print(r.id, r.created, r.status)
|
|
436
436
|
"""
|
|
@@ -509,8 +509,8 @@ def get(automation_id: str) -> Automation:
|
|
|
509
509
|
An Automation object.
|
|
510
510
|
|
|
511
511
|
Example:
|
|
512
|
-
>>>
|
|
513
|
-
>>> print(
|
|
512
|
+
>>> automation = automations.get("abc123")
|
|
513
|
+
>>> print(automation.name, automation.input_schema)
|
|
514
514
|
"""
|
|
515
515
|
automation_id = automation_id.strip()
|
|
516
516
|
if not automation_id:
|
|
@@ -535,8 +535,8 @@ def get_by_external_id(external_id: str) -> Automation:
|
|
|
535
535
|
LumeraAPIError: If no automation with that external_id exists.
|
|
536
536
|
|
|
537
537
|
Example:
|
|
538
|
-
>>>
|
|
539
|
-
>>> print(
|
|
538
|
+
>>> automation = automations.get_by_external_id("deposit_matching:step1")
|
|
539
|
+
>>> print(automation.id, automation.name)
|
|
540
540
|
"""
|
|
541
541
|
external_id = external_id.strip()
|
|
542
542
|
if not external_id:
|
|
@@ -582,7 +582,7 @@ def create(
|
|
|
582
582
|
The created Automation object.
|
|
583
583
|
|
|
584
584
|
Example:
|
|
585
|
-
>>>
|
|
585
|
+
>>> automation = automations.create(
|
|
586
586
|
... name="My Automation",
|
|
587
587
|
... code="def main(x): return {'result': x * 2}",
|
|
588
588
|
... input_schema={
|
lumera/google.py
CHANGED
|
@@ -1,270 +1,47 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
#
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
#
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
#
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
# Authentication & Service Initialization
|
|
49
|
-
# =====================================================================================
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def get_google_credentials() -> Credentials:
|
|
53
|
-
"""
|
|
54
|
-
Retrieves a Google OAuth token from Lumera and
|
|
55
|
-
converts it into a Credentials object usable by googleapiclient.
|
|
56
|
-
"""
|
|
57
|
-
logger.debug("Fetching Google access token from Lumera…")
|
|
58
|
-
access_token = get_access_token("google")
|
|
59
|
-
logger.debug("Access token received.")
|
|
60
|
-
creds = Credentials(token=access_token)
|
|
61
|
-
logger.debug("Credentials object created.")
|
|
62
|
-
return creds
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def get_sheets_service(credentials: Optional[Credentials] = None) -> 'Resource':
|
|
66
|
-
"""
|
|
67
|
-
Initializes and returns the Google Sheets API service.
|
|
68
|
-
|
|
69
|
-
If no credentials are provided, this function will automatically fetch a
|
|
70
|
-
Google access token from Lumera and construct the appropriate
|
|
71
|
-
``google.oauth2.credentials.Credentials`` instance.
|
|
72
|
-
"""
|
|
73
|
-
if credentials is None:
|
|
74
|
-
logger.info("No credentials provided; fetching Google token…")
|
|
75
|
-
credentials = get_google_credentials()
|
|
76
|
-
logger.info("Google Sheets API service being initialized…")
|
|
77
|
-
return build('sheets', 'v4', credentials=credentials)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def get_drive_service(credentials: Optional[Credentials] = None) -> 'Resource':
|
|
81
|
-
"""
|
|
82
|
-
Initializes and returns the Google Drive API service.
|
|
83
|
-
|
|
84
|
-
If no credentials are provided, this function will automatically fetch a
|
|
85
|
-
Google access token from Lumera and construct the appropriate
|
|
86
|
-
``google.oauth2.credentials.Credentials`` instance.
|
|
87
|
-
"""
|
|
88
|
-
if credentials is None:
|
|
89
|
-
logger.info("No credentials provided; fetching Google token…")
|
|
90
|
-
credentials = get_google_credentials()
|
|
91
|
-
logger.info("Google Drive API service being initialized…")
|
|
92
|
-
return build('drive', 'v3', credentials=credentials)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
# =====================================================================================
|
|
96
|
-
# Google Sheets & Drive Utility Functions
|
|
97
|
-
# =====================================================================================
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def get_spreadsheet_and_sheet_id(
|
|
101
|
-
service: 'Resource', spreadsheet_url: str, tab_name: str
|
|
102
|
-
) -> Tuple[Optional[str], Optional[int]]:
|
|
103
|
-
"""
|
|
104
|
-
Given a Google Sheets URL and a tab (sheet) name, returns a tuple:
|
|
105
|
-
(spreadsheet_id, sheet_id)
|
|
106
|
-
"""
|
|
107
|
-
spreadsheet_id = _extract_spreadsheet_id(spreadsheet_url)
|
|
108
|
-
if not spreadsheet_id:
|
|
109
|
-
return None, None
|
|
110
|
-
|
|
111
|
-
sheet_id = _get_sheet_id_from_name(service, spreadsheet_id, tab_name)
|
|
112
|
-
return spreadsheet_id, sheet_id
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def _extract_spreadsheet_id(spreadsheet_url: str) -> Optional[str]:
|
|
116
|
-
"""Extracts the spreadsheet ID from a Google Sheets URL."""
|
|
117
|
-
logger.debug(f"Extracting spreadsheet ID from URL: {spreadsheet_url}")
|
|
118
|
-
pattern = r"/d/([a-zA-Z0-9-_]+)"
|
|
119
|
-
match = re.search(pattern, spreadsheet_url)
|
|
120
|
-
if match:
|
|
121
|
-
spreadsheet_id = match.group(1)
|
|
122
|
-
logger.debug(f"Spreadsheet ID extracted: {spreadsheet_id}")
|
|
123
|
-
return spreadsheet_id
|
|
124
|
-
logger.warning("Could not extract Spreadsheet ID.")
|
|
125
|
-
return None
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def _get_sheet_id_from_name(
|
|
129
|
-
service: 'Resource', spreadsheet_id: str, tab_name: str
|
|
130
|
-
) -> Optional[int]:
|
|
131
|
-
"""Uses the Google Sheets API to fetch the sheet ID corresponding to 'tab_name'."""
|
|
132
|
-
logger.debug(f"Requesting sheet metadata for spreadsheet ID: {spreadsheet_id}")
|
|
133
|
-
response = (
|
|
134
|
-
service.spreadsheets()
|
|
135
|
-
.get(spreadsheetId=spreadsheet_id, fields="sheets.properties")
|
|
136
|
-
.execute()
|
|
137
|
-
)
|
|
138
|
-
logger.debug("Metadata received. Searching for tab…")
|
|
139
|
-
|
|
140
|
-
for sheet in response.get("sheets", []):
|
|
141
|
-
properties = sheet.get("properties", {})
|
|
142
|
-
if properties.get("title") == tab_name:
|
|
143
|
-
sheet_id = properties.get("sheetId")
|
|
144
|
-
logger.debug(f"Match found for tab '{tab_name}'. Sheet ID is {sheet_id}")
|
|
145
|
-
return sheet_id
|
|
146
|
-
logger.warning(f"No sheet found with tab name '{tab_name}'.")
|
|
147
|
-
return None
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def sheet_name_from_gid(service: 'Resource', spreadsheet_id: str, gid: int) -> Optional[str]:
|
|
151
|
-
"""Resolve a sheet's human-readable name (title) from its gid."""
|
|
152
|
-
logger.debug(f"Resolving sheet name from gid={gid} …")
|
|
153
|
-
meta = (
|
|
154
|
-
service.spreadsheets()
|
|
155
|
-
.get(
|
|
156
|
-
spreadsheetId=spreadsheet_id,
|
|
157
|
-
includeGridData=False,
|
|
158
|
-
fields="sheets(properties(sheetId,title))",
|
|
159
|
-
)
|
|
160
|
-
.execute()
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
for sheet in meta.get("sheets", []):
|
|
164
|
-
props = sheet.get("properties", {})
|
|
165
|
-
if props.get("sheetId") == gid:
|
|
166
|
-
title = props["title"]
|
|
167
|
-
logger.debug(f"Sheet gid={gid} corresponds to sheet name='{title}'.")
|
|
168
|
-
return title
|
|
169
|
-
logger.warning(f"No sheet found with gid={gid}")
|
|
170
|
-
return None
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
def read_cell(service: 'Resource', spreadsheet_id: str, range_a1: str) -> Optional[str]:
|
|
174
|
-
"""Fetch a single cell value (as string); returns None if empty."""
|
|
175
|
-
logger.debug(f"Reading cell '{range_a1}' …")
|
|
176
|
-
resp = (
|
|
177
|
-
service.spreadsheets()
|
|
178
|
-
.values()
|
|
179
|
-
.get(spreadsheetId=spreadsheet_id, range=range_a1, majorDimension="ROWS")
|
|
180
|
-
.execute()
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
values = resp.get("values", [])
|
|
184
|
-
return values[0][0] if values and values[0] else None
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
# NOTE: The function performs I/O side-effects and does not return a value.
|
|
188
|
-
def download_file_direct(drive_service: 'Resource', file_id: str, dest_path: str) -> None:
|
|
189
|
-
"""
|
|
190
|
-
Downloads a file directly from Google Drive using files().get_media
|
|
191
|
-
without any format conversion.
|
|
192
|
-
"""
|
|
193
|
-
logger.info(f"Initiating direct download for file ID: {file_id}")
|
|
194
|
-
|
|
195
|
-
request = drive_service.files().get_media(fileId=file_id)
|
|
196
|
-
fh = io.BytesIO()
|
|
197
|
-
downloader = MediaIoBaseDownload(fh, request)
|
|
198
|
-
|
|
199
|
-
done = False
|
|
200
|
-
while not done:
|
|
201
|
-
status, done = downloader.next_chunk()
|
|
202
|
-
if status:
|
|
203
|
-
logger.debug(f"Download progress: {int(status.progress() * 100)}%")
|
|
204
|
-
|
|
205
|
-
with open(dest_path, "wb") as f:
|
|
206
|
-
f.write(fh.getvalue())
|
|
207
|
-
logger.info(f"File saved to: {dest_path}")
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
def upload_excel_as_google_sheet(
|
|
211
|
-
drive_service: 'Resource', local_path: str, desired_name: str
|
|
212
|
-
) -> Tuple[Optional[str], Optional[str]]:
|
|
213
|
-
"""
|
|
214
|
-
Uploads a local XLSX file to Google Drive, converting it to Google Sheets format.
|
|
215
|
-
Returns the file ID and web link.
|
|
216
|
-
"""
|
|
217
|
-
logger.info(f"Preparing to upload '{local_path}' as Google Sheet named '{desired_name}'")
|
|
218
|
-
|
|
219
|
-
if not os.path.isfile(local_path):
|
|
220
|
-
logger.error(f"Local file not found at '{local_path}'. Aborting.")
|
|
221
|
-
return None, None
|
|
222
|
-
|
|
223
|
-
media = MediaFileUpload(local_path, mimetype=MIME_EXCEL, resumable=True)
|
|
224
|
-
file_metadata = {"name": desired_name, "mimeType": MIME_GOOGLE_SHEET}
|
|
225
|
-
|
|
226
|
-
logger.info("Initiating Google Drive upload & conversion…")
|
|
227
|
-
request = drive_service.files().create(
|
|
228
|
-
body=file_metadata, media_body=media, fields="id, webViewLink"
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
response = None
|
|
232
|
-
while response is None:
|
|
233
|
-
status, response = request.next_chunk()
|
|
234
|
-
if status:
|
|
235
|
-
logger.debug(f"Upload progress: {int(status.progress() * 100)}%")
|
|
236
|
-
|
|
237
|
-
file_id = response.get("id")
|
|
238
|
-
web_view_link = response.get("webViewLink")
|
|
239
|
-
logger.info(f"Upload completed. File ID: {file_id}")
|
|
240
|
-
return file_id, web_view_link
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
# Remove rows from a sheet. All parameters are 1-based (both *start_row* and
|
|
244
|
-
# *end_row* are inclusive) mirroring the UI behaviour in Google Sheets.
|
|
245
|
-
def delete_rows_api_call(
|
|
246
|
-
service: 'Resource',
|
|
247
|
-
spreadsheet_id: str,
|
|
248
|
-
sheet_gid: int,
|
|
249
|
-
start_row: int,
|
|
250
|
-
end_row: int,
|
|
251
|
-
) -> None:
|
|
252
|
-
"""Executes the API call to delete rows."""
|
|
253
|
-
logger.info(f"Deleting rows {start_row}-{end_row} (1-based inclusive)…")
|
|
254
|
-
|
|
255
|
-
body = {
|
|
256
|
-
"requests": [
|
|
257
|
-
{
|
|
258
|
-
"deleteDimension": {
|
|
259
|
-
"range": {
|
|
260
|
-
"sheetId": sheet_gid,
|
|
261
|
-
"dimension": "ROWS",
|
|
262
|
-
"startIndex": start_row - 1, # 0-based
|
|
263
|
-
"endIndex": end_row, # end-exclusive
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
]
|
|
268
|
-
}
|
|
269
|
-
service.spreadsheets().batchUpdate(spreadsheetId=spreadsheet_id, body=body).execute()
|
|
270
|
-
logger.info("Rows deleted.")
|
|
1
|
+
"""
|
|
2
|
+
Backward compatibility shim for lumera.google.
|
|
3
|
+
|
|
4
|
+
This module has moved to lumera.integrations.google.
|
|
5
|
+
All imports are re-exported here for backward compatibility.
|
|
6
|
+
|
|
7
|
+
New code should use:
|
|
8
|
+
from lumera.integrations import google
|
|
9
|
+
# or
|
|
10
|
+
from lumera.integrations.google import get_sheets_service, get_drive_service
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
# Re-export everything from the new location
|
|
14
|
+
from lumera.integrations.google import (
|
|
15
|
+
MIME_EXCEL,
|
|
16
|
+
MIME_GOOGLE_SHEET,
|
|
17
|
+
delete_rows_api_call,
|
|
18
|
+
download_file_direct,
|
|
19
|
+
get_credentials,
|
|
20
|
+
get_drive_service,
|
|
21
|
+
get_google_credentials,
|
|
22
|
+
get_sheets_service,
|
|
23
|
+
get_spreadsheet_and_sheet_id,
|
|
24
|
+
read_cell,
|
|
25
|
+
sheet_name_from_gid,
|
|
26
|
+
upload_excel_as_google_sheet,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
# Authentication
|
|
31
|
+
"get_credentials",
|
|
32
|
+
"get_google_credentials",
|
|
33
|
+
# Services
|
|
34
|
+
"get_sheets_service",
|
|
35
|
+
"get_drive_service",
|
|
36
|
+
# Sheets helpers
|
|
37
|
+
"get_spreadsheet_and_sheet_id",
|
|
38
|
+
"sheet_name_from_gid",
|
|
39
|
+
"read_cell",
|
|
40
|
+
"delete_rows_api_call",
|
|
41
|
+
# Drive helpers
|
|
42
|
+
"download_file_direct",
|
|
43
|
+
"upload_excel_as_google_sheet",
|
|
44
|
+
# Constants
|
|
45
|
+
"MIME_GOOGLE_SHEET",
|
|
46
|
+
"MIME_EXCEL",
|
|
47
|
+
]
|
lumera/sdk.py
CHANGED
|
@@ -399,10 +399,10 @@ def run_automation(
|
|
|
399
399
|
external_id: str | None = None,
|
|
400
400
|
metadata: Mapping[str, Any] | None = None,
|
|
401
401
|
) -> dict[str, Any]:
|
|
402
|
-
"""Create an
|
|
402
|
+
"""Create an automation run and optionally upload files for file inputs.
|
|
403
403
|
|
|
404
404
|
Args:
|
|
405
|
-
automation_id: The automation
|
|
405
|
+
automation_id: The automation 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``).
|
|
@@ -463,12 +463,13 @@ def get_automation_run(
|
|
|
463
463
|
run_id: str | None = None,
|
|
464
464
|
external_id: str | None = None,
|
|
465
465
|
) -> dict[str, Any]:
|
|
466
|
-
"""Fetch an
|
|
466
|
+
"""Fetch an automation run by id or by automation_id + external_id idempotency key.
|
|
467
467
|
|
|
468
468
|
Args:
|
|
469
|
-
automation_id:
|
|
470
|
-
|
|
471
|
-
|
|
469
|
+
automation_id: Automation id for external_id lookup.
|
|
470
|
+
Required when ``run_id`` is not provided.
|
|
471
|
+
run_id: Optional run id. When provided, takes precedence over external_id.
|
|
472
|
+
external_id: Optional idempotency key to look up the latest run for the automation.
|
|
472
473
|
|
|
473
474
|
Raises:
|
|
474
475
|
ValueError: If required identifiers are missing.
|
|
@@ -510,7 +511,7 @@ def update_automation_run(
|
|
|
510
511
|
error: str | None = None,
|
|
511
512
|
metadata: Mapping[str, Any] | None = None,
|
|
512
513
|
) -> dict[str, Any]:
|
|
513
|
-
"""Update an
|
|
514
|
+
"""Update an automation run with result, status, or other fields.
|
|
514
515
|
|
|
515
516
|
Args:
|
|
516
517
|
run_id: The run id to update. Required.
|
|
@@ -520,7 +521,7 @@ def update_automation_run(
|
|
|
520
521
|
metadata: Optional metadata update.
|
|
521
522
|
|
|
522
523
|
Returns:
|
|
523
|
-
The updated
|
|
524
|
+
The updated automation run record.
|
|
524
525
|
"""
|
|
525
526
|
run_id = run_id.strip() if isinstance(run_id, str) else ""
|
|
526
527
|
if not run_id:
|
lumera/webhooks.py
CHANGED
|
@@ -25,7 +25,7 @@ Example:
|
|
|
25
25
|
>>> # Get the public URL to configure in Stripe
|
|
26
26
|
>>> webhook_url = webhooks.url("stripe-events")
|
|
27
27
|
>>> print(webhook_url)
|
|
28
|
-
https://app.lumerahq.com/webhooks/
|
|
28
|
+
https://app.lumerahq.com/webhooks/rec_abc123/stripe-events
|
|
29
29
|
>>>
|
|
30
30
|
>>> # List all endpoints
|
|
31
31
|
>>> for ep in webhooks.list():
|
|
@@ -171,11 +171,18 @@ def get(external_id: str) -> dict[str, Any]:
|
|
|
171
171
|
result = _api_request("GET", f"collections/{_COLLECTION}/records", params=params)
|
|
172
172
|
|
|
173
173
|
if not isinstance(result, dict):
|
|
174
|
-
raise LumeraAPIError(
|
|
174
|
+
raise LumeraAPIError(
|
|
175
|
+
404, "endpoint not found", url=f"collections/{_COLLECTION}/records", payload=None
|
|
176
|
+
)
|
|
175
177
|
|
|
176
178
|
items = result.get("items", [])
|
|
177
179
|
if not items:
|
|
178
|
-
raise LumeraAPIError(
|
|
180
|
+
raise LumeraAPIError(
|
|
181
|
+
404,
|
|
182
|
+
f"webhook endpoint '{external_id}' not found",
|
|
183
|
+
url=f"collections/{_COLLECTION}/records",
|
|
184
|
+
payload=None,
|
|
185
|
+
)
|
|
179
186
|
|
|
180
187
|
return items[0]
|
|
181
188
|
|
|
@@ -223,7 +230,9 @@ def update(
|
|
|
223
230
|
if not payload:
|
|
224
231
|
raise ValueError("at least one field (name or description) must be provided")
|
|
225
232
|
|
|
226
|
-
result = _api_request(
|
|
233
|
+
result = _api_request(
|
|
234
|
+
"PATCH", f"collections/{_COLLECTION}/records/{record_id}", json_body=payload
|
|
235
|
+
)
|
|
227
236
|
if not isinstance(result, dict):
|
|
228
237
|
raise RuntimeError("unexpected response payload")
|
|
229
238
|
return result
|
|
@@ -257,7 +266,7 @@ def url(external_id: str) -> str:
|
|
|
257
266
|
"""Get the public webhook URL for an endpoint.
|
|
258
267
|
|
|
259
268
|
This returns the full URL that external services should send webhooks to.
|
|
260
|
-
The URL format is: https://{base}/webhooks/{
|
|
269
|
+
The URL format is: https://{base}/webhooks/{company_id}/{external_id}
|
|
261
270
|
|
|
262
271
|
Args:
|
|
263
272
|
external_id: The external_id of the endpoint
|
|
@@ -267,12 +276,12 @@ def url(external_id: str) -> str:
|
|
|
267
276
|
|
|
268
277
|
Raises:
|
|
269
278
|
ValueError: If external_id is empty
|
|
270
|
-
RuntimeError: If
|
|
279
|
+
RuntimeError: If COMPANY_ID environment variable is not set
|
|
271
280
|
|
|
272
281
|
Example:
|
|
273
282
|
>>> url = webhooks.url("stripe-events")
|
|
274
283
|
>>> print(url)
|
|
275
|
-
https://app.lumerahq.com/webhooks/
|
|
284
|
+
https://app.lumerahq.com/webhooks/rec_abc123/stripe-events
|
|
276
285
|
|
|
277
286
|
# Use this URL when configuring webhooks in Stripe, GitHub, etc.
|
|
278
287
|
"""
|
|
@@ -280,10 +289,10 @@ def url(external_id: str) -> str:
|
|
|
280
289
|
if not external_id:
|
|
281
290
|
raise ValueError("external_id is required")
|
|
282
291
|
|
|
283
|
-
|
|
284
|
-
if not
|
|
292
|
+
company_id = os.getenv("COMPANY_ID", "").strip()
|
|
293
|
+
if not company_id:
|
|
285
294
|
raise RuntimeError(
|
|
286
|
-
"
|
|
295
|
+
"COMPANY_ID environment variable not set. "
|
|
287
296
|
"This is required to construct the webhook URL."
|
|
288
297
|
)
|
|
289
298
|
|
|
@@ -292,4 +301,4 @@ def url(external_id: str) -> str:
|
|
|
292
301
|
if base_url.endswith("/api"):
|
|
293
302
|
base_url = base_url[:-4]
|
|
294
303
|
|
|
295
|
-
return f"{base_url}/webhooks/{
|
|
304
|
+
return f"{base_url}/webhooks/{company_id}/{external_id}"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
lumera/__init__.py,sha256=rOTVv9u-Wtl0ky4oPN5yF9AMRO3UOSZo-BEMwqmMoV4,2352
|
|
2
|
+
lumera/_utils.py,sha256=Gr3WRaU6MI-jpx3e5OMxAkVYYQmkUXg2J56Sy7AuEGE,24443
|
|
3
|
+
lumera/automations.py,sha256=8BTvq91SXBtO5PO5SKEBdKcz7JRsKl2o4bzp3VCS8_U,24813
|
|
4
|
+
lumera/exceptions.py,sha256=bNsx4iYaroAAGsYxErfELC2B5ZJ3w5lVa1kKdIx5s9g,2173
|
|
5
|
+
lumera/google.py,sha256=MwgPd3hNNuOj6CKmxJMYTQ7F81a_R4x0Wx19GSiOGoU,1136
|
|
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=EZVH1FnT7smkvFgT6QjVf4gDnVuktF1w7RZFaeTPgDE,30470
|
|
10
|
+
lumera/storage.py,sha256=b0W6JNSGfmhJIcmK3vrATXAwxIr_bfrj-hPuQRVLTYU,8206
|
|
11
|
+
lumera/webhooks.py,sha256=L_Q5YHBJKQNpv7G9Nq0QqlGMRch6x9ptlwu1xD2qwUc,8661
|
|
12
|
+
lumera-0.8.2.dist-info/METADATA,sha256=UJHFpJG3igC8bKiDnEFYom92fzXb0d_zZ7St127aGpc,1611
|
|
13
|
+
lumera-0.8.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
+
lumera-0.8.2.dist-info/top_level.txt,sha256=HgfK4XQkpMTnM2E5iWM4kB711FnYqUY9dglzib3pWlE,7
|
|
15
|
+
lumera-0.8.2.dist-info/RECORD,,
|
lumera-0.8.0.dist-info/RECORD
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
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,,
|
|
File without changes
|
|
File without changes
|