data-contract-validator 1.0.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,24 @@
1
+ """
2
+ Data Contract Validator
3
+
4
+ Prevent production API breaks by validating data contracts between
5
+ your data pipelines and API frameworks.
6
+ """
7
+
8
+ __version__ = "1.0.0"
9
+ __author__ = "Your Name"
10
+ __email__ = "your.email@example.com"
11
+
12
+ from .core.validator import ContractValidator
13
+ from .core.models import ValidationResult, ValidationIssue, IssueSeverity
14
+ from .extractors.dbt import DBTExtractor
15
+ from .extractors.fastapi import FastAPIExtractor
16
+
17
+ __all__ = [
18
+ "ContractValidator",
19
+ "ValidationResult",
20
+ "ValidationIssue",
21
+ "IssueSeverity",
22
+ "DBTExtractor",
23
+ "FastAPIExtractor",
24
+ ]
@@ -0,0 +1,672 @@
1
+ import os
2
+ import sys
3
+ import json
4
+ import yaml
5
+ import subprocess
6
+ import click
7
+ from pathlib import Path
8
+ from typing import Optional, Dict, Any
9
+
10
+ from .core.validator import ContractValidator
11
+ from .extractors.dbt import DBTExtractor
12
+ from .extractors.fastapi import FastAPIExtractor
13
+
14
+
15
+ @click.group()
16
+ @click.version_option()
17
+ def cli():
18
+ """๐Ÿ›ก๏ธ Data Contract Validator - Prevent production API breaks with lightweight governance."""
19
+ pass
20
+
21
+
22
+ @cli.command()
23
+ @click.option(
24
+ "--interactive", is_flag=True, help="Interactive setup wizard (recommended)"
25
+ )
26
+ @click.option(
27
+ "--framework",
28
+ type=click.Choice(["fastapi", "django", "flask"]),
29
+ help="Target framework",
30
+ )
31
+ @click.option("--dbt-path", default=".", help="DBT project path")
32
+ @click.option("--output-dir", default=".", help="Output directory")
33
+ def init(interactive: bool, framework: str, dbt_path: str, output_dir: str):
34
+ """๐Ÿš€ Initialize contract validation for your project (takes 30 seconds)."""
35
+
36
+ click.echo("๐Ÿ›ก๏ธ Setting up Data Contract Validation...")
37
+ click.echo(" This prevents production breaks forever!")
38
+ click.echo()
39
+
40
+ if interactive:
41
+ config = _interactive_setup()
42
+ else:
43
+ config = _quick_setup(framework, dbt_path)
44
+
45
+ output_path = Path(output_dir)
46
+
47
+ # Write config file
48
+ config_file = output_path / ".retl-validator.yml"
49
+ with open(config_file, "w") as f:
50
+ yaml.dump(config, f, default_flow_style=False, indent=2)
51
+
52
+ click.echo(f"โœ… Created configuration: {config_file}")
53
+
54
+ # Create GitHub Actions workflow
55
+ if _create_github_workflow(output_path, config):
56
+ click.echo("โœ… Created GitHub Actions workflow")
57
+
58
+ # Test the setup
59
+ click.echo("\n๐Ÿงช Testing your setup...")
60
+ if _test_setup(config_file):
61
+ click.echo("\n๐ŸŽ‰ Setup complete! Your contracts are now protected.")
62
+ click.echo("\n๐Ÿš€ Next steps:")
63
+ click.echo(" 1. git add .retl-validator.yml .github/workflows/")
64
+ click.echo(" 2. git commit -m 'Add data contract validation'")
65
+ click.echo(" 3. git push (triggers validation in CI/CD)")
66
+ click.echo(" 4. Watch it prevent production breaks! ๐Ÿ›ก๏ธ")
67
+ else:
68
+ click.echo(
69
+ "\nโš ๏ธ Setup needs attention. Run 'contract-validator test' for details."
70
+ )
71
+
72
+
73
+ def _interactive_setup() -> Dict[str, Any]:
74
+ """Interactive setup wizard - 3 simple questions."""
75
+ click.echo("๐Ÿ“‹ Quick Setup (3 questions):")
76
+ click.echo()
77
+
78
+ # Question 1: DBT project location
79
+ dbt_path = click.prompt(
80
+ "1๏ธโƒฃ Where is your DBT project?", default=".", show_default=True
81
+ )
82
+
83
+ # Auto-detect if DBT project exists
84
+ if not Path(dbt_path).exists() or not (Path(dbt_path) / "dbt_project.yml").exists():
85
+ click.echo(f" โš ๏ธ No dbt_project.yml found at {dbt_path}")
86
+ if click.confirm(" Continue anyway?"):
87
+ pass
88
+ else:
89
+ click.echo(" ๐Ÿ’ก Make sure you're in your DBT project directory")
90
+ sys.exit(1)
91
+ else:
92
+ click.echo(" โœ… DBT project found")
93
+
94
+ # Question 2: API framework
95
+ click.echo()
96
+ framework = click.prompt(
97
+ "2๏ธโƒฃ What API framework do you use?",
98
+ type=click.Choice(["fastapi", "django", "flask", "other"]),
99
+ default="fastapi",
100
+ show_default=True,
101
+ )
102
+
103
+ # Question 3: API models location
104
+ click.echo()
105
+ if framework == "fastapi":
106
+ default_path = "app/models.py"
107
+ prompt_text = "3๏ธโƒฃ Where are your Pydantic models?"
108
+ elif framework == "django":
109
+ default_path = "models.py"
110
+ prompt_text = "3๏ธโƒฃ Where are your Django models?"
111
+ else:
112
+ default_path = "models.py"
113
+ prompt_text = "3๏ธโƒฃ Where are your API models?"
114
+
115
+ api_location = click.prompt(prompt_text, default=default_path, show_default=True)
116
+
117
+ # Auto-detect if it's local file or GitHub repo
118
+ is_github_repo = "/" in api_location and not api_location.startswith((".", "/"))
119
+
120
+ if is_github_repo:
121
+ # Format: "org/repo" or "org/repo/path/to/file.py"
122
+ parts = api_location.split("/")
123
+ if len(parts) >= 2:
124
+ repo = "/".join(parts[:2])
125
+ path = "/".join(parts[2:]) if len(parts) > 2 else "models.py"
126
+ else:
127
+ repo = api_location
128
+ path = "models.py"
129
+
130
+ api_config = {"type": "github", "repo": repo, "path": path}
131
+ click.echo(f" ๐Ÿ™ GitHub repo detected: {repo}/{path}")
132
+ else:
133
+ api_config = {"type": "local", "path": api_location}
134
+
135
+ # Check if local file exists
136
+ if Path(api_location).exists():
137
+ click.echo(" โœ… Local file found")
138
+ else:
139
+ click.echo(f" โš ๏ธ File not found: {api_location}")
140
+ if not click.confirm(" Continue anyway?"):
141
+ sys.exit(1)
142
+
143
+ return {
144
+ "version": "1.0",
145
+ "name": f"contracts-{Path.cwd().name}",
146
+ "description": "Auto-generated data contract validation",
147
+ "source": {
148
+ "dbt": {"project_path": dbt_path, "auto_compile": True, "timeout": 120}
149
+ },
150
+ "target": {framework: api_config},
151
+ "validation": {
152
+ "fail_on": ["missing_tables", "missing_required_columns"],
153
+ "warn_on": ["type_mismatches", "missing_optional_columns"],
154
+ "mode": "strict",
155
+ },
156
+ "output": {"format": "terminal", "show_suggestions": True, "max_issues": 20},
157
+ }
158
+
159
+
160
+ def _quick_setup(framework: str, dbt_path: str) -> Dict[str, Any]:
161
+ """Quick non-interactive setup with smart defaults."""
162
+
163
+ click.echo("๐Ÿ” Auto-detecting project structure...")
164
+
165
+ # Auto-detect API models location
166
+ framework = framework or "fastapi"
167
+
168
+ common_paths = {
169
+ "fastapi": ["app/models.py", "src/models.py", "models.py", "api/models.py"],
170
+ "django": ["models.py", "*/models.py", "app/models.py"],
171
+ "flask": ["models.py", "app/models.py", "src/models.py"],
172
+ }
173
+
174
+ api_path = None
175
+ if framework in common_paths:
176
+ for path in common_paths[framework]:
177
+ if "*" in path:
178
+ # Handle wildcard patterns
179
+ import glob
180
+
181
+ matches = glob.glob(path)
182
+ if matches:
183
+ api_path = matches[0]
184
+ break
185
+ elif Path(path).exists():
186
+ api_path = path
187
+ break
188
+
189
+ if api_path:
190
+ click.echo(f" โœ… Found {framework} models: {api_path}")
191
+ else:
192
+ api_path = common_paths[framework][0] # Use default
193
+ click.echo(f" โš ๏ธ Using default path: {api_path}")
194
+
195
+ return {
196
+ "version": "1.0",
197
+ "name": f"contracts-{Path.cwd().name}",
198
+ "description": "Auto-generated data contract validation",
199
+ "source": {"dbt": {"project_path": dbt_path, "auto_compile": True}},
200
+ "target": {framework: {"type": "local", "path": api_path}},
201
+ "validation": {
202
+ "fail_on": ["missing_tables", "missing_required_columns"],
203
+ "warn_on": ["type_mismatches"],
204
+ "mode": "balanced", # Less strict than interactive
205
+ },
206
+ }
207
+
208
+
209
+ def _create_github_workflow(output_path: Path, config: Dict[str, Any]) -> bool:
210
+ """Auto-create GitHub Actions workflow."""
211
+
212
+ workflow_dir = output_path / ".github" / "workflows"
213
+ workflow_dir.mkdir(parents=True, exist_ok=True)
214
+
215
+ # Determine trigger paths based on config
216
+ dbt_path = config.get("source", {}).get("dbt", {}).get("project_path", ".")
217
+
218
+ trigger_paths = [
219
+ f"{dbt_path}/models/**/*.sql" if dbt_path != "." else "models/**/*.sql",
220
+ f"{dbt_path}/dbt_project.yml" if dbt_path != "." else "dbt_project.yml",
221
+ "**/*models*.py",
222
+ ".retl-validator.yml",
223
+ ]
224
+
225
+ workflow_content = f"""# ๐Ÿค– Auto-generated by data-contract-validator
226
+ name: ๐Ÿ›ก๏ธ Data Contract Validation
227
+
228
+ on:
229
+ pull_request:
230
+ paths:
231
+ {chr(10).join(f' - "{path}"' for path in trigger_paths)}
232
+
233
+ workflow_dispatch:
234
+
235
+ permissions:
236
+ contents: read
237
+ pull-requests: write
238
+
239
+ jobs:
240
+ validate-contracts:
241
+ name: Validate Data Contracts
242
+ runs-on: ubuntu-latest
243
+
244
+ steps:
245
+ - name: Checkout code
246
+ uses: actions/checkout@v4
247
+
248
+ - name: Setup Python
249
+ uses: actions/setup-python@v4
250
+ with:
251
+ python-version: '3.9'
252
+
253
+ - name: Install data contract validator
254
+ run: pip install data-contract-validator
255
+
256
+ - name: Validate contracts
257
+ env:
258
+ GITHUB_TOKEN: ${{{{ secrets.GITHUB_TOKEN }}}}
259
+ run: |
260
+ contract-validator validate \\
261
+ --config .retl-validator.yml \\
262
+ --output github
263
+
264
+ - name: Comment on PR (if validation fails)
265
+ if: failure()
266
+ uses: actions/github-script@v6
267
+ with:
268
+ script: |
269
+ github.rest.issues.createComment({{
270
+ issue_number: context.issue.number,
271
+ owner: context.repo.owner,
272
+ repo: context.repo.repo,
273
+ body: `## ๐Ÿšจ Data Contract Validation Failed
274
+
275
+ Your changes don't satisfy API requirements.
276
+ Check the logs above for specific issues.
277
+
278
+ **Common fixes:**
279
+ - Add missing columns to your DBT model
280
+ - Update API models to match DBT output
281
+ - Check for type mismatches
282
+
283
+ ---
284
+ ๐Ÿค– Automated by [Data Contract Validator](https://github.com/OGsiji/retl_validator)`
285
+ }})
286
+ """
287
+
288
+ workflow_file = workflow_dir / "validate-contracts.yml"
289
+ try:
290
+ with open(workflow_file, "w") as f:
291
+ f.write(workflow_content)
292
+ return True
293
+ except Exception as e:
294
+ click.echo(f" โš ๏ธ Could not create workflow: {e}")
295
+ return False
296
+
297
+
298
+ @cli.command()
299
+ def test():
300
+ """๐Ÿงช Test your contract validation setup."""
301
+
302
+ click.echo("๐Ÿงช Testing Data Contract Validation Setup...")
303
+ click.echo("=" * 45)
304
+
305
+ config_file = Path(".retl-validator.yml")
306
+ return _test_setup(config_file)
307
+
308
+
309
+ def _test_setup(config_file: Path) -> bool:
310
+ """Internal setup test with detailed output."""
311
+
312
+ all_passed = True
313
+
314
+ # Test 1: Config file exists
315
+ click.echo("\n1๏ธโƒฃ Checking configuration file...")
316
+ if not config_file.exists():
317
+ click.echo(f" โŒ No {config_file} found")
318
+ click.echo(" ๐Ÿ’ก Run 'contract-validator init' first")
319
+ return False
320
+
321
+ click.echo(f" โœ… Configuration file found: {config_file}")
322
+
323
+ # Test 2: Load and validate config
324
+ click.echo("\n2๏ธโƒฃ Validating configuration...")
325
+ try:
326
+ with open(config_file) as f:
327
+ config = yaml.safe_load(f)
328
+ click.echo(" โœ… Configuration is valid YAML")
329
+
330
+ # Check required sections
331
+ required_sections = ["version", "source", "target", "validation"]
332
+ missing_sections = [s for s in required_sections if s not in config]
333
+ if missing_sections:
334
+ click.echo(f" โš ๏ธ Missing sections: {missing_sections}")
335
+ all_passed = False
336
+ else:
337
+ click.echo(" โœ… All required sections present")
338
+
339
+ except Exception as e:
340
+ click.echo(f" โŒ Configuration file is invalid: {e}")
341
+ return False
342
+
343
+ # Test 3: Check DBT project
344
+ click.echo("\n3๏ธโƒฃ Checking DBT project...")
345
+ dbt_config = config.get("source", {}).get("dbt", {})
346
+ dbt_path = Path(dbt_config.get("project_path", "."))
347
+
348
+ if not dbt_path.exists():
349
+ click.echo(f" โŒ DBT project directory not found: {dbt_path}")
350
+ all_passed = False
351
+ elif not (dbt_path / "dbt_project.yml").exists():
352
+ click.echo(f" โš ๏ธ No dbt_project.yml found in {dbt_path}")
353
+ click.echo(" ๐Ÿ’ก Make sure this is a valid DBT project")
354
+ all_passed = False
355
+ else:
356
+ click.echo(f" โœ… DBT project found: {dbt_path}")
357
+
358
+ # Test 4: Check target configuration
359
+ click.echo("\n4๏ธโƒฃ Checking target configuration...")
360
+ target_config = config.get("target", {})
361
+
362
+ if not target_config:
363
+ click.echo(" โŒ No target configuration found")
364
+ all_passed = False
365
+ else:
366
+ for target_name, target_info in target_config.items():
367
+ click.echo(f" ๐ŸŽฏ Target: {target_name}")
368
+
369
+ if target_info.get("type") == "local":
370
+ api_path = Path(target_info.get("path", ""))
371
+ if not api_path.exists():
372
+ click.echo(f" โš ๏ธ Local file not found: {api_path}")
373
+ all_passed = False
374
+ else:
375
+ click.echo(f" โœ… Local file found: {api_path}")
376
+
377
+ elif target_info.get("type") == "github":
378
+ repo = target_info.get("repo")
379
+ path = target_info.get("path")
380
+ click.echo(f" ๐Ÿ™ GitHub repo: {repo}/{path}")
381
+
382
+ else:
383
+ click.echo(f" โš ๏ธ Unknown target type: {target_info.get('type')}")
384
+ all_passed = False
385
+
386
+ # Test 5: Try a dry run validation
387
+ click.echo("\n5๏ธโƒฃ Testing validation...")
388
+ try:
389
+ from .core.validator import ContractValidator
390
+ from .extractors.dbt import DBTExtractor
391
+ from .extractors.fastapi import FastAPIExtractor
392
+
393
+ # Quick validation test
394
+ dbt_extractor = DBTExtractor(str(dbt_path))
395
+
396
+ # Test DBT extraction
397
+ click.echo(" ๐Ÿ” Testing DBT extraction...")
398
+ dbt_schemas = dbt_extractor.extract_schemas()
399
+ if dbt_schemas:
400
+ click.echo(f" โœ… Found {len(dbt_schemas)} DBT models")
401
+ else:
402
+ click.echo(" โš ๏ธ No DBT models found")
403
+ all_passed = False
404
+
405
+ # Test target extraction (for local files only)
406
+ for target_name, target_info in target_config.items():
407
+ if target_info.get("type") == "local":
408
+ click.echo(f" ๐ŸŽฏ Testing {target_name} extraction...")
409
+ try:
410
+ if target_name == "fastapi":
411
+ target_extractor = FastAPIExtractor.from_local_file(
412
+ target_info.get("path")
413
+ )
414
+ target_schemas = target_extractor.extract_schemas()
415
+ if target_schemas:
416
+ click.echo(
417
+ f" โœ… Found {len(target_schemas)} API models"
418
+ )
419
+ else:
420
+ click.echo(" โš ๏ธ No API models found")
421
+ all_passed = False
422
+ except Exception as e:
423
+ click.echo(f" โš ๏ธ Extraction error: {e}")
424
+ all_passed = False
425
+
426
+ except Exception as e:
427
+ click.echo(f" โš ๏ธ Validation test error: {e}")
428
+ all_passed = False
429
+
430
+ # Final result
431
+ click.echo("\n" + "=" * 45)
432
+ if all_passed:
433
+ click.echo("๐ŸŽ‰ All tests passed! Your setup is ready.")
434
+ click.echo("\n๐Ÿš€ Next steps:")
435
+ click.echo(" โ€ข Run 'contract-validator validate' to test validation")
436
+ click.echo(" โ€ข Commit your config and workflow files")
437
+ click.echo(" โ€ข Push to activate protection in CI/CD")
438
+ else:
439
+ click.echo("โš ๏ธ Some tests had issues. See details above.")
440
+ click.echo("\n๐Ÿ’ก Common fixes:")
441
+ click.echo(" โ€ข Make sure you're in your DBT project directory")
442
+ click.echo(" โ€ข Check that API model files exist")
443
+ click.echo(" โ€ข Run 'contract-validator init' to regenerate config")
444
+
445
+ return all_passed
446
+
447
+
448
+ @cli.command()
449
+ @click.option("--config", default=".retl-validator.yml", help="Config file path")
450
+ @click.option(
451
+ "--dry-run", is_flag=True, help="Test configuration without full validation"
452
+ )
453
+ @click.option(
454
+ "--output", type=click.Choice(["terminal", "json", "github"]), default="terminal"
455
+ )
456
+ @click.option("--dbt-project", help="Override DBT project path")
457
+ @click.option("--fastapi-local", help="Override FastAPI models path")
458
+ @click.option("--fastapi-repo", help="Override FastAPI repo (org/repo)")
459
+ @click.option("--fastapi-path", default="app/models.py", help="Path in FastAPI repo")
460
+ def validate(
461
+ config: str,
462
+ dry_run: bool,
463
+ output: str,
464
+ dbt_project: str,
465
+ fastapi_local: str,
466
+ fastapi_repo: str,
467
+ fastapi_path: str,
468
+ ):
469
+ """๐Ÿ” Validate data contracts (prevents production breaks)."""
470
+
471
+ # Load config if it exists
472
+ config_data = {}
473
+ config_file = Path(config)
474
+ if config_file.exists():
475
+ with open(config_file) as f:
476
+ config_data = yaml.safe_load(f)
477
+ click.echo(f"๐Ÿ“‹ Using config: {config}")
478
+ elif not any([dbt_project, fastapi_local, fastapi_repo]):
479
+ click.echo("โŒ No config file found and no command line options provided")
480
+ click.echo("๐Ÿ’ก Run 'contract-validator init' to create a config file")
481
+ click.echo(" Or use command line options:")
482
+ click.echo(
483
+ " contract-validator validate --dbt-project . --fastapi-local app/models.py"
484
+ )
485
+ sys.exit(1)
486
+
487
+ if dry_run:
488
+ click.echo("๐Ÿงช Dry run - testing configuration only")
489
+ _test_configuration(config_data, dbt_project, fastapi_local, fastapi_repo)
490
+ return
491
+
492
+ # Run actual validation
493
+ _run_validation(
494
+ config_data, output, dbt_project, fastapi_local, fastapi_repo, fastapi_path
495
+ )
496
+
497
+
498
+ def _test_configuration(
499
+ config_data: Dict[str, Any], dbt_project: str, fastapi_local: str, fastapi_repo: str
500
+ ):
501
+ """Test configuration without running full validation."""
502
+
503
+ dbt_path = dbt_project or config_data.get("source", {}).get("dbt", {}).get(
504
+ "project_path", "."
505
+ )
506
+
507
+ click.echo(f" ๐Ÿ“Š DBT project: {dbt_path}")
508
+ if Path(dbt_path).exists():
509
+ click.echo(" โœ… Path exists")
510
+ else:
511
+ click.echo(" โŒ Path not found")
512
+
513
+ if fastapi_local:
514
+ click.echo(f" ๐ŸŽฏ FastAPI models: {fastapi_local}")
515
+ if Path(fastapi_local).exists():
516
+ click.echo(" โœ… File exists")
517
+ else:
518
+ click.echo(" โŒ File not found")
519
+
520
+ if fastapi_repo:
521
+ click.echo(f" ๐Ÿ™ FastAPI repo: {fastapi_repo}")
522
+
523
+ click.echo("โœ… Configuration test complete!")
524
+
525
+
526
+ def _run_validation(
527
+ config_data: Dict[str, Any],
528
+ output: str,
529
+ dbt_project: str,
530
+ fastapi_local: str,
531
+ fastapi_repo: str,
532
+ fastapi_path: str,
533
+ ):
534
+ """Run the actual validation."""
535
+
536
+ # Get DBT project path
537
+ dbt_path = dbt_project or config_data.get("source", {}).get("dbt", {}).get(
538
+ "project_path", "."
539
+ )
540
+
541
+ # Initialize DBT extractor
542
+ try:
543
+ dbt_extractor = DBTExtractor(dbt_path)
544
+ except Exception as e:
545
+ click.echo(f"โŒ Error initializing DBT extractor: {e}")
546
+ sys.exit(1)
547
+
548
+ # Initialize FastAPI extractor
549
+ try:
550
+ if fastapi_local:
551
+ fastapi_extractor = FastAPIExtractor.from_local_file(fastapi_local)
552
+ elif fastapi_repo:
553
+ github_token = os.environ.get("GITHUB_TOKEN")
554
+ fastapi_extractor = FastAPIExtractor.from_github_repo(
555
+ repo=fastapi_repo, path=fastapi_path, token=github_token
556
+ )
557
+ else:
558
+ # Get from config
559
+ target_config = list(config_data.get("target", {}).values())[0]
560
+ if target_config.get("type") == "local":
561
+ fastapi_extractor = FastAPIExtractor.from_local_file(
562
+ target_config.get("path")
563
+ )
564
+ elif target_config.get("type") == "github":
565
+ github_token = os.environ.get("GITHUB_TOKEN")
566
+ fastapi_extractor = FastAPIExtractor.from_github_repo(
567
+ repo=target_config.get("repo"),
568
+ path=target_config.get("path", "app/models.py"),
569
+ token=github_token,
570
+ )
571
+ else:
572
+ click.echo("โŒ No valid FastAPI configuration found")
573
+ sys.exit(1)
574
+
575
+ except Exception as e:
576
+ click.echo(f"โŒ Error initializing FastAPI extractor: {e}")
577
+ sys.exit(1)
578
+
579
+ # Run validation
580
+ try:
581
+ validator = ContractValidator(
582
+ source_extractor=dbt_extractor, target_extractor=fastapi_extractor
583
+ )
584
+
585
+ result = validator.validate()
586
+
587
+ # Output results
588
+ if output == "json":
589
+ click.echo(json.dumps(result.to_dict(), indent=2))
590
+ elif output == "github":
591
+ _output_github_actions(result)
592
+ else:
593
+ _output_terminal(result)
594
+
595
+ # Exit with appropriate code
596
+ validation_config = config_data.get("validation", {})
597
+ fail_on = validation_config.get(
598
+ "fail_on", ["missing_tables", "missing_required_columns"]
599
+ )
600
+
601
+ if "missing_tables" in fail_on and any(
602
+ "Missing Table" in issue.category for issue in result.critical_issues
603
+ ):
604
+ sys.exit(1)
605
+ elif "missing_required_columns" in fail_on and any(
606
+ "Missing Column" in issue.category for issue in result.critical_issues
607
+ ):
608
+ sys.exit(1)
609
+ elif result.critical_issues:
610
+ sys.exit(1)
611
+
612
+ except Exception as e:
613
+ click.echo(f"โŒ Validation error: {e}")
614
+ sys.exit(1)
615
+
616
+
617
+ def _output_terminal(result):
618
+ """Output results to terminal with emojis and colors."""
619
+ click.echo(f"\n๐Ÿ›ก๏ธ Data Contract Validation Results:")
620
+ click.echo("=" * 45)
621
+ click.echo(f"Status: {'โœ… PASSED' if result.success else 'โŒ FAILED'}")
622
+ click.echo(f"Total issues: {len(result.issues)}")
623
+ click.echo(f"Critical: {len(result.critical_issues)}")
624
+ click.echo(f"Warnings: {len(result.warnings)}")
625
+
626
+ if result.critical_issues:
627
+ click.echo("\n๐Ÿšจ Critical Issues (Must Fix):")
628
+ for issue in result.critical_issues:
629
+ click.echo(f" ๐Ÿ’ฅ {issue.table}")
630
+ if issue.column:
631
+ click.echo(f" Column: {issue.column}")
632
+ click.echo(f" Problem: {issue.message}")
633
+ if issue.suggested_fix:
634
+ click.echo(f" ๐Ÿ”ง Fix: {issue.suggested_fix}")
635
+ click.echo()
636
+
637
+ if result.warnings and not result.critical_issues:
638
+ click.echo("\nโš ๏ธ Warnings (Good to Know):")
639
+ for issue in result.warnings[:5]:
640
+ click.echo(f" โš ๏ธ {issue.table}.{issue.column}: {issue.message}")
641
+
642
+ if len(result.warnings) > 5:
643
+ click.echo(f" ... and {len(result.warnings) - 5} more warnings")
644
+
645
+ click.echo(f"\n{result.summary}")
646
+
647
+ if result.success:
648
+ click.echo("\n๐ŸŽ‰ Great! Your API contracts are protected.")
649
+ else:
650
+ click.echo("\n๐Ÿ’ก Fix the critical issues above to proceed.")
651
+
652
+
653
+ def _output_github_actions(result):
654
+ """Output results for GitHub Actions."""
655
+ if result.success:
656
+ click.echo("โœ… Data contract validation passed")
657
+ click.echo(f"::notice::Validation successful - {result.summary}")
658
+ else:
659
+ click.echo("โŒ Data contract validation failed")
660
+ click.echo(f"::error::Validation failed - {result.summary}")
661
+
662
+ for issue in result.critical_issues:
663
+ click.echo(f"::error::{issue.table}.{issue.column}: {issue.message}")
664
+
665
+
666
+ def main():
667
+ """Main entry point."""
668
+ cli()
669
+
670
+
671
+ if __name__ == "__main__":
672
+ main()
File without changes