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 +1 -1
- velocity/db/core/engine.py +64 -674
- {velocity_python-0.0.110.dist-info → velocity_python-0.0.112.dist-info}/METADATA +1 -1
- {velocity_python-0.0.110.dist-info → velocity_python-0.0.112.dist-info}/RECORD +7 -7
- {velocity_python-0.0.110.dist-info → velocity_python-0.0.112.dist-info}/WHEEL +0 -0
- {velocity_python-0.0.110.dist-info → velocity_python-0.0.112.dist-info}/licenses/LICENSE +0 -0
- {velocity_python-0.0.110.dist-info → velocity_python-0.0.112.dist-info}/top_level.txt +0 -0
velocity/__init__.py
CHANGED
velocity/db/core/engine.py
CHANGED
|
@@ -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(
|
|
359
|
+
raise exceptions.DbApplicationError(enhanced_message) from exception
|
|
355
360
|
if error_code in self.sql.ColumnMissingErrorCodes:
|
|
356
|
-
raise exceptions.DbColumnMissingError(
|
|
361
|
+
raise exceptions.DbColumnMissingError(enhanced_message) from exception
|
|
357
362
|
if error_code in self.sql.TableMissingErrorCodes:
|
|
358
|
-
raise exceptions.DbTableMissingError(
|
|
363
|
+
raise exceptions.DbTableMissingError(enhanced_message) from exception
|
|
359
364
|
if error_code in self.sql.DatabaseMissingErrorCodes:
|
|
360
|
-
raise exceptions.DbDatabaseMissingError(
|
|
365
|
+
raise exceptions.DbDatabaseMissingError(enhanced_message) from exception
|
|
361
366
|
if error_code in self.sql.ForeignKeyMissingErrorCodes:
|
|
362
|
-
raise exceptions.DbForeignKeyMissingError(
|
|
367
|
+
raise exceptions.DbForeignKeyMissingError(enhanced_message) from exception
|
|
363
368
|
if error_code in self.sql.TruncationErrorCodes:
|
|
364
|
-
raise exceptions.DbTruncationError(
|
|
369
|
+
raise exceptions.DbTruncationError(enhanced_message) from exception
|
|
365
370
|
if error_code in self.sql.DataIntegrityErrorCodes:
|
|
366
|
-
raise exceptions.DbDataIntegrityError(
|
|
371
|
+
raise exceptions.DbDataIntegrityError(enhanced_message) from exception
|
|
367
372
|
if error_code in self.sql.ConnectionErrorCodes:
|
|
368
|
-
raise exceptions.DbConnectionError(
|
|
373
|
+
raise exceptions.DbConnectionError(enhanced_message) from exception
|
|
369
374
|
if error_code in self.sql.DuplicateKeyErrorCodes:
|
|
370
|
-
raise exceptions.DbDuplicateKeyError(
|
|
375
|
+
raise exceptions.DbDuplicateKeyError(enhanced_message) from exception
|
|
371
376
|
if error_code in self.sql.DatabaseObjectExistsErrorCodes:
|
|
372
|
-
raise exceptions.DbObjectExistsError(
|
|
377
|
+
raise exceptions.DbObjectExistsError(enhanced_message) from exception
|
|
373
378
|
if error_code in self.sql.LockTimeoutErrorCodes:
|
|
374
|
-
raise exceptions.DbLockTimeoutError(
|
|
379
|
+
raise exceptions.DbLockTimeoutError(enhanced_message) from exception
|
|
375
380
|
if error_code in self.sql.RetryTransactionCodes:
|
|
376
|
-
raise exceptions.DbRetryTransaction(
|
|
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(
|
|
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(
|
|
387
|
+
raise exceptions.DbDatabaseMissingError(enhanced_message) from exception
|
|
383
388
|
if re.findall(r"no such database", msg, re.M):
|
|
384
|
-
raise exceptions.DbDatabaseMissingError(
|
|
389
|
+
raise exceptions.DbDatabaseMissingError(enhanced_message) from exception
|
|
385
390
|
if re.findall(r"already exists", msg, re.M):
|
|
386
|
-
raise exceptions.DbObjectExistsError(
|
|
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(
|
|
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(
|
|
395
|
+
raise exceptions.DbConnectionError(enhanced_message) from exception
|
|
391
396
|
if re.findall(r"connection timed out", msg, re.M):
|
|
392
|
-
raise exceptions.DbConnectionError(
|
|
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(
|
|
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(
|
|
401
|
+
raise exceptions.DbConnectionError(enhanced_message) from exception
|
|
397
402
|
if re.findall(r"connection already closed", msg, re.M):
|
|
398
|
-
raise exceptions.DbConnectionError(
|
|
403
|
+
raise exceptions.DbConnectionError(enhanced_message) from exception
|
|
399
404
|
if re.findall(r"cursor already closed", msg, re.M):
|
|
400
|
-
raise exceptions.DbConnectionError(
|
|
405
|
+
raise exceptions.DbConnectionError(enhanced_message) from exception
|
|
401
406
|
if "no such table:" in msg:
|
|
402
|
-
raise exceptions.DbTableMissingError(
|
|
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
|
|
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
|
|
423
|
+
def _format_sql_with_params(self, sql, parameters):
|
|
593
424
|
"""
|
|
594
|
-
Format
|
|
425
|
+
Format SQL query with parameters merged for easy copy-paste debugging.
|
|
595
426
|
"""
|
|
596
|
-
|
|
597
|
-
|
|
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('&', '&')
|
|
742
|
-
.replace('<', '<')
|
|
743
|
-
.replace('>', '>')
|
|
744
|
-
.replace('"', '"')
|
|
745
|
-
.replace("'", '''))
|
|
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
|
-
#
|
|
765
|
-
|
|
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
|
|
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
|
-
|
|
772
|
-
|
|
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
|
-
|
|
446
|
+
formatted_params.append('TRUE' if param else 'FALSE')
|
|
778
447
|
else:
|
|
779
|
-
|
|
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
|
-
#
|
|
458
|
+
# Handle named parameters
|
|
789
459
|
formatted_sql = sql
|
|
790
460
|
for key, value in parameters.items():
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
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
|
-
|
|
467
|
+
replacement = 'TRUE' if value else 'FALSE'
|
|
800
468
|
else:
|
|
801
|
-
|
|
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
|
|
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,4 +1,4 @@
|
|
|
1
|
-
velocity/__init__.py,sha256=
|
|
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=
|
|
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.
|
|
53
|
-
velocity_python-0.0.
|
|
54
|
-
velocity_python-0.0.
|
|
55
|
-
velocity_python-0.0.
|
|
56
|
-
velocity_python-0.0.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|