velocity-python 0.0.101__py3-none-any.whl → 0.0.103__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 velocity-python might be problematic. Click here for more details.
- velocity/db/__init__.py +4 -1
- velocity/db/core/engine.py +660 -32
- velocity/db/core/exceptions.py +16 -0
- velocity/db/core/transaction.py +2 -2
- velocity/db/exceptions.py +112 -0
- velocity/db/servers/postgres/sql.py +1 -1
- {velocity_python-0.0.101.dist-info → velocity_python-0.0.103.dist-info}/METADATA +1 -1
- {velocity_python-0.0.101.dist-info → velocity_python-0.0.103.dist-info}/RECORD +11 -10
- {velocity_python-0.0.101.dist-info → velocity_python-0.0.103.dist-info}/WHEEL +0 -0
- {velocity_python-0.0.101.dist-info → velocity_python-0.0.103.dist-info}/licenses/LICENSE +0 -0
- {velocity_python-0.0.101.dist-info → velocity_python-0.0.103.dist-info}/top_level.txt +0 -0
velocity/db/__init__.py
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
from velocity.db
|
|
1
|
+
from velocity.db import exceptions
|
|
2
2
|
from velocity.db.servers import postgres
|
|
3
3
|
from velocity.db.servers import mysql
|
|
4
4
|
from velocity.db.servers import sqlite
|
|
5
5
|
from velocity.db.servers import sqlserver
|
|
6
|
+
|
|
7
|
+
# Export exceptions at the package level for backward compatibility
|
|
8
|
+
from velocity.db.exceptions import *
|
velocity/db/core/engine.py
CHANGED
|
@@ -78,8 +78,8 @@ class Engine:
|
|
|
78
78
|
f"Unhandled configuration parameter type: {type(self.config)}"
|
|
79
79
|
)
|
|
80
80
|
|
|
81
|
-
except Exception:
|
|
82
|
-
self.process_error()
|
|
81
|
+
except Exception as e:
|
|
82
|
+
raise self.process_error(e)
|
|
83
83
|
|
|
84
84
|
def transaction(self, func_or_cls=None):
|
|
85
85
|
"""
|
|
@@ -340,43 +340,40 @@ class Engine:
|
|
|
340
340
|
"""
|
|
341
341
|
logger = logging.getLogger(__name__)
|
|
342
342
|
|
|
343
|
-
# Enhanced logging with context
|
|
344
|
-
|
|
345
|
-
'exception_type': type(exception).__name__,
|
|
346
|
-
'sql': sql,
|
|
347
|
-
'parameters': parameters
|
|
348
|
-
}
|
|
343
|
+
# Enhanced logging with context - more readable format
|
|
344
|
+
sql_preview = sql[:100] + "..." if sql and len(sql) > 100 else sql or "None"
|
|
349
345
|
|
|
350
346
|
logger.error(
|
|
351
|
-
f"Database
|
|
352
|
-
f"
|
|
353
|
-
|
|
347
|
+
f"🔴 Database Error Detected\n"
|
|
348
|
+
f" Exception Type: {type(exception).__name__}\n"
|
|
349
|
+
f" SQL Statement: {sql_preview}\n"
|
|
350
|
+
f" Processing error for classification..."
|
|
354
351
|
)
|
|
355
352
|
|
|
356
353
|
# Safely get error code and message with fallbacks
|
|
357
354
|
try:
|
|
358
355
|
error_code = getattr(exception, 'pgcode', None) or self.get_error(exception)
|
|
359
356
|
except Exception as e:
|
|
360
|
-
logger.warning(f"
|
|
357
|
+
logger.warning(f"⚠️ Unable to extract database error code: {e}")
|
|
361
358
|
error_code = None
|
|
362
359
|
|
|
363
360
|
try:
|
|
364
361
|
error_message = str(exception)
|
|
365
362
|
except Exception as e:
|
|
366
|
-
logger.warning(f"
|
|
363
|
+
logger.warning(f"⚠️ Unable to convert exception to string: {e}")
|
|
367
364
|
error_message = f"<Error converting exception: {type(exception).__name__}>"
|
|
368
365
|
|
|
369
366
|
# Primary error classification by error code
|
|
370
367
|
if error_code and hasattr(self, 'error_codes'):
|
|
371
368
|
for error_class, codes in self.error_codes.items():
|
|
372
369
|
if error_code in codes:
|
|
373
|
-
logger.info(f"
|
|
370
|
+
logger.info(f"✅ Successfully classified error: {error_code} → {error_class}")
|
|
374
371
|
try:
|
|
375
372
|
return self._create_exception_with_chaining(
|
|
376
373
|
error_class, error_message, exception, sql, parameters
|
|
377
374
|
)
|
|
378
375
|
except Exception as creation_error:
|
|
379
|
-
logger.error(f"Failed to create {error_class} exception: {creation_error}")
|
|
376
|
+
logger.error(f"❌ Failed to create {error_class} exception: {creation_error}")
|
|
380
377
|
# Fall through to regex classification
|
|
381
378
|
break
|
|
382
379
|
|
|
@@ -447,30 +444,601 @@ class Engine:
|
|
|
447
444
|
for pattern in patterns:
|
|
448
445
|
try:
|
|
449
446
|
if re.search(pattern, error_message_lower):
|
|
450
|
-
logger.info(f"Classified error by pattern: '{pattern}'
|
|
447
|
+
logger.info(f"✅ Classified error by pattern match: '{pattern}' → {error_class}")
|
|
451
448
|
return self._create_exception_with_chaining(
|
|
452
449
|
error_class, error_message, exception, sql, parameters
|
|
453
450
|
)
|
|
454
451
|
except re.error as regex_error:
|
|
455
|
-
logger.warning(f"Regex pattern error '{pattern}': {regex_error}")
|
|
452
|
+
logger.warning(f"⚠️ Regex pattern error for '{pattern}': {regex_error}")
|
|
456
453
|
continue
|
|
457
454
|
except Exception as pattern_error:
|
|
458
|
-
logger.error(f"Error applying pattern '{pattern}': {pattern_error}")
|
|
455
|
+
logger.error(f"❌ Error applying pattern '{pattern}': {pattern_error}")
|
|
459
456
|
continue
|
|
460
457
|
|
|
461
458
|
# Fallback: return generic database error with full context
|
|
459
|
+
available_codes = list(getattr(self, 'error_codes', {}).keys()) if hasattr(self, 'error_codes') else []
|
|
462
460
|
logger.warning(
|
|
463
|
-
f"
|
|
464
|
-
f"
|
|
461
|
+
f"⚠️ Unable to classify database error automatically\n"
|
|
462
|
+
f" → Falling back to generic DatabaseError\n"
|
|
463
|
+
f" → Error Code: {error_code or 'Unknown'}\n"
|
|
464
|
+
f" → Available Classifications: {available_codes or 'None configured'}"
|
|
465
465
|
)
|
|
466
466
|
|
|
467
467
|
return self._create_exception_with_chaining(
|
|
468
468
|
'DatabaseError', error_message, exception, sql, parameters
|
|
469
469
|
)
|
|
470
470
|
|
|
471
|
-
def
|
|
471
|
+
def _format_human_readable_error(self, error_class, message, original_exception, sql=None, parameters=None, format_type='console'):
|
|
472
|
+
"""
|
|
473
|
+
Format a human-readable error message with proper context and formatting.
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
error_class: The name of the exception class
|
|
477
|
+
message: The raw error message
|
|
478
|
+
original_exception: The original exception
|
|
479
|
+
sql: The SQL statement (optional)
|
|
480
|
+
parameters: The SQL parameters (optional)
|
|
481
|
+
format_type: 'console' for terminal output, 'email' for HTML email format
|
|
482
|
+
|
|
483
|
+
Returns:
|
|
484
|
+
A nicely formatted, human-readable error message
|
|
485
|
+
"""
|
|
486
|
+
if format_type == 'email':
|
|
487
|
+
return self._format_email_error(error_class, message, original_exception, sql, parameters)
|
|
488
|
+
else:
|
|
489
|
+
return self._format_console_error(error_class, message, original_exception, sql, parameters)
|
|
490
|
+
|
|
491
|
+
def _format_console_error(self, error_class, message, original_exception, sql=None, parameters=None):
|
|
492
|
+
"""
|
|
493
|
+
Format error message for console/terminal output with Unicode box drawing.
|
|
494
|
+
"""
|
|
495
|
+
# Map error classes to user-friendly descriptions
|
|
496
|
+
error_descriptions = {
|
|
497
|
+
'DbColumnMissingError': 'Column Not Found',
|
|
498
|
+
'DbTableMissingError': 'Table Not Found',
|
|
499
|
+
'DbDatabaseMissingError': 'Database Not Found',
|
|
500
|
+
'DbForeignKeyMissingError': 'Foreign Key Constraint Violation',
|
|
501
|
+
'DbDuplicateKeyError': 'Duplicate Key Violation',
|
|
502
|
+
'DbConnectionError': 'Database Connection Failed',
|
|
503
|
+
'DbDataIntegrityError': 'Data Integrity Violation',
|
|
504
|
+
'DbQueryError': 'Query Execution Error',
|
|
505
|
+
'DbTransactionError': 'Transaction Error',
|
|
506
|
+
'DbTruncationError': 'Data Truncation Error',
|
|
507
|
+
'DatabaseError': 'Database Error'
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
# Get user-friendly error type
|
|
511
|
+
friendly_type = error_descriptions.get(error_class, error_class.replace('Db', '').replace('Error', ' Error'))
|
|
512
|
+
|
|
513
|
+
# Clean up the original message
|
|
514
|
+
clean_message = str(message).strip()
|
|
515
|
+
|
|
516
|
+
# Extract specific details from PostgreSQL errors
|
|
517
|
+
details = self._extract_error_details(original_exception, clean_message)
|
|
518
|
+
|
|
519
|
+
# Build the formatted message
|
|
520
|
+
lines = []
|
|
521
|
+
lines.append(f"╭─ {friendly_type} ─" + "─" * max(0, 60 - len(friendly_type)))
|
|
522
|
+
lines.append("│")
|
|
523
|
+
|
|
524
|
+
# Add the main error description
|
|
525
|
+
if details.get('description'):
|
|
526
|
+
lines.append(f"│ {details['description']}")
|
|
527
|
+
else:
|
|
528
|
+
lines.append(f"│ {clean_message}")
|
|
529
|
+
lines.append("│")
|
|
530
|
+
|
|
531
|
+
# Add error code if available
|
|
532
|
+
error_code = getattr(original_exception, 'pgcode', None)
|
|
533
|
+
if error_code:
|
|
534
|
+
lines.append(f"│ Error Code: {error_code}")
|
|
535
|
+
|
|
536
|
+
# Add specific details if available
|
|
537
|
+
if details.get('column'):
|
|
538
|
+
lines.append(f"│ Column: {details['column']}")
|
|
539
|
+
if details.get('table'):
|
|
540
|
+
lines.append(f"│ Table: {details['table']}")
|
|
541
|
+
if details.get('constraint'):
|
|
542
|
+
lines.append(f"│ Constraint: {details['constraint']}")
|
|
543
|
+
if details.get('hint'):
|
|
544
|
+
lines.append(f"│ Hint: {details['hint']}")
|
|
545
|
+
|
|
546
|
+
# Add SQL context if available
|
|
547
|
+
if sql:
|
|
548
|
+
lines.append("│")
|
|
549
|
+
lines.append("│ SQL Statement:")
|
|
550
|
+
# Show complete SQL without truncation for debugging
|
|
551
|
+
for line in sql.split('\n'):
|
|
552
|
+
lines.append(f"│ {line.strip()}")
|
|
553
|
+
|
|
554
|
+
# Add parameters if available
|
|
555
|
+
if parameters is not None:
|
|
556
|
+
lines.append("│")
|
|
557
|
+
lines.append(f"│ Parameters: {parameters}")
|
|
558
|
+
|
|
559
|
+
# Add debugging section with copy-paste ready format
|
|
560
|
+
if sql or parameters is not None:
|
|
561
|
+
lines.append("│")
|
|
562
|
+
lines.append("│ ┌─ DEBUG COPY-PASTE SECTION ─────────────────────────────")
|
|
563
|
+
|
|
564
|
+
if sql and parameters is not None:
|
|
565
|
+
# Format for direct copy-paste into database console
|
|
566
|
+
lines.append("│ │")
|
|
567
|
+
lines.append("│ │ Complete SQL with Parameters:")
|
|
568
|
+
lines.append("│ │ " + "─" * 45)
|
|
569
|
+
|
|
570
|
+
# Show the raw SQL
|
|
571
|
+
lines.append("│ │ Raw SQL:")
|
|
572
|
+
for line in sql.split('\n'):
|
|
573
|
+
lines.append(f"│ │ {line}")
|
|
574
|
+
|
|
575
|
+
lines.append("│ │")
|
|
576
|
+
lines.append(f"│ │ Raw Parameters: {parameters}")
|
|
577
|
+
|
|
578
|
+
# Try to create an executable version
|
|
579
|
+
lines.append("│ │")
|
|
580
|
+
lines.append("│ │ Executable Format (for PostgreSQL):")
|
|
581
|
+
lines.append("│ │ " + "─" * 35)
|
|
582
|
+
|
|
583
|
+
try:
|
|
584
|
+
# Create a version with parameters substituted for testing
|
|
585
|
+
executable_sql = self._format_executable_sql(sql, parameters)
|
|
586
|
+
for line in executable_sql.split('\n'):
|
|
587
|
+
lines.append(f"│ │ {line}")
|
|
588
|
+
except Exception:
|
|
589
|
+
lines.append("│ │ [Unable to format executable SQL]")
|
|
590
|
+
for line in sql.split('\n'):
|
|
591
|
+
lines.append(f"│ │ {line}")
|
|
592
|
+
lines.append(f"│ │ -- Parameters: {parameters}")
|
|
593
|
+
|
|
594
|
+
elif sql:
|
|
595
|
+
lines.append("│ │")
|
|
596
|
+
lines.append("│ │ SQL Statement (no parameters):")
|
|
597
|
+
lines.append("│ │ " + "─" * 30)
|
|
598
|
+
for line in sql.split('\n'):
|
|
599
|
+
lines.append(f"│ │ {line}")
|
|
600
|
+
|
|
601
|
+
lines.append("│ │")
|
|
602
|
+
lines.append("│ └─────────────────────────────────────────────────────────")
|
|
603
|
+
|
|
604
|
+
# Add detailed call stack information for debugging
|
|
605
|
+
stack_info = self._extract_call_stack_info()
|
|
606
|
+
if stack_info:
|
|
607
|
+
lines.append("│")
|
|
608
|
+
lines.append("│ ┌─ CALL STACK ANALYSIS ──────────────────────────────────")
|
|
609
|
+
lines.append("│ │")
|
|
610
|
+
|
|
611
|
+
if stack_info.get('top_level_call'):
|
|
612
|
+
lines.append("│ │ Top-Level Function (most helpful for debugging):")
|
|
613
|
+
lines.append("│ │ " + "─" * 48)
|
|
614
|
+
call = stack_info['top_level_call']
|
|
615
|
+
lines.append(f"│ │ Function: {call['function']}")
|
|
616
|
+
lines.append(f"│ │ File: {call['file']}")
|
|
617
|
+
lines.append(f"│ │ Line: {call['line']}")
|
|
618
|
+
if call.get('code'):
|
|
619
|
+
lines.append(f"│ │ Code: {call['code'].strip()}")
|
|
620
|
+
|
|
621
|
+
if stack_info.get('relevant_calls'):
|
|
622
|
+
lines.append("│ │")
|
|
623
|
+
lines.append("│ │ Relevant Call Chain (excluding middleware):")
|
|
624
|
+
lines.append("│ │ " + "─" * 44)
|
|
625
|
+
for i, call in enumerate(stack_info['relevant_calls'][:5], 1): # Show top 5
|
|
626
|
+
lines.append(f"│ │ {i}. {call['function']} in {call['file']}:{call['line']}")
|
|
627
|
+
if call.get('code'):
|
|
628
|
+
lines.append(f"│ │ → {call['code'].strip()}")
|
|
629
|
+
|
|
630
|
+
if stack_info.get('lambda_context'):
|
|
631
|
+
lines.append("│ │")
|
|
632
|
+
lines.append("│ │ AWS Lambda Context:")
|
|
633
|
+
lines.append("│ │ " + "─" * 19)
|
|
634
|
+
for key, value in stack_info['lambda_context'].items():
|
|
635
|
+
lines.append(f"│ │ {key}: {value}")
|
|
636
|
+
|
|
637
|
+
lines.append("│ │")
|
|
638
|
+
lines.append("│ └─────────────────────────────────────────────────────────")
|
|
639
|
+
|
|
640
|
+
lines.append("│")
|
|
641
|
+
lines.append("╰" + "─" * 70)
|
|
642
|
+
|
|
643
|
+
return '\n'.join(lines)
|
|
644
|
+
|
|
645
|
+
def _format_email_error(self, error_class, message, original_exception, sql=None, parameters=None):
|
|
646
|
+
"""
|
|
647
|
+
Format error message for email delivery with HTML formatting.
|
|
472
648
|
"""
|
|
473
|
-
|
|
649
|
+
# Map error classes to user-friendly descriptions
|
|
650
|
+
error_descriptions = {
|
|
651
|
+
'DbColumnMissingError': 'Column Not Found',
|
|
652
|
+
'DbTableMissingError': 'Table Not Found',
|
|
653
|
+
'DbDatabaseMissingError': 'Database Not Found',
|
|
654
|
+
'DbForeignKeyMissingError': 'Foreign Key Constraint Violation',
|
|
655
|
+
'DbDuplicateKeyError': 'Duplicate Key Violation',
|
|
656
|
+
'DbConnectionError': 'Database Connection Failed',
|
|
657
|
+
'DbDataIntegrityError': 'Data Integrity Violation',
|
|
658
|
+
'DbQueryError': 'Query Execution Error',
|
|
659
|
+
'DbTransactionError': 'Transaction Error',
|
|
660
|
+
'DbTruncationError': 'Data Truncation Error',
|
|
661
|
+
'DatabaseError': 'Database Error'
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
# Get user-friendly error type
|
|
665
|
+
friendly_type = error_descriptions.get(error_class, error_class.replace('Db', '').replace('Error', ' Error'))
|
|
666
|
+
|
|
667
|
+
# Clean up the original message
|
|
668
|
+
clean_message = str(message).strip()
|
|
669
|
+
|
|
670
|
+
# Extract specific details from PostgreSQL errors
|
|
671
|
+
details = self._extract_error_details(original_exception, clean_message)
|
|
672
|
+
|
|
673
|
+
# Get error code
|
|
674
|
+
error_code = getattr(original_exception, 'pgcode', None)
|
|
675
|
+
|
|
676
|
+
# Get stack info
|
|
677
|
+
stack_info = self._extract_call_stack_info()
|
|
678
|
+
|
|
679
|
+
# Build HTML email format
|
|
680
|
+
html_parts = []
|
|
681
|
+
|
|
682
|
+
# Email header
|
|
683
|
+
html_parts.append("""
|
|
684
|
+
<html>
|
|
685
|
+
<head>
|
|
686
|
+
<style>
|
|
687
|
+
body { font-family: 'Courier New', monospace; margin: 20px; }
|
|
688
|
+
.error-container { border: 2px solid #dc3545; border-radius: 8px; padding: 20px; background-color: #f8f9fa; }
|
|
689
|
+
.error-header { background-color: #dc3545; color: white; padding: 10px; border-radius: 5px; font-weight: bold; font-size: 16px; margin-bottom: 15px; }
|
|
690
|
+
.error-section { margin: 15px 0; padding: 10px; background-color: #ffffff; border-left: 4px solid #007bff; }
|
|
691
|
+
.section-title { font-weight: bold; color: #007bff; margin-bottom: 8px; }
|
|
692
|
+
.code-block { background-color: #f1f3f4; padding: 10px; border-radius: 4px; font-family: 'Courier New', monospace; margin: 5px 0; white-space: pre-wrap; }
|
|
693
|
+
.highlight { background-color: #fff3cd; padding: 2px 4px; border-radius: 3px; }
|
|
694
|
+
.stack-call { margin: 5px 0; padding: 5px; background-color: #e9ecef; border-radius: 3px; }
|
|
695
|
+
.copy-section { background-color: #d1ecf1; border: 1px solid #bee5eb; padding: 15px; border-radius: 5px; margin: 10px 0; }
|
|
696
|
+
</style>
|
|
697
|
+
</head>
|
|
698
|
+
<body>
|
|
699
|
+
<div class="error-container">
|
|
700
|
+
""")
|
|
701
|
+
|
|
702
|
+
# Error header
|
|
703
|
+
html_parts.append(f' <div class="error-header">🚨 {friendly_type}</div>')
|
|
704
|
+
|
|
705
|
+
# Main error description
|
|
706
|
+
description = details.get('description', clean_message)
|
|
707
|
+
html_parts.append(f' <div class="error-section"><strong>{description}</strong></div>')
|
|
708
|
+
|
|
709
|
+
# Error details section
|
|
710
|
+
if error_code or details.get('column') or details.get('table') or details.get('constraint'):
|
|
711
|
+
html_parts.append(' <div class="error-section">')
|
|
712
|
+
html_parts.append(' <div class="section-title">Error Details:</div>')
|
|
713
|
+
if error_code:
|
|
714
|
+
html_parts.append(f' <div><strong>Error Code:</strong> <span class="highlight">{error_code}</span></div>')
|
|
715
|
+
if details.get('column'):
|
|
716
|
+
html_parts.append(f' <div><strong>Column:</strong> <span class="highlight">{details["column"]}</span></div>')
|
|
717
|
+
if details.get('table'):
|
|
718
|
+
html_parts.append(f' <div><strong>Table:</strong> <span class="highlight">{details["table"]}</span></div>')
|
|
719
|
+
if details.get('constraint'):
|
|
720
|
+
html_parts.append(f' <div><strong>Constraint:</strong> <span class="highlight">{details["constraint"]}</span></div>')
|
|
721
|
+
if details.get('hint'):
|
|
722
|
+
html_parts.append(f' <div><strong>Hint:</strong> {details["hint"]}</div>')
|
|
723
|
+
html_parts.append(' </div>')
|
|
724
|
+
|
|
725
|
+
# SQL and Parameters section
|
|
726
|
+
if sql or parameters is not None:
|
|
727
|
+
html_parts.append(' <div class="copy-section">')
|
|
728
|
+
html_parts.append(' <div class="section-title">📋 Debug Information (Copy-Paste Ready)</div>')
|
|
729
|
+
|
|
730
|
+
if sql:
|
|
731
|
+
html_parts.append(' <div><strong>SQL Statement:</strong></div>')
|
|
732
|
+
html_parts.append(f' <div class="code-block">{self._html_escape(sql)}</div>')
|
|
733
|
+
|
|
734
|
+
if parameters is not None:
|
|
735
|
+
html_parts.append(f' <div><strong>Parameters:</strong> <code>{self._html_escape(str(parameters))}</code></div>')
|
|
736
|
+
|
|
737
|
+
# Executable SQL
|
|
738
|
+
if sql and parameters is not None:
|
|
739
|
+
try:
|
|
740
|
+
executable_sql = self._format_executable_sql(sql, parameters)
|
|
741
|
+
html_parts.append(' <div><strong>Executable SQL (for testing):</strong></div>')
|
|
742
|
+
html_parts.append(f' <div class="code-block">{self._html_escape(executable_sql)}</div>')
|
|
743
|
+
except Exception:
|
|
744
|
+
pass
|
|
745
|
+
|
|
746
|
+
html_parts.append(' </div>')
|
|
747
|
+
|
|
748
|
+
# Call stack section
|
|
749
|
+
if stack_info and stack_info.get('top_level_call'):
|
|
750
|
+
html_parts.append(' <div class="error-section">')
|
|
751
|
+
html_parts.append(' <div class="section-title">🔍 Source Code Location</div>')
|
|
752
|
+
|
|
753
|
+
call = stack_info['top_level_call']
|
|
754
|
+
html_parts.append(' <div class="stack-call">')
|
|
755
|
+
html_parts.append(f' <strong>Function:</strong> {call["function"]}<br>')
|
|
756
|
+
html_parts.append(f' <strong>File:</strong> {call["file"]}<br>')
|
|
757
|
+
html_parts.append(f' <strong>Line:</strong> {call["line"]}')
|
|
758
|
+
if call.get('code'):
|
|
759
|
+
html_parts.append(f'<br> <strong>Code:</strong> <code>{self._html_escape(call["code"].strip())}</code>')
|
|
760
|
+
html_parts.append(' </div>')
|
|
761
|
+
|
|
762
|
+
# Show relevant call chain
|
|
763
|
+
if stack_info.get('relevant_calls') and len(stack_info['relevant_calls']) > 1:
|
|
764
|
+
html_parts.append(' <div><strong>Call Chain:</strong></div>')
|
|
765
|
+
for i, call in enumerate(stack_info['relevant_calls'][:4], 1):
|
|
766
|
+
html_parts.append(' <div class="stack-call">')
|
|
767
|
+
html_parts.append(f' {i}. <strong>{call["function"]}</strong> in {call["file"]}:{call["line"]}')
|
|
768
|
+
html_parts.append(' </div>')
|
|
769
|
+
|
|
770
|
+
html_parts.append(' </div>')
|
|
771
|
+
|
|
772
|
+
# Lambda context
|
|
773
|
+
if stack_info and stack_info.get('lambda_context'):
|
|
774
|
+
html_parts.append(' <div class="error-section">')
|
|
775
|
+
html_parts.append(' <div class="section-title">⚡ AWS Lambda Context</div>')
|
|
776
|
+
for key, value in stack_info['lambda_context'].items():
|
|
777
|
+
html_parts.append(f' <div><strong>{key.title()}:</strong> {value}</div>')
|
|
778
|
+
html_parts.append(' </div>')
|
|
779
|
+
|
|
780
|
+
# Email footer
|
|
781
|
+
html_parts.append("""
|
|
782
|
+
</div>
|
|
783
|
+
</body>
|
|
784
|
+
</html>
|
|
785
|
+
""")
|
|
786
|
+
|
|
787
|
+
return ''.join(html_parts)
|
|
788
|
+
|
|
789
|
+
def _html_escape(self, text):
|
|
790
|
+
"""Escape HTML special characters."""
|
|
791
|
+
if not text:
|
|
792
|
+
return ""
|
|
793
|
+
return (str(text)
|
|
794
|
+
.replace('&', '&')
|
|
795
|
+
.replace('<', '<')
|
|
796
|
+
.replace('>', '>')
|
|
797
|
+
.replace('"', '"')
|
|
798
|
+
.replace("'", '''))
|
|
799
|
+
|
|
800
|
+
def _format_executable_sql(self, sql, parameters):
|
|
801
|
+
"""
|
|
802
|
+
Format SQL with parameters substituted for easy copy-paste debugging.
|
|
803
|
+
|
|
804
|
+
Args:
|
|
805
|
+
sql: The SQL statement with placeholders
|
|
806
|
+
parameters: The parameters to substitute
|
|
807
|
+
|
|
808
|
+
Returns:
|
|
809
|
+
SQL statement with parameters properly formatted for execution
|
|
810
|
+
"""
|
|
811
|
+
if not parameters:
|
|
812
|
+
return sql
|
|
813
|
+
|
|
814
|
+
try:
|
|
815
|
+
# Handle different parameter formats
|
|
816
|
+
if isinstance(parameters, (list, tuple)):
|
|
817
|
+
# For positional parameters (%s style)
|
|
818
|
+
formatted_sql = sql
|
|
819
|
+
|
|
820
|
+
# Replace %s placeholders with properly formatted values
|
|
821
|
+
for param in parameters:
|
|
822
|
+
if isinstance(param, str):
|
|
823
|
+
# Escape single quotes and wrap in quotes
|
|
824
|
+
formatted_param = f"'{param.replace(chr(39), chr(39)+chr(39))}'"
|
|
825
|
+
elif isinstance(param, (int, float)):
|
|
826
|
+
formatted_param = str(param)
|
|
827
|
+
elif param is None:
|
|
828
|
+
formatted_param = 'NULL'
|
|
829
|
+
elif isinstance(param, bool):
|
|
830
|
+
formatted_param = 'TRUE' if param else 'FALSE'
|
|
831
|
+
else:
|
|
832
|
+
# For other types, try to convert to string and quote
|
|
833
|
+
formatted_param = f"'{str(param).replace(chr(39), chr(39)+chr(39))}'"
|
|
834
|
+
|
|
835
|
+
# Replace first occurrence of %s
|
|
836
|
+
formatted_sql = formatted_sql.replace('%s', formatted_param, 1)
|
|
837
|
+
|
|
838
|
+
return formatted_sql
|
|
839
|
+
|
|
840
|
+
elif isinstance(parameters, dict):
|
|
841
|
+
# For named parameters (%(name)s style)
|
|
842
|
+
formatted_sql = sql
|
|
843
|
+
for key, value in parameters.items():
|
|
844
|
+
placeholder = f'%({key})s'
|
|
845
|
+
if isinstance(value, str):
|
|
846
|
+
formatted_value = f"'{value.replace(chr(39), chr(39)+chr(39))}'"
|
|
847
|
+
elif isinstance(value, (int, float)):
|
|
848
|
+
formatted_value = str(value)
|
|
849
|
+
elif value is None:
|
|
850
|
+
formatted_value = 'NULL'
|
|
851
|
+
elif isinstance(value, bool):
|
|
852
|
+
formatted_value = 'TRUE' if value else 'FALSE'
|
|
853
|
+
else:
|
|
854
|
+
formatted_value = f"'{str(value).replace(chr(39), chr(39)+chr(39))}'"
|
|
855
|
+
|
|
856
|
+
formatted_sql = formatted_sql.replace(placeholder, formatted_value)
|
|
857
|
+
|
|
858
|
+
return formatted_sql
|
|
859
|
+
|
|
860
|
+
else:
|
|
861
|
+
# Fallback: just append parameters as comment
|
|
862
|
+
return f"{sql}\n-- Parameters: {parameters}"
|
|
863
|
+
|
|
864
|
+
except Exception:
|
|
865
|
+
# If formatting fails, return original with parameters as comment
|
|
866
|
+
return f"{sql}\n-- Parameters: {parameters}"
|
|
867
|
+
|
|
868
|
+
def _extract_call_stack_info(self):
|
|
869
|
+
"""
|
|
870
|
+
Extract relevant call stack information for debugging, filtering out
|
|
871
|
+
middleware and focusing on the most useful frames.
|
|
872
|
+
|
|
873
|
+
Returns:
|
|
874
|
+
Dictionary with call stack analysis
|
|
875
|
+
"""
|
|
876
|
+
import traceback
|
|
877
|
+
import os
|
|
878
|
+
|
|
879
|
+
try:
|
|
880
|
+
# Get the current stack
|
|
881
|
+
stack = traceback.extract_stack()
|
|
882
|
+
|
|
883
|
+
# Common middleware/decorator patterns to filter out
|
|
884
|
+
middleware_patterns = [
|
|
885
|
+
'decorator',
|
|
886
|
+
'wrapper',
|
|
887
|
+
'new_function',
|
|
888
|
+
'exec_function',
|
|
889
|
+
'_execute',
|
|
890
|
+
'process_error',
|
|
891
|
+
'_create_exception',
|
|
892
|
+
'_format_human_readable',
|
|
893
|
+
'__enter__',
|
|
894
|
+
'__exit__',
|
|
895
|
+
'contextlib',
|
|
896
|
+
'functools'
|
|
897
|
+
]
|
|
898
|
+
|
|
899
|
+
# Lambda/AWS specific patterns
|
|
900
|
+
lambda_patterns = [
|
|
901
|
+
'lambda_handler',
|
|
902
|
+
'handler',
|
|
903
|
+
'bootstrap',
|
|
904
|
+
'runtime'
|
|
905
|
+
]
|
|
906
|
+
|
|
907
|
+
relevant_calls = []
|
|
908
|
+
top_level_call = None
|
|
909
|
+
lambda_context = {}
|
|
910
|
+
|
|
911
|
+
# Analyze stack frames from top to bottom (most recent first)
|
|
912
|
+
for frame in reversed(stack[:-4]): # Skip the last few frames (this method, etc.)
|
|
913
|
+
file_path = frame.filename
|
|
914
|
+
function_name = frame.name
|
|
915
|
+
line_number = frame.lineno
|
|
916
|
+
code_line = frame.line or ""
|
|
917
|
+
|
|
918
|
+
# Extract just the filename
|
|
919
|
+
filename = os.path.basename(file_path)
|
|
920
|
+
|
|
921
|
+
# Skip internal Python and library frames
|
|
922
|
+
if any(skip in file_path.lower() for skip in [
|
|
923
|
+
'python3', 'site-packages', '/usr/', '/opt/python',
|
|
924
|
+
'psycopg2', 'boto3', 'botocore'
|
|
925
|
+
]):
|
|
926
|
+
continue
|
|
927
|
+
|
|
928
|
+
# Capture Lambda context if found
|
|
929
|
+
if any(pattern in function_name.lower() for pattern in lambda_patterns):
|
|
930
|
+
lambda_context.update({
|
|
931
|
+
'handler': function_name,
|
|
932
|
+
'file': filename,
|
|
933
|
+
'line': line_number
|
|
934
|
+
})
|
|
935
|
+
|
|
936
|
+
# Skip middleware/decorator frames but keep track of meaningful ones
|
|
937
|
+
is_middleware = any(pattern in function_name.lower() for pattern in middleware_patterns)
|
|
938
|
+
|
|
939
|
+
if not is_middleware:
|
|
940
|
+
call_info = {
|
|
941
|
+
'function': function_name,
|
|
942
|
+
'file': filename,
|
|
943
|
+
'line': line_number,
|
|
944
|
+
'code': code_line,
|
|
945
|
+
'full_path': file_path
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
relevant_calls.append(call_info)
|
|
949
|
+
|
|
950
|
+
# The first non-middleware call is likely the most important
|
|
951
|
+
if not top_level_call:
|
|
952
|
+
# Look for application-level calls (not in velocity internals)
|
|
953
|
+
if not any(internal in file_path for internal in [
|
|
954
|
+
'velocity/db/', 'velocity/aws/', 'velocity/misc/'
|
|
955
|
+
]):
|
|
956
|
+
top_level_call = call_info
|
|
957
|
+
|
|
958
|
+
# If no clear top-level call found, use the first relevant call
|
|
959
|
+
if not top_level_call and relevant_calls:
|
|
960
|
+
top_level_call = relevant_calls[0]
|
|
961
|
+
|
|
962
|
+
# Look for handler functions in the stack
|
|
963
|
+
handler_calls = [call for call in relevant_calls
|
|
964
|
+
if any(pattern in call['function'].lower()
|
|
965
|
+
for pattern in ['handler', 'main', 'process', 'action'])]
|
|
966
|
+
|
|
967
|
+
if handler_calls and not top_level_call:
|
|
968
|
+
top_level_call = handler_calls[0]
|
|
969
|
+
|
|
970
|
+
return {
|
|
971
|
+
'top_level_call': top_level_call,
|
|
972
|
+
'relevant_calls': relevant_calls,
|
|
973
|
+
'lambda_context': lambda_context if lambda_context else None
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
except Exception:
|
|
977
|
+
# If stack analysis fails, return minimal info
|
|
978
|
+
return None
|
|
979
|
+
|
|
980
|
+
def _extract_error_details(self, exception, message):
|
|
981
|
+
"""
|
|
982
|
+
Extract specific details from database errors for better formatting.
|
|
983
|
+
|
|
984
|
+
Args:
|
|
985
|
+
exception: The original database exception
|
|
986
|
+
message: The error message string
|
|
987
|
+
|
|
988
|
+
Returns:
|
|
989
|
+
Dictionary with extracted details
|
|
990
|
+
"""
|
|
991
|
+
import re
|
|
992
|
+
|
|
993
|
+
details = {}
|
|
994
|
+
|
|
995
|
+
# PostgreSQL specific error parsing
|
|
996
|
+
if hasattr(exception, 'pgcode'):
|
|
997
|
+
# Column does not exist
|
|
998
|
+
if 'column' in message.lower() and 'does not exist' in message.lower():
|
|
999
|
+
match = re.search(r'column "([^"]+)" does not exist', message, re.IGNORECASE)
|
|
1000
|
+
if match:
|
|
1001
|
+
details['column'] = match.group(1)
|
|
1002
|
+
details['description'] = f'The column "{match.group(1)}" was not found in the table.'
|
|
1003
|
+
|
|
1004
|
+
# Table does not exist
|
|
1005
|
+
elif 'relation' in message.lower() and 'does not exist' in message.lower():
|
|
1006
|
+
match = re.search(r'relation "([^"]+)" does not exist', message, re.IGNORECASE)
|
|
1007
|
+
if match:
|
|
1008
|
+
details['table'] = match.group(1)
|
|
1009
|
+
details['description'] = f'The table "{match.group(1)}" was not found in the database.'
|
|
1010
|
+
|
|
1011
|
+
# Foreign key violation
|
|
1012
|
+
elif 'foreign key constraint' in message.lower():
|
|
1013
|
+
match = re.search(r'violates foreign key constraint "([^"]+)"', message, re.IGNORECASE)
|
|
1014
|
+
if match:
|
|
1015
|
+
details['constraint'] = match.group(1)
|
|
1016
|
+
details['description'] = 'A foreign key constraint was violated.'
|
|
1017
|
+
details['hint'] = 'Make sure the referenced record exists.'
|
|
1018
|
+
|
|
1019
|
+
# Unique constraint violation
|
|
1020
|
+
elif 'unique constraint' in message.lower() or 'duplicate key' in message.lower():
|
|
1021
|
+
match = re.search(r'violates unique constraint "([^"]+)"', message, re.IGNORECASE)
|
|
1022
|
+
if match:
|
|
1023
|
+
details['constraint'] = match.group(1)
|
|
1024
|
+
details['description'] = 'A unique constraint was violated (duplicate key).'
|
|
1025
|
+
details['hint'] = 'The value you are trying to insert already exists.'
|
|
1026
|
+
|
|
1027
|
+
# Connection errors
|
|
1028
|
+
elif any(term in message.lower() for term in ['connection', 'connect', 'server']):
|
|
1029
|
+
details['description'] = 'Failed to connect to the database server.'
|
|
1030
|
+
details['hint'] = 'Check your database connection settings and network connectivity.'
|
|
1031
|
+
|
|
1032
|
+
# Data type errors
|
|
1033
|
+
elif 'invalid input syntax' in message.lower():
|
|
1034
|
+
details['description'] = 'Invalid data format provided.'
|
|
1035
|
+
details['hint'] = 'Check that your data matches the expected format for the column type.'
|
|
1036
|
+
|
|
1037
|
+
return details
|
|
1038
|
+
|
|
1039
|
+
def _create_exception_with_chaining(self, error_class, message, original_exception, sql=None, parameters=None, format_type=None):
|
|
1040
|
+
"""
|
|
1041
|
+
Create a velocity exception with proper exception chaining and human-readable formatting.
|
|
474
1042
|
|
|
475
1043
|
Args:
|
|
476
1044
|
error_class: The name of the exception class to create
|
|
@@ -478,9 +1046,10 @@ class Engine:
|
|
|
478
1046
|
original_exception: The original exception to chain
|
|
479
1047
|
sql: The SQL statement (optional)
|
|
480
1048
|
parameters: The SQL parameters (optional)
|
|
1049
|
+
format_type: 'console', 'email', or None (auto-detect)
|
|
481
1050
|
|
|
482
1051
|
Returns:
|
|
483
|
-
The created exception with proper chaining
|
|
1052
|
+
The created exception with proper chaining and formatting
|
|
484
1053
|
"""
|
|
485
1054
|
logger = logging.getLogger(__name__)
|
|
486
1055
|
|
|
@@ -489,15 +1058,33 @@ class Engine:
|
|
|
489
1058
|
exception_module = __import__('velocity.db.exceptions', fromlist=[error_class])
|
|
490
1059
|
ExceptionClass = getattr(exception_module, error_class)
|
|
491
1060
|
|
|
492
|
-
#
|
|
493
|
-
if
|
|
494
|
-
|
|
1061
|
+
# Auto-detect format if not specified
|
|
1062
|
+
if format_type is None:
|
|
1063
|
+
format_type = self._detect_output_format()
|
|
1064
|
+
|
|
1065
|
+
# Create human-readable, formatted message
|
|
1066
|
+
formatted_message = self._format_human_readable_error(
|
|
1067
|
+
error_class, message, original_exception, sql, parameters, format_type
|
|
1068
|
+
)
|
|
1069
|
+
|
|
1070
|
+
# For email format, also create a console version for logging
|
|
1071
|
+
if format_type == 'email':
|
|
1072
|
+
console_message = self._format_human_readable_error(
|
|
1073
|
+
error_class, message, original_exception, sql, parameters, 'console'
|
|
1074
|
+
)
|
|
1075
|
+
# Log the console version for server logs
|
|
1076
|
+
logger.error(f"Database Error (Console Format):\n{console_message}")
|
|
1077
|
+
|
|
1078
|
+
# Create custom exception with both formats
|
|
1079
|
+
new_exception = ExceptionClass(formatted_message)
|
|
1080
|
+
new_exception.console_format = console_message
|
|
1081
|
+
new_exception.email_format = formatted_message
|
|
495
1082
|
else:
|
|
496
|
-
|
|
1083
|
+
new_exception = ExceptionClass(formatted_message)
|
|
497
1084
|
|
|
498
|
-
#
|
|
499
|
-
|
|
500
|
-
|
|
1085
|
+
# Only set __cause__ if original_exception is actually a BaseException
|
|
1086
|
+
if isinstance(original_exception, BaseException):
|
|
1087
|
+
new_exception.__cause__ = original_exception # Preserve exception chain
|
|
501
1088
|
|
|
502
1089
|
return new_exception
|
|
503
1090
|
|
|
@@ -507,10 +1094,51 @@ class Engine:
|
|
|
507
1094
|
try:
|
|
508
1095
|
exception_module = __import__('velocity.db.exceptions', fromlist=['DatabaseError'])
|
|
509
1096
|
DatabaseError = getattr(exception_module, 'DatabaseError')
|
|
510
|
-
|
|
511
|
-
|
|
1097
|
+
|
|
1098
|
+
# Auto-detect format if not specified for fallback too
|
|
1099
|
+
if format_type is None:
|
|
1100
|
+
format_type = self._detect_output_format()
|
|
1101
|
+
|
|
1102
|
+
# Still format the fallback nicely
|
|
1103
|
+
formatted_message = self._format_human_readable_error(
|
|
1104
|
+
'DatabaseError', message, original_exception, sql, parameters, format_type
|
|
1105
|
+
)
|
|
1106
|
+
|
|
1107
|
+
fallback_exception = DatabaseError(formatted_message)
|
|
1108
|
+
# Only set __cause__ if original_exception is actually a BaseException
|
|
1109
|
+
if isinstance(original_exception, BaseException):
|
|
1110
|
+
fallback_exception.__cause__ = original_exception
|
|
512
1111
|
return fallback_exception
|
|
513
1112
|
except Exception as fallback_error:
|
|
514
1113
|
logger.critical(f"Failed to create fallback exception: {fallback_error}")
|
|
515
1114
|
# Last resort: return the original exception
|
|
516
1115
|
return original_exception
|
|
1116
|
+
|
|
1117
|
+
def _detect_output_format(self):
|
|
1118
|
+
"""
|
|
1119
|
+
Detect whether we should use console or email formatting based on context.
|
|
1120
|
+
|
|
1121
|
+
Returns:
|
|
1122
|
+
'email' if in email/notification context, 'console' otherwise
|
|
1123
|
+
"""
|
|
1124
|
+
import inspect
|
|
1125
|
+
|
|
1126
|
+
# Look at the call stack for email-related functions
|
|
1127
|
+
stack = inspect.stack()
|
|
1128
|
+
|
|
1129
|
+
email_indicators = [
|
|
1130
|
+
'email', 'mail', 'notification', 'alert', 'send', 'notify',
|
|
1131
|
+
'smtp', 'message', 'recipient', 'subject', 'body'
|
|
1132
|
+
]
|
|
1133
|
+
|
|
1134
|
+
for frame_info in stack:
|
|
1135
|
+
function_name = frame_info.function.lower()
|
|
1136
|
+
filename = frame_info.filename.lower()
|
|
1137
|
+
|
|
1138
|
+
# Check if we're in an email-related context
|
|
1139
|
+
if any(indicator in function_name or indicator in filename
|
|
1140
|
+
for indicator in email_indicators):
|
|
1141
|
+
return 'email'
|
|
1142
|
+
|
|
1143
|
+
# Default to console format
|
|
1144
|
+
return 'console'
|
velocity/db/core/exceptions.py
CHANGED
|
@@ -52,3 +52,19 @@ class DbDataIntegrityError(DbException):
|
|
|
52
52
|
|
|
53
53
|
class DuplicateRowsFoundError(Exception):
|
|
54
54
|
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class DbQueryError(DbException):
|
|
58
|
+
"""Database query error"""
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class DbTransactionError(DbException):
|
|
63
|
+
"""Database transaction error"""
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# Add aliases for backward compatibility with engine.py
|
|
68
|
+
class DatabaseError(DbException):
|
|
69
|
+
"""Generic database error - alias for DbException"""
|
|
70
|
+
pass
|
velocity/db/core/transaction.py
CHANGED
|
@@ -81,8 +81,8 @@ class Transaction:
|
|
|
81
81
|
cursor.execute(sql, parms)
|
|
82
82
|
else:
|
|
83
83
|
cursor.execute(sql)
|
|
84
|
-
except:
|
|
85
|
-
self.engine.process_error(sql, parms)
|
|
84
|
+
except Exception as e:
|
|
85
|
+
raise self.engine.process_error(e, sql, parms)
|
|
86
86
|
|
|
87
87
|
if single:
|
|
88
88
|
self.connection.autocommit = False
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Database exceptions for the velocity library.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DbException(Exception):
|
|
7
|
+
"""Base class for all database exceptions."""
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DbApplicationError(DbException):
|
|
12
|
+
"""Application-level database error."""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DbForeignKeyMissingError(DbException):
|
|
17
|
+
"""Foreign key constraint violation."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DbDatabaseMissingError(DbException):
|
|
22
|
+
"""Database does not exist."""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DbTableMissingError(DbException):
|
|
27
|
+
"""Table does not exist."""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DbColumnMissingError(DbException):
|
|
32
|
+
"""Column does not exist."""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DbTruncationError(DbException):
|
|
37
|
+
"""Data truncation error."""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class DbConnectionError(DbException):
|
|
42
|
+
"""Database connection error."""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class DbDuplicateKeyError(DbException):
|
|
47
|
+
"""Duplicate key constraint violation."""
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class DbObjectExistsError(DbException):
|
|
52
|
+
"""Database object already exists."""
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class DbLockTimeoutError(DbException):
|
|
57
|
+
"""Lock timeout error."""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class DbRetryTransaction(DbException):
|
|
62
|
+
"""Transaction should be retried."""
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class DbDataIntegrityError(DbException):
|
|
67
|
+
"""Data integrity constraint violation."""
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class DbQueryError(DbException):
|
|
72
|
+
"""Database query error."""
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class DbTransactionError(DbException):
|
|
77
|
+
"""Database transaction error."""
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class DuplicateRowsFoundError(Exception):
|
|
82
|
+
"""Multiple rows found when expecting single result."""
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# Add aliases for backward compatibility with engine.py
|
|
87
|
+
class DatabaseError(DbException):
|
|
88
|
+
"""Generic database error - alias for DbException."""
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
__all__ = [
|
|
92
|
+
# Base exceptions
|
|
93
|
+
'DbException',
|
|
94
|
+
'DatabaseError',
|
|
95
|
+
|
|
96
|
+
# Specific exceptions
|
|
97
|
+
'DbApplicationError',
|
|
98
|
+
'DbForeignKeyMissingError',
|
|
99
|
+
'DbDatabaseMissingError',
|
|
100
|
+
'DbTableMissingError',
|
|
101
|
+
'DbColumnMissingError',
|
|
102
|
+
'DbTruncationError',
|
|
103
|
+
'DbConnectionError',
|
|
104
|
+
'DbDuplicateKeyError',
|
|
105
|
+
'DbObjectExistsError',
|
|
106
|
+
'DbLockTimeoutError',
|
|
107
|
+
'DbRetryTransaction',
|
|
108
|
+
'DbDataIntegrityError',
|
|
109
|
+
'DbQueryError',
|
|
110
|
+
'DbTransactionError',
|
|
111
|
+
'DuplicateRowsFoundError',
|
|
112
|
+
]
|
|
@@ -11,18 +11,19 @@ velocity/aws/handlers/context.py,sha256=UIjNR83y2NSIyK8HMPX8t5tpJHFNabiZvNgmmdQL
|
|
|
11
11
|
velocity/aws/handlers/lambda_handler.py,sha256=0KrT6UIxDILzBRpoRSvwDgHpQ-vWfubcZFOCbJsewDc,6516
|
|
12
12
|
velocity/aws/handlers/response.py,sha256=LXhtizLKnVBWjtHyE0h0bk-NYDrRpj7CHa7tRz9KkC4,9324
|
|
13
13
|
velocity/aws/handlers/sqs_handler.py,sha256=nqt8NMOc5yO-L6CZo7NjgR8Q4KPKTDFBO-0eHu6oxkY,7149
|
|
14
|
-
velocity/db/__init__.py,sha256=
|
|
14
|
+
velocity/db/__init__.py,sha256=Xr-kN98AKgjDic1Lxi0yLiobsEpN5a6ZjDMS8KHSTlE,301
|
|
15
|
+
velocity/db/exceptions.py,sha256=oTXzdxP0GrraGrqRD1JgIVP5urO5yNN7A3IzTiAtNJ0,2173
|
|
15
16
|
velocity/db/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
17
|
velocity/db/core/column.py,sha256=tAr8tL3a2nyaYpNHhGl508FrY_pGZTzyYgjAV5CEBv4,4092
|
|
17
18
|
velocity/db/core/database.py,sha256=3zNGItklu9tZCKsbx2T2vCcU1so8AL9PPL0DLjvaz6s,3554
|
|
18
19
|
velocity/db/core/decorators.py,sha256=76Jkr9XptXt8cvcgp1zbHfuL8uHzWy8lwfR29u-DVu4,4574
|
|
19
|
-
velocity/db/core/engine.py,sha256=
|
|
20
|
-
velocity/db/core/exceptions.py,sha256=
|
|
20
|
+
velocity/db/core/engine.py,sha256=uF0XC_kVTNQ2LdX1xaUPSCYYtGHyGSZ-qmhRtSQvSO8,49187
|
|
21
|
+
velocity/db/core/exceptions.py,sha256=tuDniRqTX8Opc2d033LPJOI3Ux4NSwUcHqW729n-HXA,1027
|
|
21
22
|
velocity/db/core/result.py,sha256=OVqoMwlx3CHNNwr-JGWRx5I8u_YX6hlUpecx99UT5nE,6164
|
|
22
23
|
velocity/db/core/row.py,sha256=aliLYTTFirgJsOvmUsANwJMyxaATuhpGpFJhcu_twwY,6709
|
|
23
24
|
velocity/db/core/sequence.py,sha256=VMBc0ZjGnOaWTwKW6xMNTdP8rZ2umQ8ml4fHTTwuGq4,3904
|
|
24
25
|
velocity/db/core/table.py,sha256=g2mq7_VzLBtxITAn47BbgMcUoJAy9XVP6ohzScNl_so,34573
|
|
25
|
-
velocity/db/core/transaction.py,sha256=
|
|
26
|
+
velocity/db/core/transaction.py,sha256=unjmVkkfb7D8Wow6V8V8aLaxUZo316i--ksZxc4-I1Q,6613
|
|
26
27
|
velocity/db/servers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
28
|
velocity/db/servers/mysql.py,sha256=qHwlB_Mg02R7QFjD5QvJCorYYiP50CqEiQyZVl3uYns,20914
|
|
28
29
|
velocity/db/servers/mysql_reserved.py,sha256=CYdZJBOpS-ptImaRZcmVLumdUdbFuf9Tfdzu_mUT5wY,3507
|
|
@@ -34,7 +35,7 @@ velocity/db/servers/tablehelper.py,sha256=qOHHKgQgUC0t_AUcY5oaPfjkRJS9wnMI4YJCDI
|
|
|
34
35
|
velocity/db/servers/postgres/__init__.py,sha256=FUvXO3R5CtKCTGRim1geisIxXbiG_aQ_VFSQX9HGsjw,529
|
|
35
36
|
velocity/db/servers/postgres/operators.py,sha256=A2T1qFwhzPl0fdXVhLZJhh5Qfx-qF8oZsDnxnq2n_V8,389
|
|
36
37
|
velocity/db/servers/postgres/reserved.py,sha256=5tKLaqFV-HrWRj-nsrxl5KGbmeM3ukn_bPZK36XEu8M,3648
|
|
37
|
-
velocity/db/servers/postgres/sql.py,sha256=
|
|
38
|
+
velocity/db/servers/postgres/sql.py,sha256=vcdzj5PtlF3Qnt4Lh3y2OtaSuq4iLIUKyGP5F9vULE8,41576
|
|
38
39
|
velocity/db/servers/postgres/types.py,sha256=Wa45ppVf_pdWul-jYWFRGMl6IdSq8dAp10SKnhL7osQ,3757
|
|
39
40
|
velocity/misc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
40
41
|
velocity/misc/db.py,sha256=MPgt-kkukKR_Wh_S_5W-MyDgaeoZ4YLoDJ54wU2ppm4,2830
|
|
@@ -47,8 +48,8 @@ velocity/misc/tools.py,sha256=_bGneHHA_BV-kUonzw5H3hdJ5AOJRCKfzhgpkFbGqIo,1502
|
|
|
47
48
|
velocity/misc/conv/__init__.py,sha256=MLYF58QHjzfDSxb1rdnmLnuEQCa3gnhzzZ30CwZVvQo,40
|
|
48
49
|
velocity/misc/conv/iconv.py,sha256=d4_BucW8HTIkGNurJ7GWrtuptqUf-9t79ObzjJ5N76U,10603
|
|
49
50
|
velocity/misc/conv/oconv.py,sha256=h5Lo05DqOQnxoD3y6Px_MQP_V-pBbWf8Hkgkb9Xp1jk,6032
|
|
50
|
-
velocity_python-0.0.
|
|
51
|
-
velocity_python-0.0.
|
|
52
|
-
velocity_python-0.0.
|
|
53
|
-
velocity_python-0.0.
|
|
54
|
-
velocity_python-0.0.
|
|
51
|
+
velocity_python-0.0.103.dist-info/licenses/LICENSE,sha256=aoN245GG8s9oRUU89KNiGTU4_4OtnNmVi4hQeChg6rM,1076
|
|
52
|
+
velocity_python-0.0.103.dist-info/METADATA,sha256=9FYOm2SrGrV7CJloPGfUn8upxC3zNSst9bFUXwVOEng,33023
|
|
53
|
+
velocity_python-0.0.103.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
54
|
+
velocity_python-0.0.103.dist-info/top_level.txt,sha256=JW2vJPmodgdgSz7H6yoZvnxF8S3fTMIv-YJWCT1sNW0,9
|
|
55
|
+
velocity_python-0.0.103.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|