erioon 0.1.5__py3-none-any.whl → 0.1.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.
- erioon/auth.py +34 -0
- erioon/client.py +118 -0
- erioon/collection.py +371 -0
- erioon/create.py +130 -0
- erioon/database.py +82 -0
- erioon/delete.py +342 -0
- erioon/functions.py +422 -0
- erioon/ping.py +53 -0
- erioon/read.py +301 -0
- erioon/transaction.py +58 -0
- erioon/update.py +322 -0
- {erioon-0.1.5.dist-info → erioon-0.1.6.dist-info}/METADATA +1 -1
- erioon-0.1.6.dist-info/RECORD +16 -0
- erioon-0.1.6.dist-info/top_level.txt +1 -0
- erioon-0.1.5.dist-info/RECORD +0 -5
- erioon-0.1.5.dist-info/top_level.txt +0 -1
- {erioon-0.1.5.dist-info → erioon-0.1.6.dist-info}/LICENSE +0 -0
- {erioon-0.1.5.dist-info → erioon-0.1.6.dist-info}/WHEEL +0 -0
erioon/read.py
ADDED
@@ -0,0 +1,301 @@
|
|
1
|
+
# Copyright 2025-present Erioon, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
# Visit www.erioon.com/dev-docs for more information about the python SDK
|
15
|
+
|
16
|
+
import io
|
17
|
+
import msgpack
|
18
|
+
from azure.storage.blob import ContainerClient
|
19
|
+
from erioon.functions import async_log
|
20
|
+
|
21
|
+
# GET ALL RECORDS OF A COLLECTION
|
22
|
+
def handle_get_all(user_id, db_id, coll_id, limit, container_url):
|
23
|
+
"""
|
24
|
+
Retrieves up to a specified number of records from a collection stored in Blob Storage
|
25
|
+
and logs the operation status asynchronously.
|
26
|
+
|
27
|
+
Parameters:
|
28
|
+
- user_id (str): Identifier of the user making the request.
|
29
|
+
- db_id (str): Database identifier (used as the directory prefix).
|
30
|
+
- coll_id (str): Collection identifier (subdirectory under the database).
|
31
|
+
- limit (int): Maximum number of records to retrieve (must not exceed 1,000,000).
|
32
|
+
- container_url (str): URL to the Blob Storage container.
|
33
|
+
|
34
|
+
Behavior:
|
35
|
+
- Scans all blobs in the specified collection path (`db_id/coll_id/`).
|
36
|
+
- Reads shard files, each containing a list of records.
|
37
|
+
- Skips duplicate records by checking their `_id`.
|
38
|
+
- Stops reading once the record limit is reached.
|
39
|
+
- Skips empty or non-conforming blobs gracefully.
|
40
|
+
|
41
|
+
Returns:
|
42
|
+
- tuple(dict, int): A tuple containing:
|
43
|
+
- A status dictionary with:
|
44
|
+
- "status": "OK" or "KO"
|
45
|
+
- "count": number of records returned (0 if none)
|
46
|
+
- "results": list of records (only for successful responses)
|
47
|
+
- "error": error message (on failure)
|
48
|
+
- HTTP status code:
|
49
|
+
- 200 if data is successfully returned.
|
50
|
+
- 404 if collection is missing or no data found.
|
51
|
+
- 500 on unexpected errors.
|
52
|
+
"""
|
53
|
+
if limit > 1_000_000:
|
54
|
+
async_log(user_id, db_id, coll_id, "GET", "ERROR", "Limit of 1,000,000 exceeded", 1, container_url)
|
55
|
+
return {"status": "KO", "count": 0, "error": "Limit of 1,000,000 exceeded"}, 404
|
56
|
+
|
57
|
+
directory_path = f"{db_id}/{coll_id}/"
|
58
|
+
container_client = ContainerClient.from_container_url(container_url)
|
59
|
+
|
60
|
+
blob_list = container_client.list_blobs(name_starts_with=directory_path)
|
61
|
+
blob_names = [blob.name for blob in blob_list]
|
62
|
+
|
63
|
+
if not blob_names:
|
64
|
+
async_log(user_id, db_id, coll_id, "GET", "ERROR", f"No collection {coll_id} found.", 1, container_url)
|
65
|
+
return {"status": "KO", "count": 0, "error": f"No collection {coll_id} found."}, 404
|
66
|
+
|
67
|
+
results = []
|
68
|
+
seen_ids = set()
|
69
|
+
|
70
|
+
for blob in blob_names:
|
71
|
+
try:
|
72
|
+
if blob.endswith(".msgpack"):
|
73
|
+
blob_client = container_client.get_blob_client(blob)
|
74
|
+
msgpack_data = blob_client.download_blob().readall()
|
75
|
+
|
76
|
+
if not msgpack_data:
|
77
|
+
continue
|
78
|
+
|
79
|
+
with io.BytesIO(msgpack_data) as buffer:
|
80
|
+
unpacked_data = msgpack.unpackb(buffer.read(), raw=False)
|
81
|
+
if isinstance(unpacked_data, list):
|
82
|
+
for record in unpacked_data:
|
83
|
+
if record["_id"] in seen_ids:
|
84
|
+
continue
|
85
|
+
|
86
|
+
results.append(record)
|
87
|
+
seen_ids.add(record["_id"])
|
88
|
+
|
89
|
+
if len(results) >= limit:
|
90
|
+
async_log(user_id, db_id, coll_id, "GET", "SUCCESS", f"OK", len(results), container_url)
|
91
|
+
return {"status": "OK", "count": len(results), "results": results}, 200
|
92
|
+
|
93
|
+
except Exception:
|
94
|
+
continue
|
95
|
+
|
96
|
+
if results:
|
97
|
+
async_log(user_id, db_id, coll_id, "GET", "SUCCESS", f"OK", len(results), container_url)
|
98
|
+
return {"status": "OK", "count": len(results), "results": results}, 200
|
99
|
+
|
100
|
+
async_log(user_id, db_id, coll_id, "GET", "ERROR", "No data found", 1, container_url)
|
101
|
+
return {"status": "KO", "count": 0, "error": "No data found"}, 404
|
102
|
+
|
103
|
+
# FIND ONE RECORD
|
104
|
+
def handle_find_one(user_id, db_id, coll_id, search_criteria, container_url):
|
105
|
+
"""
|
106
|
+
Search for a single record matching all given criteria in a collection stored in Blob Storage.
|
107
|
+
|
108
|
+
The function loads each MsgPack blob under `{db_id}/{coll_id}/` and iterates records to find
|
109
|
+
the first one where all key-value criteria match, including nested keys using dot notation.
|
110
|
+
It logs the operation result asynchronously.
|
111
|
+
|
112
|
+
Args:
|
113
|
+
user_id (str): ID of the user making the request.
|
114
|
+
db_id (str): Identifier for the database.
|
115
|
+
coll_id (str): Identifier for the collection.
|
116
|
+
search_criteria (list[dict]): List of key-value dicts representing the search filters.
|
117
|
+
Nested keys are supported via dot notation (e.g., "address.city").
|
118
|
+
container_url (str): Blob Storage container URL.
|
119
|
+
|
120
|
+
Returns:
|
121
|
+
tuple(dict, int): A tuple containing:
|
122
|
+
- A dictionary with keys:
|
123
|
+
- "status" (str): "OK" if a record is found, "KO" if not.
|
124
|
+
- "record" (dict): The found record if successful.
|
125
|
+
- "error" (str): Error message if not found.
|
126
|
+
- HTTP status code (int): 200 if found, 404 if no matching record or collection.
|
127
|
+
"""
|
128
|
+
|
129
|
+
directory_path = f"{db_id}/{coll_id}/"
|
130
|
+
container_client = ContainerClient.from_container_url(container_url)
|
131
|
+
blob_list = container_client.list_blobs(name_starts_with=directory_path)
|
132
|
+
blob_names = [blob.name for blob in blob_list if blob.name.endswith(".msgpack")]
|
133
|
+
|
134
|
+
if not blob_names:
|
135
|
+
async_log(user_id, db_id, coll_id, "GET_ONE", "ERROR", f"No collection {coll_id} found.", 1, container_url)
|
136
|
+
return {"status": "KO", "error": f"No collection {coll_id} found."}, 404
|
137
|
+
|
138
|
+
for blob_name in blob_names:
|
139
|
+
try:
|
140
|
+
blob_client = container_client.get_blob_client(blob_name)
|
141
|
+
msgpack_data = blob_client.download_blob().readall()
|
142
|
+
if not msgpack_data:
|
143
|
+
continue
|
144
|
+
|
145
|
+
records = msgpack.unpackb(msgpack_data, raw=False)
|
146
|
+
|
147
|
+
for record in records:
|
148
|
+
matched_all = True
|
149
|
+
for criteria in search_criteria:
|
150
|
+
key, value = list(criteria.items())[0]
|
151
|
+
current = record
|
152
|
+
for part in key.split("."):
|
153
|
+
if isinstance(current, dict) and part in current:
|
154
|
+
current = current[part]
|
155
|
+
else:
|
156
|
+
matched_all = False
|
157
|
+
break
|
158
|
+
if current != value:
|
159
|
+
matched_all = False
|
160
|
+
break
|
161
|
+
|
162
|
+
if matched_all:
|
163
|
+
async_log(user_id, db_id, coll_id, "GET_ONE", "SUCCESS", "Found one record", 1, container_url)
|
164
|
+
return {"status": "OK", "record": record}, 200
|
165
|
+
|
166
|
+
except Exception:
|
167
|
+
continue
|
168
|
+
|
169
|
+
async_log(user_id, db_id, coll_id, "GET_ONE", "ERROR", "No matching record found", 1, container_url)
|
170
|
+
return {"status": "KO", "error": "No matching record found"}, 404
|
171
|
+
|
172
|
+
# FIND MULTIPLE RECORDS
|
173
|
+
def handle_find_many(user_id, db_id, coll_id, search_criteria, limit, container_url):
|
174
|
+
"""
|
175
|
+
Search for multiple records matching all given criteria in a collection stored as MsgPack blobs.
|
176
|
+
|
177
|
+
The function scans all MsgPack blobs under `{db_id}/{coll_id}/` and collects unique records
|
178
|
+
that match all provided search criteria, supporting nested keys with dot notation.
|
179
|
+
It returns up to `limit` records and logs the operation asynchronously.
|
180
|
+
|
181
|
+
Args:
|
182
|
+
user_id (str): ID of the user making the request.
|
183
|
+
db_id (str): Identifier for the database.
|
184
|
+
coll_id (str): Identifier for the collection.
|
185
|
+
search_criteria (list[dict]): List of key-value dicts for filtering records.
|
186
|
+
limit (int): Maximum number of matching records to return.
|
187
|
+
container_url (str): Blob Storage container URL.
|
188
|
+
|
189
|
+
Returns:
|
190
|
+
tuple(dict, int): A tuple containing:
|
191
|
+
- A dictionary with keys:
|
192
|
+
- "status" (str): "OK" if matching records found, "KO" otherwise.
|
193
|
+
- "count" (int): Number of records returned.
|
194
|
+
- "results" (list[dict]): List of matching records if successful.
|
195
|
+
- "error" (str): Error message if none found.
|
196
|
+
- HTTP status code (int): 200 if matches found, 404 if none or collection missing.
|
197
|
+
"""
|
198
|
+
|
199
|
+
directory_path = f"{db_id}/{coll_id}/"
|
200
|
+
container_client = ContainerClient.from_container_url(container_url)
|
201
|
+
blob_list = container_client.list_blobs(name_starts_with=directory_path)
|
202
|
+
blob_names = [blob.name for blob in blob_list if blob.name.endswith(".msgpack")]
|
203
|
+
|
204
|
+
if not blob_names:
|
205
|
+
async_log(user_id, db_id, coll_id, "GET_MANY", "ERROR", f"No collection {coll_id} found.", 1, container_url)
|
206
|
+
return {"status": "KO", "count": 0, "error": f"No collection {coll_id} found."}, 404
|
207
|
+
|
208
|
+
results = []
|
209
|
+
seen_ids = set()
|
210
|
+
|
211
|
+
for blob_name in blob_names:
|
212
|
+
try:
|
213
|
+
blob_client = container_client.get_blob_client(blob_name)
|
214
|
+
msgpack_data = blob_client.download_blob().readall()
|
215
|
+
if not msgpack_data:
|
216
|
+
continue
|
217
|
+
|
218
|
+
records = msgpack.unpackb(msgpack_data, raw=False)
|
219
|
+
|
220
|
+
for record in records:
|
221
|
+
if record.get("_id") in seen_ids:
|
222
|
+
continue
|
223
|
+
|
224
|
+
matched_all = True
|
225
|
+
for criteria in search_criteria:
|
226
|
+
key, value = list(criteria.items())[0]
|
227
|
+
current = record
|
228
|
+
for part in key.split("."):
|
229
|
+
if isinstance(current, dict) and part in current:
|
230
|
+
current = current[part]
|
231
|
+
else:
|
232
|
+
matched_all = False
|
233
|
+
break
|
234
|
+
if current != value:
|
235
|
+
matched_all = False
|
236
|
+
break
|
237
|
+
|
238
|
+
if matched_all:
|
239
|
+
results.append(record)
|
240
|
+
seen_ids.add(record.get("_id"))
|
241
|
+
if len(results) >= limit:
|
242
|
+
async_log(user_id, db_id, coll_id, "GET_MANY", "SUCCESS", "OK", len(results), container_url)
|
243
|
+
return {"status": "OK", "count": len(results), "results": results}, 200
|
244
|
+
|
245
|
+
except Exception:
|
246
|
+
continue
|
247
|
+
|
248
|
+
if results:
|
249
|
+
async_log(user_id, db_id, coll_id, "GET_MANY", "SUCCESS", "OK", len(results), container_url)
|
250
|
+
return {"status": "OK", "count": len(results), "results": results}, 200
|
251
|
+
|
252
|
+
async_log(user_id, db_id, coll_id, "GET_MANY", "ERROR", "No matching records found", 1, container_url)
|
253
|
+
return {"status": "KO", "count": 0, "error": "No matching records found"}, 404
|
254
|
+
|
255
|
+
# COUNT ALL THE RECORDS
|
256
|
+
def handle_count_records(user_id, db_id, coll_id, container_url):
|
257
|
+
"""
|
258
|
+
Count all records across all MsgPack blobs in a specified collection within Blob Storage.
|
259
|
+
|
260
|
+
The function aggregates the total number of records found in every `.msgpack` blob under the
|
261
|
+
directory `{db_id}/{coll_id}/` and logs the count asynchronously.
|
262
|
+
|
263
|
+
Args:
|
264
|
+
user_id (str): ID of the user making the request.
|
265
|
+
db_id (str): Identifier for the database.
|
266
|
+
coll_id (str): Identifier for the collection.
|
267
|
+
container_url (str): Blob Storage container URL.
|
268
|
+
|
269
|
+
Returns:
|
270
|
+
tuple(int, int): A tuple containing:
|
271
|
+
- The total count of records (int).
|
272
|
+
- HTTP status code (int): 200 if count obtained successfully, 404 if collection not found.
|
273
|
+
"""
|
274
|
+
directory_path = f"{db_id}/{coll_id}/"
|
275
|
+
container_client = ContainerClient.from_container_url(container_url)
|
276
|
+
blob_list = container_client.list_blobs(name_starts_with=directory_path)
|
277
|
+
blob_names = [blob.name for blob in blob_list if blob.name.endswith(".msgpack")]
|
278
|
+
|
279
|
+
if not blob_names:
|
280
|
+
async_log(user_id, db_id, coll_id, "COUNT", "ERROR", f"No collection {coll_id} found.", 0, container_url)
|
281
|
+
return 0, 404
|
282
|
+
|
283
|
+
total_count = 0
|
284
|
+
|
285
|
+
for blob_name in blob_names:
|
286
|
+
try:
|
287
|
+
blob_client = container_client.get_blob_client(blob_name)
|
288
|
+
msgpack_data = blob_client.download_blob().readall()
|
289
|
+
|
290
|
+
if not msgpack_data:
|
291
|
+
continue
|
292
|
+
|
293
|
+
records = msgpack.unpackb(msgpack_data, raw=False)
|
294
|
+
if isinstance(records, list):
|
295
|
+
total_count += len(records)
|
296
|
+
|
297
|
+
except Exception:
|
298
|
+
continue
|
299
|
+
|
300
|
+
async_log(user_id, db_id, coll_id, "COUNT", "SUCCESS", f"Total docs: {total_count}", total_count, container_url)
|
301
|
+
return total_count, 200
|
erioon/transaction.py
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
# Copyright 2025-present Erioon, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
# Visit www.erioon.com/dev-docs for more information about the python SDK
|
15
|
+
|
16
|
+
import uuid
|
17
|
+
from erioon.functions import get_index_data, create_msgpack_file, calculate_shard_number, async_log, update_index_file_insert
|
18
|
+
|
19
|
+
class Transaction:
|
20
|
+
def __init__(self, user_id_cont, database, collection, container_url):
|
21
|
+
self.user_id_cont = user_id_cont
|
22
|
+
self.database = database
|
23
|
+
self.collection = collection
|
24
|
+
self.container_url = container_url
|
25
|
+
self.staged_records = []
|
26
|
+
|
27
|
+
def insert_one(self, record):
|
28
|
+
if "_id" not in record or not record["_id"]:
|
29
|
+
record["_id"] = str(uuid.uuid4())
|
30
|
+
self.staged_records.append(record)
|
31
|
+
|
32
|
+
def commit(self):
|
33
|
+
# Check duplicates for all staged records
|
34
|
+
index_data = get_index_data(self.user_id_cont, self.database, self.collection, self.container_url)
|
35
|
+
existing_ids = set()
|
36
|
+
for shard in index_data:
|
37
|
+
for ids in shard.values():
|
38
|
+
existing_ids.update(ids)
|
39
|
+
|
40
|
+
# Assign new IDs if duplicates found
|
41
|
+
for record in self.staged_records:
|
42
|
+
if record["_id"] in existing_ids:
|
43
|
+
new_id = str(uuid.uuid4())
|
44
|
+
record["_id"] = new_id
|
45
|
+
|
46
|
+
# Write all records to shard files
|
47
|
+
for record in self.staged_records:
|
48
|
+
create_msgpack_file(self.user_id_cont, self.database, self.collection, record, self.container_url)
|
49
|
+
shard_number = calculate_shard_number(self.user_id_cont, self.database, self.collection, self.container_url)
|
50
|
+
update_index_file_insert(self.user_id_cont, self.database, self.collection, record["_id"], shard_number, self.container_url)
|
51
|
+
|
52
|
+
async_log(self.user_id_cont, self.database, self.collection, "POST", "SUCCESS", f"{len(self.staged_records)} records inserted atomically", len(self.staged_records), self.container_url)
|
53
|
+
|
54
|
+
self.staged_records.clear()
|
55
|
+
|
56
|
+
def rollback(self):
|
57
|
+
self.staged_records.clear()
|
58
|
+
async_log(self.user_id_cont, self.database, self.collection, "POST", "ERROR", "Transaction rollback: no records inserted", 0, self.container_url)
|
erioon/update.py
ADDED
@@ -0,0 +1,322 @@
|
|
1
|
+
# Copyright 2025-present Erioon, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
# Visit www.erioon.com/dev-docs for more information about the python SDK
|
15
|
+
|
16
|
+
import msgpack
|
17
|
+
from azure.storage.blob import ContainerClient
|
18
|
+
from erioon.functions import async_log
|
19
|
+
|
20
|
+
# UPDATE ONE RECORD
|
21
|
+
def handle_update_one(user_id, db_id, coll_id, filter_query, update_query, container_url):
|
22
|
+
"""
|
23
|
+
Updates a single record in a collection stored in Blob Storage based on a filter condition,
|
24
|
+
applying one of the supported update operations, and logs the result asynchronously.
|
25
|
+
|
26
|
+
Supported operations in `update_query`:
|
27
|
+
- "$set": Overwrites the value at the specified (possibly nested) key.
|
28
|
+
- "$push": Appends a value to a list at the specified key, or initializes the list if it doesn't exist.
|
29
|
+
- "$remove": Deletes the specified key from the record.
|
30
|
+
|
31
|
+
Parameters:
|
32
|
+
- user_id (str): Identifier of the user making the update request.
|
33
|
+
- db_id (str): Database identifier (used as a directory prefix).
|
34
|
+
- coll_id (str): Collection identifier (used as a subdirectory under the database).
|
35
|
+
- filter_query (dict): Key-value pairs that must match exactly in the record for it to be updated.
|
36
|
+
- update_query (dict): Update operations to apply, using one of the supported operators ($set, $push, $remove).
|
37
|
+
- container_url (str): URL of the Blob Storage container where the data is stored.
|
38
|
+
|
39
|
+
Returns:
|
40
|
+
- tuple(dict, int): A tuple containing:
|
41
|
+
- A dictionary with either:
|
42
|
+
- "success": Confirmation message if update succeeded.
|
43
|
+
- "error": Error message if update failed.
|
44
|
+
- HTTP status code:
|
45
|
+
- 200 if a matching record is updated successfully.
|
46
|
+
- 404 if no collections or matching records are found.
|
47
|
+
- No 500s are explicitly returned; internal exceptions are silently caught.
|
48
|
+
"""
|
49
|
+
container_client = ContainerClient.from_container_url(container_url)
|
50
|
+
directory_path = f"{db_id}/{coll_id}/"
|
51
|
+
|
52
|
+
blob_list = container_client.list_blobs(name_starts_with=directory_path)
|
53
|
+
blob_names = [blob.name for blob in blob_list if blob.name.endswith(".msgpack")]
|
54
|
+
|
55
|
+
if not blob_names:
|
56
|
+
async_log(user_id, db_id, coll_id, "PATCH_UPDT", "ERROR",
|
57
|
+
f"No collections found for the database {db_id}", 1, container_url)
|
58
|
+
return {"error": f"No collections found for the database {db_id}"}, 404
|
59
|
+
|
60
|
+
updated = False
|
61
|
+
|
62
|
+
for blob_name in blob_names:
|
63
|
+
try:
|
64
|
+
blob_client = container_client.get_blob_client(blob_name)
|
65
|
+
msgpack_data = blob_client.download_blob().readall()
|
66
|
+
|
67
|
+
if not msgpack_data:
|
68
|
+
continue
|
69
|
+
|
70
|
+
data_records = msgpack.unpackb(msgpack_data, raw=False)
|
71
|
+
modified_records = []
|
72
|
+
local_updated = False
|
73
|
+
|
74
|
+
for record in data_records:
|
75
|
+
if not updated:
|
76
|
+
match_found = all(record.get(k) == v for k, v in filter_query.items())
|
77
|
+
if match_found:
|
78
|
+
for op, changes in update_query.items():
|
79
|
+
if op == "$set":
|
80
|
+
for key, new_value in changes.items():
|
81
|
+
keys = key.split(".")
|
82
|
+
nested_obj = record
|
83
|
+
for k in keys[:-1]:
|
84
|
+
nested_obj = nested_obj.setdefault(k, {})
|
85
|
+
nested_obj[keys[-1]] = new_value
|
86
|
+
|
87
|
+
elif op == "$push":
|
88
|
+
for key, new_value in changes.items():
|
89
|
+
keys = key.split(".")
|
90
|
+
nested_obj = record
|
91
|
+
for k in keys[:-1]:
|
92
|
+
nested_obj = nested_obj.setdefault(k, {})
|
93
|
+
last_key = keys[-1]
|
94
|
+
if last_key not in nested_obj:
|
95
|
+
nested_obj[last_key] = [new_value]
|
96
|
+
elif isinstance(nested_obj[last_key], list):
|
97
|
+
nested_obj[last_key].append(new_value)
|
98
|
+
else:
|
99
|
+
nested_obj[last_key] = [nested_obj[last_key], new_value]
|
100
|
+
|
101
|
+
elif op == "$remove":
|
102
|
+
for key in changes:
|
103
|
+
keys = key.split(".")
|
104
|
+
nested_obj = record
|
105
|
+
for k in keys[:-1]:
|
106
|
+
nested_obj = nested_obj.get(k, {})
|
107
|
+
last_key = keys[-1]
|
108
|
+
if isinstance(nested_obj, dict) and last_key in nested_obj:
|
109
|
+
del nested_obj[last_key]
|
110
|
+
|
111
|
+
updated = True
|
112
|
+
local_updated = True
|
113
|
+
|
114
|
+
modified_records.append(record)
|
115
|
+
|
116
|
+
if local_updated:
|
117
|
+
packed_data = msgpack.packb(modified_records, use_bin_type=True)
|
118
|
+
blob_client.upload_blob(packed_data, overwrite=True)
|
119
|
+
async_log(user_id, db_id, coll_id, "PATCH_UPDT", "SUCCESS",
|
120
|
+
"Record updated successfully", len(modified_records), container_url)
|
121
|
+
return {"success": "Record updated successfully"}, 200
|
122
|
+
|
123
|
+
except Exception:
|
124
|
+
continue
|
125
|
+
|
126
|
+
if not updated:
|
127
|
+
async_log(user_id, db_id, coll_id, "PATCH_UPDT", "ERROR",
|
128
|
+
"No matching record found", 1, container_url)
|
129
|
+
return {"error": "No matching record found"}, 404
|
130
|
+
|
131
|
+
# UPDATE MULTIPLE RECORDS
|
132
|
+
def handle_update_many(user_id, db_id, coll_id, update_tasks, container_url):
|
133
|
+
"""
|
134
|
+
Perform multiple record updates based on a list of filter-and-update tasks.
|
135
|
+
|
136
|
+
Each task specifies a filter to match records and an update query to apply (using "$set", "$push", "$remove").
|
137
|
+
The function applies updates across all blobs within the collection and aggregates results per task.
|
138
|
+
For each task, it tracks how many records were updated and logs successes asynchronously.
|
139
|
+
|
140
|
+
Args:
|
141
|
+
user_id (str): The user ID performing the update.
|
142
|
+
db_id (str): Database identifier.
|
143
|
+
coll_id (str): Collection identifier.
|
144
|
+
update_tasks (list[dict]): List of update tasks, each with:
|
145
|
+
- "filter": dict specifying key-value pairs to match.
|
146
|
+
- "update": dict with update operations.
|
147
|
+
container_url (str): Blob Storage container URL.
|
148
|
+
|
149
|
+
Returns:
|
150
|
+
tuple(dict, int): A tuple containing:
|
151
|
+
- Response dictionary summarizing:
|
152
|
+
- Total number of updated records across all tasks.
|
153
|
+
- Detailed results per task (updated count or errors).
|
154
|
+
- HTTP status code:
|
155
|
+
- 200 if one or more records updated.
|
156
|
+
- 404 if no records were updated.
|
157
|
+
"""
|
158
|
+
|
159
|
+
container_client = ContainerClient.from_container_url(container_url)
|
160
|
+
directory_path = f"{db_id}/{coll_id}/"
|
161
|
+
blob_list = list(container_client.list_blobs(name_starts_with=directory_path))
|
162
|
+
blob_names = [blob.name for blob in blob_list if blob.name.endswith(".msgpack")]
|
163
|
+
|
164
|
+
if not blob_names:
|
165
|
+
return {"error": f"No collections found under {directory_path}"}, 404
|
166
|
+
|
167
|
+
total_updates = 0
|
168
|
+
task_results = []
|
169
|
+
|
170
|
+
for task in update_tasks:
|
171
|
+
filter_query = task.get("filter", {})
|
172
|
+
update_query = task.get("update", {})
|
173
|
+
updated_count = 0
|
174
|
+
|
175
|
+
for blob_name in blob_names:
|
176
|
+
try:
|
177
|
+
blob_client = container_client.get_blob_client(blob_name)
|
178
|
+
msgpack_data = blob_client.download_blob().readall()
|
179
|
+
|
180
|
+
if not msgpack_data:
|
181
|
+
continue
|
182
|
+
|
183
|
+
records = msgpack.unpackb(msgpack_data, raw=False)
|
184
|
+
modified = False
|
185
|
+
|
186
|
+
for record in records:
|
187
|
+
if all(record.get(k) == v for k, v in filter_query.items()):
|
188
|
+
for op, changes in update_query.items():
|
189
|
+
if op == "$set":
|
190
|
+
for key, new_value in changes.items():
|
191
|
+
nested = record
|
192
|
+
keys = key.split(".")
|
193
|
+
for k in keys[:-1]:
|
194
|
+
nested = nested.setdefault(k, {})
|
195
|
+
nested[keys[-1]] = new_value
|
196
|
+
|
197
|
+
elif op == "$push":
|
198
|
+
for key, value in changes.items():
|
199
|
+
nested = record
|
200
|
+
keys = key.split(".")
|
201
|
+
for k in keys[:-1]:
|
202
|
+
nested = nested.setdefault(k, {})
|
203
|
+
last_key = keys[-1]
|
204
|
+
if last_key not in nested:
|
205
|
+
nested[last_key] = [value]
|
206
|
+
elif isinstance(nested[last_key], list):
|
207
|
+
nested[last_key].append(value)
|
208
|
+
else:
|
209
|
+
nested[last_key] = [nested[last_key], value]
|
210
|
+
|
211
|
+
elif op == "$remove":
|
212
|
+
for key in changes:
|
213
|
+
nested = record
|
214
|
+
keys = key.split(".")
|
215
|
+
for k in keys[:-1]:
|
216
|
+
nested = nested.get(k, {})
|
217
|
+
last_key = keys[-1]
|
218
|
+
if isinstance(nested, dict) and last_key in nested:
|
219
|
+
del nested[last_key]
|
220
|
+
|
221
|
+
modified = True
|
222
|
+
updated_count += 1
|
223
|
+
|
224
|
+
if modified:
|
225
|
+
packed_data = msgpack.packb(records, use_bin_type=True)
|
226
|
+
blob_client.upload_blob(packed_data, overwrite=True)
|
227
|
+
|
228
|
+
except Exception as e:
|
229
|
+
task_results.append({
|
230
|
+
"filter": filter_query,
|
231
|
+
"error": str(e)
|
232
|
+
})
|
233
|
+
continue
|
234
|
+
|
235
|
+
if updated_count > 0:
|
236
|
+
async_log(user_id, db_id, coll_id, "PATCH_UPDT_MANY", "SUCCESS",
|
237
|
+
f"Updated {updated_count} records", updated_count, container_url)
|
238
|
+
task_results.append({
|
239
|
+
"filter": filter_query,
|
240
|
+
"updated": updated_count
|
241
|
+
})
|
242
|
+
total_updates += updated_count
|
243
|
+
else:
|
244
|
+
task_results.append({
|
245
|
+
"filter": filter_query,
|
246
|
+
"updated": 0,
|
247
|
+
"error": "No matching records found"
|
248
|
+
})
|
249
|
+
|
250
|
+
if total_updates == 0:
|
251
|
+
return {"error": "No records updated", "details": task_results}, 404
|
252
|
+
return {"success": f"{total_updates} records updated", "details": task_results}, 200
|
253
|
+
|
254
|
+
# REPLACE ONE RECORD
|
255
|
+
def handle_replace_one(user_id, db_id, coll_id, filter_query, replacement, container_url):
|
256
|
+
"""
|
257
|
+
Replace a single record identified by a single-field filter in a MsgPack collection.
|
258
|
+
|
259
|
+
This function searches blobs under `{db_id}/{coll_id}/` for a record matching the single filter field.
|
260
|
+
Upon finding the record, it replaces the entire record with the provided `replacement` dictionary.
|
261
|
+
If `replacement` lacks the `_id` field, the original record’s `_id` is preserved.
|
262
|
+
Only one field is allowed in `filter_query`.
|
263
|
+
|
264
|
+
Args:
|
265
|
+
user_id (str): User ID performing the replacement.
|
266
|
+
db_id (str): Database identifier.
|
267
|
+
coll_id (str): Collection identifier.
|
268
|
+
filter_query (dict): Dictionary with exactly one key-value pair to match.
|
269
|
+
replacement (dict): New record to replace the matched one.
|
270
|
+
container_url (str): Blob Storage container URL.
|
271
|
+
|
272
|
+
Returns:
|
273
|
+
tuple(dict, int): A tuple containing:
|
274
|
+
- Response dictionary with:
|
275
|
+
- "success": Confirmation message if replacement succeeded.
|
276
|
+
- "error": Error message if no matching record was found or filter invalid.
|
277
|
+
- HTTP status code:
|
278
|
+
- 200 if replacement succeeded.
|
279
|
+
- 400 if filter_query is invalid.
|
280
|
+
- 404 if no matching record was found.
|
281
|
+
"""
|
282
|
+
|
283
|
+
container_client = ContainerClient.from_container_url(container_url)
|
284
|
+
directory_path = f"{db_id}/{coll_id}/"
|
285
|
+
blob_list = container_client.list_blobs(name_starts_with=directory_path)
|
286
|
+
blob_names = [b.name for b in blob_list if b.name.endswith(".msgpack")]
|
287
|
+
|
288
|
+
if len(filter_query) != 1:
|
289
|
+
return {"error": "filter_query must contain exactly one field"}, 400
|
290
|
+
|
291
|
+
filter_key, filter_value = list(filter_query.items())[0]
|
292
|
+
|
293
|
+
for blob_name in blob_names:
|
294
|
+
try:
|
295
|
+
blob_client = container_client.get_blob_client(blob_name)
|
296
|
+
blob_data = blob_client.download_blob().readall()
|
297
|
+
records = msgpack.unpackb(blob_data, raw=False)
|
298
|
+
modified = False
|
299
|
+
|
300
|
+
for i, record in enumerate(records):
|
301
|
+
if record.get(filter_key) == filter_value:
|
302
|
+
original_id = record.get("_id")
|
303
|
+
new_record = replacement.copy()
|
304
|
+
if "_id" not in new_record:
|
305
|
+
new_record["_id"] = original_id
|
306
|
+
records[i] = new_record
|
307
|
+
modified = True
|
308
|
+
break
|
309
|
+
|
310
|
+
if modified:
|
311
|
+
packed = msgpack.packb(records, use_bin_type=True)
|
312
|
+
blob_client.upload_blob(packed, overwrite=True)
|
313
|
+
async_log(user_id, db_id, coll_id, "REPLACE", "SUCCESS",
|
314
|
+
"Record replaced successfully", 1, container_url)
|
315
|
+
return {"success": "Record replaced successfully"}, 200
|
316
|
+
|
317
|
+
except Exception as e:
|
318
|
+
continue
|
319
|
+
|
320
|
+
async_log(user_id, db_id, coll_id, "REPLACE", "ERROR",
|
321
|
+
"No matching record found", 1, container_url)
|
322
|
+
return {"error": "No matching record found"}, 404
|