surreal-orm-lite 0.3.0__tar.gz → 0.4.0__tar.gz
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_lite-0.3.0 → surreal_orm_lite-0.4.0}/CHANGELOG.md +25 -0
- {surreal_orm_lite-0.3.0 → surreal_orm_lite-0.4.0}/PKG-INFO +76 -1
- {surreal_orm_lite-0.3.0 → surreal_orm_lite-0.4.0}/README.md +75 -0
- {surreal_orm_lite-0.3.0 → surreal_orm_lite-0.4.0}/pyproject.toml +1 -1
- {surreal_orm_lite-0.3.0 → surreal_orm_lite-0.4.0}/src/surreal_orm_lite/__init__.py +26 -1
- {surreal_orm_lite-0.3.0 → surreal_orm_lite-0.4.0}/src/surreal_orm_lite/model_base.py +101 -14
- surreal_orm_lite-0.4.0/src/surreal_orm_lite/signals.py +286 -0
- {surreal_orm_lite-0.3.0 → surreal_orm_lite-0.4.0}/.gitignore +0 -0
- {surreal_orm_lite-0.3.0 → surreal_orm_lite-0.4.0}/LICENSE +0 -0
- {surreal_orm_lite-0.3.0 → surreal_orm_lite-0.4.0}/Makefile +0 -0
- {surreal_orm_lite-0.3.0 → surreal_orm_lite-0.4.0}/src/__init__.py +0 -0
- {surreal_orm_lite-0.3.0 → surreal_orm_lite-0.4.0}/src/surreal_orm_lite/aggregations.py +0 -0
- {surreal_orm_lite-0.3.0 → surreal_orm_lite-0.4.0}/src/surreal_orm_lite/connection_manager.py +0 -0
- {surreal_orm_lite-0.3.0 → surreal_orm_lite-0.4.0}/src/surreal_orm_lite/constants.py +0 -0
- {surreal_orm_lite-0.3.0 → surreal_orm_lite-0.4.0}/src/surreal_orm_lite/enum.py +0 -0
- {surreal_orm_lite-0.3.0 → surreal_orm_lite-0.4.0}/src/surreal_orm_lite/exceptions.py +0 -0
- {surreal_orm_lite-0.3.0 → surreal_orm_lite-0.4.0}/src/surreal_orm_lite/py.typed +0 -0
- {surreal_orm_lite-0.3.0 → surreal_orm_lite-0.4.0}/src/surreal_orm_lite/query_set.py +0 -0
- {surreal_orm_lite-0.3.0 → surreal_orm_lite-0.4.0}/src/surreal_orm_lite/utils.py +0 -0
|
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.4.0] - 2026-02-07
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Model Signals**: Django-style event system for model lifecycle
|
|
13
|
+
- `Signal` class for pre/post event handlers
|
|
14
|
+
- `AroundSignal` class for context manager-style wrapping signals with `yield`
|
|
15
|
+
- `pre_save` / `post_save` - Fired before/after `save()` operations
|
|
16
|
+
- `pre_update` / `post_update` - Fired before/after `update()` and `merge()` operations
|
|
17
|
+
- `pre_delete` / `post_delete` - Fired before/after `delete()` operations
|
|
18
|
+
- `around_save` / `around_update` / `around_delete` - Wrap operations for timing, logging, etc.
|
|
19
|
+
- `connect(model_class)` decorator for registering handlers
|
|
20
|
+
- `disconnect(handler, model_class)` for removing handlers
|
|
21
|
+
- `clear()` for removing all handlers
|
|
22
|
+
- `has_handlers()` for checking if handlers are registered
|
|
23
|
+
|
|
24
|
+
- `post_save` signal includes `created` flag to distinguish new records
|
|
25
|
+
- `pre_update` / `post_update` signals include `update_fields` list
|
|
26
|
+
- New test file `tests/test_signals.py` with unit and e2e tests
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- `save()`, `update()`, `merge()`, `delete()` now emit signals when handlers are registered
|
|
31
|
+
- Internal `_do_save()` method extracted from `save()` for signal integration
|
|
32
|
+
|
|
8
33
|
## [0.3.0] - 2026-02-05
|
|
9
34
|
|
|
10
35
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: surreal-orm-lite
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Lightweight Django-style ORM for SurrealDB using the official Python SDK. Async support with Pydantic validation.
|
|
5
5
|
Project-URL: Homepage, https://github.com/EulogySnowfall/SurrealDB-ORM-lite
|
|
6
6
|
Project-URL: Documentation, https://github.com/EulogySnowfall/SurrealDB-ORM-lite
|
|
@@ -191,6 +191,9 @@ results = await User.objects().query(
|
|
|
191
191
|
| Custom primary keys | ✅ |
|
|
192
192
|
| HTTP connections | ✅ |
|
|
193
193
|
| WebSocket connections | ✅ |
|
|
194
|
+
| Aggregations | ✅ |
|
|
195
|
+
| GROUP BY | ✅ |
|
|
196
|
+
| Model Signals | ✅ |
|
|
194
197
|
|
|
195
198
|
### Supported Filter Lookups
|
|
196
199
|
|
|
@@ -201,6 +204,78 @@ results = await User.objects().query(
|
|
|
201
204
|
- `startswith`, `istartswith`
|
|
202
205
|
- `endswith`, `iendswith`
|
|
203
206
|
|
|
207
|
+
### 5. Aggregations
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
from surreal_orm_lite import Count, Sum, Avg, Min, Max
|
|
211
|
+
|
|
212
|
+
# Simple aggregations
|
|
213
|
+
count = await User.objects().count()
|
|
214
|
+
total = await Order.objects().sum("amount")
|
|
215
|
+
avg_age = await User.objects().avg("age")
|
|
216
|
+
max_price = await Product.objects().max("price")
|
|
217
|
+
min_price = await Product.objects().min("price")
|
|
218
|
+
|
|
219
|
+
# Check existence
|
|
220
|
+
has_admins = await User.objects().filter(role="admin").exists()
|
|
221
|
+
|
|
222
|
+
# GROUP BY with annotations
|
|
223
|
+
results = await User.objects().values("status").annotate(count=Count()).exec()
|
|
224
|
+
# [{"status": "active", "count": 42}, {"status": "inactive", "count": 8}]
|
|
225
|
+
|
|
226
|
+
# Raw SurrealQL queries
|
|
227
|
+
results = await User.raw_query(
|
|
228
|
+
"SELECT * FROM User WHERE age > $min_age",
|
|
229
|
+
variables={"min_age": 18}
|
|
230
|
+
)
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### 6. Model Signals
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
from surreal_orm_lite import pre_save, post_save, pre_delete, post_delete
|
|
237
|
+
|
|
238
|
+
@post_save.connect(User)
|
|
239
|
+
async def on_user_saved(sender, instance, created, **kwargs):
|
|
240
|
+
"""Called after every User save."""
|
|
241
|
+
if created:
|
|
242
|
+
await send_welcome_email(instance.email)
|
|
243
|
+
await invalidate_cache(f"user:{instance.id}")
|
|
244
|
+
|
|
245
|
+
@pre_delete.connect(User)
|
|
246
|
+
async def on_user_deleting(sender, instance, **kwargs):
|
|
247
|
+
"""Called before User deletion."""
|
|
248
|
+
await archive_user_data(instance.id)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Available signals:**
|
|
252
|
+
|
|
253
|
+
| Signal | When | Extra kwargs |
|
|
254
|
+
| --------------- | --------------------------- | ---------------- |
|
|
255
|
+
| `pre_save` | Before `save()` | |
|
|
256
|
+
| `post_save` | After `save()` | `created` |
|
|
257
|
+
| `pre_update` | Before `update()`/`merge()` | `update_fields` |
|
|
258
|
+
| `post_update` | After `update()`/`merge()` | `update_fields` |
|
|
259
|
+
| `pre_delete` | Before `delete()` | |
|
|
260
|
+
| `post_delete` | After `delete()` | |
|
|
261
|
+
| `around_save` | Wraps `save()` | |
|
|
262
|
+
| `around_update` | Wraps `update()`/`merge()` | `update_fields` |
|
|
263
|
+
| `around_delete` | Wraps `delete()` | |
|
|
264
|
+
|
|
265
|
+
**Around signals** use async generators to wrap operations:
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
from surreal_orm_lite import around_save
|
|
269
|
+
|
|
270
|
+
@around_save.connect(User)
|
|
271
|
+
async def time_user_save(sender, instance, **kwargs):
|
|
272
|
+
import time
|
|
273
|
+
start = time.time()
|
|
274
|
+
yield # save() executes here
|
|
275
|
+
duration = time.time() - start
|
|
276
|
+
print(f"Save took {duration:.3f}s")
|
|
277
|
+
```
|
|
278
|
+
|
|
204
279
|
---
|
|
205
280
|
|
|
206
281
|
## Configuration Options
|
|
@@ -141,6 +141,9 @@ results = await User.objects().query(
|
|
|
141
141
|
| Custom primary keys | ✅ |
|
|
142
142
|
| HTTP connections | ✅ |
|
|
143
143
|
| WebSocket connections | ✅ |
|
|
144
|
+
| Aggregations | ✅ |
|
|
145
|
+
| GROUP BY | ✅ |
|
|
146
|
+
| Model Signals | ✅ |
|
|
144
147
|
|
|
145
148
|
### Supported Filter Lookups
|
|
146
149
|
|
|
@@ -151,6 +154,78 @@ results = await User.objects().query(
|
|
|
151
154
|
- `startswith`, `istartswith`
|
|
152
155
|
- `endswith`, `iendswith`
|
|
153
156
|
|
|
157
|
+
### 5. Aggregations
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from surreal_orm_lite import Count, Sum, Avg, Min, Max
|
|
161
|
+
|
|
162
|
+
# Simple aggregations
|
|
163
|
+
count = await User.objects().count()
|
|
164
|
+
total = await Order.objects().sum("amount")
|
|
165
|
+
avg_age = await User.objects().avg("age")
|
|
166
|
+
max_price = await Product.objects().max("price")
|
|
167
|
+
min_price = await Product.objects().min("price")
|
|
168
|
+
|
|
169
|
+
# Check existence
|
|
170
|
+
has_admins = await User.objects().filter(role="admin").exists()
|
|
171
|
+
|
|
172
|
+
# GROUP BY with annotations
|
|
173
|
+
results = await User.objects().values("status").annotate(count=Count()).exec()
|
|
174
|
+
# [{"status": "active", "count": 42}, {"status": "inactive", "count": 8}]
|
|
175
|
+
|
|
176
|
+
# Raw SurrealQL queries
|
|
177
|
+
results = await User.raw_query(
|
|
178
|
+
"SELECT * FROM User WHERE age > $min_age",
|
|
179
|
+
variables={"min_age": 18}
|
|
180
|
+
)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### 6. Model Signals
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from surreal_orm_lite import pre_save, post_save, pre_delete, post_delete
|
|
187
|
+
|
|
188
|
+
@post_save.connect(User)
|
|
189
|
+
async def on_user_saved(sender, instance, created, **kwargs):
|
|
190
|
+
"""Called after every User save."""
|
|
191
|
+
if created:
|
|
192
|
+
await send_welcome_email(instance.email)
|
|
193
|
+
await invalidate_cache(f"user:{instance.id}")
|
|
194
|
+
|
|
195
|
+
@pre_delete.connect(User)
|
|
196
|
+
async def on_user_deleting(sender, instance, **kwargs):
|
|
197
|
+
"""Called before User deletion."""
|
|
198
|
+
await archive_user_data(instance.id)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Available signals:**
|
|
202
|
+
|
|
203
|
+
| Signal | When | Extra kwargs |
|
|
204
|
+
| --------------- | --------------------------- | ---------------- |
|
|
205
|
+
| `pre_save` | Before `save()` | |
|
|
206
|
+
| `post_save` | After `save()` | `created` |
|
|
207
|
+
| `pre_update` | Before `update()`/`merge()` | `update_fields` |
|
|
208
|
+
| `post_update` | After `update()`/`merge()` | `update_fields` |
|
|
209
|
+
| `pre_delete` | Before `delete()` | |
|
|
210
|
+
| `post_delete` | After `delete()` | |
|
|
211
|
+
| `around_save` | Wraps `save()` | |
|
|
212
|
+
| `around_update` | Wraps `update()`/`merge()` | `update_fields` |
|
|
213
|
+
| `around_delete` | Wraps `delete()` | |
|
|
214
|
+
|
|
215
|
+
**Around signals** use async generators to wrap operations:
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
from surreal_orm_lite import around_save
|
|
219
|
+
|
|
220
|
+
@around_save.connect(User)
|
|
221
|
+
async def time_user_save(sender, instance, **kwargs):
|
|
222
|
+
import time
|
|
223
|
+
start = time.time()
|
|
224
|
+
yield # save() executes here
|
|
225
|
+
duration = time.time() - start
|
|
226
|
+
print(f"Save took {duration:.3f}s")
|
|
227
|
+
```
|
|
228
|
+
|
|
154
229
|
---
|
|
155
230
|
|
|
156
231
|
## Configuration Options
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.4.0"
|
|
2
2
|
|
|
3
3
|
from .aggregations import Aggregation, Avg, Count, Max, Min, Sum
|
|
4
4
|
from .connection_manager import SurrealDBConnectionManager
|
|
@@ -12,6 +12,19 @@ from .exceptions import (
|
|
|
12
12
|
)
|
|
13
13
|
from .model_base import BaseSurrealModel, SurrealConfigDict
|
|
14
14
|
from .query_set import QuerySet
|
|
15
|
+
from .signals import (
|
|
16
|
+
AroundSignal,
|
|
17
|
+
Signal,
|
|
18
|
+
around_delete,
|
|
19
|
+
around_save,
|
|
20
|
+
around_update,
|
|
21
|
+
post_delete,
|
|
22
|
+
post_save,
|
|
23
|
+
post_update,
|
|
24
|
+
pre_delete,
|
|
25
|
+
pre_save,
|
|
26
|
+
pre_update,
|
|
27
|
+
)
|
|
15
28
|
|
|
16
29
|
__all__ = [
|
|
17
30
|
"__version__",
|
|
@@ -30,6 +43,18 @@ __all__ = [
|
|
|
30
43
|
"Avg",
|
|
31
44
|
"Min",
|
|
32
45
|
"Max",
|
|
46
|
+
# Signals
|
|
47
|
+
"Signal",
|
|
48
|
+
"AroundSignal",
|
|
49
|
+
"pre_save",
|
|
50
|
+
"post_save",
|
|
51
|
+
"pre_update",
|
|
52
|
+
"post_update",
|
|
53
|
+
"pre_delete",
|
|
54
|
+
"post_delete",
|
|
55
|
+
"around_save",
|
|
56
|
+
"around_update",
|
|
57
|
+
"around_delete",
|
|
33
58
|
# Exceptions
|
|
34
59
|
"SurrealORMError",
|
|
35
60
|
"SurrealDbError",
|
|
@@ -7,6 +7,17 @@ from surrealdb import RecordID
|
|
|
7
7
|
|
|
8
8
|
from .connection_manager import SurrealDBConnectionManager
|
|
9
9
|
from .exceptions import SurrealDbError
|
|
10
|
+
from .signals import (
|
|
11
|
+
around_delete,
|
|
12
|
+
around_save,
|
|
13
|
+
around_update,
|
|
14
|
+
post_delete,
|
|
15
|
+
post_save,
|
|
16
|
+
post_update,
|
|
17
|
+
pre_delete,
|
|
18
|
+
pre_save,
|
|
19
|
+
pre_update,
|
|
20
|
+
)
|
|
10
21
|
|
|
11
22
|
logger = logging.getLogger(__name__)
|
|
12
23
|
|
|
@@ -52,7 +63,7 @@ class BaseSurrealModel(BaseModel):
|
|
|
52
63
|
Get the ID of the model instance.
|
|
53
64
|
"""
|
|
54
65
|
if hasattr(self, "id"):
|
|
55
|
-
id_value = self.id
|
|
66
|
+
id_value = self.id # type: ignore[attr-defined]
|
|
56
67
|
return str(id_value) if id_value is not None else None
|
|
57
68
|
|
|
58
69
|
if hasattr(self, "model_config"):
|
|
@@ -111,9 +122,10 @@ class BaseSurrealModel(BaseModel):
|
|
|
111
122
|
if hasattr(self, key):
|
|
112
123
|
object.__setattr__(self, key, value)
|
|
113
124
|
|
|
114
|
-
async def
|
|
125
|
+
async def _do_save(self) -> tuple[Self, bool]:
|
|
115
126
|
"""
|
|
116
|
-
|
|
127
|
+
Internal save logic. Returns (self, created) where created indicates
|
|
128
|
+
whether a new record was created (always True for save).
|
|
117
129
|
"""
|
|
118
130
|
client = await SurrealDBConnectionManager.get_client()
|
|
119
131
|
data = self.model_dump(exclude={"id"})
|
|
@@ -122,13 +134,13 @@ class BaseSurrealModel(BaseModel):
|
|
|
122
134
|
|
|
123
135
|
if id is not None:
|
|
124
136
|
# Escape special characters in ID
|
|
125
|
-
escaped_id = f"`{id}`" if any(c in str(id) for c in "@#$%^&*()") else id
|
|
137
|
+
escaped_id = f"`{id}`" if any(c in str(id) for c in "@#$%^&*()-+=/\\! ") else id
|
|
126
138
|
thing = f"{table}:{escaped_id}"
|
|
127
139
|
result = await client.create(thing, data)
|
|
128
140
|
# SDK 1.0.8 returns error message as string instead of raising exception
|
|
129
141
|
if isinstance(result, str) and "already exists" in result:
|
|
130
142
|
raise SurrealDbError(f"There was a problem with the database: {result}")
|
|
131
|
-
return self
|
|
143
|
+
return self, True
|
|
132
144
|
|
|
133
145
|
# Auto-generate the ID
|
|
134
146
|
record = await client.create(table, data) # pragma: no cover
|
|
@@ -150,38 +162,95 @@ class BaseSurrealModel(BaseModel):
|
|
|
150
162
|
value = str(value).split(":")[1]
|
|
151
163
|
if hasattr(self, key):
|
|
152
164
|
object.__setattr__(self, key, value)
|
|
153
|
-
return self
|
|
165
|
+
return self, True
|
|
154
166
|
|
|
155
167
|
raise SurrealDbError("Can't save data, no record returned.") # pragma: no cover
|
|
156
168
|
|
|
169
|
+
async def save(self) -> Self:
|
|
170
|
+
"""
|
|
171
|
+
Save the model instance to the database.
|
|
172
|
+
|
|
173
|
+
Emits pre_save, post_save, and around_save signals.
|
|
174
|
+
"""
|
|
175
|
+
sender = self.__class__
|
|
176
|
+
has_signals = pre_save.has_handlers(sender) or post_save.has_handlers(sender) or around_save.has_handlers(sender)
|
|
177
|
+
|
|
178
|
+
if not has_signals:
|
|
179
|
+
result, created = await self._do_save()
|
|
180
|
+
return result
|
|
181
|
+
|
|
182
|
+
await pre_save.send(sender, instance=self)
|
|
183
|
+
|
|
184
|
+
async with around_save.wrap(sender, instance=self):
|
|
185
|
+
result, created = await self._do_save()
|
|
186
|
+
|
|
187
|
+
await post_save.send(sender, instance=self, created=created)
|
|
188
|
+
|
|
189
|
+
return result
|
|
190
|
+
|
|
157
191
|
async def update(self) -> Any:
|
|
158
192
|
"""
|
|
159
193
|
Update the model instance to the database.
|
|
194
|
+
|
|
195
|
+
Emits pre_update, post_update, and around_update signals.
|
|
160
196
|
"""
|
|
161
197
|
client = await SurrealDBConnectionManager.get_client()
|
|
198
|
+
sender = self.__class__
|
|
162
199
|
|
|
163
200
|
data = self.model_dump(exclude={"id"})
|
|
164
201
|
id = self.get_id()
|
|
165
202
|
if id is not None:
|
|
166
203
|
thing = f"{self.__class__.__name__}:{id}"
|
|
167
|
-
|
|
204
|
+
update_fields = list(data.keys())
|
|
205
|
+
has_signals = (
|
|
206
|
+
pre_update.has_handlers(sender) or post_update.has_handlers(sender) or around_update.has_handlers(sender)
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
if not has_signals:
|
|
210
|
+
return await client.update(thing, data)
|
|
211
|
+
|
|
212
|
+
await pre_update.send(sender, instance=self, update_fields=update_fields)
|
|
213
|
+
|
|
214
|
+
async with around_update.wrap(sender, instance=self, update_fields=update_fields):
|
|
215
|
+
result = await client.update(thing, data)
|
|
216
|
+
|
|
217
|
+
await post_update.send(sender, instance=self, update_fields=update_fields)
|
|
218
|
+
|
|
168
219
|
return result
|
|
169
220
|
raise SurrealDbError("Can't update data, no id found.")
|
|
170
221
|
|
|
171
222
|
async def merge(self, **data: Any) -> Any:
|
|
172
223
|
"""
|
|
173
|
-
|
|
224
|
+
Partial update of the model instance in the database.
|
|
225
|
+
|
|
226
|
+
Emits pre_update, post_update, and around_update signals.
|
|
174
227
|
"""
|
|
175
228
|
|
|
176
229
|
client = await SurrealDBConnectionManager.get_client()
|
|
230
|
+
sender = self.__class__
|
|
177
231
|
data_set = dict(data.items())
|
|
178
232
|
|
|
179
233
|
id = self.get_id()
|
|
180
|
-
if id:
|
|
234
|
+
if id is not None:
|
|
181
235
|
thing = f"{self.get_table_name()}:{id}"
|
|
236
|
+
update_fields = list(data_set.keys())
|
|
237
|
+
has_signals = (
|
|
238
|
+
pre_update.has_handlers(sender) or post_update.has_handlers(sender) or around_update.has_handlers(sender)
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if not has_signals:
|
|
242
|
+
await client.merge(thing, data_set)
|
|
243
|
+
await self.refresh()
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
await pre_update.send(sender, instance=self, update_fields=update_fields)
|
|
247
|
+
|
|
248
|
+
async with around_update.wrap(sender, instance=self, update_fields=update_fields):
|
|
249
|
+
await client.merge(thing, data_set)
|
|
250
|
+
await self.refresh()
|
|
251
|
+
|
|
252
|
+
await post_update.send(sender, instance=self, update_fields=update_fields)
|
|
182
253
|
|
|
183
|
-
await client.merge(thing, data_set)
|
|
184
|
-
await self.refresh()
|
|
185
254
|
return
|
|
186
255
|
|
|
187
256
|
raise SurrealDbError(f"No Id for the data to merge: {data}")
|
|
@@ -189,18 +258,36 @@ class BaseSurrealModel(BaseModel):
|
|
|
189
258
|
async def delete(self) -> None:
|
|
190
259
|
"""
|
|
191
260
|
Delete the model instance from the database.
|
|
261
|
+
|
|
262
|
+
Emits pre_delete, post_delete, and around_delete signals.
|
|
192
263
|
"""
|
|
193
264
|
|
|
194
265
|
client = await SurrealDBConnectionManager.get_client()
|
|
266
|
+
sender = self.__class__
|
|
195
267
|
|
|
196
268
|
id = self.get_id()
|
|
269
|
+
if id is None:
|
|
270
|
+
raise SurrealDbError("Can't delete data, no id found.")
|
|
197
271
|
|
|
198
272
|
thing = f"{self.get_table_name()}:{id}"
|
|
273
|
+
has_signals = pre_delete.has_handlers(sender) or post_delete.has_handlers(sender) or around_delete.has_handlers(sender)
|
|
274
|
+
|
|
275
|
+
if not has_signals:
|
|
276
|
+
deleted = await client.delete(thing)
|
|
277
|
+
if not deleted:
|
|
278
|
+
raise SurrealDbError(f"Can't delete Record id -> '{id}' not found!")
|
|
279
|
+
logger.info(f"Record deleted -> {deleted}.")
|
|
280
|
+
return
|
|
281
|
+
|
|
282
|
+
await pre_delete.send(sender, instance=self)
|
|
283
|
+
|
|
284
|
+
async with around_delete.wrap(sender, instance=self):
|
|
285
|
+
deleted = await client.delete(thing)
|
|
199
286
|
|
|
200
|
-
|
|
287
|
+
if not deleted:
|
|
288
|
+
raise SurrealDbError(f"Can't delete Record id -> '{id}' not found!")
|
|
201
289
|
|
|
202
|
-
|
|
203
|
-
raise SurrealDbError(f"Can't delete Record id -> '{id}' not found!")
|
|
290
|
+
await post_delete.send(sender, instance=self)
|
|
204
291
|
|
|
205
292
|
logger.info(f"Record deleted -> {deleted}.")
|
|
206
293
|
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Signal system for SurrealDB-ORM-lite.
|
|
3
|
+
|
|
4
|
+
Provides Django-style pre/post signals and around signals
|
|
5
|
+
for model lifecycle events (save, update, delete).
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from surreal_orm_lite import pre_save, post_save, pre_delete, post_delete
|
|
9
|
+
|
|
10
|
+
@post_save.connect(User)
|
|
11
|
+
async def on_user_saved(sender, instance, created, **kwargs):
|
|
12
|
+
if created:
|
|
13
|
+
await send_welcome_email(instance.email)
|
|
14
|
+
|
|
15
|
+
@around_save.connect(User)
|
|
16
|
+
async def time_user_save(sender, instance, **kwargs):
|
|
17
|
+
import time
|
|
18
|
+
start = time.time()
|
|
19
|
+
yield # save() executes here
|
|
20
|
+
duration = time.time() - start
|
|
21
|
+
print(f"Save took {duration:.3f}s")
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import contextlib
|
|
27
|
+
import logging
|
|
28
|
+
from collections import defaultdict
|
|
29
|
+
from collections.abc import AsyncGenerator, Callable, Coroutine
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
# Type alias for signal handlers
|
|
35
|
+
SignalHandler = Callable[..., Coroutine[Any, Any, None]]
|
|
36
|
+
AroundHandler = Callable[..., AsyncGenerator[None, None]]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Signal:
|
|
40
|
+
"""
|
|
41
|
+
A signal that dispatches events to registered async handlers.
|
|
42
|
+
|
|
43
|
+
Handlers are registered per model class and called in registration order
|
|
44
|
+
when the signal is sent.
|
|
45
|
+
|
|
46
|
+
Example:
|
|
47
|
+
pre_save = Signal("pre_save")
|
|
48
|
+
|
|
49
|
+
@pre_save.connect(User)
|
|
50
|
+
async def on_user_pre_save(sender, instance, **kwargs):
|
|
51
|
+
print(f"About to save {instance}")
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, name: str) -> None:
|
|
55
|
+
self.name = name
|
|
56
|
+
self._handlers: dict[type, list[SignalHandler]] = defaultdict(list)
|
|
57
|
+
|
|
58
|
+
def connect(self, model_class: type) -> Callable[[SignalHandler], SignalHandler]:
|
|
59
|
+
"""
|
|
60
|
+
Decorator to register a handler for a specific model class.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
model_class: The model class to listen for signals on.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Decorator that registers the handler.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def decorator(handler: SignalHandler) -> SignalHandler:
|
|
70
|
+
if handler not in self._handlers[model_class]:
|
|
71
|
+
self._handlers[model_class].append(handler)
|
|
72
|
+
return handler
|
|
73
|
+
|
|
74
|
+
return decorator
|
|
75
|
+
|
|
76
|
+
def disconnect(self, handler: SignalHandler, model_class: type) -> bool:
|
|
77
|
+
"""
|
|
78
|
+
Remove a handler for a specific model class.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
handler: The handler function to remove.
|
|
82
|
+
model_class: The model class the handler was registered for.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
True if the handler was found and removed, False otherwise.
|
|
86
|
+
"""
|
|
87
|
+
handlers = self._handlers.get(model_class, [])
|
|
88
|
+
if handler in handlers:
|
|
89
|
+
handlers.remove(handler)
|
|
90
|
+
if not handlers:
|
|
91
|
+
self._handlers.pop(model_class, None)
|
|
92
|
+
return True
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
async def send(self, sender: type, **kwargs: Any) -> None:
|
|
96
|
+
"""
|
|
97
|
+
Dispatch the signal to all registered handlers for the sender class.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
sender: The model class that is sending the signal.
|
|
101
|
+
**kwargs: Additional keyword arguments passed to handlers.
|
|
102
|
+
"""
|
|
103
|
+
for handler in self._handlers.get(sender, []):
|
|
104
|
+
await handler(sender=sender, **kwargs)
|
|
105
|
+
|
|
106
|
+
def has_handlers(self, model_class: type) -> bool:
|
|
107
|
+
"""Check if any handlers are registered for the given model class."""
|
|
108
|
+
return bool(self._handlers.get(model_class))
|
|
109
|
+
|
|
110
|
+
def clear(self, model_class: type | None = None) -> None:
|
|
111
|
+
"""
|
|
112
|
+
Clear registered handlers.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
model_class: If provided, clear handlers only for this model.
|
|
116
|
+
If None, clear all handlers for all models.
|
|
117
|
+
"""
|
|
118
|
+
if model_class is not None:
|
|
119
|
+
self._handlers.pop(model_class, None)
|
|
120
|
+
else:
|
|
121
|
+
self._handlers.clear()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class AroundSignal:
|
|
125
|
+
"""
|
|
126
|
+
A signal that wraps an operation using async generators (context manager style).
|
|
127
|
+
|
|
128
|
+
Handlers yield once: code before yield runs before the operation,
|
|
129
|
+
code after yield runs after. This allows measuring timing, wrapping
|
|
130
|
+
in try/except, etc.
|
|
131
|
+
|
|
132
|
+
Example:
|
|
133
|
+
around_save = AroundSignal("around_save")
|
|
134
|
+
|
|
135
|
+
@around_save.connect(User)
|
|
136
|
+
async def time_user_save(sender, instance, **kwargs):
|
|
137
|
+
start = time.time()
|
|
138
|
+
yield # save() executes here
|
|
139
|
+
duration = time.time() - start
|
|
140
|
+
print(f"Save took {duration:.3f}s")
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
def __init__(self, name: str) -> None:
|
|
144
|
+
self.name = name
|
|
145
|
+
self._handlers: dict[type, list[AroundHandler]] = defaultdict(list)
|
|
146
|
+
|
|
147
|
+
def connect(self, model_class: type) -> Callable[[AroundHandler], AroundHandler]:
|
|
148
|
+
"""
|
|
149
|
+
Decorator to register an around handler for a specific model class.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
model_class: The model class to listen for signals on.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Decorator that registers the handler.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
def decorator(handler: AroundHandler) -> AroundHandler:
|
|
159
|
+
if handler not in self._handlers[model_class]:
|
|
160
|
+
self._handlers[model_class].append(handler)
|
|
161
|
+
return handler
|
|
162
|
+
|
|
163
|
+
return decorator
|
|
164
|
+
|
|
165
|
+
def disconnect(self, handler: AroundHandler, model_class: type) -> bool:
|
|
166
|
+
"""
|
|
167
|
+
Remove a handler for a specific model class.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
handler: The handler function to remove.
|
|
171
|
+
model_class: The model class the handler was registered for.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
True if the handler was found and removed, False otherwise.
|
|
175
|
+
"""
|
|
176
|
+
handlers = self._handlers.get(model_class, [])
|
|
177
|
+
if handler in handlers:
|
|
178
|
+
handlers.remove(handler)
|
|
179
|
+
if not handlers:
|
|
180
|
+
self._handlers.pop(model_class, None)
|
|
181
|
+
return True
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
@contextlib.asynccontextmanager
|
|
185
|
+
async def wrap(self, sender: type, **kwargs: Any) -> AsyncGenerator[None, None]:
|
|
186
|
+
"""
|
|
187
|
+
Context manager that runs all around handlers for the sender class.
|
|
188
|
+
|
|
189
|
+
The handlers' pre-yield code runs before yielding,
|
|
190
|
+
and their post-yield code runs after in reverse (LIFO) order,
|
|
191
|
+
similar to nested context managers.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
sender: The model class that is sending the signal.
|
|
195
|
+
**kwargs: Additional keyword arguments passed to handlers.
|
|
196
|
+
"""
|
|
197
|
+
handlers = self._handlers.get(sender, [])
|
|
198
|
+
active: list[tuple[AsyncGenerator[None, None], AroundHandler]] = []
|
|
199
|
+
|
|
200
|
+
# Start all generators (run pre-yield code)
|
|
201
|
+
try:
|
|
202
|
+
for handler in handlers:
|
|
203
|
+
gen = handler(sender=sender, **kwargs)
|
|
204
|
+
try:
|
|
205
|
+
await gen.__anext__()
|
|
206
|
+
except StopAsyncIteration:
|
|
207
|
+
logger.warning(
|
|
208
|
+
"AroundSignal handler %r for sender %r did not yield; it must yield exactly once.",
|
|
209
|
+
handler,
|
|
210
|
+
sender,
|
|
211
|
+
)
|
|
212
|
+
with contextlib.suppress(Exception):
|
|
213
|
+
await gen.aclose()
|
|
214
|
+
continue
|
|
215
|
+
active.append((gen, handler))
|
|
216
|
+
except Exception:
|
|
217
|
+
# If startup fails, close any already-started generators in LIFO order
|
|
218
|
+
for gen, _ in reversed(active):
|
|
219
|
+
with contextlib.suppress(Exception):
|
|
220
|
+
await gen.aclose()
|
|
221
|
+
raise
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
yield
|
|
225
|
+
finally:
|
|
226
|
+
# Complete all generators (run post-yield code) in reverse (LIFO) order.
|
|
227
|
+
# Post-yield handler errors are logged and suppressed so they don't
|
|
228
|
+
# mask an exception raised by the wrapped operation.
|
|
229
|
+
for gen, handler in reversed(active):
|
|
230
|
+
try:
|
|
231
|
+
await gen.__anext__()
|
|
232
|
+
except StopAsyncIteration:
|
|
233
|
+
continue
|
|
234
|
+
except Exception:
|
|
235
|
+
logger.exception(
|
|
236
|
+
"AroundSignal handler %r for sender %r raised during post-yield cleanup.",
|
|
237
|
+
handler,
|
|
238
|
+
sender,
|
|
239
|
+
)
|
|
240
|
+
else:
|
|
241
|
+
logger.warning(
|
|
242
|
+
"AroundSignal handler %r for sender %r yielded more than once; handlers must yield exactly once.",
|
|
243
|
+
handler,
|
|
244
|
+
sender,
|
|
245
|
+
)
|
|
246
|
+
with contextlib.suppress(Exception):
|
|
247
|
+
await gen.aclose()
|
|
248
|
+
|
|
249
|
+
def has_handlers(self, model_class: type) -> bool:
|
|
250
|
+
"""Check if any handlers are registered for the given model class."""
|
|
251
|
+
return bool(self._handlers.get(model_class))
|
|
252
|
+
|
|
253
|
+
def clear(self, model_class: type | None = None) -> None:
|
|
254
|
+
"""
|
|
255
|
+
Clear registered handlers.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
model_class: If provided, clear handlers only for this model.
|
|
259
|
+
If None, clear all handlers for all models.
|
|
260
|
+
"""
|
|
261
|
+
if model_class is not None:
|
|
262
|
+
self._handlers.pop(model_class, None)
|
|
263
|
+
else:
|
|
264
|
+
self._handlers.clear()
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# =============================================================================
|
|
268
|
+
# Pre-defined signal instances
|
|
269
|
+
# =============================================================================
|
|
270
|
+
|
|
271
|
+
# Pre/Post signals for save operations
|
|
272
|
+
pre_save = Signal("pre_save")
|
|
273
|
+
post_save = Signal("post_save")
|
|
274
|
+
|
|
275
|
+
# Pre/Post signals for update operations
|
|
276
|
+
pre_update = Signal("pre_update")
|
|
277
|
+
post_update = Signal("post_update")
|
|
278
|
+
|
|
279
|
+
# Pre/Post signals for delete operations
|
|
280
|
+
pre_delete = Signal("pre_delete")
|
|
281
|
+
post_delete = Signal("post_delete")
|
|
282
|
+
|
|
283
|
+
# Around signals
|
|
284
|
+
around_save = AroundSignal("around_save")
|
|
285
|
+
around_update = AroundSignal("around_update")
|
|
286
|
+
around_delete = AroundSignal("around_delete")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{surreal_orm_lite-0.3.0 → surreal_orm_lite-0.4.0}/src/surreal_orm_lite/connection_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|