datasecops-cli 0.5.2__tar.gz → 0.5.4__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 (57) hide show
  1. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/CHANGELOG.md +15 -0
  2. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/PKG-INFO +1 -1
  3. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/mcp-servers.json +2 -2
  4. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/pyproject.toml +1 -1
  5. datasecops_cli-0.5.4/src/datasecops_cli/__init__.py +1 -0
  6. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/src/datasecops_mcp/server.py +69 -17
  7. datasecops_cli-0.5.2/src/datasecops_cli/__init__.py +0 -1
  8. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/.github/workflows/auto-tag.yml +0 -0
  9. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/.github/workflows/publish-cli.yml +0 -0
  10. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/.github/workflows/test.yml +0 -0
  11. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/.gitignore +0 -0
  12. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/DEVELOPMENT.md +0 -0
  13. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/LICENSE +0 -0
  14. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/README.md +0 -0
  15. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/docs/getting-started.md +0 -0
  16. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/docs/git-operations.md +0 -0
  17. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/docs/legacy.md +0 -0
  18. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/docs/legacy_plan_of_action.md +0 -0
  19. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/docs/mcp-server.md +0 -0
  20. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/setup.ps1 +0 -0
  21. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/setup.sh +0 -0
  22. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/src/datasecops_cli/config.py +0 -0
  23. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/src/datasecops_cli/main.py +0 -0
  24. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/src/datasecops_cli/menus/__init__.py +0 -0
  25. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/src/datasecops_cli/menus/configuration.py +0 -0
  26. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/src/datasecops_cli/menus/development.py +0 -0
  27. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/src/datasecops_cli/menus/downloads.py +0 -0
  28. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/src/datasecops_cli/menus/git_operations.py +0 -0
  29. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/src/datasecops_cli/models/__init__.py +0 -0
  30. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/src/datasecops_cli/models/git_helpers.py +0 -0
  31. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/src/datasecops_cli/models/project_config.py +0 -0
  32. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/src/datasecops_cli/services/__init__.py +0 -0
  33. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/src/datasecops_cli/services/bootstrap_service.py +0 -0
  34. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/src/datasecops_cli/services/dbt_project_generator.py +0 -0
  35. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/src/datasecops_cli/services/dbt_runner.py +0 -0
  36. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/src/datasecops_cli/services/directory_scaffolder.py +0 -0
  37. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/src/datasecops_cli/services/download_service.py +0 -0
  38. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/src/datasecops_cli/services/git_service.py +0 -0
  39. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/src/datasecops_cli/services/linting_service.py +0 -0
  40. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/src/datasecops_cli/services/skill_service.py +0 -0
  41. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/src/datasecops_cli/services/snowflake_service.py +0 -0
  42. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/src/datasecops_cli/services/upstream_service.py +0 -0
  43. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/src/datasecops_cli/utilities/__init__.py +0 -0
  44. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/src/datasecops_cli/utilities/display.py +0 -0
  45. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/src/datasecops_cli/utilities/file_utils.py +0 -0
  46. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/src/datasecops_cli/utilities/mcp.py +0 -0
  47. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/src/datasecops_cli/utilities/yaml_utils.py +0 -0
  48. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/src/datasecops_mcp/__init__.py +0 -0
  49. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/src/datasecops_mcp/__main__.py +0 -0
  50. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/src/datasecops_mcp/connection.py +0 -0
  51. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/tests/__init__.py +0 -0
  52. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/tests/test_config.py +0 -0
  53. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/tests/test_file_utils.py +0 -0
  54. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/tests/test_main.py +0 -0
  55. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/tests/test_models.py +0 -0
  56. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/tests/test_version.py +0 -0
  57. {datasecops_cli-0.5.2 → datasecops_cli-0.5.4}/tests/test_yaml_utils.py +0 -0
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to the DataSecOps CLI are documented in this file.
4
4
 
5
+ ## [0.5.4] - 2026-06-05
6
+
7
+ ### Changed
8
+
9
+ - **`get_linting_rules` returns `.sqlfluff` INI format** — the MCP tool now returns the rendered `.sqlfluff` config file content (INI format) instead of raw JSON, matching what `get_sqlfluff_config` returns and what SQLFluff actually consumes. Returns an empty string (not a prose message) when no config is found, so callers can reliably detect the empty case.
10
+ - **MCP server uses project `.venv`** — `mcp-servers.json` now points `command` to `${PROJECT_DIR}/.venv/${VENV_BIN:-bin}/datasecops-mcp`, defaulting to `bin` (Unix) with `VENV_BIN=Scripts` for Windows, making the config cross-platform.
11
+
12
+ ## [0.5.3] - 2026-06-05
13
+
14
+ ### Changed
15
+
16
+ - **MCP linting tools report progress** — `lint_sql`, `fix_sql`, and `lint_project` now use MCP `Context` to send progress notifications and info messages during execution, so callers can see what stage the tool is at (project discovery, running SQLFluff, processing results).
17
+ - **MCP linting tools no longer block the event loop** — subprocess calls to SQLFluff are wrapped in `asyncio.to_thread` so the MCP server remains responsive while linting runs.
18
+ - **`_ensure_dbt_packages` skips `dbt deps` when packages exist** — `dbt deps` is only triggered when `dbt_packages/` is actually empty, but the `dbt-snowflake` Python package check always runs to ensure the dbt templater works.
19
+
5
20
  ## [0.5.2] - 2026-06-05
6
21
 
7
22
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datasecops-cli
3
- Version: 0.5.2
3
+ Version: 0.5.4
4
4
  Summary: DataSecOps Framework CLI for Snowflake Native App
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -2,9 +2,9 @@
2
2
  "mcpServers": {
3
3
  "datasecops-framework": {
4
4
  "type": "stdio",
5
- "command": "datasecops-mcp",
5
+ "command": "${PROJECT_DIR}/.venv/${VENV_BIN:-bin}/datasecops-mcp",
6
6
  "args": ["--connection-name", "<CONNECTION>", "--app-database", "<APP_DB>"],
7
- "description": "DataSecOps Framework governance rules, branching conventions, linting config, and project profiles from your Snowflake Native App"
7
+ "description": "DataSecOps Framework governance rules, branching conventions, linting config, and project profiles from your Snowflake Native App. Set VENV_BIN=Scripts on Windows."
8
8
  },
9
9
  "github": {
10
10
  "type": "stdio",
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "datasecops-cli"
7
- version = "0.5.2"
7
+ version = "0.5.4"
8
8
  description = "DataSecOps Framework CLI for Snowflake Native App"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1 @@
1
+ __version__ = "0.5.3"
@@ -8,7 +8,7 @@ dbt packages, project profiles, and deployment settings in real-time.
8
8
  import json
9
9
  import logging
10
10
 
11
- from mcp.server.fastmcp import FastMCP
11
+ from mcp.server.fastmcp import Context, FastMCP
12
12
 
13
13
  from datasecops_mcp.connection import get_snowflake_service
14
14
  from datasecops_cli.models.project_config import DEFAULT_BRANCH_FORMAT
@@ -64,16 +64,18 @@ def get_work_items_config() -> str:
64
64
  def get_linting_rules() -> str:
65
65
  """Get the SQLFluff linting rules configured for this project.
66
66
 
67
- Returns the active SQL linting rules including dialect, indentation settings,
68
- and enabled rule codes. Use this when writing or reviewing SQL to ensure
67
+ Returns the .sqlfluff INI config file content (dialect, indentation settings,
68
+ and enabled rule codes). Use this when writing or reviewing SQL to ensure
69
69
  compliance with the team's standards.
70
+
71
+ Returns an empty string if no configuration is found in the native app.
70
72
  """
71
73
  sf = get_snowflake_service()
72
- raw = sf.get_framework_config("SQLFLUFF_RULES")
74
+ raw = sf.get_sqlfluff_config_file()
73
75
  if not raw:
74
- return "No SQLFluff rules found in native app"
76
+ return ""
75
77
 
76
- return json.dumps(raw, indent=2)
78
+ return raw
77
79
 
78
80
 
79
81
  @mcp.tool()
@@ -429,7 +431,7 @@ def get_deployment_workflow() -> str:
429
431
 
430
432
 
431
433
  @mcp.tool()
432
- def lint_sql(file_path: str) -> str:
434
+ async def lint_sql(file_path: str, ctx: Context) -> str:
433
435
  """Lint a SQL file using SQLFluff with the project's configured rules.
434
436
 
435
437
  Args:
@@ -438,11 +440,15 @@ def lint_sql(file_path: str) -> str:
438
440
  Returns the linting results including rule violations, line numbers,
439
441
  and descriptions. Uses the project's .sqlfluff config from the framework.
440
442
  """
443
+ import asyncio
441
444
  import subprocess
442
445
  from pathlib import Path
443
446
 
447
+ await ctx.report_progress(0, 3)
448
+ await ctx.info(f"Finding dbt project for: {file_path}")
444
449
  project_dir = _find_project_dir()
445
450
  _ensure_dbt_packages(project_dir)
451
+
446
452
  target = Path(file_path)
447
453
  if not target.is_absolute():
448
454
  target = project_dir / target
@@ -450,12 +456,20 @@ def lint_sql(file_path: str) -> str:
450
456
  if not target.exists():
451
457
  return json.dumps({"error": f"File not found: {target}"})
452
458
 
459
+ await ctx.report_progress(1, 3)
460
+ await ctx.info(f"Running SQLFluff lint on {target.name}...")
461
+
453
462
  cmd = ["sqlfluff", "lint", str(target), "--format", "json"]
454
463
  config_path = project_dir / ".sqlfluff"
455
464
  if config_path.exists():
456
465
  cmd += ["--config", str(config_path)]
457
466
 
458
- result = subprocess.run(cmd, capture_output=True, text=True, cwd=str(project_dir))
467
+ result = await asyncio.to_thread(
468
+ subprocess.run, cmd, capture_output=True, text=True, cwd=str(project_dir)
469
+ )
470
+
471
+ await ctx.report_progress(2, 3)
472
+ await ctx.info("Processing lint results...")
459
473
 
460
474
  # SQLFluff returns exit code 1 when violations are found (not an error)
461
475
  if result.returncode > 1:
@@ -464,14 +478,17 @@ def lint_sql(file_path: str) -> str:
464
478
  if result.stdout.strip():
465
479
  try:
466
480
  violations = json.loads(result.stdout)
481
+ await ctx.report_progress(3, 3)
467
482
  return json.dumps(violations, indent=2)
468
483
  except json.JSONDecodeError:
484
+ await ctx.report_progress(3, 3)
469
485
  return result.stdout
486
+ await ctx.report_progress(3, 3)
470
487
  return json.dumps({"status": "clean", "message": "No linting issues found"})
471
488
 
472
489
 
473
490
  @mcp.tool()
474
- def fix_sql(file_path: str) -> str:
491
+ async def fix_sql(file_path: str, ctx: Context) -> str:
475
492
  """Auto-fix linting issues in a SQL file using SQLFluff.
476
493
 
477
494
  Args:
@@ -480,11 +497,15 @@ def fix_sql(file_path: str) -> str:
480
497
  Applies automatic fixes using the project's .sqlfluff config.
481
498
  Returns the fix results and what was changed.
482
499
  """
500
+ import asyncio
483
501
  import subprocess
484
502
  from pathlib import Path
485
503
 
504
+ await ctx.report_progress(0, 3)
505
+ await ctx.info(f"Finding dbt project for: {file_path}")
486
506
  project_dir = _find_project_dir()
487
507
  _ensure_dbt_packages(project_dir)
508
+
488
509
  target = Path(file_path)
489
510
  if not target.is_absolute():
490
511
  target = project_dir / target
@@ -492,12 +513,20 @@ def fix_sql(file_path: str) -> str:
492
513
  if not target.exists():
493
514
  return json.dumps({"error": f"File not found: {target}"})
494
515
 
516
+ await ctx.report_progress(1, 3)
517
+ await ctx.info(f"Running SQLFluff fix on {target.name}...")
518
+
495
519
  cmd = ["sqlfluff", "fix", str(target), "--force", "--format", "json"]
496
520
  config_path = project_dir / ".sqlfluff"
497
521
  if config_path.exists():
498
522
  cmd += ["--config", str(config_path)]
499
523
 
500
- result = subprocess.run(cmd, capture_output=True, text=True, cwd=str(project_dir))
524
+ result = await asyncio.to_thread(
525
+ subprocess.run, cmd, capture_output=True, text=True, cwd=str(project_dir)
526
+ )
527
+
528
+ await ctx.report_progress(2, 3)
529
+ await ctx.info("Processing fix results...")
501
530
 
502
531
  if result.returncode > 1:
503
532
  return json.dumps({"error": f"SQLFluff fix failed: {result.stderr.strip()}"})
@@ -505,14 +534,17 @@ def fix_sql(file_path: str) -> str:
505
534
  if result.stdout.strip():
506
535
  try:
507
536
  fix_results = json.loads(result.stdout)
537
+ await ctx.report_progress(3, 3)
508
538
  return json.dumps(fix_results, indent=2)
509
539
  except json.JSONDecodeError:
540
+ await ctx.report_progress(3, 3)
510
541
  return result.stdout
542
+ await ctx.report_progress(3, 3)
511
543
  return json.dumps({"status": "fixed", "message": f"Fixes applied to {target.name}"})
512
544
 
513
545
 
514
546
  @mcp.tool()
515
- def lint_project(fix: bool = False) -> str:
547
+ async def lint_project(fix: bool, ctx: Context) -> str:
516
548
  """Lint all SQL models in the dbt project.
517
549
 
518
550
  Args:
@@ -521,16 +553,23 @@ def lint_project(fix: bool = False) -> str:
521
553
  Lints the models/ directory using the project's .sqlfluff config.
522
554
  Returns a summary of violations or fixes applied.
523
555
  """
556
+ import asyncio
524
557
  import subprocess
525
558
  from pathlib import Path
526
559
 
560
+ action = "fix" if fix else "lint"
561
+
562
+ await ctx.report_progress(0, 3)
563
+ await ctx.info("Finding dbt project...")
527
564
  project_dir = _find_project_dir()
528
565
  _ensure_dbt_packages(project_dir)
529
566
  models_dir = project_dir / "models"
530
567
  if not models_dir.exists():
531
568
  return json.dumps({"error": f"No models directory found at {models_dir}"})
532
569
 
533
- action = "fix" if fix else "lint"
570
+ await ctx.report_progress(1, 3)
571
+ await ctx.info(f"Running SQLFluff {action} on models/...")
572
+
534
573
  cmd = ["sqlfluff", action, str(models_dir), "--format", "json"]
535
574
  if fix:
536
575
  cmd.append("--force")
@@ -539,7 +578,12 @@ def lint_project(fix: bool = False) -> str:
539
578
  if config_path.exists():
540
579
  cmd += ["--config", str(config_path)]
541
580
 
542
- result = subprocess.run(cmd, capture_output=True, text=True, cwd=str(project_dir))
581
+ result = await asyncio.to_thread(
582
+ subprocess.run, cmd, capture_output=True, text=True, cwd=str(project_dir)
583
+ )
584
+
585
+ await ctx.report_progress(2, 3)
586
+ await ctx.info("Processing results...")
543
587
 
544
588
  if result.returncode > 1:
545
589
  return json.dumps({"error": f"SQLFluff {action} failed: {result.stderr.strip()}"})
@@ -560,10 +604,14 @@ def lint_project(fix: bool = False) -> str:
560
604
  "truncated": True,
561
605
  "message": f"Showing first 5 of {len(output)} files",
562
606
  }
607
+ await ctx.report_progress(3, 3)
563
608
  return json.dumps(summary, indent=2)
609
+ await ctx.report_progress(3, 3)
564
610
  return json.dumps(output, indent=2)
565
611
  except json.JSONDecodeError:
612
+ await ctx.report_progress(3, 3)
566
613
  return result.stdout
614
+ await ctx.report_progress(3, 3)
567
615
  return json.dumps({"status": "clean", "action": action, "message": "No issues found"})
568
616
 
569
617
 
@@ -580,15 +628,18 @@ def _find_project_dir() -> "Path":
580
628
 
581
629
 
582
630
  def _ensure_dbt_packages(project_dir: "Path") -> None:
583
- """Ensure dbt-snowflake and dbt packages are available for sqlfluff dbt templater."""
631
+ """Ensure dbt-snowflake and dbt packages are available for sqlfluff dbt templater.
632
+
633
+ Skips expensive dbt deps if packages are already present, but always
634
+ ensures the dbt-snowflake Python package is importable.
635
+ """
584
636
  import subprocess
585
637
  from pathlib import Path
586
638
 
587
- # Check that dbt-snowflake Python package is installed (required by sqlfluff-templater-dbt)
639
+ # Always ensure dbt-snowflake Python package is available (required by sqlfluff-templater-dbt)
588
640
  try:
589
641
  import dbt.adapters.snowflake # noqa: F401
590
642
  except ImportError:
591
- # Attempt to install dbt-snowflake
592
643
  subprocess.run(
593
644
  ["uv", "pip", "install", "dbt-snowflake"],
594
645
  capture_output=True, text=True,
@@ -609,8 +660,9 @@ def _ensure_dbt_packages(project_dir: "Path") -> None:
609
660
  if not packages_yml.exists():
610
661
  return # No packages to install
611
662
 
612
- # Run dbt deps to install packages
663
+ # Packages are missing — run dbt deps to install them
613
664
  import shutil
665
+
614
666
  for cmd in [["dbtf", "deps"], ["dbt", "deps"]]:
615
667
  if shutil.which(cmd[0]):
616
668
  subprocess.run(cmd, capture_output=True, text=True, cwd=str(project_dir))
@@ -1 +0,0 @@
1
- __version__ = "0.5.2"
File without changes
File without changes
File without changes
File without changes