pyplaykit 1.4.0__tar.gz → 1.5.0__tar.gz

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.
Files changed (91) hide show
  1. {pyplaykit-1.4.0/pyplaykit.egg-info → pyplaykit-1.5.0}/PKG-INFO +39 -2
  2. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/README.md +38 -1
  3. {pyplaykit-1.4.0 → pyplaykit-1.5.0/pyplaykit.egg-info}/PKG-INFO +39 -2
  4. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/pyplaykit.egg-info/SOURCES.txt +1 -0
  5. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/pyproject.toml +1 -1
  6. pyplaykit-1.5.0/utils/__init__.py +5 -0
  7. pyplaykit-1.5.0/utils/data_comparison_report.py +487 -0
  8. pyplaykit-1.4.0/utils/__init__.py +0 -0
  9. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/MANIFEST.in +0 -0
  10. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/components/__init__.py +0 -0
  11. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/components/base_component.py +0 -0
  12. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/components/data_table.py +0 -0
  13. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/components/form_field.py +0 -0
  14. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/components/modal.py +0 -0
  15. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/config/config.yaml +0 -0
  16. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/config/environments.yaml +0 -0
  17. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/config/projects/customs_modernization.yaml +0 -0
  18. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/config/projects/dispatcho.yaml +0 -0
  19. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/config/projects/timelyquote.yaml +0 -0
  20. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/core/__init__.py +0 -0
  21. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/core/base_page.py +0 -0
  22. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/core/base_test.py +0 -0
  23. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/core/browser_factory.py +0 -0
  24. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/core/playwright_manager.py +0 -0
  25. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/dist_pkg/__init__.py +0 -0
  26. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/dist_pkg/distribution.py +0 -0
  27. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/integrations/__init__.py +0 -0
  28. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/integrations/adapters.py +0 -0
  29. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/orchestration/__init__.py +0 -0
  30. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/orchestration/enforcer.py +0 -0
  31. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/orchestration/planner.py +0 -0
  32. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/pages/__init__.py +0 -0
  33. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/pages/dashboard_page.py +0 -0
  34. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/pages/login_page.py +0 -0
  35. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/pages/orangehrm_login_page.py +0 -0
  36. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/pages/saucedemo_inventory_page.py +0 -0
  37. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/pages/saucedemo_login_page.py +0 -0
  38. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/plugins/__init__.py +0 -0
  39. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/plugins/api_plugin.py +0 -0
  40. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/plugins/base.py +0 -0
  41. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/plugins/data_plugin.py +0 -0
  42. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/plugins/registry.py +0 -0
  43. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/plugins/security_plugin.py +0 -0
  44. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/pyplaykit/__init__.py +0 -0
  45. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/pyplaykit/cli.py +0 -0
  46. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/pyplaykit/plugin.py +0 -0
  47. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/pyplaykit.egg-info/dependency_links.txt +0 -0
  48. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/pyplaykit.egg-info/entry_points.txt +0 -0
  49. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/pyplaykit.egg-info/requires.txt +0 -0
  50. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/pyplaykit.egg-info/top_level.txt +0 -0
  51. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/resilience/__init__.py +0 -0
  52. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/resilience/locator_recovery.py +0 -0
  53. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/setup.cfg +0 -0
  54. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/test_data/api/ecom/auth_payloads.json +0 -0
  55. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/test_data/api/ecom/product_payloads.json +0 -0
  56. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/test_data/api_test_data.json +0 -0
  57. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/test_data/data_validation_test_data.json +0 -0
  58. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/test_data/login_test_data.json +0 -0
  59. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/test_data/orangehrm_login_data.json +0 -0
  60. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/test_data/ui_test_data.json +0 -0
  61. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/utils/api_client.py +0 -0
  62. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/utils/api_test_helper.py +0 -0
  63. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/utils/assertion_helper.py +0 -0
  64. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/utils/auth_manager.py +0 -0
  65. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/utils/data_loader.py +0 -0
  66. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/utils/data_source.py +0 -0
  67. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/utils/data_validator.py +0 -0
  68. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/utils/db_client.py +0 -0
  69. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/utils/db_drivers/__init__.py +0 -0
  70. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/utils/db_drivers/db2.py +0 -0
  71. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/utils/db_drivers/mysql.py +0 -0
  72. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/utils/db_drivers/oracle.py +0 -0
  73. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/utils/db_drivers/snowflake.py +0 -0
  74. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/utils/db_drivers/sqlite.py +0 -0
  75. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/utils/db_drivers/sqlserver.py +0 -0
  76. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/utils/db_factory.py +0 -0
  77. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/utils/db_fixtures.py +0 -0
  78. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/utils/db_pool.py +0 -0
  79. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/utils/db_validation_helper.py +0 -0
  80. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/utils/environment_validator.py +0 -0
  81. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/utils/file_reader.py +0 -0
  82. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/utils/graphql_client.py +0 -0
  83. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/utils/logger.py +0 -0
  84. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/utils/observability.py +0 -0
  85. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/utils/response_validator.py +0 -0
  86. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/utils/screenshot_helper.py +0 -0
  87. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/utils/soap_client.py +0 -0
  88. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/utils/tdm.py +0 -0
  89. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/utils/transformation_validator.py +0 -0
  90. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/utils/ui_db_validator.py +0 -0
  91. {pyplaykit-1.4.0 → pyplaykit-1.5.0}/utils/yaml_config_reader.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyplaykit
3
- Version: 1.4.0
3
+ Version: 1.5.0
4
4
  Summary: Enterprise Python + Playwright automation framework
5
5
  Author-email: Shan Konduru <shankonduru@gmail.com>
6
6
  License: MIT
@@ -49,10 +49,11 @@ PyPlayKit is a **multi-project enterprise test automation framework** built with
49
49
  - 👥 **Scalable**: Designed for 50-100 QE engineers working simultaneously
50
50
  - 🎯 **Zero Merge Conflicts**: Project-first organization eliminates conflicts
51
51
  - 📊 **Data Comparison**: File-to-File, File-to-DB, DB-to-File validation (Excel, CSV, JSON)
52
+ - 📈 **Interactive HTML Reports**: Filterable validation reports with KPIs ⭐ **NEW**
52
53
  - 📚 **Comprehensive Documentation**: 8,700+ lines across 15+ guides
53
54
  - 🔧 **Extensible**: Plugin architecture with orchestration support
54
55
  - 🔍 **Self-Healing**: Locator recovery with fallback chains
55
- - 📈 **Observable**: Comprehensive metrics and reporting
56
+ - 🔭 **Observable**: Comprehensive metrics and reporting
56
57
 
57
58
  ---
58
59
 
@@ -509,6 +510,42 @@ pytest examples/data_comparison_examples.py -v -s
509
510
  pytest examples/data_comparison_examples.py::test_excel_to_excel_basic -v -s
510
511
  ```
511
512
 
513
+ ### Interactive HTML Reports ⭐ **NEW**
514
+
515
+ Generate filterable, searchable HTML validation reports:
516
+
517
+ ```python
518
+ from utils.data_comparison_report import DataComparisonReport
519
+
520
+ # Initialize report
521
+ report = DataComparisonReport()
522
+ report.set_comparison_type("FILE_TO_FILE (Excel)")
523
+ report.set_source("baseline.xlsx", 100)
524
+ report.set_target("current.xlsx", 100)
525
+
526
+ # Add mismatch
527
+ report.add_mismatch(
528
+ mismatch_type="VALUE_MISMATCH",
529
+ row_index=5,
530
+ mismatching_fields=["Price"],
531
+ source_value=99.99,
532
+ target_value=100.00,
533
+ reason="Price increased by $0.01"
534
+ )
535
+
536
+ # Generate interactive HTML report
537
+ report.generate_report("reports/data_validation/report.html")
538
+ ```
539
+
540
+ **Report Features:**
541
+ - 📊 KPI dashboard with pass/fail rates
542
+ - 🔍 Column-level filtering and search
543
+ - 📋 Row-by-row mismatch details
544
+ - 🎨 Color-coded status indicators
545
+ - 📱 Mobile-responsive design
546
+
547
+ See **[examples/data_comparison_with_html_report.py](examples/data_comparison_with_html_report.py)** for complete examples.
548
+
512
549
  ### Full Documentation
513
550
 
514
551
  - **[docs/PIP_INSTALL_GUIDE.md](docs/PIP_INSTALL_GUIDE.md#step-8--data-comparison-patterns)** - Complete patterns guide
@@ -13,10 +13,11 @@ PyPlayKit is a **multi-project enterprise test automation framework** built with
13
13
  - 👥 **Scalable**: Designed for 50-100 QE engineers working simultaneously
14
14
  - 🎯 **Zero Merge Conflicts**: Project-first organization eliminates conflicts
15
15
  - 📊 **Data Comparison**: File-to-File, File-to-DB, DB-to-File validation (Excel, CSV, JSON)
16
+ - 📈 **Interactive HTML Reports**: Filterable validation reports with KPIs ⭐ **NEW**
16
17
  - 📚 **Comprehensive Documentation**: 8,700+ lines across 15+ guides
17
18
  - 🔧 **Extensible**: Plugin architecture with orchestration support
18
19
  - 🔍 **Self-Healing**: Locator recovery with fallback chains
19
- - 📈 **Observable**: Comprehensive metrics and reporting
20
+ - 🔭 **Observable**: Comprehensive metrics and reporting
20
21
 
21
22
  ---
22
23
 
@@ -473,6 +474,42 @@ pytest examples/data_comparison_examples.py -v -s
473
474
  pytest examples/data_comparison_examples.py::test_excel_to_excel_basic -v -s
474
475
  ```
475
476
 
477
+ ### Interactive HTML Reports ⭐ **NEW**
478
+
479
+ Generate filterable, searchable HTML validation reports:
480
+
481
+ ```python
482
+ from utils.data_comparison_report import DataComparisonReport
483
+
484
+ # Initialize report
485
+ report = DataComparisonReport()
486
+ report.set_comparison_type("FILE_TO_FILE (Excel)")
487
+ report.set_source("baseline.xlsx", 100)
488
+ report.set_target("current.xlsx", 100)
489
+
490
+ # Add mismatch
491
+ report.add_mismatch(
492
+ mismatch_type="VALUE_MISMATCH",
493
+ row_index=5,
494
+ mismatching_fields=["Price"],
495
+ source_value=99.99,
496
+ target_value=100.00,
497
+ reason="Price increased by $0.01"
498
+ )
499
+
500
+ # Generate interactive HTML report
501
+ report.generate_report("reports/data_validation/report.html")
502
+ ```
503
+
504
+ **Report Features:**
505
+ - 📊 KPI dashboard with pass/fail rates
506
+ - 🔍 Column-level filtering and search
507
+ - 📋 Row-by-row mismatch details
508
+ - 🎨 Color-coded status indicators
509
+ - 📱 Mobile-responsive design
510
+
511
+ See **[examples/data_comparison_with_html_report.py](examples/data_comparison_with_html_report.py)** for complete examples.
512
+
476
513
  ### Full Documentation
477
514
 
478
515
  - **[docs/PIP_INSTALL_GUIDE.md](docs/PIP_INSTALL_GUIDE.md#step-8--data-comparison-patterns)** - Complete patterns guide
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyplaykit
3
- Version: 1.4.0
3
+ Version: 1.5.0
4
4
  Summary: Enterprise Python + Playwright automation framework
5
5
  Author-email: Shan Konduru <shankonduru@gmail.com>
6
6
  License: MIT
@@ -49,10 +49,11 @@ PyPlayKit is a **multi-project enterprise test automation framework** built with
49
49
  - 👥 **Scalable**: Designed for 50-100 QE engineers working simultaneously
50
50
  - 🎯 **Zero Merge Conflicts**: Project-first organization eliminates conflicts
51
51
  - 📊 **Data Comparison**: File-to-File, File-to-DB, DB-to-File validation (Excel, CSV, JSON)
52
+ - 📈 **Interactive HTML Reports**: Filterable validation reports with KPIs ⭐ **NEW**
52
53
  - 📚 **Comprehensive Documentation**: 8,700+ lines across 15+ guides
53
54
  - 🔧 **Extensible**: Plugin architecture with orchestration support
54
55
  - 🔍 **Self-Healing**: Locator recovery with fallback chains
55
- - 📈 **Observable**: Comprehensive metrics and reporting
56
+ - 🔭 **Observable**: Comprehensive metrics and reporting
56
57
 
57
58
  ---
58
59
 
@@ -509,6 +510,42 @@ pytest examples/data_comparison_examples.py -v -s
509
510
  pytest examples/data_comparison_examples.py::test_excel_to_excel_basic -v -s
510
511
  ```
511
512
 
513
+ ### Interactive HTML Reports ⭐ **NEW**
514
+
515
+ Generate filterable, searchable HTML validation reports:
516
+
517
+ ```python
518
+ from utils.data_comparison_report import DataComparisonReport
519
+
520
+ # Initialize report
521
+ report = DataComparisonReport()
522
+ report.set_comparison_type("FILE_TO_FILE (Excel)")
523
+ report.set_source("baseline.xlsx", 100)
524
+ report.set_target("current.xlsx", 100)
525
+
526
+ # Add mismatch
527
+ report.add_mismatch(
528
+ mismatch_type="VALUE_MISMATCH",
529
+ row_index=5,
530
+ mismatching_fields=["Price"],
531
+ source_value=99.99,
532
+ target_value=100.00,
533
+ reason="Price increased by $0.01"
534
+ )
535
+
536
+ # Generate interactive HTML report
537
+ report.generate_report("reports/data_validation/report.html")
538
+ ```
539
+
540
+ **Report Features:**
541
+ - 📊 KPI dashboard with pass/fail rates
542
+ - 🔍 Column-level filtering and search
543
+ - 📋 Row-by-row mismatch details
544
+ - 🎨 Color-coded status indicators
545
+ - 📱 Mobile-responsive design
546
+
547
+ See **[examples/data_comparison_with_html_report.py](examples/data_comparison_with_html_report.py)** for complete examples.
548
+
512
549
  ### Full Documentation
513
550
 
514
551
  - **[docs/PIP_INSTALL_GUIDE.md](docs/PIP_INSTALL_GUIDE.md#step-8--data-comparison-patterns)** - Complete patterns guide
@@ -58,6 +58,7 @@ utils/api_client.py
58
58
  utils/api_test_helper.py
59
59
  utils/assertion_helper.py
60
60
  utils/auth_manager.py
61
+ utils/data_comparison_report.py
61
62
  utils/data_loader.py
62
63
  utils/data_source.py
63
64
  utils/data_validator.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyplaykit"
7
- version = "1.4.0"
7
+ version = "1.5.0"
8
8
  description = "Enterprise Python + Playwright automation framework"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -0,0 +1,5 @@
1
+ """Utils package for PyPlayKit framework."""
2
+
3
+ from utils.data_comparison_report import DataComparisonReport
4
+
5
+ __all__ = ["DataComparisonReport"]
@@ -0,0 +1,487 @@
1
+ """
2
+ HTML/CSS Data Comparison Report Generator for PyPlayKit.
3
+
4
+ Generates interactive, filterable HTML reports for data validation scenarios:
5
+ - File-to-File comparison (Excel, CSV, JSON)
6
+ - File-to-Database comparison
7
+ - Database-to-File comparison
8
+
9
+ Features:
10
+ - Overall validation status with KPIs
11
+ - Validation summary section
12
+ - Filterable/searchable discrepancy table
13
+ - Row-by-row mismatch details
14
+ - Field-level difference highlighting
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ from datetime import datetime
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+
25
+ class DataComparisonReport:
26
+ """Generate interactive HTML reports for data validation results."""
27
+
28
+ def __init__(self):
29
+ self.comparison_type = "UNKNOWN"
30
+ self.source_name = ""
31
+ self.target_name = ""
32
+ self.source_count = 0
33
+ self.target_count = 0
34
+ self.passed_count = 0
35
+ self.failed_count = 0
36
+ self.source_only_count = 0
37
+ self.target_only_count = 0
38
+ self.mismatches: list[dict[str, Any]] = []
39
+ self.validation_summary = ""
40
+ self.metadata: dict[str, str] = {}
41
+
42
+ def set_comparison_type(self, comp_type: str) -> None:
43
+ """Set the comparison type (FILE_TO_FILE, FILE_TO_DB, DB_TO_FILE)."""
44
+ self.comparison_type = comp_type
45
+
46
+ def set_source(self, name: str, count: int) -> None:
47
+ """Set source details."""
48
+ self.source_name = name
49
+ self.source_count = count
50
+
51
+ def set_target(self, name: str, count: int) -> None:
52
+ """Set target details."""
53
+ self.target_name = name
54
+ self.target_count = count
55
+
56
+ def set_counts(self, passed: int, failed: int, source_only: int = 0, target_only: int = 0) -> None:
57
+ """Set validation result counts."""
58
+ self.passed_count = passed
59
+ self.failed_count = failed
60
+ self.source_only_count = source_only
61
+ self.target_only_count = target_only
62
+
63
+ def add_mismatch(
64
+ self,
65
+ mismatch_type: str,
66
+ row_index: int | None,
67
+ mismatching_fields: list[str],
68
+ source_value: Any,
69
+ target_value: Any,
70
+ reason: str,
71
+ ) -> None:
72
+ """Add a single mismatch record."""
73
+ self.mismatches.append({
74
+ "type": mismatch_type,
75
+ "row": row_index,
76
+ "mismatching_fields": mismatching_fields,
77
+ "source_value": self._safe_str(source_value),
78
+ "target_value": self._safe_str(target_value),
79
+ "reason": reason,
80
+ })
81
+
82
+ def set_validation_summary(self, summary: str) -> None:
83
+ """Set the validation summary text."""
84
+ self.validation_summary = summary
85
+
86
+ def add_metadata(self, key: str, value: str) -> None:
87
+ """Add metadata key-value pair."""
88
+ self.metadata[key] = value
89
+
90
+ def generate_report(self, output_path: str | Path) -> None:
91
+ """Generate and save the HTML report."""
92
+ output_path = Path(output_path)
93
+ output_path.parent.mkdir(parents=True, exist_ok=True)
94
+
95
+ html_content = self._build_html()
96
+
97
+ with open(output_path, "w", encoding="utf-8") as f:
98
+ f.write(html_content)
99
+
100
+ def _safe_str(self, value: Any) -> str:
101
+ """Safely convert value to string."""
102
+ if value is None:
103
+ return "NULL"
104
+ if isinstance(value, (dict, list)):
105
+ return json.dumps(value, default=str)
106
+ return str(value)
107
+
108
+ def _calculate_pass_rate(self) -> float:
109
+ """Calculate pass rate percentage."""
110
+ total = self.passed_count + self.failed_count
111
+ if total == 0:
112
+ return 0.0
113
+ return (self.passed_count / total) * 100
114
+
115
+ def _calculate_fail_rate(self) -> float:
116
+ """Calculate fail rate percentage."""
117
+ total = self.passed_count + self.failed_count
118
+ if total == 0:
119
+ return 0.0
120
+ return (self.failed_count / total) * 100
121
+
122
+ def _get_overall_status(self) -> str:
123
+ """Determine overall validation status."""
124
+ if self.failed_count == 0 and self.source_only_count == 0 and self.target_only_count == 0:
125
+ return "PASSED"
126
+ return "FAILED"
127
+
128
+ def _build_html(self) -> str:
129
+ """Build complete HTML report."""
130
+ overall_status = self._get_overall_status()
131
+ pass_rate = self._calculate_pass_rate()
132
+ fail_rate = self._calculate_fail_rate()
133
+ generated_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
134
+
135
+ # Build mismatch table rows
136
+ mismatch_rows = ""
137
+ if not self.mismatches:
138
+ mismatch_rows = "<tr><td colspan='6' style='text-align:center;color:#15803d;font-weight:600;'>✓ No discrepancies found</td></tr>"
139
+ else:
140
+ for idx, mismatch in enumerate(self.mismatches, 1):
141
+ row_display = str(mismatch["row"]) if mismatch["row"] is not None else "N/A"
142
+ fields_display = ", ".join(mismatch["mismatching_fields"]) if mismatch["mismatching_fields"] else "N/A"
143
+
144
+ mismatch_rows += f"""
145
+ <tr>
146
+ <td>{self._escape_html(mismatch['type'])}</td>
147
+ <td>{row_display}</td>
148
+ <td>{self._escape_html(fields_display)}</td>
149
+ <td><div style="max-width:300px;overflow:auto;">{self._escape_html(mismatch['source_value'])}</div></td>
150
+ <td><div style="max-width:300px;overflow:auto;">{self._escape_html(mismatch['target_value'])}</div></td>
151
+ <td>{self._escape_html(mismatch['reason'])}</td>
152
+ </tr>"""
153
+
154
+ # Build metadata section
155
+ metadata_html = ""
156
+ for key, value in self.metadata.items():
157
+ metadata_html += f'<div><strong>{self._escape_html(key)}:</strong> {self._escape_html(value)}</div>\n '
158
+
159
+ # Status pill class
160
+ status_class = "status-pill-success" if overall_status == "PASSED" else "status-pill-danger"
161
+
162
+ return f"""<!doctype html>
163
+ <html lang="en">
164
+ <head>
165
+ <meta charset="utf-8" />
166
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
167
+ <title>PyPlayKit Data Validation Report</title>
168
+ <style>
169
+ :root {{
170
+ --bg: #f4f7fb;
171
+ --surface: #ffffff;
172
+ --line: #d9e2ec;
173
+ --text: #0f172a;
174
+ --muted: #475569;
175
+ --brand: #0b3b66;
176
+ }}
177
+
178
+ * {{ box-sizing: border-box; }}
179
+
180
+ body {{
181
+ margin: 0;
182
+ font-family: "Segoe UI", Tahoma, sans-serif;
183
+ color: var(--text);
184
+ background: radial-gradient(circle at top right, #d9ebff, var(--bg) 35%);
185
+ min-height: 100vh;
186
+ display: flex;
187
+ flex-direction: column;
188
+ }}
189
+
190
+ .page {{
191
+ max-width: 1400px;
192
+ width: 100%;
193
+ margin: 0 auto;
194
+ padding: 24px;
195
+ flex: 1;
196
+ }}
197
+
198
+ .brand-header {{
199
+ background: linear-gradient(130deg, var(--brand), #155a96);
200
+ color: #fff;
201
+ border-radius: 14px;
202
+ padding: 20px;
203
+ box-shadow: 0 8px 24px rgba(17, 24, 39, 0.12);
204
+ margin-bottom: 18px;
205
+ }}
206
+
207
+ .brand-title {{ margin: 0; font-size: 30px; letter-spacing: 0.4px; }}
208
+ .brand-sub {{ margin: 6px 0 0 0; opacity: 0.92; }}
209
+
210
+ .status-pill-success {{
211
+ display: inline-block;
212
+ margin-top: 14px;
213
+ padding: 8px 14px;
214
+ border-radius: 999px;
215
+ font-weight: 700;
216
+ color: #0f5132;
217
+ background: #dcfce7;
218
+ border: 1px solid #14532d;
219
+ }}
220
+
221
+ .status-pill-danger {{
222
+ display: inline-block;
223
+ margin-top: 14px;
224
+ padding: 8px 14px;
225
+ border-radius: 999px;
226
+ font-weight: 700;
227
+ color: #991b1b;
228
+ background: #fee2e2;
229
+ border: 1px solid #7f1d1d;
230
+ }}
231
+
232
+ .kpi-grid {{
233
+ display: grid;
234
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
235
+ gap: 12px;
236
+ margin-bottom: 16px;
237
+ }}
238
+
239
+ .kpi-card {{
240
+ background: var(--surface);
241
+ border: 1px solid var(--line);
242
+ border-radius: 12px;
243
+ padding: 12px 14px;
244
+ box-shadow: 0 4px 14px rgba(15, 23, 42, 0.05);
245
+ }}
246
+
247
+ .kpi-card.success {{
248
+ background: #f0fdf4;
249
+ border-color: #86efac;
250
+ box-shadow: 0 4px 14px rgba(34, 197, 94, 0.15);
251
+ }}
252
+
253
+ .kpi-card.danger {{
254
+ background: #fef2f2;
255
+ border-color: #fca5a5;
256
+ box-shadow: 0 4px 14px rgba(239, 68, 68, 0.15);
257
+ }}
258
+
259
+ .kpi-card.warning {{
260
+ background: #fffbeb;
261
+ border-color: #fcd34d;
262
+ box-shadow: 0 4px 14px rgba(245, 158, 11, 0.15);
263
+ }}
264
+
265
+ .kpi-label {{
266
+ color: var(--muted);
267
+ font-size: 12px;
268
+ text-transform: uppercase;
269
+ letter-spacing: 0.4px;
270
+ margin-bottom: 6px;
271
+ }}
272
+
273
+ .kpi-card.success .kpi-label {{ color: #15803d; }}
274
+ .kpi-card.danger .kpi-label {{ color: #991b1b; }}
275
+ .kpi-card.warning .kpi-label {{ color: #92400e; }}
276
+
277
+ .kpi-value {{ font-size: 24px; font-weight: 700; line-height: 1; }}
278
+
279
+ .kpi-card.success .kpi-value {{ color: #15803d; }}
280
+ .kpi-card.danger .kpi-value {{ color: #dc2626; }}
281
+ .kpi-card.warning .kpi-value {{ color: #d97706; }}
282
+
283
+ .card {{
284
+ background: var(--surface);
285
+ border: 1px solid var(--line);
286
+ border-radius: 12px;
287
+ padding: 16px;
288
+ margin-bottom: 16px;
289
+ }}
290
+
291
+ .meta-grid {{
292
+ display: grid;
293
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
294
+ gap: 8px 16px;
295
+ color: var(--muted);
296
+ }}
297
+
298
+ .table-wrap {{
299
+ width: 100%;
300
+ overflow-x: auto;
301
+ overflow-y: auto;
302
+ max-height: 60vh;
303
+ border: 1px solid var(--line);
304
+ border-radius: 10px;
305
+ }}
306
+
307
+ table {{
308
+ width: max-content;
309
+ min-width: 100%;
310
+ border-collapse: collapse;
311
+ font-size: 13px;
312
+ }}
313
+
314
+ th, td {{
315
+ border: 1px solid #cfd8e3;
316
+ padding: 8px;
317
+ text-align: left;
318
+ vertical-align: top;
319
+ }}
320
+
321
+ th {{
322
+ background: #eef4fb;
323
+ position: sticky;
324
+ top: 0;
325
+ z-index: 2;
326
+ font-weight: 600;
327
+ }}
328
+
329
+ .filters th {{
330
+ background: #f8fafc;
331
+ padding: 6px;
332
+ position: sticky;
333
+ top: 38px;
334
+ z-index: 1;
335
+ }}
336
+
337
+ .filters input {{
338
+ width: 100%;
339
+ border: 1px solid #cbd5e1;
340
+ border-radius: 6px;
341
+ padding: 6px 8px;
342
+ font-size: 12px;
343
+ }}
344
+
345
+ .filters input:focus {{
346
+ outline: none;
347
+ border-color: #3b82f6;
348
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
349
+ }}
350
+
351
+ .footer {{
352
+ background: #0b3b66;
353
+ color: #e2ecf7;
354
+ padding: 16px 24px;
355
+ text-align: center;
356
+ font-size: 14px;
357
+ }}
358
+
359
+ .footer a {{
360
+ color: #f8bf3b;
361
+ text-decoration: none;
362
+ font-weight: 600;
363
+ }}
364
+
365
+ .footer a:hover {{
366
+ text-decoration: underline;
367
+ }}
368
+
369
+ @media (max-width: 720px) {{
370
+ .brand-title {{ font-size: 24px; }}
371
+ .filters th {{ top: 74px; }}
372
+ .kpi-grid {{ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }}
373
+ }}
374
+ </style>
375
+ </head>
376
+ <body>
377
+ <main class="page">
378
+ <header class="brand-header">
379
+ <h1 class="brand-title">PyPlayKit Data Validation Report</h1>
380
+ <p class="brand-sub">Data Comparison & Validation | Automated Reconciliation</p>
381
+ <span class="{status_class}">{overall_status}</span>
382
+ </header>
383
+
384
+ <section class="kpi-grid">
385
+ <article class="kpi-card"><div class="kpi-label">Source Count</div><div class="kpi-value">{self.source_count}</div></article>
386
+ <article class="kpi-card"><div class="kpi-label">Target Count</div><div class="kpi-value">{self.target_count}</div></article>
387
+ <article class="kpi-card success"><div class="kpi-label">Passed</div><div class="kpi-value">{self.passed_count}</div></article>
388
+ <article class="kpi-card danger"><div class="kpi-label">Failed</div><div class="kpi-value">{self.failed_count}</div></article>
389
+ <article class="kpi-card success"><div class="kpi-label">Pass Rate</div><div class="kpi-value">{pass_rate:.2f}%</div></article>
390
+ <article class="kpi-card danger"><div class="kpi-label">Fail Rate</div><div class="kpi-value">{fail_rate:.2f}%</div></article>
391
+ <article class="kpi-card warning"><div class="kpi-label">Source Only</div><div class="kpi-value">{self.source_only_count}</div></article>
392
+ <article class="kpi-card warning"><div class="kpi-label">Target Only</div><div class="kpi-value">{self.target_only_count}</div></article>
393
+ </section>
394
+
395
+ <section class="card">
396
+ <div class="meta-grid">
397
+ <div><strong>Comparison Type:</strong> {self.comparison_type}</div>
398
+ <div><strong>Generated:</strong> {generated_timestamp}</div>
399
+ <div><strong>Source:</strong> {self._escape_html(self.source_name)}</div>
400
+ <div><strong>Target:</strong> {self._escape_html(self.target_name)}</div>
401
+ {metadata_html}
402
+ </div>
403
+ <p style="margin-top:12px;"><strong>Summary:</strong> {self._escape_html(self.validation_summary)}</p>
404
+ </section>
405
+
406
+ <section class="card">
407
+ <h2 style="margin-top:0;">Discrepancy Details</h2>
408
+ <p style="color: var(--muted); margin-top: -4px;">Use the filter boxes in each column to search and narrow results. Total discrepancies: <strong>{len(self.mismatches)}</strong></p>
409
+ <div class="table-wrap">
410
+ <table id="mismatchTable">
411
+ <thead>
412
+ <tr>
413
+ <th>Type</th>
414
+ <th>Row</th>
415
+ <th>Mismatching Fields</th>
416
+ <th>Source Value</th>
417
+ <th>Target Value</th>
418
+ <th>Reason</th>
419
+ </tr>
420
+ <tr class="filters">
421
+ <th><input type="text" oninput="filterTable(0, this.value)" placeholder="Filter type" /></th>
422
+ <th><input type="text" oninput="filterTable(1, this.value)" placeholder="Filter row" /></th>
423
+ <th><input type="text" oninput="filterTable(2, this.value)" placeholder="Filter fields" /></th>
424
+ <th><input type="text" oninput="filterTable(3, this.value)" placeholder="Filter source" /></th>
425
+ <th><input type="text" oninput="filterTable(4, this.value)" placeholder="Filter target" /></th>
426
+ <th><input type="text" oninput="filterTable(5, this.value)" placeholder="Filter reason" /></th>
427
+ </tr>
428
+ </thead>
429
+ <tbody>
430
+ {mismatch_rows}
431
+ </tbody>
432
+ </table>
433
+ </div>
434
+ </section>
435
+ </main>
436
+
437
+ <footer class="footer">
438
+ <div>PyPlayKit Framework - Enterprise Test Automation | Designed by <a href="https://www.linkedin.com/in/shankonduru" target="_blank" rel="noopener noreferrer">ShanKonduru</a></div>
439
+ <div>Available at <a href="https://github.com/ShanKonduru/pyplaykit" target="_blank" rel="noopener noreferrer">GitHub</a></div>
440
+ </footer>
441
+
442
+ <script>
443
+ const activeFilters = ["", "", "", "", "", ""];
444
+
445
+ function filterTable(columnIndex, searchValue) {{
446
+ activeFilters[columnIndex] = (searchValue || "").toLowerCase();
447
+ const table = document.getElementById("mismatchTable");
448
+ const rows = table.tBodies[0].rows;
449
+
450
+ let visibleCount = 0;
451
+
452
+ for (let i = 0; i < rows.length; i += 1) {{
453
+ let showRow = true;
454
+
455
+ for (let col = 0; col < activeFilters.length; col += 1) {{
456
+ const term = activeFilters[col];
457
+ if (!term) {{
458
+ continue;
459
+ }}
460
+
461
+ const cell = rows[i].cells[col];
462
+ const text = (cell ? cell.textContent : "").toLowerCase();
463
+ if (text.indexOf(term) === -1) {{
464
+ showRow = false;
465
+ break;
466
+ }}
467
+ }}
468
+
469
+ rows[i].style.display = showRow ? "" : "none";
470
+ if (showRow) visibleCount++;
471
+ }}
472
+ }}
473
+ </script>
474
+ </body>
475
+ </html>"""
476
+
477
+ def _escape_html(self, text: str) -> str:
478
+ """Escape HTML special characters."""
479
+ if not isinstance(text, str):
480
+ text = str(text)
481
+ return (
482
+ text.replace("&", "&amp;")
483
+ .replace("<", "&lt;")
484
+ .replace(">", "&gt;")
485
+ .replace('"', "&quot;")
486
+ .replace("'", "&#39;")
487
+ )
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes