surrealdb-orm 0.5.3__py3-none-any.whl → 0.5.3.1__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.
Potentially problematic release.
This version of surrealdb-orm might be problematic. Click here for more details.
- surreal_orm/model_base.py +90 -15
- {surrealdb_orm-0.5.3.dist-info → surrealdb_orm-0.5.3.1.dist-info}/METADATA +8 -2
- {surrealdb_orm-0.5.3.dist-info → surrealdb_orm-0.5.3.1.dist-info}/RECORD +6 -6
- {surrealdb_orm-0.5.3.dist-info → surrealdb_orm-0.5.3.1.dist-info}/WHEEL +0 -0
- {surrealdb_orm-0.5.3.dist-info → surrealdb_orm-0.5.3.1.dist-info}/entry_points.txt +0 -0
- {surrealdb_orm-0.5.3.dist-info → surrealdb_orm-0.5.3.1.dist-info}/licenses/LICENSE +0 -0
surreal_orm/model_base.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
from
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Any, Literal, Self, TYPE_CHECKING, get_args, get_origin
|
|
2
3
|
|
|
3
|
-
from pydantic import BaseModel, ConfigDict, model_validator
|
|
4
|
+
from pydantic import BaseModel, ConfigDict, PrivateAttr, model_validator
|
|
4
5
|
|
|
5
6
|
from .connection_manager import SurrealDBConnectionManager
|
|
6
7
|
from .types import SchemaMode, TableType
|
|
@@ -53,6 +54,36 @@ def _parse_record_id(record_id: Any) -> str | None:
|
|
|
53
54
|
return record_str
|
|
54
55
|
|
|
55
56
|
|
|
57
|
+
def _parse_datetime(value: Any) -> Any:
|
|
58
|
+
"""
|
|
59
|
+
Parse a datetime value from SurrealDB.
|
|
60
|
+
|
|
61
|
+
SurrealDB returns datetime as ISO 8601 strings.
|
|
62
|
+
"""
|
|
63
|
+
if value is None:
|
|
64
|
+
return None
|
|
65
|
+
if isinstance(value, datetime):
|
|
66
|
+
return value
|
|
67
|
+
if isinstance(value, str):
|
|
68
|
+
try:
|
|
69
|
+
# Try parsing ISO format (with or without timezone)
|
|
70
|
+
# SurrealDB returns format like "2026-02-02T13:21:23.641315924Z"
|
|
71
|
+
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
72
|
+
except ValueError:
|
|
73
|
+
pass
|
|
74
|
+
return value # Return as-is if we can't parse
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _is_datetime_field(field_type: Any) -> bool:
|
|
78
|
+
"""Check if a field type is datetime or Optional[datetime]."""
|
|
79
|
+
# Handle Optional[datetime] which is Union[datetime, None]
|
|
80
|
+
origin = get_origin(field_type)
|
|
81
|
+
if origin is not None:
|
|
82
|
+
args = get_args(field_type)
|
|
83
|
+
return datetime in args
|
|
84
|
+
return field_type is datetime
|
|
85
|
+
|
|
86
|
+
|
|
56
87
|
class SurrealConfigDict(ConfigDict):
|
|
57
88
|
"""
|
|
58
89
|
SurrealConfigDict is a configuration dictionary for SurrealDB models.
|
|
@@ -109,6 +140,10 @@ class BaseSurrealModel(BaseModel):
|
|
|
109
140
|
password: Encrypted
|
|
110
141
|
"""
|
|
111
142
|
|
|
143
|
+
# Private attribute to track if this instance has been persisted to the database.
|
|
144
|
+
# This helps distinguish between create (first save) and update (subsequent saves).
|
|
145
|
+
_db_persisted: bool = PrivateAttr(default=False)
|
|
146
|
+
|
|
112
147
|
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
113
148
|
"""Register subclasses in the model registry for migration introspection."""
|
|
114
149
|
super().__init_subclass__(**kwargs)
|
|
@@ -285,7 +320,9 @@ class BaseSurrealModel(BaseModel):
|
|
|
285
320
|
if isinstance(record, list):
|
|
286
321
|
return [cls.from_db(rs) for rs in record] # type: ignore
|
|
287
322
|
|
|
288
|
-
|
|
323
|
+
instance = cls(**record)
|
|
324
|
+
instance._db_persisted = True
|
|
325
|
+
return instance
|
|
289
326
|
|
|
290
327
|
@model_validator(mode="before")
|
|
291
328
|
@classmethod
|
|
@@ -305,15 +342,26 @@ class BaseSurrealModel(BaseModel):
|
|
|
305
342
|
This preserves the original __pydantic_fields_set__ so that exclude_unset=True
|
|
306
343
|
continues to work correctly on subsequent saves.
|
|
307
344
|
|
|
345
|
+
Also handles type coercion for datetime fields (SurrealDB returns ISO strings).
|
|
346
|
+
|
|
308
347
|
Args:
|
|
309
348
|
record: Dictionary of field values from the database.
|
|
310
349
|
"""
|
|
311
350
|
# Store original fields_set to preserve user-set tracking
|
|
312
351
|
original_fields_set = self.__pydantic_fields_set__.copy()
|
|
313
352
|
|
|
353
|
+
# Get field type annotations for datetime parsing
|
|
354
|
+
field_types = self.__class__.model_fields
|
|
355
|
+
|
|
314
356
|
for key, value in record.items():
|
|
315
357
|
if key == "id":
|
|
316
358
|
value = _parse_record_id(value)
|
|
359
|
+
elif key in field_types:
|
|
360
|
+
# Check if this field is a datetime type and parse if needed
|
|
361
|
+
field_info = field_types[key]
|
|
362
|
+
if _is_datetime_field(field_info.annotation):
|
|
363
|
+
value = _parse_datetime(value)
|
|
364
|
+
|
|
317
365
|
if hasattr(self, key):
|
|
318
366
|
setattr(self, key, value)
|
|
319
367
|
|
|
@@ -321,6 +369,9 @@ class BaseSurrealModel(BaseModel):
|
|
|
321
369
|
# DB-loaded fields should not be considered as "set" for exclude_unset
|
|
322
370
|
object.__setattr__(self, "__pydantic_fields_set__", original_fields_set)
|
|
323
371
|
|
|
372
|
+
# Mark as persisted since we just loaded data from DB
|
|
373
|
+
self._db_persisted = True
|
|
374
|
+
|
|
324
375
|
async def refresh(self) -> None:
|
|
325
376
|
"""
|
|
326
377
|
Refresh the model instance from the database.
|
|
@@ -347,6 +398,11 @@ class BaseSurrealModel(BaseModel):
|
|
|
347
398
|
"""
|
|
348
399
|
Save the model instance to the database.
|
|
349
400
|
|
|
401
|
+
For persisted records: uses merge() for partial update, only sending
|
|
402
|
+
explicitly set fields (preserving server-side values like timestamps).
|
|
403
|
+
For new records with ID: uses upsert() to create or fully replace.
|
|
404
|
+
For new records without ID: uses create() to auto-generate an ID.
|
|
405
|
+
|
|
350
406
|
Args:
|
|
351
407
|
tx: Optional transaction to use for this operation.
|
|
352
408
|
If provided, the operation will be part of the transaction.
|
|
@@ -361,37 +417,51 @@ class BaseSurrealModel(BaseModel):
|
|
|
361
417
|
"""
|
|
362
418
|
# Build exclude set: always exclude 'id' and any server-generated fields
|
|
363
419
|
exclude_fields = {"id"} | self.get_server_fields()
|
|
420
|
+
id = self.get_id()
|
|
421
|
+
table = self.get_table_name()
|
|
364
422
|
|
|
365
423
|
if tx is not None:
|
|
366
424
|
# Use transaction
|
|
367
425
|
data = self.model_dump(exclude=exclude_fields, exclude_unset=True)
|
|
368
|
-
|
|
369
|
-
|
|
426
|
+
|
|
427
|
+
if self._db_persisted and id is not None:
|
|
428
|
+
# Already persisted: use merge for partial update
|
|
429
|
+
# Only sends explicitly set fields, preserving server-side values
|
|
430
|
+
thing = f"{table}:{id}"
|
|
431
|
+
await tx.merge(thing, data)
|
|
432
|
+
return self
|
|
370
433
|
|
|
371
434
|
if id is not None:
|
|
372
|
-
#
|
|
435
|
+
# New record with user-provided ID: use upsert
|
|
373
436
|
thing = f"{table}:{id}"
|
|
374
437
|
await tx.upsert(thing, data)
|
|
375
|
-
|
|
438
|
+
else:
|
|
439
|
+
# Auto-generate ID
|
|
440
|
+
await tx.create(table, data)
|
|
376
441
|
|
|
377
|
-
|
|
378
|
-
await tx.create(table, data)
|
|
442
|
+
self._db_persisted = True
|
|
379
443
|
return self
|
|
380
444
|
|
|
381
445
|
# Original behavior without transaction
|
|
382
446
|
client = await SurrealDBConnectionManager.get_client()
|
|
383
447
|
data = self.model_dump(exclude=exclude_fields, exclude_unset=True)
|
|
384
|
-
|
|
385
|
-
|
|
448
|
+
|
|
449
|
+
if self._db_persisted and id is not None:
|
|
450
|
+
# Already persisted: use merge for partial update
|
|
451
|
+
# Only sends explicitly set fields, preserving server-side values
|
|
452
|
+
thing = f"{table}:{id}"
|
|
453
|
+
await client.merge(thing, data)
|
|
454
|
+
return self
|
|
386
455
|
|
|
387
456
|
if id is not None:
|
|
388
|
-
#
|
|
457
|
+
# New record with user-provided ID: use upsert
|
|
389
458
|
thing = f"{table}:{id}"
|
|
390
459
|
await client.upsert(thing, data)
|
|
460
|
+
self._db_persisted = True
|
|
391
461
|
return self
|
|
392
462
|
|
|
393
463
|
# Auto-generate the ID
|
|
394
|
-
result = await client.create(table, data)
|
|
464
|
+
result = await client.create(table, data)
|
|
395
465
|
|
|
396
466
|
# SDK returns RecordResponse
|
|
397
467
|
if not result.exists:
|
|
@@ -411,6 +481,9 @@ class BaseSurrealModel(BaseModel):
|
|
|
411
481
|
"""
|
|
412
482
|
Update the model instance to the database.
|
|
413
483
|
|
|
484
|
+
Uses merge() to only update the specified fields, preserving
|
|
485
|
+
any fields that weren't explicitly set.
|
|
486
|
+
|
|
414
487
|
Args:
|
|
415
488
|
tx: Optional transaction to use for this operation.
|
|
416
489
|
"""
|
|
@@ -425,11 +498,13 @@ class BaseSurrealModel(BaseModel):
|
|
|
425
498
|
thing = f"{self.__class__.__name__}:{id}"
|
|
426
499
|
|
|
427
500
|
if tx is not None:
|
|
428
|
-
|
|
501
|
+
# Use merge for partial update - preserves unspecified fields
|
|
502
|
+
await tx.merge(thing, data)
|
|
429
503
|
return None
|
|
430
504
|
|
|
431
505
|
client = await SurrealDBConnectionManager.get_client()
|
|
432
|
-
|
|
506
|
+
# Use merge for partial update - preserves unspecified fields
|
|
507
|
+
result = await client.merge(thing, data)
|
|
433
508
|
return result.records
|
|
434
509
|
|
|
435
510
|
@classmethod
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: surrealdb-orm
|
|
3
|
-
Version: 0.5.3
|
|
3
|
+
Version: 0.5.3.1
|
|
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
|
|
@@ -68,9 +68,15 @@ Description-Content-Type: text/markdown
|
|
|
68
68
|
|
|
69
69
|
## What's New in 0.5.x
|
|
70
70
|
|
|
71
|
+
### v0.5.3.1 - Bug Fixes
|
|
72
|
+
|
|
73
|
+
- **Partial updates for persisted records** - `save()` now uses `merge()` for already-persisted records, only sending modified fields
|
|
74
|
+
- **datetime parsing** - `_update_from_db()` now parses ISO 8601 strings to `datetime` objects automatically
|
|
75
|
+
- **`_db_persisted` flag** - Internal tracking to distinguish new vs persisted records
|
|
76
|
+
|
|
71
77
|
### v0.5.3 - ORM Improvements
|
|
72
78
|
|
|
73
|
-
- **Upsert save behavior** - `save()` now uses `upsert` for
|
|
79
|
+
- **Upsert save behavior** - `save()` now uses `upsert` for new records with ID (idempotent, Django-like)
|
|
74
80
|
- **`server_fields` config** - Exclude server-generated fields (created_at, updated_at) from saves
|
|
75
81
|
- **`merge()` returns self** - Now returns the updated model instance instead of None
|
|
76
82
|
- **`save()` updates self** - Updates original instance attributes instead of returning new object
|
|
@@ -3,7 +3,7 @@ surreal_orm/aggregations.py,sha256=5ERMHMWQfaW76OrMNazMjyg7dbf9bJ3GX_8QWz6tfxY,3
|
|
|
3
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=mbU9zEDBN2Sty9c__JyVVFUJ_g_rkNrwxrKUZtDsiDg,28649
|
|
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
|
|
@@ -45,8 +45,8 @@ surreal_sdk/streaming/__init__.py,sha256=TljF9HFN-XOshK_1smmTanF68hcdNsTgdlHtfFo
|
|
|
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.3.dist-info/METADATA,sha256=
|
|
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,,
|
|
48
|
+
surrealdb_orm-0.5.3.1.dist-info/METADATA,sha256=PjZ0BlUAAnqXOJPmIX1spcFq8C_6IV0x35xHpcJH9eo,17464
|
|
49
|
+
surrealdb_orm-0.5.3.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
50
|
+
surrealdb_orm-0.5.3.1.dist-info/entry_points.txt,sha256=3zbp1VzVPSxFrxNuvKZAyt9U432QITdBNjp5QnMZ2o8,62
|
|
51
|
+
surrealdb_orm-0.5.3.1.dist-info/licenses/LICENSE,sha256=OYVJQ7TKsjHAgVndePmKcMWOZkxwJxOHiXOG1TAnCk4,1079
|
|
52
|
+
surrealdb_orm-0.5.3.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|