surreal-orm-lite 0.2.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.
@@ -0,0 +1,24 @@
1
+ from .connection_manager import SurrealDBConnectionManager
2
+ from .enum import OrderBy
3
+ from .exceptions import (
4
+ SurrealDbConnectionError,
5
+ SurrealDbError,
6
+ SurrealDbNotFoundError,
7
+ SurrealDbValidationError,
8
+ SurrealORMError,
9
+ )
10
+ from .model_base import BaseSurrealModel, SurrealConfigDict
11
+ from .query_set import QuerySet
12
+
13
+ __all__ = [
14
+ "SurrealDBConnectionManager",
15
+ "BaseSurrealModel",
16
+ "QuerySet",
17
+ "OrderBy",
18
+ "SurrealConfigDict",
19
+ "SurrealORMError",
20
+ "SurrealDbError",
21
+ "SurrealDbConnectionError",
22
+ "SurrealDbValidationError",
23
+ "SurrealDbNotFoundError",
24
+ ]
@@ -0,0 +1,313 @@
1
+ import contextlib
2
+ import logging
3
+ from typing import Any
4
+
5
+ from surrealdb import AsyncSurreal
6
+
7
+ from .exceptions import SurrealDbConnectionError
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class SurrealDBConnectionManager:
13
+ __url: str | None = None
14
+ __user: str | None = None
15
+ __password: str | None = None
16
+ __namespace: str | None = None
17
+ __database: str | None = None
18
+ __client: Any = None
19
+
20
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
21
+ await SurrealDBConnectionManager.close_connection()
22
+
23
+ async def __aenter__(self) -> Any:
24
+ return await SurrealDBConnectionManager.get_client()
25
+
26
+ @classmethod
27
+ def set_connection(cls, url: str, user: str, password: str, namespace: str, database: str) -> None:
28
+ """
29
+ Set the connection kwargs for the SurrealDB instance.
30
+
31
+ :param kwargs: The connection kwargs for the SurrealDB instance.
32
+ """
33
+ cls.__url = url
34
+ cls.__user = user
35
+ cls.__password = password
36
+ cls.__namespace = namespace
37
+ cls.__database = database
38
+
39
+ @classmethod
40
+ async def unset_connection(cls) -> None:
41
+ """
42
+ Set the connection kwargs for the SurrealDB instance.
43
+
44
+ :param kwargs: The connection kwargs for the SurrealDB instance.
45
+ """
46
+ cls.__url = None
47
+ cls.__user = None
48
+ cls.__password = None
49
+ cls.__namespace = None
50
+ cls.__database = None
51
+ await cls.close_connection()
52
+
53
+ @classmethod
54
+ def is_connection_set(cls) -> bool:
55
+ """
56
+ Check if the connection kwargs are set.
57
+
58
+ :return: True if the connection kwargs are set, False otherwise.
59
+ """
60
+ return all([cls.__url, cls.__user, cls.__password, cls.__namespace, cls.__database])
61
+
62
+ @classmethod
63
+ async def get_client(cls) -> Any:
64
+ """
65
+ Connect to the SurrealDB instance.
66
+
67
+ :return: The SurrealDB instance.
68
+ """
69
+
70
+ if cls.__client is not None:
71
+ return cls.__client
72
+
73
+ if not cls.is_connection_set():
74
+ raise ValueError("Connection not been set.")
75
+
76
+ # After is_connection_set(), these are guaranteed to be non-None
77
+ assert cls.__url is not None
78
+ assert cls.__namespace is not None
79
+ assert cls.__database is not None
80
+ assert cls.__user is not None
81
+ assert cls.__password is not None
82
+
83
+ # Établir la connexion
84
+ try:
85
+ url = cls.__url
86
+ _client = AsyncSurreal(url)
87
+
88
+ # WebSocket connections require explicit connect()
89
+ if url.startswith(("ws://", "wss://")):
90
+ await _client.connect(url)
91
+
92
+ await _client.use(cls.__namespace, cls.__database)
93
+ await _client.signin({"username": cls.__user, "password": cls.__password})
94
+
95
+ cls.__client = _client
96
+ return cls.__client
97
+ except Exception as e:
98
+ logger.warning(f"Can't get connection: {e}")
99
+ if cls.__client is not None: # pragma: no cover
100
+ with contextlib.suppress(NotImplementedError):
101
+ await cls.__client.close()
102
+ cls.__client = None
103
+ raise SurrealDbConnectionError("Can't connect to the database.") from None
104
+
105
+ @classmethod
106
+ async def close_connection(cls) -> None:
107
+ """
108
+ Close the connection to the SurrealDB instance.
109
+ """
110
+ # Fermer la connexion
111
+
112
+ if cls.__client is None:
113
+ return
114
+
115
+ with contextlib.suppress(NotImplementedError):
116
+ await cls.__client.close()
117
+ cls.__client = None
118
+
119
+ @classmethod
120
+ async def reconnect(cls) -> Any:
121
+ """
122
+ Reconnect to the SurrealDB instance.
123
+ """
124
+ # Fermer la connexion
125
+ await cls.close_connection()
126
+ # Établir la connexion
127
+ return await cls.get_client()
128
+
129
+ @classmethod
130
+ async def validate_connection(cls) -> bool:
131
+ """
132
+ Validate the connection to the SurrealDB instance.
133
+
134
+ :return: True if the connection is valid, False otherwise.
135
+ """
136
+ # Valider la connexion
137
+ try:
138
+ await cls.reconnect()
139
+ return True
140
+ except SurrealDbConnectionError:
141
+ return False
142
+
143
+ @classmethod
144
+ def get_connection_string(cls) -> str | None:
145
+ """
146
+ Get the connection string for the SurrealDB instance.
147
+
148
+ :return: The connection string for the SurrealDB instance.
149
+ """
150
+ return cls.__url
151
+
152
+ @classmethod
153
+ def get_connection_kwargs(cls) -> dict[str, str | None]:
154
+ """
155
+ Get the connection kwargs for the SurrealDB instance.
156
+
157
+ :return: The connection kwargs for the SurrealDB instance.
158
+ """
159
+ return {
160
+ "url": cls.__url,
161
+ "user": cls.__user,
162
+ "namespace": cls.__namespace,
163
+ "database": cls.__database,
164
+ }
165
+
166
+ @classmethod
167
+ async def set_url(cls, url: str, reconnect: bool = False) -> bool:
168
+ """
169
+ Set the URL for the SurrealDB instance.
170
+
171
+ :param url: The URL of the SurrealDB instance.
172
+ """
173
+
174
+ if not cls.is_connection_set():
175
+ raise ValueError("You can't change the URL when the others setting are not already set.")
176
+
177
+ cls.__url = url
178
+
179
+ if reconnect and not await cls.validate_connection(): # pragma: no cover
180
+ cls.__url = None
181
+ return False
182
+
183
+ return True
184
+
185
+ @classmethod
186
+ async def set_user(cls, user: str, reconnect: bool = False) -> bool:
187
+ """
188
+ Set the username for authentication.
189
+
190
+ :param user: The username for authentication.
191
+ """
192
+
193
+ if not cls.is_connection_set():
194
+ raise ValueError("You can't change the User when the others setting are not already set.")
195
+
196
+ cls.__user = user
197
+
198
+ if reconnect and not await cls.validate_connection(): # pragma: no cover
199
+ cls.__user = None
200
+ return False
201
+
202
+ return True
203
+
204
+ @classmethod
205
+ async def set_password(cls, password: str, reconnect: bool = False) -> bool:
206
+ """
207
+ Set the password for authentication.
208
+
209
+ :param password: The password for authentication.
210
+ """
211
+
212
+ if not cls.is_connection_set():
213
+ raise ValueError("You can't change the password when the others setting are not already set.")
214
+
215
+ cls.__password = password
216
+
217
+ if reconnect and not await cls.validate_connection(): # pragma: no cover
218
+ cls.__password = None
219
+ return False
220
+
221
+ return True
222
+
223
+ @classmethod
224
+ async def set_namespace(cls, namespace: str, reconnect: bool = False) -> bool:
225
+ """
226
+ Set the namespace to use.
227
+
228
+ :param namespace: The namespace to use.
229
+ """
230
+
231
+ if not cls.is_connection_set():
232
+ raise ValueError("You can't change the namespace when the others setting are not already set.")
233
+
234
+ cls.__namespace = namespace
235
+
236
+ if reconnect and not await cls.validate_connection(): # pragma: no cover
237
+ cls.__namespace = None
238
+ return False
239
+
240
+ return True
241
+
242
+ @classmethod
243
+ async def set_database(cls, database: str, reconnect: bool = False) -> bool:
244
+ """
245
+ Set the database to use.
246
+
247
+ :param database: The database to use.
248
+ """
249
+ if not cls.is_connection_set():
250
+ raise ValueError("You can't change the database when the others setting are not already set.")
251
+
252
+ cls.__database = database
253
+
254
+ if reconnect and not await cls.validate_connection(): # pragma: no cover
255
+ cls.__database = None
256
+ return False
257
+
258
+ return True
259
+
260
+ @classmethod
261
+ def get_url(cls) -> str | None:
262
+ """
263
+ Get the URL of the SurrealDB instance.
264
+
265
+ :return: The URL of the SurrealDB instance.
266
+ """
267
+ return cls.__url
268
+
269
+ @classmethod
270
+ def get_user(cls) -> str | None:
271
+ """
272
+ Get the username for authentication.
273
+
274
+ :return: The username for authentication.
275
+ """
276
+ return cls.__user
277
+
278
+ @classmethod
279
+ def get_namespace(cls) -> str | None:
280
+ """
281
+ Get the namespace to use.
282
+
283
+ :return: The namespace to use.
284
+ """
285
+ return cls.__namespace
286
+
287
+ @classmethod
288
+ def get_database(cls) -> str | None:
289
+ """
290
+ Get the database to use.
291
+
292
+ :return: The database to use.
293
+ """
294
+ return cls.__database
295
+
296
+ @classmethod
297
+ def is_password_set(cls) -> bool:
298
+ """
299
+ Get the database to use.
300
+
301
+ :return: The database to use.
302
+ """
303
+ return cls.__password is not None
304
+
305
+ @classmethod
306
+ def is_connected(cls) -> bool:
307
+ """
308
+ Check if the connection to the SurrealDB instance is established.
309
+
310
+ :return: True if the connection is established, False otherwise.
311
+ """
312
+
313
+ return cls.__client is not None
@@ -0,0 +1,20 @@
1
+ LOOKUP_OPERATORS = {
2
+ "exact": "=",
3
+ "gt": ">",
4
+ "gte": ">=",
5
+ "lt": "<",
6
+ "lte": "<=",
7
+ "in": "IN",
8
+ "like": "LIKE",
9
+ "ilike": "ILIKE",
10
+ "contains": "CONTAINS",
11
+ "icontains": "CONTAINS",
12
+ "startswith": "STARTSWITH",
13
+ "istartswith": "STARTSWITH",
14
+ "endswith": "ENDSWITH",
15
+ "iendswith": "ENDSWITH",
16
+ "match": "MATCH",
17
+ "regex": "REGEX",
18
+ "iregex": "REGEX",
19
+ "isnull": "IS",
20
+ }
@@ -0,0 +1,12 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class OrderBy(StrEnum):
5
+ ASC = "ASC"
6
+ DESC = "DESC"
7
+
8
+
9
+ class Operator(StrEnum):
10
+ AND = "AND"
11
+ OR = "OR"
12
+ NOT = "NOT"
@@ -0,0 +1,54 @@
1
+ """
2
+ Custom exceptions for SurrealDB ORM.
3
+
4
+ These exceptions provide a consistent error handling interface
5
+ for the ORM, independent of the underlying SDK error types.
6
+ """
7
+
8
+
9
+ class SurrealORMError(Exception):
10
+ """Base exception for all SurrealDB ORM errors."""
11
+
12
+ pass
13
+
14
+
15
+ class SurrealDbError(SurrealORMError):
16
+ """
17
+ General database error.
18
+
19
+ Raised when a database operation fails for reasons
20
+ like invalid data, constraint violations, etc.
21
+ """
22
+
23
+ pass
24
+
25
+
26
+ class SurrealDbConnectionError(SurrealORMError):
27
+ """
28
+ Connection error.
29
+
30
+ Raised when the connection to SurrealDB cannot be established
31
+ or when an existing connection is lost.
32
+ """
33
+
34
+ pass
35
+
36
+
37
+ class SurrealDbValidationError(SurrealORMError):
38
+ """
39
+ Validation error.
40
+
41
+ Raised when data validation fails before sending to the database.
42
+ """
43
+
44
+ pass
45
+
46
+
47
+ class SurrealDbNotFoundError(SurrealORMError):
48
+ """
49
+ Not found error.
50
+
51
+ Raised when a requested record or resource is not found.
52
+ """
53
+
54
+ pass
@@ -0,0 +1,212 @@
1
+ import logging
2
+ from typing import Any, Self
3
+
4
+ from pydantic import BaseModel, ConfigDict, model_validator
5
+ from surrealdb import RecordID
6
+
7
+ from .connection_manager import SurrealDBConnectionManager
8
+ from .exceptions import SurrealDbError
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class SurrealConfigDict(ConfigDict):
14
+ """
15
+ SurrealConfigDict is a configuration dictionary for SurrealDB models.
16
+
17
+ Attributes:
18
+ primary_key (str | None): The primary key field name for the model.
19
+ """
20
+
21
+ primary_key: str | None
22
+ " The primary key field name for the model. "
23
+
24
+
25
+ class BaseSurrealModel(BaseModel):
26
+ """
27
+ Base class for models interacting with SurrealDB.
28
+ """
29
+
30
+ @classmethod
31
+ def get_table_name(cls) -> str:
32
+ """
33
+ Get the table name for the model.
34
+ """
35
+ return cls.__name__
36
+
37
+ @classmethod
38
+ def get_index_primary_key(cls) -> str | None:
39
+ """
40
+ Get the primary key field name for the model.
41
+ """
42
+ if hasattr(cls, "model_config"): # pragma: no cover
43
+ primary_key = cls.model_config.get("primary_key", None)
44
+ if isinstance(primary_key, str):
45
+ return primary_key
46
+
47
+ return None
48
+
49
+ def get_id(self) -> None | str | RecordID:
50
+ """
51
+ Get the ID of the model instance.
52
+ """
53
+ if hasattr(self, "id"):
54
+ id_value = self.id
55
+ return str(id_value) if id_value is not None else None
56
+
57
+ if hasattr(self, "model_config"):
58
+ primary_key = self.model_config.get("primary_key", None)
59
+ if isinstance(primary_key, str) and hasattr(self, primary_key):
60
+ primary_key_value = getattr(self, primary_key)
61
+ return str(primary_key_value) if primary_key_value is not None else None
62
+
63
+ return None # pragma: no cover
64
+
65
+ @classmethod
66
+ def from_db(cls, record: dict | list) -> Self | list[Self]:
67
+ """
68
+ Create an instance from a SurrealDB record.
69
+ """
70
+ if isinstance(record, list):
71
+ return [cls.from_db(rs) for rs in record] # type: ignore
72
+
73
+ return cls(**record)
74
+
75
+ @model_validator(mode="before")
76
+ @classmethod
77
+ def set_data(cls, data: Any) -> Any:
78
+ """
79
+ Set the ID of the model instance.
80
+ """
81
+ if isinstance(data, dict): # pragma: no cover
82
+ if "id" in data and isinstance(data["id"], RecordID):
83
+ data["id"] = str(data["id"]).split(":")[1]
84
+ return data
85
+
86
+ async def refresh(self) -> None:
87
+ """
88
+ Refresh the model instance from the database.
89
+ """
90
+ if not self.get_id():
91
+ raise SurrealDbError("Can't refresh data, not recorded yet.") # pragma: no cover
92
+
93
+ client = await SurrealDBConnectionManager.get_client()
94
+ record = await client.select(f"{self.get_table_name()}:{self.get_id()}")
95
+
96
+ if record is None:
97
+ raise SurrealDbError("Can't refresh data, no record found.") # pragma: no cover
98
+
99
+ self.from_db(record)
100
+ return None
101
+
102
+ async def save(self) -> Self:
103
+ """
104
+ Save the model instance to the database.
105
+ """
106
+ client = await SurrealDBConnectionManager.get_client()
107
+ data = self.model_dump(exclude={"id"})
108
+ id = self.get_id()
109
+ table = self.get_table_name()
110
+
111
+ if id is not None:
112
+ # Escape special characters in ID
113
+ escaped_id = f"`{id}`" if any(c in str(id) for c in "@#$%^&*()") else id
114
+ thing = f"{table}:{escaped_id}"
115
+ result = await client.create(thing, data)
116
+ # SDK 1.0.8 returns error message as string instead of raising exception
117
+ if isinstance(result, str) and "already exists" in result:
118
+ raise SurrealDbError(f"There was a problem with the database: {result}")
119
+ return self
120
+
121
+ # Auto-generate the ID
122
+ record = await client.create(table, data) # pragma: no cover
123
+
124
+ # SDK 1.0.8 returns error message as string
125
+ if isinstance(record, str):
126
+ raise SurrealDbError(f"Can't save data: {record}") # pragma: no cover
127
+
128
+ if isinstance(record, list):
129
+ raise SurrealDbError("Can't save data, multiple records returned.") # pragma: no cover
130
+
131
+ if record is None:
132
+ raise SurrealDbError("Can't save data, no record returned.") # pragma: no cover
133
+
134
+ obj = self.from_db(record)
135
+ if isinstance(obj, type(self)):
136
+ self = obj
137
+ return self
138
+
139
+ raise SurrealDbError("Can't save data, no record returned.") # pragma: no cover
140
+
141
+ async def update(self) -> Any:
142
+ """
143
+ Update the model instance to the database.
144
+ """
145
+ client = await SurrealDBConnectionManager.get_client()
146
+
147
+ data = self.model_dump(exclude={"id"})
148
+ id = self.get_id()
149
+ if id is not None:
150
+ thing = f"{self.__class__.__name__}:{id}"
151
+ test = await client.update(thing, data)
152
+ return test
153
+ raise SurrealDbError("Can't update data, no id found.")
154
+
155
+ async def merge(self, **data: Any) -> Any:
156
+ """
157
+ Update the model instance to the database.
158
+ """
159
+
160
+ client = await SurrealDBConnectionManager.get_client()
161
+ data_set = dict(data.items())
162
+
163
+ id = self.get_id()
164
+ if id:
165
+ thing = f"{self.get_table_name()}:{id}"
166
+
167
+ await client.merge(thing, data_set)
168
+ await self.refresh()
169
+ return
170
+
171
+ raise SurrealDbError(f"No Id for the data to merge: {data}")
172
+
173
+ async def delete(self) -> None:
174
+ """
175
+ Delete the model instance from the database.
176
+ """
177
+
178
+ client = await SurrealDBConnectionManager.get_client()
179
+
180
+ id = self.get_id()
181
+
182
+ thing = f"{self.get_table_name()}:{id}"
183
+
184
+ deleted = await client.delete(thing)
185
+
186
+ if not deleted:
187
+ raise SurrealDbError(f"Can't delete Record id -> '{id}' not found!")
188
+
189
+ logger.info(f"Record deleted -> {deleted}.")
190
+ del self
191
+
192
+ @model_validator(mode="after")
193
+ def check_config(self) -> Self:
194
+ """
195
+ Check the model configuration.
196
+ """
197
+
198
+ if not self.get_index_primary_key() and not hasattr(self, "id"):
199
+ raise SurrealDbError( # pragma: no cover
200
+ "Can't create model, the model need either 'id' field or primirary_key in 'model_config'."
201
+ )
202
+
203
+ return self
204
+
205
+ @classmethod
206
+ def objects(cls) -> Any:
207
+ """
208
+ Return a QuerySet for the model class.
209
+ """
210
+ from .query_set import QuerySet
211
+
212
+ return QuerySet(cls)
File without changes