|
13 | 13 | # limitations under the License. |
14 | 14 | import json |
15 | 15 | from pathlib import Path |
16 | | -from typing import List, Optional |
| 16 | +from dataclasses import dataclass, field |
| 17 | +from typing import Dict, List, Optional |
17 | 18 |
|
18 | 19 | import typer |
| 20 | +from snowflake.cli._plugins.dcm.manager import AnalysisType |
19 | 21 | from snowflake.cli._plugins.dcm.manager import DCMProjectManager |
20 | 22 | from snowflake.cli._plugins.dcm.utils import ( |
21 | 23 | TestResultFormat, |
|
56 | 58 | is_hidden=FeatureFlag.ENABLE_SNOWFLAKE_PROJECTS.is_disabled, |
57 | 59 | ) |
58 | 60 |
|
| 61 | + |
59 | 62 | dcm_identifier = identifier_argument(sf_object="DCM Project", example="MY_PROJECT") |
60 | 63 | variables_flag = variables_option( |
61 | 64 | 'Variables for the execution context; for example: `-D "<key>=<value>"`.' |
@@ -376,6 +379,124 @@ def preview( |
376 | 379 | return QueryResult(result) |
377 | 380 |
|
378 | 381 |
|
| 382 | +@app.command(requires_connection=True) |
| 383 | +def analyze( |
| 384 | + identifier: FQN = dcm_identifier, |
| 385 | + from_location: Optional[str] = from_option, |
| 386 | + variables: Optional[List[str]] = variables_flag, |
| 387 | + configuration: Optional[str] = configuration_flag, |
| 388 | + analysis_type: Optional[AnalysisType] = typer.Option( |
| 389 | + None, |
| 390 | + "--type", |
| 391 | + help="Type of analysis to perform.", |
| 392 | + show_default=False, |
| 393 | + case_sensitive=False, |
| 394 | + ), |
| 395 | + output_path: Optional[str] = output_path_option( |
| 396 | + help="Path where the analysis result will be stored. Can be a stage path (starting with '@') or a local directory path." |
| 397 | + ), |
| 398 | + **options, |
| 399 | +): |
| 400 | + """ |
| 401 | + Analyzes a DCM Project. |
| 402 | + """ |
| 403 | + manager = DCMProjectManager() |
| 404 | + effective_stage = _get_effective_stage(identifier, from_location) |
| 405 | + |
| 406 | + with cli_console.spinner() as spinner: |
| 407 | + spinner.add_task(description=f"Analyzing dcm project {identifier}", total=None) |
| 408 | + result = manager.analyze( |
| 409 | + project_identifier=identifier, |
| 410 | + configuration=configuration, |
| 411 | + from_stage=effective_stage, |
| 412 | + variables=variables, |
| 413 | + analysis_type=analysis_type, |
| 414 | + output_path=output_path, |
| 415 | + ) |
| 416 | + |
| 417 | + row = result.fetchone() |
| 418 | + if not row: |
| 419 | + return MessageResult("No data.") |
| 420 | + |
| 421 | + result_data = row[0] |
| 422 | + result_json = ( |
| 423 | + json.loads(result_data) if isinstance(result_data, str) else result_data |
| 424 | + ) |
| 425 | + |
| 426 | + summary = _analyze_result_summary(result_json) |
| 427 | + |
| 428 | + if summary.has_errors: |
| 429 | + error_message = _format_error_message(summary) |
| 430 | + raise CliError(error_message) |
| 431 | + |
| 432 | + return MessageResult( |
| 433 | + f"✓ Analysis complete: {summary.total_files} file(s) analyzed, " |
| 434 | + f"{summary.total_definitions} definition(s) found. No errors detected." |
| 435 | + ) |
| 436 | + |
| 437 | + |
| 438 | +@dataclass |
| 439 | +class AnalysisSummary: |
| 440 | + total_files: int = 0 |
| 441 | + total_definitions: int = 0 |
| 442 | + files_with_errors: int = 0 |
| 443 | + total_errors: int = 0 |
| 444 | + errors_by_file: Dict[str, List[str]] = field(default_factory=dict) |
| 445 | + has_errors: bool = False |
| 446 | + |
| 447 | + |
| 448 | +def _analyze_result_summary(result_json) -> AnalysisSummary: |
| 449 | + summary = AnalysisSummary() |
| 450 | + |
| 451 | + if not isinstance(result_json, dict): |
| 452 | + return summary |
| 453 | + |
| 454 | + files = result_json.get("files", []) |
| 455 | + summary.total_files = len(files) |
| 456 | + |
| 457 | + for file_info in files: |
| 458 | + source_path = file_info.get("sourcePath", "unknown") |
| 459 | + file_errors = [] |
| 460 | + |
| 461 | + # Check file-level errors |
| 462 | + for error in file_info.get("errors", []): |
| 463 | + error_msg = error.get("message", "Unknown error") |
| 464 | + file_errors.append(error_msg) |
| 465 | + summary.total_errors += 1 |
| 466 | + |
| 467 | + # Check definition-level errors |
| 468 | + definitions = file_info.get("definitions", []) |
| 469 | + summary.total_definitions += len(definitions) |
| 470 | + |
| 471 | + for definition in definitions: |
| 472 | + for error in definition.get("errors", []): |
| 473 | + error_msg = error.get("message", "Unknown error") |
| 474 | + file_errors.append(error_msg) |
| 475 | + summary.total_errors += 1 |
| 476 | + |
| 477 | + if file_errors: |
| 478 | + summary.errors_by_file[source_path] = file_errors |
| 479 | + summary.files_with_errors += 1 |
| 480 | + summary.has_errors = True |
| 481 | + |
| 482 | + return summary |
| 483 | + |
| 484 | + |
| 485 | +def _format_error_message(summary: AnalysisSummary) -> str: |
| 486 | + lines = [ |
| 487 | + f"Analysis found {summary.total_errors} error(s) in {summary.files_with_errors} file(s):", |
| 488 | + "", |
| 489 | + ] |
| 490 | + |
| 491 | + for file_path, errors in summary.errors_by_file.items(): |
| 492 | + lines.append(f" {file_path}:") |
| 493 | + for error in errors: |
| 494 | + lines.append(f" • {error}") |
| 495 | + lines.append("") |
| 496 | + |
| 497 | + return "\n".join(lines).rstrip() |
| 498 | + |
| 499 | + |
379 | 500 | def _get_effective_stage(identifier: FQN, from_location: Optional[str]): |
380 | 501 | manager = DCMProjectManager() |
381 | 502 | if not from_location: |
|
0 commit comments