surrealdb-orm 0.5.0__py3-none-any.whl → 0.5.2__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/__init__.py +1 -1
- surreal_orm/connection_manager.py +20 -3
- surreal_orm/migrations/operations.py +79 -12
- surreal_orm/model_base.py +3 -3
- surreal_orm/types.py +98 -6
- surreal_sdk/__init__.py +1 -1
- surreal_sdk/connection/base.py +2 -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
- {surrealdb_orm-0.5.0.dist-info → surrealdb_orm-0.5.2.dist-info}/METADATA +71 -10
- {surrealdb_orm-0.5.0.dist-info → surrealdb_orm-0.5.2.dist-info}/RECORD +16 -16
- {surrealdb_orm-0.5.0.dist-info → surrealdb_orm-0.5.2.dist-info}/WHEEL +0 -0
- {surrealdb_orm-0.5.0.dist-info → surrealdb_orm-0.5.2.dist-info}/entry_points.txt +0 -0
- {surrealdb_orm-0.5.0.dist-info → surrealdb_orm-0.5.2.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
|
@@ -320,7 +320,7 @@ class BaseSurrealModel(BaseModel):
|
|
|
320
320
|
"""
|
|
321
321
|
if tx is not None:
|
|
322
322
|
# Use transaction
|
|
323
|
-
data = self.model_dump(exclude={"id"})
|
|
323
|
+
data = self.model_dump(exclude={"id"}, exclude_unset=True)
|
|
324
324
|
id = self.get_id()
|
|
325
325
|
table = self.get_table_name()
|
|
326
326
|
|
|
@@ -335,7 +335,7 @@ class BaseSurrealModel(BaseModel):
|
|
|
335
335
|
|
|
336
336
|
# Original behavior without transaction
|
|
337
337
|
client = await SurrealDBConnectionManager.get_client()
|
|
338
|
-
data = self.model_dump(exclude={"id"})
|
|
338
|
+
data = self.model_dump(exclude={"id"}, exclude_unset=True)
|
|
339
339
|
id = self.get_id()
|
|
340
340
|
table = self.get_table_name()
|
|
341
341
|
|
|
@@ -365,7 +365,7 @@ class BaseSurrealModel(BaseModel):
|
|
|
365
365
|
Args:
|
|
366
366
|
tx: Optional transaction to use for this operation.
|
|
367
367
|
"""
|
|
368
|
-
data = self.model_dump(exclude={"id"})
|
|
368
|
+
data = self.model_dump(exclude={"id"}, exclude_unset=True)
|
|
369
369
|
id = self.get_id()
|
|
370
370
|
|
|
371
371
|
if id is None:
|
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
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: surrealdb-orm
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
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,36 @@ 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.2 - Bug Fixes & FieldType Improvements
|
|
72
|
+
|
|
73
|
+
- **FieldType enum** - Enhanced migration type system with `generic()` and `from_python_type()` methods
|
|
74
|
+
- **datetime serialization** - Proper JSON encoding for datetime, date, time, Decimal, UUID
|
|
75
|
+
- **Fluent API** - `connect()` now returns `self` for method chaining
|
|
76
|
+
- **Session cleanup** - WebSocket callback tasks properly tracked and cancelled
|
|
77
|
+
- **Optional fields** - `exclude_unset=True` prevents None from overriding DB defaults
|
|
78
|
+
- **Parameter alias** - `username` parameter alias for `user` in ConnectionManager
|
|
79
|
+
|
|
80
|
+
### v0.5.1 - Security Workflows
|
|
81
|
+
|
|
82
|
+
- **Dependabot integration** - Automatic dependency security updates
|
|
83
|
+
- **Auto-merge** - Dependabot PRs merged after CI passes
|
|
84
|
+
- **SurrealDB monitoring** - Integration tests on new SurrealDB releases
|
|
85
|
+
|
|
86
|
+
### v0.5.0 - Real-time SDK Enhancements
|
|
87
|
+
|
|
88
|
+
- **Live Select Stream** - Async iterator pattern for real-time changes
|
|
89
|
+
- `async with db.live_select("table") as stream: async for change in stream:`
|
|
90
|
+
- `LiveChange` dataclass with `record_id`, `action`, `result`, `changed_fields`
|
|
91
|
+
- WHERE clause support with parameterized queries
|
|
92
|
+
- **Auto-Resubscribe** - Automatic reconnection after WebSocket disconnect
|
|
93
|
+
- `auto_resubscribe=True` parameter for seamless K8s pod restart recovery
|
|
94
|
+
- `on_reconnect(old_id, new_id)` callback for tracking ID changes
|
|
95
|
+
- **Typed Function Calls** - Pydantic/dataclass return type support
|
|
96
|
+
- `await db.call("fn::my_func", params={...}, return_type=MyModel)`
|
|
97
|
+
|
|
98
|
+
### v0.4.0 - Relations & Graph
|
|
70
99
|
|
|
71
100
|
- **Relations & Graph Traversal** - Django-style relation definitions with SurrealDB graph support
|
|
72
101
|
- `ForeignKey`, `ManyToMany`, `Relation` field types
|
|
@@ -250,22 +279,54 @@ result = await db.fn.my_custom_function(arg1, arg2)
|
|
|
250
279
|
Real-time updates via WebSocket:
|
|
251
280
|
|
|
252
281
|
```python
|
|
253
|
-
from surreal_sdk import
|
|
282
|
+
from surreal_sdk import LiveAction
|
|
283
|
+
|
|
284
|
+
# Async iterator pattern (recommended)
|
|
285
|
+
async with db.live_select(
|
|
286
|
+
"orders",
|
|
287
|
+
where="status = $status",
|
|
288
|
+
params={"status": "pending"},
|
|
289
|
+
auto_resubscribe=True, # Auto-reconnect on WebSocket drop
|
|
290
|
+
) as stream:
|
|
291
|
+
async for change in stream:
|
|
292
|
+
match change.action:
|
|
293
|
+
case LiveAction.CREATE:
|
|
294
|
+
print(f"New order: {change.result}")
|
|
295
|
+
case LiveAction.UPDATE:
|
|
296
|
+
print(f"Updated: {change.record_id}")
|
|
297
|
+
case LiveAction.DELETE:
|
|
298
|
+
print(f"Deleted: {change.record_id}")
|
|
299
|
+
|
|
300
|
+
# Callback-based pattern
|
|
301
|
+
from surreal_sdk import LiveQuery, LiveNotification
|
|
254
302
|
|
|
255
303
|
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}")
|
|
304
|
+
print(f"{notification.action}: {notification.result}")
|
|
262
305
|
|
|
263
306
|
live = LiveQuery(ws_conn, "orders")
|
|
264
307
|
await live.subscribe(on_change)
|
|
265
|
-
# ...
|
|
308
|
+
# ... record changes trigger callbacks ...
|
|
266
309
|
await live.unsubscribe()
|
|
267
310
|
```
|
|
268
311
|
|
|
312
|
+
**Typed Function Calls:**
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
from pydantic import BaseModel
|
|
316
|
+
|
|
317
|
+
class VoteResult(BaseModel):
|
|
318
|
+
success: bool
|
|
319
|
+
count: int
|
|
320
|
+
|
|
321
|
+
# Call SurrealDB function with typed return
|
|
322
|
+
result = await db.call(
|
|
323
|
+
"cast_vote",
|
|
324
|
+
params={"user": "alice", "vote": "yes"},
|
|
325
|
+
return_type=VoteResult
|
|
326
|
+
)
|
|
327
|
+
print(result.success, result.count) # Typed access
|
|
328
|
+
```
|
|
329
|
+
|
|
269
330
|
---
|
|
270
331
|
|
|
271
332
|
## 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=8UjJGAHhNyltaSKp5yriXJHrxw_g05Z3nQvJ99Bb5QE,23013
|
|
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=
|
|
34
|
+
surreal_sdk/pyproject.toml,sha256=pbcDsXdadtxF2d4aQKlinbEYEIftYfGy-T_lZX_8sRw,1421
|
|
35
35
|
surreal_sdk/transaction.py,sha256=FkRcMO3qIb0CmjYnPRseDcZ1GIe-v1KRJeuATBuT_HU,13396
|
|
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=Z8I3thpIHaKxI6CTv-sErdDzopaOZJHQLas3BX6tXXY,15194
|
|
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.2.dist-info/METADATA,sha256=K8-wXndSr-_25BO46XjHatDzUJikSChhSqDm9paEMqc,16596
|
|
49
|
+
surrealdb_orm-0.5.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
50
|
+
surrealdb_orm-0.5.2.dist-info/entry_points.txt,sha256=3zbp1VzVPSxFrxNuvKZAyt9U432QITdBNjp5QnMZ2o8,62
|
|
51
|
+
surrealdb_orm-0.5.2.dist-info/licenses/LICENSE,sha256=OYVJQ7TKsjHAgVndePmKcMWOZkxwJxOHiXOG1TAnCk4,1079
|
|
52
|
+
surrealdb_orm-0.5.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|