snowglobe-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.
- snowglobe/__init__.py +6 -0
- snowglobe/__main__.py +3 -0
- snowglobe/cli/__init__.py +0 -0
- snowglobe/cli/access.py +197 -0
- snowglobe/cli/app.py +148 -0
- snowglobe/cli/context.py +48 -0
- snowglobe/cli/cost.py +291 -0
- snowglobe/cli/debug.py +265 -0
- snowglobe/cli/diff.py +34 -0
- snowglobe/cli/optimizer.py +91 -0
- snowglobe/cli/prompts.py +161 -0
- snowglobe/cli/report.py +91 -0
- snowglobe/cli/shell.py +1437 -0
- snowglobe/cli/shell_completer.py +128 -0
- snowglobe/collectors/access.py +882 -0
- snowglobe/collectors/query_history.py +46 -0
- snowglobe/collectors/query_profile.py +101 -0
- snowglobe/config/loader.py +42 -0
- snowglobe/core/access_service.py +721 -0
- snowglobe/core/cost_service.py +929 -0
- snowglobe/core/optimizer.py +92 -0
- snowglobe/core/query_service.py +48 -0
- snowglobe/core/report_service.py +110 -0
- snowglobe/core/risk_service.py +358 -0
- snowglobe/engines/access/__init__.py +0 -0
- snowglobe/engines/access/explainer.py +113 -0
- snowglobe/engines/access/resolver.py +199 -0
- snowglobe/engines/ai/cortex_optimizer.py +69 -0
- snowglobe/engines/optimizer/query_optimizer.py +326 -0
- snowglobe/graphs/__init__.py +0 -0
- snowglobe/graphs/role_graph.py +140 -0
- snowglobe/graphs/user_graph.py +64 -0
- snowglobe/models/__init__.py +0 -0
- snowglobe/models/access.py +65 -0
- snowglobe/models/access_path.py +15 -0
- snowglobe/models/object_ref.py +11 -0
- snowglobe/models/object_type.py +50 -0
- snowglobe/models/optimizer.py +15 -0
- snowglobe/models/privilege.py +78 -0
- snowglobe/models/query.py +59 -0
- snowglobe/output/__init__.py +0 -0
- snowglobe/output/cli.py +413 -0
- snowglobe/queries/__init__.py +0 -0
- snowglobe/queries/query_history.py +37 -0
- snowglobe/snowflake/connection.py +75 -0
- snowglobe/state/db.py +559 -0
- snowglobe/state/state.py +60 -0
- snowglobe/templates/report.md.j2 +55 -0
- snowglobe/tests/access_tests.py +5 -0
- snowglobe/tui/__init__.py +1 -0
- snowglobe/tui/__main__.py +3 -0
- snowglobe/tui/app.py +299 -0
- snowglobe/tui/screens/__init__.py +0 -0
- snowglobe/tui/screens/access.py +627 -0
- snowglobe/tui/screens/cost.py +831 -0
- snowglobe/tui/screens/home.py +222 -0
- snowglobe/tui/screens/refresh.py +222 -0
- snowglobe/tui/screens/reports.py +252 -0
- snowglobe/tui/screens/risk.py +417 -0
- snowglobe/tui/screens/tune.py +254 -0
- snowglobe/tui/widgets/__init__.py +0 -0
- snowglobe/tui/widgets/access_paths.py +63 -0
- snowglobe/tui/widgets/cache_badge.py +28 -0
- snowglobe/tui/widgets/header.py +21 -0
- snowglobe/tui/widgets/nav.py +32 -0
- snowglobe_cli-0.1.0.dist-info/METADATA +368 -0
- snowglobe_cli-0.1.0.dist-info/RECORD +71 -0
- snowglobe_cli-0.1.0.dist-info/WHEEL +5 -0
- snowglobe_cli-0.1.0.dist-info/entry_points.txt +2 -0
- snowglobe_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
- snowglobe_cli-0.1.0.dist-info/top_level.txt +1 -0
snowglobe/__init__.py
ADDED
snowglobe/__main__.py
ADDED
|
File without changes
|
snowglobe/cli/access.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from snowglobe.core.access_service import AccessService
|
|
4
|
+
from snowglobe.cli.prompts import resolve_access_inputs
|
|
5
|
+
from snowglobe.output import cli
|
|
6
|
+
|
|
7
|
+
access_app = typer.Typer(
|
|
8
|
+
help="Inspect Snowflake access and identify roles/users with access and privileges on database objects",
|
|
9
|
+
no_args_is_help=True,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@access_app.command()
|
|
14
|
+
def check(
|
|
15
|
+
ctx: typer.Context,
|
|
16
|
+
ignore_excluded_roles: bool = typer.Option(False, help="Ignore excluded roles"),
|
|
17
|
+
role: Optional[str] = typer.Option(None, help="Role to inspect access for"),
|
|
18
|
+
username: Optional[str] = typer.Option(None, help="Username to inspect access for"),
|
|
19
|
+
object_type: Optional[str] = typer.Option(None, help="Object type (e.g. TABLE)"),
|
|
20
|
+
object_name: Optional[str] = typer.Option(None, help="Object name (e.g. DB.SCHEMA.TABLE)"),
|
|
21
|
+
privilege: Optional[str] = typer.Option(None, help="Privilege to check (e.g. SELECT)"),
|
|
22
|
+
output: str = typer.Option("text", help="Output format: text, json"),
|
|
23
|
+
refresh_state: bool = typer.Option(False, help="Refresh state from Snowflake")
|
|
24
|
+
):
|
|
25
|
+
"""
|
|
26
|
+
Check access for a user or role on a specific object.
|
|
27
|
+
|
|
28
|
+
In interactive mode (TTY), missing arguments will be prompted for
|
|
29
|
+
with fuzzy completion. In headless mode (piped/CI), all arguments
|
|
30
|
+
must be provided or the command will exit with an error.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
context = ctx.obj
|
|
34
|
+
access_service = AccessService(context)
|
|
35
|
+
|
|
36
|
+
# Load graphs for interactive resolution (needed for completions)
|
|
37
|
+
access_service.setup_state()
|
|
38
|
+
if refresh_state:
|
|
39
|
+
access_service.refresh_state()
|
|
40
|
+
access_service.load_state()
|
|
41
|
+
|
|
42
|
+
# Resolve missing inputs (interactive prompts or headless error)
|
|
43
|
+
resolved = resolve_access_inputs(
|
|
44
|
+
username=username,
|
|
45
|
+
role=role,
|
|
46
|
+
object_type=object_type,
|
|
47
|
+
object_name=object_name,
|
|
48
|
+
privilege=privilege,
|
|
49
|
+
user_graph=access_service.user_graph,
|
|
50
|
+
role_graph=access_service.role_graph,
|
|
51
|
+
grants=[],
|
|
52
|
+
object_index=access_service.object_index,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Run the access check with fully resolved args
|
|
56
|
+
try:
|
|
57
|
+
query_output = access_service.inspect_access(
|
|
58
|
+
username=resolved["username"],
|
|
59
|
+
role=resolved["role"],
|
|
60
|
+
object_type=resolved["object_type"],
|
|
61
|
+
object_name=resolved["object_name"],
|
|
62
|
+
privilege=resolved["privilege"],
|
|
63
|
+
ignore_excluded_roles=ignore_excluded_roles,
|
|
64
|
+
refresh_state=False, # Already refreshed above if needed
|
|
65
|
+
)
|
|
66
|
+
except ValueError as e:
|
|
67
|
+
typer.secho(f"Error: {e}", fg=typer.colors.RED)
|
|
68
|
+
raise typer.Exit(1)
|
|
69
|
+
|
|
70
|
+
# Output
|
|
71
|
+
if output == "text":
|
|
72
|
+
typer.echo(cli.format_access_text(query_output))
|
|
73
|
+
elif output == "json":
|
|
74
|
+
cli.format_json(query_output)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@access_app.command()
|
|
78
|
+
def create(
|
|
79
|
+
ctx: typer.Context,
|
|
80
|
+
role: Optional[str] = typer.Option(None, help="Role to check CREATE privilege for"),
|
|
81
|
+
username: Optional[str] = typer.Option(None, help="Username to check CREATE privilege for"),
|
|
82
|
+
privilege: str = typer.Option("CREATE TABLE", help="CREATE privilege (e.g. 'CREATE TABLE', 'CREATE VIEW')"),
|
|
83
|
+
scope: Optional[str] = typer.Option(None, help="Optional scope: DB or DB.SCHEMA to filter"),
|
|
84
|
+
output: str = typer.Option("text", help="Output format: text, json"),
|
|
85
|
+
):
|
|
86
|
+
"""
|
|
87
|
+
Check CREATE privileges for a user or role.
|
|
88
|
+
|
|
89
|
+
Shows where the role/user can create objects — at account level,
|
|
90
|
+
specific databases, or specific schemas. Optionally filter by a
|
|
91
|
+
specific database or schema scope.
|
|
92
|
+
|
|
93
|
+
Examples:
|
|
94
|
+
snowglobe access create --role SYSADMIN --privilege "CREATE TABLE"
|
|
95
|
+
snowglobe access create --role DEV_DW_DESIGNER --privilege "CREATE TABLE" --scope DEV_REFINED.UNIFIED
|
|
96
|
+
"""
|
|
97
|
+
context = ctx.obj
|
|
98
|
+
|
|
99
|
+
if not role and not username:
|
|
100
|
+
typer.secho("Must provide --role or --username.", fg=typer.colors.RED)
|
|
101
|
+
raise typer.Exit(1)
|
|
102
|
+
|
|
103
|
+
access_service = AccessService(context)
|
|
104
|
+
try:
|
|
105
|
+
result = access_service.inspect_create(
|
|
106
|
+
username=username,
|
|
107
|
+
role=role,
|
|
108
|
+
privilege=privilege,
|
|
109
|
+
scope=scope,
|
|
110
|
+
)
|
|
111
|
+
except ValueError as e:
|
|
112
|
+
typer.secho(f"Error: {e}", fg=typer.colors.RED)
|
|
113
|
+
raise typer.Exit(1)
|
|
114
|
+
|
|
115
|
+
if output == "text":
|
|
116
|
+
typer.echo(cli.format_create_text(result))
|
|
117
|
+
elif output == "json":
|
|
118
|
+
cli.format_json(result)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@access_app.command()
|
|
122
|
+
def whoaccess(
|
|
123
|
+
ctx: typer.Context,
|
|
124
|
+
object_type: Optional[str] = typer.Option(None, "--object-type", help="Object type (e.g. TABLE, VIEW, SCHEMA)"),
|
|
125
|
+
object_name: Optional[str] = typer.Option(None, "--object-name", help="Object FQN (e.g. DB.SCHEMA.TABLE)"),
|
|
126
|
+
privilege: Optional[str] = typer.Option(None, "--privilege", help="Filter to a specific privilege (e.g. SELECT)"),
|
|
127
|
+
output: str = typer.Option("text", help="Output format: text, json"),
|
|
128
|
+
):
|
|
129
|
+
"""
|
|
130
|
+
Reverse lookup: who can access this object?
|
|
131
|
+
|
|
132
|
+
Shows all roles and users that have access to the specified object,
|
|
133
|
+
grouped by privilege. Optionally filter to a specific privilege.
|
|
134
|
+
|
|
135
|
+
Examples:
|
|
136
|
+
snowglobe access whoaccess --object-type TABLE --object-name DEV_REFINED.UNIFIED.HUB_EMAIL
|
|
137
|
+
snowglobe access whoaccess --object-type TABLE --object-name DEV_REFINED.UNIFIED.HUB_EMAIL --privilege SELECT
|
|
138
|
+
"""
|
|
139
|
+
context = ctx.obj
|
|
140
|
+
access_service = AccessService(context)
|
|
141
|
+
|
|
142
|
+
# Load state for interactive completions
|
|
143
|
+
access_service.setup_state()
|
|
144
|
+
access_service.load_state()
|
|
145
|
+
|
|
146
|
+
# Resolve missing inputs interactively if TTY
|
|
147
|
+
if not object_type or not object_name:
|
|
148
|
+
from snowglobe.cli.prompts import is_interactive
|
|
149
|
+
if not is_interactive():
|
|
150
|
+
missing = []
|
|
151
|
+
if not object_type:
|
|
152
|
+
missing.append("--object-type")
|
|
153
|
+
if not object_name:
|
|
154
|
+
missing.append("--object-name")
|
|
155
|
+
typer.secho(
|
|
156
|
+
f"Error: Missing required arguments: {', '.join(missing)}.",
|
|
157
|
+
fg=typer.colors.RED
|
|
158
|
+
)
|
|
159
|
+
raise typer.Exit(1)
|
|
160
|
+
|
|
161
|
+
from prompt_toolkit import PromptSession
|
|
162
|
+
from prompt_toolkit.completion import WordCompleter, FuzzyCompleter
|
|
163
|
+
from snowglobe.models.access import ObjectType
|
|
164
|
+
|
|
165
|
+
session = PromptSession()
|
|
166
|
+
|
|
167
|
+
if not object_type:
|
|
168
|
+
items = [ot.value for ot in ObjectType]
|
|
169
|
+
word = WordCompleter(items, ignore_case=True)
|
|
170
|
+
fuzzy = FuzzyCompleter(word)
|
|
171
|
+
object_type = session.prompt(
|
|
172
|
+
"Object type: ",
|
|
173
|
+
completer=fuzzy,
|
|
174
|
+
complete_while_typing=True,
|
|
175
|
+
).strip().upper()
|
|
176
|
+
|
|
177
|
+
if not object_name:
|
|
178
|
+
obj_index = access_service.object_index or {}
|
|
179
|
+
items = obj_index.get(object_type.upper(), [])
|
|
180
|
+
word = WordCompleter(items, ignore_case=True, sentence=True)
|
|
181
|
+
fuzzy = FuzzyCompleter(word)
|
|
182
|
+
object_name = session.prompt(
|
|
183
|
+
"Object name (FQN): ",
|
|
184
|
+
completer=fuzzy,
|
|
185
|
+
complete_while_typing=True,
|
|
186
|
+
).strip()
|
|
187
|
+
|
|
188
|
+
result = access_service.inspect_reverse(
|
|
189
|
+
object_type=object_type,
|
|
190
|
+
object_name=object_name,
|
|
191
|
+
privilege=privilege,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
if output == "text":
|
|
195
|
+
typer.echo(cli.format_reverse_text(result))
|
|
196
|
+
elif output == "json":
|
|
197
|
+
cli.format_json(result)
|
snowglobe/cli/app.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from snowglobe.cli.context import SnowglobeContext
|
|
3
|
+
from snowglobe.cli.access import access_app
|
|
4
|
+
from snowglobe.cli.optimizer import opt_app
|
|
5
|
+
from snowglobe.cli.cost import cost_app
|
|
6
|
+
from snowglobe.cli.diff import diff_app
|
|
7
|
+
from snowglobe.cli.report import report_app
|
|
8
|
+
from snowglobe.cli.debug import debug_app
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(
|
|
11
|
+
help="Snowglobe — Explainable cost and access visibility for Snowflake",
|
|
12
|
+
no_args_is_help=False,
|
|
13
|
+
context_settings={"ignore_unknown_options": True}
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
app.add_typer(access_app, name="access")
|
|
17
|
+
app.add_typer(cost_app, name="cost")
|
|
18
|
+
app.add_typer(diff_app, name="diff")
|
|
19
|
+
app.add_typer(opt_app, name="optimize")
|
|
20
|
+
app.add_typer(report_app, name="report")
|
|
21
|
+
app.add_typer(debug_app, name="debug")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@app.command()
|
|
25
|
+
def refresh(
|
|
26
|
+
ctx: typer.Context,
|
|
27
|
+
full: bool = typer.Option(False, "--full", help="Force full refresh (ignore incremental)")
|
|
28
|
+
):
|
|
29
|
+
"""Refresh cached state from Snowflake. Incremental by default."""
|
|
30
|
+
from snowglobe.core.access_service import AccessService
|
|
31
|
+
|
|
32
|
+
context = ctx.obj
|
|
33
|
+
service = AccessService(context)
|
|
34
|
+
service.setup_state()
|
|
35
|
+
|
|
36
|
+
service.refresh_state(full=full)
|
|
37
|
+
|
|
38
|
+
typer.secho(f" Users: {len(service.user_graph.assigned_roles)}", fg=typer.colors.GREEN)
|
|
39
|
+
typer.secho(f" Roles: {len(service.role_graph.parents)}", fg=typer.colors.GREEN)
|
|
40
|
+
total_objects = sum(len(v) for v in service.object_index.values())
|
|
41
|
+
typer.secho(f" Object index: {total_objects} FQNs", fg=typer.colors.GREEN)
|
|
42
|
+
typer.secho("Done.", fg=typer.colors.GREEN, bold=True)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _launch_tui(context, *, vim_flag: bool = False, fallback_to_shell: bool = False) -> None:
|
|
46
|
+
"""
|
|
47
|
+
Start the Textual TUI.
|
|
48
|
+
|
|
49
|
+
If `fallback_to_shell=True` and Textual isn't installed, drop into the
|
|
50
|
+
REPL shell instead with a one-line notice. Used by the default
|
|
51
|
+
`snowglobe` (no-args) path so users without the TUI extra still get
|
|
52
|
+
something useful. The explicit `snowglobe tui` subcommand passes
|
|
53
|
+
`fallback_to_shell=False` and exits with an error if Textual is missing.
|
|
54
|
+
"""
|
|
55
|
+
try:
|
|
56
|
+
from snowglobe.tui.app import SnowglobeApp, VimSnowglobeApp
|
|
57
|
+
except ImportError as e:
|
|
58
|
+
if "textual" in str(e).lower():
|
|
59
|
+
if fallback_to_shell:
|
|
60
|
+
typer.secho(
|
|
61
|
+
"TUI not available (install with: pip install 'snowglobe[tui]'). "
|
|
62
|
+
"Falling back to the REPL shell.",
|
|
63
|
+
fg=typer.colors.YELLOW,
|
|
64
|
+
)
|
|
65
|
+
from snowglobe.cli.shell import start_shell
|
|
66
|
+
start_shell(context)
|
|
67
|
+
return
|
|
68
|
+
typer.secho(
|
|
69
|
+
"TUI requires the 'textual' package. Install with:\n"
|
|
70
|
+
" pip install 'snowglobe[tui]' (or) pip install textual",
|
|
71
|
+
fg=typer.colors.YELLOW,
|
|
72
|
+
)
|
|
73
|
+
raise typer.Exit(1)
|
|
74
|
+
raise
|
|
75
|
+
|
|
76
|
+
# CLI flag wins; otherwise inherit from the active profile's `vim: true`.
|
|
77
|
+
profile_vim = bool((context.profile or {}).get("vim", False)) if context.profile else False
|
|
78
|
+
context.vim_mode = vim_flag or profile_vim
|
|
79
|
+
|
|
80
|
+
app_cls = VimSnowglobeApp if context.vim_mode else SnowglobeApp
|
|
81
|
+
app_cls(context=context).run()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@app.command()
|
|
85
|
+
def tui(
|
|
86
|
+
ctx: typer.Context,
|
|
87
|
+
vim: bool = typer.Option(
|
|
88
|
+
False, "--vim",
|
|
89
|
+
help="Enable vim-style navigation (j/k/h/l/g/G/Ctrl-d/Ctrl-u + Esc blurs inputs).",
|
|
90
|
+
),
|
|
91
|
+
):
|
|
92
|
+
"""Launch the rich Textual-based TUI (same as running `snowglobe` with no command)."""
|
|
93
|
+
_launch_tui(ctx.obj, vim_flag=vim, fallback_to_shell=False)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@app.command()
|
|
97
|
+
def shell(ctx: typer.Context):
|
|
98
|
+
"""Launch the interactive REPL shell (the prompt_toolkit fuzzy-completion REPL)."""
|
|
99
|
+
from snowglobe.cli.shell import start_shell
|
|
100
|
+
start_shell(ctx.obj)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@app.callback(invoke_without_command=True)
|
|
104
|
+
def main(
|
|
105
|
+
ctx: typer.Context,
|
|
106
|
+
profile_name: str = typer.Option(
|
|
107
|
+
"default",
|
|
108
|
+
"--profile",
|
|
109
|
+
help="Snowflake connection profile to use",
|
|
110
|
+
),
|
|
111
|
+
role: str | None = typer.Option(
|
|
112
|
+
None,
|
|
113
|
+
"--role",
|
|
114
|
+
help="Override Snowflake role",
|
|
115
|
+
),
|
|
116
|
+
output: str = typer.Option(
|
|
117
|
+
"table",
|
|
118
|
+
"--output",
|
|
119
|
+
help="Output format: table | json",
|
|
120
|
+
),
|
|
121
|
+
verbose: bool = typer.Option(
|
|
122
|
+
False,
|
|
123
|
+
"--verbose",
|
|
124
|
+
"-v",
|
|
125
|
+
help="Enable verbose output"
|
|
126
|
+
)
|
|
127
|
+
):
|
|
128
|
+
"""
|
|
129
|
+
Inspect and understand Snowflake cost, access, and ownership.
|
|
130
|
+
|
|
131
|
+
Snowglobe is read-only by design.
|
|
132
|
+
|
|
133
|
+
Run without a command to launch the TUI.
|
|
134
|
+
Use `snowglobe shell` for the REPL shell, or any subcommand for headless use.
|
|
135
|
+
"""
|
|
136
|
+
context = SnowglobeContext(
|
|
137
|
+
profile_name=profile_name,
|
|
138
|
+
role=role,
|
|
139
|
+
output=output,
|
|
140
|
+
verbose=verbose
|
|
141
|
+
)
|
|
142
|
+
context.load_profile()
|
|
143
|
+
ctx.obj = context
|
|
144
|
+
|
|
145
|
+
# No subcommand → launch the TUI (falling back to the REPL shell
|
|
146
|
+
# if the optional Textual dependency isn't installed).
|
|
147
|
+
if ctx.invoked_subcommand is None:
|
|
148
|
+
_launch_tui(context, vim_flag=False, fallback_to_shell=True)
|
snowglobe/cli/context.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Optional, Dict, Any, List
|
|
3
|
+
from snowglobe.config.loader import SnowglobeConfig
|
|
4
|
+
from snowglobe.snowflake.connection import SnowflakeReadOnly
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class SnowglobeContext:
|
|
9
|
+
profile_name: str = "default"
|
|
10
|
+
profile: Optional[Dict[str, Any]] = None
|
|
11
|
+
role: Optional[str] = None
|
|
12
|
+
output: str = "table"
|
|
13
|
+
verbose: bool = False
|
|
14
|
+
vim_mode: bool = False
|
|
15
|
+
|
|
16
|
+
# Working state (used by shell and interactive prompts)
|
|
17
|
+
target_role: Optional[str] = None
|
|
18
|
+
username: Optional[str] = None
|
|
19
|
+
object_type: Optional[str] = None
|
|
20
|
+
object_name: Optional[str] = None
|
|
21
|
+
privilege: Optional[str] = None
|
|
22
|
+
|
|
23
|
+
# Preloaded graphs (populated by shell or on-demand)
|
|
24
|
+
user_graph: Any = None
|
|
25
|
+
role_graph: Any = None
|
|
26
|
+
object_index: Optional[Dict[str, List[str]]] = None
|
|
27
|
+
|
|
28
|
+
def load_profile(self):
|
|
29
|
+
if self.profile is not None:
|
|
30
|
+
return
|
|
31
|
+
config = SnowglobeConfig()
|
|
32
|
+
self.profile = config.get_profile(self.profile_name)
|
|
33
|
+
if self.role:
|
|
34
|
+
self.profile["role"] = self.role
|
|
35
|
+
|
|
36
|
+
def connect(self) -> SnowflakeReadOnly:
|
|
37
|
+
self.load_profile()
|
|
38
|
+
if not hasattr(self, "_sf"):
|
|
39
|
+
self._sf = SnowflakeReadOnly(
|
|
40
|
+
account=self.profile["account"],
|
|
41
|
+
warehouse=self.profile.get("warehouse"),
|
|
42
|
+
user=self.profile["user"],
|
|
43
|
+
role=self.profile.get("role"),
|
|
44
|
+
password=self.profile.get("password"),
|
|
45
|
+
private_key_path=self.profile.get("private_key_path"),
|
|
46
|
+
private_key_pwd=self.profile.get("private_key_pwd")
|
|
47
|
+
)
|
|
48
|
+
return self._sf
|