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/common.py ADDED
@@ -0,0 +1,93 @@
1
+ """What every hand-written command shares with the generated ones: the common flags and the console."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from contextlib import contextmanager
7
+ from dataclasses import replace
8
+ from typing import TYPE_CHECKING, Any
9
+
10
+ from databay.cli.build import click_parameter
11
+ from databay.cli.flags import common_flags
12
+
13
+ if TYPE_CHECKING:
14
+ from collections.abc import Callable, Iterator
15
+
16
+ import click
17
+ import httpx
18
+
19
+ from databay.cli.console import Console
20
+ from databay.invoke import CallValues, Result
21
+ from databay.operation import Operation
22
+
23
+
24
+ def common_options(command: Callable[..., Any], *, profile_help: str | None = None) -> Callable[..., Any]:
25
+ """
26
+ Add ``--output``, ``--profile``, ``--verbose`` and ``--no-retry``, built as the generated commands build them.
27
+
28
+ ``profile_help`` replaces the help of ``--profile`` for a command that writes the profile (``login``).
29
+ """
30
+ flags = [replace(f, help=profile_help) if profile_help and f.role == "profile" else f for f in common_flags()]
31
+ params = getattr(command, "__click_params__", [])
32
+ params.extend(click_parameter(flag) for flag in reversed(flags))
33
+ command.__click_params__ = params # type: ignore[attr-defined]
34
+ return command
35
+
36
+
37
+ def login_options(command: Callable[..., Any]) -> Callable[..., Any]:
38
+ from databay.settings import DEFAULT_PROFILE # noqa: PLC0415
39
+
40
+ return common_options(command, profile_help=f"the profile to create or update (default: {DEFAULT_PROFILE})")
41
+
42
+
43
+ def console_for(ctx: click.Context, values: dict[str, Any]) -> tuple[Console, dict[str, Any]]:
44
+ """The command's console, registered in the CLI state so a failure is printed through it."""
45
+ from databay.cli.console import Console, output_format # noqa: PLC0415
46
+
47
+ state: dict[str, Any] = ctx.ensure_object(dict)
48
+ console = Console(output=output_format(values.get("output"), sys.stdout), verbose=bool(values.get("verbose")))
49
+ state["console"] = console
50
+ return console, state
51
+
52
+
53
+ @contextmanager
54
+ def http_client(state: dict[str, Any]) -> Iterator[httpx.Client]:
55
+ """The httpx client requests go through: the one ``main`` was handed (tests), else a new one closed after."""
56
+ if state.get("http") is not None:
57
+ yield state["http"]
58
+ return
59
+ from databay.settings import SETTINGS # noqa: PLC0415
60
+ from databay.transport import new_http_client # noqa: PLC0415
61
+
62
+ with new_http_client(SETTINGS) as client:
63
+ yield client
64
+
65
+
66
+ def call_once(ctx: click.Context, values: dict[str, Any], op: Operation, call: CallValues) -> tuple[Console, Result]:
67
+ """Send ``op`` once as a curated read (``sql``, ``metrics query``), recording the context a failure prints."""
68
+ from databay.cli.runner import open_session # noqa: PLC0415
69
+ from databay.invoke import resolve_project # noqa: PLC0415
70
+
71
+ console, state = console_for(ctx, values)
72
+ session, chosen = open_session(values, console, state)
73
+ try:
74
+ state["context"] = {
75
+ "server": chosen.server,
76
+ "profile": chosen.name,
77
+ "project": resolve_project(op, call, session.active_project),
78
+ "request_id": session.transport.request_id,
79
+ }
80
+ state["reading"] = True
81
+ result = session.call(op, call)
82
+ finally:
83
+ session.transport.close()
84
+ state["context"]["request_id"] = result.request_id
85
+ return console, result
86
+
87
+
88
+ def result_envelope(result: Result) -> dict[str, Any]:
89
+ """``{"result": ..., "project": ...}``: the JSON a curated command prints."""
90
+ envelope: dict[str, Any] = {"result": result.raw}
91
+ if result.project is not None:
92
+ envelope["project"] = result.project
93
+ return envelope
databay/cli/console.py ADDED
@@ -0,0 +1,121 @@
1
+ """
2
+ Where the CLI writes: the result on stdout, everything else on stderr.
3
+
4
+ stdout carries only the result, so piping it never mixes in progress or hints. On stderr, a
5
+ terminal gets short human lines and captured output gets one JSON object per line.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ import sys
13
+ from dataclasses import dataclass, field
14
+ from typing import Any, TextIO
15
+
16
+ from databay.errors import DatabayUsageError
17
+ from databay.settings import ENV_NO_COLOR, ENV_OUTPUT, OUTPUT_FORMATS, OutputFormat
18
+
19
+
20
+ def output_format(flag: str | None, stdout: TextIO) -> OutputFormat:
21
+ """``--output``, else ``$DATABAY_OUTPUT``, else a table on a terminal and JSON when captured."""
22
+ return output_rule(flag, stdout)[0]
23
+
24
+
25
+ def output_rule(flag: str | None, stdout: TextIO) -> tuple[OutputFormat, str]:
26
+ """The output format and which part of the output rule chose it (``databay status`` prints it)."""
27
+ if flag is not None:
28
+ return flag, "--output" # type: ignore[return-value] — click already checked the choice
29
+ env = os.environ.get(ENV_OUTPUT)
30
+ if env:
31
+ if env not in OUTPUT_FORMATS:
32
+ raise DatabayUsageError(f"{ENV_OUTPUT}={env!r} is not one of {', '.join(OUTPUT_FORMATS)}")
33
+ return env, ENV_OUTPUT # type: ignore[return-value]
34
+ if stdout.isatty():
35
+ return "table", "stdout is a terminal"
36
+ return "json", "stdout is captured"
37
+
38
+
39
+ @dataclass
40
+ class Console:
41
+ output: OutputFormat = "json"
42
+ verbose: bool = False
43
+ stdout: TextIO = field(default_factory=lambda: sys.stdout)
44
+ stderr: TextIO = field(default_factory=lambda: sys.stderr)
45
+ _live_line: bool = False
46
+ #: What ``notice`` already showed on a terminal, so the error lines do not repeat it.
47
+ _noticed: dict[str, str] = field(default_factory=dict)
48
+
49
+ @property
50
+ def stderr_tty(self) -> bool:
51
+ return self.stderr.isatty()
52
+
53
+ @property
54
+ def color(self) -> bool:
55
+ return self.stdout.isatty() and not os.environ.get(ENV_NO_COLOR)
56
+
57
+ def _end_live_line(self) -> None:
58
+ if self._live_line:
59
+ self.stderr.write("\n")
60
+ self._live_line = False
61
+
62
+ def event(self, name: str, fields: dict[str, Any]) -> None:
63
+ """One stderr event; ``request`` and ``response`` events only under ``--verbose``."""
64
+ if name in {"request", "response", "cache_invalid", "cache_write_failed"} and not self.verbose:
65
+ return
66
+ if not self.stderr_tty:
67
+ self.stderr.write(json.dumps({"event": name, **fields}, default=str) + "\n")
68
+ self.stderr.flush()
69
+ return
70
+ self._end_live_line()
71
+ details = " ".join(f"{key}={value}" for key, value in fields.items() if value is not None)
72
+ self.stderr.write(f"{name}: {details}\n")
73
+ self.stderr.flush()
74
+
75
+ def progress(self, state: str | None, elapsed_s: float, operation: str) -> None:
76
+ """A wait's state: a live line on a terminal, one ``wait`` event per state change when captured."""
77
+ if not self.stderr_tty:
78
+ self.event("wait", {"state": state, "elapsed_s": round(elapsed_s, 1), "status_operation": operation})
79
+ return
80
+ self.stderr.write(f"\rwaiting: {state} ({round(elapsed_s)}s)\033[K")
81
+ self.stderr.flush()
82
+ self._live_line = True
83
+
84
+ def notice(self, name: str, value: str, *, only_on_terminal: bool) -> None:
85
+ """
86
+ A value the result does not carry, such as ``next``: ``name: value`` on a terminal, an event when captured.
87
+
88
+ ``only_on_terminal`` is for JSON output, whose envelope already holds the value.
89
+ """
90
+ if self.stderr_tty:
91
+ self._end_live_line()
92
+ self.stderr.write(f"{name}: {value}\n")
93
+ self._noticed[name] = value
94
+ return
95
+ if not only_on_terminal:
96
+ self.event(name, {"value": value})
97
+
98
+ def inform(self, name: str, fields: dict[str, Any], text: str) -> None:
99
+ """Something the user must act on: ``text`` on a terminal, the ``name`` event with ``fields`` when captured."""
100
+ if not self.stderr_tty:
101
+ self.event(name, fields)
102
+ return
103
+ self._end_live_line()
104
+ self.stderr.write(text + "\n")
105
+ self.stderr.flush()
106
+
107
+ def error(self, body: dict[str, Any]) -> None:
108
+ self._end_live_line()
109
+ if not self.stderr_tty:
110
+ self.stderr.write(json.dumps({"error": body}, default=str) + "\n")
111
+ self.stderr.flush()
112
+ return
113
+ self.stderr.write(f"error: {body['code']}: {body['message']}\n")
114
+ if body.get("hint"):
115
+ self.stderr.write(f"hint: {body['hint']}\n")
116
+ if body.get("next") and self._noticed.get("next") != body["next"]:
117
+ self.stderr.write(f"next: {body['next']}\n")
118
+ trace_id = (body.get("details") or {}).get("trace_id")
119
+ if trace_id and (body.get("status") or 0) >= 500: # noqa: PLR2004
120
+ self.stderr.write(f"trace_id: {trace_id}\n")
121
+ self.stderr.flush()
databay/cli/flags.py ADDED
@@ -0,0 +1,183 @@
1
+ """
2
+ The arguments and flags of one generated command, as plain data.
3
+
4
+ ``databay.cli.build`` turns them into click parameters and ``databay.reference`` renders them into
5
+ ``commands.md``, so ``--help`` and the reference list the same flags with the same text. Standard
6
+ library only: ``python/tools/gen_client.py`` reads it.
7
+
8
+ - A path parameter is a positional argument, in path order. The project's own path parameter is
9
+ optional when nothing required follows it.
10
+ - A query or header parameter, or a body field, is ``--<name in kebab case>``. A list repeats the
11
+ flag; an object takes JSON text.
12
+ - A secret field (``format: password``) is never a value flag: ``--<name>-env NAME`` names a
13
+ variable that holds it and ``--<name>-stdin`` reads it from stdin.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from dataclasses import dataclass
19
+ from typing import TYPE_CHECKING, Literal
20
+
21
+ from databay.settings import OUTPUT_FORMATS
22
+
23
+ if TYPE_CHECKING:
24
+ from databay.operation import Operation, Param
25
+
26
+ Role = Literal[
27
+ "argument",
28
+ "option",
29
+ "secret_env",
30
+ "secret_stdin",
31
+ "project",
32
+ "file",
33
+ "yes",
34
+ "wait",
35
+ "timeout",
36
+ "idempotency_key",
37
+ "output",
38
+ "profile",
39
+ "verbose",
40
+ "no_retry",
41
+ ]
42
+
43
+ #: Flags the CLI adds itself; a parameter or body field that would take one of these names is a
44
+ #: generator error.
45
+ RESERVED_FLAGS = frozenset(
46
+ {"project", "file", "yes", "wait", "timeout", "idempotency-key", "output", "profile", "verbose", "no-retry", "help"}
47
+ )
48
+
49
+
50
+ @dataclass(frozen=True, slots=True)
51
+ class Flag:
52
+ role: Role
53
+ #: The flag (``--source-id``) or the argument's metavar (``PIPE_UUID``).
54
+ name: str
55
+ dest: str
56
+ help: str
57
+ required: bool = False
58
+ param: Param | None = None
59
+ #: ``value``: takes a value; ``switch``: a bare flag; ``bool``: ``--x`` sends true and ``--no-x``
60
+ #: false; ``multiple``: repeated, one item each.
61
+ takes: Literal["value", "switch", "bool", "multiple", "argument"] = "value"
62
+ choices: tuple[str, ...] = ()
63
+ metavar: str | None = None
64
+
65
+
66
+ def _value_help(param: Param) -> str:
67
+ text = param.description.strip().splitlines()[0] if param.description.strip() else ""
68
+ if param.kind == "json":
69
+ text = f"{text} (JSON)".strip()
70
+ return text
71
+
72
+
73
+ def _argument(param: Param, *, required: bool) -> Flag:
74
+ return Flag(
75
+ "argument",
76
+ param.name.upper(),
77
+ param.identifier,
78
+ _value_help(param),
79
+ required=required,
80
+ param=param,
81
+ takes="argument",
82
+ )
83
+
84
+
85
+ def _option(param: Param, *, role: Role = "option") -> list[Flag]:
86
+ if param.secret:
87
+ return [
88
+ Flag(
89
+ "secret_env",
90
+ f"--{param.flag}-env",
91
+ f"{param.identifier}_env",
92
+ f"name of the environment variable that holds {param.name} (secret)",
93
+ param=param,
94
+ metavar="NAME",
95
+ ),
96
+ Flag(
97
+ "secret_stdin",
98
+ f"--{param.flag}-stdin",
99
+ f"{param.identifier}_stdin",
100
+ f"read {param.name} (secret) from stdin",
101
+ param=param,
102
+ takes="switch",
103
+ ),
104
+ ]
105
+ takes = "multiple" if param.kind == "list" else "bool" if param.kind == "boolean" else "value"
106
+ return [
107
+ Flag(
108
+ role,
109
+ f"--{param.flag}",
110
+ param.identifier,
111
+ _value_help(param),
112
+ required=param.required,
113
+ param=param,
114
+ takes=takes,
115
+ choices=param.choices,
116
+ )
117
+ ]
118
+
119
+
120
+ def _project_path_optional(op: Operation) -> bool:
121
+ location = op.project_location
122
+ if location is None or location[0] != "path":
123
+ return False
124
+ names = [p.name for p in op.path_params]
125
+ return names[-1] == location[1]
126
+
127
+
128
+ def command_flags(op: Operation, variant: str | None = None) -> list[Flag]:
129
+ """Every argument and flag of ``op`` (of its body variant ``variant``), in the order ``--help`` shows them."""
130
+ location = op.project_location
131
+ flags: list[Flag] = []
132
+ optional_last = _project_path_optional(op)
133
+ for index, param in enumerate(op.path_params):
134
+ is_project = location is not None and location == ("path", param.name)
135
+ last = index == len(op.path_params) - 1
136
+ flags.append(_argument(param, required=not (is_project and optional_last and last)))
137
+ for param in op.params:
138
+ if param.location == "path":
139
+ continue
140
+ if location is not None and location == (param.location, param.name):
141
+ flags.append(
142
+ Flag("option", f"--{param.flag}", param.identifier, _value_help(param), param=param, takes="value")
143
+ )
144
+ continue
145
+ flags += _option(param)
146
+ if op.body is not None:
147
+ for param in op.body.fields_of(variant):
148
+ if location is not None and location == ("body", param.name):
149
+ flags.append(
150
+ Flag("option", f"--{param.flag}", param.identifier, _value_help(param), param=param, takes="value")
151
+ )
152
+ continue
153
+ flags += _option(param)
154
+ flags.append(Flag("file", "--file", "file", "read the whole body as JSON from a file, or - for stdin"))
155
+ if op.project is not None:
156
+ flags.append(Flag("project", "--project", "project", "the project to act in (default: the active project)"))
157
+ if op.idempotent:
158
+ flags.append(
159
+ Flag(
160
+ "idempotency_key",
161
+ "--idempotency-key",
162
+ "idempotency_key",
163
+ "repeat an earlier call: the server answers it again and creates nothing (default: a new key)",
164
+ )
165
+ )
166
+ if op.destructive:
167
+ flags.append(Flag("yes", "--yes", "yes", "confirm: this removes or overwrites state", takes="switch"))
168
+ if op.offers_wait:
169
+ flags.append(Flag("wait", "--wait", "wait", "wait until it is done, polling its status", takes="switch"))
170
+ flags.append(
171
+ Flag("timeout", "--timeout", "timeout", "seconds --wait may take before exit 4 (resumable)", metavar="S")
172
+ )
173
+ flags += common_flags()
174
+ return flags
175
+
176
+
177
+ def common_flags() -> list[Flag]:
178
+ return [
179
+ Flag("output", "--output", "output", "json, table or csv", choices=OUTPUT_FORMATS),
180
+ Flag("profile", "--profile", "profile", "the profile to use (default: current_profile)"),
181
+ Flag("verbose", "--verbose", "verbose", "log each request on stderr, secrets redacted", takes="switch"),
182
+ Flag("no_retry", "--no-retry", "no_retry", "send each request once", takes="switch"),
183
+ ]
@@ -0,0 +1,22 @@
1
+ """
2
+ Commands written by hand, for work no single operation does: local state, sign-in, curated flows.
3
+
4
+ Each entry is ``command -> (module, attribute, summary)``. A one-word command sits beside the
5
+ generated nouns; ``"<noun> <verb>"`` adds a verb to that generated noun (``project use``). The root
6
+ group imports the module only when the command, or its noun, runs. An operation declared
7
+ ``agent_shape="curated:<command>"`` gets no generated command; its command belongs here.
8
+ """
9
+
10
+ HANDWRITTEN: dict[str, tuple[str, str, str]] = {
11
+ "login": ("databay.cli.auth", "login", "Sign in and save a profile"),
12
+ "logout": ("databay.cli.auth", "logout", "Sign out: revoke and delete the profile's credential"),
13
+ "profile": ("databay.cli.profile", "profile", "Switch between and list profiles"),
14
+ "status": ("databay.cli.profile", "status", "Who and where: profile, server, user, org, project, skill"),
15
+ "skill": ("databay.cli.skill", "skill", "Install the agent skill for Claude Code or Codex"),
16
+ "org switch": ("databay.cli.org", "org_switch", "Make another organization active and refresh the token"),
17
+ "project use": ("databay.cli.profile", "project_use", "Set the profile's active project"),
18
+ "pipe wait": ("databay.cli.pipe_wait", "pipe_wait", "Wait until a pipe is active or its table is ready to query"),
19
+ "metrics describe": ("databay.cli.metrics", "describe", "Describe one metric you can query"),
20
+ "metrics query": ("databay.cli.metrics", "query", "Query one metric by dimensions and time grain"),
21
+ "sql": ("databay.cli.sql", "sql", "Run one read-only SQL statement"),
22
+ }
databay/cli/main.py ADDED
@@ -0,0 +1,198 @@
1
+ """
2
+ The ``databay`` entry point.
3
+
4
+ Each noun's commands live in their own generated module, imported only when that noun runs, so
5
+ ``databay --help`` and ``databay pipe get --help`` load almost nothing. Every failure leaves through
6
+ ``fail``: one error object on stderr and the exit code its code names.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import importlib
12
+ import sys
13
+ import time
14
+ import traceback
15
+ from typing import TYPE_CHECKING, Any
16
+
17
+ import click
18
+
19
+ from databay.cli.handwritten import HANDWRITTEN
20
+ from databay.errors import DatabayError
21
+ from databay.platform_check import ensure_supported
22
+ from databay.settings import (
23
+ ENV_CONFIG_DIR,
24
+ ENV_NO_UPDATE_CHECK,
25
+ ENV_OUTPUT,
26
+ ENV_STATE_DIR,
27
+ ENV_TOKEN,
28
+ OUTPUT_RULE,
29
+ )
30
+ from databay.spec import API_VERSION, NOUNS
31
+ from databay.version import CLIENT_VERSION
32
+
33
+ if TYPE_CHECKING:
34
+ from collections.abc import Sequence
35
+
36
+ import httpx
37
+
38
+ from databay.update_check import UpdateCheck
39
+
40
+ ROOT_HELP = f"""Databay from the command line: databay <noun> <verb> [ARGS] [OPTIONS].
41
+
42
+ {OUTPUT_RULE} stdout carries only the result; progress, hints and errors go to stderr.
43
+
44
+ \b
45
+ Environment:
46
+ {ENV_TOKEN} the access token, for a profile whose auth is "env";
47
+ set under a login or api_key profile it is an error
48
+ {ENV_OUTPUT} json, table or csv
49
+ {ENV_CONFIG_DIR} where config.toml lives (default: the platform's config dir)
50
+ {ENV_STATE_DIR} where caches and crash logs go (default: the platform's state dir)
51
+ {ENV_NO_UPDATE_CHECK}=1 no daily check for a newer release (also off under CI)
52
+ NO_COLOR no colors in tables
53
+
54
+ \b
55
+ Exit codes: 0 ok, 1 refused, 2 usage, 3 profile or token, 4 still pending,
56
+ 5 wait failed, 6 transient, 7 version mismatch, 8 client outdated or bug,
57
+ 9 not ready. A failure prints {{"error": {{"code", "message", "hint", "next",
58
+ "recovery", "context"}}}} on stderr when captured."""
59
+
60
+
61
+ class RootGroup(click.Group):
62
+ """Lists every noun from the generated index; imports a noun's module only to run it."""
63
+
64
+ def list_commands(self, ctx: click.Context) -> list[str]: # noqa: ARG002 — click's signature
65
+ return sorted(set(NOUNS) | {name for name in HANDWRITTEN if " " not in name})
66
+
67
+ def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None: # noqa: ARG002
68
+ if cmd_name in HANDWRITTEN:
69
+ return _handwritten(cmd_name)
70
+ if cmd_name not in NOUNS:
71
+ return None
72
+ module, _ = NOUNS[cmd_name]
73
+ group: click.Group = importlib.import_module(f"databay.cli.commands.{module}").group
74
+ for verb in _handwritten_verbs(cmd_name):
75
+ group.add_command(_handwritten(f"{cmd_name} {verb}"), verb)
76
+ return group
77
+
78
+ def format_commands(self, ctx: click.Context, formatter: click.HelpFormatter) -> None: # noqa: ARG002
79
+ rows = [(noun, ", ".join(sorted([*verbs, *_handwritten_verbs(noun)]))) for noun, (_, verbs) in NOUNS.items()]
80
+ rows += [(name, summary) for name, (_, _, summary) in HANDWRITTEN.items() if " " not in name]
81
+ with formatter.section("Commands"):
82
+ formatter.write_dl(sorted(rows))
83
+
84
+
85
+ def _handwritten(command: str) -> click.Command:
86
+ module, attribute, _ = HANDWRITTEN[command]
87
+ return getattr(importlib.import_module(module), attribute)
88
+
89
+
90
+ def _handwritten_verbs(noun: str) -> list[str]:
91
+ """The hand-written verbs of a generated noun (``project use``)."""
92
+ return [verb for name in HANDWRITTEN for head, _, verb in [name.partition(" ")] if verb and head == noun]
93
+
94
+
95
+ @click.group(cls=RootGroup, help=ROOT_HELP, context_settings={"max_content_width": 110})
96
+ @click.version_option(f"{CLIENT_VERSION} (API {API_VERSION})", prog_name="databay", message="%(prog)s %(version)s")
97
+ def root() -> None:
98
+ pass
99
+
100
+
101
+ def _crash_log(exc: BaseException) -> str | None:
102
+ from databay.paths import state_dir # noqa: PLC0415
103
+
104
+ try:
105
+ directory = state_dir()
106
+ directory.mkdir(parents=True, exist_ok=True)
107
+ path = directory / f"crash-{time.strftime('%Y%m%dT%H%M%S')}.log"
108
+ path.write_text("".join(traceback.format_exception(exc)), encoding="utf-8")
109
+ except OSError:
110
+ return None
111
+ return str(path)
112
+
113
+
114
+ def fail(error: DatabayError, state: dict[str, Any]) -> int:
115
+ from databay.cli.console import Console # noqa: PLC0415
116
+
117
+ console: Console = state.get("console") or Console()
118
+ body = error.to_dict()
119
+ body["context"] = {
120
+ "cli_version": CLIENT_VERSION,
121
+ "api_version": API_VERSION,
122
+ **dict.fromkeys(("server", "profile", "project", "request_id")),
123
+ **state.get("context", {}),
124
+ }
125
+ console.error(body)
126
+ return error.exit_code
127
+
128
+
129
+ def main(argv: Sequence[str] | None = None, *, http: httpx.Client | None = None) -> int:
130
+ """
131
+ Run the CLI and return its exit code; the console script exits with it.
132
+
133
+ ``http`` is the httpx client every request goes through; tests pass one on ``httpx.MockTransport``.
134
+ """
135
+ state: dict[str, Any] = {"http": http}
136
+ update = _start_update_check()
137
+ try:
138
+ ensure_supported()
139
+ root.main(args=list(argv) if argv is not None else None, prog_name="databay", standalone_mode=False, obj=state)
140
+ except DatabayError as exc:
141
+ code = fail(exc, state)
142
+ except click.exceptions.NoArgsIsHelpError as exc:
143
+ click.echo(exc.ctx.get_help() if exc.ctx is not None else exc.format_message())
144
+ code = 0
145
+ except click.exceptions.Abort:
146
+ from databay.errors import DatabayCancelled # noqa: PLC0415
147
+
148
+ if state.get("sent"):
149
+ key = state.get("idempotency_key")
150
+ error = DatabayCancelled(
151
+ "interrupted after the request was sent",
152
+ code="interrupted",
153
+ details={"idempotency_key": key} if key else {},
154
+ next_command=state.get("next"),
155
+ )
156
+ elif state.get("reading"):
157
+ error = DatabayCancelled(
158
+ "stopped while reading; nothing was changed", code="stopped", next_command=state.get("next")
159
+ )
160
+ else:
161
+ error = DatabayCancelled("cancelled; nothing was sent")
162
+ code = fail(error, state)
163
+ except click.UsageError as exc:
164
+ from databay.errors import DatabayUsageError # noqa: PLC0415
165
+
166
+ hint = exc.ctx.get_usage() if exc.ctx is not None else None
167
+ code = fail(DatabayUsageError(exc.format_message(), hint=hint), state)
168
+ except click.ClickException as exc:
169
+ from databay.errors import DatabayUsageError # noqa: PLC0415
170
+
171
+ code = fail(DatabayUsageError(exc.format_message()), state)
172
+ except Exception as exc: # noqa: BLE001 — the last line: every crash leaves as internal_error
173
+ path = _crash_log(exc)
174
+ if state.get("console") is not None and state["console"].verbose:
175
+ traceback.print_exception(exc, file=sys.stderr)
176
+ error = DatabayError(
177
+ f"databay crashed: {type(exc).__name__}" + (f"; the trace is in {path}" if path else ""),
178
+ details={"crash_log": path},
179
+ )
180
+ code = fail(error, state)
181
+ else:
182
+ code = 0
183
+ if update is not None:
184
+ console = state.get("console")
185
+ update.finish(sys.stderr, console.event if console is not None and console.verbose else None)
186
+ return code
187
+
188
+
189
+ def _start_update_check() -> UpdateCheck | None:
190
+ import os # noqa: PLC0415
191
+
192
+ from databay import update_check # noqa: PLC0415
193
+
194
+ if not update_check.enabled(dict(os.environ), sys.stdout, sys.stderr):
195
+ return None
196
+ check = update_check.UpdateCheck()
197
+ check.start()
198
+ return check