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 CHANGED
@@ -78,4 +78,4 @@ __all__ = [
78
78
  "AuthenticatedUserMixin",
79
79
  ]
80
80
 
81
- __version__ = "0.4.0"
81
+ __version__ = "0.5.2"
@@ -29,14 +29,31 @@ class SurrealDBConnectionManager:
29
29
  return await SurrealDBConnectionManager.get_client()
30
30
 
31
31
  @classmethod
32
- def set_connection(cls, url: str, user: str, password: str, namespace: str, database: str) -> None:
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 kwargs: The connection kwargs for the SurrealDB instance.
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 = 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
- parts.append(f"TYPE {self.field_type}")
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
- parts.append(f"TYPE {self.field_type}")
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
- parts = [f"DEFINE FIELD {self.name} ON {self.table} TYPE {self.previous_type}"]
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
- STRING = "string"
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
- DECIMAL = "decimal"
92
+ BYTES = "bytes"
93
+ UUID = "uuid"
94
+
95
+ # Collection types
54
96
  ARRAY = "array"
97
+ SET = "set"
55
98
  OBJECT = "object"
56
- RECORD = "record"
57
- GEOMETRY = "geometry"
99
+
100
+ # Special types
58
101
  ANY = "any"
59
102
  OPTION = "option"
60
- BYTES = "bytes"
61
- UUID = "uuid"
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
@@ -64,7 +64,7 @@ from .functions import (
64
64
  CryptoFunctions,
65
65
  )
66
66
 
67
- __version__ = "0.5.0"
67
+ __version__ = "0.5.2"
68
68
  __all__ = [
69
69
  # Connections
70
70
  "BaseSurrealConnection",
@@ -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) -> None:
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
@@ -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) -> None:
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
- json=request.to_dict(),
118
- headers=self.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) -> None:
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
- asyncio.create_task(callback(message))
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."""
@@ -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":
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "surreal-sdk"
3
- version = "0.5.0"
3
+ version = "0.5.2"
4
4
  description = "Custom Python SDK for SurrealDB with HTTP and WebSocket support. No dependency on official surrealdb package."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: surrealdb-orm
3
- Version: 0.5.0
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.4.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 LiveQuery, LiveNotification, LiveAction
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
- if notification.action == LiveAction.CREATE:
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
- # ... records changes trigger callbacks ...
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=zVeDrYHh1VOd1IDnUAnvIdJhilHGtzPP_8TDQOAEv48,1645
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=SA4W399idh8vVl5JI0LX_l-YE1C9KTfv51CaFNtQyGc,9971
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=STFrLauWm3r1GOm4bX6hTdSwupcfjAZGIk6hsDnYO0M,22953
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=bEbEXXVSRdjOFjt6IRZIXUjAjDvIymWdrxUGGa5imQY,2022
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=8WVn0vo51blD8KpzuDFUAvR211vSkZZKyBFtcoa0Qmc,14207
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=3lILkwMYryPxLUQrYdlKI3swWAIw_tHHSbopnno5_ko,4055
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=L9pvaMK4wYDbxE3_yHWWQN39ThNXhqpzrQunkB5i3wk,1421
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=8InOGVHgcUiGLQcReNAK_D1yLH6gVP_jKXWgCVEzbBk,15165
39
- surreal_sdk/connection/http.py,sha256=0fef-CGJVUVQPXu_RGyR2bUQiebXk7l0pqXUHvc5m9M,12435
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=veqDVLu3fpWMIZEi3QrKSBFyGlHLiQfMichgH8u-LiU,17739
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=_P-Am0T6OpXxO_9Un7RZnNe90ebeX0EJFHnBu7_k8TE,5945
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.0.dist-info/METADATA,sha256=Ej7jNafjum_k4iOE3RrO9F3uclX7tqU7QkL8_hZiYJA,14375
49
- surrealdb_orm-0.5.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
50
- surrealdb_orm-0.5.0.dist-info/entry_points.txt,sha256=3zbp1VzVPSxFrxNuvKZAyt9U432QITdBNjp5QnMZ2o8,62
51
- surrealdb_orm-0.5.0.dist-info/licenses/LICENSE,sha256=OYVJQ7TKsjHAgVndePmKcMWOZkxwJxOHiXOG1TAnCk4,1079
52
- surrealdb_orm-0.5.0.dist-info/RECORD,,
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,,