lumera 0.4.22__py3-none-any.whl → 0.5.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 +35 -5
- lumera/exceptions.py +72 -0
- lumera/llm.py +481 -0
- lumera/locks.py +216 -0
- lumera/pb.py +316 -0
- lumera/sdk.py +31 -2
- lumera/storage.py +269 -0
- {lumera-0.4.22.dist-info → lumera-0.5.0.dist-info}/METADATA +3 -3
- lumera-0.5.0.dist-info/RECORD +13 -0
- lumera-0.4.22.dist-info/RECORD +0 -8
- {lumera-0.4.22.dist-info → lumera-0.5.0.dist-info}/WHEEL +0 -0
- {lumera-0.4.22.dist-info → lumera-0.5.0.dist-info}/top_level.txt +0 -0
lumera/locks.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lock management for preventing concurrent operations.
|
|
3
|
+
|
|
4
|
+
Provides two types of locks:
|
|
5
|
+
1. Record-level locks: Lock specific records (uses platform lm_locks table)
|
|
6
|
+
2. Operation-level locks: Lock entire operations globally (requires custom collection)
|
|
7
|
+
|
|
8
|
+
Available functions:
|
|
9
|
+
claim_record_locks() - Lock specific records for processing
|
|
10
|
+
release_record_locks() - Release previously claimed record locks
|
|
11
|
+
acquire_operation_lock() - Lock an entire operation (NOT YET IMPLEMENTED)
|
|
12
|
+
release_operation_lock() - Release operation lock (NOT YET IMPLEMENTED)
|
|
13
|
+
operation_lock() - Context manager for operation locks (NOT YET IMPLEMENTED)
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
>>> from lumera import locks
|
|
17
|
+
>>> result = locks.claim_record_locks("export", "deposits", ["dep_1", "dep_2"])
|
|
18
|
+
>>> for id in result["claimed"]:
|
|
19
|
+
... process(id)
|
|
20
|
+
>>> locks.release_record_locks("export", record_ids=result["claimed"])
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"claim_record_locks",
|
|
25
|
+
"release_record_locks",
|
|
26
|
+
"acquire_operation_lock",
|
|
27
|
+
"release_operation_lock",
|
|
28
|
+
"operation_lock",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
from contextlib import contextmanager
|
|
32
|
+
from typing import Any, Iterator
|
|
33
|
+
|
|
34
|
+
# Import platform lock primitives from the main SDK module
|
|
35
|
+
from .sdk import claim_locks as _claim_locks
|
|
36
|
+
from .sdk import release_locks as _release_locks
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def claim_record_locks(
|
|
40
|
+
job_type: str,
|
|
41
|
+
collection: str,
|
|
42
|
+
record_ids: list[str],
|
|
43
|
+
*,
|
|
44
|
+
ttl_seconds: int = 900,
|
|
45
|
+
job_id: str | None = None,
|
|
46
|
+
) -> dict[str, Any]:
|
|
47
|
+
"""Claim record-level locks (using platform lm_locks).
|
|
48
|
+
|
|
49
|
+
Prevents multiple workers from processing the same records concurrently.
|
|
50
|
+
Uses the platform's built-in lm_locks table.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
job_type: Workflow name (e.g., "deposit_processing")
|
|
54
|
+
collection: Collection name
|
|
55
|
+
record_ids: List of record IDs to lock
|
|
56
|
+
ttl_seconds: Lock duration in seconds (default 900 = 15 minutes)
|
|
57
|
+
job_id: Optional job identifier for grouping locks
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
{
|
|
61
|
+
"claimed": ["id1", "id2"], # Successfully locked
|
|
62
|
+
"skipped": ["id3"], # Already locked by another process
|
|
63
|
+
"ttl_seconds": 900
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
>>> result = claim_record_locks(
|
|
68
|
+
... job_type="export",
|
|
69
|
+
... collection="deposits",
|
|
70
|
+
... record_ids=["dep_1", "dep_2", "dep_3"]
|
|
71
|
+
... )
|
|
72
|
+
>>> for dep_id in result["claimed"]:
|
|
73
|
+
... process(dep_id)
|
|
74
|
+
>>> # Release when done
|
|
75
|
+
>>> release_record_locks("export", record_ids=result["claimed"])
|
|
76
|
+
"""
|
|
77
|
+
return _claim_locks(
|
|
78
|
+
job_type=job_type,
|
|
79
|
+
collection=collection,
|
|
80
|
+
record_ids=record_ids,
|
|
81
|
+
ttl_seconds=ttl_seconds,
|
|
82
|
+
job_id=job_id,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def release_record_locks(
|
|
87
|
+
job_type: str,
|
|
88
|
+
*,
|
|
89
|
+
collection: str | None = None,
|
|
90
|
+
record_ids: list[str] | None = None,
|
|
91
|
+
job_id: str | None = None,
|
|
92
|
+
) -> int:
|
|
93
|
+
"""Release record-level locks.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
job_type: Workflow name (required)
|
|
97
|
+
collection: Optional collection filter
|
|
98
|
+
record_ids: Optional specific records to release
|
|
99
|
+
job_id: Optional job identifier filter
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Number of locks released
|
|
103
|
+
|
|
104
|
+
Example:
|
|
105
|
+
>>> released = release_record_locks(
|
|
106
|
+
... job_type="export",
|
|
107
|
+
... record_ids=["dep_1", "dep_2"]
|
|
108
|
+
... )
|
|
109
|
+
>>> print(f"Released {released} locks")
|
|
110
|
+
"""
|
|
111
|
+
return _release_locks(
|
|
112
|
+
job_type=job_type, collection=collection, record_ids=record_ids, job_id=job_id
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# Operation-level locks (simple key-value locks)
|
|
117
|
+
# Note: These would need a custom collection like "export_locks" to be implemented
|
|
118
|
+
# For now, providing the interface that should be implemented
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def acquire_operation_lock(
|
|
122
|
+
lock_name: str, *, ttl_seconds: int = 600, wait: bool = False, wait_timeout: int = 30
|
|
123
|
+
) -> bool:
|
|
124
|
+
"""Acquire an operation-level lock.
|
|
125
|
+
|
|
126
|
+
For preventing concurrent execution of entire operations (like exports).
|
|
127
|
+
Uses a simple key-value lock, not tied to specific records.
|
|
128
|
+
|
|
129
|
+
Note: This requires a custom locks collection to be created.
|
|
130
|
+
See the Charter Impact export_locks collection as an example.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
lock_name: Unique lock identifier (e.g., "csv_export")
|
|
134
|
+
ttl_seconds: Lock duration in seconds (default 600 = 10 minutes)
|
|
135
|
+
wait: If True, wait for lock to become available
|
|
136
|
+
wait_timeout: Max seconds to wait (if wait=True)
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
True if lock acquired, False if already held (when wait=False)
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
TimeoutError: If wait=True and timeout exceeded
|
|
143
|
+
NotImplementedError: If operation locks collection doesn't exist
|
|
144
|
+
|
|
145
|
+
Example:
|
|
146
|
+
>>> if acquire_operation_lock("csv_export"):
|
|
147
|
+
... try:
|
|
148
|
+
... perform_export()
|
|
149
|
+
... finally:
|
|
150
|
+
... release_operation_lock("csv_export")
|
|
151
|
+
... else:
|
|
152
|
+
... print("Export already in progress")
|
|
153
|
+
"""
|
|
154
|
+
raise NotImplementedError(
|
|
155
|
+
"Operation-level locks require a custom locks collection. "
|
|
156
|
+
"Create a collection like 'operation_locks' with fields: "
|
|
157
|
+
"lock_name (text, unique), held_by (text), acquired_at (date), expires_at (date). "
|
|
158
|
+
"Then implement acquire/release using pb.search/create/delete."
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def release_operation_lock(lock_name: str) -> bool:
|
|
163
|
+
"""Release an operation-level lock.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
lock_name: Lock identifier
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
True if lock was released, False if wasn't held
|
|
170
|
+
|
|
171
|
+
Raises:
|
|
172
|
+
NotImplementedError: If operation locks collection doesn't exist
|
|
173
|
+
|
|
174
|
+
Example:
|
|
175
|
+
>>> release_operation_lock("csv_export")
|
|
176
|
+
"""
|
|
177
|
+
raise NotImplementedError(
|
|
178
|
+
"Operation-level locks require a custom locks collection. "
|
|
179
|
+
"See acquire_operation_lock() for details."
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@contextmanager
|
|
184
|
+
def operation_lock(
|
|
185
|
+
lock_name: str, *, ttl_seconds: int = 600, wait: bool = False, wait_timeout: int = 30
|
|
186
|
+
) -> Iterator[None]:
|
|
187
|
+
"""Context manager for operation locks.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
lock_name: Lock identifier
|
|
191
|
+
ttl_seconds: Lock duration in seconds
|
|
192
|
+
wait: Wait for lock if held
|
|
193
|
+
wait_timeout: Max wait time in seconds
|
|
194
|
+
|
|
195
|
+
Raises:
|
|
196
|
+
NotImplementedError: If operation locks collection doesn't exist
|
|
197
|
+
TimeoutError: If wait=True and timeout exceeded
|
|
198
|
+
|
|
199
|
+
Example:
|
|
200
|
+
>>> with operation_lock("csv_export"):
|
|
201
|
+
... perform_export()
|
|
202
|
+
... # Lock automatically released on exit
|
|
203
|
+
"""
|
|
204
|
+
# This would acquire the lock
|
|
205
|
+
acquired = acquire_operation_lock(
|
|
206
|
+
lock_name, ttl_seconds=ttl_seconds, wait=wait, wait_timeout=wait_timeout
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
if not acquired:
|
|
210
|
+
raise RuntimeError(f"Failed to acquire lock: {lock_name}")
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
yield
|
|
214
|
+
finally:
|
|
215
|
+
# Always release the lock
|
|
216
|
+
release_operation_lock(lock_name)
|
lumera/pb.py
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Record operations for Lumera collections.
|
|
3
|
+
|
|
4
|
+
This module provides a clean interface for working with Lumera collections,
|
|
5
|
+
using the `pb.` namespace convention familiar from automation contexts.
|
|
6
|
+
|
|
7
|
+
Available functions:
|
|
8
|
+
search() - Query records with filters, pagination, sorting
|
|
9
|
+
get() - Get single record by ID
|
|
10
|
+
get_by_external_id() - Get record by external_id field
|
|
11
|
+
create() - Create new record
|
|
12
|
+
update() - Update existing record
|
|
13
|
+
upsert() - Create or update by external_id
|
|
14
|
+
delete() - Delete record
|
|
15
|
+
iter_all() - Iterate all matching records (auto-pagination)
|
|
16
|
+
|
|
17
|
+
Filter Syntax:
|
|
18
|
+
Filters can be passed as dict or JSON string to search() and iter_all().
|
|
19
|
+
|
|
20
|
+
Simple equality:
|
|
21
|
+
{"status": "pending"}
|
|
22
|
+
{"external_id": "dep-001"}
|
|
23
|
+
|
|
24
|
+
Comparison operators (eq, gt, gte, lt, lte):
|
|
25
|
+
{"amount": {"gt": 1000}}
|
|
26
|
+
{"amount": {"gte": 100, "lte": 500}}
|
|
27
|
+
|
|
28
|
+
OR logic:
|
|
29
|
+
{"or": [{"status": "pending"}, {"status": "review"}]}
|
|
30
|
+
|
|
31
|
+
AND logic (implicit - multiple fields at same level):
|
|
32
|
+
{"status": "pending", "amount": {"gt": 1000}}
|
|
33
|
+
|
|
34
|
+
Combined:
|
|
35
|
+
{"status": "active", "or": [{"priority": "high"}, {"amount": {"gt": 5000}}]}
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
>>> from lumera import pb
|
|
39
|
+
>>> results = pb.search("deposits", filter={"status": "pending"})
|
|
40
|
+
>>> deposit = pb.get("deposits", "rec_abc123")
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from typing import Any, Iterator, Mapping
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"search",
|
|
47
|
+
"get",
|
|
48
|
+
"get_by_external_id",
|
|
49
|
+
"create",
|
|
50
|
+
"update",
|
|
51
|
+
"upsert",
|
|
52
|
+
"delete",
|
|
53
|
+
"iter_all",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
# Import underlying SDK functions (prefixed with _ to indicate internal use)
|
|
57
|
+
from .sdk import (
|
|
58
|
+
create_record as _create_record,
|
|
59
|
+
)
|
|
60
|
+
from .sdk import (
|
|
61
|
+
delete_record as _delete_record,
|
|
62
|
+
)
|
|
63
|
+
from .sdk import (
|
|
64
|
+
get_record as _get_record,
|
|
65
|
+
)
|
|
66
|
+
from .sdk import (
|
|
67
|
+
get_record_by_external_id as _get_record_by_external_id,
|
|
68
|
+
)
|
|
69
|
+
from .sdk import (
|
|
70
|
+
list_records as _list_records,
|
|
71
|
+
)
|
|
72
|
+
from .sdk import (
|
|
73
|
+
update_record as _update_record,
|
|
74
|
+
)
|
|
75
|
+
from .sdk import (
|
|
76
|
+
upsert_record as _upsert_record,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def search(
|
|
81
|
+
collection: str,
|
|
82
|
+
*,
|
|
83
|
+
filter: Mapping[str, Any] | str | None = None,
|
|
84
|
+
per_page: int = 50,
|
|
85
|
+
page: int = 1,
|
|
86
|
+
sort: str | None = None,
|
|
87
|
+
expand: str | None = None,
|
|
88
|
+
) -> dict[str, Any]:
|
|
89
|
+
"""Search records in a collection.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
collection: Collection name or ID
|
|
93
|
+
filter: Filter as dict or JSON string. See module docstring for syntax.
|
|
94
|
+
Examples:
|
|
95
|
+
- {"status": "pending"} - equality
|
|
96
|
+
- {"amount": {"gt": 1000}} - comparison
|
|
97
|
+
- {"or": [{"status": "a"}, {"status": "b"}]} - OR logic
|
|
98
|
+
per_page: Results per page (max 500, default 50)
|
|
99
|
+
page: Page number, 1-indexed (default 1)
|
|
100
|
+
sort: Sort expression (e.g., "-created,name" for created DESC, name ASC)
|
|
101
|
+
expand: Comma-separated relation fields to expand (e.g., "user_id,company_id")
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Paginated results with structure:
|
|
105
|
+
{
|
|
106
|
+
"items": [...], # List of records
|
|
107
|
+
"page": 1,
|
|
108
|
+
"perPage": 50,
|
|
109
|
+
"totalItems": 100,
|
|
110
|
+
"totalPages": 2
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
Example:
|
|
114
|
+
>>> results = pb.search("deposits",
|
|
115
|
+
... filter={"status": "pending", "amount": {"gt": 1000}},
|
|
116
|
+
... per_page=100,
|
|
117
|
+
... sort="-created"
|
|
118
|
+
... )
|
|
119
|
+
>>> for deposit in results["items"]:
|
|
120
|
+
... print(deposit["id"], deposit["amount"])
|
|
121
|
+
"""
|
|
122
|
+
# Note: _list_records handles both dict and str filters via JSON encoding
|
|
123
|
+
return _list_records(
|
|
124
|
+
collection,
|
|
125
|
+
filter=filter,
|
|
126
|
+
per_page=per_page,
|
|
127
|
+
page=page,
|
|
128
|
+
sort=sort,
|
|
129
|
+
expand=expand,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def get(collection: str, record_id: str) -> dict[str, Any]:
|
|
134
|
+
"""Get a single record by ID.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
collection: Collection name or ID
|
|
138
|
+
record_id: Record ID (15-character alphanumeric ID)
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Record data with all fields
|
|
142
|
+
|
|
143
|
+
Raises:
|
|
144
|
+
LumeraAPIError: If record doesn't exist (404)
|
|
145
|
+
|
|
146
|
+
Example:
|
|
147
|
+
>>> deposit = pb.get("deposits", "dep_abc123")
|
|
148
|
+
>>> print(deposit["amount"])
|
|
149
|
+
"""
|
|
150
|
+
return _get_record(collection, record_id)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def get_by_external_id(collection: str, external_id: str) -> dict[str, Any]:
|
|
154
|
+
"""Get a single record by external_id (unique field).
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
collection: Collection name or ID
|
|
158
|
+
external_id: Value of the external_id field (your business identifier)
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Record data
|
|
162
|
+
|
|
163
|
+
Raises:
|
|
164
|
+
LumeraAPIError: If no record with that external_id (404)
|
|
165
|
+
|
|
166
|
+
Example:
|
|
167
|
+
>>> deposit = pb.get_by_external_id("deposits", "dep-2024-001")
|
|
168
|
+
"""
|
|
169
|
+
return _get_record_by_external_id(collection, external_id)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def create(collection: str, data: dict[str, Any]) -> dict[str, Any]:
|
|
173
|
+
"""Create a new record.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
collection: Collection name or ID
|
|
177
|
+
data: Record data as dict mapping field names to values
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Created record with id, created, and updated timestamps
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
LumeraAPIError: If validation fails or unique constraint violated
|
|
184
|
+
|
|
185
|
+
Example:
|
|
186
|
+
>>> deposit = pb.create("deposits", {
|
|
187
|
+
... "external_id": "dep-001",
|
|
188
|
+
... "amount": 1000,
|
|
189
|
+
... "status": "pending"
|
|
190
|
+
... })
|
|
191
|
+
>>> print(deposit["id"])
|
|
192
|
+
"""
|
|
193
|
+
return _create_record(collection, data)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def update(collection: str, record_id: str, data: dict[str, Any]) -> dict[str, Any]:
|
|
197
|
+
"""Update an existing record (partial update).
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
collection: Collection name or ID
|
|
201
|
+
record_id: Record ID to update
|
|
202
|
+
data: Fields to update (only include fields you want to change)
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Updated record
|
|
206
|
+
|
|
207
|
+
Raises:
|
|
208
|
+
LumeraAPIError: If record doesn't exist or validation fails
|
|
209
|
+
|
|
210
|
+
Example:
|
|
211
|
+
>>> deposit = pb.update("deposits", "dep_abc123", {
|
|
212
|
+
... "status": "processed",
|
|
213
|
+
... "processed_at": datetime.utcnow().isoformat()
|
|
214
|
+
... })
|
|
215
|
+
"""
|
|
216
|
+
return _update_record(collection, record_id, data)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def upsert(collection: str, data: dict[str, Any]) -> dict[str, Any]:
|
|
220
|
+
"""Create or update a record by external_id.
|
|
221
|
+
|
|
222
|
+
If a record with the given external_id exists, updates it.
|
|
223
|
+
Otherwise, creates a new record. This is useful for idempotent imports.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
collection: Collection name or ID
|
|
227
|
+
data: Record data (MUST include "external_id" field)
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Created or updated record
|
|
231
|
+
|
|
232
|
+
Raises:
|
|
233
|
+
ValueError: If data doesn't contain "external_id"
|
|
234
|
+
LumeraAPIError: If validation fails
|
|
235
|
+
|
|
236
|
+
Example:
|
|
237
|
+
>>> deposit = pb.upsert("deposits", {
|
|
238
|
+
... "external_id": "dep-001",
|
|
239
|
+
... "amount": 1000,
|
|
240
|
+
... "status": "pending"
|
|
241
|
+
... })
|
|
242
|
+
>>> # Second call updates the existing record
|
|
243
|
+
>>> deposit = pb.upsert("deposits", {
|
|
244
|
+
... "external_id": "dep-001",
|
|
245
|
+
... "amount": 2000
|
|
246
|
+
... })
|
|
247
|
+
"""
|
|
248
|
+
return _upsert_record(collection, data)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def delete(collection: str, record_id: str) -> None:
|
|
252
|
+
"""Delete a record.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
collection: Collection name or ID
|
|
256
|
+
record_id: Record ID to delete
|
|
257
|
+
|
|
258
|
+
Raises:
|
|
259
|
+
LumeraAPIError: If record doesn't exist
|
|
260
|
+
|
|
261
|
+
Example:
|
|
262
|
+
>>> pb.delete("deposits", "dep_abc123")
|
|
263
|
+
"""
|
|
264
|
+
_delete_record(collection, record_id)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def iter_all(
|
|
268
|
+
collection: str,
|
|
269
|
+
*,
|
|
270
|
+
filter: Mapping[str, Any] | str | None = None,
|
|
271
|
+
sort: str | None = None,
|
|
272
|
+
expand: str | None = None,
|
|
273
|
+
batch_size: int = 500,
|
|
274
|
+
) -> Iterator[dict[str, Any]]:
|
|
275
|
+
"""Iterate over all matching records, handling pagination automatically.
|
|
276
|
+
|
|
277
|
+
This is a convenience function for processing large result sets without
|
|
278
|
+
manually handling pagination. Use this instead of manual page loops.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
collection: Collection name or ID
|
|
282
|
+
filter: Optional filter as dict or JSON string (e.g., {"status": "pending"})
|
|
283
|
+
sort: Optional sort expression (e.g., "-created" for newest first)
|
|
284
|
+
expand: Optional comma-separated relation fields to expand
|
|
285
|
+
batch_size: Records per API request (max 500, default 500)
|
|
286
|
+
|
|
287
|
+
Yields:
|
|
288
|
+
Individual records
|
|
289
|
+
|
|
290
|
+
Example:
|
|
291
|
+
>>> for deposit in pb.iter_all("deposits", filter={"status": "pending"}):
|
|
292
|
+
... process(deposit)
|
|
293
|
+
"""
|
|
294
|
+
page = 1
|
|
295
|
+
while True:
|
|
296
|
+
result = search(
|
|
297
|
+
collection,
|
|
298
|
+
filter=filter,
|
|
299
|
+
per_page=batch_size,
|
|
300
|
+
page=page,
|
|
301
|
+
sort=sort,
|
|
302
|
+
expand=expand,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
items = result.get("items", [])
|
|
306
|
+
if not items:
|
|
307
|
+
break
|
|
308
|
+
|
|
309
|
+
yield from items
|
|
310
|
+
|
|
311
|
+
# Check if there are more pages
|
|
312
|
+
total_pages = result.get("totalPages", 0)
|
|
313
|
+
if page >= total_pages:
|
|
314
|
+
break
|
|
315
|
+
|
|
316
|
+
page += 1
|
lumera/sdk.py
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Low-level SDK implementation - prefer high-level modules instead.
|
|
3
|
+
|
|
4
|
+
For most use cases, import from these modules instead of sdk.py:
|
|
5
|
+
|
|
6
|
+
from lumera import pb # Record operations (pb.search, pb.create, pb.update, etc.)
|
|
7
|
+
from lumera import storage # File uploads (storage.upload, storage.upload_file)
|
|
8
|
+
from lumera import llm # LLM completions (llm.complete, llm.chat, llm.embed)
|
|
9
|
+
from lumera import locks # Locking (locks.claim_record_locks, locks.release_record_locks)
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
# Instead of:
|
|
13
|
+
from lumera.sdk import list_records, create_record
|
|
14
|
+
result = list_records("deposits", filter={"status": "pending"})
|
|
15
|
+
|
|
16
|
+
# Use:
|
|
17
|
+
from lumera import pb
|
|
18
|
+
result = pb.search("deposits", filter={"status": "pending"})
|
|
19
|
+
|
|
20
|
+
The functions in this module are used internally by the high-level modules.
|
|
21
|
+
Direct usage is discouraged unless you need low-level control.
|
|
22
|
+
"""
|
|
23
|
+
|
|
1
24
|
import json
|
|
2
25
|
import os
|
|
3
26
|
from typing import Any, Iterable, Mapping, MutableMapping, Sequence, TypedDict
|
|
@@ -233,8 +256,9 @@ def list_records(
|
|
|
233
256
|
offset: int | None = None,
|
|
234
257
|
sort: str | None = None,
|
|
235
258
|
filter: Mapping[str, Any] | Sequence[Any] | None = None,
|
|
259
|
+
expand: str | None = None,
|
|
236
260
|
) -> dict[str, Any]:
|
|
237
|
-
"""List records for the given
|
|
261
|
+
"""List records for the given collection.
|
|
238
262
|
|
|
239
263
|
Args:
|
|
240
264
|
collection_id_or_name: Collection name or ID. Required.
|
|
@@ -244,7 +268,7 @@ def list_records(
|
|
|
244
268
|
limit: Alternative to ``per_page`` for cursor-style queries.
|
|
245
269
|
offset: Starting offset for cursor-style queries.
|
|
246
270
|
sort: Optional sort expression (e.g. ``"-created"``).
|
|
247
|
-
filter: Accepts either a raw
|
|
271
|
+
filter: Accepts either a raw filter string (e.g.
|
|
248
272
|
``"status = 'ok'"``) or a structured filter encoded as a mapping/
|
|
249
273
|
sequence. Structured filters mirror the Page Builder helpers, e.g.:
|
|
250
274
|
|
|
@@ -253,6 +277,9 @@ def list_records(
|
|
|
253
277
|
|
|
254
278
|
The SDK JSON-encodes structured filters so the API can build
|
|
255
279
|
tenant-aware expressions automatically.
|
|
280
|
+
expand: Optional comma-separated list of relation fields to expand.
|
|
281
|
+
Expanded relations are included inline in the record response.
|
|
282
|
+
Example: ``"user_id,company_id"`` or ``"line_items_via_deposit_id"``
|
|
256
283
|
|
|
257
284
|
Returns:
|
|
258
285
|
The raw response from ``GET /collections/{id}/records`` including
|
|
@@ -275,6 +302,8 @@ def list_records(
|
|
|
275
302
|
params["sort"] = sort
|
|
276
303
|
if filter is not None:
|
|
277
304
|
params["filter"] = json.dumps(filter)
|
|
305
|
+
if expand is not None:
|
|
306
|
+
params["expand"] = expand
|
|
278
307
|
|
|
279
308
|
path = f"collections/{collection_id_or_name}/records"
|
|
280
309
|
return _api_request("GET", path, params=params or None)
|