velocity-python 0.0.110__py3-none-any.whl → 0.0.112__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/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.0.110"
1
+ __version__ = version = "0.0.112"
2
2
 
3
3
  from . import aws
4
4
  from . import db
@@ -343,6 +343,11 @@ class Engine:
343
343
 
344
344
  msg = str(exception).strip().lower()
345
345
 
346
+ # Create enhanced error message with SQL query
347
+ enhanced_message = str(exception)
348
+ if sql:
349
+ enhanced_message += f"\n\nSQL Query:\n{self._format_sql_with_params(sql, parameters)}"
350
+
346
351
  logger.warning(
347
352
  "Database error caught. Attempting to transform: code=%s message=%s",
348
353
  error_code,
@@ -351,55 +356,55 @@ class Engine:
351
356
 
352
357
  # Direct error code mapping
353
358
  if error_code in self.sql.ApplicationErrorCodes:
354
- raise exceptions.DbApplicationError(str(exception)) from exception
359
+ raise exceptions.DbApplicationError(enhanced_message) from exception
355
360
  if error_code in self.sql.ColumnMissingErrorCodes:
356
- raise exceptions.DbColumnMissingError(str(exception)) from exception
361
+ raise exceptions.DbColumnMissingError(enhanced_message) from exception
357
362
  if error_code in self.sql.TableMissingErrorCodes:
358
- raise exceptions.DbTableMissingError(str(exception)) from exception
363
+ raise exceptions.DbTableMissingError(enhanced_message) from exception
359
364
  if error_code in self.sql.DatabaseMissingErrorCodes:
360
- raise exceptions.DbDatabaseMissingError(str(exception)) from exception
365
+ raise exceptions.DbDatabaseMissingError(enhanced_message) from exception
361
366
  if error_code in self.sql.ForeignKeyMissingErrorCodes:
362
- raise exceptions.DbForeignKeyMissingError(str(exception)) from exception
367
+ raise exceptions.DbForeignKeyMissingError(enhanced_message) from exception
363
368
  if error_code in self.sql.TruncationErrorCodes:
364
- raise exceptions.DbTruncationError(str(exception)) from exception
369
+ raise exceptions.DbTruncationError(enhanced_message) from exception
365
370
  if error_code in self.sql.DataIntegrityErrorCodes:
366
- raise exceptions.DbDataIntegrityError(str(exception)) from exception
371
+ raise exceptions.DbDataIntegrityError(enhanced_message) from exception
367
372
  if error_code in self.sql.ConnectionErrorCodes:
368
- raise exceptions.DbConnectionError(str(exception)) from exception
373
+ raise exceptions.DbConnectionError(enhanced_message) from exception
369
374
  if error_code in self.sql.DuplicateKeyErrorCodes:
370
- raise exceptions.DbDuplicateKeyError(str(exception)) from exception
375
+ raise exceptions.DbDuplicateKeyError(enhanced_message) from exception
371
376
  if error_code in self.sql.DatabaseObjectExistsErrorCodes:
372
- raise exceptions.DbObjectExistsError(str(exception)) from exception
377
+ raise exceptions.DbObjectExistsError(enhanced_message) from exception
373
378
  if error_code in self.sql.LockTimeoutErrorCodes:
374
- raise exceptions.DbLockTimeoutError(str(exception)) from exception
379
+ raise exceptions.DbLockTimeoutError(enhanced_message) from exception
375
380
  if error_code in self.sql.RetryTransactionCodes:
376
- raise exceptions.DbRetryTransaction(str(exception)) from exception
381
+ raise exceptions.DbRetryTransaction(enhanced_message) from exception
377
382
 
378
383
  # Regex-based fallback patterns
379
384
  if re.search(r"key \(sys_id\)=\(\d+\) already exists.", msg, re.M):
380
- raise exceptions.DbDuplicateKeyError(str(exception)) from exception
385
+ raise exceptions.DbDuplicateKeyError(enhanced_message) from exception
381
386
  if re.findall(r"database.*does not exist", msg, re.M):
382
- raise exceptions.DbDatabaseMissingError(str(exception)) from exception
387
+ raise exceptions.DbDatabaseMissingError(enhanced_message) from exception
383
388
  if re.findall(r"no such database", msg, re.M):
384
- raise exceptions.DbDatabaseMissingError(str(exception)) from exception
389
+ raise exceptions.DbDatabaseMissingError(enhanced_message) from exception
385
390
  if re.findall(r"already exists", msg, re.M):
386
- raise exceptions.DbObjectExistsError(str(exception)) from exception
391
+ raise exceptions.DbObjectExistsError(enhanced_message) from exception
387
392
  if re.findall(r"server closed the connection unexpectedly", msg, re.M):
388
- raise exceptions.DbConnectionError(str(exception)) from exception
393
+ raise exceptions.DbConnectionError(enhanced_message) from exception
389
394
  if re.findall(r"no connection to the server", msg, re.M):
390
- raise exceptions.DbConnectionError(str(exception)) from exception
395
+ raise exceptions.DbConnectionError(enhanced_message) from exception
391
396
  if re.findall(r"connection timed out", msg, re.M):
392
- raise exceptions.DbConnectionError(str(exception)) from exception
397
+ raise exceptions.DbConnectionError(enhanced_message) from exception
393
398
  if re.findall(r"could not connect to server", msg, re.M):
394
- raise exceptions.DbConnectionError(str(exception)) from exception
399
+ raise exceptions.DbConnectionError(enhanced_message) from exception
395
400
  if re.findall(r"cannot connect to server", msg, re.M):
396
- raise exceptions.DbConnectionError(str(exception)) from exception
401
+ raise exceptions.DbConnectionError(enhanced_message) from exception
397
402
  if re.findall(r"connection already closed", msg, re.M):
398
- raise exceptions.DbConnectionError(str(exception)) from exception
403
+ raise exceptions.DbConnectionError(enhanced_message) from exception
399
404
  if re.findall(r"cursor already closed", msg, re.M):
400
- raise exceptions.DbConnectionError(str(exception)) from exception
405
+ raise exceptions.DbConnectionError(enhanced_message) from exception
401
406
  if "no such table:" in msg:
402
- raise exceptions.DbTableMissingError(str(exception)) from exception
407
+ raise exceptions.DbTableMissingError(enhanced_message) from exception
403
408
 
404
409
  logger.error(
405
410
  "Unhandled/Unknown Error in engine.process_error",
@@ -412,680 +417,65 @@ class Engine:
412
417
  },
413
418
  )
414
419
 
415
- # If we can't classify it, re-raise the original exception
416
- raise exception
417
-
418
- def _format_human_readable_error(self, error_class, message, original_exception, sql=None, parameters=None, format_type='console'):
419
- """
420
- Format a human-readable error message with proper context and formatting.
421
-
422
- Args:
423
- error_class: The name of the exception class
424
- message: The raw error message
425
- original_exception: The original exception
426
- sql: The SQL statement (optional)
427
- parameters: The SQL parameters (optional)
428
- format_type: 'console' for terminal output, 'email' for HTML email format
429
-
430
- Returns:
431
- A nicely formatted, human-readable error message
432
- """
433
- if format_type == 'email':
434
- return self._format_email_error(error_class, message, original_exception, sql, parameters)
435
- else:
436
- return self._format_console_error(error_class, message, original_exception, sql, parameters)
437
-
438
- def _format_console_error(self, error_class, message, original_exception, sql=None, parameters=None):
439
- """
440
- Format error message for console/terminal output with Unicode box drawing.
441
- """
442
- # Map error classes to user-friendly descriptions
443
- error_descriptions = {
444
- 'DbColumnMissingError': 'Column Not Found',
445
- 'DbTableMissingError': 'Table Not Found',
446
- 'DbDatabaseMissingError': 'Database Not Found',
447
- 'DbForeignKeyMissingError': 'Foreign Key Constraint Violation',
448
- 'DbDuplicateKeyError': 'Duplicate Key Violation',
449
- 'DbConnectionError': 'Database Connection Failed',
450
- 'DbDataIntegrityError': 'Data Integrity Violation',
451
- 'DbQueryError': 'Query Execution Error',
452
- 'DbTransactionError': 'Transaction Error',
453
- 'DbTruncationError': 'Data Truncation Error',
454
- 'DatabaseError': 'Database Error'
455
- }
456
-
457
- # Get user-friendly error type
458
- friendly_type = error_descriptions.get(error_class, error_class.replace('Db', '').replace('Error', ' Error'))
459
-
460
- # Clean up the original message
461
- clean_message = str(message).strip()
462
-
463
- # Extract specific details from PostgreSQL errors
464
- details = self._extract_error_details(original_exception, clean_message)
465
-
466
- # Build the formatted message
467
- lines = []
468
- lines.append(f"╭─ {friendly_type} ─" + "─" * max(0, 60 - len(friendly_type)))
469
- lines.append("│")
470
-
471
- # Add the main error description
472
- if details.get('description'):
473
- lines.append(f"│ {details['description']}")
474
- else:
475
- lines.append(f"│ {clean_message}")
476
- lines.append("│")
477
-
478
- # Add error code if available
479
- error_code = getattr(original_exception, 'pgcode', None)
480
- if error_code:
481
- lines.append(f"│ Error Code: {error_code}")
482
-
483
- # Add specific details if available
484
- if details.get('column'):
485
- lines.append(f"│ Column: {details['column']}")
486
- if details.get('table'):
487
- lines.append(f"│ Table: {details['table']}")
488
- if details.get('constraint'):
489
- lines.append(f"│ Constraint: {details['constraint']}")
490
- if details.get('hint'):
491
- lines.append(f"│ Hint: {details['hint']}")
492
-
493
- # Add SQL context if available
494
- if sql:
495
- lines.append("│")
496
- lines.append("│ SQL Statement:")
497
- # Show complete SQL without truncation for debugging
498
- for line in sql.split('\n'):
499
- lines.append(f"│ {line.strip()}")
500
-
501
- # Add parameters if available
502
- if parameters is not None:
503
- lines.append("│")
504
- lines.append(f"│ Parameters: {parameters}")
505
-
506
- # Add debugging section with copy-paste ready format
507
- if sql or parameters is not None:
508
- lines.append("│")
509
- lines.append("│ ┌─ DEBUG COPY-PASTE SECTION ─────────────────────────────")
510
-
511
- if sql and parameters is not None:
512
- # Format for direct copy-paste into database console
513
- lines.append("│ │")
514
- lines.append("│ │ Complete SQL with Parameters:")
515
- lines.append("│ │ " + "─" * 45)
516
-
517
- # Show the raw SQL
518
- lines.append("│ │ Raw SQL:")
519
- for line in sql.split('\n'):
520
- lines.append(f"│ │ {line}")
521
-
522
- lines.append("│ │")
523
- lines.append(f"│ │ Raw Parameters: {parameters}")
524
-
525
- # Try to create an executable version
526
- lines.append("│ │")
527
- lines.append("│ │ Executable Format (for PostgreSQL):")
528
- lines.append("│ │ " + "─" * 35)
529
-
530
- try:
531
- # Create a version with parameters substituted for testing
532
- executable_sql = self._format_executable_sql(sql, parameters)
533
- for line in executable_sql.split('\n'):
534
- lines.append(f"│ │ {line}")
535
- except Exception:
536
- lines.append("│ │ [Unable to format executable SQL]")
537
- for line in sql.split('\n'):
538
- lines.append(f"│ │ {line}")
539
- lines.append(f"│ │ -- Parameters: {parameters}")
540
-
541
- elif sql:
542
- lines.append("│ │")
543
- lines.append("│ │ SQL Statement (no parameters):")
544
- lines.append("│ │ " + "─" * 30)
545
- for line in sql.split('\n'):
546
- lines.append(f"│ │ {line}")
547
-
548
- lines.append("│ │")
549
- lines.append("│ └─────────────────────────────────────────────────────────")
550
-
551
- # Add detailed call stack information for debugging
552
- stack_info = self._extract_call_stack_info()
553
- if stack_info:
554
- lines.append("│")
555
- lines.append("│ ┌─ CALL STACK ANALYSIS ──────────────────────────────────")
556
- lines.append("│ │")
557
-
558
- if stack_info.get('top_level_call'):
559
- lines.append("│ │ Top-Level Function (most helpful for debugging):")
560
- lines.append("│ │ " + "─" * 48)
561
- call = stack_info['top_level_call']
562
- lines.append(f"│ │ Function: {call['function']}")
563
- lines.append(f"│ │ File: {call['file']}")
564
- lines.append(f"│ │ Line: {call['line']}")
565
- if call.get('code'):
566
- lines.append(f"│ │ Code: {call['code'].strip()}")
567
-
568
- if stack_info.get('relevant_calls'):
569
- lines.append("│ │")
570
- lines.append("│ │ Relevant Call Chain (excluding middleware):")
571
- lines.append("│ │ " + "─" * 44)
572
- for i, call in enumerate(stack_info['relevant_calls'][:5], 1): # Show top 5
573
- lines.append(f"│ │ {i}. {call['function']} in {call['file']}:{call['line']}")
574
- if call.get('code'):
575
- lines.append(f"│ │ → {call['code'].strip()}")
576
-
577
- if stack_info.get('lambda_context'):
578
- lines.append("│ │")
579
- lines.append("│ │ AWS Lambda Context:")
580
- lines.append("│ │ " + "─" * 19)
581
- for key, value in stack_info['lambda_context'].items():
582
- lines.append(f"│ │ {key}: {value}")
583
-
584
- lines.append("│ │")
585
- lines.append("│ └─────────────────────────────────────────────────────────")
586
-
587
- lines.append("│")
588
- lines.append("╰" + "─" * 70)
589
-
590
- return '\n'.join(lines)
420
+ # If we can't classify it, re-raise with enhanced message
421
+ raise type(exception)(enhanced_message) from exception
591
422
 
592
- def _format_email_error(self, error_class, message, original_exception, sql=None, parameters=None):
423
+ def _format_sql_with_params(self, sql, parameters):
593
424
  """
594
- Format error message for email delivery with HTML formatting.
425
+ Format SQL query with parameters merged for easy copy-paste debugging.
595
426
  """
596
- # Map error classes to user-friendly descriptions
597
- error_descriptions = {
598
- 'DbColumnMissingError': 'Column Not Found',
599
- 'DbTableMissingError': 'Table Not Found',
600
- 'DbDatabaseMissingError': 'Database Not Found',
601
- 'DbForeignKeyMissingError': 'Foreign Key Constraint Violation',
602
- 'DbDuplicateKeyError': 'Duplicate Key Violation',
603
- 'DbConnectionError': 'Database Connection Failed',
604
- 'DbDataIntegrityError': 'Data Integrity Violation',
605
- 'DbQueryError': 'Query Execution Error',
606
- 'DbTransactionError': 'Transaction Error',
607
- 'DbTruncationError': 'Data Truncation Error',
608
- 'DatabaseError': 'Database Error'
609
- }
610
-
611
- # Get user-friendly error type
612
- friendly_type = error_descriptions.get(error_class, error_class.replace('Db', '').replace('Error', ' Error'))
613
-
614
- # Clean up the original message
615
- clean_message = str(message).strip()
616
-
617
- # Extract specific details from PostgreSQL errors
618
- details = self._extract_error_details(original_exception, clean_message)
619
-
620
- # Get error code
621
- error_code = getattr(original_exception, 'pgcode', None)
622
-
623
- # Get stack info
624
- stack_info = self._extract_call_stack_info()
625
-
626
- # Build HTML email format
627
- html_parts = []
628
-
629
- # Email header
630
- html_parts.append("""
631
- <html>
632
- <head>
633
- <style>
634
- body { font-family: 'Courier New', monospace; margin: 20px; }
635
- .error-container { border: 2px solid #dc3545; border-radius: 8px; padding: 20px; background-color: #f8f9fa; }
636
- .error-header { background-color: #dc3545; color: white; padding: 10px; border-radius: 5px; font-weight: bold; font-size: 16px; margin-bottom: 15px; }
637
- .error-section { margin: 15px 0; padding: 10px; background-color: #ffffff; border-left: 4px solid #007bff; }
638
- .section-title { font-weight: bold; color: #007bff; margin-bottom: 8px; }
639
- .code-block { background-color: #f1f3f4; padding: 10px; border-radius: 4px; font-family: 'Courier New', monospace; margin: 5px 0; white-space: pre-wrap; }
640
- .highlight { background-color: #fff3cd; padding: 2px 4px; border-radius: 3px; }
641
- .stack-call { margin: 5px 0; padding: 5px; background-color: #e9ecef; border-radius: 3px; }
642
- .copy-section { background-color: #d1ecf1; border: 1px solid #bee5eb; padding: 15px; border-radius: 5px; margin: 10px 0; }
643
- </style>
644
- </head>
645
- <body>
646
- <div class="error-container">
647
- """)
648
-
649
- # Error header
650
- html_parts.append(f' <div class="error-header">🚨 {friendly_type}</div>')
651
-
652
- # Main error description
653
- description = details.get('description', clean_message)
654
- html_parts.append(f' <div class="error-section"><strong>{description}</strong></div>')
655
-
656
- # Error details section
657
- if error_code or details.get('column') or details.get('table') or details.get('constraint'):
658
- html_parts.append(' <div class="error-section">')
659
- html_parts.append(' <div class="section-title">Error Details:</div>')
660
- if error_code:
661
- html_parts.append(f' <div><strong>Error Code:</strong> <span class="highlight">{error_code}</span></div>')
662
- if details.get('column'):
663
- html_parts.append(f' <div><strong>Column:</strong> <span class="highlight">{details["column"]}</span></div>')
664
- if details.get('table'):
665
- html_parts.append(f' <div><strong>Table:</strong> <span class="highlight">{details["table"]}</span></div>')
666
- if details.get('constraint'):
667
- html_parts.append(f' <div><strong>Constraint:</strong> <span class="highlight">{details["constraint"]}</span></div>')
668
- if details.get('hint'):
669
- html_parts.append(f' <div><strong>Hint:</strong> {details["hint"]}</div>')
670
- html_parts.append(' </div>')
671
-
672
- # SQL and Parameters section
673
- if sql or parameters is not None:
674
- html_parts.append(' <div class="copy-section">')
675
- html_parts.append(' <div class="section-title">📋 Debug Information (Copy-Paste Ready)</div>')
676
-
677
- if sql:
678
- html_parts.append(' <div><strong>SQL Statement:</strong></div>')
679
- html_parts.append(f' <div class="code-block">{self._html_escape(sql)}</div>')
680
-
681
- if parameters is not None:
682
- html_parts.append(f' <div><strong>Parameters:</strong> <code>{self._html_escape(str(parameters))}</code></div>')
683
-
684
- # Executable SQL
685
- if sql and parameters is not None:
686
- try:
687
- executable_sql = self._format_executable_sql(sql, parameters)
688
- html_parts.append(' <div><strong>Executable SQL (for testing):</strong></div>')
689
- html_parts.append(f' <div class="code-block">{self._html_escape(executable_sql)}</div>')
690
- except Exception:
691
- pass
692
-
693
- html_parts.append(' </div>')
694
-
695
- # Call stack section
696
- if stack_info and stack_info.get('top_level_call'):
697
- html_parts.append(' <div class="error-section">')
698
- html_parts.append(' <div class="section-title">🔍 Source Code Location</div>')
699
-
700
- call = stack_info['top_level_call']
701
- html_parts.append(' <div class="stack-call">')
702
- html_parts.append(f' <strong>Function:</strong> {call["function"]}<br>')
703
- html_parts.append(f' <strong>File:</strong> {call["file"]}<br>')
704
- html_parts.append(f' <strong>Line:</strong> {call["line"]}')
705
- if call.get('code'):
706
- html_parts.append(f'<br> <strong>Code:</strong> <code>{self._html_escape(call["code"].strip())}</code>')
707
- html_parts.append(' </div>')
708
-
709
- # Show relevant call chain
710
- if stack_info.get('relevant_calls') and len(stack_info['relevant_calls']) > 1:
711
- html_parts.append(' <div><strong>Call Chain:</strong></div>')
712
- for i, call in enumerate(stack_info['relevant_calls'][:4], 1):
713
- html_parts.append(' <div class="stack-call">')
714
- html_parts.append(f' {i}. <strong>{call["function"]}</strong> in {call["file"]}:{call["line"]}')
715
- html_parts.append(' </div>')
716
-
717
- html_parts.append(' </div>')
718
-
719
- # Lambda context
720
- if stack_info and stack_info.get('lambda_context'):
721
- html_parts.append(' <div class="error-section">')
722
- html_parts.append(' <div class="section-title">⚡ AWS Lambda Context</div>')
723
- for key, value in stack_info['lambda_context'].items():
724
- html_parts.append(f' <div><strong>{key.title()}:</strong> {value}</div>')
725
- html_parts.append(' </div>')
726
-
727
- # Email footer
728
- html_parts.append("""
729
- </div>
730
- </body>
731
- </html>
732
- """)
733
-
734
- return ''.join(html_parts)
735
-
736
- def _html_escape(self, text):
737
- """Escape HTML special characters."""
738
- if not text:
739
- return ""
740
- return (str(text)
741
- .replace('&', '&amp;')
742
- .replace('<', '&lt;')
743
- .replace('>', '&gt;')
744
- .replace('"', '&quot;')
745
- .replace("'", '&#x27;'))
746
-
747
- def _format_executable_sql(self, sql, parameters):
748
- """
749
- Format SQL with parameters substituted for easy copy-paste debugging.
750
-
751
- Args:
752
- sql: The SQL statement with placeholders
753
- parameters: The parameters to substitute
427
+ if not sql:
428
+ return "No SQL provided"
754
429
 
755
- Returns:
756
- SQL statement with parameters properly formatted for execution
757
- """
758
430
  if not parameters:
759
431
  return sql
760
432
 
761
433
  try:
762
434
  # Handle different parameter formats
763
435
  if isinstance(parameters, (list, tuple)):
764
- # For positional parameters (%s style)
765
- formatted_sql = sql
766
-
767
- # Replace %s placeholders with properly formatted values
436
+ # Convert parameters to strings and handle None values
437
+ formatted_params = []
768
438
  for param in parameters:
769
- if isinstance(param, str):
439
+ if param is None:
440
+ formatted_params.append('NULL')
441
+ elif isinstance(param, str):
770
442
  # Escape single quotes and wrap in quotes
771
- formatted_param = f"'{param.replace(chr(39), chr(39)+chr(39))}'"
772
- elif isinstance(param, (int, float)):
773
- formatted_param = str(param)
774
- elif param is None:
775
- formatted_param = 'NULL'
443
+ escaped = param.replace("'", "''")
444
+ formatted_params.append(f"'{escaped}'")
776
445
  elif isinstance(param, bool):
777
- formatted_param = 'TRUE' if param else 'FALSE'
446
+ formatted_params.append('TRUE' if param else 'FALSE')
778
447
  else:
779
- # For other types, try to convert to string and quote
780
- formatted_param = f"'{str(param).replace(chr(39), chr(39)+chr(39))}'"
781
-
782
- # Replace first occurrence of %s
783
- formatted_sql = formatted_sql.replace('%s', formatted_param, 1)
448
+ formatted_params.append(str(param))
784
449
 
450
+ # Replace %s placeholders with actual values
451
+ formatted_sql = sql
452
+ for param in formatted_params:
453
+ formatted_sql = formatted_sql.replace('%s', param, 1)
454
+
785
455
  return formatted_sql
786
456
 
787
457
  elif isinstance(parameters, dict):
788
- # For named parameters (%(name)s style)
458
+ # Handle named parameters
789
459
  formatted_sql = sql
790
460
  for key, value in parameters.items():
791
- placeholder = f'%({key})s'
792
- if isinstance(value, str):
793
- formatted_value = f"'{value.replace(chr(39), chr(39)+chr(39))}'"
794
- elif isinstance(value, (int, float)):
795
- formatted_value = str(value)
796
- elif value is None:
797
- formatted_value = 'NULL'
461
+ if value is None:
462
+ replacement = 'NULL'
463
+ elif isinstance(value, str):
464
+ escaped = value.replace("'", "''")
465
+ replacement = f"'{escaped}'"
798
466
  elif isinstance(value, bool):
799
- formatted_value = 'TRUE' if value else 'FALSE'
467
+ replacement = 'TRUE' if value else 'FALSE'
800
468
  else:
801
- formatted_value = f"'{str(value).replace(chr(39), chr(39)+chr(39))}'"
469
+ replacement = str(value)
470
+
471
+ # Replace %(key)s or :key patterns
472
+ formatted_sql = formatted_sql.replace(f'%({key})s', replacement)
473
+ formatted_sql = formatted_sql.replace(f':{key}', replacement)
802
474
 
803
- formatted_sql = formatted_sql.replace(placeholder, formatted_value)
804
-
805
475
  return formatted_sql
806
-
807
476
  else:
808
- # Fallback: just append parameters as comment
809
477
  return f"{sql}\n-- Parameters: {parameters}"
810
478
 
811
- except Exception:
812
- # If formatting fails, return original with parameters as comment
813
- return f"{sql}\n-- Parameters: {parameters}"
814
-
815
- def _extract_call_stack_info(self):
816
- """
817
- Extract relevant call stack information for debugging, filtering out
818
- middleware and focusing on the most useful frames.
819
-
820
- Returns:
821
- Dictionary with call stack analysis
822
- """
823
- import traceback
824
- import os
825
-
826
- try:
827
- # Get the current stack
828
- stack = traceback.extract_stack()
829
-
830
- # Common middleware/decorator patterns to filter out
831
- middleware_patterns = [
832
- 'decorator',
833
- 'wrapper',
834
- 'new_function',
835
- 'exec_function',
836
- '_execute',
837
- 'process_error',
838
- '_create_exception',
839
- '_format_human_readable',
840
- '__enter__',
841
- '__exit__',
842
- 'contextlib',
843
- 'functools'
844
- ]
845
-
846
- # Lambda/AWS specific patterns
847
- lambda_patterns = [
848
- 'lambda_handler',
849
- 'handler',
850
- 'bootstrap',
851
- 'runtime'
852
- ]
853
-
854
- relevant_calls = []
855
- top_level_call = None
856
- lambda_context = {}
857
-
858
- # Analyze stack frames from top to bottom (most recent first)
859
- for frame in reversed(stack[:-4]): # Skip the last few frames (this method, etc.)
860
- file_path = frame.filename
861
- function_name = frame.name
862
- line_number = frame.lineno
863
- code_line = frame.line or ""
864
-
865
- # Extract just the filename
866
- filename = os.path.basename(file_path)
867
-
868
- # Skip internal Python and library frames
869
- if any(skip in file_path.lower() for skip in [
870
- 'python3', 'site-packages', '/usr/', '/opt/python',
871
- 'psycopg2', 'boto3', 'botocore'
872
- ]):
873
- continue
874
-
875
- # Capture Lambda context if found
876
- if any(pattern in function_name.lower() for pattern in lambda_patterns):
877
- lambda_context.update({
878
- 'handler': function_name,
879
- 'file': filename,
880
- 'line': line_number
881
- })
882
-
883
- # Skip middleware/decorator frames but keep track of meaningful ones
884
- is_middleware = any(pattern in function_name.lower() for pattern in middleware_patterns)
885
-
886
- if not is_middleware:
887
- call_info = {
888
- 'function': function_name,
889
- 'file': filename,
890
- 'line': line_number,
891
- 'code': code_line,
892
- 'full_path': file_path
893
- }
894
-
895
- relevant_calls.append(call_info)
896
-
897
- # The first non-middleware call is likely the most important
898
- if not top_level_call:
899
- # Look for application-level calls (not in velocity internals)
900
- if not any(internal in file_path for internal in [
901
- 'velocity/db/', 'velocity/aws/', 'velocity/misc/'
902
- ]):
903
- top_level_call = call_info
904
-
905
- # If no clear top-level call found, use the first relevant call
906
- if not top_level_call and relevant_calls:
907
- top_level_call = relevant_calls[0]
908
-
909
- # Look for handler functions in the stack
910
- handler_calls = [call for call in relevant_calls
911
- if any(pattern in call['function'].lower()
912
- for pattern in ['handler', 'main', 'process', 'action'])]
913
-
914
- if handler_calls and not top_level_call:
915
- top_level_call = handler_calls[0]
916
-
917
- return {
918
- 'top_level_call': top_level_call,
919
- 'relevant_calls': relevant_calls,
920
- 'lambda_context': lambda_context if lambda_context else None
921
- }
922
-
923
- except Exception:
924
- # If stack analysis fails, return minimal info
925
- return None
926
-
927
- def _extract_error_details(self, exception, message):
928
- """
929
- Extract specific details from database errors for better formatting.
930
-
931
- Args:
932
- exception: The original database exception
933
- message: The error message string
934
-
935
- Returns:
936
- Dictionary with extracted details
937
- """
938
- import re
939
-
940
- details = {}
941
-
942
- # PostgreSQL specific error parsing
943
- if hasattr(exception, 'pgcode'):
944
- # Column does not exist
945
- if 'column' in message.lower() and 'does not exist' in message.lower():
946
- match = re.search(r'column "([^"]+)" does not exist', message, re.IGNORECASE)
947
- if match:
948
- details['column'] = match.group(1)
949
- details['description'] = f'The column "{match.group(1)}" was not found in the table.'
950
-
951
- # Table does not exist
952
- elif 'relation' in message.lower() and 'does not exist' in message.lower():
953
- match = re.search(r'relation "([^"]+)" does not exist', message, re.IGNORECASE)
954
- if match:
955
- details['table'] = match.group(1)
956
- details['description'] = f'The table "{match.group(1)}" was not found in the database.'
957
-
958
- # Foreign key violation
959
- elif 'foreign key constraint' in message.lower():
960
- match = re.search(r'violates foreign key constraint "([^"]+)"', message, re.IGNORECASE)
961
- if match:
962
- details['constraint'] = match.group(1)
963
- details['description'] = 'A foreign key constraint was violated.'
964
- details['hint'] = 'Make sure the referenced record exists.'
965
-
966
- # Unique constraint violation
967
- elif 'unique constraint' in message.lower() or 'duplicate key' in message.lower():
968
- match = re.search(r'violates unique constraint "([^"]+)"', message, re.IGNORECASE)
969
- if match:
970
- details['constraint'] = match.group(1)
971
- details['description'] = 'A unique constraint was violated (duplicate key).'
972
- details['hint'] = 'The value you are trying to insert already exists.'
973
-
974
- # Connection errors
975
- elif any(term in message.lower() for term in ['connection', 'connect', 'server']):
976
- details['description'] = 'Failed to connect to the database server.'
977
- details['hint'] = 'Check your database connection settings and network connectivity.'
978
-
979
- # Data type errors
980
- elif 'invalid input syntax' in message.lower():
981
- details['description'] = 'Invalid data format provided.'
982
- details['hint'] = 'Check that your data matches the expected format for the column type.'
983
-
984
- return details
985
-
986
- def _create_exception_with_chaining(self, error_class, message, original_exception, sql=None, parameters=None, format_type=None):
987
- """
988
- Create a velocity exception with proper exception chaining and human-readable formatting.
989
-
990
- Args:
991
- error_class: The name of the exception class to create
992
- message: The error message
993
- original_exception: The original exception to chain
994
- sql: The SQL statement (optional)
995
- parameters: The SQL parameters (optional)
996
- format_type: 'console', 'email', or None (auto-detect)
997
-
998
- Returns:
999
- The created exception with proper chaining and formatting
1000
- """
1001
- logger = logging.getLogger(__name__)
1002
-
1003
- try:
1004
- # Import the exception class dynamically
1005
- exception_module = __import__('velocity.db.exceptions', fromlist=[error_class])
1006
- ExceptionClass = getattr(exception_module, error_class)
1007
-
1008
- # Auto-detect format if not specified
1009
- if format_type is None:
1010
- format_type = self._detect_output_format()
1011
-
1012
- # Create human-readable, formatted message
1013
- formatted_message = self._format_human_readable_error(
1014
- error_class, message, original_exception, sql, parameters, format_type
1015
- )
1016
-
1017
- # For email format, also create a console version for logging
1018
- if format_type == 'email':
1019
- console_message = self._format_human_readable_error(
1020
- error_class, message, original_exception, sql, parameters, 'console'
1021
- )
1022
- # Log the console version for server logs
1023
- logger.error(f"Database Error (Console Format):\n{console_message}")
1024
-
1025
- # Create custom exception with both formats
1026
- new_exception = ExceptionClass(formatted_message)
1027
- new_exception.console_format = console_message
1028
- new_exception.email_format = formatted_message
1029
- else:
1030
- new_exception = ExceptionClass(formatted_message)
1031
-
1032
- # Only set __cause__ if original_exception is not None and derives from BaseException
1033
- if isinstance(original_exception, BaseException):
1034
- new_exception.__cause__ = original_exception # Preserve exception chain
1035
-
1036
- return new_exception
1037
-
1038
- except (ImportError, AttributeError) as e:
1039
- logger.error(f"Could not import exception class {error_class}: {e}")
1040
- # Fallback to generic database error
1041
- try:
1042
- exception_module = __import__('velocity.db.exceptions', fromlist=['DatabaseError'])
1043
- DatabaseError = getattr(exception_module, 'DatabaseError')
1044
-
1045
- # Auto-detect format if not specified for fallback too
1046
- if format_type is None:
1047
- format_type = self._detect_output_format()
1048
-
1049
- # Still format the fallback nicely
1050
- formatted_message = self._format_human_readable_error(
1051
- 'DatabaseError', message, original_exception, sql, parameters, format_type
1052
- )
1053
-
1054
- fallback_exception = DatabaseError(formatted_message)
1055
- # Only set __cause__ if original_exception is not None and derives from BaseException
1056
- if isinstance(original_exception, BaseException):
1057
- fallback_exception.__cause__ = original_exception
1058
- return fallback_exception
1059
- except Exception as fallback_error:
1060
- logger.critical(f"Failed to create fallback exception: {fallback_error}")
1061
- # Last resort: return the original exception
1062
- return original_exception
1063
-
1064
- def _detect_output_format(self):
1065
- """
1066
- Detect whether we should use console or email formatting based on context.
1067
-
1068
- Returns:
1069
- 'email' if in email/notification context, 'console' otherwise
1070
- """
1071
- import inspect
1072
-
1073
- # Look at the call stack for email-related functions
1074
- stack = inspect.stack()
1075
-
1076
- email_indicators = [
1077
- 'email', 'mail', 'notification', 'alert', 'send', 'notify',
1078
- 'smtp', 'message', 'recipient', 'subject', 'body'
1079
- ]
1080
-
1081
- for frame_info in stack:
1082
- function_name = frame_info.function.lower()
1083
- filename = frame_info.filename.lower()
1084
-
1085
- # Check if we're in an email-related context
1086
- if any(indicator in function_name or indicator in filename
1087
- for indicator in email_indicators):
1088
- return 'email'
1089
-
1090
- # Default to console format
1091
- return 'console'
479
+ except Exception as e:
480
+ # If formatting fails, return original SQL with parameters shown separately
481
+ return f"{sql}\n-- Parameters (formatting failed): {parameters}\n-- Error: {e}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.0.110
3
+ Version: 0.0.112
4
4
  Summary: A rapid application development library for interfacing with data storage
5
5
  Author-email: Velocity Team <info@codeclubs.org>
6
6
  License-Expression: MIT
@@ -1,4 +1,4 @@
1
- velocity/__init__.py,sha256=5Na4DTc1X43viY7Pm3xBOpXkedLfKb2sDq7Y3pK6Z8M,107
1
+ velocity/__init__.py,sha256=iH2m31ZNvp-oPV3zuzJRMhEbgHm_d-Gs5k6ljPudBlg,107
2
2
  velocity/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  velocity/app/invoices.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  velocity/app/orders.py,sha256=W-HAXEwY8-IFXbKh82HnMeRVZM7P-TWGEQOWtkLIzI4,6298
@@ -18,7 +18,7 @@ velocity/db/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
18
18
  velocity/db/core/column.py,sha256=tAr8tL3a2nyaYpNHhGl508FrY_pGZTzyYgjAV5CEBv4,4092
19
19
  velocity/db/core/database.py,sha256=3zNGItklu9tZCKsbx2T2vCcU1so8AL9PPL0DLjvaz6s,3554
20
20
  velocity/db/core/decorators.py,sha256=76Jkr9XptXt8cvcgp1zbHfuL8uHzWy8lwfR29u-DVu4,4574
21
- velocity/db/core/engine.py,sha256=ykF-UjYlDqDgD_KDLp7kiot-TEOrh5BifnPLyQEewgo,47549
21
+ velocity/db/core/engine.py,sha256=Yy6uwxT44a4oEHrFF312ChzFO01cR2W79BTryFgTe-s,18635
22
22
  velocity/db/core/exceptions.py,sha256=tuDniRqTX8Opc2d033LPJOI3Ux4NSwUcHqW729n-HXA,1027
23
23
  velocity/db/core/result.py,sha256=dgiOXH-iJXuDH4PbSTWVkn-heAkJQcXCC-gs0ZuqF94,12814
24
24
  velocity/db/core/row.py,sha256=zZ3zZbWjZkZfYAYuZJLHFJ8jdXc7dYv8Iyv9Ut8W8tE,7261
@@ -49,8 +49,8 @@ velocity/misc/tools.py,sha256=_bGneHHA_BV-kUonzw5H3hdJ5AOJRCKfzhgpkFbGqIo,1502
49
49
  velocity/misc/conv/__init__.py,sha256=MLYF58QHjzfDSxb1rdnmLnuEQCa3gnhzzZ30CwZVvQo,40
50
50
  velocity/misc/conv/iconv.py,sha256=d4_BucW8HTIkGNurJ7GWrtuptqUf-9t79ObzjJ5N76U,10603
51
51
  velocity/misc/conv/oconv.py,sha256=h5Lo05DqOQnxoD3y6Px_MQP_V-pBbWf8Hkgkb9Xp1jk,6032
52
- velocity_python-0.0.110.dist-info/licenses/LICENSE,sha256=aoN245GG8s9oRUU89KNiGTU4_4OtnNmVi4hQeChg6rM,1076
53
- velocity_python-0.0.110.dist-info/METADATA,sha256=KSop21_ZqWqh1EZNPMcOge75C9POxKMVG5ZQhg6NrrM,34262
54
- velocity_python-0.0.110.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
55
- velocity_python-0.0.110.dist-info/top_level.txt,sha256=JW2vJPmodgdgSz7H6yoZvnxF8S3fTMIv-YJWCT1sNW0,9
56
- velocity_python-0.0.110.dist-info/RECORD,,
52
+ velocity_python-0.0.112.dist-info/licenses/LICENSE,sha256=aoN245GG8s9oRUU89KNiGTU4_4OtnNmVi4hQeChg6rM,1076
53
+ velocity_python-0.0.112.dist-info/METADATA,sha256=LEPElQOJU4yBn0Zwdw4VzpLRNhC4M6MM1_6Qa28VBDU,34262
54
+ velocity_python-0.0.112.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
55
+ velocity_python-0.0.112.dist-info/top_level.txt,sha256=JW2vJPmodgdgSz7H6yoZvnxF8S3fTMIv-YJWCT1sNW0,9
56
+ velocity_python-0.0.112.dist-info/RECORD,,