velocity-python 0.0.102__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.

@@ -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
- extra_data = {
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 error caught. Attempting to transform: "
352
- f"type={type(exception).__name__}, sql={sql[:100] if sql else 'None'}...",
353
- extra=extra_data
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"Failed to extract error code: {e}")
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"Failed to convert exception to string: {e}")
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"Classified error by code: {error_code} -> {error_class}")
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}' -> {error_class}")
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"Could not classify error. Returning generic DatabaseError. "
464
- f"Error code: {error_code}, Available error codes: {list(getattr(self, 'error_codes', {}).keys()) if hasattr(self, 'error_codes') else 'None'}"
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 _create_exception_with_chaining(self, error_class, message, original_exception, sql=None, parameters=None):
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
- Create a velocity exception with proper exception chaining.
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('&', '&amp;')
795
+ .replace('<', '&lt;')
796
+ .replace('>', '&gt;')
797
+ .replace('"', '&quot;')
798
+ .replace("'", '&#x27;'))
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
- # Create enhanced message with context
493
- if sql:
494
- enhanced_message = f"{message} (SQL: {sql[:200]}{'...' if len(sql) > 200 else ''})"
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
- enhanced_message = message
1083
+ new_exception = ExceptionClass(formatted_message)
497
1084
 
498
- # Create the exception with chaining
499
- new_exception = ExceptionClass(enhanced_message)
500
- new_exception.__cause__ = original_exception # Preserve exception chain
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
- fallback_exception = DatabaseError(f"Database error: {message}")
511
- fallback_exception.__cause__ = original_exception
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'
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.0.102
3
+ Version: 0.0.103
4
4
  Summary: A rapid application development library for interfacing with data storage
5
5
  Author-email: Velocity Team <contact@example.com>
6
6
  License: MIT
@@ -17,13 +17,13 @@ velocity/db/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
17
17
  velocity/db/core/column.py,sha256=tAr8tL3a2nyaYpNHhGl508FrY_pGZTzyYgjAV5CEBv4,4092
18
18
  velocity/db/core/database.py,sha256=3zNGItklu9tZCKsbx2T2vCcU1so8AL9PPL0DLjvaz6s,3554
19
19
  velocity/db/core/decorators.py,sha256=76Jkr9XptXt8cvcgp1zbHfuL8uHzWy8lwfR29u-DVu4,4574
20
- velocity/db/core/engine.py,sha256=UmaYwVCDBtvDHdHdKNsZ4kZ0GpZ7uMp1MaqpfOHblNI,19311
20
+ velocity/db/core/engine.py,sha256=uF0XC_kVTNQ2LdX1xaUPSCYYtGHyGSZ-qmhRtSQvSO8,49187
21
21
  velocity/db/core/exceptions.py,sha256=tuDniRqTX8Opc2d033LPJOI3Ux4NSwUcHqW729n-HXA,1027
22
22
  velocity/db/core/result.py,sha256=OVqoMwlx3CHNNwr-JGWRx5I8u_YX6hlUpecx99UT5nE,6164
23
23
  velocity/db/core/row.py,sha256=aliLYTTFirgJsOvmUsANwJMyxaATuhpGpFJhcu_twwY,6709
24
24
  velocity/db/core/sequence.py,sha256=VMBc0ZjGnOaWTwKW6xMNTdP8rZ2umQ8ml4fHTTwuGq4,3904
25
25
  velocity/db/core/table.py,sha256=g2mq7_VzLBtxITAn47BbgMcUoJAy9XVP6ohzScNl_so,34573
26
- velocity/db/core/transaction.py,sha256=IQEmrHAjCg6hqxQQOpPLWUbyLXrTjIPGLHHv7P6urKU,6589
26
+ velocity/db/core/transaction.py,sha256=unjmVkkfb7D8Wow6V8V8aLaxUZo316i--ksZxc4-I1Q,6613
27
27
  velocity/db/servers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
28
  velocity/db/servers/mysql.py,sha256=qHwlB_Mg02R7QFjD5QvJCorYYiP50CqEiQyZVl3uYns,20914
29
29
  velocity/db/servers/mysql_reserved.py,sha256=CYdZJBOpS-ptImaRZcmVLumdUdbFuf9Tfdzu_mUT5wY,3507
@@ -48,8 +48,8 @@ velocity/misc/tools.py,sha256=_bGneHHA_BV-kUonzw5H3hdJ5AOJRCKfzhgpkFbGqIo,1502
48
48
  velocity/misc/conv/__init__.py,sha256=MLYF58QHjzfDSxb1rdnmLnuEQCa3gnhzzZ30CwZVvQo,40
49
49
  velocity/misc/conv/iconv.py,sha256=d4_BucW8HTIkGNurJ7GWrtuptqUf-9t79ObzjJ5N76U,10603
50
50
  velocity/misc/conv/oconv.py,sha256=h5Lo05DqOQnxoD3y6Px_MQP_V-pBbWf8Hkgkb9Xp1jk,6032
51
- velocity_python-0.0.102.dist-info/licenses/LICENSE,sha256=aoN245GG8s9oRUU89KNiGTU4_4OtnNmVi4hQeChg6rM,1076
52
- velocity_python-0.0.102.dist-info/METADATA,sha256=OWZ2EgBtgok62DzEOHW0tLMexelmqJUgvadHjhNNe4M,33023
53
- velocity_python-0.0.102.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
54
- velocity_python-0.0.102.dist-info/top_level.txt,sha256=JW2vJPmodgdgSz7H6yoZvnxF8S3fTMIv-YJWCT1sNW0,9
55
- velocity_python-0.0.102.dist-info/RECORD,,
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,,