surrealdb-orm 0.5.1__py3-none-any.whl → 0.5.3__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.
- surreal_orm/__init__.py +1 -1
- surreal_orm/connection_manager.py +20 -3
- surreal_orm/migrations/operations.py +79 -12
- surreal_orm/model_base.py +76 -17
- surreal_orm/types.py +98 -6
- surreal_sdk/__init__.py +1 -1
- surreal_sdk/connection/base.py +19 -2
- surreal_sdk/connection/http.py +9 -6
- surreal_sdk/connection/websocket.py +16 -5
- surreal_sdk/protocol/rpc.py +32 -2
- surreal_sdk/pyproject.toml +1 -1
- surreal_sdk/transaction.py +20 -0
- {surrealdb_orm-0.5.1.dist-info → surrealdb_orm-0.5.3.dist-info}/METADATA +79 -10
- {surrealdb_orm-0.5.1.dist-info → surrealdb_orm-0.5.3.dist-info}/RECORD +17 -17
- {surrealdb_orm-0.5.1.dist-info → surrealdb_orm-0.5.3.dist-info}/WHEEL +0 -0
- {surrealdb_orm-0.5.1.dist-info → surrealdb_orm-0.5.3.dist-info}/entry_points.txt +0 -0
- {surrealdb_orm-0.5.1.dist-info → surrealdb_orm-0.5.3.dist-info}/licenses/LICENSE +0 -0
surreal_orm/__init__.py
CHANGED
|
@@ -29,14 +29,31 @@ class SurrealDBConnectionManager:
|
|
|
29
29
|
return await SurrealDBConnectionManager.get_client()
|
|
30
30
|
|
|
31
31
|
@classmethod
|
|
32
|
-
def set_connection(
|
|
32
|
+
def set_connection(
|
|
33
|
+
cls,
|
|
34
|
+
url: str,
|
|
35
|
+
user: str,
|
|
36
|
+
password: str,
|
|
37
|
+
namespace: str,
|
|
38
|
+
database: str,
|
|
39
|
+
*,
|
|
40
|
+
username: str | None = None,
|
|
41
|
+
) -> None:
|
|
33
42
|
"""
|
|
34
43
|
Set the connection kwargs for the SurrealDB instance.
|
|
35
44
|
|
|
36
|
-
:param
|
|
45
|
+
:param url: The URL of the SurrealDB instance.
|
|
46
|
+
:param user: The username for authentication.
|
|
47
|
+
:param password: The password for authentication.
|
|
48
|
+
:param namespace: The namespace to use.
|
|
49
|
+
:param database: The database to use.
|
|
50
|
+
:param username: Keyword-only alias for 'user' (overrides 'user' if provided).
|
|
37
51
|
"""
|
|
52
|
+
# Allow 'username' keyword to override 'user' for API flexibility
|
|
53
|
+
actual_user = username if username is not None else user
|
|
54
|
+
|
|
38
55
|
cls.__url = url
|
|
39
|
-
cls.__user =
|
|
56
|
+
cls.__user = actual_user
|
|
40
57
|
cls.__password = password
|
|
41
58
|
cls.__namespace = namespace
|
|
42
59
|
cls.__database = database
|
|
@@ -9,6 +9,53 @@ from abc import ABC, abstractmethod
|
|
|
9
9
|
from dataclasses import dataclass, field
|
|
10
10
|
from typing import Any, Callable, Coroutine
|
|
11
11
|
|
|
12
|
+
from ..types import FieldType
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _normalize_field_type(field_type: FieldType | str) -> str:
|
|
16
|
+
"""
|
|
17
|
+
Normalize a field type to its string representation.
|
|
18
|
+
|
|
19
|
+
Accepts FieldType enum or string. For strings, validates that it's either
|
|
20
|
+
a known FieldType value or a valid generic type (e.g., "array<string>").
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
field_type: FieldType enum or string type specification
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
String representation of the type for SurrealQL
|
|
27
|
+
|
|
28
|
+
Raises:
|
|
29
|
+
ValueError: If the string is not a valid SurrealDB type
|
|
30
|
+
"""
|
|
31
|
+
if isinstance(field_type, FieldType):
|
|
32
|
+
return field_type.value
|
|
33
|
+
|
|
34
|
+
# Check if it's a known base type
|
|
35
|
+
try:
|
|
36
|
+
return FieldType(field_type).value
|
|
37
|
+
except ValueError:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
# Check if it's a generic type (e.g., "array<string>", "record<users>")
|
|
41
|
+
if "<" in field_type and field_type.endswith(">"):
|
|
42
|
+
base_type = field_type.split("<")[0]
|
|
43
|
+
try:
|
|
44
|
+
FieldType(base_type)
|
|
45
|
+
return field_type # Valid generic type
|
|
46
|
+
except ValueError:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
# Check for union types (e.g., "int | null", "option<string>")
|
|
50
|
+
if "|" in field_type:
|
|
51
|
+
return field_type # Allow union types
|
|
52
|
+
|
|
53
|
+
raise ValueError(
|
|
54
|
+
f"Invalid field type: '{field_type}'. "
|
|
55
|
+
f"Must be a FieldType enum value, a valid SurrealDB type string, "
|
|
56
|
+
f"or a generic type like 'array<string>' or 'record<users>'."
|
|
57
|
+
)
|
|
58
|
+
|
|
12
59
|
|
|
13
60
|
@dataclass
|
|
14
61
|
class Operation(ABC):
|
|
@@ -123,17 +170,24 @@ class AddField(Operation):
|
|
|
123
170
|
AddField(
|
|
124
171
|
table="users",
|
|
125
172
|
name="email",
|
|
126
|
-
field_type="string"
|
|
173
|
+
field_type=FieldType.STRING, # or "string"
|
|
127
174
|
assertion="is::email($value)"
|
|
128
175
|
)
|
|
129
176
|
|
|
177
|
+
# With generic types
|
|
178
|
+
AddField(
|
|
179
|
+
table="users",
|
|
180
|
+
name="tags",
|
|
181
|
+
field_type=FieldType.ARRAY.generic("string"), # "array<string>"
|
|
182
|
+
)
|
|
183
|
+
|
|
130
184
|
Generates:
|
|
131
185
|
DEFINE FIELD email ON users TYPE string ASSERT is::email($value);
|
|
132
186
|
"""
|
|
133
187
|
|
|
134
188
|
table: str
|
|
135
189
|
name: str
|
|
136
|
-
field_type: str
|
|
190
|
+
field_type: FieldType | str
|
|
137
191
|
default: Any = None
|
|
138
192
|
assertion: str | None = None
|
|
139
193
|
encrypted: bool = False
|
|
@@ -142,13 +196,19 @@ class AddField(Operation):
|
|
|
142
196
|
value: str | None = None
|
|
143
197
|
comment: str | None = None
|
|
144
198
|
|
|
199
|
+
def __post_init__(self) -> None:
|
|
200
|
+
"""Validate field_type on initialization."""
|
|
201
|
+
# Validate the field type (raises ValueError if invalid)
|
|
202
|
+
_normalize_field_type(self.field_type)
|
|
203
|
+
|
|
145
204
|
def forwards(self) -> str:
|
|
146
205
|
parts = [f"DEFINE FIELD {self.name} ON {self.table}"]
|
|
147
206
|
|
|
148
207
|
if self.flexible:
|
|
149
208
|
parts.append("FLEXIBLE")
|
|
150
209
|
|
|
151
|
-
|
|
210
|
+
normalized_type = _normalize_field_type(self.field_type)
|
|
211
|
+
parts.append(f"TYPE {normalized_type}")
|
|
152
212
|
|
|
153
213
|
# For encrypted fields, use VALUE clause with crypto function
|
|
154
214
|
if self.encrypted:
|
|
@@ -224,7 +284,7 @@ class AlterField(Operation):
|
|
|
224
284
|
AlterField(
|
|
225
285
|
table="users",
|
|
226
286
|
name="email",
|
|
227
|
-
field_type="string"
|
|
287
|
+
field_type=FieldType.STRING, # or "string"
|
|
228
288
|
assertion="is::email($value)"
|
|
229
289
|
)
|
|
230
290
|
|
|
@@ -234,7 +294,7 @@ class AlterField(Operation):
|
|
|
234
294
|
|
|
235
295
|
table: str
|
|
236
296
|
name: str
|
|
237
|
-
field_type: str | None = None
|
|
297
|
+
field_type: FieldType | str | None = None
|
|
238
298
|
default: Any = None
|
|
239
299
|
assertion: str | None = None
|
|
240
300
|
encrypted: bool = False
|
|
@@ -242,10 +302,19 @@ class AlterField(Operation):
|
|
|
242
302
|
readonly: bool = False
|
|
243
303
|
value: str | None = None
|
|
244
304
|
# Store previous definition for rollback
|
|
245
|
-
previous_type: str | None = None
|
|
305
|
+
previous_type: FieldType | str | None = None
|
|
246
306
|
previous_default: Any = None
|
|
247
307
|
previous_assertion: str | None = None
|
|
248
308
|
|
|
309
|
+
def __post_init__(self) -> None:
|
|
310
|
+
"""Validate field_type and set reversible based on previous state."""
|
|
311
|
+
# Validate field types if provided
|
|
312
|
+
if self.field_type is not None:
|
|
313
|
+
_normalize_field_type(self.field_type)
|
|
314
|
+
if self.previous_type is not None:
|
|
315
|
+
_normalize_field_type(self.previous_type)
|
|
316
|
+
object.__setattr__(self, "reversible", self.previous_type is not None)
|
|
317
|
+
|
|
249
318
|
def forwards(self) -> str:
|
|
250
319
|
# DEFINE FIELD is idempotent - it creates or updates
|
|
251
320
|
parts = [f"DEFINE FIELD {self.name} ON {self.table}"]
|
|
@@ -254,7 +323,8 @@ class AlterField(Operation):
|
|
|
254
323
|
parts.append("FLEXIBLE")
|
|
255
324
|
|
|
256
325
|
if self.field_type:
|
|
257
|
-
|
|
326
|
+
normalized_type = _normalize_field_type(self.field_type)
|
|
327
|
+
parts.append(f"TYPE {normalized_type}")
|
|
258
328
|
|
|
259
329
|
if self.encrypted:
|
|
260
330
|
parts.append("VALUE crypto::argon2::generate($value)")
|
|
@@ -284,7 +354,8 @@ class AlterField(Operation):
|
|
|
284
354
|
if not self.previous_type:
|
|
285
355
|
return ""
|
|
286
356
|
|
|
287
|
-
|
|
357
|
+
normalized_prev_type = _normalize_field_type(self.previous_type)
|
|
358
|
+
parts = [f"DEFINE FIELD {self.name} ON {self.table} TYPE {normalized_prev_type}"]
|
|
288
359
|
|
|
289
360
|
if self.previous_default is not None:
|
|
290
361
|
if isinstance(self.previous_default, str):
|
|
@@ -297,10 +368,6 @@ class AlterField(Operation):
|
|
|
297
368
|
|
|
298
369
|
return " ".join(parts) + ";"
|
|
299
370
|
|
|
300
|
-
def __post_init__(self) -> None:
|
|
301
|
-
"""Set reversible based on whether previous state is stored."""
|
|
302
|
-
object.__setattr__(self, "reversible", self.previous_type is not None)
|
|
303
|
-
|
|
304
371
|
def describe(self) -> str:
|
|
305
372
|
return f"Alter field {self.name} on {self.table}"
|
|
306
373
|
|
surreal_orm/model_base.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Any, Literal, Self,
|
|
1
|
+
from typing import Any, Literal, Self, TYPE_CHECKING
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel, ConfigDict, model_validator
|
|
4
4
|
|
|
@@ -71,6 +71,10 @@ class SurrealConfigDict(ConfigDict):
|
|
|
71
71
|
password_field: Field containing password (USER type, default: "password")
|
|
72
72
|
token_duration: JWT token duration (USER type, default: "15m")
|
|
73
73
|
session_duration: Session duration (USER type, default: "12h")
|
|
74
|
+
server_fields: List of field names that are server-generated and should
|
|
75
|
+
be excluded from save/update operations (e.g., ["created_at", "updated_at"]).
|
|
76
|
+
These fields are populated by SurrealDB's VALUE clause and should not be
|
|
77
|
+
sent back during updates.
|
|
74
78
|
"""
|
|
75
79
|
|
|
76
80
|
primary_key: str | None
|
|
@@ -83,6 +87,7 @@ class SurrealConfigDict(ConfigDict):
|
|
|
83
87
|
password_field: str | None
|
|
84
88
|
token_duration: str | None
|
|
85
89
|
session_duration: str | None
|
|
90
|
+
server_fields: list[str] | None
|
|
86
91
|
|
|
87
92
|
|
|
88
93
|
class BaseSurrealModel(BaseModel):
|
|
@@ -236,6 +241,23 @@ class BaseSurrealModel(BaseModel):
|
|
|
236
241
|
|
|
237
242
|
return None
|
|
238
243
|
|
|
244
|
+
@classmethod
|
|
245
|
+
def get_server_fields(cls) -> set[str]:
|
|
246
|
+
"""
|
|
247
|
+
Get the list of server-generated field names.
|
|
248
|
+
|
|
249
|
+
Server fields are populated by SurrealDB (e.g., via VALUE time::now())
|
|
250
|
+
and should be excluded from save/update operations.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Set of field names to exclude from save/update operations.
|
|
254
|
+
"""
|
|
255
|
+
if hasattr(cls, "model_config"):
|
|
256
|
+
server_fields = cls.model_config.get("server_fields", None)
|
|
257
|
+
if isinstance(server_fields, list):
|
|
258
|
+
return set(server_fields)
|
|
259
|
+
return set()
|
|
260
|
+
|
|
239
261
|
def get_id(self) -> str | None:
|
|
240
262
|
"""
|
|
241
263
|
Get the ID of the model instance.
|
|
@@ -276,6 +298,29 @@ class BaseSurrealModel(BaseModel):
|
|
|
276
298
|
data["id"] = _parse_record_id(data["id"])
|
|
277
299
|
return data
|
|
278
300
|
|
|
301
|
+
def _update_from_db(self, record: dict[str, Any]) -> None:
|
|
302
|
+
"""
|
|
303
|
+
Update instance fields from a database record without marking them as user-set.
|
|
304
|
+
|
|
305
|
+
This preserves the original __pydantic_fields_set__ so that exclude_unset=True
|
|
306
|
+
continues to work correctly on subsequent saves.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
record: Dictionary of field values from the database.
|
|
310
|
+
"""
|
|
311
|
+
# Store original fields_set to preserve user-set tracking
|
|
312
|
+
original_fields_set = self.__pydantic_fields_set__.copy()
|
|
313
|
+
|
|
314
|
+
for key, value in record.items():
|
|
315
|
+
if key == "id":
|
|
316
|
+
value = _parse_record_id(value)
|
|
317
|
+
if hasattr(self, key):
|
|
318
|
+
setattr(self, key, value)
|
|
319
|
+
|
|
320
|
+
# Restore original fields_set - only user-set fields should be marked
|
|
321
|
+
# DB-loaded fields should not be considered as "set" for exclude_unset
|
|
322
|
+
object.__setattr__(self, "__pydantic_fields_set__", original_fields_set)
|
|
323
|
+
|
|
279
324
|
async def refresh(self) -> None:
|
|
280
325
|
"""
|
|
281
326
|
Refresh the model instance from the database.
|
|
@@ -294,12 +339,8 @@ class BaseSurrealModel(BaseModel):
|
|
|
294
339
|
if record is None:
|
|
295
340
|
raise SurrealDbError("Can't refresh data, no record found.") # pragma: no cover
|
|
296
341
|
|
|
297
|
-
# Update instance fields
|
|
298
|
-
|
|
299
|
-
if key == "id":
|
|
300
|
-
value = _parse_record_id(value)
|
|
301
|
-
if hasattr(self, key):
|
|
302
|
-
setattr(self, key, value)
|
|
342
|
+
# Update instance fields without marking them as user-set
|
|
343
|
+
self._update_from_db(record)
|
|
303
344
|
return None
|
|
304
345
|
|
|
305
346
|
async def save(self, tx: "BaseTransaction | None" = None) -> Self:
|
|
@@ -318,15 +359,19 @@ class BaseSurrealModel(BaseModel):
|
|
|
318
359
|
async with SurrealDBConnectionManager.transaction() as tx:
|
|
319
360
|
await user.save(tx=tx)
|
|
320
361
|
"""
|
|
362
|
+
# Build exclude set: always exclude 'id' and any server-generated fields
|
|
363
|
+
exclude_fields = {"id"} | self.get_server_fields()
|
|
364
|
+
|
|
321
365
|
if tx is not None:
|
|
322
366
|
# Use transaction
|
|
323
|
-
data = self.model_dump(exclude=
|
|
367
|
+
data = self.model_dump(exclude=exclude_fields, exclude_unset=True)
|
|
324
368
|
id = self.get_id()
|
|
325
369
|
table = self.get_table_name()
|
|
326
370
|
|
|
327
371
|
if id is not None:
|
|
372
|
+
# Use upsert for idempotent save (create or update)
|
|
328
373
|
thing = f"{table}:{id}"
|
|
329
|
-
await tx.
|
|
374
|
+
await tx.upsert(thing, data)
|
|
330
375
|
return self
|
|
331
376
|
|
|
332
377
|
# Auto-generate ID - create without specific ID
|
|
@@ -335,13 +380,14 @@ class BaseSurrealModel(BaseModel):
|
|
|
335
380
|
|
|
336
381
|
# Original behavior without transaction
|
|
337
382
|
client = await SurrealDBConnectionManager.get_client()
|
|
338
|
-
data = self.model_dump(exclude=
|
|
383
|
+
data = self.model_dump(exclude=exclude_fields, exclude_unset=True)
|
|
339
384
|
id = self.get_id()
|
|
340
385
|
table = self.get_table_name()
|
|
341
386
|
|
|
342
387
|
if id is not None:
|
|
388
|
+
# Use upsert for idempotent save (create or update)
|
|
343
389
|
thing = f"{table}:{id}"
|
|
344
|
-
await client.
|
|
390
|
+
await client.upsert(thing, data)
|
|
345
391
|
return self
|
|
346
392
|
|
|
347
393
|
# Auto-generate the ID
|
|
@@ -351,9 +397,12 @@ class BaseSurrealModel(BaseModel):
|
|
|
351
397
|
if not result.exists:
|
|
352
398
|
raise SurrealDbError("Can't save data, no record returned.") # pragma: no cover
|
|
353
399
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
400
|
+
# Update self's attributes from the database response
|
|
401
|
+
# This includes the auto-generated ID and any server-side fields
|
|
402
|
+
# Use _update_from_db to avoid marking DB fields as user-set
|
|
403
|
+
record = result.record
|
|
404
|
+
if isinstance(record, dict):
|
|
405
|
+
self._update_from_db(record)
|
|
357
406
|
return self
|
|
358
407
|
|
|
359
408
|
raise SurrealDbError("Can't save data, no record returned.") # pragma: no cover
|
|
@@ -365,7 +414,9 @@ class BaseSurrealModel(BaseModel):
|
|
|
365
414
|
Args:
|
|
366
415
|
tx: Optional transaction to use for this operation.
|
|
367
416
|
"""
|
|
368
|
-
|
|
417
|
+
# Build exclude set: always exclude 'id' and any server-generated fields
|
|
418
|
+
exclude_fields = {"id"} | self.get_server_fields()
|
|
419
|
+
data = self.model_dump(exclude=exclude_fields, exclude_unset=True)
|
|
369
420
|
id = self.get_id()
|
|
370
421
|
|
|
371
422
|
if id is None:
|
|
@@ -388,13 +439,16 @@ class BaseSurrealModel(BaseModel):
|
|
|
388
439
|
"""
|
|
389
440
|
return f"{cls.__name__}:{item}"
|
|
390
441
|
|
|
391
|
-
async def merge(self, tx: "BaseTransaction | None" = None, **data: Any) ->
|
|
442
|
+
async def merge(self, tx: "BaseTransaction | None" = None, **data: Any) -> Self:
|
|
392
443
|
"""
|
|
393
444
|
Merge (partial update) the model instance in the database.
|
|
394
445
|
|
|
395
446
|
Args:
|
|
396
447
|
tx: Optional transaction to use for this operation.
|
|
397
448
|
**data: Fields to update.
|
|
449
|
+
|
|
450
|
+
Returns:
|
|
451
|
+
Self: The updated model instance.
|
|
398
452
|
"""
|
|
399
453
|
data_set = {key: value for key, value in data.items()}
|
|
400
454
|
|
|
@@ -406,11 +460,16 @@ class BaseSurrealModel(BaseModel):
|
|
|
406
460
|
|
|
407
461
|
if tx is not None:
|
|
408
462
|
await tx.merge(thing, data_set)
|
|
409
|
-
|
|
463
|
+
# Update local instance with merged data
|
|
464
|
+
for key, value in data_set.items():
|
|
465
|
+
if hasattr(self, key):
|
|
466
|
+
setattr(self, key, value)
|
|
467
|
+
return self
|
|
410
468
|
|
|
411
469
|
client = await SurrealDBConnectionManager.get_client()
|
|
412
470
|
await client.merge(thing, data_set)
|
|
413
471
|
await self.refresh()
|
|
472
|
+
return self
|
|
414
473
|
|
|
415
474
|
async def delete(self, tx: "BaseTransaction | None" = None) -> None:
|
|
416
475
|
"""
|
surreal_orm/types.py
CHANGED
|
@@ -42,23 +42,114 @@ class FieldType(StrEnum):
|
|
|
42
42
|
SurrealDB field types for schema definitions.
|
|
43
43
|
|
|
44
44
|
Maps to SurrealDB's native type system.
|
|
45
|
+
See: https://surrealdb.com/docs/surrealql/datamodel
|
|
46
|
+
|
|
47
|
+
Numeric Types:
|
|
48
|
+
- INT: 64-bit signed integer (-9223372036854775808 to 9223372036854775807)
|
|
49
|
+
- FLOAT: 64-bit double-precision floating point
|
|
50
|
+
- DECIMAL: Arbitrary precision decimal (for financial calculations)
|
|
51
|
+
- NUMBER: Auto-detected numeric type (stores using minimal bytes)
|
|
52
|
+
|
|
53
|
+
Primitive Types:
|
|
54
|
+
- STRING: Text data
|
|
55
|
+
- BOOL: Boolean true/false
|
|
56
|
+
- DATETIME: RFC 3339 timestamp with timezone
|
|
57
|
+
- DURATION: Time length (e.g., "1h30m", "7d")
|
|
58
|
+
- BYTES: Binary data / byte array
|
|
59
|
+
- UUID: Universal unique identifier
|
|
60
|
+
|
|
61
|
+
Collection Types:
|
|
62
|
+
- ARRAY: Ordered collection (can be typed: array<string>)
|
|
63
|
+
- SET: Unique collection (auto-deduplicated)
|
|
64
|
+
- OBJECT: Flexible JSON-like container
|
|
65
|
+
|
|
66
|
+
Special Types:
|
|
67
|
+
- ANY: Accepts any value type
|
|
68
|
+
- OPTION: Optional value (can be typed: option<string>)
|
|
69
|
+
- RECORD: Reference to another record (can be typed: record<users>)
|
|
70
|
+
- GEOMETRY: GeoJSON spatial data (point, line, polygon, etc.)
|
|
71
|
+
- REGEX: Compiled regular expression
|
|
72
|
+
|
|
73
|
+
Generic Type Syntax:
|
|
74
|
+
For typed collections/references, use the generic() class method:
|
|
75
|
+
- FieldType.ARRAY.generic("string") -> "array<string>"
|
|
76
|
+
- FieldType.RECORD.generic("users") -> "record<users>"
|
|
77
|
+
- FieldType.OPTION.generic("int") -> "option<int>"
|
|
78
|
+
- FieldType.GEOMETRY.generic("point") -> "geometry<point>"
|
|
45
79
|
"""
|
|
46
80
|
|
|
47
|
-
|
|
81
|
+
# Numeric types
|
|
48
82
|
INT = "int"
|
|
49
83
|
FLOAT = "float"
|
|
84
|
+
DECIMAL = "decimal"
|
|
85
|
+
NUMBER = "number"
|
|
86
|
+
|
|
87
|
+
# Primitive types
|
|
88
|
+
STRING = "string"
|
|
50
89
|
BOOL = "bool"
|
|
51
90
|
DATETIME = "datetime"
|
|
52
91
|
DURATION = "duration"
|
|
53
|
-
|
|
92
|
+
BYTES = "bytes"
|
|
93
|
+
UUID = "uuid"
|
|
94
|
+
|
|
95
|
+
# Collection types
|
|
54
96
|
ARRAY = "array"
|
|
97
|
+
SET = "set"
|
|
55
98
|
OBJECT = "object"
|
|
56
|
-
|
|
57
|
-
|
|
99
|
+
|
|
100
|
+
# Special types
|
|
58
101
|
ANY = "any"
|
|
59
102
|
OPTION = "option"
|
|
60
|
-
|
|
61
|
-
|
|
103
|
+
RECORD = "record"
|
|
104
|
+
GEOMETRY = "geometry"
|
|
105
|
+
REGEX = "regex"
|
|
106
|
+
|
|
107
|
+
def generic(self, inner_type: str) -> str:
|
|
108
|
+
"""
|
|
109
|
+
Create a generic type string for parameterized types.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
inner_type: The inner type parameter (e.g., "string", "users", "point")
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Formatted type string (e.g., "array<string>", "record<users>")
|
|
116
|
+
|
|
117
|
+
Examples:
|
|
118
|
+
>>> FieldType.ARRAY.generic("string")
|
|
119
|
+
'array<string>'
|
|
120
|
+
>>> FieldType.RECORD.generic("users")
|
|
121
|
+
'record<users>'
|
|
122
|
+
>>> FieldType.GEOMETRY.generic("point|polygon")
|
|
123
|
+
'geometry<point|polygon>'
|
|
124
|
+
"""
|
|
125
|
+
return f"{self.value}<{inner_type}>"
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def from_python_type(cls, python_type: type) -> "FieldType":
|
|
129
|
+
"""
|
|
130
|
+
Map a Python type to a SurrealDB FieldType.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
python_type: A Python type (str, int, float, bool, list, dict, bytes)
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
The corresponding FieldType
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
ValueError: If the type cannot be mapped
|
|
140
|
+
"""
|
|
141
|
+
mapping: dict[type, FieldType] = {
|
|
142
|
+
str: cls.STRING,
|
|
143
|
+
int: cls.INT,
|
|
144
|
+
float: cls.FLOAT,
|
|
145
|
+
bool: cls.BOOL,
|
|
146
|
+
list: cls.ARRAY,
|
|
147
|
+
dict: cls.OBJECT,
|
|
148
|
+
bytes: cls.BYTES,
|
|
149
|
+
}
|
|
150
|
+
if python_type in mapping:
|
|
151
|
+
return mapping[python_type]
|
|
152
|
+
raise ValueError(f"Cannot map Python type {python_type} to SurrealDB FieldType")
|
|
62
153
|
|
|
63
154
|
|
|
64
155
|
class EncryptionAlgorithm(StrEnum):
|
|
@@ -75,6 +166,7 @@ class EncryptionAlgorithm(StrEnum):
|
|
|
75
166
|
|
|
76
167
|
|
|
77
168
|
# Type mapping from Python types to SurrealDB types
|
|
169
|
+
# Deprecated: Use FieldType.from_python_type() instead
|
|
78
170
|
PYTHON_TO_SURREAL_TYPE: dict[type, FieldType] = {
|
|
79
171
|
str: FieldType.STRING,
|
|
80
172
|
int: FieldType.INT,
|
surreal_sdk/__init__.py
CHANGED
surreal_sdk/connection/base.py
CHANGED
|
@@ -71,8 +71,8 @@ class BaseSurrealConnection(ABC):
|
|
|
71
71
|
# Abstract methods that must be implemented
|
|
72
72
|
|
|
73
73
|
@abstractmethod
|
|
74
|
-
async def connect(self) ->
|
|
75
|
-
"""Establish connection to SurrealDB."""
|
|
74
|
+
async def connect(self) -> Self:
|
|
75
|
+
"""Establish connection to SurrealDB. Returns self for fluent API."""
|
|
76
76
|
...
|
|
77
77
|
|
|
78
78
|
@abstractmethod
|
|
@@ -315,6 +315,23 @@ class BaseSurrealConnection(ABC):
|
|
|
315
315
|
result = await self.rpc("update", [thing, data])
|
|
316
316
|
return RecordsResponse.from_rpc_result(result)
|
|
317
317
|
|
|
318
|
+
async def upsert(self, thing: str, data: dict[str, Any]) -> RecordsResponse:
|
|
319
|
+
"""
|
|
320
|
+
Upsert record(s) - create if not exists, update if exists.
|
|
321
|
+
|
|
322
|
+
This is the recommended method for save operations when you have
|
|
323
|
+
a specific ID and want idempotent behavior.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
thing: Table name or record ID
|
|
327
|
+
data: Record data
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
RecordsResponse containing upserted record(s)
|
|
331
|
+
"""
|
|
332
|
+
result = await self.rpc("upsert", [thing, data])
|
|
333
|
+
return RecordsResponse.from_rpc_result(result)
|
|
334
|
+
|
|
318
335
|
async def merge(self, thing: str, data: dict[str, Any]) -> RecordsResponse:
|
|
319
336
|
"""
|
|
320
337
|
Merge data into record(s), updating only specified fields.
|
surreal_sdk/connection/http.py
CHANGED
|
@@ -4,7 +4,7 @@ HTTP Connection Implementation for SurrealDB SDK.
|
|
|
4
4
|
Provides stateless HTTP-based connection, ideal for microservices and serverless.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from typing import TYPE_CHECKING, Any
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Self
|
|
8
8
|
|
|
9
9
|
import httpx
|
|
10
10
|
|
|
@@ -71,10 +71,10 @@ class HTTPConnection(BaseSurrealConnection):
|
|
|
71
71
|
self._request_id += 1
|
|
72
72
|
return self._request_id
|
|
73
73
|
|
|
74
|
-
async def connect(self) ->
|
|
75
|
-
"""Establish HTTP client connection."""
|
|
74
|
+
async def connect(self) -> Self:
|
|
75
|
+
"""Establish HTTP client connection. Returns self for fluent API."""
|
|
76
76
|
if self._connected:
|
|
77
|
-
return
|
|
77
|
+
return self
|
|
78
78
|
|
|
79
79
|
self._client = httpx.AsyncClient(
|
|
80
80
|
base_url=self.url,
|
|
@@ -83,6 +83,7 @@ class HTTPConnection(BaseSurrealConnection):
|
|
|
83
83
|
limits=httpx.Limits(max_keepalive_connections=0, max_connections=100),
|
|
84
84
|
)
|
|
85
85
|
self._connected = True
|
|
86
|
+
return self
|
|
86
87
|
|
|
87
88
|
async def close(self) -> None:
|
|
88
89
|
"""Close HTTP client."""
|
|
@@ -112,10 +113,12 @@ class HTTPConnection(BaseSurrealConnection):
|
|
|
112
113
|
request.id = self._next_request_id()
|
|
113
114
|
|
|
114
115
|
try:
|
|
116
|
+
# Use pre-encoded JSON with custom encoder for datetime, UUID, etc.
|
|
117
|
+
headers = {**self.headers, "Content-Type": "application/json"}
|
|
115
118
|
response = await self._client.post(
|
|
116
119
|
"/rpc",
|
|
117
|
-
|
|
118
|
-
headers=
|
|
120
|
+
content=request.to_json(),
|
|
121
|
+
headers=headers,
|
|
119
122
|
)
|
|
120
123
|
response.raise_for_status()
|
|
121
124
|
return RPCResponse.from_dict(response.json())
|
|
@@ -4,7 +4,7 @@ WebSocket Connection Implementation for SurrealDB SDK.
|
|
|
4
4
|
Provides stateful WebSocket-based connection for real-time features.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from typing import TYPE_CHECKING, Any, Callable, Coroutine
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Callable, Coroutine, Self
|
|
8
8
|
import asyncio
|
|
9
9
|
import json
|
|
10
10
|
|
|
@@ -80,16 +80,17 @@ class WebSocketConnection(BaseSurrealConnection):
|
|
|
80
80
|
self._reader_task: asyncio.Task[None] | None = None
|
|
81
81
|
self._reconnect_task: asyncio.Task[None] | None = None
|
|
82
82
|
self._closing = False
|
|
83
|
+
self._callback_tasks: set[asyncio.Task[Any]] = set() # Track fire-and-forget callback tasks
|
|
83
84
|
|
|
84
85
|
def _next_request_id(self) -> int:
|
|
85
86
|
"""Generate next request ID."""
|
|
86
87
|
self._request_id += 1
|
|
87
88
|
return self._request_id
|
|
88
89
|
|
|
89
|
-
async def connect(self) ->
|
|
90
|
-
"""Establish WebSocket connection."""
|
|
90
|
+
async def connect(self) -> Self:
|
|
91
|
+
"""Establish WebSocket connection. Returns self for fluent API."""
|
|
91
92
|
if self._connected:
|
|
92
|
-
return
|
|
93
|
+
return self
|
|
93
94
|
|
|
94
95
|
self._closing = False
|
|
95
96
|
self._session = aiohttp.ClientSession()
|
|
@@ -113,6 +114,8 @@ class WebSocketConnection(BaseSurrealConnection):
|
|
|
113
114
|
# Set namespace and database
|
|
114
115
|
await self.use(self.namespace, self.database)
|
|
115
116
|
|
|
117
|
+
return self
|
|
118
|
+
|
|
116
119
|
except aiohttp.ClientError as e:
|
|
117
120
|
await self._cleanup()
|
|
118
121
|
raise ConnectionError(f"WebSocket connection failed: {e}")
|
|
@@ -151,6 +154,11 @@ class WebSocketConnection(BaseSurrealConnection):
|
|
|
151
154
|
future.set_exception(ConnectionError("Connection closed"))
|
|
152
155
|
self._pending.clear()
|
|
153
156
|
|
|
157
|
+
# Cancel all callback tasks
|
|
158
|
+
for task in self._callback_tasks:
|
|
159
|
+
task.cancel()
|
|
160
|
+
self._callback_tasks.clear()
|
|
161
|
+
|
|
154
162
|
# Close WebSocket
|
|
155
163
|
if self._ws:
|
|
156
164
|
await self._ws.close()
|
|
@@ -217,7 +225,10 @@ class WebSocketConnection(BaseSurrealConnection):
|
|
|
217
225
|
live_id = message.get("id")
|
|
218
226
|
if live_id and live_id in self._live_callbacks:
|
|
219
227
|
callback = self._live_callbacks[live_id]
|
|
220
|
-
|
|
228
|
+
# Track task for proper cleanup on connection close
|
|
229
|
+
task = asyncio.create_task(callback(message))
|
|
230
|
+
self._callback_tasks.add(task)
|
|
231
|
+
task.add_done_callback(self._callback_tasks.discard)
|
|
221
232
|
|
|
222
233
|
async def _reconnect(self) -> None:
|
|
223
234
|
"""Attempt to reconnect after disconnection."""
|
surreal_sdk/protocol/rpc.py
CHANGED
|
@@ -5,10 +5,40 @@ Handles the JSON-RPC style messaging format used by SurrealDB.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from dataclasses import dataclass, field
|
|
8
|
+
from datetime import date, datetime, time
|
|
9
|
+
from decimal import Decimal
|
|
8
10
|
from typing import Any
|
|
11
|
+
from uuid import UUID
|
|
9
12
|
import json
|
|
10
13
|
|
|
11
14
|
|
|
15
|
+
class SurrealJSONEncoder(json.JSONEncoder):
|
|
16
|
+
"""
|
|
17
|
+
Custom JSON encoder for SurrealDB types.
|
|
18
|
+
|
|
19
|
+
Handles serialization of Python types that are not natively JSON serializable:
|
|
20
|
+
- datetime → ISO 8601 string
|
|
21
|
+
- date → ISO 8601 string
|
|
22
|
+
- time → ISO 8601 string
|
|
23
|
+
- Decimal → float
|
|
24
|
+
- UUID → string
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def default(self, obj: Any) -> Any:
|
|
28
|
+
"""Encode non-standard types to JSON-serializable values."""
|
|
29
|
+
if isinstance(obj, datetime):
|
|
30
|
+
return obj.isoformat()
|
|
31
|
+
if isinstance(obj, date):
|
|
32
|
+
return obj.isoformat()
|
|
33
|
+
if isinstance(obj, time):
|
|
34
|
+
return obj.isoformat()
|
|
35
|
+
if isinstance(obj, Decimal):
|
|
36
|
+
return float(obj)
|
|
37
|
+
if isinstance(obj, UUID):
|
|
38
|
+
return str(obj)
|
|
39
|
+
return super().default(obj)
|
|
40
|
+
|
|
41
|
+
|
|
12
42
|
@dataclass
|
|
13
43
|
class RPCRequest:
|
|
14
44
|
"""
|
|
@@ -33,8 +63,8 @@ class RPCRequest:
|
|
|
33
63
|
}
|
|
34
64
|
|
|
35
65
|
def to_json(self) -> str:
|
|
36
|
-
"""Serialize to JSON string."""
|
|
37
|
-
return json.dumps(self.to_dict())
|
|
66
|
+
"""Serialize to JSON string with custom encoder for datetime, UUID, etc."""
|
|
67
|
+
return json.dumps(self.to_dict(), cls=SurrealJSONEncoder)
|
|
38
68
|
|
|
39
69
|
@classmethod
|
|
40
70
|
def query(cls, sql: str, vars: dict[str, Any] | None = None, request_id: int = 1) -> "RPCRequest":
|
surreal_sdk/pyproject.toml
CHANGED
surreal_sdk/transaction.py
CHANGED
|
@@ -119,6 +119,11 @@ class BaseTransaction(ABC):
|
|
|
119
119
|
"""Update records within the transaction."""
|
|
120
120
|
...
|
|
121
121
|
|
|
122
|
+
@abstractmethod
|
|
123
|
+
async def upsert(self, thing: str, data: dict[str, Any]) -> "RecordsResponse":
|
|
124
|
+
"""Upsert records within the transaction (create or update)."""
|
|
125
|
+
...
|
|
126
|
+
|
|
122
127
|
@abstractmethod
|
|
123
128
|
async def merge(self, thing: str, data: dict[str, Any]) -> "RecordsResponse":
|
|
124
129
|
"""Merge data into records within the transaction."""
|
|
@@ -256,6 +261,15 @@ class HTTPTransaction(BaseTransaction):
|
|
|
256
261
|
self._queue_statement(sql, data)
|
|
257
262
|
return RecordsResponse(records=[], raw=[])
|
|
258
263
|
|
|
264
|
+
async def upsert(self, thing: str, data: dict[str, Any]) -> "RecordsResponse":
|
|
265
|
+
"""Queue an upsert operation (create or update)."""
|
|
266
|
+
from .types import RecordsResponse
|
|
267
|
+
|
|
268
|
+
fields = ", ".join(f"{k} = ${k}" for k in data.keys())
|
|
269
|
+
sql = f"UPSERT {thing} SET {fields};"
|
|
270
|
+
self._queue_statement(sql, data)
|
|
271
|
+
return RecordsResponse(records=[], raw=[])
|
|
272
|
+
|
|
259
273
|
async def merge(self, thing: str, data: dict[str, Any]) -> "RecordsResponse":
|
|
260
274
|
"""Queue a merge operation."""
|
|
261
275
|
from .types import RecordsResponse
|
|
@@ -361,6 +375,12 @@ class WebSocketTransaction(BaseTransaction):
|
|
|
361
375
|
raise TransactionError("Transaction not active")
|
|
362
376
|
return await self._connection.update(thing, data)
|
|
363
377
|
|
|
378
|
+
async def upsert(self, thing: str, data: dict[str, Any]) -> "RecordsResponse":
|
|
379
|
+
"""Execute upsert immediately within transaction (create or update)."""
|
|
380
|
+
if not self.is_active:
|
|
381
|
+
raise TransactionError("Transaction not active")
|
|
382
|
+
return await self._connection.upsert(thing, data)
|
|
383
|
+
|
|
364
384
|
async def merge(self, thing: str, data: dict[str, Any]) -> "RecordsResponse":
|
|
365
385
|
"""Execute merge immediately within transaction."""
|
|
366
386
|
if not self.is_active:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: surrealdb-orm
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3
|
|
4
4
|
Summary: SurrealDB ORM as 'DJango style' for Python with async support. Works with pydantic validation.
|
|
5
5
|
Project-URL: Homepage, https://github.com/EulogySnowfall/SurrealDB-ORM
|
|
6
6
|
Project-URL: Documentation, https://github.com/EulogySnowfall/SurrealDB-ORM
|
|
@@ -66,7 +66,44 @@ Description-Content-Type: text/markdown
|
|
|
66
66
|
|
|
67
67
|
---
|
|
68
68
|
|
|
69
|
-
## What's New in 0.
|
|
69
|
+
## What's New in 0.5.x
|
|
70
|
+
|
|
71
|
+
### v0.5.3 - ORM Improvements
|
|
72
|
+
|
|
73
|
+
- **Upsert save behavior** - `save()` now uses `upsert` for existing records (idempotent, Django-like)
|
|
74
|
+
- **`server_fields` config** - Exclude server-generated fields (created_at, updated_at) from saves
|
|
75
|
+
- **`merge()` returns self** - Now returns the updated model instance instead of None
|
|
76
|
+
- **`save()` updates self** - Updates original instance attributes instead of returning new object
|
|
77
|
+
- **NULL values fix** - `exclude_unset=True` now works correctly after loading from DB
|
|
78
|
+
|
|
79
|
+
### v0.5.2 - Bug Fixes & FieldType Improvements
|
|
80
|
+
|
|
81
|
+
- **FieldType enum** - Enhanced migration type system with `generic()` and `from_python_type()` methods
|
|
82
|
+
- **datetime serialization** - Proper JSON encoding for datetime, date, time, Decimal, UUID
|
|
83
|
+
- **Fluent API** - `connect()` now returns `self` for method chaining
|
|
84
|
+
- **Session cleanup** - WebSocket callback tasks properly tracked and cancelled
|
|
85
|
+
- **Optional fields** - `exclude_unset=True` prevents None from overriding DB defaults
|
|
86
|
+
- **Parameter alias** - `username` parameter alias for `user` in ConnectionManager
|
|
87
|
+
|
|
88
|
+
### v0.5.1 - Security Workflows
|
|
89
|
+
|
|
90
|
+
- **Dependabot integration** - Automatic dependency security updates
|
|
91
|
+
- **Auto-merge** - Dependabot PRs merged after CI passes
|
|
92
|
+
- **SurrealDB monitoring** - Integration tests on new SurrealDB releases
|
|
93
|
+
|
|
94
|
+
### v0.5.0 - Real-time SDK Enhancements
|
|
95
|
+
|
|
96
|
+
- **Live Select Stream** - Async iterator pattern for real-time changes
|
|
97
|
+
- `async with db.live_select("table") as stream: async for change in stream:`
|
|
98
|
+
- `LiveChange` dataclass with `record_id`, `action`, `result`, `changed_fields`
|
|
99
|
+
- WHERE clause support with parameterized queries
|
|
100
|
+
- **Auto-Resubscribe** - Automatic reconnection after WebSocket disconnect
|
|
101
|
+
- `auto_resubscribe=True` parameter for seamless K8s pod restart recovery
|
|
102
|
+
- `on_reconnect(old_id, new_id)` callback for tracking ID changes
|
|
103
|
+
- **Typed Function Calls** - Pydantic/dataclass return type support
|
|
104
|
+
- `await db.call("fn::my_func", params={...}, return_type=MyModel)`
|
|
105
|
+
|
|
106
|
+
### v0.4.0 - Relations & Graph
|
|
70
107
|
|
|
71
108
|
- **Relations & Graph Traversal** - Django-style relation definitions with SurrealDB graph support
|
|
72
109
|
- `ForeignKey`, `ManyToMany`, `Relation` field types
|
|
@@ -250,22 +287,54 @@ result = await db.fn.my_custom_function(arg1, arg2)
|
|
|
250
287
|
Real-time updates via WebSocket:
|
|
251
288
|
|
|
252
289
|
```python
|
|
253
|
-
from surreal_sdk import
|
|
290
|
+
from surreal_sdk import LiveAction
|
|
291
|
+
|
|
292
|
+
# Async iterator pattern (recommended)
|
|
293
|
+
async with db.live_select(
|
|
294
|
+
"orders",
|
|
295
|
+
where="status = $status",
|
|
296
|
+
params={"status": "pending"},
|
|
297
|
+
auto_resubscribe=True, # Auto-reconnect on WebSocket drop
|
|
298
|
+
) as stream:
|
|
299
|
+
async for change in stream:
|
|
300
|
+
match change.action:
|
|
301
|
+
case LiveAction.CREATE:
|
|
302
|
+
print(f"New order: {change.result}")
|
|
303
|
+
case LiveAction.UPDATE:
|
|
304
|
+
print(f"Updated: {change.record_id}")
|
|
305
|
+
case LiveAction.DELETE:
|
|
306
|
+
print(f"Deleted: {change.record_id}")
|
|
307
|
+
|
|
308
|
+
# Callback-based pattern
|
|
309
|
+
from surreal_sdk import LiveQuery, LiveNotification
|
|
254
310
|
|
|
255
311
|
async def on_change(notification: LiveNotification):
|
|
256
|
-
|
|
257
|
-
print(f"New record: {notification.result}")
|
|
258
|
-
elif notification.action == LiveAction.UPDATE:
|
|
259
|
-
print(f"Updated: {notification.result}")
|
|
260
|
-
elif notification.action == LiveAction.DELETE:
|
|
261
|
-
print(f"Deleted: {notification.result}")
|
|
312
|
+
print(f"{notification.action}: {notification.result}")
|
|
262
313
|
|
|
263
314
|
live = LiveQuery(ws_conn, "orders")
|
|
264
315
|
await live.subscribe(on_change)
|
|
265
|
-
# ...
|
|
316
|
+
# ... record changes trigger callbacks ...
|
|
266
317
|
await live.unsubscribe()
|
|
267
318
|
```
|
|
268
319
|
|
|
320
|
+
**Typed Function Calls:**
|
|
321
|
+
|
|
322
|
+
```python
|
|
323
|
+
from pydantic import BaseModel
|
|
324
|
+
|
|
325
|
+
class VoteResult(BaseModel):
|
|
326
|
+
success: bool
|
|
327
|
+
count: int
|
|
328
|
+
|
|
329
|
+
# Call SurrealDB function with typed return
|
|
330
|
+
result = await db.call(
|
|
331
|
+
"cast_vote",
|
|
332
|
+
params={"user": "alice", "vote": "yes"},
|
|
333
|
+
return_type=VoteResult
|
|
334
|
+
)
|
|
335
|
+
print(result.success, result.count) # Typed access
|
|
336
|
+
```
|
|
337
|
+
|
|
269
338
|
---
|
|
270
339
|
|
|
271
340
|
## ORM Features
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
surreal_orm/__init__.py,sha256=
|
|
1
|
+
surreal_orm/__init__.py,sha256=ucGuE53NINPmdGmEnoObF93n7iwyhORC-r0C-f_3n5Q,1645
|
|
2
2
|
surreal_orm/aggregations.py,sha256=5ERMHMWQfaW76OrMNazMjyg7dbf9bJ3GX_8QWz6tfxY,3218
|
|
3
|
-
surreal_orm/connection_manager.py,sha256=
|
|
3
|
+
surreal_orm/connection_manager.py,sha256=VJVfsuUXK5OEG0rtuiu3DKbyWxkkeFrCEtzjXFd6lAw,10496
|
|
4
4
|
surreal_orm/constants.py,sha256=CLavEca1M6cLJLqVl4l4KoE-cBrgVQNsuGxW9zGJBmg,429
|
|
5
5
|
surreal_orm/enum.py,sha256=kR-vzkHqnqy9YaYOvWTwAHdl2-WCzPcSEch-YTyJv1Y,158
|
|
6
|
-
surreal_orm/model_base.py,sha256=
|
|
6
|
+
surreal_orm/model_base.py,sha256=yXFsCskiCkmqpWtpNpYWy-MIYsrQvovd71jHMgzUNG4,25652
|
|
7
7
|
surreal_orm/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
8
|
surreal_orm/query_set.py,sha256=3nmQ9A13g-1EJlRZsJQX8z7yFO3pjI_6MscFT3W4Lm0,38890
|
|
9
9
|
surreal_orm/relations.py,sha256=pbsbKz2jw1h9FdFQowidUkx8krYv-l3OreQEViasN5w,21137
|
|
10
10
|
surreal_orm/surreal_function.py,sha256=zsiJHPa4Z-RfqJpekXupJI79SKxUGtsnptwFpq1KFEM,2627
|
|
11
11
|
surreal_orm/surreal_ql.py,sha256=k3XHIdesu5Yb6RVU62ESizLLrJ8NtYYDQ8pI-WSqdRI,3436
|
|
12
|
-
surreal_orm/types.py,sha256=
|
|
12
|
+
surreal_orm/types.py,sha256=0mb3FLd9qYlDN2d_qv7ZXwhiqHpW9ytFrDIWSZIj2QE,5167
|
|
13
13
|
surreal_orm/utils.py,sha256=mni_dTtb4VGTdge8eWSZpBw5xoWci2m-XThKFHYPKTo,171
|
|
14
14
|
surreal_orm/auth/__init__.py,sha256=wydszeh5ee4OpQu_OSX7CpCNKNIXPGFTlAoTmapxho4,340
|
|
15
15
|
surreal_orm/auth/access.py,sha256=FUuBAgnyq_j4a6RfLJ96TsgSHpaNXFpwLvrpCVrUbVA,5584
|
|
@@ -24,29 +24,29 @@ surreal_orm/migrations/executor.py,sha256=6QNhsJAt0EL1QmBXmXBrG5LyVhKyJa0xxqd-9C
|
|
|
24
24
|
surreal_orm/migrations/generator.py,sha256=PutA0OjXZ8YgBXKTAJrMymo-E0h1zyaVpqh7qCzQ5LM,7994
|
|
25
25
|
surreal_orm/migrations/introspector.py,sha256=49AWhOAZRhWdv2jSaW3n6uuoInNh_U8AzUCo6toYgPo,9673
|
|
26
26
|
surreal_orm/migrations/migration.py,sha256=6z7rJC-oWBDzAj_tqIpOCDSjRK6kJoHdKdkI6blb718,5663
|
|
27
|
-
surreal_orm/migrations/operations.py,sha256=
|
|
27
|
+
surreal_orm/migrations/operations.py,sha256=KHxxPIv-etJpMDrB9yqINhX1vnSisjX8hHviiuHSJCo,16555
|
|
28
28
|
surreal_orm/migrations/state.py,sha256=cWLLOuvojeoafBTYBfgKHlWJ0n31mnBcbeBVTtRvn8Q,15848
|
|
29
29
|
surreal_sdk/README.md,sha256=SCHz5yMMvgHE70V7JGupvg3VZTllVsu60D_DtOngJjk,1957
|
|
30
|
-
surreal_sdk/__init__.py,sha256=
|
|
30
|
+
surreal_sdk/__init__.py,sha256=h7Bwv9N7FxrIM_YSi_Yy-bZYaMAw4o8kCeKH7UMtxvA,4055
|
|
31
31
|
surreal_sdk/exceptions.py,sha256=qiDA3xJ2VkY8HUhvmDmW8kkNxhZ9NJu-FCUKUzfBrbU,1489
|
|
32
32
|
surreal_sdk/functions.py,sha256=eTJA6zWlivTEnKJgtR53MAygElhMB1o9p6_FPt8_ErI,20537
|
|
33
33
|
surreal_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
|
-
surreal_sdk/pyproject.toml,sha256=
|
|
35
|
-
surreal_sdk/transaction.py,sha256=
|
|
34
|
+
surreal_sdk/pyproject.toml,sha256=pbcDsXdadtxF2d4aQKlinbEYEIftYfGy-T_lZX_8sRw,1421
|
|
35
|
+
surreal_sdk/transaction.py,sha256=AIvEJhGcqmaQ4WiyTCD_knhpGW7zffyd7eSYT32V3Vc,14287
|
|
36
36
|
surreal_sdk/types.py,sha256=y08l9x-ZYgPSAIFztzly474HiZ9slxYqZ0oAA7jJpOI,9841
|
|
37
37
|
surreal_sdk/connection/__init__.py,sha256=zTe2qPLHsl9Upy6NYUr04rr4aWP-D2vCMA2iM5eUcU0,363
|
|
38
|
-
surreal_sdk/connection/base.py,sha256=
|
|
39
|
-
surreal_sdk/connection/http.py,sha256=
|
|
38
|
+
surreal_sdk/connection/base.py,sha256=AN-Ab84Vr5TcI4Wpxm_Px63yDBvBo2vLFUdffgBMtLw,15769
|
|
39
|
+
surreal_sdk/connection/http.py,sha256=bUJ7bwEGKbufFo6NNggOBSimvJZZGmhoY9IR0Jd4YPw,12648
|
|
40
40
|
surreal_sdk/connection/pool.py,sha256=v9FZmpfwWn674iY--xdDtaiYimBgpv3UtGT4k6q0DHE,7849
|
|
41
|
-
surreal_sdk/connection/websocket.py,sha256=
|
|
41
|
+
surreal_sdk/connection/websocket.py,sha256=sTbYSAi9-C8W3T2gR1bm-XO3vtCirtdO95MYJoxH3z8,18238
|
|
42
42
|
surreal_sdk/protocol/__init__.py,sha256=ibLAVvwGfLChUHu3TStO7-bM1dkXggEE8x0BU7YNLTM,217
|
|
43
|
-
surreal_sdk/protocol/rpc.py,sha256=
|
|
43
|
+
surreal_sdk/protocol/rpc.py,sha256=602CvidbgawtmzW6d80WRl-V6D7hRLSQkTld7ZR2nUg,6925
|
|
44
44
|
surreal_sdk/streaming/__init__.py,sha256=TljF9HFN-XOshK_1smmTanF68hcdNsTgdlHtfFoAmtQ,714
|
|
45
45
|
surreal_sdk/streaming/change_feed.py,sha256=lS6CGNinsMkzslf1y0aV0VgZ2gq01EDIEZrnvvYhv-E,8540
|
|
46
46
|
surreal_sdk/streaming/live_query.py,sha256=QwPXmRIsH0j3jgGQY9ULJU7MB0eTk8dnOVbPzy4SeSo,7561
|
|
47
47
|
surreal_sdk/streaming/live_select.py,sha256=mYg6NKMx3GPShg_hYz4SjjDwhG7baI3t_Gynv8YCBNE,12035
|
|
48
|
-
surrealdb_orm-0.5.
|
|
49
|
-
surrealdb_orm-0.5.
|
|
50
|
-
surrealdb_orm-0.5.
|
|
51
|
-
surrealdb_orm-0.5.
|
|
52
|
-
surrealdb_orm-0.5.
|
|
48
|
+
surrealdb_orm-0.5.3.dist-info/METADATA,sha256=qCZAOpLAFD3y7PY_o8HJ2fgLldLm4wxE8E_G2nANl6k,17102
|
|
49
|
+
surrealdb_orm-0.5.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
50
|
+
surrealdb_orm-0.5.3.dist-info/entry_points.txt,sha256=3zbp1VzVPSxFrxNuvKZAyt9U432QITdBNjp5QnMZ2o8,62
|
|
51
|
+
surrealdb_orm-0.5.3.dist-info/licenses/LICENSE,sha256=OYVJQ7TKsjHAgVndePmKcMWOZkxwJxOHiXOG1TAnCk4,1079
|
|
52
|
+
surrealdb_orm-0.5.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|