snowglobe-cli 0.2.1__tar.gz → 0.2.3__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.
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/PKG-INFO +1 -1
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/pyproject.toml +1 -1
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/core/cost_service.py +371 -7
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/tui/screens/cost.py +246 -8
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe_cli.egg-info/PKG-INFO +1 -1
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/LICENSE +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/README.md +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/setup.cfg +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/__init__.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/__main__.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/cli/__init__.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/cli/access.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/cli/app.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/cli/context.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/cli/cost.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/cli/debug.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/cli/diff.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/cli/optimizer.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/cli/prompts.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/cli/report.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/cli/shell.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/cli/shell_completer.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/collectors/access.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/collectors/query_history.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/collectors/query_profile.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/config/loader.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/core/access_service.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/core/optimizer.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/core/query_service.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/core/report_service.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/core/risk_service.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/engines/access/__init__.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/engines/access/explainer.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/engines/access/resolver.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/engines/ai/cortex_optimizer.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/engines/optimizer/query_optimizer.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/graphs/__init__.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/graphs/role_graph.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/graphs/user_graph.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/models/__init__.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/models/access.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/models/access_path.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/models/object_ref.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/models/object_type.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/models/optimizer.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/models/privilege.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/models/query.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/output/__init__.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/output/cli.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/queries/__init__.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/queries/query_history.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/snowflake/connection.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/state/db.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/state/state.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/templates/report.md.j2 +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/tests/access_tests.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/tui/__init__.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/tui/__main__.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/tui/app.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/tui/screens/__init__.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/tui/screens/access.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/tui/screens/home.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/tui/screens/refresh.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/tui/screens/reports.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/tui/screens/risk.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/tui/screens/tune.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/tui/styles.tcss +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/tui/widgets/__init__.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/tui/widgets/access_paths.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/tui/widgets/cache_badge.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/tui/widgets/header.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe/tui/widgets/nav.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe_cli.egg-info/SOURCES.txt +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe_cli.egg-info/dependency_links.txt +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe_cli.egg-info/entry_points.txt +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe_cli.egg-info/requires.txt +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/snowglobe_cli.egg-info/top_level.txt +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/tests/test_access.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/tests/test_optimizer_engine.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/tests/test_output.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/tests/test_query_validation.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/tests/test_state_db.py +0 -0
- {snowglobe_cli-0.2.1 → snowglobe_cli-0.2.3}/tests/test_tui_tune.py +0 -0
|
@@ -952,22 +952,386 @@ class CostService:
|
|
|
952
952
|
# View may not exist if no MVs are used
|
|
953
953
|
return pd.DataFrame(), None
|
|
954
954
|
|
|
955
|
-
def
|
|
955
|
+
def get_day_warehouse_breakdown(self, date: str) -> pd.DataFrame:
|
|
956
956
|
"""Warehouse credit breakdown for a single calendar day."""
|
|
957
957
|
conn = self.context.connect()
|
|
958
958
|
with conn:
|
|
959
959
|
rows = conn.query(f"""
|
|
960
|
-
SELECT WAREHOUSE_NAME,
|
|
960
|
+
SELECT WAREHOUSE_NAME AS RESOURCE_NAME,
|
|
961
961
|
ROUND(SUM(CREDITS_USED), 4) AS CREDITS
|
|
962
962
|
FROM SNOWFLAKE.ACCOUNT_USAGE.WAREHOUSE_METERING_HISTORY
|
|
963
|
-
WHERE START_TIME::DATE = '{date}'
|
|
963
|
+
WHERE START_TIME::DATE = '{_sq(date)}'
|
|
964
964
|
AND WAREHOUSE_NAME IS NOT NULL
|
|
965
|
-
GROUP BY 1
|
|
966
|
-
ORDER BY 2 DESC
|
|
967
|
-
LIMIT 30
|
|
965
|
+
GROUP BY 1 ORDER BY 2 DESC LIMIT 50
|
|
968
966
|
""")
|
|
969
967
|
if not rows:
|
|
970
|
-
return pd.DataFrame(columns=["
|
|
968
|
+
return pd.DataFrame(columns=["RESOURCE_NAME", "CREDITS"])
|
|
971
969
|
df = pd.DataFrame(rows)
|
|
972
970
|
df["CREDITS"] = df["CREDITS"].astype(float)
|
|
973
971
|
return df
|
|
972
|
+
|
|
973
|
+
def get_day_service_breakdown(self, date: str) -> pd.DataFrame:
|
|
974
|
+
"""All service types and credit totals for a single calendar day."""
|
|
975
|
+
conn = self.context.connect()
|
|
976
|
+
with conn:
|
|
977
|
+
rows = conn.query(f"""
|
|
978
|
+
SELECT SERVICE_TYPE,
|
|
979
|
+
ROUND(SUM(CREDITS_BILLED), 4) AS CREDITS
|
|
980
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.METERING_DAILY_HISTORY
|
|
981
|
+
WHERE USAGE_DATE = '{_sq(date)}'
|
|
982
|
+
AND SERVICE_TYPE IS NOT NULL
|
|
983
|
+
GROUP BY 1 ORDER BY 2 DESC
|
|
984
|
+
""")
|
|
985
|
+
if not rows:
|
|
986
|
+
return pd.DataFrame(columns=["SERVICE_TYPE", "CREDITS"])
|
|
987
|
+
df = pd.DataFrame(rows)
|
|
988
|
+
df["CREDITS"] = df["CREDITS"].astype(float)
|
|
989
|
+
return df
|
|
990
|
+
|
|
991
|
+
def get_day_resource_breakdown(self, date: str, service_type: str) -> tuple[pd.DataFrame, str, bool]:
|
|
992
|
+
"""
|
|
993
|
+
Resource-level credits for a service type on a single calendar day.
|
|
994
|
+
Returns (df, resource_label, found).
|
|
995
|
+
found=False when service_type has no known backing view.
|
|
996
|
+
"""
|
|
997
|
+
_empty = pd.DataFrame(columns=["RESOURCE_NAME", "CREDITS"])
|
|
998
|
+
d = _sq(date)
|
|
999
|
+
s = service_type.upper()
|
|
1000
|
+
|
|
1001
|
+
candidates = [
|
|
1002
|
+
(
|
|
1003
|
+
s == "WAREHOUSE_METERING",
|
|
1004
|
+
"Warehouse",
|
|
1005
|
+
f"""SELECT WAREHOUSE_NAME AS RESOURCE_NAME,
|
|
1006
|
+
ROUND(SUM(CREDITS_USED), 4) AS CREDITS
|
|
1007
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.WAREHOUSE_METERING_HISTORY
|
|
1008
|
+
WHERE START_TIME::DATE = '{d}' AND WAREHOUSE_NAME IS NOT NULL
|
|
1009
|
+
GROUP BY 1 ORDER BY 2 DESC LIMIT 50""",
|
|
1010
|
+
),
|
|
1011
|
+
(
|
|
1012
|
+
"PIPE" in s or "SNOWPIPE" in s,
|
|
1013
|
+
"Pipe",
|
|
1014
|
+
f"""SELECT PIPE_NAME AS RESOURCE_NAME,
|
|
1015
|
+
ROUND(SUM(CREDITS_USED), 4) AS CREDITS
|
|
1016
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.PIPE_USAGE_HISTORY
|
|
1017
|
+
WHERE START_TIME::DATE = '{d}'
|
|
1018
|
+
GROUP BY 1 ORDER BY 2 DESC LIMIT 50""",
|
|
1019
|
+
),
|
|
1020
|
+
(
|
|
1021
|
+
"TASK" in s,
|
|
1022
|
+
"Task",
|
|
1023
|
+
f"""SELECT DATABASE_NAME || '.' || SCHEMA_NAME || '.' || TASK_NAME AS RESOURCE_NAME,
|
|
1024
|
+
ROUND(SUM(CREDITS_USED), 4) AS CREDITS
|
|
1025
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.SERVERLESS_TASK_HISTORY
|
|
1026
|
+
WHERE START_TIME::DATE = '{d}'
|
|
1027
|
+
GROUP BY 1 ORDER BY 2 DESC LIMIT 50""",
|
|
1028
|
+
),
|
|
1029
|
+
(
|
|
1030
|
+
"CONTAINER" in s or "SPCS" in s or "SNOWPARK" in s,
|
|
1031
|
+
"Compute Pool",
|
|
1032
|
+
f"""SELECT COMPUTE_POOL_NAME AS RESOURCE_NAME,
|
|
1033
|
+
ROUND(SUM(CREDITS_USED), 4) AS CREDITS
|
|
1034
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.SNOWPARK_CONTAINER_SERVICES_HISTORY
|
|
1035
|
+
WHERE START_TIME::DATE = '{d}'
|
|
1036
|
+
GROUP BY 1 ORDER BY 2 DESC LIMIT 50""",
|
|
1037
|
+
),
|
|
1038
|
+
(
|
|
1039
|
+
"CLUSTERING" in s,
|
|
1040
|
+
"Table",
|
|
1041
|
+
f"""SELECT DATABASE_NAME || '.' || SCHEMA_NAME || '.' || TABLE_NAME AS RESOURCE_NAME,
|
|
1042
|
+
ROUND(SUM(CREDITS_USED), 4) AS CREDITS
|
|
1043
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.AUTOMATIC_CLUSTERING_HISTORY
|
|
1044
|
+
WHERE START_TIME::DATE = '{d}'
|
|
1045
|
+
GROUP BY 1 ORDER BY 2 DESC LIMIT 50""",
|
|
1046
|
+
),
|
|
1047
|
+
(
|
|
1048
|
+
"SEARCH" in s,
|
|
1049
|
+
"Table",
|
|
1050
|
+
f"""SELECT DATABASE_NAME || '.' || SCHEMA_NAME || '.' || TABLE_NAME AS RESOURCE_NAME,
|
|
1051
|
+
ROUND(SUM(CREDITS_USED), 4) AS CREDITS
|
|
1052
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.SEARCH_OPTIMIZATION_HISTORY
|
|
1053
|
+
WHERE START_TIME::DATE = '{d}'
|
|
1054
|
+
GROUP BY 1 ORDER BY 2 DESC LIMIT 50""",
|
|
1055
|
+
),
|
|
1056
|
+
(
|
|
1057
|
+
"REPLICATION" in s,
|
|
1058
|
+
"Replication Group",
|
|
1059
|
+
f"""SELECT REPLICATION_GROUP_NAME AS RESOURCE_NAME,
|
|
1060
|
+
ROUND(SUM(CREDITS_USED), 4) AS CREDITS
|
|
1061
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.REPLICATION_GROUP_USAGE_HISTORY
|
|
1062
|
+
WHERE START_TIME::DATE = '{d}'
|
|
1063
|
+
GROUP BY 1 ORDER BY 2 DESC LIMIT 50""",
|
|
1064
|
+
),
|
|
1065
|
+
(
|
|
1066
|
+
"MATERIALIZED" in s or s == "MV",
|
|
1067
|
+
"Materialized View",
|
|
1068
|
+
f"""SELECT DATABASE_NAME || '.' || SCHEMA_NAME || '.' || TABLE_NAME AS RESOURCE_NAME,
|
|
1069
|
+
ROUND(SUM(CREDITS_USED), 4) AS CREDITS
|
|
1070
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.MATERIALIZED_VIEW_REFRESH_HISTORY
|
|
1071
|
+
WHERE START_TIME::DATE = '{d}'
|
|
1072
|
+
GROUP BY 1 ORDER BY 2 DESC LIMIT 50""",
|
|
1073
|
+
),
|
|
1074
|
+
]
|
|
1075
|
+
|
|
1076
|
+
conn = self.context.connect()
|
|
1077
|
+
for matches, label, sql in candidates:
|
|
1078
|
+
if not matches:
|
|
1079
|
+
continue
|
|
1080
|
+
try:
|
|
1081
|
+
with conn:
|
|
1082
|
+
rows = conn.query(sql)
|
|
1083
|
+
if not rows:
|
|
1084
|
+
return _empty, label, True
|
|
1085
|
+
df = pd.DataFrame(rows)
|
|
1086
|
+
df["CREDITS"] = df["CREDITS"].astype(float)
|
|
1087
|
+
return df, label, True
|
|
1088
|
+
except Exception:
|
|
1089
|
+
return _empty, label, True
|
|
1090
|
+
|
|
1091
|
+
return _empty, service_type, False
|
|
1092
|
+
|
|
1093
|
+
def get_day_ai_breakdown(self, date: str) -> tuple[pd.DataFrame, bool]:
|
|
1094
|
+
"""Cortex AI service credit breakdown for a single calendar day."""
|
|
1095
|
+
d = _sq(date)
|
|
1096
|
+
sources = [
|
|
1097
|
+
("Cortex Functions", f"""
|
|
1098
|
+
SELECT u.LOGIN_NAME AS USER_NAME, 'Cortex Functions' AS SERVICE,
|
|
1099
|
+
f.credits AS TOKEN_CREDITS
|
|
1100
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.CORTEX_AI_FUNCTIONS_USAGE_HISTORY f
|
|
1101
|
+
INNER JOIN SNOWFLAKE.ACCOUNT_USAGE.USERS u ON u.USER_ID = f.USER_ID
|
|
1102
|
+
WHERE f.START_TIME::DATE = '{d}' AND f.credits > 0
|
|
1103
|
+
"""),
|
|
1104
|
+
("Cortex Analyst", f"""
|
|
1105
|
+
SELECT USERNAME AS USER_NAME, 'Cortex Analyst' AS SERVICE,
|
|
1106
|
+
credits AS TOKEN_CREDITS
|
|
1107
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.CORTEX_ANALYST_USAGE_HISTORY
|
|
1108
|
+
WHERE START_TIME::DATE = '{d}' AND credits > 0
|
|
1109
|
+
"""),
|
|
1110
|
+
("Cortex Agent", f"""
|
|
1111
|
+
SELECT USER_NAME, 'Cortex Agent' AS SERVICE, token_credits AS TOKEN_CREDITS
|
|
1112
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.CORTEX_AGENT_USAGE_HISTORY
|
|
1113
|
+
WHERE START_TIME::DATE = '{d}' AND token_credits > 0
|
|
1114
|
+
"""),
|
|
1115
|
+
("Cortex Code", f"""
|
|
1116
|
+
SELECT USER_NAME, 'Cortex Code' AS SERVICE, token_credits AS TOKEN_CREDITS
|
|
1117
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.CORTEX_CODE_CLI_USAGE_HISTORY
|
|
1118
|
+
WHERE USAGE_TIME::DATE = '{d}' AND token_credits > 0
|
|
1119
|
+
UNION ALL
|
|
1120
|
+
SELECT USER_NAME, 'Cortex Code', token_credits
|
|
1121
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.CORTEX_CODE_SNOWSIGHT_USAGE_HISTORY
|
|
1122
|
+
WHERE USAGE_TIME::DATE = '{d}' AND token_credits > 0
|
|
1123
|
+
UNION ALL
|
|
1124
|
+
SELECT USER_NAME, 'Cortex Code', token_credits
|
|
1125
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.CORTEX_CODE_DESKTOP_USAGE_HISTORY
|
|
1126
|
+
WHERE USAGE_TIME::DATE = '{d}' AND token_credits > 0
|
|
1127
|
+
"""),
|
|
1128
|
+
("Snowflake Intelligence", f"""
|
|
1129
|
+
SELECT USER_NAME, 'Snowflake Intelligence' AS SERVICE,
|
|
1130
|
+
token_credits AS TOKEN_CREDITS
|
|
1131
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.SNOWFLAKE_INTELLIGENCE_USAGE_HISTORY
|
|
1132
|
+
WHERE START_TIME::DATE = '{d}' AND token_credits > 0
|
|
1133
|
+
"""),
|
|
1134
|
+
]
|
|
1135
|
+
frames = []
|
|
1136
|
+
any_missing = False
|
|
1137
|
+
conn = self.context.connect()
|
|
1138
|
+
with conn:
|
|
1139
|
+
for _, sql in sources:
|
|
1140
|
+
try:
|
|
1141
|
+
rows = conn.query(sql)
|
|
1142
|
+
if rows:
|
|
1143
|
+
frames.append(pd.DataFrame(rows))
|
|
1144
|
+
except Exception:
|
|
1145
|
+
any_missing = True
|
|
1146
|
+
|
|
1147
|
+
_empty = pd.DataFrame(columns=["RESOURCE_NAME", "CREDITS", "REQUEST_COUNT"])
|
|
1148
|
+
if not frames:
|
|
1149
|
+
return _empty, any_missing
|
|
1150
|
+
|
|
1151
|
+
raw = pd.concat(frames, ignore_index=True)
|
|
1152
|
+
raw["TOKEN_CREDITS"] = raw["TOKEN_CREDITS"].astype(float)
|
|
1153
|
+
df = (
|
|
1154
|
+
raw.groupby("SERVICE")
|
|
1155
|
+
.agg(CREDITS=("TOKEN_CREDITS", "sum"), REQUEST_COUNT=("TOKEN_CREDITS", "count"))
|
|
1156
|
+
.reset_index()
|
|
1157
|
+
.rename(columns={"SERVICE": "RESOURCE_NAME"})
|
|
1158
|
+
.sort_values("CREDITS", ascending=False)
|
|
1159
|
+
.reset_index(drop=True)
|
|
1160
|
+
)
|
|
1161
|
+
df["CREDITS"] = df["CREDITS"].round(4)
|
|
1162
|
+
return df, any_missing
|
|
1163
|
+
|
|
1164
|
+
def get_day_resource_users(self, date: str, service_type: str, resource: str) -> tuple[pd.DataFrame, str | None]:
|
|
1165
|
+
"""
|
|
1166
|
+
User breakdown for a specific resource on a single calendar day.
|
|
1167
|
+
Supports WAREHOUSE_METERING (QUERY_ATTRIBUTION_HISTORY) and AI/Cortex types.
|
|
1168
|
+
Returns (df[USER_NAME, CREDITS, REQUESTS], note).
|
|
1169
|
+
"""
|
|
1170
|
+
_empty = pd.DataFrame(columns=["USER_NAME", "CREDITS", "REQUESTS"])
|
|
1171
|
+
d = _sq(date)
|
|
1172
|
+
svc_upper = service_type.upper()
|
|
1173
|
+
|
|
1174
|
+
if svc_upper == "WAREHOUSE_METERING":
|
|
1175
|
+
safe_wh = _sq(resource)
|
|
1176
|
+
conn = self.context.connect()
|
|
1177
|
+
try:
|
|
1178
|
+
with conn:
|
|
1179
|
+
rows = conn.query(f"""
|
|
1180
|
+
SELECT USER_NAME,
|
|
1181
|
+
ROUND(SUM(CREDITS_ATTRIBUTED_COMPUTE), 4) AS CREDITS,
|
|
1182
|
+
COUNT(*) AS REQUESTS
|
|
1183
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.QUERY_ATTRIBUTION_HISTORY
|
|
1184
|
+
WHERE START_TIME::DATE = '{d}'
|
|
1185
|
+
AND WAREHOUSE_NAME = '{safe_wh}'
|
|
1186
|
+
GROUP BY 1 ORDER BY 2 DESC LIMIT 50
|
|
1187
|
+
""")
|
|
1188
|
+
if not rows:
|
|
1189
|
+
return _empty, None
|
|
1190
|
+
df = pd.DataFrame(rows)
|
|
1191
|
+
df["CREDITS"] = df["CREDITS"].astype(float)
|
|
1192
|
+
return df, None
|
|
1193
|
+
except Exception:
|
|
1194
|
+
pass
|
|
1195
|
+
conn2 = self.context.connect()
|
|
1196
|
+
try:
|
|
1197
|
+
with conn2:
|
|
1198
|
+
rows = conn2.query(f"""
|
|
1199
|
+
SELECT USER_NAME, 0.0 AS CREDITS, COUNT(*) AS REQUESTS
|
|
1200
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY
|
|
1201
|
+
WHERE START_TIME::DATE = '{d}'
|
|
1202
|
+
AND WAREHOUSE_NAME = '{safe_wh}'
|
|
1203
|
+
AND EXECUTION_STATUS = 'SUCCESS'
|
|
1204
|
+
GROUP BY 1 ORDER BY 3 DESC LIMIT 50
|
|
1205
|
+
""")
|
|
1206
|
+
if not rows:
|
|
1207
|
+
return _empty, None
|
|
1208
|
+
df = pd.DataFrame(rows)
|
|
1209
|
+
df["CREDITS"] = df["CREDITS"].astype(float)
|
|
1210
|
+
return df, "Query Attribution not enabled — showing query counts only"
|
|
1211
|
+
except Exception:
|
|
1212
|
+
return _empty, None
|
|
1213
|
+
|
|
1214
|
+
_ai_kw = ("AI", "CORTEX", "INTELLIGENCE")
|
|
1215
|
+
if not any(kw in svc_upper for kw in _ai_kw):
|
|
1216
|
+
return _empty, "No user-level detail available for this service type"
|
|
1217
|
+
|
|
1218
|
+
sql_map = {
|
|
1219
|
+
"Cortex Functions": f"""
|
|
1220
|
+
SELECT u.LOGIN_NAME AS USER_NAME,
|
|
1221
|
+
ROUND(SUM(f.credits), 4) AS CREDITS, COUNT(*) AS REQUESTS
|
|
1222
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.CORTEX_AI_FUNCTIONS_USAGE_HISTORY f
|
|
1223
|
+
INNER JOIN SNOWFLAKE.ACCOUNT_USAGE.USERS u ON u.USER_ID = f.USER_ID
|
|
1224
|
+
WHERE f.START_TIME::DATE = '{d}' AND f.credits > 0
|
|
1225
|
+
GROUP BY 1 ORDER BY 2 DESC LIMIT 50
|
|
1226
|
+
""",
|
|
1227
|
+
"Cortex Analyst": f"""
|
|
1228
|
+
SELECT USERNAME AS USER_NAME,
|
|
1229
|
+
ROUND(SUM(credits), 4) AS CREDITS, COUNT(*) AS REQUESTS
|
|
1230
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.CORTEX_ANALYST_USAGE_HISTORY
|
|
1231
|
+
WHERE START_TIME::DATE = '{d}' AND credits > 0
|
|
1232
|
+
GROUP BY 1 ORDER BY 2 DESC LIMIT 50
|
|
1233
|
+
""",
|
|
1234
|
+
"Cortex Agent": f"""
|
|
1235
|
+
SELECT USER_NAME,
|
|
1236
|
+
ROUND(SUM(token_credits), 4) AS CREDITS, COUNT(*) AS REQUESTS
|
|
1237
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.CORTEX_AGENT_USAGE_HISTORY
|
|
1238
|
+
WHERE START_TIME::DATE = '{d}' AND token_credits > 0
|
|
1239
|
+
GROUP BY 1 ORDER BY 2 DESC LIMIT 50
|
|
1240
|
+
""",
|
|
1241
|
+
"Cortex Code": f"""
|
|
1242
|
+
SELECT USER_NAME,
|
|
1243
|
+
ROUND(SUM(token_credits), 4) AS CREDITS, COUNT(*) AS REQUESTS
|
|
1244
|
+
FROM (
|
|
1245
|
+
SELECT USER_NAME, token_credits
|
|
1246
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.CORTEX_CODE_CLI_USAGE_HISTORY
|
|
1247
|
+
WHERE USAGE_TIME::DATE = '{d}' AND token_credits > 0
|
|
1248
|
+
UNION ALL
|
|
1249
|
+
SELECT USER_NAME, token_credits
|
|
1250
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.CORTEX_CODE_SNOWSIGHT_USAGE_HISTORY
|
|
1251
|
+
WHERE USAGE_TIME::DATE = '{d}' AND token_credits > 0
|
|
1252
|
+
UNION ALL
|
|
1253
|
+
SELECT USER_NAME, token_credits
|
|
1254
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.CORTEX_CODE_DESKTOP_USAGE_HISTORY
|
|
1255
|
+
WHERE USAGE_TIME::DATE = '{d}' AND token_credits > 0
|
|
1256
|
+
) GROUP BY 1 ORDER BY 2 DESC LIMIT 50
|
|
1257
|
+
""",
|
|
1258
|
+
"Snowflake Intelligence": f"""
|
|
1259
|
+
SELECT USER_NAME,
|
|
1260
|
+
ROUND(SUM(token_credits), 4) AS CREDITS, COUNT(*) AS REQUESTS
|
|
1261
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.SNOWFLAKE_INTELLIGENCE_USAGE_HISTORY
|
|
1262
|
+
WHERE START_TIME::DATE = '{d}' AND token_credits > 0
|
|
1263
|
+
GROUP BY 1 ORDER BY 2 DESC LIMIT 50
|
|
1264
|
+
""",
|
|
1265
|
+
}
|
|
1266
|
+
sql = sql_map.get(resource)
|
|
1267
|
+
if not sql:
|
|
1268
|
+
return _empty, f"No user breakdown available for: {resource}"
|
|
1269
|
+
conn = self.context.connect()
|
|
1270
|
+
try:
|
|
1271
|
+
with conn:
|
|
1272
|
+
rows = conn.query(sql)
|
|
1273
|
+
if not rows:
|
|
1274
|
+
return _empty, None
|
|
1275
|
+
df = pd.DataFrame(rows)
|
|
1276
|
+
df["CREDITS"] = df["CREDITS"].astype(float)
|
|
1277
|
+
return df, None
|
|
1278
|
+
except Exception:
|
|
1279
|
+
return _empty, "Could not load user breakdown"
|
|
1280
|
+
|
|
1281
|
+
def get_day_user_queries(self, date: str, warehouse: str, user: str, limit: int = 3) -> tuple[pd.DataFrame, str | None]:
|
|
1282
|
+
"""Top queries for a user on a specific warehouse+date.
|
|
1283
|
+
Uses QUERY_ATTRIBUTION_HISTORY; falls back to QUERY_HISTORY ranked by elapsed time."""
|
|
1284
|
+
d = _sq(date)
|
|
1285
|
+
safe_wh = _sq(warehouse)
|
|
1286
|
+
safe_user = _sq(user)
|
|
1287
|
+
conn = self.context.connect()
|
|
1288
|
+
try:
|
|
1289
|
+
with conn:
|
|
1290
|
+
rows = conn.query(f"""
|
|
1291
|
+
SELECT a.QUERY_ID,
|
|
1292
|
+
ROUND(a.CREDITS_ATTRIBUTED_COMPUTE, 4) AS CREDITS,
|
|
1293
|
+
q.QUERY_TYPE,
|
|
1294
|
+
LEFT(q.QUERY_TEXT, 80) AS QUERY_PREVIEW,
|
|
1295
|
+
ROUND(q.TOTAL_ELAPSED_TIME / 1000, 1) AS SECONDS,
|
|
1296
|
+
ROUND(q.BYTES_SCANNED / 1e9, 2) AS GB_SCANNED
|
|
1297
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.QUERY_ATTRIBUTION_HISTORY a
|
|
1298
|
+
LEFT JOIN SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY q ON a.QUERY_ID = q.QUERY_ID
|
|
1299
|
+
WHERE a.START_TIME::DATE = '{d}'
|
|
1300
|
+
AND a.WAREHOUSE_NAME = '{safe_wh}'
|
|
1301
|
+
AND a.USER_NAME = '{safe_user}'
|
|
1302
|
+
ORDER BY a.CREDITS_ATTRIBUTED_COMPUTE DESC
|
|
1303
|
+
LIMIT {limit}
|
|
1304
|
+
""")
|
|
1305
|
+
if not rows:
|
|
1306
|
+
return pd.DataFrame(), None
|
|
1307
|
+
df = pd.DataFrame(rows)
|
|
1308
|
+
df["CREDITS"] = df["CREDITS"].astype(float)
|
|
1309
|
+
return df, None
|
|
1310
|
+
except Exception:
|
|
1311
|
+
pass
|
|
1312
|
+
|
|
1313
|
+
conn2 = self.context.connect()
|
|
1314
|
+
try:
|
|
1315
|
+
with conn2:
|
|
1316
|
+
rows = conn2.query(f"""
|
|
1317
|
+
SELECT QUERY_ID,
|
|
1318
|
+
0.0 AS CREDITS,
|
|
1319
|
+
QUERY_TYPE,
|
|
1320
|
+
LEFT(QUERY_TEXT, 80) AS QUERY_PREVIEW,
|
|
1321
|
+
ROUND(TOTAL_ELAPSED_TIME / 1000, 1) AS SECONDS,
|
|
1322
|
+
ROUND(BYTES_SCANNED / 1e9, 2) AS GB_SCANNED
|
|
1323
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY
|
|
1324
|
+
WHERE START_TIME::DATE = '{d}'
|
|
1325
|
+
AND WAREHOUSE_NAME = '{safe_wh}'
|
|
1326
|
+
AND USER_NAME = '{safe_user}'
|
|
1327
|
+
AND EXECUTION_STATUS = 'SUCCESS'
|
|
1328
|
+
ORDER BY TOTAL_ELAPSED_TIME DESC
|
|
1329
|
+
LIMIT {limit}
|
|
1330
|
+
""")
|
|
1331
|
+
if not rows:
|
|
1332
|
+
return pd.DataFrame(), "Query Attribution not enabled — showing slowest queries"
|
|
1333
|
+
df = pd.DataFrame(rows)
|
|
1334
|
+
df["CREDITS"] = df["CREDITS"].astype(float)
|
|
1335
|
+
return df, "Query Attribution not enabled — showing slowest queries"
|
|
1336
|
+
except Exception:
|
|
1337
|
+
return pd.DataFrame(), "Could not load queries"
|
|
@@ -20,6 +20,8 @@ from textual.widgets import Button, DataTable, Label, ListItem, ListView, Select
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
_BAR_WIDTH = 24 # max █ characters in trend column
|
|
23
|
+
_WAREHOUSE_SERVICE_TYPE = "WAREHOUSE_METERING"
|
|
24
|
+
_AI_SERVICE_KEYWORDS = ("AI", "CORTEX", "INTELLIGENCE")
|
|
23
25
|
|
|
24
26
|
|
|
25
27
|
def _bar(pct_or_ratio: float, width: int = _BAR_WIDTH) -> str:
|
|
@@ -38,12 +40,17 @@ class CostScreen(Vertical):
|
|
|
38
40
|
|
|
39
41
|
# Active view: "summary" | "trend" | "top_queries" | "warehouses" | "users"
|
|
40
42
|
# | "ai" | "ai_users" | "services" | "storage" | "replication" | "mv" | "budget"
|
|
41
|
-
# | "drill_service" | "drill_warehouse" | "drill_user"
|
|
43
|
+
# | "drill_service" | "drill_warehouse" | "drill_user"
|
|
44
|
+
# | "drill_day" | "drill_day_resource" | "drill_day_resource_users" | "drill_day_user_queries"
|
|
42
45
|
_current_view: str = "summary"
|
|
43
46
|
_drill_service: str | None = None
|
|
44
47
|
_drill_warehouse: str | None = None
|
|
45
48
|
_drill_user: str | None = None
|
|
46
49
|
_drill_day: str | None = None
|
|
50
|
+
_drill_day_service_type: str | None = None
|
|
51
|
+
_drill_day_resource: str | None = None
|
|
52
|
+
_drill_day_user: str | None = None
|
|
53
|
+
_drill_day_resource_kind: str = "other" # "warehouse" | "ai" | "other"
|
|
47
54
|
# Becomes True after the user explicitly picks a view, so Select.Changed
|
|
48
55
|
# events fired during the initial mount don't auto-trigger a Snowflake fetch.
|
|
49
56
|
_user_initiated: bool = False
|
|
@@ -140,6 +147,33 @@ class CostScreen(Vertical):
|
|
|
140
147
|
elif self._current_view == "trend":
|
|
141
148
|
self._drill_day = key
|
|
142
149
|
self._fetch_day_drill(key)
|
|
150
|
+
elif self._current_view == "drill_day":
|
|
151
|
+
self._drill_day_service_type = key
|
|
152
|
+
self._fetch_day_resource_drill(self._drill_day, key)
|
|
153
|
+
elif self._current_view == "drill_day_resource":
|
|
154
|
+
if self._drill_day_resource_kind in ("warehouse", "ai"):
|
|
155
|
+
self._fetch_day_resource_users(
|
|
156
|
+
self._drill_day, self._drill_day_service_type, key
|
|
157
|
+
)
|
|
158
|
+
else:
|
|
159
|
+
self.app.notify(
|
|
160
|
+
"No user-level detail available for this resource type.", timeout=3
|
|
161
|
+
)
|
|
162
|
+
elif self._current_view == "drill_day_resource_users":
|
|
163
|
+
if self._drill_day_resource_kind == "warehouse":
|
|
164
|
+
self._fetch_day_user_queries(self._drill_day, self._drill_day_resource, key)
|
|
165
|
+
else:
|
|
166
|
+
self.app.notify("No query detail available for AI services.", timeout=3)
|
|
167
|
+
elif self._current_view == "drill_day_user_queries":
|
|
168
|
+
try:
|
|
169
|
+
from textual.widgets import ContentSwitcher, Input
|
|
170
|
+
from snowglobe.tui.screens.tune import TuneScreen
|
|
171
|
+
self.app.query_one(ContentSwitcher).current = "tune"
|
|
172
|
+
tune = self.app.query_one(TuneScreen)
|
|
173
|
+
tune.query_one("#tu-query-id", Input).value = key
|
|
174
|
+
self.app.notify(f"Loaded {key[:24]}… into Tune. Press Analyse.", timeout=4)
|
|
175
|
+
except Exception:
|
|
176
|
+
self.app.notify(f"Query: {key[:24]}…", timeout=4)
|
|
143
177
|
elif self._current_view == "top_queries":
|
|
144
178
|
# Hand off to Tune via the App's content switcher.
|
|
145
179
|
try:
|
|
@@ -185,6 +219,18 @@ class CostScreen(Vertical):
|
|
|
185
219
|
self._fetch_user_drill(self._drill_user)
|
|
186
220
|
elif self._current_view == "drill_day" and self._drill_day:
|
|
187
221
|
self._fetch_day_drill(self._drill_day)
|
|
222
|
+
elif self._current_view == "drill_day_resource" and self._drill_day and self._drill_day_service_type:
|
|
223
|
+
self._fetch_day_resource_drill(self._drill_day, self._drill_day_service_type)
|
|
224
|
+
elif (self._current_view == "drill_day_resource_users"
|
|
225
|
+
and self._drill_day and self._drill_day_service_type and self._drill_day_resource):
|
|
226
|
+
self._fetch_day_resource_users(
|
|
227
|
+
self._drill_day, self._drill_day_service_type, self._drill_day_resource
|
|
228
|
+
)
|
|
229
|
+
elif (self._current_view == "drill_day_user_queries"
|
|
230
|
+
and self._drill_day and self._drill_day_resource and self._drill_day_user):
|
|
231
|
+
self._fetch_day_user_queries(
|
|
232
|
+
self._drill_day, self._drill_day_resource, self._drill_day_user
|
|
233
|
+
)
|
|
188
234
|
|
|
189
235
|
def action_back_from_drill(self) -> None:
|
|
190
236
|
if self._current_view == "drill_service":
|
|
@@ -198,7 +244,19 @@ class CostScreen(Vertical):
|
|
|
198
244
|
self._fetch_users()
|
|
199
245
|
elif self._current_view == "drill_day":
|
|
200
246
|
self._drill_day = None
|
|
247
|
+
self._drill_day_service_type = None
|
|
201
248
|
self._fetch_trend()
|
|
249
|
+
elif self._current_view == "drill_day_resource":
|
|
250
|
+
self._drill_day_service_type = None
|
|
251
|
+
self._fetch_day_drill(self._drill_day)
|
|
252
|
+
elif self._current_view == "drill_day_resource_users":
|
|
253
|
+
self._drill_day_resource = None
|
|
254
|
+
self._fetch_day_resource_drill(self._drill_day, self._drill_day_service_type)
|
|
255
|
+
elif self._current_view == "drill_day_user_queries":
|
|
256
|
+
self._drill_day_user = None
|
|
257
|
+
self._fetch_day_resource_users(
|
|
258
|
+
self._drill_day, self._drill_day_service_type, self._drill_day_resource
|
|
259
|
+
)
|
|
202
260
|
|
|
203
261
|
# --- Days helper --------------------------------------------------
|
|
204
262
|
|
|
@@ -545,17 +603,17 @@ class CostScreen(Vertical):
|
|
|
545
603
|
avg_str,
|
|
546
604
|
)
|
|
547
605
|
|
|
548
|
-
# --- Drill: day
|
|
606
|
+
# --- Drill: day breakdown (Level 1 — service types) --------------
|
|
549
607
|
|
|
550
608
|
def _fetch_day_drill(self, date: str) -> None:
|
|
551
609
|
self._current_view = "drill_day"
|
|
552
|
-
self._set_status(f"Loading breakdown for {date}… Esc to return")
|
|
610
|
+
self._set_status(f"Loading service breakdown for {date}… Esc to return")
|
|
553
611
|
self._day_drill_worker(date=date)
|
|
554
612
|
|
|
555
613
|
@work(thread=True, exclusive=True, group="cost")
|
|
556
614
|
def _day_drill_worker(self, *, date: str) -> None:
|
|
557
615
|
try:
|
|
558
|
-
df = self.app.get_cost_service().
|
|
616
|
+
df = self.app.get_cost_service().get_day_service_breakdown(date)
|
|
559
617
|
except Exception as e:
|
|
560
618
|
self.app.call_from_thread(self._fetch_failed, e)
|
|
561
619
|
return
|
|
@@ -565,23 +623,203 @@ class CostScreen(Vertical):
|
|
|
565
623
|
self._reset_table()
|
|
566
624
|
table = self.query_one(DataTable)
|
|
567
625
|
if df is None or df.empty:
|
|
568
|
-
table.add_columns("(no
|
|
626
|
+
table.add_columns("(no activity on this date)")
|
|
569
627
|
self._set_status(f"{date} · no data · Esc to return")
|
|
570
628
|
return
|
|
571
629
|
total = float(df["CREDITS"].sum())
|
|
572
630
|
max_c = float(df["CREDITS"].max()) if total > 0 else 1.0
|
|
573
|
-
table.add_columns("
|
|
631
|
+
table.add_columns("SERVICE TYPE", "CREDITS", "% OF DAY", "BAR")
|
|
574
632
|
for _, row in df.iterrows():
|
|
575
633
|
credits = float(row["CREDITS"])
|
|
576
634
|
pct = (credits / total * 100) if total > 0 else 0
|
|
577
635
|
table.add_row(
|
|
578
|
-
str(row["
|
|
636
|
+
str(row["SERVICE_TYPE"]),
|
|
579
637
|
f"{credits:,.4f}",
|
|
580
638
|
f"{pct:.1f}%",
|
|
581
639
|
_bar(credits / max_c),
|
|
640
|
+
key=str(row["SERVICE_TYPE"]),
|
|
641
|
+
)
|
|
642
|
+
self._set_status(
|
|
643
|
+
f"Day breakdown — {date} · {total:,.4f} credits · click a service to drill · Esc to return"
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
# --- Drill: resource breakdown (Level 2 — all service types) -----
|
|
647
|
+
|
|
648
|
+
def _fetch_day_resource_drill(self, date: str, service_type: str) -> None:
|
|
649
|
+
self._current_view = "drill_day_resource"
|
|
650
|
+
self._drill_day_service_type = service_type
|
|
651
|
+
self._set_status(f"Loading {service_type} breakdown for {date}… Esc to return")
|
|
652
|
+
upper = service_type.upper()
|
|
653
|
+
is_ai = any(kw in upper for kw in _AI_SERVICE_KEYWORDS)
|
|
654
|
+
self._day_resource_drill_worker(date=date, service_type=service_type, is_ai=is_ai)
|
|
655
|
+
|
|
656
|
+
@work(thread=True, exclusive=True, group="cost")
|
|
657
|
+
def _day_resource_drill_worker(self, *, date: str, service_type: str, is_ai: bool) -> None:
|
|
658
|
+
try:
|
|
659
|
+
svc = self.app.get_cost_service()
|
|
660
|
+
if is_ai:
|
|
661
|
+
df, any_missing = svc.get_day_ai_breakdown(date)
|
|
662
|
+
label = "AI Service"
|
|
663
|
+
found = True
|
|
664
|
+
note = "some Cortex views unavailable" if any_missing else None
|
|
665
|
+
else:
|
|
666
|
+
df, label, found = svc.get_day_resource_breakdown(date, service_type)
|
|
667
|
+
note = None
|
|
668
|
+
except Exception as e:
|
|
669
|
+
self.app.call_from_thread(self._fetch_failed, e)
|
|
670
|
+
return
|
|
671
|
+
self.app.call_from_thread(
|
|
672
|
+
self._render_day_resource_drill, df, date, service_type, label, found, note, is_ai
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
def _render_day_resource_drill(self, df: pd.DataFrame, date: str, service_type: str,
|
|
676
|
+
label: str, found: bool, note: str | None,
|
|
677
|
+
is_ai: bool = False) -> None:
|
|
678
|
+
# Store kind so navigation decisions don't depend on the SERVICE_TYPE string.
|
|
679
|
+
if is_ai:
|
|
680
|
+
self._drill_day_resource_kind = "ai"
|
|
681
|
+
elif label == "Warehouse":
|
|
682
|
+
self._drill_day_resource_kind = "warehouse"
|
|
683
|
+
else:
|
|
684
|
+
self._drill_day_resource_kind = "other"
|
|
685
|
+
|
|
686
|
+
self._reset_table()
|
|
687
|
+
table = self.query_one(DataTable)
|
|
688
|
+
note_suffix = f" · ⚠ {note}" if note else ""
|
|
689
|
+
if not found:
|
|
690
|
+
table.add_columns("(no detail available for this service type)")
|
|
691
|
+
self._set_status(
|
|
692
|
+
f"{service_type} — {date} · no resource detail available · Esc to return"
|
|
693
|
+
)
|
|
694
|
+
return
|
|
695
|
+
if df is None or df.empty:
|
|
696
|
+
table.add_columns(f"(no {label.lower()} activity on this date)")
|
|
697
|
+
self._set_status(f"{service_type} — {date} · no data{note_suffix} · Esc to return")
|
|
698
|
+
return
|
|
699
|
+
total = float(df["CREDITS"].sum())
|
|
700
|
+
max_c = float(df["CREDITS"].max()) if total > 0 else 1.0
|
|
701
|
+
has_requests = "REQUEST_COUNT" in df.columns
|
|
702
|
+
if has_requests:
|
|
703
|
+
table.add_columns(label.upper(), "CREDITS", "REQUESTS", "% OF SERVICE", "BAR")
|
|
704
|
+
else:
|
|
705
|
+
table.add_columns(label.upper(), "CREDITS", "% OF SERVICE", "BAR")
|
|
706
|
+
for _, row in df.iterrows():
|
|
707
|
+
credits = float(row["CREDITS"])
|
|
708
|
+
pct = (credits / total * 100) if total > 0 else 0
|
|
709
|
+
cells = [str(row["RESOURCE_NAME"]), f"{credits:,.4f}"]
|
|
710
|
+
if has_requests:
|
|
711
|
+
cells.append(str(int(row.get("REQUEST_COUNT", 0))))
|
|
712
|
+
cells += [f"{pct:.1f}%", _bar(credits / max_c)]
|
|
713
|
+
table.add_row(*cells, key=str(row["RESOURCE_NAME"]))
|
|
714
|
+
user_hint = (
|
|
715
|
+
" · click to see users"
|
|
716
|
+
if self._drill_day_resource_kind in ("warehouse", "ai")
|
|
717
|
+
else ""
|
|
718
|
+
)
|
|
719
|
+
self._set_status(
|
|
720
|
+
f"{service_type} — {date} · {total:,.4f} credits{note_suffix}{user_hint} · Esc to return"
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
# --- Drill: user breakdown (Level 3) -----------------------------
|
|
724
|
+
|
|
725
|
+
def _fetch_day_resource_users(self, date: str, service_type: str, resource: str) -> None:
|
|
726
|
+
self._current_view = "drill_day_resource_users"
|
|
727
|
+
self._drill_day_resource = resource
|
|
728
|
+
self._set_status(f"Loading users for {resource} on {date}… Esc to return")
|
|
729
|
+
self._day_resource_users_worker(date=date, service_type=service_type, resource=resource)
|
|
730
|
+
|
|
731
|
+
@work(thread=True, exclusive=True, group="cost")
|
|
732
|
+
def _day_resource_users_worker(self, *, date: str, service_type: str, resource: str) -> None:
|
|
733
|
+
try:
|
|
734
|
+
df, note = self.app.get_cost_service().get_day_resource_users(
|
|
735
|
+
date, service_type, resource
|
|
736
|
+
)
|
|
737
|
+
except Exception as e:
|
|
738
|
+
self.app.call_from_thread(self._fetch_failed, e)
|
|
739
|
+
return
|
|
740
|
+
self.app.call_from_thread(self._render_day_resource_users, df, date, resource, note)
|
|
741
|
+
|
|
742
|
+
def _render_day_resource_users(self, df: pd.DataFrame, date: str,
|
|
743
|
+
resource: str, note: str | None) -> None:
|
|
744
|
+
self._reset_table()
|
|
745
|
+
table = self.query_one(DataTable)
|
|
746
|
+
note_suffix = f" · ⚠ {note}" if note else ""
|
|
747
|
+
if note and "No user-level" in note:
|
|
748
|
+
table.add_columns("(no user-level detail available for this resource type)")
|
|
749
|
+
self._set_status(f"{resource} — {date}{note_suffix} · Esc to return")
|
|
750
|
+
return
|
|
751
|
+
if df is None or df.empty:
|
|
752
|
+
table.add_columns("(no users found)")
|
|
753
|
+
self._set_status(f"{resource} — {date} · no users{note_suffix} · Esc to return")
|
|
754
|
+
return
|
|
755
|
+
total = float(df["CREDITS"].sum())
|
|
756
|
+
max_c = float(df["CREDITS"].max()) if total > 0 else 1.0
|
|
757
|
+
table.add_columns("USER", "CREDITS", "QUERIES", "% OF RESOURCE", "BAR")
|
|
758
|
+
for _, row in df.iterrows():
|
|
759
|
+
credits = float(row["CREDITS"])
|
|
760
|
+
pct = (credits / total * 100) if total > 0 else 0
|
|
761
|
+
table.add_row(
|
|
762
|
+
str(row["USER_NAME"]),
|
|
763
|
+
f"{credits:,.4f}",
|
|
764
|
+
str(int(row.get("REQUESTS", 0))),
|
|
765
|
+
f"{pct:.1f}%",
|
|
766
|
+
_bar(credits / max_c),
|
|
767
|
+
key=str(row["USER_NAME"]),
|
|
768
|
+
)
|
|
769
|
+
query_hint = (
|
|
770
|
+
" · click user to see top queries"
|
|
771
|
+
if self._drill_day_resource_kind == "warehouse"
|
|
772
|
+
else ""
|
|
773
|
+
)
|
|
774
|
+
self._set_status(
|
|
775
|
+
f"{resource} users — {date} · {total:,.4f} credits{note_suffix}{query_hint} · Esc to return"
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
# --- Drill: top queries for a user (Level 4) ----------------------
|
|
779
|
+
|
|
780
|
+
def _fetch_day_user_queries(self, date: str, warehouse: str, user: str) -> None:
|
|
781
|
+
self._current_view = "drill_day_user_queries"
|
|
782
|
+
self._drill_day_user = user
|
|
783
|
+
self._set_status(
|
|
784
|
+
f"Loading top queries for {user} on {warehouse} ({date})… Esc to return"
|
|
785
|
+
)
|
|
786
|
+
self._day_user_queries_worker(date=date, warehouse=warehouse, user=user)
|
|
787
|
+
|
|
788
|
+
@work(thread=True, exclusive=True, group="cost")
|
|
789
|
+
def _day_user_queries_worker(self, *, date: str, warehouse: str, user: str) -> None:
|
|
790
|
+
try:
|
|
791
|
+
df, note = self.app.get_cost_service().get_day_user_queries(
|
|
792
|
+
date, warehouse, user, limit=3
|
|
793
|
+
)
|
|
794
|
+
except Exception as e:
|
|
795
|
+
self.app.call_from_thread(self._fetch_failed, e)
|
|
796
|
+
return
|
|
797
|
+
self.app.call_from_thread(self._render_day_user_queries, df, date, warehouse, user, note)
|
|
798
|
+
|
|
799
|
+
def _render_day_user_queries(self, df: pd.DataFrame, date: str,
|
|
800
|
+
warehouse: str, user: str, note: str | None) -> None:
|
|
801
|
+
self._reset_table()
|
|
802
|
+
table = self.query_one(DataTable)
|
|
803
|
+
note_suffix = f" · ⚠ {note}" if note else ""
|
|
804
|
+
if df is None or df.empty:
|
|
805
|
+
table.add_columns("(no queries found)")
|
|
806
|
+
self._set_status(
|
|
807
|
+
f"{user} on {warehouse} — {date} · no queries{note_suffix} · Esc to return"
|
|
808
|
+
)
|
|
809
|
+
return
|
|
810
|
+
table.add_columns("CREDITS", "TYPE", "SECONDS", "GB", "PREVIEW")
|
|
811
|
+
for _, row in df.iterrows():
|
|
812
|
+
table.add_row(
|
|
813
|
+
f"{float(row.get('CREDITS', 0)):,.4f}",
|
|
814
|
+
str(row.get("QUERY_TYPE", "")),
|
|
815
|
+
f"{float(row.get('SECONDS', 0)):.1f}",
|
|
816
|
+
f"{float(row.get('GB_SCANNED', 0)):.2f}",
|
|
817
|
+
str(row.get("QUERY_PREVIEW", ""))[:48],
|
|
818
|
+
key=str(row.get("QUERY_ID", "")),
|
|
582
819
|
)
|
|
583
820
|
self._set_status(
|
|
584
|
-
f"
|
|
821
|
+
f"Top queries — {user} on {warehouse} — {date}{note_suffix}"
|
|
822
|
+
f" · ⏎ open in Tune · Esc to return"
|
|
585
823
|
)
|
|
586
824
|
|
|
587
825
|
# --- View 6: AI services -----------------------------------------
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|