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 ADDED
@@ -0,0 +1,34 @@
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
+ from erioon.client import ErioonClient
17
+
18
+ def Auth(credential_string):
19
+ """
20
+ Authenticates a user using a colon-separated email:password string.
21
+
22
+ Parameters:
23
+ - credential_string (str): A string in the format "email:password"
24
+
25
+ Returns:
26
+ - ErioonClient instance: An instance representing the authenticated user.
27
+ If authentication fails, the instance will contain the error message.
28
+
29
+ Example usage:
30
+ >>> from erioon.auth import Auth
31
+ >>> client = Auth("<API_KEY>:<EMAIL>:<PASSWORD>")
32
+ >>> print(client) # prints user_id if successful or error message if not
33
+ """
34
+ return ErioonClient(credential_string)
erioon/client.py ADDED
@@ -0,0 +1,118 @@
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 requests
17
+ from erioon.database import Database
18
+
19
+ class ErioonClient:
20
+ def __init__(self, credential_string, base_url="https://sdk.erioon.com"):
21
+ self.credential_string = credential_string
22
+ self.base_url = base_url
23
+ self.user_id = None
24
+ self.login_metadata = None
25
+
26
+ parts = credential_string.split(":")
27
+
28
+ if len(parts) == 1 and credential_string.startswith("erioon"):
29
+ self.api = credential_string
30
+ self.email = None
31
+ self.password = None
32
+ else:
33
+ if len(parts) == 2:
34
+ self.email, self.password = parts
35
+ self.api = None
36
+ else:
37
+ raise ValueError("Invalid credential format. Use 'erioon-xxxxx' or 'email:password'")
38
+
39
+ try:
40
+ self.login_metadata = self._login()
41
+ self._update_metadata_fields()
42
+ except Exception as e:
43
+ print(f"[ErioonClient] Initialization error: {e}")
44
+
45
+
46
+ def _login(self):
47
+ url = f"{self.base_url}/login"
48
+ payload = {"api_key": self.api, "email": self.email, "password": self.password}
49
+ headers = {"Content-Type": "application/json"}
50
+
51
+ response = requests.post(url, json=payload, headers=headers)
52
+ if response.status_code == 200:
53
+ data = response.json()
54
+ self.login_metadata = data
55
+ return data
56
+ else:
57
+ try:
58
+ msg = response.json().get("error", "Login failed")
59
+ except Exception:
60
+ msg = response.text
61
+ print(f"[ErioonClient] Login failed: {msg}")
62
+ raise RuntimeError(msg)
63
+
64
+ def _update_metadata_fields(self):
65
+ if self.login_metadata:
66
+ self.user_id = self.login_metadata.get("_id")
67
+ self.cluster = self.login_metadata.get("cluster")
68
+ self.database = self.login_metadata.get("database")
69
+ self.sas_tokens = self.login_metadata.get("sas_tokens", {})
70
+
71
+ def __getitem__(self, db_id):
72
+ if not self.user_id:
73
+ raise ValueError("Client not authenticated. Cannot access database.")
74
+
75
+ return self._get_database_info(db_id)
76
+
77
+ def _get_database_info(self, db_id):
78
+ payload = {"user_id": self.user_id, "db_id": db_id}
79
+ headers = {"Content-Type": "application/json"}
80
+ response = requests.post(f"{self.base_url}/db_info", json=payload, headers=headers)
81
+
82
+ if response.status_code == 200:
83
+ db_info = response.json()
84
+ sas_info = self.sas_tokens.get(db_id)
85
+ if not sas_info:
86
+ raise Exception(f"No SAS token info for database id {db_id}")
87
+
88
+ container_url = sas_info.get("container_url")
89
+ sas_token = sas_info.get("sas_token")
90
+
91
+ if not container_url or not sas_token:
92
+ raise Exception("Missing SAS URL components")
93
+
94
+ if not sas_token.startswith("?"):
95
+ sas_token = "?" + sas_token
96
+
97
+ sas_url = container_url.split("?")[0] + sas_token
98
+
99
+ return Database(
100
+ user_id=self.user_id,
101
+ metadata=db_info,
102
+ database=self.database,
103
+ cluster=self.cluster,
104
+ sas_url=sas_url
105
+ )
106
+ else:
107
+ try:
108
+ error_json = response.json()
109
+ error_msg = error_json.get("error", response.text)
110
+ except Exception:
111
+ error_msg = response.text
112
+ raise Exception(f"Failed to fetch database info: {error_msg}")
113
+
114
+ def __str__(self):
115
+ return self.user_id if self.user_id else "[ErioonClient] Unauthenticated"
116
+
117
+ def __repr__(self):
118
+ return f"<ErioonClient user_id={self.user_id}>" if self.user_id else "<ErioonClient unauthenticated>"
erioon/collection.py ADDED
@@ -0,0 +1,371 @@
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 json
17
+ from urllib.parse import urlparse
18
+ from erioon.read import handle_get_all, handle_find_one, handle_find_many, handle_count_records
19
+ from erioon.create import handle_insert_one, handle_insert_many
20
+ from erioon.delete import handle_delete_one, handle_delete_many
21
+ from erioon.update import handle_update_one, handle_update_many, handle_replace_one
22
+ from erioon.ping import handle_connection_ping
23
+
24
+ class Collection:
25
+ def __init__(
26
+ self,
27
+ user_id,
28
+ db_id,
29
+ coll_id,
30
+ metadata,
31
+ database,
32
+ cluster,
33
+ sas_url,
34
+ ):
35
+ """
36
+ Initialize a Collection object that wraps Erioon collection access.
37
+
38
+ Args:
39
+ user_id (str): The authenticated user's ID.
40
+ db_id (str): The database ID.
41
+ coll_id (str): The collection ID.
42
+ metadata (dict): Metadata info about this collection (e.g., schema, indexing, etc.).
43
+ database (str): Name or ID of the database.
44
+ cluster (str): Cluster name or ID hosting the database.
45
+ sas_url (str): Full SAS URL used to access the storage container.
46
+ """
47
+ self.user_id = user_id
48
+ self.db_id = db_id
49
+ self.coll_id = coll_id
50
+ self.metadata = metadata
51
+ self.database = database
52
+ self.cluster = cluster
53
+
54
+ parsed_url = urlparse(sas_url.rstrip("/"))
55
+ container_name = parsed_url.path.lstrip("/").split("/")[0]
56
+ account_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
57
+ sas_token = parsed_url.query
58
+ self.container_url = f"{account_url}/{container_name}?{sas_token}"
59
+
60
+ # PRINT ERIOON
61
+ def _print_loading(self):
62
+ """Prints a loading message (likely for UX in CLI or SDK usage)."""
63
+ print("Erioon is loading...")
64
+
65
+ # CHECK READ / WRITE LICENCE
66
+ def _is_read_only(self):
67
+ """Check if the current database is marked as read-only."""
68
+ return self.database == "read"
69
+
70
+ # RESPONSE FOR ONLY WRITE
71
+ def _read_only_response(self):
72
+ """Standardized error response for blocked write operations."""
73
+ return "This user is not allowed to perform write operations.", 403
74
+
75
+ # GET ALL RECORDS OF A COLLECTION
76
+ def get_all(self, limit=1000000):
77
+ """
78
+ Fetch all records from the collection (up to a limit).
79
+ """
80
+ self._print_loading()
81
+ result, status_code = handle_get_all(
82
+ user_id=self.user_id,
83
+ db_id=self.db_id,
84
+ coll_id=self.coll_id,
85
+ limit=limit,
86
+ container_url=self.container_url,
87
+ )
88
+ return result
89
+
90
+ # FINDS A SPECIFIC RECORD OF A COLLECTION
91
+ def find_one(self, filters: dict | None = None):
92
+ """
93
+ Fetch a single record that matches specific key-value filters.
94
+ """
95
+ if self._is_read_only():
96
+ return self._read_only_response()
97
+
98
+ if filters is None:
99
+ filters = {}
100
+
101
+ search_criteria = [{k: v} for k, v in filters.items()]
102
+
103
+ result, status_code = handle_find_one(
104
+ user_id=self.user_id,
105
+ db_id=self.db_id,
106
+ coll_id=self.coll_id,
107
+ search_criteria=search_criteria,
108
+ container_url=self.container_url,
109
+ )
110
+ return result
111
+
112
+ # FINDS MULTIPLE RECORDS OF A COLLECTION
113
+ def find_many(self, filters: dict | None = None, limit: int = 1000):
114
+ """
115
+ Fetch multiple records that match specific key-value filters.
116
+
117
+ Args:
118
+ filters (dict): Filters to match records.
119
+ limit (int): Maximum number of records to return (default: 1000).
120
+
121
+ Returns:
122
+ dict: Result from `handle_find_many()`
123
+ """
124
+ if self._is_read_only():
125
+ return self._read_only_response()
126
+
127
+ self._print_loading()
128
+
129
+ if filters is None:
130
+ filters = {}
131
+
132
+ if limit > 500_000:
133
+ raise ValueError("Limit of 500,000 exceeded")
134
+
135
+ search_criteria = [{k: v} for k, v in filters.items()]
136
+
137
+ result, status_code = handle_find_many(
138
+ user_id=self.user_id,
139
+ db_id=self.db_id,
140
+ coll_id=self.coll_id,
141
+ search_criteria=search_criteria,
142
+ limit=limit,
143
+ container_url=self.container_url,
144
+ )
145
+ return result
146
+
147
+ # INSERT A SINGLE RECORD IN A COLLECTION
148
+ def insert_one(self, record):
149
+ """
150
+ Insert a single record into the collection.
151
+ """
152
+ if self._is_read_only():
153
+ return self._read_only_response()
154
+ response, status = handle_insert_one(
155
+ user_id_cont=self.user_id,
156
+ database=self.db_id,
157
+ collection=self.coll_id,
158
+ record=record,
159
+ container_url=self.container_url,
160
+ )
161
+ if status == 200:
162
+ print("Insertion was successful.")
163
+ else:
164
+ print(f"Error inserting record: {response}")
165
+ return response, status
166
+
167
+ # INSERT MULTIPLE RECORDS INTO A COLLECTION
168
+ def insert_many(self, data):
169
+ """
170
+ Insert multiple records into the collection.
171
+
172
+ Args:
173
+ data (list): List of record dicts.
174
+
175
+ Returns:
176
+ tuple: (response message, HTTP status code)
177
+ """
178
+ if self._is_read_only():
179
+ return self._read_only_response()
180
+ self._print_loading()
181
+ response, status = handle_insert_many(
182
+ user_id_cont=self.user_id,
183
+ database=self.db_id,
184
+ collection=self.coll_id,
185
+ data=data,
186
+ container_url=self.container_url,
187
+ )
188
+ if status == 200:
189
+ print("Insertion of multiple records was successful.")
190
+ else:
191
+ print(f"Error inserting records: {response}")
192
+ return response, status
193
+
194
+ # DELETE A SINGLE RECORD BASED ON _ID OR KEY
195
+ def delete_one(self, record_to_delete):
196
+ """
197
+ Delete a single record based on its _id or nested key.
198
+ """
199
+ if self._is_read_only():
200
+ return self._read_only_response()
201
+ response, status = handle_delete_one(
202
+ user_id=self.user_id,
203
+ db_id=self.db_id,
204
+ coll_id=self.coll_id,
205
+ data_to_delete=record_to_delete,
206
+ container_url=self.container_url,
207
+ )
208
+ if status == 200:
209
+ print("Deletion was successful.")
210
+ else:
211
+ print(f"Error deleting record: {response}")
212
+ return response, status
213
+
214
+ # DELETE MANY RECORDS IN BATCHES
215
+ def delete_many(self, records_to_delete_list, batch_size=10):
216
+ """
217
+ Delete multiple records in batches.
218
+ """
219
+ if self._is_read_only():
220
+ return self._read_only_response()
221
+ self._print_loading()
222
+ response, status = handle_delete_many(
223
+ user_id=self.user_id,
224
+ db_id=self.db_id,
225
+ coll_id=self.coll_id,
226
+ data_to_delete_list=records_to_delete_list,
227
+ batch_size=batch_size,
228
+ container_url=self.container_url,
229
+ )
230
+ if status == 200:
231
+ print("Batch deletion was successful.")
232
+ else:
233
+ print(f"Error deleting records: {response}")
234
+ return response, status
235
+
236
+ # UPDATE A RECORD
237
+ def update_one(self, filter_query: dict, update_query: dict):
238
+ """
239
+ Update a record in-place by filtering and applying update logic.
240
+ """
241
+ if self._is_read_only():
242
+ return self._read_only_response()
243
+ response, status = handle_update_one(
244
+ user_id=self.user_id,
245
+ db_id=self.db_id,
246
+ coll_id=self.coll_id,
247
+ filter_query=filter_query,
248
+ update_query=update_query,
249
+ container_url=self.container_url,
250
+ )
251
+ if status == 200:
252
+ print("Update was successful.")
253
+ else:
254
+ print(f"Error updating record: {response}")
255
+ return response, status
256
+
257
+ # UPDATE MULTIPLE RECORDS
258
+ def update_many(self, update_tasks: list):
259
+ """
260
+ Update multiple records in-place by applying a list of filter + update operations.
261
+
262
+ Each item in `update_tasks` should be a dict:
263
+ {
264
+ "filter": { ... },
265
+ "update": {
266
+ "$set": {...}, "$push": {...}, "$remove": [...]
267
+ }
268
+ }
269
+
270
+ Returns:
271
+ (dict, int): Summary response and HTTP status code.
272
+ """
273
+ if self._is_read_only():
274
+ return self._read_only_response()
275
+ self._print_loading()
276
+
277
+ response, status = handle_update_many(
278
+ user_id=self.user_id,
279
+ db_id=self.db_id,
280
+ coll_id=self.coll_id,
281
+ update_tasks=update_tasks,
282
+ container_url=self.container_url,
283
+ )
284
+
285
+ if status == 200:
286
+ print(f"Successfully updated {response.get('success')}")
287
+ else:
288
+ print(f"Error updating records: {response}")
289
+
290
+ return response, status
291
+
292
+ # REPLACE A SINGLE RECORDS BASED ON THE FILTER QUERY
293
+ def replace_one(self, filter_query: dict, replacement: dict):
294
+ """
295
+ Replaces a single record matching `filter_query` with the full `replacement` document.
296
+
297
+ - If `_id` is **not** in the replacement, preserves the original `_id`.
298
+ - If `_id` **is** in the replacement, uses the new `_id`.
299
+
300
+ Args:
301
+ filter_query (dict): Must contain a single key-value pair.
302
+ replacement (dict): New record to replace the old one.
303
+
304
+ Returns:
305
+ (dict, int): Response message and HTTP status code.
306
+ """
307
+ if self._is_read_only():
308
+ return self._read_only_response()
309
+
310
+ response, status = handle_replace_one(
311
+ user_id=self.user_id,
312
+ db_id=self.db_id,
313
+ coll_id=self.coll_id,
314
+ filter_query=filter_query,
315
+ replacement=replacement,
316
+ container_url=self.container_url,
317
+ )
318
+
319
+ if status == 200:
320
+ print("Replacement was successful.")
321
+ else:
322
+ print(f"Error replacing record: {response}")
323
+
324
+ return response, status
325
+
326
+ # PING AND CHECK CONNECTION
327
+ def ping(self):
328
+ """
329
+ Health check / ping to verify collection accessibility.
330
+ """
331
+ self._print_loading()
332
+ response, status = handle_connection_ping(
333
+ user_id=self.user_id,
334
+ db_id=self.db_id,
335
+ coll_id=self.coll_id,
336
+ container_url=self.container_url,
337
+ )
338
+ if status == 200:
339
+ print("Connection ping successful.")
340
+ else:
341
+ print(f"Ping failed: {response}")
342
+ return response, status
343
+
344
+ # COUNT ALL THE RECORDS
345
+ def count_records(self) -> int:
346
+ """
347
+ Count the total number of documents in the collection (across all shards).
348
+
349
+ Returns:
350
+ int: Total document count.
351
+ """
352
+ if self._is_read_only():
353
+ return 0
354
+ self._print_loading()
355
+
356
+ count, status = handle_count_records(
357
+ user_id=self.user_id,
358
+ db_id=self.db_id,
359
+ coll_id=self.coll_id,
360
+ container_url=self.container_url,
361
+ )
362
+ return count
363
+
364
+
365
+ def __str__(self):
366
+ """Pretty print the collection metadata."""
367
+ return json.dumps(self.metadata, indent=4)
368
+
369
+ def __repr__(self):
370
+ """Simplified representation for debugging or introspection."""
371
+ return f"<Collection coll_id={self.coll_id}>"
erioon/create.py ADDED
@@ -0,0 +1,130 @@
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 (
18
+ create_msgpack_file,
19
+ update_index_file_insert,
20
+ calculate_shard_number,
21
+ async_log,
22
+ is_duplicate_id
23
+ )
24
+
25
+ # INSERT ONE RECORD
26
+ def handle_insert_one(user_id_cont, database, collection, record, container_url):
27
+ """
28
+ Insert a single record into the collection.
29
+
30
+ - If no '_id' provided, generate a new UUID.
31
+ - If provided '_id' is duplicate, generate a new one and update the record.
32
+ - Create or append the record in a shard file.
33
+ - Update index.json to map the record to the appropriate shard.
34
+ - Log success or errors asynchronously.
35
+
36
+ Args:
37
+ user_id_cont: User identifier.
38
+ database: Database name.
39
+ collection: Collection name.
40
+ record: Dict representing the record to insert.
41
+ container_url: Blob Storage container SAS URL.
42
+
43
+ Returns:
44
+ Tuple (response dict, status code) indicating success or failure.
45
+ """
46
+ try:
47
+ if "_id" not in record or not record["_id"]:
48
+ record["_id"] = str(uuid.uuid4())
49
+
50
+ rec_id = record["_id"]
51
+
52
+ if is_duplicate_id(user_id_cont, database, collection, rec_id, container_url):
53
+ new_id = str(uuid.uuid4())
54
+ record["_id"] = new_id
55
+ rec_id = new_id
56
+ msg = f"Record inserted successfully in {collection} with a new _id {rec_id} because the provided _id was already present."
57
+ else:
58
+ msg = f"Record inserted successfully in {collection} with _id {rec_id}"
59
+
60
+ async_log(user_id_cont, database, collection, "POST", "SUCCESS", msg, 1, container_url)
61
+
62
+ create_msgpack_file(user_id_cont, database, collection, record, container_url)
63
+
64
+ shard_number = calculate_shard_number(user_id_cont, database, collection, container_url)
65
+ update_index_file_insert(user_id_cont, database, collection, rec_id, shard_number, container_url)
66
+
67
+ return {"status": "OK", "message": msg, "record": record}, 200
68
+
69
+ except Exception as e:
70
+ error_msg = f"An error occurred during insert in {collection}: {str(e)}"
71
+ async_log(user_id_cont, database, collection,"POST", "ERROR", error_msg, 1, container_url)
72
+ return {"status": "KO", "message": "Failed to insert record.", "error": str(e)}, 500
73
+
74
+ # INSERT MANY RECORDS
75
+ def handle_insert_many(user_id_cont, database, collection, data, container_url):
76
+ """
77
+ Insert multiple records in bulk.
78
+
79
+ - `data` is a list of dicts, each representing a record.
80
+ - For each record:
81
+ - Ensure it has a unique _id (generate new UUID if missing or duplicate).
82
+ - Write the record to the appropriate shard.
83
+ - Update index.json with _id to shard mapping.
84
+ - Log the batch insert operation with details.
85
+ - Return aggregate success or failure response.
86
+
87
+ Args:
88
+ user_id_cont: User identifier.
89
+ database: Database name.
90
+ collection: Collection name.
91
+ data: List of record dicts.
92
+ container_url: Blob Storage container SAS URL.
93
+
94
+ Returns:
95
+ Tuple (response dict, status code) with summary of insert results.
96
+ """
97
+ insert_results = []
98
+ count = len(data)
99
+
100
+ try:
101
+ for record in data:
102
+ if "_id" not in record or not record["_id"]:
103
+ record["_id"] = str(uuid.uuid4())
104
+
105
+ rec_id = record["_id"]
106
+
107
+ if is_duplicate_id(user_id_cont, database, collection, rec_id, container_url):
108
+ new_id = str(uuid.uuid4())
109
+ record["_id"] = new_id
110
+ rec_id = new_id
111
+ msg = f"Inserted with new _id {rec_id} (original _id was already present)."
112
+ else:
113
+ msg = f"Inserted with _id {rec_id}."
114
+
115
+ create_msgpack_file(user_id_cont, database, collection, record, container_url)
116
+
117
+ shard_number = calculate_shard_number(user_id_cont, database, collection, container_url)
118
+ update_index_file_insert(
119
+ user_id_cont, database, collection, rec_id, shard_number, container_url
120
+ )
121
+
122
+ insert_results.append({"_id": rec_id, "message": msg})
123
+
124
+ async_log(user_id_cont, database, collection, "POST", "SUCCESS", insert_results, count, container_url)
125
+ return {"success": "Records inserted successfully", "details": insert_results}, 200
126
+
127
+ except Exception as e:
128
+ general_error_msg = f"Unexpected error during bulk insert: {str(e)}"
129
+ async_log(user_id_cont, database, collection, "POST", "ERROR", general_error_msg, 1, container_url)
130
+ return {"status": "KO", "message": general_error_msg}, 500