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
@@ -0,0 +1,357 @@
1
+ """
2
+ ``databay pipe wait``: when a pipe is provisioned, and when its table is ready to query.
3
+
4
+ ``active`` is the wait ``pipe status --wait`` runs. ``data`` waits for that first, then reads the
5
+ pipe and measures its destination each tick. It ends on rows in the table. Only a destination
6
+ that reports ``not_supported`` can end on a healthy freshness verdict: there, no row count exists.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import shlex
12
+ import time
13
+ from dataclasses import replace
14
+ from typing import TYPE_CHECKING, Any
15
+
16
+ import click
17
+
18
+ from databay.cli.common import common_options, console_for
19
+ from databay.operation import Condition, Wait
20
+ from databay.settings import SETTINGS
21
+
22
+ if TYPE_CHECKING:
23
+ from collections.abc import Callable
24
+
25
+ from databay.cli.console import Console
26
+ from databay.invoke import Session
27
+ from databay.waits import Fetch, Reading, WaitOutcome
28
+
29
+ COMMAND = "databay pipe wait"
30
+
31
+ #: A pipe's freshness once it is active. ``not_landing_data`` fails in every freshness state and a
32
+ #: null value is pending: ``data_step`` applies both, because ``classify`` reads only one value.
33
+ #: Success ends the wait only for a table that cannot be measured; see ``_no_rows_yet``.
34
+ DATA_WAIT = Wait(
35
+ status_operation="pipe_get",
36
+ bind=(("pipe_uuid", "path:pipe_uuid"),),
37
+ field="/freshness_state",
38
+ pending=("never_checked", "behind"),
39
+ success=("fresh",),
40
+ failure=("stale", "not_monitored"),
41
+ error_field="/health_detail",
42
+ conditional=(
43
+ (
44
+ "unknown",
45
+ Condition(
46
+ field="/health",
47
+ pending=("unknown", "stale", "not_reporting", "not_monitored"),
48
+ success=("healthy",),
49
+ failure=("not_landing_data",),
50
+ ),
51
+ ),
52
+ ),
53
+ )
54
+
55
+ PIPE_WAIT_HELP = """Wait until a pipe is active, or until its table is ready to query.
56
+
57
+ \b
58
+ --until active ends when the pipe is provisioned; data may not have arrived.
59
+ --until data waits for active, then ends when the table holds rows
60
+ (rows_landed). A warehouse that cannot report a row count ends when the
61
+ pipe reports healthy (healthy_unmeasured). An empty table keeps waiting.
62
+ \b
63
+ A pipe that ends in another status reports that status. A pipe that is not
64
+ landing data, or whose stats fail for good, exits 5. Running out of time
65
+ exits 4, and `next` runs the same wait again.
66
+
67
+ \b
68
+ Examples:
69
+ databay pipe wait <pipe_uuid> --until active
70
+ databay pipe wait <pipe_uuid> --until data --timeout 540"""
71
+
72
+
73
+ def _summary(
74
+ pipe_uuid: str, pipe_status: str, answer: dict[str, Any], destination: dict[str, Any] | None
75
+ ) -> dict[str, Any]:
76
+ """What the wait prints: the fields of the two answers that decide it, and what ``next`` is built from."""
77
+ measured = destination or {}
78
+ return {
79
+ "pipe_uuid": pipe_uuid,
80
+ "pipe_status": pipe_status,
81
+ "last_error": answer.get("last_error"),
82
+ "freshness_state": answer.get("freshness_state"),
83
+ "health": answer.get("health"),
84
+ "health_detail": answer.get("health_detail"),
85
+ "destination_table_exists": measured.get("destination_table_exists"),
86
+ "stored_rows": measured.get("stored_rows"),
87
+ "unavailable_code": measured.get("unavailable_code"),
88
+ "sink_db_uuid": answer.get("sink_db_uuid"),
89
+ "dest_table_name": answer.get("dest_table_name"),
90
+ "project_id": answer.get("project_id"),
91
+ }
92
+
93
+
94
+ def _unavailable(destination: dict[str, Any], status: dict[str, Any]) -> Reading:
95
+ """A destination that could not be measured: retried when the server says a retry may help."""
96
+ from databay.errors import DatabayWaitError # noqa: PLC0415
97
+ from databay.waits import Reading # noqa: PLC0415
98
+
99
+ code = destination.get("unavailable_code")
100
+ state = f"stats_unavailable ({code})"
101
+ retriable = destination.get("retriable")
102
+ if retriable is True:
103
+ return Reading("pending", state, status, "pipe_destination_get")
104
+ if retriable is None:
105
+ stop = DatabayWaitError(
106
+ f"{COMMAND}: the destination reports stats_unavailable ({code}) without saying if a retry may help",
107
+ code="unexpected_wait_state",
108
+ details={"state": state},
109
+ )
110
+ return Reading("failure", state, status, "pipe_destination_get", stop)
111
+ message = destination.get("unavailable_message")
112
+ stop = DatabayWaitError(
113
+ f"{COMMAND}: the destination cannot be measured ({code})" + (f": {message}" if message else ""),
114
+ hint=destination.get("remediation"),
115
+ support_ref=destination.get("support_ref"),
116
+ details={"state": state, "unavailable_code": code, "error": message},
117
+ )
118
+ return Reading("failure", state, status, "pipe_destination_get", stop)
119
+
120
+
121
+ def _freshness(pipe: dict[str, Any], status: dict[str, Any]) -> Reading:
122
+ from databay.errors import DatabayWaitError # noqa: PLC0415
123
+ from databay.invoke import json_text # noqa: PLC0415
124
+ from databay.waits import Reading, read_status # noqa: PLC0415
125
+
126
+ freshness, health = pipe.get("freshness_state"), pipe.get("health")
127
+ if freshness is None or health is None:
128
+ state = f"freshness_state={json_text(freshness)} health={json_text(health)}"
129
+ return Reading("pending", state, status, DATA_WAIT.status_operation)
130
+ health_rule = DATA_WAIT.conditional[0][1]
131
+ if health not in (*health_rule.pending, *health_rule.success, *health_rule.failure):
132
+ stop = DatabayWaitError(
133
+ f"{COMMAND}: health is {health!r}, a state this CLI does not know",
134
+ code="unexpected_wait_state",
135
+ details={"state": health},
136
+ )
137
+ return Reading("failure", health, status, DATA_WAIT.status_operation, stop)
138
+ reading = read_status(DATA_WAIT, pipe, COMMAND)
139
+ reason = "healthy_unmeasured" if reading.outcome == "success" else None
140
+ return replace(reading, status=status, reason=reason)
141
+
142
+
143
+ def _no_rows_yet(pipe: dict[str, Any], destination: dict[str, Any], status: dict[str, Any]) -> Reading:
144
+ """A destination that can be measured and shows no rows: only a failure verdict ends the wait."""
145
+ from databay.invoke import json_text # noqa: PLC0415
146
+ from databay.waits import Reading # noqa: PLC0415
147
+
148
+ verdict = _freshness(pipe, status)
149
+ if verdict.outcome == "failure":
150
+ return verdict
151
+ exists = destination.get("destination_table_exists")
152
+ if exists is False:
153
+ missing = "the destination table does not exist yet"
154
+ elif exists and destination.get("stored_rows") == 0:
155
+ missing = "no rows have landed"
156
+ else:
157
+ missing = "the row count was not measured"
158
+ health = pipe.get("health")
159
+ state = f"healthy but {missing}" if health == "healthy" else f"{missing} (health={json_text(health)})"
160
+ return Reading("pending", state, status, "pipe_destination_get")
161
+
162
+
163
+ def _measured(pipe: dict[str, Any], destination: dict[str, Any], status: dict[str, Any]) -> Reading:
164
+ """What an active pipe's destination answer means."""
165
+ from databay.waits import Reading # noqa: PLC0415
166
+
167
+ if destination.get("stats_unavailable"):
168
+ if destination.get("unavailable_code") == "not_supported":
169
+ return _freshness(pipe, status)
170
+ return _unavailable(destination, status)
171
+ stored = destination.get("stored_rows")
172
+ if destination.get("destination_table_exists") and stored is not None and stored > 0:
173
+ return Reading("success", f"stored_rows={stored}", status, "pipe_destination_get", reason="rows_landed")
174
+ return _no_rows_yet(pipe, destination, status)
175
+
176
+
177
+ def data_step(pipe_uuid: str) -> Callable[[Fetch], Reading]:
178
+ """One tick of ``--until data``: the status route until the pipe is active, then its two routes."""
179
+ from databay.errors import DatabayWaitError # noqa: PLC0415
180
+ from databay.invoke import CallValues, build_request # noqa: PLC0415
181
+ from databay.operations.pipe import PIPE_GET, PIPE_STATUS # noqa: PLC0415
182
+ from databay.operations.pipe_destination import PIPE_DESTINATION_GET # noqa: PLC0415
183
+ from databay.waits import Reading, read_status # noqa: PLC0415
184
+
185
+ active_wait = PIPE_STATUS.resumes
186
+ assert active_wait is not None
187
+ pipe_status_wait = replace(active_wait, status_operation=PIPE_GET.operation_id, field="/status")
188
+ values = CallValues(path={"pipe_uuid": pipe_uuid})
189
+ status_request = build_request(PIPE_STATUS, values, None)
190
+ pipe_request = build_request(PIPE_GET, values, None)
191
+ destination_request = build_request(PIPE_DESTINATION_GET, values, None)
192
+ has_been_active = False
193
+
194
+ def step(fetch: Fetch) -> Reading:
195
+ nonlocal has_been_active
196
+ if not has_been_active:
197
+ answer = fetch(PIPE_STATUS, status_request)
198
+ reading = read_status(active_wait, answer, COMMAND)
199
+ if reading.outcome != "success":
200
+ return replace(reading, status=_summary(pipe_uuid, reading.state, answer, None))
201
+ has_been_active = True
202
+ pipe = fetch(PIPE_GET, pipe_request)
203
+ reading = read_status(pipe_status_wait, pipe, COMMAND)
204
+ if reading.outcome != "success":
205
+ return replace(reading, status=_summary(pipe_uuid, reading.state, pipe, None))
206
+ if pipe.get("health") == "not_landing_data":
207
+ status = _summary(pipe_uuid, reading.state, pipe, None)
208
+ detail = pipe.get("health_detail")
209
+ stop = DatabayWaitError(
210
+ f"{COMMAND}: the pipe is not landing data" + (f": {detail}" if detail else ""),
211
+ details={"state": "not_landing_data", "error": detail},
212
+ )
213
+ return Reading("failure", "not_landing_data", status, PIPE_GET.operation_id, stop)
214
+ destination = fetch(PIPE_DESTINATION_GET, destination_request)
215
+ return _measured(pipe, destination, _summary(pipe_uuid, reading.state, pipe, destination))
216
+
217
+ return step
218
+
219
+
220
+ def wait_for_data(
221
+ session: Session,
222
+ pipe_uuid: str,
223
+ *,
224
+ timeout_s: float | None,
225
+ on_state: Callable[[WaitOutcome], None] | None = None,
226
+ clock: Callable[[], float] = time.monotonic,
227
+ sleep: Callable[[float], None] = time.sleep,
228
+ ) -> WaitOutcome:
229
+ from databay import waits # noqa: PLC0415
230
+
231
+ return waits.poll(
232
+ session,
233
+ data_step(pipe_uuid),
234
+ command=COMMAND,
235
+ status_operation="pipe_status",
236
+ bound={"pipe_uuid": pipe_uuid},
237
+ limit=timeout_s if timeout_s is not None else session.transport.settings.wait.data_timeout_s,
238
+ on_state=on_state,
239
+ clock=clock,
240
+ sleep=sleep,
241
+ )
242
+
243
+
244
+ def _profile_words(profile: str | None) -> list[str]:
245
+ return ["--profile", profile] if profile is not None else []
246
+
247
+
248
+ def _resume(pipe_uuid: str, until: str, timeout: float | None, profile: str | None) -> str:
249
+ words = ["databay", "pipe", "wait", pipe_uuid, "--until", until]
250
+ if timeout is not None:
251
+ words += ["--timeout", f"{timeout:g}"]
252
+ return shlex.join(words + _profile_words(profile))
253
+
254
+
255
+ def _query_next(session: Session, console: Console, status: dict[str, Any], profile: str | None) -> str:
256
+ """
257
+ The ``sql`` command counting the table's rows, on the pipe's own warehouse.
258
+
259
+ The table is already ready, so a warehouse that cannot be read changes only ``next``: it becomes
260
+ the command that reads the name, and a ``warehouse_unread`` event says why. One attempt, because
261
+ the answer is not printed until this returns and ``--timeout`` may be spent.
262
+ """
263
+ from databay.errors import DatabayError # noqa: PLC0415
264
+ from databay.invoke import CallValues, build_request, decode, resolve_project # noqa: PLC0415
265
+ from databay.operations.warehouse import WAREHOUSE_GET # noqa: PLC0415
266
+ from databay.transport import error_from # noqa: PLC0415
267
+
268
+ sink = status["sink_db_uuid"]
269
+ project = status["project_id"]
270
+ flags = ["--project", project] if project is not None and project != session.active_project else []
271
+ flags += _profile_words(profile)
272
+ read_name = shlex.join(["databay", "warehouse", "get", sink, *flags])
273
+ call = CallValues(path={"db_uuid": sink}, project_explicit=project)
274
+ try:
275
+ request = build_request(WAREHOUSE_GET, call, resolve_project(WAREHOUSE_GET, call, session.active_project))
276
+ answer = session.transport.exchange(request, retries=False)
277
+ if answer.status >= 400: # noqa: PLR2004
278
+ raise error_from(answer, request)
279
+ database = decode(WAREHOUSE_GET, answer)[0].get("db_name")
280
+ except DatabayError as exc:
281
+ console.event("warehouse_unread", {"sink_db_uuid": sink, "code": exc.code})
282
+ return read_name
283
+ if database is None:
284
+ console.event("warehouse_unread", {"sink_db_uuid": sink, "code": "db_name_missing"})
285
+ return read_name
286
+ statement = f"select count(*) from {database}.{status['dest_table_name']}" # noqa: S608 — printed for the caller to run; `sql` runs it read-only
287
+ return shlex.join(["databay", "sql", "--db", sink, *flags, statement])
288
+
289
+
290
+ @click.command("wait", help=PIPE_WAIT_HELP, short_help="Wait until a pipe is active or its table is ready to query")
291
+ @click.argument("pipe_uuid", metavar="PIPE_UUID")
292
+ @click.option(
293
+ "--until",
294
+ "until",
295
+ required=True,
296
+ type=click.Choice(("active", "data")),
297
+ help="active: provisioned; data: the table is ready to query",
298
+ )
299
+ @click.option(
300
+ "--timeout",
301
+ "timeout",
302
+ type=click.FloatRange(min=0, min_open=True),
303
+ default=None,
304
+ metavar="S",
305
+ help=(
306
+ "seconds to wait before exit 4, resumable "
307
+ f"(default: {SETTINGS.wait.default_timeout_s:g} for active, {SETTINGS.wait.data_timeout_s:g} for data)"
308
+ ),
309
+ )
310
+ @common_options
311
+ @click.pass_context
312
+ def pipe_wait(
313
+ ctx: click.Context,
314
+ pipe_uuid: str,
315
+ until: str,
316
+ timeout: float | None,
317
+ **values: Any, # noqa: ANN401
318
+ ) -> None:
319
+ from databay import waits # noqa: PLC0415
320
+ from databay.cli.runner import open_session, report_wait # noqa: PLC0415
321
+ from databay.operations.pipe import PIPE_GET, PIPE_STATUS # noqa: PLC0415
322
+ from databay.operations.pipe_destination import PIPE_DESTINATION_GET # noqa: PLC0415
323
+
324
+ console, state = console_for(ctx, values)
325
+ session, profile = open_session(values, console, state)
326
+ state["context"] = {"server": profile.server, "profile": profile.name, "request_id": session.transport.request_id}
327
+ state["reading"] = True
328
+ explicit_profile = values.get("profile")
329
+ resume = _resume(pipe_uuid, until, timeout, explicit_profile)
330
+ active_wait = PIPE_STATUS.resumes
331
+ assert active_wait is not None
332
+
333
+ def run_active(on_state: Callable[[WaitOutcome], None]) -> WaitOutcome:
334
+ return waits.run(session, active_wait, {"pipe_uuid": pipe_uuid}, timeout_s=timeout, on_state=on_state)
335
+
336
+ def run_data(on_state: Callable[[WaitOutcome], None]) -> WaitOutcome:
337
+ return wait_for_data(session, pipe_uuid, timeout_s=timeout, on_state=on_state)
338
+
339
+ def active_next(_: WaitOutcome) -> str:
340
+ return _resume(pipe_uuid, "data", timeout, explicit_profile)
341
+
342
+ def data_next(outcome: WaitOutcome) -> str:
343
+ return _query_next(session, console, outcome.status, explicit_profile)
344
+
345
+ try:
346
+ if until == "active":
347
+ session.ensure_compatible(PIPE_STATUS)
348
+ columns, rows = PIPE_STATUS.columns, PIPE_STATUS.response.rows
349
+ report_wait(
350
+ run_active, console, {"result": None}, columns, rows, resume, state, from_status=True, then=active_next
351
+ )
352
+ return
353
+ for operation in (PIPE_STATUS, PIPE_GET, PIPE_DESTINATION_GET):
354
+ session.ensure_compatible(operation)
355
+ report_wait(run_data, console, {"result": None}, (), None, resume, state, from_status=True, then=data_next)
356
+ finally:
357
+ session.transport.close()
databay/cli/profile.py ADDED
@@ -0,0 +1,230 @@
1
+ """``databay profile use|list``, ``databay status`` and ``databay project use``: local state and who you are."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ import click
9
+
10
+ from databay.cli.common import common_options, console_for
11
+ from databay.cli.handwritten import HANDWRITTEN
12
+ from databay.settings import OUTPUT_FORMATS
13
+
14
+ if TYPE_CHECKING:
15
+ from pathlib import Path
16
+
17
+ from databay.agent_skill import Installed
18
+ from databay.cli.console import Console
19
+
20
+ PROFILE_COLUMNS = ("name", "current", "server", "auth", "user", "token_store", "active_project")
21
+
22
+
23
+ @click.group("profile", short_help="Switch between and list profiles")
24
+ def profile() -> None:
25
+ """Profiles in config.toml: each is one server, its sign-in and its active project."""
26
+
27
+
28
+ @profile.command("use")
29
+ @click.argument("name")
30
+ @click.option("--output", type=click.Choice(OUTPUT_FORMATS), default=None, help="json, table or csv")
31
+ @click.pass_context
32
+ def profile_use(ctx: click.Context, name: str, output: str | None) -> None:
33
+ """Make NAME the current profile."""
34
+ from databay.cli.output import emit # noqa: PLC0415
35
+ from databay.errors import DatabayProfileError # noqa: PLC0415
36
+ from databay.paths import config_file # noqa: PLC0415
37
+ from databay.profiles import edit_config # noqa: PLC0415
38
+
39
+ console, _ = console_for(ctx, {"output": output})
40
+
41
+ def change(data: dict[str, Any]) -> None:
42
+ if name not in data["profiles"]:
43
+ known = ", ".join(sorted(data["profiles"])) or "none"
44
+ raise DatabayProfileError(f"no profile {name!r} in {config_file()} (profiles: {known})")
45
+ data["current_profile"] = name
46
+
47
+ edit_config(change)
48
+ emit(console, {"result": {"current_profile": name}}, ())
49
+
50
+
51
+ @profile.command("list")
52
+ @click.option("--output", type=click.Choice(OUTPUT_FORMATS), default=None, help="json, table or csv")
53
+ @click.pass_context
54
+ def profile_list(ctx: click.Context, output: str | None) -> None:
55
+ """Every profile, and which one is current."""
56
+ from databay.cli.output import emit # noqa: PLC0415
57
+ from databay.credentials import key_sub # noqa: PLC0415
58
+ from databay.profiles import all_profiles # noqa: PLC0415
59
+
60
+ console, _ = console_for(ctx, {"output": output})
61
+ current, profiles = all_profiles()
62
+ rows = [
63
+ {
64
+ "name": p.name,
65
+ "current": p.name == current,
66
+ "server": p.server,
67
+ "auth": p.auth,
68
+ "user": key_sub(p.credential) if p.credential else None,
69
+ "token_store": p.token_store,
70
+ "active_project": p.active_project,
71
+ }
72
+ for p in profiles
73
+ ]
74
+ emit(console, {"result": rows}, PROFILE_COLUMNS)
75
+
76
+
77
+ @click.command("status", short_help=HANDWRITTEN["status"][2])
78
+ @common_options
79
+ @click.pass_context
80
+ def status(ctx: click.Context, **values: Any) -> None: # noqa: ANN401
81
+ """
82
+ Show the profile, the server, and who you are signed in as.
83
+
84
+ Prints the server's product and API version, the user, the org the token names, the active project,
85
+ which output rule applied, and every installed agent skill. It calls the server, so it also checks the
86
+ sign-in works. needs_org_choice is true when no org is active and you belong to several: run
87
+ `databay org switch`. skill.state is skill_outdated when an installed skill is from another CLI version.
88
+ """
89
+ from databay.cli.console import output_rule # noqa: PLC0415
90
+ from databay.cli.output import emit # noqa: PLC0415
91
+ from databay.cli.runner import open_session # noqa: PLC0415
92
+ from databay.invoke import CallValues # noqa: PLC0415
93
+ from databay.operations.me import ME_GET # noqa: PLC0415
94
+
95
+ console, state = console_for(ctx, values)
96
+ _, rule = output_rule(values.get("output"), sys.stdout)
97
+ session, chosen = open_session(values, console, state)
98
+ state["context"] = {"server": chosen.server, "profile": chosen.name, "project": chosen.active_project}
99
+ try:
100
+ me = session.call(ME_GET, CallValues(query={"include_orgs": True})).parsed
101
+ assert session.server is not None
102
+ info, _ = session.server
103
+ finally:
104
+ session.transport.close()
105
+ result = {
106
+ "profile": chosen.name,
107
+ "server": chosen.server,
108
+ "product": info.product,
109
+ "api_version": info.api_version,
110
+ "auth": chosen.auth,
111
+ "user": me.preferred_username or me.email,
112
+ "sub": me.sub,
113
+ "principal": str(me.principal),
114
+ "org": me.organization[0] if me.organization else None,
115
+ "needs_org_choice": me.needs_org_choice,
116
+ "active_project": chosen.active_project,
117
+ "output": {"format": console.output, "rule": rule},
118
+ "skill": _skill_status(console),
119
+ }
120
+ if me.needs_org_choice:
121
+ console.inform(
122
+ "org_choice_required",
123
+ {"next": "databay org list"},
124
+ "No organization is active: run databay org list, then databay org switch <organization>.",
125
+ )
126
+ emit(console, {"result": result}, ())
127
+
128
+
129
+ def _skill_status(console: Console) -> dict[str, Any]:
130
+ """
131
+ Every installed copy of the agent skill and whether it matches this CLI's version.
132
+
133
+ ``state`` is ``current``, ``skill_outdated`` (a copy records another version, or none) or
134
+ ``not_installed``; each outdated copy is also a ``skill_outdated`` notice on stderr.
135
+ """
136
+ from pathlib import Path # noqa: PLC0415
137
+
138
+ from databay.agent_skill import find_installed # noqa: PLC0415
139
+ from databay.version import CLIENT_VERSION # noqa: PLC0415
140
+
141
+ cwd = Path.cwd()
142
+ copies = []
143
+ for copy in find_installed(home=Path.home(), cwd=cwd):
144
+ is_outdated = copy.version != CLIENT_VERSION
145
+ copies.append(
146
+ {
147
+ "target": copy.target,
148
+ "scope": copy.scope,
149
+ "path": str(copy.path),
150
+ "version": copy.version,
151
+ "problem": copy.problem,
152
+ "outdated": is_outdated,
153
+ }
154
+ )
155
+ if is_outdated:
156
+ command = _reinstall_command(copy, cwd)
157
+ console.inform(
158
+ "skill_outdated",
159
+ {
160
+ "path": str(copy.path),
161
+ "installed_version": copy.version,
162
+ "problem": copy.problem,
163
+ "cli_version": CLIENT_VERSION,
164
+ "next": command,
165
+ },
166
+ f"skill_outdated: {copy.path} is for CLI {copy.version or f'an unknown version ({copy.problem})'}, "
167
+ f"this CLI is {CLIENT_VERSION}; run {command}",
168
+ )
169
+ if not copies:
170
+ state = "not_installed"
171
+ elif any(c["outdated"] for c in copies):
172
+ state = "skill_outdated"
173
+ else:
174
+ state = "current"
175
+ return {"state": state, "cli_version": CLIENT_VERSION, "installed": copies}
176
+
177
+
178
+ def _reinstall_command(copy: Installed, cwd: Path) -> str:
179
+ """The command that replaces ``copy``; a project copy above ``cwd`` is reinstalled from its own folder."""
180
+ import shlex # noqa: PLC0415
181
+
182
+ command = f"databay skill install --target {copy.target} --scope {copy.scope} --force"
183
+ project = copy.path.parents[2]
184
+ if copy.scope == "project" and project != cwd:
185
+ return f"cd {shlex.quote(str(project))} && {command}"
186
+ return command
187
+
188
+
189
+ @click.command("use", short_help="Set the profile's active project")
190
+ @click.argument("project_id")
191
+ @common_options
192
+ @click.pass_context
193
+ def project_use(ctx: click.Context, project_id: str, **values: Any) -> None: # noqa: ANN401
194
+ """
195
+ Make PROJECT_ID the active project of the profile, after checking it is one you can see.
196
+
197
+ Ids only: `databay project list` shows names. A project being deleted is refused like one you cannot see.
198
+ """
199
+ from databay.cli.output import emit # noqa: PLC0415
200
+ from databay.cli.runner import open_session # noqa: PLC0415
201
+ from databay.errors import api_error # noqa: PLC0415
202
+ from databay.invoke import CallValues # noqa: PLC0415
203
+ from databay.operations.project import PROJECT_LIST # noqa: PLC0415
204
+ from databay.profiles import edit_config # noqa: PLC0415
205
+
206
+ console, state = console_for(ctx, values)
207
+ session, chosen = open_session(values, console, state)
208
+ state["context"] = {"server": chosen.server, "profile": chosen.name, "project": project_id}
209
+ try:
210
+ visible = session.call(PROJECT_LIST, CallValues()).parsed
211
+ finally:
212
+ session.transport.close()
213
+ match = next((p for p in visible if p.id == project_id and p.status == "active"), None)
214
+ if match is None:
215
+ raise api_error(
216
+ "project_not_found",
217
+ f"project {project_id} is not an active project you can see on {chosen.server}",
218
+ status=404,
219
+ details={"project_id": project_id},
220
+ )
221
+
222
+ def change(data: dict[str, Any]) -> None:
223
+ data["profiles"][chosen.name]["active_project"] = project_id
224
+
225
+ edit_config(change)
226
+ emit(
227
+ console,
228
+ {"result": {"profile": chosen.name, "active_project": project_id, "name": match.name}, "project": project_id},
229
+ (),
230
+ )