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.
@@ -0,0 +1,538 @@
1
+ """SQLObjects Exception System
2
+
3
+ This module provides a comprehensive exception hierarchy for SQLObjects.
4
+ """
5
+
6
+ from typing import Any
7
+
8
+ from sqlalchemy.exc import DataError as SQLADataError
9
+ from sqlalchemy.exc import DisconnectionError as SQLADisconnectionError
10
+ from sqlalchemy.exc import IntegrityError as SQLAIntegrityError
11
+ from sqlalchemy.exc import OperationalError as SQLAOperationalError
12
+ from sqlalchemy.exc import ProgrammingError as SQLAProgrammingError
13
+ from sqlalchemy.exc import SQLAlchemyError
14
+
15
+
16
+ __all__ = [
17
+ "SQLObjectsError",
18
+ "DoesNotExist",
19
+ "MultipleObjectsReturned",
20
+ "ValidationError",
21
+ "ValidationErrorCollector",
22
+ "DatabaseError",
23
+ "IntegrityError",
24
+ "TransactionError",
25
+ "ConfigurationError",
26
+ "DeferredFieldError",
27
+ "PrimaryKeyError",
28
+ "SQLError",
29
+ "OperationalError",
30
+ "DataError",
31
+ "ProgrammingError",
32
+ "create_validation_error",
33
+ "convert_sqlalchemy_error",
34
+ ]
35
+
36
+
37
+ class SQLObjectsError(Exception):
38
+ """Base exception class for all SQLObjects-related errors.
39
+
40
+ This is the root exception class that all other SQLObjects exceptions
41
+ inherit from. It can be used to catch any SQLObjects-specific error.
42
+
43
+ Examples:
44
+ >>> try:
45
+ ... # SQLObjects operations
46
+ ... pass
47
+ ... except SQLObjectsError as e:
48
+ ... # Handle any SQLObjects error
49
+ ... print(f"SQLObjects error: {e}")
50
+ """
51
+
52
+ pass
53
+
54
+
55
+ class DoesNotExist(SQLObjectsError):
56
+ """Raised when a database query returns no results when one was expected.
57
+
58
+ This exception is typically raised by get() methods when no object
59
+ matches the specified criteria.
60
+
61
+ Examples:
62
+ >>> try:
63
+ ... user = await User.objects.get(id=999)
64
+ ... except DoesNotExist:
65
+ ... print("User not found")
66
+ """
67
+
68
+ pass
69
+
70
+
71
+ class MultipleObjectsReturned(SQLObjectsError):
72
+ """Raised when a query returns multiple objects when only one was expected.
73
+
74
+ This exception is typically raised by get() methods when multiple objects
75
+ match the specified criteria.
76
+
77
+ Examples:
78
+ >>> try:
79
+ ... user = await User.objects.get(name="John")
80
+ ... except MultipleObjectsReturned:
81
+ ... print("Multiple users found with name 'John'")
82
+ """
83
+
84
+ pass
85
+
86
+
87
+ class ValidationError(SQLObjectsError):
88
+ """Raised when data validation fails during model operations.
89
+
90
+ This exception supports both single field validation errors and multiple
91
+ field validation errors. It provides detailed information about what
92
+ validation failed and why.
93
+
94
+ Attributes:
95
+ field: Name of the field that failed validation (for single errors)
96
+ message: Human-readable error message
97
+ code: Error code for programmatic handling
98
+ params: Parameters used in the error message
99
+ field_errors: Dictionary of field names to error lists (for multiple errors)
100
+ model_class: Name of the model class where validation failed
101
+ operation: Operation that triggered the validation (e.g., 'create', 'update')
102
+ """
103
+
104
+ def __init__(
105
+ self,
106
+ message: str,
107
+ field: str | None = None,
108
+ code: str | None = None,
109
+ params: dict[str, Any] | None = None,
110
+ field_errors: dict[str, list[str]] | None = None,
111
+ ) -> None:
112
+ """Initialize a validation error.
113
+
114
+ Args:
115
+ message: Human-readable error message
116
+ field: Name of the field that failed validation (for single errors)
117
+ code: Error code for programmatic handling
118
+ params: Parameters used in the error message
119
+ field_errors: Dictionary of field names to error lists (for multiple errors)
120
+
121
+ Examples:
122
+ >>> # Single field error
123
+ >>> raise ValidationError("Email is required", field="email", code="required")
124
+ >>> # Multiple field errors
125
+ >>> errors = {"email": ["Email is required"], "age": ["Age must be positive"]}
126
+ >>> raise ValidationError("Validation failed", field_errors=errors)
127
+ """
128
+ super().__init__(message)
129
+ self.field = field
130
+ self.message = message
131
+ self.code = code or "invalid"
132
+ self.params = params or {}
133
+ self.field_errors = field_errors or {}
134
+ self.model_class: str | None = None
135
+ self.operation: str | None = None
136
+
137
+ @property
138
+ def is_multiple(self) -> bool:
139
+ """Check if this validation error contains multiple field errors.
140
+
141
+ Returns:
142
+ True if this error contains multiple field errors, False otherwise
143
+
144
+ Examples:
145
+ >>> error = ValidationError("Email is required", field="email")
146
+ >>> error.is_multiple # False
147
+ >>> errors = {"email": ["Required"], "age": ["Invalid"]}
148
+ >>> error = ValidationError("Multiple errors", field_errors=errors)
149
+ >>> error.is_multiple # True
150
+ """
151
+ return bool(self.field_errors)
152
+
153
+ def add_field_error(self, field: str, message: str) -> None:
154
+ """Add a validation error for a specific field.
155
+
156
+ Args:
157
+ field: Name of the field
158
+ message: Error message for the field
159
+
160
+ Examples:
161
+ >>> error = ValidationError("Validation failed")
162
+ >>> error.add_field_error("email", "Email is required")
163
+ >>> error.add_field_error("email", "Email format is invalid")
164
+ """
165
+ if field not in self.field_errors:
166
+ self.field_errors[field] = []
167
+ self.field_errors[field].append(message)
168
+
169
+ def get_field_errors(self, field: str) -> list[str]:
170
+ """Get all validation errors for a specific field.
171
+
172
+ Args:
173
+ field: Name of the field
174
+
175
+ Returns:
176
+ List of error messages for the specified field
177
+
178
+ Examples:
179
+ >>> errors = {"email": ["Required", "Invalid format"]}
180
+ >>> error = ValidationError("Multiple errors", field_errors=errors)
181
+ >>> error.get_field_errors("email") # ["Required", "Invalid format"]
182
+ >>> error.get_field_errors("name") # []
183
+ """
184
+ return self.field_errors.get(field, [])
185
+
186
+ def to_dict(self) -> dict[str, Any]:
187
+ """Convert the validation error to a dictionary format suitable for APIs.
188
+
189
+ Returns:
190
+ Dictionary representation of the validation error
191
+
192
+ Examples:
193
+ >>> # Single field error
194
+ >>> error = ValidationError("Email is required", field="email", code="required")
195
+ >>> error.to_dict()
196
+ {'message': 'Email is required', 'code': 'required', 'field': 'email'}
197
+
198
+ >>> # Multiple field errors
199
+ >>> errors = {"email": ["Required"], "age": ["Invalid"]}
200
+ >>> error = ValidationError("Validation failed", field_errors=errors)
201
+ >>> error.to_dict()
202
+ {'message': 'Validation failed', 'field_errors': {...}, 'error_count': 2}
203
+ """
204
+ if self.is_multiple:
205
+ # Multiple field errors format
206
+ return {
207
+ "message": self.message,
208
+ "field_errors": self.field_errors,
209
+ "error_count": sum(len(errors) for errors in self.field_errors.values()),
210
+ }
211
+ else:
212
+ # Single field error format
213
+ result: dict = {"message": self.message, "code": self.code}
214
+ if self.field:
215
+ result["field"] = self.field
216
+ if self.params:
217
+ result["params"] = self.params
218
+ return result
219
+
220
+
221
+ class ValidationErrorCollector:
222
+ """Helper class for collecting multiple validation errors before raising.
223
+
224
+ This class provides a convenient way to collect validation errors from
225
+ multiple fields and then raise a single ValidationError with all the
226
+ collected errors.
227
+
228
+ Examples:
229
+ >>> collector = ValidationErrorCollector()
230
+ >>> if not user.email:
231
+ ... collector.add_error("email", "Email is required")
232
+ >>> if user.age < 0:
233
+ ... collector.add_error("age", "Age must be positive")
234
+ >>> collector.raise_if_errors() # Raises ValidationError if any errors
235
+ """
236
+
237
+ def __init__(self) -> None:
238
+ """Initialize an empty error collector."""
239
+ self._errors: dict[str, list[str]] = {}
240
+
241
+ def add_error(self, field: str, message: str) -> None:
242
+ """Add a validation error for a specific field.
243
+
244
+ Args:
245
+ field: Name of the field that has the error
246
+ message: Error message describing the validation failure
247
+
248
+ Examples:
249
+ >>> collector = ValidationErrorCollector()
250
+ >>> collector.add_error("email", "Email is required")
251
+ >>> collector.add_error("email", "Email format is invalid")
252
+ """
253
+ if field not in self._errors:
254
+ self._errors[field] = []
255
+ self._errors[field].append(message)
256
+
257
+ def has_errors(self) -> bool:
258
+ """Check if any validation errors have been collected.
259
+
260
+ Returns:
261
+ True if there are any errors, False otherwise
262
+
263
+ Examples:
264
+ >>> collector = ValidationErrorCollector()
265
+ >>> collector.has_errors() # False
266
+ >>> collector.add_error("email", "Required")
267
+ >>> collector.has_errors() # True
268
+ """
269
+ return bool(self._errors)
270
+
271
+ def raise_if_errors(self) -> None:
272
+ """Raise a ValidationError if any errors have been collected.
273
+
274
+ Raises:
275
+ ValidationError: If there are any collected errors
276
+
277
+ Examples:
278
+ >>> collector = ValidationErrorCollector()
279
+ >>> collector.add_error("email", "Required")
280
+ >>> collector.raise_if_errors() # Raises ValidationError
281
+ """
282
+ if self.has_errors():
283
+ field_count = len(self._errors)
284
+ total_errors = sum(len(errors) for errors in self._errors.values())
285
+ message = f"Validation failed for {field_count} field(s) with {total_errors} error(s)"
286
+ raise ValidationError(message, field_errors=self._errors)
287
+
288
+ @property
289
+ def errors(self) -> dict[str, list[str]]:
290
+ """Get a copy of all collected errors.
291
+
292
+ Returns:
293
+ Dictionary mapping field names to lists of error messages
294
+
295
+ Examples:
296
+ >>> collector = ValidationErrorCollector()
297
+ >>> collector.add_error("email", "Required")
298
+ >>> collector.errors # {"email": ["Required"]}
299
+ """
300
+ return self._errors.copy()
301
+
302
+
303
+ class DatabaseError(SQLObjectsError):
304
+ """Raised when a database operation fails.
305
+
306
+ This is a general exception for database-related errors that don't
307
+ fall into more specific categories like IntegrityError or TransactionError.
308
+
309
+ Examples:
310
+ >>> try:
311
+ ... await db.execute("INVALID SQL")
312
+ ... except DatabaseError as e:
313
+ ... print(f"Database error: {e}")
314
+ """
315
+
316
+ pass
317
+
318
+
319
+ class IntegrityError(DatabaseError):
320
+ """Raised when a database integrity constraint is violated.
321
+
322
+ This exception is typically raised when operations violate database
323
+ constraints such as unique constraints, foreign key constraints, or
324
+ check constraints.
325
+
326
+ Examples:
327
+ >>> try:
328
+ ... await User.objects.create(email="existing@example.com")
329
+ ... except IntegrityError:
330
+ ... print("Email already exists")
331
+ """
332
+
333
+ def __init__(self, message: str, original_error: Exception | None = None):
334
+ self.original_error = original_error
335
+ super().__init__(message)
336
+
337
+
338
+ class TransactionError(DatabaseError):
339
+ """Raised when a database transaction operation fails.
340
+
341
+ This exception is raised for transaction-specific errors such as
342
+ deadlocks, transaction rollbacks, or commit failures.
343
+
344
+ Examples:
345
+ >>> try:
346
+ ... async with session.begin():
347
+ ... # Database operations that might cause deadlock
348
+ ... pass
349
+ ... except TransactionError as e:
350
+ ... print(f"Transaction failed: {e}")
351
+ """
352
+
353
+ pass
354
+
355
+
356
+ class ConfigurationError(SQLObjectsError):
357
+ """Raised when there is an error in SQLObjects configuration.
358
+
359
+ This exception is raised when invalid configuration is detected,
360
+ such as invalid database URLs, missing required settings, or
361
+ conflicting configuration options.
362
+
363
+ Examples:
364
+ >>> try:
365
+ ... init_db("invalid://database/url")
366
+ ... except ConfigurationError as e:
367
+ ... print(f"Configuration error: {e}")
368
+ """
369
+
370
+ pass
371
+
372
+
373
+ class DeferredFieldError(SQLObjectsError):
374
+ """Raised when accessing a deferred field that hasn't been loaded.
375
+
376
+ This exception is raised when trying to access a field that was
377
+ deferred during the query and hasn't been explicitly loaded.
378
+
379
+ Examples:
380
+ >>> try:
381
+ ... content = article.content # deferred field
382
+ ... except DeferredFieldError:
383
+ ... await article.load_deferred_field("content")
384
+ """
385
+
386
+ def __init__(self, field_name: str):
387
+ self.field_name = field_name
388
+ super().__init__(f"Deferred field '{field_name}' not loaded")
389
+
390
+
391
+ class PrimaryKeyError(SQLObjectsError):
392
+ """Raised when there's an issue with primary key operations.
393
+
394
+ This exception is raised for primary key related errors such as
395
+ missing primary key values for update/delete operations.
396
+
397
+ Examples:
398
+ >>> try:
399
+ ... await user.delete() # user has no primary key
400
+ ... except PrimaryKeyError:
401
+ ... print("Cannot delete without primary key")
402
+ """
403
+
404
+ def __init__(self, message: str, operation: str | None = None):
405
+ self.operation = operation
406
+ super().__init__(message)
407
+
408
+
409
+ class SQLError(SQLObjectsError):
410
+ """Base class for SQLAlchemy operation errors.
411
+
412
+ This exception wraps SQLAlchemy errors to provide a consistent
413
+ exception interface for SQLObjects users.
414
+ """
415
+
416
+ def __init__(self, message: str, original_error: Exception | None = None):
417
+ self.original_error = original_error
418
+ super().__init__(message)
419
+
420
+
421
+ class OperationalError(SQLError):
422
+ """Database operational errors (connection, timeout, etc.).
423
+
424
+ This exception is raised for operational database errors such as
425
+ connection failures, timeouts, or server unavailability.
426
+ """
427
+
428
+ pass
429
+
430
+
431
+ class DataError(SQLError):
432
+ """Data-related errors (invalid values, type conversion).
433
+
434
+ This exception is raised for data-related errors such as invalid
435
+ data types, constraint violations, or conversion failures.
436
+ """
437
+
438
+ pass
439
+
440
+
441
+ class ProgrammingError(SQLError):
442
+ """SQL programming errors (syntax, missing tables).
443
+
444
+ This exception is raised for programming errors such as SQL syntax
445
+ errors, missing tables, or invalid column references.
446
+ """
447
+
448
+ pass
449
+
450
+
451
+ # Private global error message mapping
452
+ _ERROR_MESSAGES = {
453
+ "required": "This field is required",
454
+ "invalid": "Invalid value",
455
+ "min_length": "Ensure this value has at least {min_length} characters",
456
+ "max_length": "Ensure this value has at most {max_length} characters",
457
+ "min_value": "Ensure this value is greater than or equal to {min_value}",
458
+ "max_value": "Ensure this value is less than or equal to {max_value}",
459
+ "invalid_email": "Enter a valid email address",
460
+ "invalid_url": "Enter a valid URL",
461
+ "invalid_choice": "'{value}' is not a valid choice",
462
+ "invalid_date": "Enter a valid date",
463
+ "invalid_time": "Enter a valid time",
464
+ "invalid_decimal": "Enter a valid decimal number",
465
+ "invalid_json": "Enter valid JSON",
466
+ "file_not_found": "File not found: {path}",
467
+ "file_too_large": "File size {size} exceeds maximum allowed size {max_size}",
468
+ "file_too_small": "File size {size} is below minimum required size {min_size}",
469
+ "invalid_file_type": "File type '{file_type}' is not allowed. Allowed types: {allowed_types}",
470
+ "file_access_error": "Cannot access file: {error}",
471
+ "invalid_file": "Invalid file",
472
+ "not_an_image": "File is not a valid image",
473
+ "not_implemented": "This method must be implemented by subclasses",
474
+ }
475
+
476
+
477
+ def create_validation_error(
478
+ code: str,
479
+ field: str | None = None,
480
+ params: dict[str, Any] | None = None,
481
+ ) -> ValidationError:
482
+ """Create a ValidationError with English message.
483
+
484
+ Args:
485
+ code: Error code for message lookup
486
+ field: Field name for the error
487
+ params: Parameters for message formatting
488
+
489
+ Returns:
490
+ ValidationError instance with English message
491
+
492
+ Examples:
493
+ >>> error = create_validation_error("required", field="email")
494
+ >>> error = create_validation_error("min_length", params={"min_length": 3})
495
+ """
496
+ message = _ERROR_MESSAGES.get(code, code)
497
+ if params:
498
+ try:
499
+ message = message.format(**params)
500
+ except (KeyError, ValueError):
501
+ pass
502
+
503
+ return ValidationError(message, field=field, code=code, params=params)
504
+
505
+
506
+ def convert_sqlalchemy_error(error: Exception) -> SQLObjectsError:
507
+ """Convert SQLAlchemy exceptions to SQLObjects exceptions.
508
+
509
+ Args:
510
+ error: SQLAlchemy exception to convert
511
+
512
+ Returns:
513
+ Corresponding SQLObjects exception
514
+
515
+ Examples:
516
+ >>> try:
517
+ ... # SQLAlchemy operation
518
+ ... pass
519
+ ... except SQLAlchemyError as e:
520
+ ... raise convert_sqlalchemy_error(e)
521
+ """
522
+ error_msg = str(error)
523
+
524
+ if isinstance(error, SQLAIntegrityError):
525
+ return IntegrityError(error_msg, original_error=error)
526
+ elif isinstance(error, SQLAOperationalError):
527
+ return OperationalError(error_msg, original_error=error)
528
+ elif isinstance(error, SQLADataError):
529
+ return DataError(error_msg, original_error=error)
530
+ elif isinstance(error, SQLAProgrammingError):
531
+ return ProgrammingError(error_msg, original_error=error)
532
+ elif isinstance(error, SQLADisconnectionError):
533
+ return OperationalError(error_msg, original_error=error)
534
+ elif isinstance(error, SQLAlchemyError):
535
+ return SQLError(error_msg, original_error=error)
536
+ else:
537
+ # Not a SQLAlchemy error, return as-is or wrap in generic SQLError
538
+ return SQLError(error_msg, original_error=error)