3
3
import os
4
4
from contextlib import asynccontextmanager , contextmanager
5
5
from datetime import datetime , timezone
6
- from typing import (Any , AsyncGenerator , ClassVar , Generator , List , Optional ,
7
- Type , TypeVar , Union )
6
+ from typing import Any , AsyncGenerator , ClassVar , Generator , List , Optional , Type , TypeVar , Union
8
7
9
8
from dotenv import load_dotenv
10
9
from pydantic import BaseModel , field_serializer
@@ -45,6 +44,14 @@ def _prepare_data(obj: BaseModel) -> str:
45
44
items .append (f"{ field_name } : { _prepare_value (value )} " )
46
45
return "{ " + ", " .join (items ) + " }"
47
46
47
+ def _log_query (query : str , result : Any = None ) -> None :
48
+ """Log query and result if debug is enabled"""
49
+ config = SurranticConfig .get_instance ()
50
+ if config .debug :
51
+ logger .debug ("Query: %s" , query )
52
+ if result is not None :
53
+ logger .debug ("Result: %s" , result )
54
+
48
55
class SurranticConfig :
49
56
"""Configuration class for Surrantic database connection.
50
57
@@ -59,6 +66,7 @@ def __init__(self):
59
66
self .password = SURREAL_PASS
60
67
self .namespace = SURREAL_NAMESPACE
61
68
self .database = SURREAL_DATABASE
69
+ self .debug = False
62
70
63
71
@classmethod
64
72
def get_instance (cls ) -> 'SurranticConfig' :
@@ -73,7 +81,8 @@ def configure(cls,
73
81
user : Optional [str ] = None ,
74
82
password : Optional [str ] = None ,
75
83
namespace : Optional [str ] = None ,
76
- database : Optional [str ] = None ) -> None :
84
+ database : Optional [str ] = None ,
85
+ debug : Optional [bool ] = None ) -> None :
77
86
"""Configure the database connection parameters.
78
87
79
88
Args:
@@ -82,6 +91,7 @@ def configure(cls,
82
91
password: The password for authentication
83
92
namespace: The namespace to use
84
93
database: The database to use
94
+ debug: If True, all queries and results will be logged
85
95
"""
86
96
config = cls .get_instance ()
87
97
if address is not None :
@@ -94,6 +104,8 @@ def configure(cls,
94
104
config .namespace = namespace
95
105
if database is not None :
96
106
config .database = database
107
+ if debug is not None :
108
+ config .debug = debug
97
109
98
110
class ObjectModel (BaseModel ):
99
111
"""Base model class for SurrealDB objects with CRUD operations.
@@ -170,60 +182,6 @@ def _get_sync_db(cls) -> Generator[SurrealDB, None, None]:
170
182
db .close ()
171
183
logger .debug ("Database connection closed" )
172
184
173
- async def asave (self ) -> None :
174
- """Asynchronously save or update the record in SurrealDB.
175
-
176
- Updates the created and updated timestamps automatically.
177
- Creates a new record if id is None, otherwise updates existing record.
178
-
179
- Raises:
180
- Exception: If table_name is not defined
181
- RuntimeError: If the database operation fails
182
- """
183
- if not self .created :
184
- self .created = datetime .now (timezone .utc ) # Make created timezone-aware
185
- self .updated = datetime .now (timezone .utc ) # Make updated timezone-aware
186
-
187
- if type (self ).table_name :
188
- table_name = type (self ).table_name
189
- else :
190
- raise Exception ("No table_name defined" )
191
-
192
- data = _prepare_data (self )
193
- logger .debug ("Prepared data for save: %s" , data )
194
-
195
- async with self ._get_db () as db :
196
- result = await db .query (f"UPSERT { table_name } CONTENT { data } " )
197
- self .id = result [0 ]["result" ][0 ]["id" ]
198
- logger .info ("Successfully saved record with ID: %s" , self .id )
199
-
200
- def save (self ) -> None :
201
- """Synchronously save or update the record in SurrealDB.
202
-
203
- Updates the created and updated timestamps automatically.
204
- Creates a new record if id is None, otherwise updates existing record.
205
-
206
- Raises:
207
- Exception: If table_name is not defined
208
- RuntimeError: If the database operation fails
209
- """
210
- if not self .created :
211
- self .created = datetime .now (timezone .utc )
212
- self .updated = datetime .now (timezone .utc )
213
-
214
- if type (self ).table_name :
215
- table_name = type (self ).table_name
216
- else :
217
- raise Exception ("No table_name defined" )
218
-
219
- data = _prepare_data (self )
220
- logger .debug ("Prepared data for save: %s" , data )
221
-
222
- with self ._get_sync_db () as db :
223
- result = db .query (f"UPSERT { table_name } CONTENT { data } " )
224
- self .id = result [0 ]["result" ][0 ]["id" ]
225
- logger .info ("Successfully saved record with ID: %s" , self .id )
226
-
227
185
@classmethod
228
186
async def aget_all (cls : Type [T ], order_by : Optional [str ] = None , order_direction : Optional [str ] = None ) -> List [T ]:
229
187
"""Asynchronously retrieve all records from the table.
@@ -239,23 +197,19 @@ async def aget_all(cls: Type[T], order_by: Optional[str] = None, order_direction
239
197
ValueError: If table_name is not set
240
198
RuntimeError: If the database operation fails
241
199
"""
242
- try :
243
- if cls .table_name :
244
- target_class = cls
245
- table_name = cls .table_name
246
- else :
247
- raise ValueError ("table_name not set in model class" )
248
-
249
- query = f"SELECT * FROM { table_name } "
250
- if order_by :
251
- query += f" ORDER BY { order_by } { order_direction } "
252
-
253
- async with cls ._get_db () as db :
254
- results = await db .query (query )
255
- return [target_class (** item ) for item in results [0 ]["result" ]]
256
- except Exception as e :
257
- logger .error ("Failed to fetch records: %s" , str (e ), exc_info = True )
258
- raise RuntimeError (f"Failed to fetch records: { str (e )} " )
200
+ if not cls .table_name :
201
+ raise ValueError ("table_name must be set" )
202
+
203
+ query = f"SELECT * FROM { cls .table_name } "
204
+ if order_by :
205
+ direction = order_direction or "ASC"
206
+ query += f" ORDER BY { order_by } { direction } "
207
+
208
+ _log_query (query )
209
+ async with cls ._get_db () as db :
210
+ result = await db .query (query )
211
+ _log_query (query , result )
212
+ return [cls (** item ) for item in result [0 ]["result" ]]
259
213
260
214
@classmethod
261
215
def get_all (cls : Type [T ], order_by : Optional [str ] = None , order_direction : Optional [str ] = None ) -> List [T ]:
@@ -272,23 +226,19 @@ def get_all(cls: Type[T], order_by: Optional[str] = None, order_direction: Optio
272
226
ValueError: If table_name is not set
273
227
RuntimeError: If the database operation fails
274
228
"""
275
- try :
276
- if cls .table_name :
277
- target_class = cls
278
- table_name = cls .table_name
279
- else :
280
- raise ValueError ("table_name not set in model class" )
281
-
282
- query = f"SELECT * FROM { table_name } "
283
- if order_by :
284
- query += f" ORDER BY { order_by } { order_direction } "
285
-
286
- with cls ._get_sync_db () as db :
287
- results = db .query (query )
288
- return [target_class (** item ) for item in results [0 ]["result" ]]
289
- except Exception as e :
290
- logger .error ("Failed to fetch records: %s" , str (e ), exc_info = True )
291
- raise RuntimeError (f"Failed to fetch records: { str (e )} " )
229
+ if not cls .table_name :
230
+ raise ValueError ("table_name must be set" )
231
+
232
+ query = f"SELECT * FROM { cls .table_name } "
233
+ if order_by :
234
+ direction = order_direction or "ASC"
235
+ query += f" ORDER BY { order_by } { direction } "
236
+
237
+ _log_query (query )
238
+ with cls ._get_sync_db () as db :
239
+ result = db .query (query )
240
+ _log_query (query , result )
241
+ return [cls (** item ) for item in result [0 ]["result" ]]
292
242
293
243
@classmethod
294
244
async def aget (cls : Type [T ], id : Union [str , RecordID ]) -> Optional [T ]:
@@ -303,16 +253,14 @@ async def aget(cls: Type[T], id: Union[str, RecordID]) -> Optional[T]:
303
253
Raises:
304
254
RuntimeError: If the database operation fails
305
255
"""
306
- try :
307
- async with cls ._get_db () as db :
308
- results = await db .select (id )
309
- if results is None :
310
- logger .info (f"No record found with ID: { id } " )
311
- return None
312
- return cls (** results )
313
- except Exception as e :
314
- logger .error ("Failed to fetch record: %s" , str (e ), exc_info = True )
315
- raise RuntimeError (f"Failed to fetch record: { str (e )} " )
256
+ query = f"SELECT * FROM { id } "
257
+ _log_query (query )
258
+ async with cls ._get_db () as db :
259
+ result = await db .query (query )
260
+ _log_query (query , result )
261
+ if result and result [0 ]:
262
+ return cls (** result [0 ][0 ])
263
+ return None
316
264
317
265
@classmethod
318
266
def get (cls : Type [T ], id : Union [str , RecordID ]) -> Optional [T ]:
@@ -327,34 +275,92 @@ def get(cls: Type[T], id: Union[str, RecordID]) -> Optional[T]:
327
275
Raises:
328
276
RuntimeError: If the database operation fails
329
277
"""
330
- try :
331
- with cls ._get_sync_db () as db :
332
- results = db .select (id )
333
- if results is None :
334
- logger .info (f"No record found with ID: { id } " )
335
- return None
336
- return cls (** results )
337
- except Exception as e :
338
- logger .error ("Failed to fetch record: %s" , str (e ), exc_info = True )
339
- raise RuntimeError (f"Failed to fetch record: { str (e )} " )
340
-
278
+ query = f"SELECT * FROM { id } "
279
+ _log_query (query )
280
+ with cls ._get_sync_db () as db :
281
+ result = db .query (query )
282
+ _log_query (query , result )
283
+ if result and result [0 ]:
284
+ return cls (** result [0 ][0 ])
285
+ return None
286
+
287
+ async def asave (self ) -> None :
288
+ """Asynchronously save or update the record in SurrealDB.
289
+
290
+ Updates the created and updated timestamps automatically.
291
+ Creates a new record if id is None, otherwise updates existing record.
292
+
293
+ Raises:
294
+ Exception: If table_name is not defined
295
+ RuntimeError: If the database operation fails
296
+ """
297
+ if not self .table_name :
298
+ raise ValueError ("table_name must be set" )
299
+
300
+ now = datetime .now (timezone .utc )
301
+ if not self .created :
302
+ self .created = now
303
+ self .updated = now
304
+
305
+ data = _prepare_data (self )
306
+ if self .id :
307
+ query = f"UPDATE { self .id } SET { data } "
308
+ else :
309
+ query = f"CREATE { self .table_name } SET { data } "
310
+
311
+ _log_query (query )
312
+ async with self ._get_db () as db :
313
+ result = await db .query (query )
314
+ _log_query (query , result )
315
+ if result and result [0 ]:
316
+ self .id = RecordID .from_string (result [0 ][0 ]["id" ])
317
+
318
+ def save (self ) -> None :
319
+ """Synchronously save or update the record in SurrealDB.
320
+
321
+ Updates the created and updated timestamps automatically.
322
+ Creates a new record if id is None, otherwise updates existing record.
323
+
324
+ Raises:
325
+ Exception: If table_name is not defined
326
+ RuntimeError: If the database operation fails
327
+ """
328
+ if not self .table_name :
329
+ raise ValueError ("table_name must be set" )
330
+
331
+ now = datetime .now (timezone .utc )
332
+ if not self .created :
333
+ self .created = now
334
+ self .updated = now
335
+
336
+ data = _prepare_data (self )
337
+ if self .id :
338
+ query = f"UPDATE { self .id } SET { data } "
339
+ else :
340
+ query = f"CREATE { self .table_name } SET { data } "
341
+
342
+ _log_query (query )
343
+ with self ._get_sync_db () as db :
344
+ result = db .query (query )
345
+ _log_query (query , result )
346
+ if result and result [0 ]:
347
+ self .id = RecordID .from_string (result [0 ][0 ]["id" ])
348
+
341
349
async def adelete (self ) -> None :
342
350
"""Asynchronously delete the record from the database.
343
351
344
352
Raises:
345
353
ValueError: If the record has no ID
346
354
RuntimeError: If the database operation fails
347
355
"""
348
- try :
349
- if not self .id :
350
- raise ValueError ("Cannot delete record without id" )
356
+ if not self .id :
357
+ raise ValueError ("Cannot delete record without ID" )
351
358
352
- async with self ._get_db () as db :
353
- await db .delete (self .id )
354
- logger .info ("Successfully deleted record with ID: %s" , self .id )
355
- except Exception as e :
356
- logger .error ("Failed to delete record: %s" , str (e ), exc_info = True )
357
- raise RuntimeError (f"Failed to delete record: { str (e )} " )
359
+ query = f"DELETE { self .id } "
360
+ _log_query (query )
361
+ async with self ._get_db () as db :
362
+ result = await db .query (query )
363
+ _log_query (query , result )
358
364
359
365
def delete (self ) -> None :
360
366
"""Synchronously delete the record from the database.
@@ -363,13 +369,11 @@ def delete(self) -> None:
363
369
ValueError: If the record has no ID
364
370
RuntimeError: If the database operation fails
365
371
"""
366
- try :
367
- if not self .id :
368
- raise ValueError ("Cannot delete record without id" )
369
-
370
- with self ._get_sync_db () as db :
371
- db .delete (self .id )
372
- logger .info ("Successfully deleted record with ID: %s" , self .id )
373
- except Exception as e :
374
- logger .error ("Failed to delete record: %s" , str (e ), exc_info = True )
375
- raise RuntimeError (f"Failed to delete record: { str (e )} " )
372
+ if not self .id :
373
+ raise ValueError ("Cannot delete record without ID" )
374
+
375
+ query = f"DELETE { self .id } "
376
+ _log_query (query )
377
+ with self ._get_sync_db () as db :
378
+ result = db .query (query )
379
+ _log_query (query , result )
0 commit comments