sqlobjects 0.1.0__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.
- sqlobjects/__init__.py +38 -0
- sqlobjects/config.py +519 -0
- sqlobjects/database.py +586 -0
- sqlobjects/exceptions.py +538 -0
- sqlobjects/expressions.py +1054 -0
- sqlobjects/fields.py +1866 -0
- sqlobjects/history.py +101 -0
- sqlobjects/metadata.py +1130 -0
- sqlobjects/model.py +1009 -0
- sqlobjects/objects.py +812 -0
- sqlobjects/queries.py +1059 -0
- sqlobjects/relations.py +843 -0
- sqlobjects/session.py +389 -0
- sqlobjects/signals.py +464 -0
- sqlobjects/utils/__init__.py +5 -0
- sqlobjects/utils/naming.py +53 -0
- sqlobjects/utils/pattern.py +644 -0
- sqlobjects/validators.py +294 -0
- sqlobjects-0.1.0.dist-info/METADATA +29 -0
- sqlobjects-0.1.0.dist-info/RECORD +23 -0
- sqlobjects-0.1.0.dist-info/WHEEL +5 -0
- sqlobjects-0.1.0.dist-info/licenses/LICENSE +21 -0
- sqlobjects-0.1.0.dist-info/top_level.txt +1 -0
sqlobjects/signals.py
ADDED
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import inspect
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any, TypeVar
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import event
|
|
9
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"Operation",
|
|
14
|
+
"SignalContext",
|
|
15
|
+
"SignalMixin",
|
|
16
|
+
"emit_signals",
|
|
17
|
+
"event",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Operation(Enum):
|
|
24
|
+
"""Enumeration of database operation types for signal handling.
|
|
25
|
+
|
|
26
|
+
This enum defines the types of database operations that can trigger
|
|
27
|
+
signals in the SQLObjects system, allowing models to respond to
|
|
28
|
+
lifecycle events.
|
|
29
|
+
|
|
30
|
+
Values:
|
|
31
|
+
CREATE: Create operations for new model instances
|
|
32
|
+
UPDATE: Update operations on existing model instances
|
|
33
|
+
DELETE: Delete operations on individual instances or bulk deletions
|
|
34
|
+
SAVE: Generic save operations (create or update, for backward compatibility)
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
>>> # Used in signal context for create operation
|
|
38
|
+
>>> context = SignalContext(
|
|
39
|
+
... operation=Operation.CREATE, session=session, model_class=User, instance=user_instance
|
|
40
|
+
... )
|
|
41
|
+
>>> # Used in signal context for update operation
|
|
42
|
+
>>> context = SignalContext(
|
|
43
|
+
... operation=Operation.UPDATE, session=session, model_class=User, instance=user_instance
|
|
44
|
+
... )
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
CREATE = "create"
|
|
48
|
+
UPDATE = "update"
|
|
49
|
+
DELETE = "delete"
|
|
50
|
+
SAVE = "save"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class SignalContext:
|
|
55
|
+
"""Context object containing information about a database operation for signal handlers.
|
|
56
|
+
|
|
57
|
+
This class provides all the necessary information about a database operation
|
|
58
|
+
to signal handlers, including the operation type, affected data, and session
|
|
59
|
+
information.
|
|
60
|
+
|
|
61
|
+
Attributes:
|
|
62
|
+
operation: Type of database operation being performed
|
|
63
|
+
session: Database session used for the operation
|
|
64
|
+
model_class: Model class involved in the operation
|
|
65
|
+
instance: Specific model instance (for single-instance operations)
|
|
66
|
+
affected_count: Number of rows affected (for bulk operations)
|
|
67
|
+
update_data: Data being updated (for update operations)
|
|
68
|
+
|
|
69
|
+
Examples:
|
|
70
|
+
>>> # Single instance save operation
|
|
71
|
+
>>> context = SignalContext(operation=Operation.SAVE, session=session, model_class=User, instance=user)
|
|
72
|
+
>>> # Bulk update operation
|
|
73
|
+
>>> context = SignalContext(
|
|
74
|
+
... operation=Operation.UPDATE,
|
|
75
|
+
... session=session,
|
|
76
|
+
... model_class=User,
|
|
77
|
+
... affected_count=10,
|
|
78
|
+
... update_data={"status": "active"},
|
|
79
|
+
... )
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
operation: Operation
|
|
83
|
+
session: AsyncSession
|
|
84
|
+
model_class: Any # Target model class (for both single and batch operations)
|
|
85
|
+
instance: Any | None = None # Instance object for single instance operations
|
|
86
|
+
affected_count: int | None = None # Number of rows affected by batch operations
|
|
87
|
+
update_data: dict[str, Any] | None = None # Data for update operations
|
|
88
|
+
actual_operation: Operation | None = None # Actual operation for SAVE (CREATE or UPDATE)
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def is_bulk(self) -> bool:
|
|
92
|
+
"""Check if this is a bulk operation affecting multiple records.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
True if this is a bulk operation, False for single-instance operations
|
|
96
|
+
|
|
97
|
+
Examples:
|
|
98
|
+
>>> # Bulk operation context
|
|
99
|
+
>>> context = SignalContext(operation=Operation.UPDATE, session=session, model_class=User)
|
|
100
|
+
>>> context.is_bulk # True
|
|
101
|
+
>>> # Single instance context
|
|
102
|
+
>>> context = SignalContext(operation=Operation.SAVE, session=session, model_class=User, instance=user)
|
|
103
|
+
>>> context.is_bulk # False
|
|
104
|
+
"""
|
|
105
|
+
return self.instance is None
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def is_single(self) -> bool:
|
|
109
|
+
"""Check if this is a single-instance operation.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
True if this is a single-instance operation, False for bulk operations
|
|
113
|
+
|
|
114
|
+
Examples:
|
|
115
|
+
>>> # Single instance context
|
|
116
|
+
>>> context = SignalContext(operation=Operation.SAVE, session=session, model_class=User, instance=user)
|
|
117
|
+
>>> context.is_single # True
|
|
118
|
+
>>> # Bulk operation context
|
|
119
|
+
>>> context = SignalContext(operation=Operation.UPDATE, session=session, model_class=User)
|
|
120
|
+
>>> context.is_single # False
|
|
121
|
+
"""
|
|
122
|
+
return self.instance is not None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class SignalMixin:
|
|
126
|
+
"""Mixin class that provides signal handling capabilities to model classes.
|
|
127
|
+
|
|
128
|
+
This mixin enables models to define signal handlers that are automatically
|
|
129
|
+
called before and after database operations. It supports both synchronous
|
|
130
|
+
and asynchronous signal handlers.
|
|
131
|
+
|
|
132
|
+
Signal Handler Methods:
|
|
133
|
+
Instance-level signals (single record operations):
|
|
134
|
+
- before_create(context): Called before create operations
|
|
135
|
+
- after_create(context): Called after create operations
|
|
136
|
+
- before_update(context): Called before update operations
|
|
137
|
+
- after_update(context): Called after update operations
|
|
138
|
+
- before_delete(context): Called before delete operations
|
|
139
|
+
- after_delete(context): Called after delete operations
|
|
140
|
+
- before_save(context): Called before save operations (backward compatibility)
|
|
141
|
+
- after_save(context): Called after save operations (backward compatibility)
|
|
142
|
+
|
|
143
|
+
Class-level signals (bulk operations):
|
|
144
|
+
- before_bulk_create(context): Called before bulk create operations
|
|
145
|
+
- after_bulk_create(context): Called after bulk create operations
|
|
146
|
+
- before_bulk_update(context): Called before bulk update operations
|
|
147
|
+
- after_bulk_update(context): Called after bulk update operations
|
|
148
|
+
- before_bulk_delete(context): Called before bulk delete operations
|
|
149
|
+
- after_bulk_delete(context): Called after bulk delete operations
|
|
150
|
+
- before_bulk_save(context): Called before bulk save operations (backward compatibility)
|
|
151
|
+
- after_bulk_save(context): Called after bulk save operations (backward compatibility)
|
|
152
|
+
|
|
153
|
+
Examples:
|
|
154
|
+
>>> class User(ObjectModel, SignalMixin):
|
|
155
|
+
... name: Column[str] = column(type="string")
|
|
156
|
+
...
|
|
157
|
+
... async def before_save(self, context: SignalContext) -> None:
|
|
158
|
+
... # Called before saving the user
|
|
159
|
+
... print(f"About to save user: {self.name}")
|
|
160
|
+
...
|
|
161
|
+
... async def after_save(self, context: SignalContext) -> None:
|
|
162
|
+
... # Called after saving the user
|
|
163
|
+
... print(f"User saved: {self.name}")
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
async def _emit_signal(self, timing: str, context: SignalContext) -> None:
|
|
167
|
+
"""Emit an instance-level signal for the specified timing and operation.
|
|
168
|
+
|
|
169
|
+
This method looks for signal handler methods on the instance and calls
|
|
170
|
+
them if they exist. It supports both sync and async handlers.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
timing: Signal timing ("before" or "after")
|
|
174
|
+
context: Signal context containing operation details
|
|
175
|
+
|
|
176
|
+
Examples:
|
|
177
|
+
>>> # This is called internally by the ORM
|
|
178
|
+
>>> await instance._emit_signal("before", context)
|
|
179
|
+
"""
|
|
180
|
+
await _emit_signal(self, timing, context)
|
|
181
|
+
|
|
182
|
+
@classmethod
|
|
183
|
+
async def _emit_bulk_signal(cls, timing: str, context: SignalContext) -> None:
|
|
184
|
+
"""Emit a bulk signal for the specified timing and operation.
|
|
185
|
+
|
|
186
|
+
This method looks for bulk signal handler methods and calls
|
|
187
|
+
them if they exist. Bulk signals are used for operations that
|
|
188
|
+
affect multiple records without specific instances.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
timing: Signal timing ("before" or "after")
|
|
192
|
+
context: Signal context containing operation details
|
|
193
|
+
|
|
194
|
+
Examples:
|
|
195
|
+
>>> class User(ObjectModel, SignalMixin):
|
|
196
|
+
... @classmethod
|
|
197
|
+
... async def before_bulk_update(cls, context: SignalContext) -> None:
|
|
198
|
+
... print(f"About to update {context.affected_count} users")
|
|
199
|
+
>>> # This is called internally by the ORM
|
|
200
|
+
>>> await User._emit_bulk_signal("before", context)
|
|
201
|
+
"""
|
|
202
|
+
await _emit_signal(cls, timing, context)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def emit_signals(operation: Operation, is_bulk: bool = False):
|
|
206
|
+
"""Decorator to automatically emit pre/post signals for database operations.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
operation: The database operation type
|
|
210
|
+
is_bulk: Whether this is a bulk operation (affects signal emission strategy)
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Decorated function that emits signals before and after execution
|
|
214
|
+
|
|
215
|
+
Examples:
|
|
216
|
+
@emit_signals(Operation.SAVE) # Automatically detects CREATE vs UPDATE
|
|
217
|
+
async def save(self, validate: bool = True):
|
|
218
|
+
# Will emit both SAVE and CREATE/UPDATE signals
|
|
219
|
+
pass
|
|
220
|
+
|
|
221
|
+
@emit_signals(Operation.DELETE)
|
|
222
|
+
async def delete(self, **kwargs):
|
|
223
|
+
# Emit DELETE signals
|
|
224
|
+
pass
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
def decorator(func: F) -> F:
|
|
228
|
+
@functools.wraps(func)
|
|
229
|
+
async def wrapper(*args, **kwargs):
|
|
230
|
+
# Extract self/cls and session from arguments
|
|
231
|
+
self_or_cls = args[0]
|
|
232
|
+
session = _extract_session(self_or_cls, kwargs)
|
|
233
|
+
|
|
234
|
+
# Create signal context with original operation
|
|
235
|
+
context = _create_signal_context(
|
|
236
|
+
operation=operation, session=session, self_or_cls=self_or_cls, is_bulk=is_bulk, kwargs=kwargs
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# For SAVE operations, determine actual CREATE/UPDATE type
|
|
240
|
+
if operation == Operation.SAVE:
|
|
241
|
+
actual_operation = _determine_save_operation(self_or_cls)
|
|
242
|
+
context.actual_operation = actual_operation
|
|
243
|
+
else:
|
|
244
|
+
context.actual_operation = operation
|
|
245
|
+
|
|
246
|
+
# Emit pre signal
|
|
247
|
+
await _emit_pre_signal(self_or_cls, context, is_bulk)
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
# Execute original method
|
|
251
|
+
result = await func(*args, **kwargs)
|
|
252
|
+
|
|
253
|
+
# Update context with result if needed
|
|
254
|
+
_update_context_with_result(context, result)
|
|
255
|
+
|
|
256
|
+
# Emit post signal
|
|
257
|
+
await _emit_post_signal(self_or_cls, context, is_bulk)
|
|
258
|
+
|
|
259
|
+
return result
|
|
260
|
+
|
|
261
|
+
except Exception:
|
|
262
|
+
raise
|
|
263
|
+
|
|
264
|
+
return wrapper # type: ignore
|
|
265
|
+
|
|
266
|
+
return decorator
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _determine_save_operation(self_or_cls) -> Operation:
|
|
270
|
+
"""Determine whether a SAVE operation is CREATE or UPDATE.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
self_or_cls: Model instance or class to check
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Operation.CREATE if instance has no primary key values, Operation.UPDATE otherwise
|
|
277
|
+
"""
|
|
278
|
+
if hasattr(self_or_cls, "__table__"): # Instance method
|
|
279
|
+
# Check if instance has primary key set (indicates UPDATE)
|
|
280
|
+
primary_keys = [col.name for col in self_or_cls.__table__.primary_key.columns]
|
|
281
|
+
if any(getattr(self_or_cls, pk, None) is not None for pk in primary_keys):
|
|
282
|
+
return Operation.UPDATE
|
|
283
|
+
else:
|
|
284
|
+
return Operation.CREATE
|
|
285
|
+
else:
|
|
286
|
+
# For class methods, cannot determine, return CREATE as default
|
|
287
|
+
return Operation.CREATE
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _extract_session(self_or_cls, kwargs) -> Any:
|
|
291
|
+
"""Extract session from method arguments or instance.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
self_or_cls: Model instance or class
|
|
295
|
+
kwargs: Method keyword arguments
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Database session for the operation
|
|
299
|
+
"""
|
|
300
|
+
# Try to get session from kwargs first
|
|
301
|
+
if "session" in kwargs:
|
|
302
|
+
return kwargs["session"]
|
|
303
|
+
|
|
304
|
+
# Try to get session from instance/class - support both property and method
|
|
305
|
+
if hasattr(self_or_cls, "_session"):
|
|
306
|
+
session_attr = self_or_cls._session # noqa
|
|
307
|
+
# Handle both property and method cases
|
|
308
|
+
return session_attr() if callable(session_attr) else session_attr
|
|
309
|
+
elif hasattr(self_or_cls, "_get_session"):
|
|
310
|
+
return self_or_cls._get_session() # noqa
|
|
311
|
+
|
|
312
|
+
# Fallback to default session
|
|
313
|
+
from .session import SessionContextManager
|
|
314
|
+
|
|
315
|
+
return SessionContextManager.get_session()
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _create_signal_context(operation: Operation, session, self_or_cls, is_bulk: bool, kwargs: dict) -> SignalContext:
|
|
319
|
+
"""Create appropriate signal context based on operation type.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
operation: Database operation type
|
|
323
|
+
session: Database session
|
|
324
|
+
self_or_cls: Model instance or class
|
|
325
|
+
is_bulk: Whether this is a bulk operation
|
|
326
|
+
kwargs: Method keyword arguments
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
SignalContext configured for the operation type
|
|
330
|
+
"""
|
|
331
|
+
if is_bulk:
|
|
332
|
+
# Bulk operation context - support ObjectsManager and QuerySet
|
|
333
|
+
if hasattr(self_or_cls, "_model"):
|
|
334
|
+
model_class = self_or_cls._model # noqa
|
|
335
|
+
elif hasattr(self_or_cls, "__table__"):
|
|
336
|
+
model_class = self_or_cls.__class__
|
|
337
|
+
else:
|
|
338
|
+
model_class = self_or_cls
|
|
339
|
+
|
|
340
|
+
return SignalContext(
|
|
341
|
+
operation=operation,
|
|
342
|
+
session=session,
|
|
343
|
+
model_class=model_class,
|
|
344
|
+
instance=None,
|
|
345
|
+
affected_count=kwargs.get("affected_count") or _extract_affected_count(kwargs),
|
|
346
|
+
update_data=kwargs.get("values") or kwargs.get("update_data"),
|
|
347
|
+
)
|
|
348
|
+
else:
|
|
349
|
+
# Single instance operation context
|
|
350
|
+
instance = self_or_cls if hasattr(self_or_cls, "__table__") else None
|
|
351
|
+
model_class = self_or_cls.__class__ if instance else self_or_cls
|
|
352
|
+
return SignalContext(operation=operation, session=session, model_class=model_class, instance=instance)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
async def _emit_pre_signal(self_or_cls, context: SignalContext, is_bulk: bool):
|
|
356
|
+
"""Emit pre-operation signal.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
self_or_cls: Model instance or class
|
|
360
|
+
context: Signal context
|
|
361
|
+
is_bulk: Whether this is a bulk operation
|
|
362
|
+
"""
|
|
363
|
+
if is_bulk:
|
|
364
|
+
# Bulk signal for bulk operations
|
|
365
|
+
model_class = context.model_class
|
|
366
|
+
if hasattr(model_class, "_emit_bulk_signal"):
|
|
367
|
+
await model_class._emit_bulk_signal("before", context) # noqa
|
|
368
|
+
else:
|
|
369
|
+
# Instance-level signal
|
|
370
|
+
if hasattr(self_or_cls, "_emit_signal"):
|
|
371
|
+
await self_or_cls._emit_signal("before", context) # noqa
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
async def _emit_post_signal(self_or_cls, context: SignalContext, is_bulk: bool):
|
|
375
|
+
"""Emit post-operation signal.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
self_or_cls: Model instance or class
|
|
379
|
+
context: Signal context
|
|
380
|
+
is_bulk: Whether this is a bulk operation
|
|
381
|
+
"""
|
|
382
|
+
if is_bulk:
|
|
383
|
+
# Bulk signal for bulk operations
|
|
384
|
+
model_class = context.model_class
|
|
385
|
+
if hasattr(model_class, "_emit_bulk_signal"):
|
|
386
|
+
await model_class._emit_bulk_signal("after", context) # noqa
|
|
387
|
+
else:
|
|
388
|
+
# Instance-level signal
|
|
389
|
+
if hasattr(self_or_cls, "_emit_signal"):
|
|
390
|
+
await self_or_cls._emit_signal("after", context) # noqa
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _extract_affected_count(kwargs: dict) -> int | None:
|
|
394
|
+
"""Extract affected count from method arguments.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
kwargs: Method keyword arguments
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
Number of records affected by the operation, or None if not determinable
|
|
401
|
+
"""
|
|
402
|
+
# For bulk operations, try to extract count from various argument patterns
|
|
403
|
+
if "mappings" in kwargs and isinstance(kwargs["mappings"], list):
|
|
404
|
+
return len(kwargs["mappings"])
|
|
405
|
+
elif "ids" in kwargs and isinstance(kwargs["ids"], list):
|
|
406
|
+
return len(kwargs["ids"])
|
|
407
|
+
elif "objects" in kwargs and isinstance(kwargs["objects"], list):
|
|
408
|
+
return len(kwargs["objects"])
|
|
409
|
+
return None
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
async def _emit_signal(target, timing: str, context: SignalContext) -> None:
|
|
413
|
+
"""Common logic for emitting signal handlers.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
target: Model instance or class containing signal handlers
|
|
417
|
+
timing: Signal timing ("before" or "after")
|
|
418
|
+
context: Signal context containing operation details
|
|
419
|
+
"""
|
|
420
|
+
# Determine if this is a bulk operation
|
|
421
|
+
is_bulk = context.is_bulk
|
|
422
|
+
bulk_prefix = "bulk_" if is_bulk else ""
|
|
423
|
+
|
|
424
|
+
if context.operation == Operation.SAVE and context.actual_operation:
|
|
425
|
+
# For SAVE operations, emit both SAVE and actual operation signals
|
|
426
|
+
# Emit SAVE signal first
|
|
427
|
+
save_signal_name = f"{timing}_{bulk_prefix}save"
|
|
428
|
+
save_handler = getattr(target, save_signal_name, None)
|
|
429
|
+
if save_handler and callable(save_handler):
|
|
430
|
+
if inspect.iscoroutinefunction(save_handler):
|
|
431
|
+
await save_handler(context)
|
|
432
|
+
else:
|
|
433
|
+
save_handler(context)
|
|
434
|
+
|
|
435
|
+
# Then emit specific CREATE/UPDATE signal
|
|
436
|
+
specific_signal_name = f"{timing}_{bulk_prefix}{context.actual_operation.value}"
|
|
437
|
+
specific_handler = getattr(target, specific_signal_name, None)
|
|
438
|
+
if specific_handler and callable(specific_handler):
|
|
439
|
+
if inspect.iscoroutinefunction(specific_handler):
|
|
440
|
+
await specific_handler(context)
|
|
441
|
+
else:
|
|
442
|
+
specific_handler(context)
|
|
443
|
+
else:
|
|
444
|
+
# For non-SAVE operations, emit the specific signal
|
|
445
|
+
signal_name = f"{timing}_{bulk_prefix}{context.operation.value}"
|
|
446
|
+
handler = getattr(target, signal_name, None)
|
|
447
|
+
|
|
448
|
+
if handler and callable(handler):
|
|
449
|
+
if inspect.iscoroutinefunction(handler):
|
|
450
|
+
await handler(context)
|
|
451
|
+
else:
|
|
452
|
+
handler(context)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _update_context_with_result(context: SignalContext, result):
|
|
456
|
+
"""Update signal context with method execution result.
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
context: Signal context to update
|
|
460
|
+
result: Result from the executed method
|
|
461
|
+
"""
|
|
462
|
+
if context.is_bulk and isinstance(result, int):
|
|
463
|
+
# Update affected_count for bulk operations that return row count
|
|
464
|
+
context.affected_count = result
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
__all__ = ["to_snake_case", "to_camel_case"]
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def to_snake_case(name: str) -> str:
|
|
8
|
+
"""Convert CamelCase to snake_case (Rails style).
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
name: CamelCase string to convert
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
snake_case string
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
>>> to_snake_case("UserProfile")
|
|
18
|
+
'user_profile'
|
|
19
|
+
>>> to_snake_case("XMLParser")
|
|
20
|
+
'xml_parser'
|
|
21
|
+
>>> to_snake_case("HTTPRequest")
|
|
22
|
+
'http_request'
|
|
23
|
+
"""
|
|
24
|
+
# Handle sequences of uppercase letters followed by lowercase
|
|
25
|
+
s1 = re.sub("([A-Z]+)([A-Z][a-z])", r"\1_\2", name)
|
|
26
|
+
# Handle lowercase followed by uppercase
|
|
27
|
+
s2 = re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1)
|
|
28
|
+
return s2.lower()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def to_camel_case(name: str, pascal_case: bool = True) -> str:
|
|
32
|
+
"""Convert snake_case to CamelCase.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
name: snake_case string to convert
|
|
36
|
+
pascal_case: If True, return PascalCase; if False, return camelCase
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
CamelCase string
|
|
40
|
+
|
|
41
|
+
Examples:
|
|
42
|
+
>>> to_camel_case("user_profile")
|
|
43
|
+
'UserProfile'
|
|
44
|
+
>>> to_camel_case("user_profile", pascal_case=False)
|
|
45
|
+
'userProfile'
|
|
46
|
+
>>> to_camel_case("xml_parser")
|
|
47
|
+
'XmlParser'
|
|
48
|
+
"""
|
|
49
|
+
components = name.split("_")
|
|
50
|
+
if pascal_case:
|
|
51
|
+
return "".join(word.capitalize() for word in components)
|
|
52
|
+
else:
|
|
53
|
+
return components[0] + "".join(word.capitalize() for word in components[1:])
|