erioon 0.0.6__py3-none-any.whl → 0.0.8__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 CHANGED
@@ -13,7 +13,7 @@ def Auth(credential_string):
13
13
 
14
14
  Example usage:
15
15
  >>> from erioon.auth import Auth
16
- >>> client = Auth("<EMAIL>:<PASSWORD>")
16
+ >>> client = Auth("<API_KEY>:<EMAIL>:<PASSWORD>")
17
17
  >>> print(client) # prints user_id if successful or error message if not
18
18
  """
19
19
  api, email, password = credential_string.split(":")
erioon/client.py CHANGED
@@ -1,33 +1,21 @@
1
1
  import os
2
2
  import json
3
3
  import requests
4
- from werkzeug.security import generate_password_hash
4
+ from datetime import datetime, timezone
5
5
  from erioon.database import Database
6
6
 
7
7
  class ErioonClient:
8
8
  """
9
9
  Client SDK for interacting with the Erioon API.
10
10
 
11
- Handles user authentication, token caching, and accessing user databases.
12
-
13
- Attributes:
14
- email (str): User email for login.
15
- password (str): User password for login.
16
- base_url (str): Base URL of the Erioon API.
17
- user_id (str | None): Authenticated user ID.
18
- error (str | None): Stores error messages if login fails.
19
- token_path (str): Local path to cached authentication token.
11
+ Handles:
12
+ - User authentication with email/password and API key
13
+ - Token caching to avoid re-authenticating every time
14
+ - SAS token expiration detection and auto-renewal
15
+ - Access to user-specific databases
20
16
  """
21
17
 
22
18
  def __init__(self, api, email, password, base_url="https://sdk.erioon.com"):
23
- """
24
- Initialize ErioonClient instance, attempts to load cached token or perform login.
25
-
26
- Args:
27
- email (str): User email for authentication.
28
- password (str): User password for authentication.
29
- base_url (str, optional): Base API URL. Defaults to "https://sdk.erioon.com".
30
- """
31
19
  self.api = api
32
20
  self.email = email
33
21
  self.password = password
@@ -35,34 +23,23 @@ class ErioonClient:
35
23
  self.user_id = None
36
24
  self.error = None
37
25
  self.token_path = os.path.expanduser(f"~/.erioon_token_{self._safe_filename(email)}")
26
+ self.login_metadata = None
38
27
 
39
28
  try:
40
- metadata = self._load_or_login()
41
- self.user_id = metadata.get("_id")
42
- self.database = metadata.get("database")
43
- self.cluster = metadata.get("cluster")
44
- self.login_metadata = metadata
29
+ self.login_metadata = self._load_or_login()
30
+ self._update_metadata_fields()
45
31
  except Exception as e:
46
32
  self.error = str(e)
47
33
 
48
34
  def _safe_filename(self, text):
49
35
  """
50
- Converts a string into a safe filename by replacing non-alphanumeric chars with underscores.
51
-
52
- Args:
53
- text (str): Input string to convert.
54
-
55
- Returns:
56
- str: Sanitized filename-safe string.
36
+ Converts unsafe filename characters to underscores for cache file naming.
57
37
  """
58
38
  return "".join(c if c.isalnum() else "_" for c in text)
59
39
 
60
40
  def _do_login_and_cache(self):
61
41
  """
62
- Perform login to API and cache the metadata locally.
63
-
64
- Returns:
65
- dict: Login metadata including user_id, database, cluster.
42
+ Logs in to the API and writes the returned metadata (e.g. SAS token, user ID) to a local file.
66
43
  """
67
44
  metadata = self._login()
68
45
  with open(self.token_path, "w") as f:
@@ -71,69 +48,80 @@ class ErioonClient:
71
48
 
72
49
  def _load_or_login(self):
73
50
  """
74
- Load cached metadata or perform login.
75
-
76
- Returns:
77
- dict: Login metadata.
51
+ Tries to load the cached login metadata.
52
+ If token is expired or file does not exist, performs a fresh login.
78
53
  """
79
54
  if os.path.exists(self.token_path):
80
55
  with open(self.token_path, "r") as f:
81
56
  metadata = json.load(f)
82
- if "user_id" in metadata:
83
- return metadata
84
-
85
- return self._do_login_and_cache()
57
+ if self._is_sas_expired(metadata):
58
+ metadata = self._do_login_and_cache()
59
+ return metadata
60
+ else:
61
+ return self._do_login_and_cache()
86
62
 
87
63
  def _login(self):
88
64
  """
89
- Authenticate and return full login metadata.
90
-
91
- Returns:
92
- dict: Metadata with user_id, database, cluster, etc.
65
+ Sends login request to Erioon API using API key, email, and password.
66
+ Returns authentication metadata including SAS token.
93
67
  """
94
68
  url = f"{self.base_url}/login_with_credentials"
95
- hashed_key = generate_password_hash(self.api)
96
- payload = {"api_key": hashed_key,"email": self.email, "password": self.password}
69
+ payload = {"api_key": self.api, "email": self.email, "password": self.password}
97
70
  headers = {"Content-Type": "application/json"}
98
71
 
99
72
  response = requests.post(url, json=payload, headers=headers)
100
73
  if response.status_code == 200:
101
74
  data = response.json()
102
75
  self.login_metadata = data
76
+ self._update_metadata_fields()
103
77
  return data
104
78
  else:
105
79
  raise Exception("Invalid account")
106
80
 
81
+ def _update_metadata_fields(self):
82
+ """
83
+ Updates internal fields like user_id, database name, and cluster info
84
+ from login metadata.
85
+ """
86
+ if self.login_metadata:
87
+ self.user_id = self.login_metadata.get("_id")
88
+ self.database = self.login_metadata.get("database")
89
+ self.cluster = self.login_metadata.get("cluster")
107
90
 
108
91
  def _clear_cached_token(self):
109
92
  """
110
- Remove cached token file and reset user_id to None.
93
+ Clears the locally cached authentication token and resets internal state.
111
94
  """
112
95
  if os.path.exists(self.token_path):
113
96
  os.remove(self.token_path)
114
97
  self.user_id = None
98
+ self.login_metadata = None
115
99
 
116
- def __getitem__(self, db_id):
100
+ def _is_sas_expired(self, metadata):
117
101
  """
118
- Access a Database object by database ID.
119
-
120
- Args:
121
- db_id (str): The ID of the database to access.
102
+ Determines whether the SAS token has expired by comparing the 'sas_token_expiry'
103
+ or 'expiry' field with the current UTC time.
104
+ """
105
+ expiry_str = metadata.get("sas_token_expiry") or metadata.get("expiry")
122
106
 
123
- Returns:
124
- Database: An instance representing the database.
107
+ if not expiry_str:
108
+ return True
125
109
 
126
- Raises:
127
- ValueError: If client is not authenticated.
128
- Exception: For other API errors not related to database existence.
110
+ try:
111
+ expiry_dt = datetime.fromisoformat(expiry_str.replace("Z", "+00:00"))
112
+ now = datetime.now(timezone.utc)
113
+ return now >= expiry_dt
114
+ except Exception:
115
+ return True
129
116
 
130
- Handles:
131
- On database-related errors, tries to relogin once. If relogin fails, returns "Login error".
132
- If database still not found after relogin, returns a formatted error message.
117
+ def __getitem__(self, db_id):
118
+ """
119
+ Allows syntax like `client["my_database_id"]` to access a database.
120
+ If the token is expired or invalid, it attempts reauthentication.
133
121
  """
134
122
  if not self.user_id:
135
123
  raise ValueError("Client not authenticated. Cannot access database.")
136
-
124
+
137
125
  try:
138
126
  return self._get_database_info(db_id)
139
127
  except Exception as e:
@@ -141,29 +129,22 @@ class ErioonClient:
141
129
  if f"database with {db_id.lower()}" in err_msg or "database" in err_msg:
142
130
  self._clear_cached_token()
143
131
  try:
144
- self.user_id = self._do_login_and_cache()
132
+ self.login_metadata = self._do_login_and_cache()
133
+ self._update_metadata_fields()
145
134
  except Exception:
146
135
  return "Login error"
147
-
136
+
148
137
  try:
149
138
  return self._get_database_info(db_id)
150
139
  except Exception:
151
140
  return f"❌ Database with _id {db_id} ..."
152
141
  else:
153
142
  raise e
154
-
143
+
155
144
  def _get_database_info(self, db_id):
156
145
  """
157
- Helper method to fetch database info from API and instantiate a Database object.
158
-
159
- Args:
160
- db_id (str): The database ID to fetch.
161
-
162
- Returns:
163
- Database: Database instance with the fetched info.
164
-
165
- Raises:
166
- Exception: If API returns an error.
146
+ Sends a POST request to fetch metadata for a given database ID.
147
+ Returns a `Database` instance initialized with SAS URL and metadata.
167
148
  """
168
149
  payload = {"user_id": self.user_id, "db_id": db_id}
169
150
  headers = {"Content-Type": "application/json"}
@@ -172,11 +153,24 @@ class ErioonClient:
172
153
 
173
154
  if response.status_code == 200:
174
155
  db_info = response.json()
156
+
157
+ container_url = self.login_metadata.get("container_url")
158
+ sas_token = self.login_metadata.get("sas_token")
159
+
160
+ if not container_url or not sas_token:
161
+ raise Exception("Missing SAS URL components for storage access")
162
+
163
+ if not sas_token.startswith("?"):
164
+ sas_token = "?" + sas_token
165
+
166
+ sas_url = container_url.split("?")[0] + sas_token
167
+
175
168
  return Database(
176
169
  user_id=self.user_id,
177
170
  metadata=db_info,
178
171
  database=self.database,
179
- cluster=self.cluster
172
+ cluster=self.cluster,
173
+ sas_url=sas_url
180
174
  )
181
175
  else:
182
176
  try:
@@ -188,12 +182,12 @@ class ErioonClient:
188
182
 
189
183
  def __str__(self):
190
184
  """
191
- String representation: returns user_id if authenticated, else the error message.
185
+ Returns user_id or error string when printed.
192
186
  """
193
187
  return self.user_id if self.user_id else self.error
194
188
 
195
189
  def __repr__(self):
196
190
  """
197
- Developer-friendly string representation of the client instance.
191
+ Developer-friendly representation of the client.
198
192
  """
199
193
  return f"<ErioonClient user_id={self.user_id}>" if self.user_id else f"<ErioonClient error='{self.error}'>"
erioon/collection.py CHANGED
@@ -1,5 +1,10 @@
1
1
  import json
2
- import requests
2
+ from urllib.parse import urlparse
3
+ from erioon.read import handle_get_all, handle_get_data
4
+ from erioon.create import handle_insert_one, handle_insert_many
5
+ from erioon.delete import handle_delete_one, handle_delete_many
6
+ from erioon.update import handle_update_query
7
+ from erioon.ping import handle_connection_ping
3
8
 
4
9
  class Collection:
5
10
  def __init__(
@@ -10,61 +15,76 @@ class Collection:
10
15
  metadata,
11
16
  database,
12
17
  cluster,
13
- base_url: str = "https://sdk.erioon.com",
18
+ sas_url,
14
19
  ):
20
+
15
21
  """
16
- Initialize a Collection instance.
22
+ Initialize a Collection object that wraps Erioon collection access.
17
23
 
18
24
  Args:
19
- user_id (str): Authenticated user ID.
20
- db_id (str): Database ID.
21
- coll_id (str): Collection ID.
22
- metadata (dict): Collection metadata.
23
- base_url (str): Base URL of the Erioon API.
25
+ user_id (str): The authenticated user's ID.
26
+ db_id (str): The database ID.
27
+ coll_id (str): The collection ID.
28
+ metadata (dict): Metadata info about this collection (e.g., schema, indexing, etc.).
29
+ database (str): Name or ID of the database.
30
+ cluster (str): Cluster name or ID hosting the database.
31
+ sas_url (str): Full SAS URL used to access the storage container.
24
32
  """
33
+
25
34
  self.user_id = user_id
26
35
  self.db_id = db_id
27
36
  self.coll_id = coll_id
28
37
  self.metadata = metadata
29
- self.database = database
38
+ self.database = database
30
39
  self.cluster = cluster
31
- self.base_url = base_url.rstrip("/")
32
40
 
33
- def _print_loading(self) -> None:
34
- """Print a green loading message to the terminal."""
41
+ parsed_url = urlparse(sas_url.rstrip("/"))
42
+ container_name = parsed_url.path.lstrip("/").split("/")[0]
43
+ account_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
44
+ sas_token = parsed_url.query
45
+ self.container_url = f"{account_url}/{container_name}?{sas_token}"
46
+
47
+ def _print_loading(self):
48
+ """Prints a loading message (likely for UX in CLI or SDK usage)."""
35
49
  print("Erioon is loading...")
36
-
50
+
37
51
  def _is_read_only(self):
52
+ """Check if the current database is marked as read-only."""
38
53
  return self.database == "read"
54
+
55
+ def _read_only_response(self):
56
+ """Standardized error response for blocked write operations."""
57
+ return "This user is not allowed to perform write operations.", 403
39
58
 
40
-
41
- # ---------- READ ---------- #
42
- def get_all(self):
59
+ def get_all(self, limit=1000000):
43
60
  """
44
- Retrieve all documents from this collection.
45
-
46
- Usage:
47
- result = collection.get_all()
61
+ Fetch all records from the collection (up to a limit).
62
+
63
+ Args:
64
+ limit (int): Max number of records to fetch.
65
+ Returns:
66
+ list: Collection of records.
48
67
  """
49
68
  self._print_loading()
50
- url = f"{self.base_url}/{self.user_id}/{self.db_id}/{self.coll_id}/get_all"
51
- response = requests.get(url)
52
- try:
53
- response.raise_for_status()
54
- return response.json()
55
- except requests.HTTPError as e:
56
- return {"status": "KO", "error": str(e), "response": response.json() if response.content else {}}
69
+ result, status_code = handle_get_all(
70
+ user_id=self.user_id,
71
+ db_id=self.db_id,
72
+ coll_id=self.coll_id,
73
+ limit=limit,
74
+ container_url=self.container_url,
75
+ )
76
+ return result
57
77
 
58
78
  def get_specific(self, filters: dict | None = None, limit: int = 1000):
59
79
  """
60
- Retrieve documents matching filters.
61
-
80
+ Fetch records that match specific key-value filters.
81
+
62
82
  Args:
63
- filters (dict): Field/value pairs for filtering.
64
- limit (int): Max number of docs to retrieve (max 500,000).
83
+ filters (dict): Dictionary of exact match filters.
84
+ limit (int): Max number of matching records to return.
65
85
 
66
- Usage:
67
- result = collection.get_specific(filters={"name": "John"}, limit=100)
86
+ Returns:
87
+ list: Filtered records from the collection.
68
88
  """
69
89
  if limit > 500_000:
70
90
  raise ValueError("Limit of 500,000 exceeded")
@@ -73,139 +93,141 @@ class Collection:
73
93
  if filters is None:
74
94
  filters = {}
75
95
 
76
- url = f"{self.base_url}/{self.user_id}/{self.db_id}/{self.coll_id}/get_specific"
77
- params = {**filters, "limit": limit}
78
- response = requests.get(url, params=params)
79
- try:
80
- response.raise_for_status()
81
- return response.json()
82
- except requests.HTTPError as e:
83
- return {"status": "KO", "error": str(e), "response": response.json() if response.content else {}}
96
+ search_criteria = [{k: v} for k, v in filters.items()]
97
+ print(search_criteria)
98
+
99
+ result, status_code = handle_get_data(
100
+ user_id=self.user_id,
101
+ db_id=self.db_id,
102
+ coll_id=self.coll_id,
103
+ search_criteria=search_criteria,
104
+ limit=limit,
105
+ container_url=self.container_url,
106
+ )
107
+ return result
84
108
 
85
- # ---------- CREATE ---------- #
86
- def insert_one(self, document: dict):
109
+ def insert_one(self, record):
87
110
  """
88
- Insert a single document.
111
+ Insert a single record into the collection.
89
112
 
90
113
  Args:
91
- document (dict): The document to insert.
114
+ record (dict): Record to insert.
92
115
 
93
- Usage:
94
- new_doc = {"name": "Alice", "age": 25}
95
- result = collection.insert_one(new_doc)
116
+ Returns:
117
+ tuple: (response message, HTTP status code)
96
118
  """
97
- self._print_loading()
98
119
  if self._is_read_only():
99
- return {"status": "KO", "error": "Method not allowed. Access is only read."}
100
-
101
- url = f"{self.base_url}/{self.user_id}/{self.db_id}/{self.coll_id}/insert_one"
102
- response = requests.post(url, json=document, headers={"Content-Type": "application/json"})
103
- try:
104
- response.raise_for_status()
105
- return response.json()
106
- except requests.HTTPError as e:
107
- return {"status": "KO", "error": str(e), "response": response.json() if response.content else {}}
120
+ return self._read_only_response()
121
+ return handle_insert_one(
122
+ user_id_cont=self.user_id,
123
+ database=self.db_id,
124
+ collection=self.coll_id,
125
+ record=record,
126
+ container_url=self.container_url,
127
+ )
108
128
 
109
- def insert_many(self, documents: list):
129
+ def insert_many(self, data):
110
130
  """
111
- Insert multiple documents at once.
131
+ Insert multiple records into the collection.
112
132
 
113
133
  Args:
114
- documents (list): List of dicts representing documents.
134
+ data (list of dicts): Multiple records to insert.
115
135
 
116
- Usage:
117
- docs = [{"name": "A"}, {"name": "B"}]
118
- result = collection.insert_many(docs)
136
+ Returns:
137
+ tuple: (response message, HTTP status code)
119
138
  """
120
- self._print_loading()
121
139
  if self._is_read_only():
122
- return {"status": "KO", "error": "Method not allowed. Access is only read."}
123
-
124
- url = f"{self.base_url}/{self.user_id}/{self.db_id}/{self.coll_id}/insert_many"
125
- response = requests.post(url, json={"records": documents})
126
- try:
127
- response.raise_for_status()
128
- return response.json()
129
- except requests.HTTPError as e:
130
- return {"status": "KO", "error": str(e), "response": response.json() if response.content else {}}
140
+ return self._read_only_response()
141
+ return handle_insert_many(
142
+ user_id_cont=self.user_id,
143
+ database=self.db_id,
144
+ collection=self.coll_id,
145
+ data=data,
146
+ container_url=self.container_url,
147
+ )
131
148
 
132
- # ---------- DELETE ---------- #
133
- def delete_one(self, filter_query: dict):
149
+ def delete_one(self, record_to_delete):
134
150
  """
135
- Delete a single document matching the query.
151
+ Delete a single record based on its _id or nested key.
136
152
 
137
153
  Args:
138
- filter_query (dict): A query to match one document.
154
+ record_to_delete (dict): Identification of the record.
139
155
 
140
- Usage:
141
- result = collection.delete_one({"name": "John"})
156
+ Returns:
157
+ tuple: (response message, HTTP status code)
142
158
  """
143
- self._print_loading()
144
159
  if self._is_read_only():
145
- return {"status": "KO", "error": "Method not allowed. Access is only read."}
146
-
147
- url = f"{self.base_url}/{self.user_id}/{self.db_id}/{self.coll_id}/delete_one"
148
- response = requests.delete(url, json=filter_query)
149
- try:
150
- response.raise_for_status()
151
- return response.json()
152
- except requests.HTTPError as e:
153
- return {"status": "KO", "error": str(e), "response": response.json() if response.content else {}}
160
+ return self._read_only_response()
161
+ return handle_delete_one(
162
+ user_id=self.user_id,
163
+ db_id=self.db_id,
164
+ coll_id=self.coll_id,
165
+ data_to_delete=record_to_delete,
166
+ container_url=self.container_url,
167
+ )
154
168
 
155
- def delete_many(self, filter_query: dict):
169
+ def delete_many(self, records_to_delete_list, batch_size=10):
156
170
  """
157
- Delete all documents matching the query.
171
+ Delete multiple records in batches.
158
172
 
159
173
  Args:
160
- filter_query (dict): A query to match multiple documents.
174
+ records_to_delete_list (list): List of record identifiers.
175
+ batch_size (int): How many to delete at once (for efficiency).
161
176
 
162
- Usage:
163
- result = collection.delete_many({"status": "inactive"})
177
+ Returns:
178
+ tuple: (response message, HTTP status code)
164
179
  """
165
- self._print_loading()
166
180
  if self._is_read_only():
167
- return {"status": "KO", "error": "Method not allowed. Access is only read."}
168
-
169
- url = f"{self.base_url}/{self.user_id}/{self.db_id}/{self.coll_id}/delete_many"
170
- response = requests.delete(url, json=filter_query)
171
- try:
172
- response.raise_for_status()
173
- return response.json()
174
- except requests.HTTPError as e:
175
- return {"status": "KO", "error": str(e), "response": response.json() if response.content else {}}
176
-
177
- # ---------- UPDATE ---------- #
181
+ return self._read_only_response()
182
+ return handle_delete_many(
183
+ user_id=self.user_id,
184
+ db_id=self.db_id,
185
+ coll_id=self.coll_id,
186
+ data_to_delete_list=records_to_delete_list,
187
+ batch_size=batch_size,
188
+ container_url=self.container_url,
189
+ )
190
+
178
191
  def update_query(self, filter_query: dict, update_query: dict):
179
192
  """
180
- Update documents matching a filter query.
193
+ Update a record in-place by filtering and applying update logic.
181
194
 
182
195
  Args:
183
- filter_query (dict): Query to find documents.
184
- update_query (dict): Update operations to apply.
196
+ filter_query (dict): Dict describing what record(s) to match.
197
+ update_query (dict): Dict describing update operators ($set, $push, $remove).
185
198
 
186
- Usage:
187
- result = collection.update_query(
188
- {"age": {"$gt": 30}},
189
- {"$set": {"status": "senior"}}
190
- )
199
+ Returns:
200
+ tuple: (response message, HTTP status code)
191
201
  """
192
- self._print_loading()
193
202
  if self._is_read_only():
194
- return {"status": "KO", "error": "Method not allowed. Access is only read."}
203
+ return self._read_only_response()
204
+ return handle_update_query(
205
+ user_id=self.user_id,
206
+ db_id=self.db_id,
207
+ coll_id=self.coll_id,
208
+ filter_query=filter_query,
209
+ update_query=update_query,
210
+ container_url=self.container_url,
211
+ )
195
212
 
196
- url = f"{self.base_url}/{self.user_id}/{self.db_id}/{self.coll_id}/update_query"
197
- response = requests.patch(url, json={"filter_query": filter_query, "update_query": update_query})
198
- try:
199
- response.raise_for_status()
200
- return response.json()
201
- except requests.HTTPError as e:
202
- return {"status": "KO", "error": str(e), "response": response.json() if response.content else {}}
203
-
204
- # ---------- dunder helpers ---------- #
205
- def __str__(self) -> str:
206
- """Return a human-readable JSON string of collection metadata."""
213
+ def ping(self):
214
+ """
215
+ Health check / ping to verify collection accessibility.
216
+
217
+ Returns:
218
+ tuple: (response message, HTTP status code)
219
+ """
220
+ return handle_connection_ping(
221
+ user_id=self.user_id,
222
+ db_id=self.db_id,
223
+ coll_id=self.coll_id,
224
+ container_url=self.container_url,
225
+ )
226
+
227
+ def __str__(self):
228
+ """Pretty print the collection metadata."""
207
229
  return json.dumps(self.metadata, indent=4)
208
230
 
209
- def __repr__(self) -> str:
210
- """Return a developer-friendly representation of the object."""
231
+ def __repr__(self):
232
+ """Simplified representation for debugging or introspection."""
211
233
  return f"<Collection coll_id={self.coll_id}>"