databay-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 (403) hide show
  1. databay/__init__.py +5 -0
  2. databay/__main__.py +7 -0
  3. databay/agent_skill.py +184 -0
  4. databay/api.py +626 -0
  5. databay/cli/__init__.py +0 -0
  6. databay/cli/auth.py +377 -0
  7. databay/cli/build.py +89 -0
  8. databay/cli/commands/__init__.py +0 -0
  9. databay/cli/commands/alerting_webhook.py +19 -0
  10. databay/cli/commands/alerting_webhook_tag.py +14 -0
  11. databay/cli/commands/alerting_webhook_type.py +14 -0
  12. databay/cli/commands/alerting_webhook_undelivered.py +15 -0
  13. databay/cli/commands/announcement.py +17 -0
  14. databay/cli/commands/api_key.py +18 -0
  15. databay/cli/commands/api_key_project.py +16 -0
  16. databay/cli/commands/backfill_reason_code.py +14 -0
  17. databay/cli/commands/backfill_run.py +17 -0
  18. databay/cli/commands/canvas.py +16 -0
  19. databay/cli/commands/canvas_graph.py +15 -0
  20. databay/cli/commands/canvas_table_check.py +15 -0
  21. databay/cli/commands/dashboards.py +21 -0
  22. databay/cli/commands/dashboards_card.py +20 -0
  23. databay/cli/commands/dashboards_dataset.py +14 -0
  24. databay/cli/commands/dashboards_folder.py +16 -0
  25. databay/cli/commands/dashboards_home.py +17 -0
  26. databay/cli/commands/dashboards_placement.py +16 -0
  27. databay/cli/commands/db.py +19 -0
  28. databay/cli/commands/db_column.py +14 -0
  29. databay/cli/commands/docs_article.py +16 -0
  30. databay/cli/commands/dq_result.py +14 -0
  31. databay/cli/commands/dq_rule.py +16 -0
  32. databay/cli/commands/feature.py +14 -0
  33. databay/cli/commands/google_ads_source.py +14 -0
  34. databay/cli/commands/google_analytics_source.py +14 -0
  35. databay/cli/commands/instagram_account.py +14 -0
  36. databay/cli/commands/instagram_source.py +14 -0
  37. databay/cli/commands/linkedin_account.py +14 -0
  38. databay/cli/commands/linkedin_source.py +14 -0
  39. databay/cli/commands/me.py +14 -0
  40. databay/cli/commands/meta_ads_source.py +14 -0
  41. databay/cli/commands/metric_models.py +26 -0
  42. databay/cli/commands/metrics.py +14 -0
  43. databay/cli/commands/models.py +16 -0
  44. databay/cli/commands/nlq.py +15 -0
  45. databay/cli/commands/notebook_credential.py +16 -0
  46. databay/cli/commands/org.py +14 -0
  47. databay/cli/commands/org_member.py +17 -0
  48. databay/cli/commands/org_offboarding.py +14 -0
  49. databay/cli/commands/pipe.py +26 -0
  50. databay/cli/commands/pipe_backfill_run.py +14 -0
  51. databay/cli/commands/pipe_config_option.py +14 -0
  52. databay/cli/commands/pipe_destination.py +16 -0
  53. databay/cli/commands/pipe_leftover.py +16 -0
  54. databay/cli/commands/pipe_sink_type.py +14 -0
  55. databay/cli/commands/pipe_source_type.py +14 -0
  56. databay/cli/commands/pipe_status_bucket.py +14 -0
  57. databay/cli/commands/pipe_stuck_delete.py +14 -0
  58. databay/cli/commands/plan.py +14 -0
  59. databay/cli/commands/project.py +21 -0
  60. databay/cli/commands/project_budget.py +15 -0
  61. databay/cli/commands/project_member.py +18 -0
  62. databay/cli/commands/project_privilege.py +16 -0
  63. databay/cli/commands/project_share.py +16 -0
  64. databay/cli/commands/provision.py +14 -0
  65. databay/cli/commands/provisioning.py +14 -0
  66. databay/cli/commands/registry.py +14 -0
  67. databay/cli/commands/resource_home.py +14 -0
  68. databay/cli/commands/schema_change.py +18 -0
  69. databay/cli/commands/schema_history.py +17 -0
  70. databay/cli/commands/schema_version.py +16 -0
  71. databay/cli/commands/shopify_store.py +14 -0
  72. databay/cli/commands/sink_data_type.py +14 -0
  73. databay/cli/commands/source.py +24 -0
  74. databay/cli/commands/source_catalog.py +15 -0
  75. databay/cli/commands/source_check_run.py +17 -0
  76. databay/cli/commands/source_hidden_type.py +14 -0
  77. databay/cli/commands/source_name.py +14 -0
  78. databay/cli/commands/source_pipe.py +14 -0
  79. databay/cli/commands/source_publication.py +16 -0
  80. databay/cli/commands/source_readiness_override.py +15 -0
  81. databay/cli/commands/source_setup_choice.py +16 -0
  82. databay/cli/commands/source_stream.py +15 -0
  83. databay/cli/commands/source_table.py +14 -0
  84. databay/cli/commands/ssbi_job.py +18 -0
  85. databay/cli/commands/transform_type.py +14 -0
  86. databay/cli/commands/usage.py +14 -0
  87. databay/cli/commands/warehouse.py +21 -0
  88. databay/cli/commands/warehouse_access_assignment.py +16 -0
  89. databay/cli/commands/warehouse_access_role.py +22 -0
  90. databay/cli/commands/warehouse_access_settings.py +17 -0
  91. databay/cli/commands/warehouse_login.py +20 -0
  92. databay/cli/commands/warehouse_principal.py +17 -0
  93. databay/cli/commands/warehouse_service_account.py +17 -0
  94. databay/cli/commands/warehouse_service_account_credential.py +18 -0
  95. databay/cli/common.py +93 -0
  96. databay/cli/console.py +121 -0
  97. databay/cli/flags.py +183 -0
  98. databay/cli/handwritten.py +22 -0
  99. databay/cli/main.py +198 -0
  100. databay/cli/metrics.py +171 -0
  101. databay/cli/org.py +193 -0
  102. databay/cli/output.py +134 -0
  103. databay/cli/pipe_wait.py +357 -0
  104. databay/cli/profile.py +230 -0
  105. databay/cli/runner.py +432 -0
  106. databay/cli/skill.py +64 -0
  107. databay/cli/sql.py +84 -0
  108. databay/compat.py +161 -0
  109. databay/credentials.py +217 -0
  110. databay/error_catalog.py +226 -0
  111. databay/error_codes.py +3265 -0
  112. databay/errors.py +222 -0
  113. databay/feature_header.py +40 -0
  114. databay/invoke.py +225 -0
  115. databay/locks.py +41 -0
  116. databay/login_flow.py +245 -0
  117. databay/model_base.py +40 -0
  118. databay/models/__init__.py +0 -0
  119. databay/models/alerting_webhook.py +46 -0
  120. databay/models/alerting_webhook_tag.py +11 -0
  121. databay/models/alerting_webhook_type.py +11 -0
  122. databay/models/alerting_webhook_undelivered.py +23 -0
  123. databay/models/announcement.py +37 -0
  124. databay/models/api_key.py +81 -0
  125. databay/models/api_key_project.py +29 -0
  126. databay/models/backfill_reason_code.py +11 -0
  127. databay/models/backfill_run.py +41 -0
  128. databay/models/canvas.py +68 -0
  129. databay/models/canvas_graph.py +86 -0
  130. databay/models/canvas_table_check.py +181 -0
  131. databay/models/dashboards.py +79 -0
  132. databay/models/dashboards_card.py +37 -0
  133. databay/models/dashboards_dataset.py +27 -0
  134. databay/models/dashboards_folder.py +18 -0
  135. databay/models/dashboards_home.py +28 -0
  136. databay/models/db.py +34 -0
  137. databay/models/db_column.py +11 -0
  138. databay/models/docs_article.py +59 -0
  139. databay/models/dq_result.py +27 -0
  140. databay/models/dq_rule.py +43 -0
  141. databay/models/feature.py +15 -0
  142. databay/models/google_ads_source.py +11 -0
  143. databay/models/google_analytics_source.py +11 -0
  144. databay/models/instagram_source.py +11 -0
  145. databay/models/linkedin_source.py +11 -0
  146. databay/models/me.py +71 -0
  147. databay/models/meta_ads_source.py +11 -0
  148. databay/models/metric_models.py +274 -0
  149. databay/models/metrics.py +158 -0
  150. databay/models/models.py +73 -0
  151. databay/models/nlq.py +92 -0
  152. databay/models/notebook_credential.py +51 -0
  153. databay/models/org.py +44 -0
  154. databay/models/org_member.py +123 -0
  155. databay/models/org_offboarding.py +36 -0
  156. databay/models/pipe.py +340 -0
  157. databay/models/pipe_config_option.py +25 -0
  158. databay/models/pipe_destination.py +128 -0
  159. databay/models/pipe_leftover.py +51 -0
  160. databay/models/pipe_sink_type.py +11 -0
  161. databay/models/pipe_source_type.py +11 -0
  162. databay/models/pipe_status_bucket.py +13 -0
  163. databay/models/pipe_stuck_delete.py +25 -0
  164. databay/models/plan.py +37 -0
  165. databay/models/project.py +154 -0
  166. databay/models/project_budget.py +22 -0
  167. databay/models/project_member.py +67 -0
  168. databay/models/project_privilege.py +21 -0
  169. databay/models/project_share.py +76 -0
  170. databay/models/provision.py +12 -0
  171. databay/models/provisioning.py +20 -0
  172. databay/models/registry.py +18 -0
  173. databay/models/resource_home.py +35 -0
  174. databay/models/schema_change.py +96 -0
  175. databay/models/schema_history.py +32 -0
  176. databay/models/schema_version.py +23 -0
  177. databay/models/shared.py +875 -0
  178. databay/models/shopify_store.py +19 -0
  179. databay/models/sink_data_type.py +11 -0
  180. databay/models/source.py +263 -0
  181. databay/models/source_catalog.py +82 -0
  182. databay/models/source_check_run.py +63 -0
  183. databay/models/source_hidden_type.py +13 -0
  184. databay/models/source_name.py +18 -0
  185. databay/models/source_pipe.py +16 -0
  186. databay/models/source_publication.py +30 -0
  187. databay/models/source_readiness_override.py +13 -0
  188. databay/models/source_setup_choice.py +33 -0
  189. databay/models/source_stream.py +65 -0
  190. databay/models/source_table.py +27 -0
  191. databay/models/sql.py +31 -0
  192. databay/models/ssbi_job.py +97 -0
  193. databay/models/transform_type.py +40 -0
  194. databay/models/usage.py +112 -0
  195. databay/models/warehouse.py +38 -0
  196. databay/models/warehouse_access_assignment.py +70 -0
  197. databay/models/warehouse_access_role.py +148 -0
  198. databay/models/warehouse_access_settings.py +35 -0
  199. databay/models/warehouse_login.py +20 -0
  200. databay/models/warehouse_principal.py +49 -0
  201. databay/models/warehouse_service_account.py +35 -0
  202. databay/models/warehouse_service_account_credential.py +11 -0
  203. databay/oidc.py +205 -0
  204. databay/operation.py +191 -0
  205. databay/operations/__init__.py +0 -0
  206. databay/operations/alerting_webhook.py +317 -0
  207. databay/operations/alerting_webhook_tag.py +35 -0
  208. databay/operations/alerting_webhook_type.py +35 -0
  209. databay/operations/alerting_webhook_undelivered.py +74 -0
  210. databay/operations/announcement.py +135 -0
  211. databay/operations/api_key.py +198 -0
  212. databay/operations/api_key_project.py +130 -0
  213. databay/operations/backfill_reason_code.py +35 -0
  214. databay/operations/backfill_run.py +245 -0
  215. databay/operations/canvas.py +136 -0
  216. databay/operations/canvas_graph.py +102 -0
  217. databay/operations/canvas_table_check.py +101 -0
  218. databay/operations/dashboards.py +441 -0
  219. databay/operations/dashboards_card.py +406 -0
  220. databay/operations/dashboards_dataset.py +50 -0
  221. databay/operations/dashboards_folder.py +146 -0
  222. databay/operations/dashboards_home.py +176 -0
  223. databay/operations/dashboards_placement.py +152 -0
  224. databay/operations/db.py +337 -0
  225. databay/operations/db_column.py +57 -0
  226. databay/operations/docs_article.py +101 -0
  227. databay/operations/dq_result.py +43 -0
  228. databay/operations/dq_rule.py +155 -0
  229. databay/operations/feature.py +35 -0
  230. databay/operations/google_ads_source.py +40 -0
  231. databay/operations/google_analytics_source.py +40 -0
  232. databay/operations/instagram_account.py +47 -0
  233. databay/operations/instagram_source.py +40 -0
  234. databay/operations/linkedin_account.py +47 -0
  235. databay/operations/linkedin_source.py +40 -0
  236. databay/operations/me.py +57 -0
  237. databay/operations/meta_ads_source.py +40 -0
  238. databay/operations/metric_models.py +612 -0
  239. databay/operations/metrics.py +218 -0
  240. databay/operations/models.py +123 -0
  241. databay/operations/nlq.py +100 -0
  242. databay/operations/notebook_credential.py +95 -0
  243. databay/operations/org.py +79 -0
  244. databay/operations/org_member.py +192 -0
  245. databay/operations/org_offboarding.py +43 -0
  246. databay/operations/pipe.py +882 -0
  247. databay/operations/pipe_backfill_run.py +72 -0
  248. databay/operations/pipe_config_option.py +50 -0
  249. databay/operations/pipe_destination.py +178 -0
  250. databay/operations/pipe_leftover.py +110 -0
  251. databay/operations/pipe_sink_type.py +35 -0
  252. databay/operations/pipe_source_type.py +35 -0
  253. databay/operations/pipe_status_bucket.py +35 -0
  254. databay/operations/pipe_stuck_delete.py +52 -0
  255. databay/operations/plan.py +35 -0
  256. databay/operations/project.py +346 -0
  257. databay/operations/project_budget.py +135 -0
  258. databay/operations/project_member.py +226 -0
  259. databay/operations/project_privilege.py +143 -0
  260. databay/operations/project_share.py +160 -0
  261. databay/operations/provision.py +35 -0
  262. databay/operations/provisioning.py +35 -0
  263. databay/operations/registry.py +35 -0
  264. databay/operations/resource_home.py +80 -0
  265. databay/operations/schema_change.py +222 -0
  266. databay/operations/schema_history.py +136 -0
  267. databay/operations/schema_version.py +115 -0
  268. databay/operations/shopify_store.py +40 -0
  269. databay/operations/sink_data_type.py +43 -0
  270. databay/operations/source.py +1207 -0
  271. databay/operations/source_catalog.py +135 -0
  272. databay/operations/source_check_run.py +242 -0
  273. databay/operations/source_hidden_type.py +59 -0
  274. databay/operations/source_name.py +69 -0
  275. databay/operations/source_pipe.py +57 -0
  276. databay/operations/source_publication.py +139 -0
  277. databay/operations/source_readiness_override.py +96 -0
  278. databay/operations/source_setup_choice.py +161 -0
  279. databay/operations/source_stream.py +99 -0
  280. databay/operations/source_table.py +59 -0
  281. databay/operations/sql.py +54 -0
  282. databay/operations/ssbi_job.py +242 -0
  283. databay/operations/transform_type.py +35 -0
  284. databay/operations/usage.py +35 -0
  285. databay/operations/warehouse.py +343 -0
  286. databay/operations/warehouse_access_assignment.py +179 -0
  287. databay/operations/warehouse_access_role.py +473 -0
  288. databay/operations/warehouse_access_settings.py +172 -0
  289. databay/operations/warehouse_login.py +308 -0
  290. databay/operations/warehouse_principal.py +174 -0
  291. databay/operations/warehouse_service_account.py +206 -0
  292. databay/operations/warehouse_service_account_credential.py +285 -0
  293. databay/paths.py +32 -0
  294. databay/platform_check.py +20 -0
  295. databay/profiles.py +181 -0
  296. databay/redact.py +22 -0
  297. databay/reference.py +218 -0
  298. databay/resource.py +167 -0
  299. databay/resources/__init__.py +0 -0
  300. databay/resources/alerting_webhook.py +84 -0
  301. databay/resources/alerting_webhook_tag.py +21 -0
  302. databay/resources/alerting_webhook_type.py +21 -0
  303. databay/resources/alerting_webhook_undelivered.py +35 -0
  304. databay/resources/announcement.py +45 -0
  305. databay/resources/api_key.py +56 -0
  306. databay/resources/api_key_project.py +47 -0
  307. databay/resources/backfill_reason_code.py +21 -0
  308. databay/resources/backfill_run.py +78 -0
  309. databay/resources/canvas.py +43 -0
  310. databay/resources/canvas_graph.py +38 -0
  311. databay/resources/canvas_table_check.py +38 -0
  312. databay/resources/dashboards.py +105 -0
  313. databay/resources/dashboards_card.py +97 -0
  314. databay/resources/dashboards_dataset.py +27 -0
  315. databay/resources/dashboards_folder.py +47 -0
  316. databay/resources/dashboards_home.py +59 -0
  317. databay/resources/dashboards_placement.py +47 -0
  318. databay/resources/db.py +87 -0
  319. databay/resources/db_column.py +28 -0
  320. databay/resources/docs_article.py +37 -0
  321. databay/resources/dq_result.py +25 -0
  322. databay/resources/dq_rule.py +48 -0
  323. databay/resources/feature.py +21 -0
  324. databay/resources/google_ads_source.py +25 -0
  325. databay/resources/google_analytics_source.py +25 -0
  326. databay/resources/instagram_account.py +27 -0
  327. databay/resources/instagram_source.py +25 -0
  328. databay/resources/linkedin_account.py +27 -0
  329. databay/resources/linkedin_source.py +25 -0
  330. databay/resources/me.py +27 -0
  331. databay/resources/meta_ads_source.py +25 -0
  332. databay/resources/metric_models.py +154 -0
  333. databay/resources/metrics.py +57 -0
  334. databay/resources/models.py +42 -0
  335. databay/resources/nlq.py +34 -0
  336. databay/resources/notebook_credential.py +41 -0
  337. databay/resources/org.py +29 -0
  338. databay/resources/org_member.py +53 -0
  339. databay/resources/org_offboarding.py +25 -0
  340. databay/resources/pipe.py +208 -0
  341. databay/resources/pipe_backfill_run.py +28 -0
  342. databay/resources/pipe_config_option.py +26 -0
  343. databay/resources/pipe_destination.py +49 -0
  344. databay/resources/pipe_leftover.py +42 -0
  345. databay/resources/pipe_sink_type.py +21 -0
  346. databay/resources/pipe_source_type.py +21 -0
  347. databay/resources/pipe_status_bucket.py +21 -0
  348. databay/resources/pipe_stuck_delete.py +27 -0
  349. databay/resources/plan.py +21 -0
  350. databay/resources/project.py +92 -0
  351. databay/resources/project_budget.py +38 -0
  352. databay/resources/project_member.py +68 -0
  353. databay/resources/project_privilege.py +48 -0
  354. databay/resources/project_share.py +47 -0
  355. databay/resources/provision.py +21 -0
  356. databay/resources/provisioning.py +21 -0
  357. databay/resources/registry.py +21 -0
  358. databay/resources/resource_home.py +29 -0
  359. databay/resources/schema_change.py +62 -0
  360. databay/resources/schema_history.py +52 -0
  361. databay/resources/schema_version.py +43 -0
  362. databay/resources/shopify_store.py +25 -0
  363. databay/resources/sink_data_type.py +25 -0
  364. databay/resources/source.py +132 -0
  365. databay/resources/source_catalog.py +34 -0
  366. databay/resources/source_check_run.py +67 -0
  367. databay/resources/source_hidden_type.py +25 -0
  368. databay/resources/source_name.py +27 -0
  369. databay/resources/source_pipe.py +28 -0
  370. databay/resources/source_publication.py +47 -0
  371. databay/resources/source_readiness_override.py +37 -0
  372. databay/resources/source_setup_choice.py +50 -0
  373. databay/resources/source_stream.py +38 -0
  374. databay/resources/source_table.py +27 -0
  375. databay/resources/sql.py +27 -0
  376. databay/resources/ssbi_job.py +69 -0
  377. databay/resources/transform_type.py +21 -0
  378. databay/resources/usage.py +21 -0
  379. databay/resources/warehouse.py +136 -0
  380. databay/resources/warehouse_access_assignment.py +51 -0
  381. databay/resources/warehouse_access_role.py +118 -0
  382. databay/resources/warehouse_access_settings.py +56 -0
  383. databay/resources/warehouse_login.py +81 -0
  384. databay/resources/warehouse_principal.py +51 -0
  385. databay/resources/warehouse_service_account.py +61 -0
  386. databay/resources/warehouse_service_account_credential.py +78 -0
  387. databay/sdk.py +77 -0
  388. databay/settings.py +90 -0
  389. databay/skill/SKILL.md +357 -0
  390. databay/skill/references/commands.md +7025 -0
  391. databay/skill/references/errors.md +727 -0
  392. databay/spec.py +362 -0
  393. databay/stored_token.py +134 -0
  394. databay/tokens.py +108 -0
  395. databay/transport.py +357 -0
  396. databay/update_check.py +151 -0
  397. databay/version.py +6 -0
  398. databay/waits.py +330 -0
  399. databay_cli-0.1.0.dist-info/METADATA +42 -0
  400. databay_cli-0.1.0.dist-info/RECORD +403 -0
  401. databay_cli-0.1.0.dist-info/WHEEL +4 -0
  402. databay_cli-0.1.0.dist-info/entry_points.txt +2 -0
  403. databay_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
databay/cli/metrics.py ADDED
@@ -0,0 +1,171 @@
1
+ """``databay metrics describe`` and ``databay metrics query``: the curated commands of the metrics API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any
7
+
8
+ import click
9
+
10
+ from databay.cli.common import call_once, common_options, result_envelope
11
+ from databay.models.metrics import Grain
12
+
13
+ #: Who ran the query, for the server's per-caller count: ``--caller``, ``cli`` unless an agent passes ``agent``.
14
+ CALLER_HEADER = "X-Databay-Caller"
15
+ CALLERS = ("cli", "agent")
16
+ #: ``--where`` operators; ``!=``, ``>=`` and ``<=`` come before ``=``, ``>`` and ``<`` so the regex reads them whole.
17
+ WHERE_OPS = {"!=": "notEquals", ">=": "gte", "<=": "lte", "=": "equals", ">": "gt", "<": "lt"}
18
+ _WHERE = re.compile(
19
+ r"\s*([a-z][a-z0-9_]*)\s*(" + "|".join(map(re.escape, WHERE_OPS)) + r")\s*([^=<>\s].*?)\s*", re.DOTALL
20
+ )
21
+ _ORDER = re.compile(r"([a-z][a-z0-9_]*)(?::(asc|desc))?")
22
+ _PROJECT_HELP = "the project to act in (default: the active project)"
23
+
24
+
25
+ def parse_order(text: str) -> dict[str, str]:
26
+ """One ``--order``: ``member`` or ``member:asc|desc``."""
27
+ from databay.errors import DatabayUsageError # noqa: PLC0415
28
+
29
+ match = _ORDER.fullmatch(text.strip())
30
+ if match is None:
31
+ raise DatabayUsageError(f"--order {text!r}: write <member> or <member>:desc (net_sales:desc)")
32
+ return {"member": match[1], "direction": match[2] or "asc"}
33
+
34
+
35
+ def parse_where(text: str) -> dict[str, Any]:
36
+ """
37
+ One ``--where`` as a filter: ``member<op>value`` with ``=``, ``!=``, ``>``, ``>=``, ``<`` or ``<=``.
38
+
39
+ The value is the rest, without the spaces around it; commas and inner spaces stay. A value starting
40
+ with ``=``, ``<`` or ``>`` (``a==b``, ``a=>b``) is refused as a mistyped operator.
41
+ """
42
+ from databay.errors import DatabayUsageError # noqa: PLC0415
43
+
44
+ match = _WHERE.fullmatch(text)
45
+ if match is None:
46
+ raise DatabayUsageError(
47
+ f"--where {text!r}: write <dimension><op><value>, op one of {', '.join(WHERE_OPS)} (channel=paid)"
48
+ )
49
+ member, symbol, value = match.groups()
50
+ return {"member": member, "op": WHERE_OPS[symbol], "values": [value]}
51
+
52
+
53
+ DESCRIBE_HELP = """Describe one metric: its dimensions, time grains, filter operators, warehouses and state.
54
+
55
+ \b
56
+ Example:
57
+ databay metrics describe net_sales"""
58
+
59
+
60
+ @click.command("describe", help=DESCRIBE_HELP, short_help="Describe one metric you can query")
61
+ @click.argument("metric", metavar="METRIC")
62
+ @click.option("--project", default=None, help=_PROJECT_HELP)
63
+ @common_options
64
+ @click.pass_context
65
+ def describe(ctx: click.Context, metric: str, project: str | None, **values: Any) -> None: # noqa: ANN401
66
+ from databay.cli.output import emit # noqa: PLC0415
67
+ from databay.invoke import CallValues # noqa: PLC0415
68
+ from databay.operations.metrics import METRICS_DESCRIBE # noqa: PLC0415
69
+
70
+ call = CallValues(path={"metric": metric}, project_flag=project)
71
+ console, result = call_once(ctx, values, METRICS_DESCRIBE, call)
72
+ emit(console, result_envelope(result), ())
73
+
74
+
75
+ QUERY_HELP = """Query one metric, grouped by dimensions and a time grain.
76
+
77
+ \b
78
+ A table or CSV prints the rows under their columns; JSON prints the whole
79
+ answer (columns, rows, row_count, truncated, answer_path, model_version,
80
+ warehouse, explain). --explain adds the metric's definition, the SQL it
81
+ compiled to and its lineage; --dry-run answers that without running it.
82
+ \b
83
+ --where member=value filters on a dimension; it also takes !=, >, >=, <
84
+ and <=, and repeats (each must hold). --order member[:desc] sorts, and
85
+ repeats; with --limit it answers a top N.
86
+ \b
87
+ --caller agent marks a query an AI agent ran, so the server counts it
88
+ apart from a person's; without it the query counts as cli.
89
+ \b
90
+ Examples:
91
+ databay metrics query net_sales --grain day --last 30d
92
+ databay metrics query channel_sessions --by channel --grain week --last 90d --where channel=Direct
93
+ databay metrics query net_sales --grain day --last 30d --explain"""
94
+
95
+
96
+ @click.command("query", help=QUERY_HELP, short_help="Query one metric by dimensions and time grain")
97
+ @click.argument("metric", metavar="METRIC")
98
+ @click.option("--by", "by", multiple=True, metavar="DIMENSION", help="group by this dimension; repeat for more")
99
+ @click.option(
100
+ "--grain",
101
+ type=click.Choice([g.value for g in Grain]),
102
+ default=None,
103
+ help="group by the metric's time at this grain",
104
+ )
105
+ @click.option("--last", default=None, metavar="ND", help="the N UTC days that end today, such as 30d")
106
+ @click.option("--from", "start", default=None, metavar="YYYY-MM-DD", help="first day (with --to)")
107
+ @click.option("--to", "end", default=None, metavar="YYYY-MM-DD", help="last day, inclusive (with --from)")
108
+ @click.option("--where", "where", multiple=True, metavar="FILTER", help="member=value (or != > >= < <=); repeat")
109
+ @click.option("--order", "order", multiple=True, metavar="MEMBER[:desc]", help="sort by this member; repeat")
110
+ @click.option("--limit", type=click.IntRange(min=1), default=None, help="most rows to answer")
111
+ @click.option("--warehouse", default=None, help="the warehouse, when the metric is in several")
112
+ @click.option("--model-version", "model_version", type=click.IntRange(min=1), default=None, help="pin a model version")
113
+ @click.option("--explain", is_flag=True, default=False, help="add the definition, compiled SQL and lineage")
114
+ @click.option("--dry-run", "dry_run", is_flag=True, default=False, help="answer the explain without running it")
115
+ @click.option("--caller", type=click.Choice(CALLERS), default="cli", help="who runs the query: cli or agent")
116
+ @click.option("--project", default=None, help=_PROJECT_HELP)
117
+ @common_options
118
+ @click.pass_context
119
+ def query(
120
+ ctx: click.Context,
121
+ metric: str,
122
+ by: tuple[str, ...],
123
+ grain: str | None,
124
+ last: str | None,
125
+ start: str | None,
126
+ end: str | None,
127
+ where: tuple[str, ...],
128
+ order: tuple[str, ...],
129
+ limit: int | None,
130
+ warehouse: str | None,
131
+ model_version: int | None,
132
+ explain: bool,
133
+ dry_run: bool,
134
+ caller: str,
135
+ project: str | None,
136
+ **values: Any, # noqa: ANN401
137
+ ) -> None:
138
+ from databay.cli.output import emit_grid # noqa: PLC0415
139
+ from databay.invoke import CallValues # noqa: PLC0415
140
+ from databay.operations.metrics import METRICS_QUERY # noqa: PLC0415
141
+
142
+ filters = [parse_where(text) for text in where]
143
+ ordering = [parse_order(text) for text in order]
144
+ time = {k: v for k, v in {"grain": grain, "last": last, "from": start, "to": end}.items() if v is not None}
145
+ body: dict[str, Any] = {"metric": metric, "explain": explain, "dry_run": dry_run}
146
+ optional = {
147
+ "dimensions": list(by) or None,
148
+ "time": time or None,
149
+ "filters": filters or None,
150
+ "order": ordering or None,
151
+ "limit": limit,
152
+ "warehouse": warehouse,
153
+ "model_version": model_version,
154
+ }
155
+ body.update({k: v for k, v in optional.items() if v is not None})
156
+ call = CallValues(body=body, has_body=True, headers={CALLER_HEADER: caller}, project_flag=project)
157
+ console, result = call_once(ctx, values, METRICS_QUERY, call)
158
+ answer = result.parsed
159
+ if answer.truncated:
160
+ console.notice(
161
+ "truncated",
162
+ f"the server returned the first {answer.row_count} rows",
163
+ only_on_terminal=console.output == "json",
164
+ )
165
+ emit_grid(console, result_envelope(result), [c.name for c in answer.columns], [list(row) for row in answer.rows])
166
+ if answer.explain is None or console.output == "json":
167
+ return
168
+ if console.output == "table":
169
+ console.stdout.write(f"sql:\n{answer.explain.sql.text}\n")
170
+ else:
171
+ console.notice("explain", "CSV holds only the rows; pass --output json for the explain", only_on_terminal=False)
databay/cli/org.py ADDED
@@ -0,0 +1,193 @@
1
+ """
2
+ ``databay org switch``, and the organization ``databay login`` makes active.
3
+
4
+ The server stores the active organization and every token names that one. Switching is a write
5
+ (``PUT /api/auth/me/active-org``) followed by a token renewal; the new token is checked to name the
6
+ organization before the command reports it. An organization is named by its key or its display name.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import sys
12
+ from typing import TYPE_CHECKING, Any
13
+
14
+ import click
15
+
16
+ from databay.cli.common import common_options, console_for
17
+ from databay.cli.handwritten import HANDWRITTEN
18
+
19
+ if TYPE_CHECKING:
20
+ from databay.cli.console import Console
21
+ from databay.invoke import Session
22
+ from databay.models.org import MyOrganizationOut, MyOrganizationsResponse
23
+ from databay.profiles import Profile
24
+
25
+ SWITCH_HELP = """Make ORGANIZATION the active organization and refresh the token.
26
+
27
+ \b
28
+ ORGANIZATION is a key (org-...) or a display name; `databay org list` shows
29
+ both. Every later command acts in that organization. The profile's active
30
+ project belongs to the previous organization, so it is cleared.
31
+
32
+ \b
33
+ Example:
34
+ databay org switch <organization>"""
35
+
36
+
37
+ def list_orgs(session: Session) -> MyOrganizationsResponse:
38
+ from databay.invoke import CallValues # noqa: PLC0415
39
+ from databay.operations.org import ORG_LIST # noqa: PLC0415
40
+
41
+ return session.call(ORG_LIST, CallValues()).parsed
42
+
43
+
44
+ def resolve_org(listing: MyOrganizationsResponse, wanted: str, server: str) -> MyOrganizationOut:
45
+ """The organization ``wanted`` names: a key, else exactly one display name, compared without case."""
46
+ from databay.errors import DatabayUsageError, api_error # noqa: PLC0415
47
+
48
+ keys = [org.organization for org in listing.organizations]
49
+ by_key = [org for org in listing.organizations if org.organization == wanted]
50
+ if by_key:
51
+ return _usable(by_key[0])
52
+ by_name = [org for org in listing.organizations if org.display_name.casefold() == wanted.casefold()]
53
+ if len(by_name) > 1:
54
+ raise DatabayUsageError(
55
+ f"{len(by_name)} of your organizations are named {wanted!r}; pass the key instead",
56
+ details={"organizations": [org.organization for org in by_name]},
57
+ )
58
+ if not by_name:
59
+ raise api_error(
60
+ "org_not_member",
61
+ f"you belong to no organization {wanted!r} on {server}",
62
+ status=403,
63
+ details={"organization": wanted, "organizations": keys},
64
+ )
65
+ return _usable(by_name[0])
66
+
67
+
68
+ def _usable(org: MyOrganizationOut) -> MyOrganizationOut:
69
+ """``org``, unless it is disabled: no token can name a disabled organization."""
70
+ from databay.errors import api_error # noqa: PLC0415
71
+
72
+ if not org.enabled:
73
+ raise api_error(
74
+ "org_disabled",
75
+ f"{org.display_name} ({org.organization}) is disabled",
76
+ status=403,
77
+ details={"organization": org.organization},
78
+ )
79
+ return org
80
+
81
+
82
+ def switch_to(
83
+ session: Session, profile: Profile, org: MyOrganizationOut, console: Console, state: dict[str, Any]
84
+ ) -> dict[str, Any]:
85
+ """
86
+ Store ``org`` as active, renew the profile's token and check it names ``org``.
87
+
88
+ A profile whose token comes from ``$DATABAY_TOKEN`` cannot renew it: the switch is stored and
89
+ ``token_refreshed`` is false. The active project is cleared when the token named another organization.
90
+ """
91
+ from databay.errors import DatabayError # noqa: PLC0415
92
+ from databay.invoke import CallValues # noqa: PLC0415
93
+ from databay.operations.org import ORG_SWITCH # noqa: PLC0415
94
+ from databay.profiles import edit_config # noqa: PLC0415
95
+ from databay.settings import ENV_TOKEN # noqa: PLC0415
96
+ from databay.stored_token import StoredToken # noqa: PLC0415
97
+
98
+ before = _token_org(session)
99
+ state["sent"] = True
100
+ session.call(ORG_SWITCH, CallValues(body={"organization": org.organization}, has_body=True))
101
+ tokens = session.transport.tokens
102
+ refreshed = isinstance(tokens, StoredToken)
103
+ if refreshed:
104
+ tokens.renewed()
105
+ now = _token_org(session)
106
+ if now != org.organization:
107
+ raise DatabayError(
108
+ f"switched to {org.organization}, but the renewed token names {now or 'no organization'}",
109
+ code="org_not_in_token",
110
+ details={"organization": org.organization, "token_organization": now},
111
+ next_command="databay status",
112
+ )
113
+ else:
114
+ console.inform(
115
+ "token_not_refreshed",
116
+ {"organization": org.organization, "auth": profile.auth},
117
+ f"{org.organization} is now your active organization, but ${ENV_TOKEN} still holds a token "
118
+ "for the previous one: get a new token before the next command.",
119
+ )
120
+ cleared = None
121
+ if before != org.organization and profile.active_project is not None:
122
+ cleared = profile.active_project
123
+
124
+ def change(data: dict[str, Any]) -> None:
125
+ data["profiles"][profile.name].pop("active_project", None)
126
+
127
+ edit_config(change)
128
+ return {
129
+ "profile": profile.name,
130
+ "organization": org.organization,
131
+ "display_name": org.display_name,
132
+ "role": None if org.role is None else str(org.role),
133
+ "token_refreshed": refreshed,
134
+ "cleared_active_project": cleared,
135
+ }
136
+
137
+
138
+ def _token_org(session: Session) -> str | None:
139
+ """The organization the token now names, as the server reads it."""
140
+ from databay.invoke import CallValues # noqa: PLC0415
141
+ from databay.operations.me import ME_GET # noqa: PLC0415
142
+
143
+ me = session.call(ME_GET, CallValues()).parsed
144
+ return me.organization[0] if me.organization else None
145
+
146
+
147
+ def choose_interactively(listing: MyOrganizationsResponse, console: Console) -> MyOrganizationOut:
148
+ """Ask on the terminal which organization to make active."""
149
+ usable = [org for org in listing.organizations if org.enabled]
150
+ lines = ["You belong to several organizations. Which one should be active?"]
151
+ for number, org in enumerate(usable, start=1):
152
+ role = f", {org.role}" if org.role is not None else ""
153
+ lines.append(f" {number}) {org.display_name} ({org.organization}{role})")
154
+ console.inform("org_choice", {}, "\n".join(lines))
155
+ picked = click.prompt("Organization", type=click.IntRange(1, len(usable)), err=True)
156
+ return usable[picked - 1]
157
+
158
+
159
+ def report_no_usable_org(listing: MyOrganizationsResponse, server: str, console: Console) -> None:
160
+ """Say that no token can name an organization: every one is disabled, or there is none."""
161
+ console.inform(
162
+ "no_usable_org",
163
+ {"organizations": [org.organization for org in listing.organizations]},
164
+ f"You are signed in, but none of your organizations on {server} can be used "
165
+ f"({'every one is disabled' if listing.organizations else 'you belong to none'}). "
166
+ "Create one in the web app, or ask an organization admin to add you.",
167
+ )
168
+
169
+
170
+ def interactive(console: Console) -> bool:
171
+ return sys.stdin.isatty() and console.stderr_tty
172
+
173
+
174
+ @click.command("switch", help=SWITCH_HELP, short_help=HANDWRITTEN["org switch"][2])
175
+ @click.argument("organization")
176
+ @common_options
177
+ @click.pass_context
178
+ def org_switch(ctx: click.Context, organization: str, **values: Any) -> None: # noqa: ANN401
179
+ from databay.cli.output import emit # noqa: PLC0415
180
+ from databay.cli.runner import open_session # noqa: PLC0415
181
+ from databay.errors import DatabayUsageError # noqa: PLC0415
182
+
183
+ console, state = console_for(ctx, values)
184
+ session, chosen = open_session(values, console, state)
185
+ state["context"] = {"server": chosen.server, "profile": chosen.name}
186
+ if chosen.auth == "api_key":
187
+ raise DatabayUsageError(f"profile {chosen.name} signs in as an org API key, which belongs to one organization")
188
+ try:
189
+ org = resolve_org(list_orgs(session), organization, chosen.server)
190
+ result = switch_to(session, chosen, org, console, state)
191
+ finally:
192
+ session.transport.close()
193
+ emit(console, {"result": result, "next": "databay status"}, ())
databay/cli/output.py ADDED
@@ -0,0 +1,134 @@
1
+ """
2
+ Printing a command's result.
3
+
4
+ JSON is one envelope: ``{"result": <the server's answer>}``, plus ``project`` when the command
5
+ acted in one, ``wait`` after ``--wait``, ``idempotency_key`` on a create and ``next`` when there is
6
+ an obvious next command. A table or CSV prints the answer alone: a list of objects as rows (the
7
+ operation's declared columns, else every key), an object as one row per field.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import csv
13
+ import json
14
+ from typing import TYPE_CHECKING, Any
15
+
16
+ from databay.errors import DatabayResponseInvalid
17
+ from databay.invoke import pointer
18
+
19
+ if TYPE_CHECKING:
20
+ from databay.cli.console import Console
21
+
22
+ MAX_CELL = 80
23
+
24
+
25
+ def _text(value: Any) -> str: # noqa: ANN401
26
+ if value is None:
27
+ return ""
28
+ return value if isinstance(value, str) else json.dumps(value, default=str)
29
+
30
+
31
+ def _cell(value: Any) -> str: # noqa: ANN401
32
+ """A table cell, cut to ``MAX_CELL`` characters; CSV keeps the whole value."""
33
+ text = _text(value)
34
+ return text if len(text) <= MAX_CELL else text[: MAX_CELL - 1] + "…"
35
+
36
+
37
+ def _rows(result: Any, columns: tuple[str, ...], cell: Any = _cell) -> tuple[list[str], list[list[str]]]: # noqa: ANN401
38
+ if isinstance(result, list) and all(isinstance(item, dict) for item in result):
39
+ header = list(columns)
40
+ if not header:
41
+ for item in result:
42
+ header += [key for key in item if key not in header]
43
+ return header, [[cell(item.get(key)) for key in header] for item in result]
44
+ if isinstance(result, dict):
45
+ return ["field", "value"], [[key, cell(value)] for key, value in result.items()]
46
+ if isinstance(result, list):
47
+ return ["value"], [[cell(item)] for item in result]
48
+ return ["value"], [[cell(result)]]
49
+
50
+
51
+ def _csv_rows(result: Any, columns: tuple[str, ...]) -> tuple[list[str], list[list[str]]]: # noqa: ANN401
52
+ if isinstance(result, dict):
53
+ header = list(columns) or list(result)
54
+ return header, [[_text(result.get(key)) for key in header]]
55
+ return _rows(result, columns, _text)
56
+
57
+
58
+ def emit(console: Console, envelope: dict[str, Any], columns: tuple[str, ...], rows: str | None = None) -> None:
59
+ """
60
+ Print ``envelope`` in the console's format.
61
+
62
+ ``rows`` is the JSON pointer of the list a table or CSV shows when the answer is an object
63
+ holding it (``/keys``); a table or CSV reports ``idempotency_key`` and ``next`` on stderr.
64
+ """
65
+ _write(console, envelope, columns, rows)
66
+ for name in ("idempotency_key", "next"):
67
+ if envelope.get(name):
68
+ console.notice(name, envelope[name], only_on_terminal=console.output == "json")
69
+
70
+
71
+ def emit_grid(console: Console, envelope: dict[str, Any], header: list[str], rows: list[list[Any]]) -> None:
72
+ """
73
+ Print an answer that carries its own grid of ``header`` and ``rows`` (``sql``).
74
+
75
+ JSON prints ``envelope``; a table or CSV prints the grid as it is, so repeated column names stay apart.
76
+ """
77
+ out = console.stdout
78
+ if console.output == "json":
79
+ out.write(json.dumps(envelope, indent=2 if out.isatty() else None, default=str) + "\n")
80
+ return
81
+ if console.output == "csv":
82
+ writer = csv.writer(out)
83
+ writer.writerow(header)
84
+ writer.writerows([[_text(value) for value in row] for row in rows])
85
+ return
86
+ if envelope.get("project"):
87
+ out.write(f"project: {envelope['project']}\n")
88
+ from rich.console import Console as RichConsole # noqa: PLC0415 — tables only
89
+ from rich.table import Table # noqa: PLC0415
90
+
91
+ table = Table(*header, show_lines=False)
92
+ for row in rows:
93
+ table.add_row(*[_cell(value) for value in row])
94
+ RichConsole(file=out, no_color=not console.color, soft_wrap=False, markup=False, highlight=False).print(table)
95
+
96
+
97
+ def _write(console: Console, envelope: dict[str, Any], columns: tuple[str, ...], rows: str | None) -> None:
98
+ out = console.stdout
99
+ result = envelope["result"]
100
+ if console.output == "json":
101
+ indent = 2 if out.isatty() else None
102
+ out.write(json.dumps(envelope, indent=indent, default=str) + "\n")
103
+ return
104
+ if isinstance(result, str):
105
+ out.write(result if result.endswith("\n") else result + "\n")
106
+ return
107
+ if rows is not None and isinstance(result, dict):
108
+ try:
109
+ result = pointer(result, rows)
110
+ except (KeyError, IndexError, ValueError):
111
+ raise DatabayResponseInvalid(f"the answer has no {rows}, the list its columns describe") from None
112
+ if console.output == "csv":
113
+ header, cells = _csv_rows(result, columns)
114
+ writer = csv.writer(out)
115
+ writer.writerow(header)
116
+ writer.writerows(cells)
117
+ return
118
+ if envelope.get("project"):
119
+ out.write(f"project: {envelope['project']}\n")
120
+ if "wait" in envelope:
121
+ wait = envelope["wait"]
122
+ reason = f"{wait['reason']}, " if wait.get("reason") else ""
123
+ out.write(f"wait: {wait['outcome']} ({reason}{wait['state']}, {wait['elapsed_s']}s)\n")
124
+ if result is None:
125
+ out.write("done\n")
126
+ return
127
+ from rich.console import Console as RichConsole # noqa: PLC0415 — tables only
128
+ from rich.table import Table # noqa: PLC0415
129
+
130
+ header, cells = _rows(result, columns)
131
+ table = Table(*header, show_lines=False)
132
+ for row in cells:
133
+ table.add_row(*row)
134
+ RichConsole(file=out, no_color=not console.color, soft_wrap=False, markup=False, highlight=False).print(table)