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,128 @@
1
+ from prompt_toolkit.completion import Completer, Completion
2
+
3
+ SHELL_COMMANDS = ["check", "roles", "members", "path", "escalation", "scan", "use", "set", "access", "whoaccess", "create", "cost", "optimize", "drift", "unused", "report", "refresh", "status", "debug", "help", "exit", "?"]
4
+ COST_SUBCOMMANDS = ["summary", "warehouses", "users", "ai", "ai-users", "services", "queries", "trend", "storage", "budget", "replication", "mv"]
5
+ SET_FIELDS = ["object_type", "object_name", "privilege"]
6
+
7
+
8
+ class SnowglobeCompleter(Completer):
9
+ def __init__(self, ctx):
10
+ self.ctx = ctx
11
+
12
+ def get_completions(self, document, complete_event):
13
+ text = document.text_before_cursor
14
+ word = document.get_word_before_cursor()
15
+ start_position = -len(word)
16
+
17
+ parts = text.strip().split()
18
+ new_word = text.endswith(" ")
19
+
20
+ # No input yet — suggest all commands
21
+ if not parts:
22
+ for cmd in SHELL_COMMANDS:
23
+ yield Completion(cmd, start_position=start_position)
24
+ return
25
+
26
+ command = parts[0]
27
+
28
+ # Completing command name (first word, still typing)
29
+ if len(parts) == 1 and not new_word:
30
+ for cmd in SHELL_COMMANDS:
31
+ if cmd.startswith(word):
32
+ yield Completion(cmd, start_position=start_position)
33
+ return
34
+
35
+ # ---- roles command (expects a username) ----
36
+ if command == "roles":
37
+ if (len(parts) == 1 and new_word) or (len(parts) == 2 and not new_word):
38
+ yield from self._complete_users(start_position, word if not new_word else None)
39
+ return
40
+
41
+ # ---- members command (expects a role) ----
42
+ if command == "members":
43
+ if (len(parts) == 1 and new_word) or (len(parts) == 2 and not new_word):
44
+ yield from self._complete_roles(start_position, word if not new_word else None)
45
+ return
46
+
47
+ # ---- path command (expects from_role, then to_role) ----
48
+ if command == "path":
49
+ if (len(parts) == 1 and new_word) or (len(parts) == 2 and not new_word):
50
+ yield from self._complete_roles(start_position, word if not new_word else None)
51
+ return
52
+ if (len(parts) == 2 and new_word) or (len(parts) == 3 and not new_word):
53
+ yield from self._complete_roles(start_position, word if not new_word else None)
54
+ return
55
+
56
+ # ---- escalation command (expects a role) ----
57
+ if command == "escalation":
58
+ if (len(parts) == 1 and new_word) or (len(parts) == 2 and not new_word):
59
+ yield from self._complete_roles(start_position, word if not new_word else None)
60
+ return
61
+
62
+ # ---- cost command (expects a subcommand) ----
63
+ if command == "cost":
64
+ if (len(parts) == 1 and new_word) or (len(parts) == 2 and not new_word):
65
+ prefix = word if not new_word else None
66
+ for sub in COST_SUBCOMMANDS:
67
+ if prefix is None or sub.startswith(prefix):
68
+ yield Completion(sub, start_position=start_position)
69
+ return
70
+
71
+ # ---- use command ----
72
+ if command == "use":
73
+ if len(parts) == 1 and new_word:
74
+ for option in ["user", "role"]:
75
+ yield Completion(option, start_position=0)
76
+ return
77
+
78
+ if len(parts) == 2 and not new_word:
79
+ for option in ["user", "role"]:
80
+ if option.startswith(word):
81
+ yield Completion(option, start_position=start_position)
82
+ return
83
+
84
+ # Completing the name (third token)
85
+ if len(parts) == 2 and new_word:
86
+ kind = parts[1]
87
+ if kind == "role":
88
+ yield from self._complete_roles(0, None)
89
+ elif kind == "user":
90
+ yield from self._complete_users(0, None)
91
+ return
92
+
93
+ if len(parts) == 3 and not new_word:
94
+ kind = parts[1]
95
+ if kind == "role":
96
+ yield from self._complete_roles(start_position, word)
97
+ elif kind == "user":
98
+ yield from self._complete_users(start_position, word)
99
+ return
100
+
101
+ # ---- set command ----
102
+ if command == "set":
103
+ if len(parts) == 1 and new_word:
104
+ for field in SET_FIELDS:
105
+ yield Completion(field, start_position=0)
106
+ return
107
+
108
+ if len(parts) == 2 and not new_word:
109
+ for field in SET_FIELDS:
110
+ if field.startswith(word):
111
+ yield Completion(field, start_position=start_position)
112
+ return
113
+
114
+ def _complete_roles(self, start_position, prefix=None):
115
+ """Yield role name completions."""
116
+ if not self.ctx.role_graph:
117
+ return
118
+ for role in self.ctx.role_graph.roles.keys():
119
+ if prefix is None or role.upper().startswith(prefix.upper()):
120
+ yield Completion(role, start_position=start_position)
121
+
122
+ def _complete_users(self, start_position, prefix=None):
123
+ """Yield user name completions."""
124
+ if not self.ctx.user_graph:
125
+ return
126
+ for user in self.ctx.user_graph.assigned_roles.keys():
127
+ if prefix is None or user.upper().startswith(prefix.upper()):
128
+ yield Completion(user, start_position=start_position)