pyairflowtester 0.3.0__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.
@@ -0,0 +1,72 @@
1
+ """
2
+ PyAirflowTester: Unified Airflow + dbt Reliability Platform
3
+
4
+ A correlation-first reliability and quality platform that combines static analysis,
5
+ runtime monitoring, and intelligent correlation to prevent data pipeline failures.
6
+ """
7
+
8
+ __version__ = "0.3.0"
9
+ __author__ = "PyAirflowTester Contributors"
10
+ __license__ = "SSPL-1.0"
11
+
12
+ try:
13
+ from pyairflowtester._core import (
14
+ PyCategory,
15
+ PyDagDefinition,
16
+ PyDagParser,
17
+ PyDbtModel,
18
+ PyDbtParser,
19
+ PyDbtProject,
20
+ PyDbtTest,
21
+ PyExecutionMode,
22
+ PyRule,
23
+ PyRuleContext,
24
+ PyRuleEngine,
25
+ PyRuleViolation,
26
+ PyScorer,
27
+ PySeverity,
28
+ evaluate_rules,
29
+ )
30
+ except ImportError:
31
+ # Fallback if Rust extension not available
32
+ PyRule = None
33
+ PyRuleViolation = None
34
+ evaluate_rules = None
35
+ PySeverity = None
36
+ PyCategory = None
37
+ PyExecutionMode = None
38
+ PyRuleContext = None
39
+ PyRuleEngine = None
40
+ PyDagDefinition = None
41
+ PyDagParser = None
42
+ PyDbtModel = None
43
+ PyDbtTest = None
44
+ PyDbtProject = None
45
+ PyDbtParser = None
46
+ PyScorer = None
47
+
48
+ from pyairflowtester.analyzer import Analyzer, AnalyzerNotImplementedError
49
+ from pyairflowtester.report import ReportGenerator
50
+ from pyairflowtester.scanner import Scanner
51
+
52
+ __all__ = [
53
+ "Scanner",
54
+ "Analyzer",
55
+ "AnalyzerNotImplementedError",
56
+ "ReportGenerator",
57
+ "PyRule",
58
+ "PyRuleViolation",
59
+ "evaluate_rules",
60
+ "PySeverity",
61
+ "PyCategory",
62
+ "PyExecutionMode",
63
+ "PyRuleContext",
64
+ "PyRuleEngine",
65
+ "PyDagDefinition",
66
+ "PyDagParser",
67
+ "PyDbtModel",
68
+ "PyDbtTest",
69
+ "PyDbtProject",
70
+ "PyDbtParser",
71
+ "PyScorer",
72
+ ]
@@ -0,0 +1,130 @@
1
+ """
2
+ Analyzer module for runtime analysis.
3
+
4
+ INTENTIONALLY UNIMPLEMENTED. This subsystem is meant to connect to live
5
+ Airflow instances and dbt systems to analyze execution patterns, failures,
6
+ and correlations. Building real runtime correlation isn't achievable
7
+ without an actual live Airflow/dbt instance to develop and test against,
8
+ so every method below raises AnalyzerNotImplementedError rather than
9
+ silently returning an empty list/dict that could be mistaken for "analyzed,
10
+ found nothing." See README for status.
11
+ """
12
+
13
+ import logging
14
+ from typing import Any, Dict, List, Optional
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class AnalyzerNotImplementedError(NotImplementedError):
20
+ """Raised by Analyzer methods: runtime correlation is not yet built.
21
+
22
+ The Analyzer subsystem requires a live Airflow metadata database (and,
23
+ for dbt methods, a live dbt run history) to implement and validate
24
+ against. That is planned future work, not something safe to fake with
25
+ stub logic. Use the static `Scanner` (via `pyairflowtester scan`) for
26
+ the analysis that is actually implemented today.
27
+ """
28
+
29
+ def __init__(self, method_name: str):
30
+ super().__init__(
31
+ f"Analyzer.{method_name}() is not implemented. Runtime "
32
+ "correlation against live Airflow/dbt instances is a planned "
33
+ "feature, not yet built. Use `pyairflowtester scan` (static "
34
+ "analysis) for functionality that exists today."
35
+ )
36
+
37
+
38
+ class Analyzer:
39
+ """Runtime analyzer for production pipelines. NOT YET IMPLEMENTED.
40
+
41
+ Every method on this class raises AnalyzerNotImplementedError. This
42
+ class is a placeholder for a future feature: correlating rule findings
43
+ against real DAG-run/task-instance history from a live Airflow metadata
44
+ database and real dbt test-run history. That requires a live
45
+ Airflow/dbt instance to build and validate against.
46
+ """
47
+
48
+ def __init__(self, airflow_home: Optional[str] = None, airflow_db: Optional[str] = None):
49
+ """
50
+ Initialize analyzer.
51
+
52
+ Args:
53
+ airflow_home: Airflow home directory
54
+ airflow_db: Airflow database connection string
55
+ """
56
+ self.airflow_home = airflow_home
57
+ self.airflow_db = airflow_db
58
+ self.db_connection = None
59
+
60
+ def connect(self) -> bool:
61
+ """
62
+ Connect to Airflow metadata database.
63
+
64
+ Raises:
65
+ AnalyzerNotImplementedError: always; not yet implemented.
66
+ """
67
+ raise AnalyzerNotImplementedError("connect")
68
+
69
+ def analyze_dag_failures(self, dag_id: str) -> List[Dict[str, Any]]:
70
+ """
71
+ Analyze failure patterns for a DAG.
72
+
73
+ Raises:
74
+ AnalyzerNotImplementedError: always; not yet implemented.
75
+ """
76
+ raise AnalyzerNotImplementedError("analyze_dag_failures")
77
+
78
+ def analyze_task_failures(self, dag_id: str, task_id: str) -> List[Dict[str, Any]]:
79
+ """
80
+ Analyze failure patterns for a task.
81
+
82
+ Raises:
83
+ AnalyzerNotImplementedError: always; not yet implemented.
84
+ """
85
+ raise AnalyzerNotImplementedError("analyze_task_failures")
86
+
87
+ def detect_hotspots(self) -> List[Dict[str, Any]]:
88
+ """
89
+ Detect task hotspots (frequently failing tasks).
90
+
91
+ Raises:
92
+ AnalyzerNotImplementedError: always; not yet implemented.
93
+ """
94
+ raise AnalyzerNotImplementedError("detect_hotspots")
95
+
96
+ def analyze_cascade_failures(self) -> List[Dict[str, Any]]:
97
+ """
98
+ Analyze cascading failure patterns.
99
+
100
+ Raises:
101
+ AnalyzerNotImplementedError: always; not yet implemented.
102
+ """
103
+ raise AnalyzerNotImplementedError("analyze_cascade_failures")
104
+
105
+ def get_dbt_test_failures(self) -> List[Dict[str, Any]]:
106
+ """
107
+ Get dbt test failure history.
108
+
109
+ Raises:
110
+ AnalyzerNotImplementedError: always; not yet implemented.
111
+ """
112
+ raise AnalyzerNotImplementedError("get_dbt_test_failures")
113
+
114
+ def detect_flaky_tests(self) -> List[Dict[str, Any]]:
115
+ """
116
+ Detect flaky dbt tests.
117
+
118
+ Raises:
119
+ AnalyzerNotImplementedError: always; not yet implemented.
120
+ """
121
+ raise AnalyzerNotImplementedError("detect_flaky_tests")
122
+
123
+ def calculate_blast_radius(self, source: str, source_type: str = "dag") -> Dict[str, Any]:
124
+ """
125
+ Calculate blast radius for a failure source.
126
+
127
+ Raises:
128
+ AnalyzerNotImplementedError: always; not yet implemented.
129
+ """
130
+ raise AnalyzerNotImplementedError("calculate_blast_radius")
pyairflowtester/cli.py ADDED
@@ -0,0 +1,299 @@
1
+ """
2
+ PyAirflowTester CLI: Command-line interface for artifact and runtime analysis.
3
+ """
4
+
5
+ import logging
6
+ from pathlib import Path
7
+
8
+ import click
9
+ from rich.console import Console
10
+ from rich.table import Table
11
+
12
+ from pyairflowtester.analyzer import Analyzer, AnalyzerNotImplementedError
13
+ from pyairflowtester.report import ReportGenerator
14
+ from pyairflowtester.scanner import Scanner
15
+
16
+ # Configure logging
17
+ logging.basicConfig(
18
+ level=logging.INFO,
19
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
20
+ )
21
+ logger = logging.getLogger(__name__)
22
+
23
+ console = Console()
24
+
25
+
26
+ @click.group()
27
+ @click.version_option()
28
+ def main():
29
+ """PyAirflowTester: Unified Airflow + dbt Reliability Platform"""
30
+ pass
31
+
32
+
33
+ @main.command()
34
+ @click.argument("path", type=click.Path(exists=True), default=".")
35
+ @click.option(
36
+ "--dags",
37
+ type=click.Path(exists=True),
38
+ help="Path to Airflow DAGs directory",
39
+ )
40
+ @click.option(
41
+ "--dbt",
42
+ type=click.Path(exists=True),
43
+ help="Path to dbt project directory",
44
+ )
45
+ @click.option(
46
+ "--airflow-cfg",
47
+ type=click.Path(exists=True),
48
+ help="Path to airflow.cfg for configuration auditing",
49
+ )
50
+ @click.option(
51
+ "--format",
52
+ type=click.Choice(["json", "html", "markdown", "sarif"]),
53
+ default="json",
54
+ help="Output format",
55
+ )
56
+ @click.option(
57
+ "--output",
58
+ type=click.Path(),
59
+ default=None,
60
+ help="Output file path",
61
+ )
62
+ @click.option(
63
+ "--severity",
64
+ type=click.Choice(["critical", "high", "medium", "low", "info"]),
65
+ default="medium",
66
+ help="Minimum severity level to report",
67
+ )
68
+ def scan(path, dags, dbt, airflow_cfg, format, output, severity):
69
+ """
70
+ Scan Airflow DAGs and dbt projects for issues.
71
+
72
+ Performs static analysis to detect configuration issues, anti-patterns,
73
+ and potential reliability risks before deployment.
74
+ """
75
+ console.print("[bold cyan]PyAirflowTester Scanner[/bold cyan]")
76
+ console.print(f"Target: {path}\n")
77
+
78
+ scanner = Scanner()
79
+
80
+ # Scan DAGs if path provided
81
+ if dags:
82
+ console.print("[yellow]Scanning Airflow DAGs...[/yellow]")
83
+ dag_violations = scanner.scan_dags(Path(dags))
84
+ console.print(f" Found {len(dag_violations)} violations\n")
85
+ else:
86
+ dag_violations = []
87
+
88
+ # Scan dbt if path provided
89
+ if dbt:
90
+ console.print("[yellow]Scanning dbt project...[/yellow]")
91
+ dbt_violations = scanner.scan_dbt(Path(dbt))
92
+ console.print(f" Found {len(dbt_violations)} violations\n")
93
+ else:
94
+ dbt_violations = []
95
+
96
+ # Scan airflow.cfg if path provided
97
+ if airflow_cfg:
98
+ console.print("[yellow]Scanning Airflow configuration...[/yellow]")
99
+ config_violations = scanner.scan_config(Path(airflow_cfg))
100
+ console.print(f" Found {len(config_violations)} violations\n")
101
+ else:
102
+ config_violations = []
103
+
104
+ all_violations = dag_violations + dbt_violations + config_violations
105
+
106
+ if not all_violations:
107
+ console.print("[green]✓ No violations found![/green]")
108
+ return
109
+
110
+ # Filter by severity
111
+ from pyairflowtester.models import SEVERITY_WEIGHTS
112
+ min_weight = SEVERITY_WEIGHTS.get(severity, 0)
113
+ filtered = [
114
+ v for v in all_violations
115
+ if SEVERITY_WEIGHTS.get(v.get("severity", "info"), 0) >= min_weight
116
+ ]
117
+
118
+ # Display results
119
+ _display_violations(filtered)
120
+
121
+ # Generate report if format specified
122
+ if format and output:
123
+ generator = ReportGenerator()
124
+ report_path = generator.generate(format, filtered, Path(output))
125
+ console.print(f"\n[green]Report generated:[/green] {report_path}")
126
+
127
+
128
+ @main.command()
129
+ @click.argument("path", type=click.Path(exists=True), default=".")
130
+ @click.option(
131
+ "--compare",
132
+ type=str,
133
+ default=None,
134
+ help="Compare to baseline (git branch)",
135
+ )
136
+ @click.option(
137
+ "--format",
138
+ type=click.Choice(["json", "markdown", "html"]),
139
+ default="markdown",
140
+ help="Output format",
141
+ )
142
+ def score(path, compare, format):
143
+ """
144
+ Calculate risk scores for DAGs and dbt models.
145
+
146
+ Generates multi-dimensional risk scores based on:
147
+ - Failure history and patterns
148
+ - Configuration quality
149
+ - Test coverage
150
+ - Performance metrics
151
+ """
152
+ console.print("[bold cyan]PyAirflowTester Scoring[/bold cyan]")
153
+ console.print(f"Target: {path}\n")
154
+
155
+ scanner = Scanner()
156
+ scorer = scanner._get_scorer()
157
+
158
+ # Scan for violations
159
+ dag_violations = scanner.scan_dags(Path(path) / "dags" if (Path(path) / "dags").exists() else Path(path))
160
+ dbt_violations = scanner.scan_dbt(Path(path) / "dbt" if (Path(path) / "dbt").exists() else Path(path))
161
+
162
+ all_violations = dag_violations + dbt_violations
163
+
164
+ # Calculate score
165
+ risk_score = scorer.calculate_risk_score(all_violations)
166
+ risk_level = scorer.categorize_risk(risk_score)
167
+
168
+ # Display scorecard
169
+ table = Table(title="Risk Scorecard")
170
+ table.add_column("Metric", style="cyan")
171
+ table.add_column("Score", style="magenta")
172
+ table.add_row("Overall Risk", f"{risk_score:.1f}/100")
173
+ table.add_row("Risk Level", risk_level)
174
+ table.add_row("Total Violations", str(len(all_violations)))
175
+
176
+ console.print(table)
177
+
178
+ if compare:
179
+ console.print(f"\n[yellow]Comparing to baseline: {compare}[/yellow]")
180
+
181
+
182
+ @main.command()
183
+ @click.option("--category", type=str, default=None, help="Filter by category")
184
+ @click.option("--severity", type=str, default=None, help="Filter by severity")
185
+ @click.option("--execution-mode", type=str, default=None, help="Filter by execution mode")
186
+ def rules(category, severity, execution_mode):
187
+ """
188
+ List all available rules.
189
+
190
+ Rules are organized by category (reliability, performance, etc.) and
191
+ execution mode (static, runtime, correlation).
192
+ """
193
+ console.print("[bold cyan]PyAirflowTester Rules[/bold cyan]\n")
194
+
195
+ from pyairflowtester.rules import get_all_rules
196
+
197
+ all_rules = get_all_rules()
198
+
199
+ # Filter
200
+ filtered_rules = all_rules
201
+ if category:
202
+ filtered_rules = [r for r in filtered_rules if r.get("category") == category]
203
+ if severity:
204
+ filtered_rules = [r for r in filtered_rules if r.get("severity") == severity]
205
+ if execution_mode:
206
+ filtered_rules = [r for r in filtered_rules if r.get("execution_mode") == execution_mode]
207
+
208
+ # Display
209
+ table = Table(title=f"Available Rules ({len(filtered_rules)})")
210
+ table.add_column("ID", style="cyan")
211
+ table.add_column("Name", style="green")
212
+ table.add_column("Severity", style="magenta")
213
+ table.add_column("Category", style="yellow")
214
+ table.add_column("Mode")
215
+
216
+ for rule in filtered_rules:
217
+ table.add_row(
218
+ rule.get("id", ""),
219
+ rule.get("name", ""),
220
+ rule.get("severity", ""),
221
+ rule.get("category", ""),
222
+ rule.get("execution_mode", ""),
223
+ )
224
+
225
+ console.print(table)
226
+
227
+
228
+ @main.command()
229
+ @click.option(
230
+ "--airflow-home",
231
+ type=click.Path(exists=True),
232
+ help="Airflow home directory",
233
+ )
234
+ @click.option(
235
+ "--airflow-db",
236
+ type=str,
237
+ help="Airflow database connection string",
238
+ )
239
+ def connect(airflow_home, airflow_db):
240
+ """
241
+ Connect to Airflow instance for runtime analysis.
242
+
243
+ NOTE: Runtime analysis (the Analyzer subsystem) is not yet implemented.
244
+ This command reports that plainly instead of a fake success message.
245
+ """
246
+ console.print("[bold cyan]PyAirflowTester Runtime Connection[/bold cyan]\n")
247
+
248
+ analyzer = Analyzer(airflow_home=airflow_home, airflow_db=airflow_db)
249
+ try:
250
+ analyzer.connect()
251
+ except AnalyzerNotImplementedError as e:
252
+ console.print(f"[red]✗ {e}[/red]")
253
+ raise SystemExit(1)
254
+
255
+
256
+ def _display_violations(violations):
257
+ """Display violations in rich table format."""
258
+ if not violations:
259
+ return
260
+
261
+ table = Table(title=f"Violations Found ({len(violations)})")
262
+ table.add_column("Rule", style="cyan")
263
+ table.add_column("Severity", style="magenta")
264
+ table.add_column("Resource", style="green")
265
+ table.add_column("Message")
266
+
267
+ for v in violations[:20]: # Show first 20
268
+ severity = v.get("severity", "info")
269
+ severity_style = {
270
+ "critical": "red",
271
+ "high": "red",
272
+ "medium": "yellow",
273
+ "low": "blue",
274
+ "info": "white",
275
+ }.get(severity, "white")
276
+
277
+ table.add_row(
278
+ v.get("rule_id", ""),
279
+ f"[{severity_style}]{severity}[/{severity_style}]",
280
+ v.get("affected_resource", ""),
281
+ v.get("message", "")[:50],
282
+ )
283
+
284
+ if len(violations) > 20:
285
+ table.add_row("...", "...", "...", f"... and {len(violations) - 20} more")
286
+
287
+ console.print(table)
288
+
289
+
290
+ # Register the dependency-intelligence command group (graph build/impact/
291
+ # blast-radius/etc.). This was previously defined but never attached to the
292
+ # main CLI group, so `pyairflowtester dependency ...` did not work.
293
+ from pyairflowtester.dependency_intelligence.cli import register_dependency_cli # noqa: E402
294
+
295
+ register_dependency_cli(main)
296
+
297
+
298
+ if __name__ == "__main__":
299
+ main()
@@ -0,0 +1,110 @@
1
+ """Dependency Intelligence Engine for PyAirflowTester."""
2
+
3
+ from .analytics import (
4
+ OwnershipAnalysis,
5
+ OwnershipAnalyzer,
6
+ SchemaEvolution,
7
+ SchemaEvolutionTracker,
8
+ SLAValidation,
9
+ SLAValidator,
10
+ TestCoverageAnalysis,
11
+ TestCoverageAnalyzer,
12
+ )
13
+ from .analyzers import (
14
+ BlastRadiusEngine,
15
+ DriftDetectionEngine,
16
+ ImpactAnalysisEngine,
17
+ RiskScoringEngine,
18
+ )
19
+ from .graph import DependencyGraphEngine
20
+ from .intelligence import (
21
+ AnomalyDetection,
22
+ AnomalyDetector,
23
+ FailurePrediction,
24
+ FailurePredictionEngine,
25
+ HealthScore,
26
+ HealthScoreCalculator,
27
+ Recommendation,
28
+ RecommendationEngine,
29
+ )
30
+ from .models import (
31
+ BlastRadiusResult,
32
+ DependencyGraph,
33
+ DriftDetectionResult,
34
+ Edge,
35
+ ImpactResult,
36
+ Node,
37
+ NodeSeverity,
38
+ NodeType,
39
+ RiskScoreResult,
40
+ )
41
+ from .observability import (
42
+ Alert,
43
+ AlertManager,
44
+ DashboardBuilder,
45
+ EventLogger,
46
+ ExecutionEvent,
47
+ Metric,
48
+ MetricsCollector,
49
+ MetricType,
50
+ )
51
+ from .parsers import (
52
+ AirflowDAGParser,
53
+ AirflowDatasetParser,
54
+ UnifiedGraphBuilder,
55
+ dbtManifestParser,
56
+ )
57
+
58
+ __all__ = [
59
+ # Models
60
+ "NodeType",
61
+ "NodeSeverity",
62
+ "Node",
63
+ "Edge",
64
+ "DependencyGraph",
65
+ "ImpactResult",
66
+ "BlastRadiusResult",
67
+ "RiskScoreResult",
68
+ "DriftDetectionResult",
69
+ # Graph
70
+ "DependencyGraphEngine",
71
+ # Parsers
72
+ "AirflowDAGParser",
73
+ "dbtManifestParser",
74
+ "AirflowDatasetParser",
75
+ "UnifiedGraphBuilder",
76
+ # Phase 1 Analyzers
77
+ "ImpactAnalysisEngine",
78
+ "BlastRadiusEngine",
79
+ "RiskScoringEngine",
80
+ "DriftDetectionEngine",
81
+ # Phase 2 Analytics
82
+ "OwnershipAnalyzer",
83
+ "SchemaEvolutionTracker",
84
+ "SLAValidator",
85
+ "TestCoverageAnalyzer",
86
+ "OwnershipAnalysis",
87
+ "SchemaEvolution",
88
+ "SLAValidation",
89
+ "TestCoverageAnalysis",
90
+ # Phase 3 Intelligence
91
+ "FailurePredictionEngine",
92
+ "AnomalyDetector",
93
+ "RecommendationEngine",
94
+ "HealthScoreCalculator",
95
+ "FailurePrediction",
96
+ "AnomalyDetection",
97
+ "Recommendation",
98
+ "HealthScore",
99
+ # Phase 4 Observability
100
+ "MetricsCollector",
101
+ "AlertManager",
102
+ "EventLogger",
103
+ "DashboardBuilder",
104
+ "MetricType",
105
+ "Metric",
106
+ "Alert",
107
+ "ExecutionEvent",
108
+ ]
109
+
110
+ __version__ = "0.4.0" # Phase 1-4 MVP complete