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/runner.py ADDED
@@ -0,0 +1,432 @@
1
+ """
2
+ Running one generated command: its inputs, the confirmation, the call, the wait and the output.
3
+
4
+ Order matters: every input is read and checked (flags, secrets, the project) before any request,
5
+ and a destructive command is confirmed before anything is sent.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ import shlex
13
+ import sys
14
+ import uuid
15
+ from pathlib import Path
16
+ from typing import TYPE_CHECKING, Any
17
+
18
+ import click
19
+
20
+ from databay import waits
21
+ from databay.cli.console import Console, output_format
22
+ from databay.cli.output import emit
23
+ from databay.errors import (
24
+ DatabayCancelled,
25
+ DatabayError,
26
+ DatabayNotReady,
27
+ DatabayUsageError,
28
+ DatabayWaitError,
29
+ )
30
+ from databay.invoke import CallValues, Session, resolve_project
31
+ from databay.transport import Transport
32
+
33
+ if TYPE_CHECKING:
34
+ from collections.abc import Callable
35
+
36
+ from databay.cli.flags import Flag
37
+ from databay.operation import Body, Operation, Param, Wait
38
+ from databay.profiles import Profile
39
+ from databay.tokens import TokenProvider
40
+
41
+
42
+ def _usage(message: str, code: str = "usage_invalid") -> DatabayUsageError:
43
+ return DatabayUsageError(message, code=code)
44
+
45
+
46
+ def _parse_json(flag: str, text: str) -> Any: # noqa: ANN401
47
+ try:
48
+ return json.loads(text)
49
+ except ValueError as exc:
50
+ raise _usage(f"{flag} takes JSON: {exc}") from None
51
+
52
+
53
+ #: A flag with choices reads its value as text (``click.Choice``): an integer or number enum sends a number.
54
+ _NUMBERS: dict[str | None, type] = {"integer": int, "number": float}
55
+
56
+
57
+ def _convert(param: Param, flag: str, value: Any) -> Any: # noqa: ANN401
58
+ if param.kind == "json":
59
+ return _parse_json(flag, value)
60
+ if param.kind == "list":
61
+ cast = _NUMBERS.get(param.item_kind)
62
+ return [cast(v) for v in value] if cast else list(value)
63
+ cast = _NUMBERS.get(param.kind)
64
+ return cast(value) if cast else value
65
+
66
+
67
+ class Stdin:
68
+ """stdin, which one input at most may read."""
69
+
70
+ def __init__(self) -> None:
71
+ self.claimed_by: str | None = None
72
+
73
+ def read(self, flag: str) -> str:
74
+ if self.claimed_by is not None:
75
+ raise _usage(f"{flag} and {self.claimed_by} both read stdin; pass one of them another way")
76
+ self.claimed_by = flag
77
+ return sys.stdin.read()
78
+
79
+
80
+ def _secret(param: Param, values: dict[str, Any], stdin: Stdin) -> Any: # noqa: ANN401
81
+ env_flag, stdin_flag = f"--{param.flag}-env", f"--{param.flag}-stdin"
82
+ env_name = values.get(f"{param.identifier}_env")
83
+ from_stdin = values.get(f"{param.identifier}_stdin")
84
+ if env_name and from_stdin:
85
+ raise _usage(f"pass {param.name} with {env_flag} or {stdin_flag}, not both")
86
+ text: str | None = None
87
+ if env_name:
88
+ text = os.environ.get(env_name, "")
89
+ if not text:
90
+ raise _usage(f"{env_flag} names ${env_name}, which is not set or empty", "secret_required")
91
+ elif from_stdin:
92
+ text = stdin.read(stdin_flag).rstrip("\r\n")
93
+ if not text:
94
+ raise _usage(f"{stdin_flag}: stdin was empty", "secret_required")
95
+ elif param.required:
96
+ if not sys.stdin.isatty():
97
+ raise _usage(f"{param.name} is a secret: pass {env_flag} NAME or {stdin_flag}", "secret_required")
98
+ text = click.prompt(param.name, hide_input=True, err=True)
99
+ if text is None:
100
+ return None
101
+ return _parse_json(env_flag, text) if param.kind == "json" else text
102
+
103
+
104
+ def _body_file(path: str, stdin: Stdin) -> Any: # noqa: ANN401
105
+ if path == "-":
106
+ return _parse_json("--file", stdin.read("--file -"))
107
+ try:
108
+ text = Path(path).read_text(encoding="utf-8")
109
+ except OSError as exc:
110
+ raise _usage(f"--file {path}: {exc.strerror}") from None
111
+ return _parse_json(f"--file {path}", text)
112
+
113
+
114
+ def _with_variant(spec: Body, variant: str, body: Any, fields: dict[str, Any]) -> dict[str, Any]: # noqa: ANN401
115
+ """``body`` with the variant's ``fields`` and discriminator in place; a file's own discriminator must agree."""
116
+ if not isinstance(body, dict):
117
+ raise _usage("--file must hold a JSON object")
118
+ holder = body if spec.variant_field is None else body.setdefault(spec.variant_field, {})
119
+ if not isinstance(holder, dict):
120
+ raise _usage(f"--file's {spec.variant_field} must hold a JSON object")
121
+ assert spec.discriminator is not None
122
+ holder.update(fields)
123
+ given = holder.get(spec.discriminator, variant)
124
+ if given != variant:
125
+ raise _usage(f"--file has {spec.discriminator}={given!r}, but the command is `{variant}`")
126
+ holder[spec.discriminator] = variant
127
+ return body
128
+
129
+
130
+ def call_values(op: Operation, variant: str | None, flags: list[Flag], values: dict[str, Any]) -> CallValues:
131
+ """The command line as ``CallValues``; a missing required input is a usage error naming its flag."""
132
+ call = CallValues(project_flag=values.get("project"), idempotency_key=values.get("idempotency_key"))
133
+ location = op.project_location
134
+ stdin = Stdin()
135
+ body: dict[str, Any] = {}
136
+ variant_body: dict[str, Any] = {}
137
+ in_variant = {p.name for p in op.body.variant(variant)[1]} if op.body is not None and variant is not None else ()
138
+ body_flags: list[str] = []
139
+ missing: list[str] = []
140
+ for flag in flags:
141
+ param = flag.param
142
+ if param is None or flag.role == "secret_stdin":
143
+ continue
144
+ is_project = location is not None and location == (param.location, param.name)
145
+ if flag.role == "secret_env":
146
+ if values.get("file") is not None:
147
+ secret_flags = {
148
+ f"{param.identifier}_env": flag.name,
149
+ f"{param.identifier}_stdin": f"--{param.flag}-stdin",
150
+ }
151
+ body_flags += [name for dest, name in secret_flags.items() if values.get(dest)]
152
+ continue
153
+ value = _secret(param, values, stdin)
154
+ if value is not None:
155
+ (variant_body if param.name in in_variant else body)[param.name] = value
156
+ body_flags.append(flag.name)
157
+ elif param.required:
158
+ missing.append(flag.name)
159
+ continue
160
+ raw = values.get(flag.dest)
161
+ if raw is None or raw == ():
162
+ if flag.required and not (param.location == "body" and values.get("file") is not None):
163
+ missing.append(flag.name)
164
+ continue
165
+ if is_project:
166
+ call.project_explicit = str(raw)
167
+ if param.location == "body":
168
+ body_flags.append(flag.name)
169
+ continue
170
+ value = _convert(param, flag.name, raw)
171
+ if param.location == "body":
172
+ (variant_body if param.name in in_variant else body)[param.name] = value
173
+ body_flags.append(flag.name)
174
+ else:
175
+ {"path": call.path, "query": call.query, "header": call.headers}[param.location][param.name] = value
176
+ if missing:
177
+ raise _usage(f"`{op.command}` needs {', '.join(missing)}")
178
+ if op.body is None:
179
+ return call
180
+ if values.get("file") is not None:
181
+ if body_flags:
182
+ raise _usage(f"pass the body with --file or with flags ({', '.join(body_flags)}), not both")
183
+ body = _body_file(values["file"], stdin)
184
+ if location is not None and location[0] == "body" and isinstance(body, dict) and location[1] in body:
185
+ # The file's own project id is an explicit one: --project must agree with it. A null one is
186
+ # not given, as the SDK reads None.
187
+ named = body.pop(location[1])
188
+ if named is not None:
189
+ call.project_explicit = str(named)
190
+ call.body = body if variant is None else _with_variant(op.body, variant, body, variant_body)
191
+ call.has_body = True
192
+ return call
193
+
194
+
195
+ def _confirm(op: Operation, call: CallValues, project: str | None, yes: bool, console: Console) -> None:
196
+ if not op.destructive or yes:
197
+ return
198
+ if not (sys.stdin.isatty() and console.stdout.isatty()):
199
+ raise _usage(f"`{op.command}` removes or overwrites state: pass --yes to confirm", "confirmation_required")
200
+ target = " ".join(str(v) for v in call.path.values())
201
+ where = f" in project {project}" if project else ""
202
+ answer = click.prompt(
203
+ f"{op.verb} {op.noun} {target}{where}? [y/N]".replace(" ", " "),
204
+ default="",
205
+ show_default=False,
206
+ err=True,
207
+ )
208
+ if answer.strip().lower() not in {"y", "yes"}:
209
+ raise DatabayCancelled(f"{op.command} cancelled; nothing was sent")
210
+
211
+
212
+ def _resume_command(
213
+ wait: Wait, bound: dict[str, str], timeout: float | None, profile: str | None, project: str | None
214
+ ) -> str:
215
+ """The status command that resumes ``wait``: same target, project and profile; nothing starts again."""
216
+ status_op = waits.operation(wait.status_operation)
217
+ words = ["databay", status_op.noun, status_op.verb]
218
+ words += [bound[p.name] for p in status_op.path_params]
219
+ for param in status_op.params:
220
+ if param.location == "query" and param.name in bound:
221
+ words += [f"--{param.flag}", bound[param.name]]
222
+ words.append("--wait")
223
+ if timeout is not None:
224
+ words += ["--timeout", f"{timeout:g}"]
225
+ if status_op.project is not None and project is not None:
226
+ words += ["--project", project]
227
+ if profile is not None:
228
+ words += ["--profile", profile]
229
+ return shlex.join(words)
230
+
231
+
232
+ def open_session(values: dict[str, Any], console: Console, state: dict[str, Any]) -> tuple[Session, Profile]:
233
+ """A session as the profile ``--profile`` (else ``current_profile``) names, with the common flags applied."""
234
+ from databay.profiles import load_profile # noqa: PLC0415
235
+ from databay.tokens import token_provider # noqa: PLC0415
236
+
237
+ profile = load_profile(values.get("profile"))
238
+ return session_for(profile, token_provider(profile, state.get("http")), values, console, state), profile
239
+
240
+
241
+ def session_for(
242
+ profile: Profile, tokens: TokenProvider, values: dict[str, Any], console: Console, state: dict[str, Any]
243
+ ) -> Session:
244
+ """A session as ``profile`` with ``tokens``, the common flags applied."""
245
+ from databay.profiles import login_command # noqa: PLC0415
246
+
247
+ transport = Transport(
248
+ server=profile.server,
249
+ tokens=tokens,
250
+ client_kind="cli",
251
+ retries=not values.get("no_retry"),
252
+ verbose=bool(values.get("verbose")),
253
+ events=console.event,
254
+ http=state.get("http"),
255
+ login_command=login_command(profile),
256
+ )
257
+ return Session(transport, active_project=profile.active_project, profile=profile.name)
258
+
259
+
260
+ def run(ctx: click.Context, op: Operation, variant: str | None, flags: list[Flag], values: dict[str, Any]) -> None:
261
+ """Run ``op`` from parsed command-line ``values``; any failure leaves as a ``DatabayError``."""
262
+ state: dict[str, Any] = ctx.ensure_object(dict)
263
+ console = Console(output=output_format(values.get("output"), sys.stdout), verbose=bool(values.get("verbose")))
264
+ state["console"] = console
265
+ if values.get("timeout") is not None and not values.get("wait"):
266
+ raise _usage("--timeout applies to --wait only")
267
+ call = call_values(op, variant, flags, values)
268
+ if op.idempotent and call.idempotency_key is None:
269
+ call.idempotency_key = str(uuid.uuid4())
270
+ state["idempotency_key"] = call.idempotency_key
271
+ session, profile = open_session(values, console, state)
272
+ state["context"] = {"server": profile.server, "profile": profile.name, "request_id": session.transport.request_id}
273
+ project = resolve_project(op, call, session.active_project)
274
+ state["context"]["project"] = project
275
+ _confirm(op, call, project, bool(values.get("yes")), console)
276
+ explicit_profile = values.get("profile")
277
+ try:
278
+ session.ensure_compatible(op)
279
+ resuming = bool(values.get("wait")) and op.resumes is not None
280
+ # From here a write may be out: an interrupt can no longer promise that nothing changed.
281
+ state["sent"] = op.method != "get" and not resuming
282
+ state["reading"] = not state["sent"]
283
+ if resuming:
284
+ _run_resume(op, call, session, console, values, explicit_profile, project, state)
285
+ return
286
+ try:
287
+ result = session.call(op, call)
288
+ except DatabayNotReady as exc:
289
+ # A not-ready verdict is still an answer: print it, then exit 9.
290
+ emit(
291
+ console,
292
+ {"result": exc.result, **({"project": project} if project else {})},
293
+ op.columns,
294
+ op.response.rows,
295
+ )
296
+ raise
297
+ state["context"]["request_id"] = result.request_id
298
+ envelope: dict[str, Any] = {"result": result.raw}
299
+ if result.project is not None:
300
+ envelope["project"] = result.project
301
+ if call.idempotency_key is not None:
302
+ envelope["idempotency_key"] = call.idempotency_key
303
+ wait = op.wait
304
+ bound = waits.bind(wait, result.raw, result.path) if wait is not None else None
305
+ if wait is not None and values.get("wait"):
306
+ if bound is None:
307
+ envelope["wait"] = waits.not_started(wait).to_dict()
308
+ emit(console, envelope, op.columns, op.response.rows)
309
+ return
310
+ resume = _resume_command(wait, bound, values.get("timeout"), explicit_profile, project)
311
+ _wait(session, wait, bound, values.get("timeout"), project, console, envelope, op, resume, state)
312
+ return
313
+ if wait is not None and bound is not None:
314
+ envelope["next"] = _resume_command(wait, bound, None, explicit_profile, project)
315
+ emit(console, envelope, op.columns, op.response.rows)
316
+ finally:
317
+ session.transport.close()
318
+
319
+
320
+ def _wait(
321
+ session: Session,
322
+ wait: Wait,
323
+ bound: dict[str, str],
324
+ timeout: float | None,
325
+ project: str | None,
326
+ console: Console,
327
+ envelope: dict[str, Any],
328
+ op: Operation,
329
+ resume: str,
330
+ state: dict[str, Any],
331
+ *,
332
+ from_status: bool = False,
333
+ ) -> None:
334
+ """
335
+ Wait, then print the envelope with the wait's outcome, also when the wait does not succeed.
336
+
337
+ Whatever stops the wait, the operation's answer is printed. ``from_status`` makes the last status
338
+ answer the result: a status command run with ``--wait``.
339
+ """
340
+
341
+ def poll(on_state: Callable[[waits.WaitOutcome], None]) -> waits.WaitOutcome:
342
+ return waits.run(session, wait, bound, timeout_s=timeout, project=project, on_state=on_state)
343
+
344
+ report_wait(poll, console, envelope, op.columns, op.response.rows, resume, state, from_status=from_status)
345
+
346
+
347
+ def report_wait(
348
+ poll: Callable[[Callable[[waits.WaitOutcome], None]], waits.WaitOutcome],
349
+ console: Console,
350
+ envelope: dict[str, Any],
351
+ columns: tuple[str, ...],
352
+ rows: str | None,
353
+ resume: str,
354
+ state: dict[str, Any],
355
+ *,
356
+ from_status: bool = False,
357
+ then: Callable[[waits.WaitOutcome], str] | None = None,
358
+ ) -> None:
359
+ """
360
+ Run ``poll``, reporting each state on stderr, then print the envelope, also when the wait does not succeed.
361
+
362
+ A wait that can still end well (out of time, a transient failure, an interrupt) carries ``next``,
363
+ the command that resumes it without starting anything again. ``then`` names ``next`` after a success.
364
+ """
365
+ state["next"] = resume
366
+
367
+ def on_state(outcome: waits.WaitOutcome) -> None:
368
+ console.progress(outcome.state, outcome.elapsed_s, outcome.status_operation)
369
+
370
+ def record(outcome: waits.WaitOutcome) -> None:
371
+ data = outcome.to_dict()
372
+ if from_status:
373
+ envelope["result"] = data.pop("status")
374
+ envelope["wait"] = data
375
+
376
+ def print_answer() -> None:
377
+ # A status command's only answer is the status: with none read yet there is nothing to print.
378
+ if envelope["result"] is not None or not from_status:
379
+ emit(console, envelope, columns, rows)
380
+
381
+ try:
382
+ outcome = poll(on_state)
383
+ except DatabayError as exc:
384
+ if isinstance(exc, DatabayWaitError) and exc.outcome is not None:
385
+ record(exc.outcome)
386
+ if exc.recovery.action == "retry" or exc.exit_code == 3: # noqa: PLR2004 — exit 3: sign in, then resume
387
+ # Only a stop that a later poll can get past is worth resuming: a transient one, or a token
388
+ # that a new sign-in replaces.
389
+ envelope["next"] = resume
390
+ exc.next_command = resume
391
+ print_answer()
392
+ raise
393
+ except KeyboardInterrupt:
394
+ envelope["next"] = resume
395
+ print_answer()
396
+ raise
397
+ record(outcome)
398
+ if then is not None:
399
+ envelope["next"] = then(outcome)
400
+ emit(console, envelope, columns, rows)
401
+
402
+
403
+ def _run_resume(
404
+ op: Operation,
405
+ call: CallValues,
406
+ session: Session,
407
+ console: Console,
408
+ values: dict[str, Any],
409
+ profile: str | None,
410
+ project: str | None,
411
+ state: dict[str, Any],
412
+ ) -> None:
413
+ """``--wait`` on a status command: poll it as the wait that started it would; the result is its last answer."""
414
+ assert op.resumes is not None
415
+ bound = {**{k: str(v) for k, v in call.path.items()}, **{k: str(v) for k, v in call.query.items()}}
416
+ resume = _resume_command(op.resumes, bound, values.get("timeout"), profile, project)
417
+ envelope: dict[str, Any] = {"result": None}
418
+ if project is not None:
419
+ envelope["project"] = project
420
+ _wait(
421
+ session,
422
+ op.resumes,
423
+ bound,
424
+ values.get("timeout"),
425
+ project,
426
+ console,
427
+ envelope,
428
+ op,
429
+ resume,
430
+ state,
431
+ from_status=True,
432
+ )
databay/cli/skill.py ADDED
@@ -0,0 +1,64 @@
1
+ """``databay skill install``: copy the agent skill that ships with this CLI to where an agent loads it."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import click
8
+
9
+ from databay.agent_skill import SCOPES
10
+ from databay.cli.common import console_for
11
+ from databay.cli.handwritten import HANDWRITTEN
12
+ from databay.settings import DEFAULT_SKILL_SCOPE, OUTPUT_FORMATS
13
+
14
+
15
+ @click.group("skill", short_help=HANDWRITTEN["skill"][2])
16
+ def skill() -> None:
17
+ """The agent skill: how an agent drives this CLI, with the command and error references."""
18
+
19
+
20
+ INSTALL_HELP = """Install the skill where the agent loads it, recording this CLI's version in its SKILL.md.
21
+
22
+ \b
23
+ claude: ~/.claude/skills/databay (user) or .claude/skills/databay (project)
24
+ codex: ~/.agents/skills/databay (user) or .agents/skills/databay (project)
25
+
26
+ A copy from another CLI version is replaced only with --force; without it the command exits 2
27
+ (skill_exists) and names both versions."""
28
+
29
+
30
+ @skill.command("install", help=INSTALL_HELP)
31
+ @click.option("--target", required=True, metavar="AGENT", help="the agent to install for: claude or codex")
32
+ @click.option(
33
+ "--scope",
34
+ type=click.Choice(SCOPES),
35
+ default=DEFAULT_SKILL_SCOPE,
36
+ show_default=True,
37
+ help="user: under your home, for every project; project: under the current directory",
38
+ )
39
+ @click.option("--force", is_flag=True, default=False, help="replace a copy installed by another CLI version")
40
+ @click.option("--output", type=click.Choice(OUTPUT_FORMATS), default=None, help="json, table or csv")
41
+ @click.pass_context
42
+ def install(ctx: click.Context, target: str, scope: str, force: bool, output: str | None) -> None:
43
+ from databay.agent_skill import install as install_skill # noqa: PLC0415
44
+ from databay.cli.output import emit # noqa: PLC0415
45
+ from databay.version import CLIENT_VERSION # noqa: PLC0415
46
+
47
+ console, _ = console_for(ctx, {"output": output})
48
+ path, previous = install_skill(
49
+ target,
50
+ scope, # type: ignore[arg-type] — click checked the choice
51
+ force=force,
52
+ version=CLIENT_VERSION,
53
+ home=Path.home(),
54
+ cwd=Path.cwd(),
55
+ )
56
+ result = {
57
+ "target": target,
58
+ "scope": scope,
59
+ "path": str(path),
60
+ "version": CLIENT_VERSION,
61
+ "replaced": previous is not None,
62
+ "replaced_version": previous.version if previous is not None else None,
63
+ }
64
+ emit(console, {"result": result}, ())
databay/cli/sql.py ADDED
@@ -0,0 +1,84 @@
1
+ """``databay sql``: the curated command for ``POST /api/configstore/sql/execute``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import click
10
+
11
+ from databay.cli.common import call_once, common_options, result_envelope
12
+
13
+ SQL_HELP = """Run one read-only statement and print its rows.
14
+
15
+ \b
16
+ The statement is the argument, or the text of --file PATH (--file - reads
17
+ stdin), or stdin when neither is given and stdin is not a terminal.
18
+ \b
19
+ JSON prints the server's answer (columns, rows, row_count, truncated,
20
+ warnings); a table or CSV prints the rows under their column names.
21
+
22
+ \b
23
+ Example:
24
+ databay sql --db <db_uuid> "select count(*) from analytics.orders\""""
25
+
26
+
27
+ def _statement(argument: str | None, path: str | None) -> str:
28
+ from databay.errors import DatabayUsageError # noqa: PLC0415
29
+
30
+ if argument is not None and path is not None:
31
+ raise DatabayUsageError("pass the statement as the argument or with --file, not both")
32
+ if argument is not None:
33
+ text = argument
34
+ elif path == "-" or (path is None and not sys.stdin.isatty()):
35
+ text = sys.stdin.read()
36
+ elif path is not None:
37
+ try:
38
+ text = Path(path).read_text(encoding="utf-8")
39
+ except OSError as exc:
40
+ raise DatabayUsageError(f"--file {path}: {exc.strerror}") from None
41
+ else:
42
+ raise DatabayUsageError("pass the statement as the argument, with --file PATH, or on stdin")
43
+ if not text.strip():
44
+ raise DatabayUsageError("the statement is empty")
45
+ return text.strip()
46
+
47
+
48
+ @click.command("sql", help=SQL_HELP, short_help="Run one read-only SQL statement")
49
+ @click.argument("statement", required=False)
50
+ @click.option("--file", "file", default=None, metavar="PATH", help="read the statement from a file, or - for stdin")
51
+ @click.option(
52
+ "--db", "db", default=None, metavar="DB_UUID", help="the warehouse to run against (default: the org's default)"
53
+ )
54
+ @click.option("--project", default=None, help="the project to act in (default: the active project)")
55
+ @common_options
56
+ @click.pass_context
57
+ def sql(
58
+ ctx: click.Context,
59
+ statement: str | None,
60
+ file: str | None,
61
+ db: str | None,
62
+ project: str | None,
63
+ **values: Any, # noqa: ANN401
64
+ ) -> None:
65
+ from databay.cli.output import emit_grid # noqa: PLC0415
66
+ from databay.invoke import CallValues # noqa: PLC0415
67
+ from databay.operations.sql import SQL_EXECUTE # noqa: PLC0415
68
+
69
+ query = _statement(statement, file)
70
+ body: dict[str, Any] = {"query": query}
71
+ if db is not None:
72
+ body["db_uuid"] = db
73
+ call = CallValues(body=body, has_body=True, project_flag=project)
74
+ console, result = call_once(ctx, values, SQL_EXECUTE, call)
75
+ answer = result.parsed
76
+ if answer.truncated:
77
+ console.notice(
78
+ "truncated",
79
+ f"the server returned the first {answer.row_count} rows",
80
+ only_on_terminal=console.output == "json",
81
+ )
82
+ for warning in answer.warnings or []:
83
+ console.notice("warning", f"{warning.code}: {warning.message}", only_on_terminal=console.output == "json")
84
+ emit_grid(console, result_envelope(result), list(answer.columns), [list(row) for row in answer.rows])