lumera 0.4.6__py3-none-any.whl → 0.9.6__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 +99 -4
- lumera/_utils.py +782 -0
- lumera/automations.py +904 -0
- lumera/exceptions.py +72 -0
- lumera/files.py +97 -0
- lumera/google.py +47 -270
- lumera/integrations/__init__.py +34 -0
- lumera/integrations/google.py +338 -0
- lumera/llm.py +481 -0
- lumera/locks.py +216 -0
- lumera/pb.py +679 -0
- lumera/sdk.py +927 -380
- lumera/storage.py +270 -0
- lumera/webhooks.py +304 -0
- lumera-0.9.6.dist-info/METADATA +37 -0
- lumera-0.9.6.dist-info/RECORD +18 -0
- {lumera-0.4.6.dist-info → lumera-0.9.6.dist-info}/WHEEL +1 -1
- lumera-0.4.6.dist-info/METADATA +0 -11
- lumera-0.4.6.dist-info/RECORD +0 -7
- {lumera-0.4.6.dist-info → lumera-0.9.6.dist-info}/top_level.txt +0 -0
lumera/storage.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Storage operations for uploading and managing files.
|
|
3
|
+
|
|
4
|
+
Files are automatically namespaced by the current automation run:
|
|
5
|
+
/agent_runs/{run_id}/{path}
|
|
6
|
+
|
|
7
|
+
This prevents collisions between different runs while allowing logical
|
|
8
|
+
organization within a run.
|
|
9
|
+
|
|
10
|
+
Available functions:
|
|
11
|
+
upload() - Upload bytes/string content to storage
|
|
12
|
+
upload_file() - Upload a local file to storage
|
|
13
|
+
download_url() - Get download URL for an uploaded file
|
|
14
|
+
list_files() - List files uploaded in current run
|
|
15
|
+
|
|
16
|
+
Environment requirements:
|
|
17
|
+
LUMERA_RUN_ID - Set automatically when running in automation context
|
|
18
|
+
LUMERA_TOKEN - API authentication token
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
>>> from lumera import storage
|
|
22
|
+
>>> result = storage.upload("exports/report.csv", csv_data, content_type="text/csv")
|
|
23
|
+
>>> print(result["url"])
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
__all__ = ["upload", "upload_file", "download_url", "list_files", "UploadResult"]
|
|
27
|
+
|
|
28
|
+
import mimetypes
|
|
29
|
+
import os
|
|
30
|
+
import pathlib
|
|
31
|
+
from typing import Any, Required, TypedDict
|
|
32
|
+
|
|
33
|
+
import requests
|
|
34
|
+
|
|
35
|
+
from ._utils import API_BASE, get_lumera_token
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class UploadResult(TypedDict, total=False):
|
|
39
|
+
"""Result of file upload.
|
|
40
|
+
|
|
41
|
+
Required fields (always present):
|
|
42
|
+
url: Public download URL
|
|
43
|
+
path: Relative path within run namespace
|
|
44
|
+
size: File size in bytes
|
|
45
|
+
content_type: MIME type
|
|
46
|
+
|
|
47
|
+
Optional fields:
|
|
48
|
+
object_key: Storage object key (platform implementation detail)
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
url: Required[str] # Public download URL (always present)
|
|
52
|
+
path: Required[str] # Relative path within run namespace (always present)
|
|
53
|
+
size: Required[int] # File size in bytes (always present)
|
|
54
|
+
content_type: Required[str] # MIME type (always present)
|
|
55
|
+
object_key: str # Storage object key (optional, platform detail)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def upload(
|
|
59
|
+
path: str,
|
|
60
|
+
content: bytes | str,
|
|
61
|
+
*,
|
|
62
|
+
content_type: str,
|
|
63
|
+
metadata: dict[str, Any] | None = None, # noqa: ARG001 - Reserved for future use
|
|
64
|
+
) -> UploadResult:
|
|
65
|
+
"""Upload content to storage.
|
|
66
|
+
|
|
67
|
+
Files are automatically namespaced by the current automation run.
|
|
68
|
+
Storage location: /agent_runs/{run_id}/{path}
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
path: Relative path within this run's namespace (e.g., "exports/daily.csv")
|
|
72
|
+
Can include subfolders for organization
|
|
73
|
+
content: File content as bytes or string (strings will be encoded as UTF-8)
|
|
74
|
+
content_type: MIME type (e.g., "text/csv", "application/json")
|
|
75
|
+
metadata: Optional searchable metadata (reserved for future use)
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Upload result with URL and metadata
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
ValueError: If path or content_type is empty
|
|
82
|
+
RuntimeError: If LUMERA_RUN_ID environment variable is not set
|
|
83
|
+
requests.HTTPError: If upload fails
|
|
84
|
+
|
|
85
|
+
Example:
|
|
86
|
+
>>> result = storage.upload(
|
|
87
|
+
... path="exports/report.csv",
|
|
88
|
+
... content=csv_data.encode("utf-8"),
|
|
89
|
+
... content_type="text/csv"
|
|
90
|
+
... )
|
|
91
|
+
>>> print(result["url"])
|
|
92
|
+
https://storage.lumerahq.com/download/abc123
|
|
93
|
+
"""
|
|
94
|
+
if not path or not path.strip():
|
|
95
|
+
raise ValueError("path is required and cannot be empty")
|
|
96
|
+
if not content_type or not content_type.strip():
|
|
97
|
+
raise ValueError("content_type is required and cannot be empty")
|
|
98
|
+
|
|
99
|
+
# Get current run ID from environment
|
|
100
|
+
run_id = os.getenv("LUMERA_RUN_ID", "").strip()
|
|
101
|
+
if not run_id:
|
|
102
|
+
raise RuntimeError(
|
|
103
|
+
"LUMERA_RUN_ID environment variable not set. "
|
|
104
|
+
"This function must be called from within an automation run."
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Convert string content to bytes
|
|
108
|
+
if isinstance(content, str):
|
|
109
|
+
content = content.encode("utf-8")
|
|
110
|
+
|
|
111
|
+
filename = os.path.basename(path)
|
|
112
|
+
size = len(content)
|
|
113
|
+
|
|
114
|
+
token = get_lumera_token()
|
|
115
|
+
headers = {"Authorization": f"token {token}", "Content-Type": "application/json"}
|
|
116
|
+
|
|
117
|
+
# Request presigned upload URL
|
|
118
|
+
resp = requests.post(
|
|
119
|
+
f"{API_BASE}/automation-runs/{run_id}/files/upload-url",
|
|
120
|
+
json={"filename": filename, "content_type": content_type, "size": size},
|
|
121
|
+
headers=headers,
|
|
122
|
+
timeout=30,
|
|
123
|
+
)
|
|
124
|
+
resp.raise_for_status()
|
|
125
|
+
data = resp.json()
|
|
126
|
+
|
|
127
|
+
upload_url = data["upload_url"]
|
|
128
|
+
|
|
129
|
+
# Upload content to presigned URL
|
|
130
|
+
put_resp = requests.put(
|
|
131
|
+
upload_url, data=content, headers={"Content-Type": content_type}, timeout=300
|
|
132
|
+
)
|
|
133
|
+
put_resp.raise_for_status()
|
|
134
|
+
|
|
135
|
+
# Construct result
|
|
136
|
+
result: UploadResult = {
|
|
137
|
+
"url": data.get("download_url", ""),
|
|
138
|
+
"object_key": data.get("object_name", ""),
|
|
139
|
+
"path": path,
|
|
140
|
+
"size": size,
|
|
141
|
+
"content_type": content_type,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return result
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def upload_file(
|
|
148
|
+
path: str,
|
|
149
|
+
file_path: str,
|
|
150
|
+
*,
|
|
151
|
+
content_type: str | None = None,
|
|
152
|
+
metadata: dict[str, Any] | None = None,
|
|
153
|
+
) -> UploadResult:
|
|
154
|
+
"""Upload a file from disk.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
path: Relative path in run namespace (e.g., "exports/report.pdf")
|
|
158
|
+
file_path: Local file path to upload
|
|
159
|
+
content_type: MIME type (auto-detected from extension if not provided)
|
|
160
|
+
metadata: Optional searchable metadata (reserved for future use)
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Upload result with URL and metadata
|
|
164
|
+
|
|
165
|
+
Raises:
|
|
166
|
+
FileNotFoundError: If file_path doesn't exist
|
|
167
|
+
ValueError: If path is empty
|
|
168
|
+
|
|
169
|
+
Example:
|
|
170
|
+
>>> result = storage.upload_file(
|
|
171
|
+
... path="exports/report.pdf",
|
|
172
|
+
... file_path="/tmp/generated_report.pdf"
|
|
173
|
+
... )
|
|
174
|
+
"""
|
|
175
|
+
file_path_obj = pathlib.Path(file_path).expanduser().resolve()
|
|
176
|
+
if not file_path_obj.is_file():
|
|
177
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
178
|
+
|
|
179
|
+
# Auto-detect content type if not provided
|
|
180
|
+
if not content_type:
|
|
181
|
+
content_type = mimetypes.guess_type(file_path)[0] or "application/octet-stream"
|
|
182
|
+
|
|
183
|
+
# Read file content
|
|
184
|
+
with open(file_path_obj, "rb") as f:
|
|
185
|
+
content = f.read()
|
|
186
|
+
|
|
187
|
+
return upload(path, content, content_type=content_type, metadata=metadata)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def download_url(path: str) -> str:
|
|
191
|
+
"""Get download URL for a file uploaded in this run.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
path: Relative path within this run (same as used in upload)
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Download URL
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
RuntimeError: If LUMERA_RUN_ID not set
|
|
201
|
+
requests.HTTPError: If file doesn't exist
|
|
202
|
+
|
|
203
|
+
Example:
|
|
204
|
+
>>> url = storage.download_url("exports/report.csv")
|
|
205
|
+
"""
|
|
206
|
+
run_id = os.getenv("LUMERA_RUN_ID", "").strip()
|
|
207
|
+
if not run_id:
|
|
208
|
+
raise RuntimeError(
|
|
209
|
+
"LUMERA_RUN_ID environment variable not set. "
|
|
210
|
+
"This function must be called from within an automation run."
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
filename = os.path.basename(path)
|
|
214
|
+
token = get_lumera_token()
|
|
215
|
+
headers = {"Authorization": f"token {token}"}
|
|
216
|
+
|
|
217
|
+
resp = requests.get(
|
|
218
|
+
f"{API_BASE}/automation-runs/{run_id}/files/download-url",
|
|
219
|
+
params={"name": filename},
|
|
220
|
+
headers=headers,
|
|
221
|
+
timeout=30,
|
|
222
|
+
)
|
|
223
|
+
resp.raise_for_status()
|
|
224
|
+
|
|
225
|
+
data = resp.json()
|
|
226
|
+
return data.get("download_url", "")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def list_files(prefix: str | None = None) -> list[dict[str, Any]]:
|
|
230
|
+
"""List files uploaded in this run.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
prefix: Optional path prefix filter (e.g., "exports/")
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
List of file metadata dicts with keys:
|
|
237
|
+
- name: filename
|
|
238
|
+
- size: size in bytes
|
|
239
|
+
- content_type: MIME type
|
|
240
|
+
- created: creation timestamp
|
|
241
|
+
|
|
242
|
+
Raises:
|
|
243
|
+
RuntimeError: If LUMERA_RUN_ID not set
|
|
244
|
+
|
|
245
|
+
Example:
|
|
246
|
+
>>> files = storage.list_files(prefix="exports/")
|
|
247
|
+
>>> for file in files:
|
|
248
|
+
... print(file["name"], file["size"])
|
|
249
|
+
"""
|
|
250
|
+
run_id = os.getenv("LUMERA_RUN_ID", "").strip()
|
|
251
|
+
if not run_id:
|
|
252
|
+
raise RuntimeError(
|
|
253
|
+
"LUMERA_RUN_ID environment variable not set. "
|
|
254
|
+
"This function must be called from within an automation run."
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
token = get_lumera_token()
|
|
258
|
+
headers = {"Authorization": f"token {token}"}
|
|
259
|
+
|
|
260
|
+
resp = requests.get(f"{API_BASE}/automation-runs/{run_id}/files", headers=headers, timeout=30)
|
|
261
|
+
resp.raise_for_status()
|
|
262
|
+
|
|
263
|
+
data = resp.json()
|
|
264
|
+
files = data.get("files", [])
|
|
265
|
+
|
|
266
|
+
# Filter by prefix if provided
|
|
267
|
+
if prefix:
|
|
268
|
+
files = [f for f in files if f.get("name", "").startswith(prefix)]
|
|
269
|
+
|
|
270
|
+
return files
|
lumera/webhooks.py
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
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/rec_abc123/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(
|
|
175
|
+
404, "endpoint not found", url=f"collections/{_COLLECTION}/records", payload=None
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
items = result.get("items", [])
|
|
179
|
+
if not items:
|
|
180
|
+
raise LumeraAPIError(
|
|
181
|
+
404,
|
|
182
|
+
f"webhook endpoint '{external_id}' not found",
|
|
183
|
+
url=f"collections/{_COLLECTION}/records",
|
|
184
|
+
payload=None,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
return items[0]
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def update(
|
|
191
|
+
external_id: str,
|
|
192
|
+
*,
|
|
193
|
+
name: str | None = None,
|
|
194
|
+
description: str | None = None,
|
|
195
|
+
) -> dict[str, Any]:
|
|
196
|
+
"""Update a webhook endpoint.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
external_id: The external_id of the endpoint to update
|
|
200
|
+
name: New name (optional)
|
|
201
|
+
description: New description (optional)
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Updated endpoint record
|
|
205
|
+
|
|
206
|
+
Raises:
|
|
207
|
+
ValueError: If external_id is empty or no fields to update
|
|
208
|
+
LumeraAPIError: If endpoint not found
|
|
209
|
+
|
|
210
|
+
Example:
|
|
211
|
+
>>> endpoint = webhooks.update(
|
|
212
|
+
... "stripe-events",
|
|
213
|
+
... description="Updated: Now includes refund events"
|
|
214
|
+
... )
|
|
215
|
+
"""
|
|
216
|
+
external_id = (external_id or "").strip()
|
|
217
|
+
if not external_id:
|
|
218
|
+
raise ValueError("external_id is required")
|
|
219
|
+
|
|
220
|
+
# First, find the endpoint to get its record ID
|
|
221
|
+
endpoint = get(external_id)
|
|
222
|
+
record_id = endpoint["id"]
|
|
223
|
+
|
|
224
|
+
payload: dict[str, Any] = {}
|
|
225
|
+
if name is not None:
|
|
226
|
+
payload["name"] = name.strip()
|
|
227
|
+
if description is not None:
|
|
228
|
+
payload["description"] = description.strip()
|
|
229
|
+
|
|
230
|
+
if not payload:
|
|
231
|
+
raise ValueError("at least one field (name or description) must be provided")
|
|
232
|
+
|
|
233
|
+
result = _api_request(
|
|
234
|
+
"PATCH", f"collections/{_COLLECTION}/records/{record_id}", json_body=payload
|
|
235
|
+
)
|
|
236
|
+
if not isinstance(result, dict):
|
|
237
|
+
raise RuntimeError("unexpected response payload")
|
|
238
|
+
return result
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def delete(external_id: str) -> None:
|
|
242
|
+
"""Delete a webhook endpoint.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
external_id: The external_id of the endpoint to delete
|
|
246
|
+
|
|
247
|
+
Raises:
|
|
248
|
+
ValueError: If external_id is empty
|
|
249
|
+
LumeraAPIError: If endpoint not found
|
|
250
|
+
|
|
251
|
+
Example:
|
|
252
|
+
>>> webhooks.delete("old-stripe-endpoint")
|
|
253
|
+
"""
|
|
254
|
+
external_id = (external_id or "").strip()
|
|
255
|
+
if not external_id:
|
|
256
|
+
raise ValueError("external_id is required")
|
|
257
|
+
|
|
258
|
+
# First, find the endpoint to get its record ID
|
|
259
|
+
endpoint = get(external_id)
|
|
260
|
+
record_id = endpoint["id"]
|
|
261
|
+
|
|
262
|
+
_api_request("DELETE", f"collections/{_COLLECTION}/records/{record_id}")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def url(external_id: str) -> str:
|
|
266
|
+
"""Get the public webhook URL for an endpoint.
|
|
267
|
+
|
|
268
|
+
This returns the full URL that external services should send webhooks to.
|
|
269
|
+
The URL format is: https://{base}/webhooks/{company_id}/{external_id}
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
external_id: The external_id of the endpoint
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Full public webhook URL
|
|
276
|
+
|
|
277
|
+
Raises:
|
|
278
|
+
ValueError: If external_id is empty
|
|
279
|
+
RuntimeError: If COMPANY_ID environment variable is not set
|
|
280
|
+
|
|
281
|
+
Example:
|
|
282
|
+
>>> url = webhooks.url("stripe-events")
|
|
283
|
+
>>> print(url)
|
|
284
|
+
https://app.lumerahq.com/webhooks/rec_abc123/stripe-events
|
|
285
|
+
|
|
286
|
+
# Use this URL when configuring webhooks in Stripe, GitHub, etc.
|
|
287
|
+
"""
|
|
288
|
+
external_id = (external_id or "").strip()
|
|
289
|
+
if not external_id:
|
|
290
|
+
raise ValueError("external_id is required")
|
|
291
|
+
|
|
292
|
+
company_id = os.getenv("COMPANY_ID", "").strip()
|
|
293
|
+
if not company_id:
|
|
294
|
+
raise RuntimeError(
|
|
295
|
+
"COMPANY_ID environment variable not set. "
|
|
296
|
+
"This is required to construct the webhook URL."
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# API_BASE is like "https://app.lumerahq.com/api" - we need the base without /api
|
|
300
|
+
base_url = API_BASE.rstrip("/")
|
|
301
|
+
if base_url.endswith("/api"):
|
|
302
|
+
base_url = base_url[:-4]
|
|
303
|
+
|
|
304
|
+
return f"{base_url}/webhooks/{company_id}/{external_id}"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lumera
|
|
3
|
+
Version: 0.9.6
|
|
4
|
+
Summary: SDK for building on Lumera platform
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: requests
|
|
7
|
+
Requires-Dist: python-dotenv
|
|
8
|
+
Requires-Dist: google-api-python-client==2.173.0
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: ruff; extra == "dev"
|
|
11
|
+
Requires-Dist: pytest; extra == "dev"
|
|
12
|
+
Provides-Extra: full
|
|
13
|
+
Requires-Dist: airtable-python-wrapper==0.15.3; extra == "full"
|
|
14
|
+
Requires-Dist: boto3==1.39.3; extra == "full"
|
|
15
|
+
Requires-Dist: google-api-python-client==2.173.0; extra == "full"
|
|
16
|
+
Requires-Dist: gspread==6.2.1; extra == "full"
|
|
17
|
+
Requires-Dist: hubspot-api-client==12.0.0; extra == "full"
|
|
18
|
+
Requires-Dist: ipykernel; extra == "full"
|
|
19
|
+
Requires-Dist: jupyter-client; extra == "full"
|
|
20
|
+
Requires-Dist: matplotlib==3.10.3; extra == "full"
|
|
21
|
+
Requires-Dist: notion-client==2.4.0; extra == "full"
|
|
22
|
+
Requires-Dist: numpy==2.3.0; extra == "full"
|
|
23
|
+
Requires-Dist: office365-rest-python-client; extra == "full"
|
|
24
|
+
Requires-Dist: openai<3.0.0,>=2.15.0; extra == "full"
|
|
25
|
+
Requires-Dist: openai-agents<1.0.0,>=0.6.5; extra == "full"
|
|
26
|
+
Requires-Dist: openpyxl==3.1.5; extra == "full"
|
|
27
|
+
Requires-Dist: pandas==2.3.0; extra == "full"
|
|
28
|
+
Requires-Dist: pdfplumber; extra == "full"
|
|
29
|
+
Requires-Dist: pydantic<3.0.0,>=2.12.3; extra == "full"
|
|
30
|
+
Requires-Dist: PyGithub==2.6.1; extra == "full"
|
|
31
|
+
Requires-Dist: python-dotenv==1.1.0; extra == "full"
|
|
32
|
+
Requires-Dist: pyzmq; extra == "full"
|
|
33
|
+
Requires-Dist: simple-salesforce==1.12.6; extra == "full"
|
|
34
|
+
Requires-Dist: slack_sdk==3.35.0; extra == "full"
|
|
35
|
+
Requires-Dist: tabulate; extra == "full"
|
|
36
|
+
Requires-Dist: xlsxwriter==3.2.5; extra == "full"
|
|
37
|
+
Requires-Dist: requests==2.32.4; extra == "full"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
lumera/__init__.py,sha256=nIEzrBMFoW6-xJ5L5CQrq6feDJUH119Vttm-L8Si6aE,2667
|
|
2
|
+
lumera/_utils.py,sha256=Gr3WRaU6MI-jpx3e5OMxAkVYYQmkUXg2J56Sy7AuEGE,24443
|
|
3
|
+
lumera/automations.py,sha256=UKKtgmsYWIzJvMnIf0K5ywXCtgIu8kb9DmsAX_upubM,27397
|
|
4
|
+
lumera/exceptions.py,sha256=bNsx4iYaroAAGsYxErfELC2B5ZJ3w5lVa1kKdIx5s9g,2173
|
|
5
|
+
lumera/files.py,sha256=xMJmLTSaQQDttM3AMmpOWc6soh4lvCCKBreV0fXWHQw,3159
|
|
6
|
+
lumera/google.py,sha256=zpWW1qSlzLZY5Ip7cGAzrv9sJrQf3JBKH2ODc1cCM_E,1130
|
|
7
|
+
lumera/llm.py,sha256=pUTZK7t3GTK0vfxMI1PJgJwNendyuiJc5MB1pUj2vxE,14412
|
|
8
|
+
lumera/locks.py,sha256=8l_qxb8nrxge7YJ-ApUTJ5MeYpIdxDeEa94Eim9O-YM,6806
|
|
9
|
+
lumera/pb.py,sha256=Q_U1cKeB3YgI7bmTquzLYFWTRWcfUZkFSl7JXMBzV7M,20700
|
|
10
|
+
lumera/sdk.py,sha256=Dw0yxlZ-ncjgPkCpVnAJQIURtIsbUA4RVu9VjXLayDc,34078
|
|
11
|
+
lumera/storage.py,sha256=b0W6JNSGfmhJIcmK3vrATXAwxIr_bfrj-hPuQRVLTYU,8206
|
|
12
|
+
lumera/webhooks.py,sha256=L_Q5YHBJKQNpv7G9Nq0QqlGMRch6x9ptlwu1xD2qwUc,8661
|
|
13
|
+
lumera/integrations/__init__.py,sha256=LnJmAnFB_p3YMKyeGVdDP4LYlJ85XFNQFAxGo6zF7CI,937
|
|
14
|
+
lumera/integrations/google.py,sha256=QkbBbbDh3I_OToPDFqcivU6sWy2UieHBxZ_TPv5rqK0,11862
|
|
15
|
+
lumera-0.9.6.dist-info/METADATA,sha256=Aedsk6Mexr9vDSKbAl7ChbH_YM3PTOo1qVmMFmQiBNo,1611
|
|
16
|
+
lumera-0.9.6.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
17
|
+
lumera-0.9.6.dist-info/top_level.txt,sha256=HgfK4XQkpMTnM2E5iWM4kB711FnYqUY9dglzib3pWlE,7
|
|
18
|
+
lumera-0.9.6.dist-info/RECORD,,
|
lumera-0.4.6.dist-info/METADATA
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: lumera
|
|
3
|
-
Version: 0.4.6
|
|
4
|
-
Summary: Lumera Agent SDK for API interaction and dynamic UI generation.
|
|
5
|
-
Requires-Python: >=3.8
|
|
6
|
-
Requires-Dist: requests
|
|
7
|
-
Requires-Dist: python-dotenv
|
|
8
|
-
Requires-Dist: google-api-python-client==2.173.0
|
|
9
|
-
Provides-Extra: dev
|
|
10
|
-
Requires-Dist: ruff; extra == "dev"
|
|
11
|
-
Requires-Dist: pytest; extra == "dev"
|
lumera-0.4.6.dist-info/RECORD
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
lumera/__init__.py,sha256=HiyCIR0Qq1MVxUZVSgcz9xUk_6TZKNAy1cw0v7RlSWg,536
|
|
2
|
-
lumera/google.py,sha256=1_xk1oyy6a6seieYbrHNWS3My06b-th_j2xWHQ9-NKs,10183
|
|
3
|
-
lumera/sdk.py,sha256=LAi7ohXR0D6SlfVH5gMcywa-E4KtN05ZK92RwYcyQdU,14663
|
|
4
|
-
lumera-0.4.6.dist-info/METADATA,sha256=UG1HtYKbhRQe49frc3F52gHOMpQRLevLcOMWdwgQH4o,342
|
|
5
|
-
lumera-0.4.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
-
lumera-0.4.6.dist-info/top_level.txt,sha256=HgfK4XQkpMTnM2E5iWM4kB711FnYqUY9dglzib3pWlE,7
|
|
7
|
-
lumera-0.4.6.dist-info/RECORD,,
|
|
File without changes
|