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.
- pyairflowtester/__init__.py +72 -0
- pyairflowtester/analyzer.py +130 -0
- pyairflowtester/cli.py +299 -0
- pyairflowtester/dependency_intelligence/__init__.py +110 -0
- pyairflowtester/dependency_intelligence/analytics.py +395 -0
- pyairflowtester/dependency_intelligence/analyzers.py +421 -0
- pyairflowtester/dependency_intelligence/cli.py +356 -0
- pyairflowtester/dependency_intelligence/graph.py +411 -0
- pyairflowtester/dependency_intelligence/intelligence.py +457 -0
- pyairflowtester/dependency_intelligence/models.py +206 -0
- pyairflowtester/dependency_intelligence/observability.py +428 -0
- pyairflowtester/dependency_intelligence/parsers.py +387 -0
- pyairflowtester/models.py +133 -0
- pyairflowtester/report.py +206 -0
- pyairflowtester/rules/__init__.py +361 -0
- pyairflowtester/rules/config.py +469 -0
- pyairflowtester/rules/dag.py +187 -0
- pyairflowtester/rules/dag_advanced.py +381 -0
- pyairflowtester/rules/dbt.py +187 -0
- pyairflowtester/scanner.py +274 -0
- pyairflowtester/scoring.py +147 -0
- pyairflowtester-0.3.0.dist-info/METADATA +283 -0
- pyairflowtester-0.3.0.dist-info/RECORD +26 -0
- pyairflowtester-0.3.0.dist-info/WHEEL +4 -0
- pyairflowtester-0.3.0.dist-info/entry_points.txt +2 -0
- pyairflowtester-0.3.0.dist-info/licenses/LICENSE +22 -0
|
@@ -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
|