datasecops-cli 0.1.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.
- datasecops_cli/__init__.py +1 -0
- datasecops_cli/config.py +100 -0
- datasecops_cli/main.py +122 -0
- datasecops_cli/menus/__init__.py +1 -0
- datasecops_cli/menus/development.py +160 -0
- datasecops_cli/menus/downloads.py +79 -0
- datasecops_cli/menus/git_operations.py +213 -0
- datasecops_cli/models/__init__.py +1 -0
- datasecops_cli/models/git_helpers.py +29 -0
- datasecops_cli/models/project_config.py +87 -0
- datasecops_cli/services/__init__.py +1 -0
- datasecops_cli/services/dbt_runner.py +130 -0
- datasecops_cli/services/download_service.py +103 -0
- datasecops_cli/services/git_service.py +183 -0
- datasecops_cli/services/linting_service.py +47 -0
- datasecops_cli/services/skill_service.py +86 -0
- datasecops_cli/services/snowflake_service.py +62 -0
- datasecops_cli/utilities/__init__.py +1 -0
- datasecops_cli/utilities/display.py +122 -0
- datasecops_cli/utilities/file_utils.py +33 -0
- datasecops_cli/utilities/yaml_utils.py +39 -0
- datasecops_cli-0.1.0.dist-info/METADATA +16 -0
- datasecops_cli-0.1.0.dist-info/RECORD +26 -0
- datasecops_cli-0.1.0.dist-info/WHEEL +4 -0
- datasecops_cli-0.1.0.dist-info/entry_points.txt +2 -0
- datasecops_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
datasecops_cli/config.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from datasecops_cli.models.project_config import (
|
|
5
|
+
DatasecopsConfig, ProjectSettings, SourceControl, ProjectProfile, DbtTarget
|
|
6
|
+
)
|
|
7
|
+
from datasecops_cli.utilities.yaml_utils import read_datasecops_config, read_dbt_project
|
|
8
|
+
from datasecops_cli.utilities.display import error_line, info_line
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Config:
|
|
12
|
+
"""Central configuration for the CLI, loaded from .datasecops.yml and native app."""
|
|
13
|
+
|
|
14
|
+
def __init__(self):
|
|
15
|
+
self.datasecops: DatasecopsConfig = DatasecopsConfig()
|
|
16
|
+
self.project_settings: ProjectSettings = ProjectSettings()
|
|
17
|
+
self.source_control: SourceControl = SourceControl()
|
|
18
|
+
self.profile: Optional[ProjectProfile] = None
|
|
19
|
+
self.all_profiles: list[ProjectProfile] = []
|
|
20
|
+
self.project_dir: Path = Path.cwd()
|
|
21
|
+
self.dbt_project_dir: Path = Path.cwd()
|
|
22
|
+
self.profile_name: str = ""
|
|
23
|
+
|
|
24
|
+
def load(self, project_dir: Path = None) -> bool:
|
|
25
|
+
"""Load configuration from .datasecops.yml and dbt_project.yml."""
|
|
26
|
+
self.project_dir = project_dir or Path.cwd()
|
|
27
|
+
|
|
28
|
+
# Load .datasecops.yml
|
|
29
|
+
raw = read_datasecops_config(self.project_dir)
|
|
30
|
+
if raw is None:
|
|
31
|
+
error_line("No .datasecops.yml found. Run setup.sh/setup.ps1 first.")
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
self.datasecops = DatasecopsConfig(
|
|
35
|
+
connection_name=raw.get("connection_name", ""),
|
|
36
|
+
app_database=raw.get("app_database", ""),
|
|
37
|
+
profile_name=raw.get("profile_name", ""),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if not self.datasecops.connection_name or not self.datasecops.app_database:
|
|
41
|
+
error_line(".datasecops.yml is missing connection_name or app_database.")
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
# Load dbt_project.yml to get profile name
|
|
45
|
+
dbt_data = read_dbt_project(self.project_dir)
|
|
46
|
+
if dbt_data:
|
|
47
|
+
self.profile_name = dbt_data.get("profile", self.datasecops.profile_name)
|
|
48
|
+
else:
|
|
49
|
+
self.profile_name = self.datasecops.profile_name
|
|
50
|
+
|
|
51
|
+
# Resolve dbt project directory
|
|
52
|
+
for candidate in [self.project_dir / "dbt", self.project_dir]:
|
|
53
|
+
if (candidate / "dbt_project.yml").exists():
|
|
54
|
+
self.dbt_project_dir = candidate
|
|
55
|
+
break
|
|
56
|
+
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
def load_from_native_app(self, snowflake_service) -> bool:
|
|
60
|
+
"""Load configuration from the native app database."""
|
|
61
|
+
try:
|
|
62
|
+
# Load project settings
|
|
63
|
+
raw = snowflake_service.get_framework_config("PROJECT")
|
|
64
|
+
if raw:
|
|
65
|
+
self.project_settings = ProjectSettings(**{k: v for k, v in raw.items() if k in ProjectSettings.model_fields})
|
|
66
|
+
|
|
67
|
+
# Load source control
|
|
68
|
+
raw = snowflake_service.get_framework_config("SOURCE_CONTROL")
|
|
69
|
+
if raw:
|
|
70
|
+
self.source_control = SourceControl(**{k: v for k, v in raw.items() if k in SourceControl.model_fields})
|
|
71
|
+
|
|
72
|
+
# Load project profiles
|
|
73
|
+
profiles_data = snowflake_service.get_project_profiles()
|
|
74
|
+
if profiles_data:
|
|
75
|
+
self.all_profiles = [ProjectProfile(**{k.lower(): v for k, v in p.items()}) for p in profiles_data]
|
|
76
|
+
|
|
77
|
+
# Find the active profile
|
|
78
|
+
if self.profile_name:
|
|
79
|
+
for p in self.all_profiles:
|
|
80
|
+
if p.profile_name == self.profile_name:
|
|
81
|
+
self.profile = p
|
|
82
|
+
break
|
|
83
|
+
|
|
84
|
+
if not self.profile and self.all_profiles:
|
|
85
|
+
self.profile = self.all_profiles[0]
|
|
86
|
+
self.profile_name = self.profile.profile_name
|
|
87
|
+
|
|
88
|
+
return True
|
|
89
|
+
except Exception as e:
|
|
90
|
+
error_line(f"Failed to load from native app: {e}")
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
def get_dbt_profiles_dir(self) -> Path:
|
|
94
|
+
return Path(self.project_settings.profile_dir).expanduser()
|
|
95
|
+
|
|
96
|
+
def get_default_target(self) -> Optional[DbtTarget]:
|
|
97
|
+
return self.project_settings.get_default_target()
|
|
98
|
+
|
|
99
|
+
def get_deployment_branches(self) -> dict[str, str]:
|
|
100
|
+
return {b: b for b in self.project_settings.get_deployment_branches()}
|
datasecops_cli/main.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from datasecops_cli.config import Config
|
|
5
|
+
from datasecops_cli.services.snowflake_service import SnowflakeService
|
|
6
|
+
from datasecops_cli.services.dbt_runner import DbtRunner
|
|
7
|
+
from datasecops_cli.services.git_service import GitService
|
|
8
|
+
from datasecops_cli.services.linting_service import LintingService
|
|
9
|
+
from datasecops_cli.services.download_service import DownloadService
|
|
10
|
+
from datasecops_cli.services.skill_service import SkillService
|
|
11
|
+
from datasecops_cli.menus.development import DevelopmentMenu
|
|
12
|
+
from datasecops_cli.menus.git_operations import GitOperationsMenu
|
|
13
|
+
from datasecops_cli.menus.downloads import DownloadsMenu
|
|
14
|
+
from datasecops_cli.utilities.display import (
|
|
15
|
+
clear, section_header, menu_option, get_input_number,
|
|
16
|
+
info_line, error_line, success_line
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main():
|
|
21
|
+
"""Main entry point for the datasecops CLI."""
|
|
22
|
+
config = Config()
|
|
23
|
+
|
|
24
|
+
if not config.load():
|
|
25
|
+
sys.exit(1)
|
|
26
|
+
|
|
27
|
+
# Connect to Snowflake
|
|
28
|
+
sf_config = config.datasecops
|
|
29
|
+
sf_service = SnowflakeService(sf_config)
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
info_line(f"Connecting to Snowflake ({sf_config.connection_name})...")
|
|
33
|
+
sf_service.connect()
|
|
34
|
+
success_line("Connected")
|
|
35
|
+
except Exception as e:
|
|
36
|
+
error_line(f"Failed to connect to Snowflake: {e}")
|
|
37
|
+
sys.exit(1)
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
# Load config from native app
|
|
41
|
+
info_line("Loading framework configuration...")
|
|
42
|
+
config.load_from_native_app(sf_service)
|
|
43
|
+
|
|
44
|
+
if not config.profile:
|
|
45
|
+
error_line(f"Profile '{config.profile_name}' not found in native app")
|
|
46
|
+
sys.exit(1)
|
|
47
|
+
|
|
48
|
+
success_line(f"Profile: {config.profile.project_name} ({config.profile_name})")
|
|
49
|
+
|
|
50
|
+
# Initialize services
|
|
51
|
+
dbt_runner = DbtRunner(
|
|
52
|
+
project_dir=config.dbt_project_dir,
|
|
53
|
+
profiles_dir=config.get_dbt_profiles_dir(),
|
|
54
|
+
target=config.project_settings.get_default_target().target_name if config.get_default_target() else "dev"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
git_service = GitService(config.project_dir)
|
|
59
|
+
except Exception:
|
|
60
|
+
git_service = None
|
|
61
|
+
|
|
62
|
+
linting_service = LintingService(config.dbt_project_dir)
|
|
63
|
+
download_service = DownloadService(sf_service, config.project_dir)
|
|
64
|
+
skill_service = SkillService(sf_service)
|
|
65
|
+
|
|
66
|
+
# Main menu loop
|
|
67
|
+
_main_menu(config, dbt_runner, git_service, linting_service,
|
|
68
|
+
download_service, skill_service, sf_service)
|
|
69
|
+
|
|
70
|
+
finally:
|
|
71
|
+
sf_service.close()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _main_menu(config: Config, dbt_runner: DbtRunner, git_service: GitService,
|
|
75
|
+
linting_service: LintingService, download_service: DownloadService,
|
|
76
|
+
skill_service: SkillService, sf_service: SnowflakeService):
|
|
77
|
+
"""Main menu loop."""
|
|
78
|
+
profile_name = config.profile_name
|
|
79
|
+
|
|
80
|
+
_show_main_menu(profile_name, git_service)
|
|
81
|
+
option = get_input_number("Choose an option: ")
|
|
82
|
+
|
|
83
|
+
while option != 0:
|
|
84
|
+
if option == 1:
|
|
85
|
+
if git_service:
|
|
86
|
+
dev_menu = DevelopmentMenu(dbt_runner, linting_service, git_service, profile_name)
|
|
87
|
+
else:
|
|
88
|
+
dev_menu = DevelopmentMenu(dbt_runner, linting_service, None, profile_name)
|
|
89
|
+
dev_menu.show()
|
|
90
|
+
|
|
91
|
+
elif option == 2 and git_service:
|
|
92
|
+
git_menu = GitOperationsMenu(
|
|
93
|
+
git_service, config.source_control,
|
|
94
|
+
config.get_deployment_branches(), profile_name
|
|
95
|
+
)
|
|
96
|
+
git_menu.show()
|
|
97
|
+
|
|
98
|
+
elif option == 3:
|
|
99
|
+
dl_menu = DownloadsMenu(
|
|
100
|
+
download_service, skill_service, dbt_runner,
|
|
101
|
+
profile_name, config.dbt_project_dir
|
|
102
|
+
)
|
|
103
|
+
dl_menu.show()
|
|
104
|
+
|
|
105
|
+
_show_main_menu(profile_name, git_service)
|
|
106
|
+
option = get_input_number("Choose an option: ")
|
|
107
|
+
|
|
108
|
+
info_line("Exiting DataSecOps Framework CLI")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _show_main_menu(profile_name: str, git_service: GitService = None):
|
|
112
|
+
clear()
|
|
113
|
+
branch = git_service.get_current_branch() if git_service else None
|
|
114
|
+
section_header("DataSecOps Framework CLI", profile_name, branch)
|
|
115
|
+
menu_option(1, "development - dbt Development Commands")
|
|
116
|
+
menu_option(2, "git - Source Control Operations")
|
|
117
|
+
menu_option(3, "downloads - Download Configs & Skills")
|
|
118
|
+
menu_option(0, "exit - Exit")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
if __name__ == "__main__":
|
|
122
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from datasecops_cli.services.dbt_runner import DbtRunner
|
|
2
|
+
from datasecops_cli.services.linting_service import LintingService
|
|
3
|
+
from datasecops_cli.services.git_service import GitService
|
|
4
|
+
from datasecops_cli.utilities.display import (
|
|
5
|
+
clear, section_header, display_action_header, menu_option,
|
|
6
|
+
get_input_number, get_input_string, get_input_true_false, complete_action,
|
|
7
|
+
info_line, error_line
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DevelopmentMenu:
|
|
12
|
+
def __init__(self, dbt_runner: DbtRunner, linting_service: LintingService,
|
|
13
|
+
git_service: GitService, profile_name: str):
|
|
14
|
+
self.dbt = dbt_runner
|
|
15
|
+
self.linting = linting_service
|
|
16
|
+
self.git = git_service
|
|
17
|
+
self.profile_name = profile_name
|
|
18
|
+
|
|
19
|
+
def show(self) -> None:
|
|
20
|
+
self._menu()
|
|
21
|
+
option = get_input_number("Choose an option: ")
|
|
22
|
+
while option != 0:
|
|
23
|
+
if option == 1:
|
|
24
|
+
self._run_menu()
|
|
25
|
+
elif option == 2:
|
|
26
|
+
self._test_menu()
|
|
27
|
+
elif option == 3:
|
|
28
|
+
self._lint_menu()
|
|
29
|
+
elif option == 4:
|
|
30
|
+
display_action_header("dbt docs")
|
|
31
|
+
self.dbt.docs_generate()
|
|
32
|
+
if get_input_true_false("Serve docs?"):
|
|
33
|
+
self.dbt.docs_serve()
|
|
34
|
+
complete_action()
|
|
35
|
+
elif option == 5:
|
|
36
|
+
display_action_header("dbt seed")
|
|
37
|
+
self.dbt.seed()
|
|
38
|
+
complete_action()
|
|
39
|
+
elif option == 6:
|
|
40
|
+
display_action_header("dbt deps")
|
|
41
|
+
self.dbt.deps()
|
|
42
|
+
complete_action()
|
|
43
|
+
elif option == 7:
|
|
44
|
+
display_action_header("dbt compile")
|
|
45
|
+
self.dbt.compile()
|
|
46
|
+
complete_action()
|
|
47
|
+
elif option == 8:
|
|
48
|
+
display_action_header("dbt snapshot")
|
|
49
|
+
self.dbt.snapshot()
|
|
50
|
+
complete_action()
|
|
51
|
+
elif option == 9:
|
|
52
|
+
display_action_header("dbt source freshness")
|
|
53
|
+
self.dbt.source_freshness()
|
|
54
|
+
complete_action()
|
|
55
|
+
elif option == 10:
|
|
56
|
+
display_action_header("dbt clean")
|
|
57
|
+
self.dbt.clean()
|
|
58
|
+
complete_action()
|
|
59
|
+
elif option == 11:
|
|
60
|
+
display_action_header("dbt debug")
|
|
61
|
+
self.dbt.debug()
|
|
62
|
+
complete_action()
|
|
63
|
+
elif option == 12:
|
|
64
|
+
display_action_header("dbt list")
|
|
65
|
+
self.dbt.list_models()
|
|
66
|
+
complete_action()
|
|
67
|
+
elif option == 13:
|
|
68
|
+
display_action_header("dbt retry")
|
|
69
|
+
self.dbt.retry()
|
|
70
|
+
complete_action()
|
|
71
|
+
self._menu()
|
|
72
|
+
option = get_input_number("Choose an option: ")
|
|
73
|
+
|
|
74
|
+
def _menu(self) -> None:
|
|
75
|
+
clear()
|
|
76
|
+
section_header("Development Menu", self.profile_name, self.git.get_current_branch() if self.git else None)
|
|
77
|
+
menu_option(1, "run - Run dbt models")
|
|
78
|
+
menu_option(2, "test - Run dbt tests")
|
|
79
|
+
menu_option(3, "lint - SQLFluff linting")
|
|
80
|
+
menu_option(4, "docs - Generate & serve dbt docs")
|
|
81
|
+
menu_option(5, "seed - Load seed data")
|
|
82
|
+
menu_option(6, "deps - Install dbt packages")
|
|
83
|
+
menu_option(7, "compile - Compile dbt models")
|
|
84
|
+
menu_option(8, "snapshot - Run dbt snapshots")
|
|
85
|
+
menu_option(9, "freshness - Check source freshness")
|
|
86
|
+
menu_option(10, "clean - Clean dbt target")
|
|
87
|
+
menu_option(11, "debug - Debug dbt connection")
|
|
88
|
+
menu_option(12, "list - List dbt resources")
|
|
89
|
+
menu_option(13, "retry - Retry failed dbt run")
|
|
90
|
+
menu_option(0, "back - Return to main menu")
|
|
91
|
+
|
|
92
|
+
def _run_menu(self) -> None:
|
|
93
|
+
clear()
|
|
94
|
+
display_action_header("dbt Run Options")
|
|
95
|
+
menu_option(1, "full run - Run all models")
|
|
96
|
+
menu_option(2, "modified - Run modified models only (state:modified+)")
|
|
97
|
+
menu_option(3, "specific - Run specific model(s)")
|
|
98
|
+
menu_option(4, "macro - Run a dbt macro")
|
|
99
|
+
menu_option(5, "retry - Retry failed run")
|
|
100
|
+
menu_option(0, "back - Return to development menu")
|
|
101
|
+
option = get_input_number("Choose an option: ")
|
|
102
|
+
if option == 1:
|
|
103
|
+
full_refresh = get_input_true_false("Full refresh?", "n")
|
|
104
|
+
self.dbt.run(full_refresh=full_refresh)
|
|
105
|
+
elif option == 2:
|
|
106
|
+
full_refresh = get_input_true_false("Full refresh?", "n")
|
|
107
|
+
self.dbt.run(modified_only=True, full_refresh=full_refresh)
|
|
108
|
+
elif option == 3:
|
|
109
|
+
select = get_input_string("Enter model selector: ")
|
|
110
|
+
if select != "0":
|
|
111
|
+
self.dbt.run(select=select)
|
|
112
|
+
elif option == 4:
|
|
113
|
+
macro = get_input_string("Enter macro name: ")
|
|
114
|
+
if macro != "0":
|
|
115
|
+
args_str = get_input_string("Enter args (JSON or empty): ", allow_empty=True)
|
|
116
|
+
self.dbt.run_operation(macro, args_str if args_str else None)
|
|
117
|
+
elif option == 5:
|
|
118
|
+
self.dbt.retry()
|
|
119
|
+
complete_action()
|
|
120
|
+
|
|
121
|
+
def _test_menu(self) -> None:
|
|
122
|
+
clear()
|
|
123
|
+
display_action_header("dbt Test Options")
|
|
124
|
+
menu_option(1, "all tests - Run all tests")
|
|
125
|
+
menu_option(2, "specific - Run specific test(s)")
|
|
126
|
+
menu_option(0, "back - Return to development menu")
|
|
127
|
+
option = get_input_number("Choose an option: ")
|
|
128
|
+
if option == 1:
|
|
129
|
+
self.dbt.test()
|
|
130
|
+
elif option == 2:
|
|
131
|
+
select = get_input_string("Enter test selector: ")
|
|
132
|
+
if select != "0":
|
|
133
|
+
self.dbt.test(select=select)
|
|
134
|
+
complete_action()
|
|
135
|
+
|
|
136
|
+
def _lint_menu(self) -> None:
|
|
137
|
+
clear()
|
|
138
|
+
display_action_header("SQLFluff Linting Options")
|
|
139
|
+
menu_option(1, "find modified - Lint modified SQL files")
|
|
140
|
+
menu_option(2, "fix modified - Fix modified SQL files")
|
|
141
|
+
menu_option(3, "find all - Lint all SQL files")
|
|
142
|
+
menu_option(4, "fix all - Fix all SQL files")
|
|
143
|
+
menu_option(5, "specific - Lint specific file")
|
|
144
|
+
menu_option(0, "back - Return to development menu")
|
|
145
|
+
option = get_input_number("Choose an option: ")
|
|
146
|
+
if option == 1:
|
|
147
|
+
changed = [f.file for f in self.git.get_changed_files()] if self.git else []
|
|
148
|
+
self.linting.lint_modified(fix=False, changed_files=changed)
|
|
149
|
+
elif option == 2:
|
|
150
|
+
changed = [f.file for f in self.git.get_changed_files()] if self.git else []
|
|
151
|
+
self.linting.lint_modified(fix=True, changed_files=changed)
|
|
152
|
+
elif option == 3:
|
|
153
|
+
self.linting.lint_file(fix=False)
|
|
154
|
+
elif option == 4:
|
|
155
|
+
self.linting.lint_file(fix=True)
|
|
156
|
+
elif option == 5:
|
|
157
|
+
path = get_input_string("Enter file path: ")
|
|
158
|
+
if path != "0":
|
|
159
|
+
self.linting.lint_file(file_path=path, fix=False)
|
|
160
|
+
complete_action()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from datasecops_cli.services.download_service import DownloadService
|
|
4
|
+
from datasecops_cli.services.skill_service import SkillService
|
|
5
|
+
from datasecops_cli.services.dbt_runner import DbtRunner
|
|
6
|
+
from datasecops_cli.utilities.display import (
|
|
7
|
+
clear, section_header, display_action_header, menu_option,
|
|
8
|
+
get_input_number, get_input_true_false, complete_action,
|
|
9
|
+
info_line, select_from_list
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DownloadsMenu:
|
|
14
|
+
def __init__(self, download_service: DownloadService, skill_service: SkillService,
|
|
15
|
+
dbt_runner: DbtRunner, profile_name: str, dbt_project_dir: Path):
|
|
16
|
+
self.downloads = download_service
|
|
17
|
+
self.skills = skill_service
|
|
18
|
+
self.dbt = dbt_runner
|
|
19
|
+
self.profile_name = profile_name
|
|
20
|
+
self.dbt_project_dir = dbt_project_dir
|
|
21
|
+
|
|
22
|
+
def show(self) -> None:
|
|
23
|
+
self._menu()
|
|
24
|
+
option = get_input_number("Choose an option: ")
|
|
25
|
+
while option != 0:
|
|
26
|
+
if option == 1:
|
|
27
|
+
display_action_header("Download SQLFluff Config")
|
|
28
|
+
self.downloads.download_sqlfluff_config()
|
|
29
|
+
complete_action()
|
|
30
|
+
elif option == 2:
|
|
31
|
+
display_action_header("Download Pipeline Files")
|
|
32
|
+
platform = select_from_list(["github", "azuredevops"], "platform", add_back=False)
|
|
33
|
+
self.downloads.download_pipelines(platform=platform)
|
|
34
|
+
complete_action()
|
|
35
|
+
elif option == 3:
|
|
36
|
+
display_action_header("Download dbt Packages")
|
|
37
|
+
self.downloads.download_dbt_packages(self.dbt_project_dir)
|
|
38
|
+
if get_input_true_false("Run dbt deps now?"):
|
|
39
|
+
self.dbt.deps()
|
|
40
|
+
complete_action()
|
|
41
|
+
elif option == 4:
|
|
42
|
+
self._skills_menu()
|
|
43
|
+
self._menu()
|
|
44
|
+
option = get_input_number("Choose an option: ")
|
|
45
|
+
|
|
46
|
+
def _menu(self) -> None:
|
|
47
|
+
clear()
|
|
48
|
+
section_header("Downloads", self.profile_name)
|
|
49
|
+
menu_option(1, "sqlfluff - Download SQLFluff configuration")
|
|
50
|
+
menu_option(2, "pipelines - Download CI/CD pipeline files")
|
|
51
|
+
menu_option(3, "dbt packages - Download dbt package versions")
|
|
52
|
+
menu_option(4, "skills - Cortex Code skills")
|
|
53
|
+
menu_option(0, "back - Return to main menu")
|
|
54
|
+
|
|
55
|
+
def _skills_menu(self) -> None:
|
|
56
|
+
clear()
|
|
57
|
+
display_action_header("Cortex Code Skills")
|
|
58
|
+
menu_option(1, "list - List available skills")
|
|
59
|
+
menu_option(2, "install - Install a specific skill")
|
|
60
|
+
menu_option(3, "install all - Install all available skills")
|
|
61
|
+
menu_option(4, "update - Update installed skills")
|
|
62
|
+
menu_option(0, "back - Return to downloads menu")
|
|
63
|
+
option = get_input_number("Choose an option: ")
|
|
64
|
+
if option == 1:
|
|
65
|
+
self.skills.list_skills()
|
|
66
|
+
elif option == 2:
|
|
67
|
+
available = self.skills.get_available_skills()
|
|
68
|
+
if available:
|
|
69
|
+
names = {s.name: s for s in available}
|
|
70
|
+
selected = select_from_list(list(names.keys()), "skill")
|
|
71
|
+
if selected != "back" and selected in names:
|
|
72
|
+
self.skills.install_skill(names[selected])
|
|
73
|
+
else:
|
|
74
|
+
info_line("No skills available")
|
|
75
|
+
elif option == 3:
|
|
76
|
+
self.skills.install_all()
|
|
77
|
+
elif option == 4:
|
|
78
|
+
self.skills.update_skills()
|
|
79
|
+
complete_action()
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
from datasecops_cli.services.git_service import GitService
|
|
2
|
+
from datasecops_cli.models.project_config import SourceControl
|
|
3
|
+
from datasecops_cli.utilities.display import (
|
|
4
|
+
clear, section_header, display_action_header, menu_option,
|
|
5
|
+
get_input_number, get_input_string, get_input_true_false,
|
|
6
|
+
complete_action, info_line, error_line, warning_line,
|
|
7
|
+
select_from_dict, select_from_list
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GitOperationsMenu:
|
|
12
|
+
def __init__(self, git_service: GitService, source_control: SourceControl,
|
|
13
|
+
deployment_branches: dict[str, str], profile_name: str):
|
|
14
|
+
self.git = git_service
|
|
15
|
+
self.source_control = source_control
|
|
16
|
+
self.deployment_branches = deployment_branches
|
|
17
|
+
self.profile_name = profile_name
|
|
18
|
+
|
|
19
|
+
def show(self) -> None:
|
|
20
|
+
self._menu()
|
|
21
|
+
option = get_input_number("Choose an option: ")
|
|
22
|
+
while option != 0:
|
|
23
|
+
if option == 1:
|
|
24
|
+
self._branch_menu()
|
|
25
|
+
elif option == 2:
|
|
26
|
+
self._commit()
|
|
27
|
+
elif option == 3:
|
|
28
|
+
self._push()
|
|
29
|
+
elif option == 4:
|
|
30
|
+
self._pull()
|
|
31
|
+
elif option == 5:
|
|
32
|
+
self._rebase_menu()
|
|
33
|
+
elif option == 6:
|
|
34
|
+
self._deploy()
|
|
35
|
+
elif option == 7:
|
|
36
|
+
self._squash_merge_test()
|
|
37
|
+
elif option == 8:
|
|
38
|
+
self._cherry_pick_test()
|
|
39
|
+
self._menu()
|
|
40
|
+
option = get_input_number("Choose an option: ")
|
|
41
|
+
|
|
42
|
+
def _menu(self) -> None:
|
|
43
|
+
clear()
|
|
44
|
+
section_header("Git Operations", self.profile_name, self.git.get_current_branch())
|
|
45
|
+
menu_option(1, "branching - Manage branches")
|
|
46
|
+
menu_option(2, "commit - Commit changes")
|
|
47
|
+
menu_option(3, "push - Push to remote")
|
|
48
|
+
menu_option(4, "pull - Pull from remote")
|
|
49
|
+
menu_option(5, "rebase - Rebase with main")
|
|
50
|
+
menu_option(6, "deploy - Deploy to environment")
|
|
51
|
+
menu_option(7, "squash to test - Squash merge into test")
|
|
52
|
+
menu_option(8, "cherry-pick test - Cherry-pick commits from test")
|
|
53
|
+
menu_option(0, "back - Return to main menu")
|
|
54
|
+
|
|
55
|
+
def _branch_menu(self) -> None:
|
|
56
|
+
clear()
|
|
57
|
+
display_action_header("Branch Management")
|
|
58
|
+
menu_option(1, "new - Create new branch")
|
|
59
|
+
menu_option(2, "checkout - Checkout remote branch")
|
|
60
|
+
menu_option(3, "switch - Switch to local branch")
|
|
61
|
+
menu_option(4, "delete - Delete local branch")
|
|
62
|
+
menu_option(5, "prune - Prune remote branches")
|
|
63
|
+
menu_option(6, "reset - Reset to main")
|
|
64
|
+
menu_option(0, "back - Return to git menu")
|
|
65
|
+
option = get_input_number("Choose an option: ")
|
|
66
|
+
if option == 1:
|
|
67
|
+
self._create_branch()
|
|
68
|
+
elif option == 2:
|
|
69
|
+
branches = self.git.get_remote_branches()
|
|
70
|
+
if branches:
|
|
71
|
+
selected = select_from_dict(branches, "branch")
|
|
72
|
+
if selected != "back":
|
|
73
|
+
self.git.checkout_branch(selected)
|
|
74
|
+
else:
|
|
75
|
+
info_line("No remote branches found")
|
|
76
|
+
elif option == 3:
|
|
77
|
+
branches = self.git.get_local_branches()
|
|
78
|
+
selected = select_from_dict(branches, "branch")
|
|
79
|
+
if selected != "back":
|
|
80
|
+
self.git.switch_branch(selected)
|
|
81
|
+
elif option == 4:
|
|
82
|
+
branches = self.git.get_local_branches()
|
|
83
|
+
selected = select_from_dict(branches, "branch")
|
|
84
|
+
if selected != "back":
|
|
85
|
+
self.git.delete_branch(selected)
|
|
86
|
+
elif option == 5:
|
|
87
|
+
self.git.prune_remote_branches()
|
|
88
|
+
elif option == 6:
|
|
89
|
+
if get_input_true_false("This will delete all local branches. Continue?", "n"):
|
|
90
|
+
self.git.reset_to_main()
|
|
91
|
+
complete_action()
|
|
92
|
+
|
|
93
|
+
def _create_branch(self) -> None:
|
|
94
|
+
branch_types = {bt.name: bt.name for bt in self.source_control.branch_types}
|
|
95
|
+
if not branch_types:
|
|
96
|
+
branch_types = {"feature": "feature", "bugfix": "bugfix", "hotfix": "hotfix"}
|
|
97
|
+
|
|
98
|
+
selected_type = select_from_dict(branch_types, "branch type")
|
|
99
|
+
if selected_type == "back":
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
ticket = ""
|
|
103
|
+
if self.source_control.ticket_number_required:
|
|
104
|
+
ticket = get_input_string("Enter ticket number: ")
|
|
105
|
+
if ticket == "0":
|
|
106
|
+
return
|
|
107
|
+
else:
|
|
108
|
+
ticket = get_input_string("Enter ticket number (or press Enter to skip): ", allow_empty=True)
|
|
109
|
+
|
|
110
|
+
name = get_input_string("Enter branch name: ")
|
|
111
|
+
if name == "0":
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
self.git.create_branch(selected_type, ticket, name.replace(" ", "-").lower())
|
|
115
|
+
|
|
116
|
+
def _commit(self) -> None:
|
|
117
|
+
display_action_header("Commit Changes")
|
|
118
|
+
if not self.git.is_dirty():
|
|
119
|
+
info_line("No changes to commit")
|
|
120
|
+
complete_action()
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
info_line(f"{self.git.get_uncommitted_file_count()} change(s) detected")
|
|
124
|
+
for f in self.git.get_changed_files():
|
|
125
|
+
info_line(f" M {f.file}")
|
|
126
|
+
for f in self.git.get_new_files():
|
|
127
|
+
info_line(f" + {f.file}")
|
|
128
|
+
|
|
129
|
+
message = get_input_string("Enter commit message (0 to cancel): ")
|
|
130
|
+
if message == "0":
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
self.git.commit_changes(message)
|
|
134
|
+
self.git.push_branch()
|
|
135
|
+
complete_action()
|
|
136
|
+
|
|
137
|
+
def _push(self) -> None:
|
|
138
|
+
display_action_header("Push")
|
|
139
|
+
self.git.push_branch()
|
|
140
|
+
complete_action()
|
|
141
|
+
|
|
142
|
+
def _pull(self) -> None:
|
|
143
|
+
display_action_header("Pull")
|
|
144
|
+
self.git.pull_branch()
|
|
145
|
+
complete_action()
|
|
146
|
+
|
|
147
|
+
def _rebase_menu(self) -> None:
|
|
148
|
+
clear()
|
|
149
|
+
display_action_header("Rebase Options")
|
|
150
|
+
menu_option(1, "rebase - Standard rebase with main")
|
|
151
|
+
menu_option(2, "squash & rebase - Squash commits then rebase")
|
|
152
|
+
menu_option(3, "continue - Continue after resolving conflicts")
|
|
153
|
+
menu_option(4, "abort - Abort rebase")
|
|
154
|
+
menu_option(0, "back - Return to git menu")
|
|
155
|
+
option = get_input_number("Choose an option: ")
|
|
156
|
+
if option == 1:
|
|
157
|
+
self.git.rebase_with_main()
|
|
158
|
+
elif option == 2:
|
|
159
|
+
self.git.squash_and_rebase()
|
|
160
|
+
elif option == 3:
|
|
161
|
+
self.git.rebase_continue()
|
|
162
|
+
elif option == 4:
|
|
163
|
+
self.git.rebase_abort()
|
|
164
|
+
complete_action()
|
|
165
|
+
|
|
166
|
+
def _deploy(self) -> None:
|
|
167
|
+
display_action_header("Deploy to Environment")
|
|
168
|
+
if not self.deployment_branches:
|
|
169
|
+
error_line("No deployment branches configured")
|
|
170
|
+
complete_action()
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
selected = select_from_dict(self.deployment_branches, "environment")
|
|
174
|
+
if selected == "back":
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
current = self.git.get_current_branch()
|
|
178
|
+
if selected.lower() == "prod" and current.lower() != "main":
|
|
179
|
+
warning_line("Production deployments must come from the main branch")
|
|
180
|
+
if get_input_true_false("Switch to main and deploy?", "n"):
|
|
181
|
+
self.git.checkout_branch("main")
|
|
182
|
+
self.git.pull_branch()
|
|
183
|
+
else:
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
force = selected.lower() != "prod"
|
|
187
|
+
self.git.push_branch_to_destination(selected, force=force)
|
|
188
|
+
complete_action()
|
|
189
|
+
|
|
190
|
+
def _squash_merge_test(self) -> None:
|
|
191
|
+
display_action_header("Squash Merge into Test")
|
|
192
|
+
current = self.git.get_current_branch()
|
|
193
|
+
info_line(f"This will squash merge {current} into the test branch")
|
|
194
|
+
if get_input_true_false("Continue?", "n"):
|
|
195
|
+
self.git.squash_merge_into_test()
|
|
196
|
+
complete_action()
|
|
197
|
+
|
|
198
|
+
def _cherry_pick_test(self) -> None:
|
|
199
|
+
display_action_header("Cherry-pick from Test")
|
|
200
|
+
commits = self.git.get_test_commits()
|
|
201
|
+
if not commits:
|
|
202
|
+
info_line("No commits found on test branch")
|
|
203
|
+
complete_action()
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
info_line("Recent commits on test:")
|
|
207
|
+
for i, c in enumerate(commits):
|
|
208
|
+
info_line(f" [{i+1}] {c['sha'][:8]} - {c['message'][:60]}")
|
|
209
|
+
|
|
210
|
+
choice = get_input_number("Select commit number (0 to cancel): ")
|
|
211
|
+
if 0 < choice <= len(commits):
|
|
212
|
+
self.git.cherry_pick_from_test(commits[choice-1]["sha"])
|
|
213
|
+
complete_action()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|