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.
Files changed (71) hide show
  1. snowglobe/__init__.py +6 -0
  2. snowglobe/__main__.py +3 -0
  3. snowglobe/cli/__init__.py +0 -0
  4. snowglobe/cli/access.py +197 -0
  5. snowglobe/cli/app.py +148 -0
  6. snowglobe/cli/context.py +48 -0
  7. snowglobe/cli/cost.py +291 -0
  8. snowglobe/cli/debug.py +265 -0
  9. snowglobe/cli/diff.py +34 -0
  10. snowglobe/cli/optimizer.py +91 -0
  11. snowglobe/cli/prompts.py +161 -0
  12. snowglobe/cli/report.py +91 -0
  13. snowglobe/cli/shell.py +1437 -0
  14. snowglobe/cli/shell_completer.py +128 -0
  15. snowglobe/collectors/access.py +882 -0
  16. snowglobe/collectors/query_history.py +46 -0
  17. snowglobe/collectors/query_profile.py +101 -0
  18. snowglobe/config/loader.py +42 -0
  19. snowglobe/core/access_service.py +721 -0
  20. snowglobe/core/cost_service.py +929 -0
  21. snowglobe/core/optimizer.py +92 -0
  22. snowglobe/core/query_service.py +48 -0
  23. snowglobe/core/report_service.py +110 -0
  24. snowglobe/core/risk_service.py +358 -0
  25. snowglobe/engines/access/__init__.py +0 -0
  26. snowglobe/engines/access/explainer.py +113 -0
  27. snowglobe/engines/access/resolver.py +199 -0
  28. snowglobe/engines/ai/cortex_optimizer.py +69 -0
  29. snowglobe/engines/optimizer/query_optimizer.py +326 -0
  30. snowglobe/graphs/__init__.py +0 -0
  31. snowglobe/graphs/role_graph.py +140 -0
  32. snowglobe/graphs/user_graph.py +64 -0
  33. snowglobe/models/__init__.py +0 -0
  34. snowglobe/models/access.py +65 -0
  35. snowglobe/models/access_path.py +15 -0
  36. snowglobe/models/object_ref.py +11 -0
  37. snowglobe/models/object_type.py +50 -0
  38. snowglobe/models/optimizer.py +15 -0
  39. snowglobe/models/privilege.py +78 -0
  40. snowglobe/models/query.py +59 -0
  41. snowglobe/output/__init__.py +0 -0
  42. snowglobe/output/cli.py +413 -0
  43. snowglobe/queries/__init__.py +0 -0
  44. snowglobe/queries/query_history.py +37 -0
  45. snowglobe/snowflake/connection.py +75 -0
  46. snowglobe/state/db.py +559 -0
  47. snowglobe/state/state.py +60 -0
  48. snowglobe/templates/report.md.j2 +55 -0
  49. snowglobe/tests/access_tests.py +5 -0
  50. snowglobe/tui/__init__.py +1 -0
  51. snowglobe/tui/__main__.py +3 -0
  52. snowglobe/tui/app.py +299 -0
  53. snowglobe/tui/screens/__init__.py +0 -0
  54. snowglobe/tui/screens/access.py +627 -0
  55. snowglobe/tui/screens/cost.py +831 -0
  56. snowglobe/tui/screens/home.py +222 -0
  57. snowglobe/tui/screens/refresh.py +222 -0
  58. snowglobe/tui/screens/reports.py +252 -0
  59. snowglobe/tui/screens/risk.py +417 -0
  60. snowglobe/tui/screens/tune.py +254 -0
  61. snowglobe/tui/widgets/__init__.py +0 -0
  62. snowglobe/tui/widgets/access_paths.py +63 -0
  63. snowglobe/tui/widgets/cache_badge.py +28 -0
  64. snowglobe/tui/widgets/header.py +21 -0
  65. snowglobe/tui/widgets/nav.py +32 -0
  66. snowglobe_cli-0.1.0.dist-info/METADATA +368 -0
  67. snowglobe_cli-0.1.0.dist-info/RECORD +71 -0
  68. snowglobe_cli-0.1.0.dist-info/WHEEL +5 -0
  69. snowglobe_cli-0.1.0.dist-info/entry_points.txt +2 -0
  70. snowglobe_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
  71. snowglobe_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,161 @@
1
+ __all__ = [
2
+ "resolve_access_inputs"
3
+ ]
4
+
5
+ import sys
6
+ import typer
7
+ from prompt_toolkit import PromptSession
8
+ from prompt_toolkit.completion import WordCompleter, FuzzyCompleter
9
+ from snowglobe.models.privilege import Privilege, privileges_for_object_type
10
+ from snowglobe.models.access import ObjectType
11
+
12
+
13
+ def is_interactive() -> bool:
14
+ """Return True if stdin is a TTY (interactive terminal)."""
15
+ return sys.stdin.isatty()
16
+
17
+
18
+ def prompt(label: str, session: PromptSession, items: list[str], strict: bool = False, output: bool = False):
19
+ word = WordCompleter(items, ignore_case=True, sentence=True)
20
+ fuzzy = FuzzyCompleter(word)
21
+
22
+ def get_value(text):
23
+ value = session.prompt(
24
+ label,
25
+ completer=text,
26
+ complete_while_typing=True,
27
+ ).strip()
28
+ return value
29
+
30
+ value = get_value(fuzzy)
31
+ if not value and strict:
32
+ typer.secho("Field required!", fg=typer.colors.YELLOW)
33
+ value = get_value(fuzzy)
34
+ elif value not in items and strict:
35
+ typer.secho(f"{label} not found!", fg=typer.colors.YELLOW)
36
+ value = get_value(fuzzy)
37
+
38
+ if output:
39
+ typer.secho(f"{label} {value}", fg=typer.colors.GREEN)
40
+ return value
41
+
42
+
43
+ def resolve_access_inputs(
44
+ *,
45
+ username,
46
+ role,
47
+ object_type,
48
+ object_name,
49
+ privilege,
50
+ user_graph,
51
+ role_graph,
52
+ grants,
53
+ object_index=None,
54
+ ) -> dict:
55
+ """
56
+ Resolve access query inputs. If all required args are provided, returns
57
+ immediately (headless-safe). If args are missing and stdin is a TTY,
58
+ prompts interactively. If args are missing and stdin is NOT a TTY,
59
+ raises an error with a clear message.
60
+ """
61
+
62
+ # Inspect type: user or role
63
+ if username and not role:
64
+ inspect_type = "user"
65
+ elif role and not username:
66
+ inspect_type = "role"
67
+ else:
68
+ inspect_type = None
69
+
70
+ # Inputs
71
+ resolved_inputs = {
72
+ "inspect_type": inspect_type,
73
+ "username": username if username else None,
74
+ "role": role if role else None,
75
+ "object_type": object_type if object_type else None,
76
+ "object_name": object_name.upper() if object_name else None,
77
+ "database": object_name.split(".", 1)[0] if object_name else None,
78
+ "privilege": privilege if privilege else None
79
+ }
80
+
81
+ # Return resolved inputs if all provided (headless-safe path)
82
+ if all([username or role, object_type, object_name, privilege]):
83
+ return resolved_inputs
84
+
85
+ # In headless mode, fail fast with a clear error
86
+ if not is_interactive():
87
+ missing = []
88
+ if not (username or role):
89
+ missing.append("--username or --role")
90
+ if not object_type:
91
+ missing.append("--object-type")
92
+ if not object_name:
93
+ missing.append("--object-name")
94
+ if not privilege:
95
+ missing.append("--privilege")
96
+ typer.secho(
97
+ f"Error: Missing required arguments: {', '.join(missing)}. "
98
+ "All arguments must be provided in non-interactive (headless) mode.",
99
+ fg=typer.colors.RED
100
+ )
101
+ raise typer.Exit(1)
102
+
103
+ # Interactive prompts for missing inputs
104
+ session = PromptSession()
105
+ if not inspect_type:
106
+ choice = prompt("Inspect type (User or Role): ", session, ["User", "Role"])
107
+ if choice.lower().startswith("u"):
108
+ inspect_type = "user"
109
+ resolved_inputs["inspect_type"] = "user"
110
+ elif choice.lower().startswith("r"):
111
+ inspect_type = "role"
112
+ resolved_inputs["inspect_type"] = "role"
113
+ else:
114
+ typer.secho("Please choose 'User' or 'Role'. Exiting.", fg=typer.colors.RED)
115
+ raise typer.Exit(1)
116
+
117
+ if inspect_type == "user" and not username:
118
+ items = list(user_graph.assigned_roles.keys())
119
+ username = prompt("User: ", session, items, strict=True)
120
+ resolved_inputs["username"] = username
121
+
122
+ elif inspect_type == "role" and not role:
123
+ items = list(role_graph.roles.keys())
124
+ role = prompt("Role: ", session, items, strict=True)
125
+ resolved_inputs["role"] = role
126
+
127
+ if not object_type:
128
+ items = [ot.value for ot in ObjectType]
129
+ object_type = prompt("Select object type ", session, items, strict=True)
130
+ typer.secho(f"Object type: {object_type}", fg=typer.colors.GREEN)
131
+ resolved_inputs['object_type'] = object_type
132
+
133
+ if not object_name:
134
+ # Use object_index for completions if available, fall back to grants
135
+ if object_index and object_type in object_index:
136
+ items = object_index[object_type]
137
+ elif object_index and object_type.upper() in object_index:
138
+ items = object_index[object_type.upper()]
139
+ else:
140
+ items = sorted(set(g.object.name for g in grants if g.object.object_type.value == object_type))
141
+ object_name = prompt("Object name (FQN): ", session, items, strict=False)
142
+ if not object_name or not object_name.strip():
143
+ typer.secho("Object name is required.", fg=typer.colors.RED)
144
+ raise typer.Exit(1)
145
+ resolved_inputs['object_name'] = object_name.upper()
146
+ resolved_inputs["database"] = object_name.split(".", 1)[0]
147
+
148
+ if not privilege:
149
+ items = _privileges_for_object_type(object_type)
150
+ privilege = prompt("Privilege to inspect : ", session, items, strict=False)
151
+ if not privilege or not privilege.strip():
152
+ typer.secho("Privilege is required.", fg=typer.colors.RED)
153
+ raise typer.Exit(1)
154
+ resolved_inputs['privilege'] = privilege.upper()
155
+
156
+ return resolved_inputs
157
+
158
+
159
+ # Privilege suggestions per object type now live in snowglobe.models.privilege
160
+ # (used by both the CLI and the TUI). Imported above as `privileges_for_object_type`.
161
+ _privileges_for_object_type = privileges_for_object_type
@@ -0,0 +1,91 @@
1
+ import typer
2
+ from datetime import date
3
+
4
+ report_app = typer.Typer(
5
+ help="Generate summarized reports — cost, AI, storage, queries",
6
+ no_args_is_help=True,
7
+ )
8
+
9
+
10
+ def _default_output_path() -> str:
11
+ return f"snowglobe_report_{date.today().isoformat()}.md"
12
+
13
+
14
+ @report_app.command()
15
+ def full(
16
+ ctx: typer.Context,
17
+ days: int = typer.Option(30, help="Number of days to cover"),
18
+ top: int = typer.Option(10, help="Number of top queries to include"),
19
+ output: str = typer.Option(None, "--output", "-o", help="Output file path (default: snowglobe_report_DATE.md)"),
20
+ ):
21
+ """Generate a full report: cost summary, AI costs, storage, top queries."""
22
+ from snowglobe.core.report_service import ReportService
23
+
24
+ output_path = output or _default_output_path()
25
+ context = ctx.obj
26
+
27
+ typer.echo(f"\nGenerating full report ({days} days)...")
28
+ service = ReportService(context)
29
+ _, data = service.generate_and_save(output_path, days=days, top_n=top)
30
+
31
+ # Print terminal summary
32
+ typer.echo(service.terminal_summary(data))
33
+ typer.secho(f" Report saved: {output_path}", fg=typer.colors.GREEN, bold=True)
34
+ typer.echo("")
35
+
36
+
37
+ @report_app.command()
38
+ def cost(
39
+ ctx: typer.Context,
40
+ days: int = typer.Option(30, help="Number of days to cover"),
41
+ output: str = typer.Option(None, "--output", "-o", help="Output file path"),
42
+ ):
43
+ """Generate a cost-focused report: summary, AI, storage."""
44
+ from snowglobe.core.report_service import ReportService
45
+
46
+ output_path = output or f"snowglobe_cost_{date.today().isoformat()}.md"
47
+ context = ctx.obj
48
+
49
+ typer.echo(f"\nGenerating cost report ({days} days)...")
50
+ service = ReportService(context)
51
+ data = service.generate_full_report(days=days, top_n=0)
52
+ # Clear queries for cost-only report
53
+ data["top_queries"] = []
54
+ markdown = service.render_markdown(data)
55
+
56
+ from pathlib import Path
57
+ Path(output_path).write_text(markdown)
58
+
59
+ typer.echo(service.terminal_summary(data))
60
+ typer.secho(f" Report saved: {output_path}", fg=typer.colors.GREEN, bold=True)
61
+ typer.echo("")
62
+
63
+
64
+ @report_app.command()
65
+ def queries(
66
+ ctx: typer.Context,
67
+ days: int = typer.Option(7, help="Number of days to cover"),
68
+ top: int = typer.Option(10, help="Number of queries to include"),
69
+ output: str = typer.Option(None, "--output", "-o", help="Output file path"),
70
+ ):
71
+ """Generate a top-queries report with optimization details."""
72
+ from snowglobe.core.cost_service import CostService
73
+ from snowglobe.output import cli
74
+
75
+ context = ctx.obj
76
+ cost_service = CostService(context)
77
+
78
+ typer.echo(f"\nFetching top {top} queries ({days} days)...")
79
+ df, _, _ = cost_service.get_top_queries(days=days, limit=top)
80
+
81
+ if df.empty:
82
+ typer.echo(" No query data found.")
83
+ return
84
+
85
+ # Terminal output
86
+ cli.print_table(df, title=f"Top {top} Expensive Queries ({days} days)")
87
+
88
+ # Save to file if requested
89
+ if output:
90
+ df.to_csv(output, index=False)
91
+ typer.secho(f"\n Exported to: {output}", fg=typer.colors.GREEN)