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/auth.py ADDED
@@ -0,0 +1,377 @@
1
+ """``databay login`` and ``databay logout``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ import click
10
+
11
+ from databay.cli.common import common_options, console_for, http_client, login_options
12
+ from databay.credentials import TOKEN_STORES
13
+ from databay.settings import DEFAULT_PROFILE, DEFAULT_TOKEN_STORE
14
+
15
+ if TYPE_CHECKING:
16
+ import httpx
17
+
18
+ from databay.cli.console import Console
19
+ from databay.credentials import Credential, TokenStore
20
+
21
+ LOGIN_HELP = f"""Sign in to a Databay server and save it as a profile.
22
+
23
+ \b
24
+ A person signs in in the browser (authorization code with PKCE, back to a
25
+ loopback port), or with --device by opening a link on any device. Over SSH
26
+ or without a display the device flow is used, and the CLI says so.
27
+ \b
28
+ An org API key signs in with --api-key-id and its secret from
29
+ --api-key-env NAME or --api-key-stdin; never as a flag value.
30
+ \b
31
+ A person who belongs to several organizations picks the active one with
32
+ --org KEY_OR_NAME; without it, the CLI asks on a terminal when none is
33
+ active yet, and fails with org_choice_required when it cannot ask.
34
+ \b
35
+ Tokens go to the OS keyring; --token-store file keeps them in a 0600 file
36
+ under the config dir instead. The profile (default: {DEFAULT_PROFILE}) becomes
37
+ the current one when there is none yet. Signing in again revokes the
38
+ refresh token it replaces."""
39
+
40
+
41
+ def _server(url: str) -> str:
42
+ from databay.errors import DatabayUsageError # noqa: PLC0415
43
+
44
+ if not url.startswith(("https://", "http://")):
45
+ raise DatabayUsageError(f"--server must be an http(s) URL, got {url!r}")
46
+ return url.rstrip("/")
47
+
48
+
49
+ def _api_key_secret(key_id: str | None, env_name: str | None, from_stdin: bool) -> str | None:
50
+ from databay.errors import DatabayUsageError # noqa: PLC0415
51
+
52
+ if key_id is None:
53
+ if env_name or from_stdin:
54
+ raise DatabayUsageError("--api-key-env and --api-key-stdin go with --api-key-id")
55
+ return None
56
+ if bool(env_name) == from_stdin:
57
+ raise DatabayUsageError(
58
+ "--api-key-id needs the key's secret from exactly one of --api-key-env NAME or --api-key-stdin",
59
+ code="secret_required",
60
+ )
61
+ if env_name:
62
+ secret = os.environ.get(env_name, "")
63
+ if not secret:
64
+ raise DatabayUsageError(
65
+ f"--api-key-env names ${env_name}, which is not set or empty", code="secret_required"
66
+ )
67
+ return secret
68
+ secret = sys.stdin.read().strip()
69
+ if not secret:
70
+ raise DatabayUsageError("--api-key-stdin: stdin was empty", code="secret_required")
71
+ return secret
72
+
73
+
74
+ def _previous(store: TokenStore, key: str, console: Console) -> Credential | None:
75
+ """What ``store`` holds under ``key`` before it is overwritten; an unreadable entry is reported and replaced."""
76
+ from databay.errors import DatabayProfileError # noqa: PLC0415
77
+
78
+ try:
79
+ return store.get(key)
80
+ except DatabayProfileError as exc:
81
+ if exc.code == "keyring_unavailable":
82
+ raise
83
+ console.event("credential_replaced", {"store": store.kind, "reason": exc.code})
84
+ return None
85
+
86
+
87
+ def revoke(http: httpx.Client, record: Credential) -> None:
88
+ """Revoke the refresh token of a person's credential at Keycloak; an API key's credential has none."""
89
+ from databay import oidc # noqa: PLC0415
90
+ from databay.errors import DatabayError # noqa: PLC0415
91
+
92
+ if record.refresh_token is None:
93
+ return
94
+ endpoints = oidc.discover(http, record.issuer)
95
+ if endpoints.revocation is None:
96
+ raise DatabayError(f"{record.issuer} names no revocation endpoint", code="revoke_failed")
97
+ oidc.revoke(http, endpoints.revocation, record.client_id, record.refresh_token)
98
+
99
+
100
+ def _revoke_superseded(http: httpx.Client, records: list[Credential], console: Console) -> int:
101
+ """Revoke refresh tokens a new sign-in replaced; a failure is reported, the sign-in still stands."""
102
+ from databay.errors import DatabayError # noqa: PLC0415
103
+
104
+ revoked = 0
105
+ for record in records:
106
+ try:
107
+ revoke(http, record)
108
+ except DatabayError as exc:
109
+ console.inform(
110
+ "revoke_failed",
111
+ {"sub": record.sub, "error": exc.code},
112
+ f"warning: could not revoke the replaced session ({exc.code}); "
113
+ "ask whoever runs the server to revoke it (docs/cli.md, Sign out)",
114
+ )
115
+ continue
116
+ revoked += record.refresh_token is not None
117
+ return revoked
118
+
119
+
120
+ def _save(name: str, server: str, record: Credential, store: TokenStore, console: Console) -> dict[str, Any]:
121
+ """
122
+ Store ``record`` and point profile ``name`` at it; return what the new sign-in replaced.
123
+
124
+ Every profile that names the same credential moves to ``store`` with it. The profile's previous
125
+ credential, when nothing names it any more, is deleted and listed for revocation.
126
+ """
127
+ from databay.credentials import credential_key, open_store # noqa: PLC0415
128
+ from databay.locks import locked # noqa: PLC0415
129
+ from databay.profiles import STORED_AUTH, all_profiles, edit_config # noqa: PLC0415
130
+
131
+ key = credential_key(server, record.sub)
132
+ _, profiles = all_profiles()
133
+ replaced: list[Credential] = []
134
+ with locked(key):
135
+ for kind in {p.token_store for p in profiles if p.credential == key} - {store.kind, None}:
136
+ other = open_store(kind)
137
+ previous = _previous(other, key, console)
138
+ if previous is not None:
139
+ replaced.append(previous)
140
+ other.delete(key)
141
+ previous = _previous(store, key, console)
142
+ if previous is not None:
143
+ replaced.append(previous)
144
+ store.put(key, record)
145
+
146
+ def change(data: dict[str, Any]) -> tuple[str | None, str | None, bool]:
147
+ entries = data["profiles"]
148
+ old = entries.get(name, {})
149
+ entry = {"server": server, "auth": record.kind, "credential": key, "token_store": store.kind}
150
+ if old.get("server") == server and old.get("active_project"):
151
+ entry["active_project"] = old["active_project"]
152
+ entries[name] = entry
153
+ for other in entries.values():
154
+ if other.get("credential") == key:
155
+ other["token_store"] = store.kind
156
+ became_current = data.get("current_profile") is None
157
+ if became_current:
158
+ data["current_profile"] = name
159
+ dropped = old.get("credential") if old.get("auth") in STORED_AUTH else None
160
+ if dropped == key or any(e.get("credential") == dropped for e in entries.values()):
161
+ dropped = None
162
+ return dropped, old.get("token_store"), became_current or data["current_profile"] == name
163
+
164
+ dropped, dropped_store, current = edit_config(change)
165
+ if dropped is not None and dropped_store is not None:
166
+ other = open_store(dropped_store)
167
+ with locked(dropped):
168
+ previous = _previous(other, dropped, console)
169
+ other.delete(dropped)
170
+ if previous is not None:
171
+ replaced.append(previous)
172
+ return {
173
+ "replaced": [r for r in replaced if r.refresh_token is not None and r.refresh_token != record.refresh_token],
174
+ "current": current,
175
+ }
176
+
177
+
178
+ def _choose_org(
179
+ name: str, wanted: str | None, values: dict[str, Any], console: Console, state: dict[str, Any]
180
+ ) -> str | None:
181
+ """
182
+ The organization the new sign-in acts in: ``wanted``, else the stored one, else the person's choice.
183
+
184
+ Runs after the credential is saved, so a refusal here leaves the profile signed in.
185
+ """
186
+ from databay.cli.org import ( # noqa: PLC0415
187
+ choose_interactively,
188
+ interactive,
189
+ list_orgs,
190
+ report_no_usable_org,
191
+ resolve_org,
192
+ switch_to,
193
+ )
194
+ from databay.cli.runner import session_for # noqa: PLC0415
195
+ from databay.errors import DatabayUsageError # noqa: PLC0415
196
+ from databay.profiles import load_profile # noqa: PLC0415
197
+ from databay.stored_token import stored_token # noqa: PLC0415
198
+
199
+ profile = load_profile(name)
200
+ # The credential just saved, even where $DATABAY_TOKEN is set: it is the sign-in this command made.
201
+ session = session_for(profile, stored_token(profile, state.get("http")), values, console, state)
202
+ try:
203
+ listing = list_orgs(session)
204
+ if wanted is not None:
205
+ chosen = resolve_org(listing, wanted, profile.server)
206
+ elif not listing.needs_org_choice:
207
+ if listing.active_org is None and listing.usable_orgs == 0:
208
+ report_no_usable_org(listing, profile.server, console)
209
+ return listing.active_org
210
+ elif interactive(console):
211
+ chosen = choose_interactively(listing, console)
212
+ else:
213
+ raise DatabayUsageError(
214
+ f"signed in to {profile.server}, but you belong to {len(listing.organizations)} organizations "
215
+ "and none is active; pass --org KEY_OR_NAME",
216
+ code="org_choice_required",
217
+ details={"organizations": [o.organization for o in listing.organizations]},
218
+ next_command="databay org list",
219
+ )
220
+ return switch_to(session, profile, chosen, console, state)["organization"]
221
+ finally:
222
+ session.transport.close()
223
+
224
+
225
+ @click.command("login", help=LOGIN_HELP, short_help="Sign in and save a profile")
226
+ @click.option("--server", required=True, help="the Databay server URL, e.g. https://databay.example.com")
227
+ @click.option("--device", is_flag=True, default=False, help="sign in with a link and code instead of the browser")
228
+ @click.option(
229
+ "--token-store",
230
+ type=click.Choice(TOKEN_STORES),
231
+ default=DEFAULT_TOKEN_STORE,
232
+ show_default=True,
233
+ help="where tokens are kept",
234
+ )
235
+ @click.option("--api-key-id", default=None, metavar="CLIENT_ID", help="sign in as an org API key (apikey-<org>-<id>)")
236
+ @click.option("--api-key-env", default=None, metavar="NAME", help="the variable that holds the API key's secret")
237
+ @click.option("--api-key-stdin", is_flag=True, default=False, help="read the API key's secret from stdin")
238
+ @click.option(
239
+ "--org", "org", default=None, metavar="KEY_OR_NAME", help="the organization to make active after signing in"
240
+ )
241
+ @login_options
242
+ @click.pass_context
243
+ def login(
244
+ ctx: click.Context,
245
+ server: str,
246
+ device: bool,
247
+ token_store: str,
248
+ api_key_id: str | None,
249
+ api_key_env: str | None,
250
+ api_key_stdin: bool,
251
+ org: str | None,
252
+ **values: Any, # noqa: ANN401
253
+ ) -> None:
254
+ from databay import compat, login_flow, oidc # noqa: PLC0415
255
+ from databay.cli.output import emit # noqa: PLC0415
256
+ from databay.credentials import open_store # noqa: PLC0415
257
+ from databay.errors import DatabayUsageError # noqa: PLC0415
258
+ from databay.profiles import check_name # noqa: PLC0415
259
+ from databay.tokens import NoToken # noqa: PLC0415
260
+ from databay.transport import Transport # noqa: PLC0415
261
+
262
+ console, state = console_for(ctx, values)
263
+ name = check_name(values.get("profile") or DEFAULT_PROFILE)
264
+ server = _server(server)
265
+ state["context"] = {"server": server, "profile": name}
266
+ if device and api_key_id:
267
+ raise DatabayUsageError("--device signs a person in; an API key signs in with --api-key-id alone")
268
+ if org is not None and api_key_id:
269
+ raise DatabayUsageError("--org chooses a person's organization; an API key belongs to one organization")
270
+ secret = _api_key_secret(api_key_id, api_key_env, api_key_stdin)
271
+ store = open_store(token_store)
272
+ store.ready()
273
+ with http_client(state) as http:
274
+ transport = Transport(
275
+ server=server,
276
+ tokens=NoToken(),
277
+ client_kind="cli",
278
+ retries=not values.get("no_retry"),
279
+ verbose=bool(values.get("verbose")),
280
+ events=console.event,
281
+ http=http,
282
+ )
283
+ info, _ = compat.server_info(transport, fresh=True)
284
+ compat.check_server(info)
285
+ endpoints = oidc.discover(http, info.issuer)
286
+ if secret is not None:
287
+ assert api_key_id is not None
288
+ signed = login_flow.api_key_flow(http, endpoints, api_key_id, secret)
289
+ client_id = api_key_id
290
+ else:
291
+ client_id = info.client_id
292
+ reason = (
293
+ None if device else login_flow.device_reason(dict(os.environ), sys.platform, login_flow.local_browser)
294
+ )
295
+ if reason is not None:
296
+ console.inform(
297
+ "device_login_chosen",
298
+ {"reason": reason},
299
+ f"{reason[0].upper()}{reason[1:]}; using device login. Pass --device to choose it directly.",
300
+ )
301
+ flow = login_flow.device_flow if device or reason else login_flow.browser_flow
302
+ signed = flow(http, endpoints, client_id, console)
303
+ record = login_flow.credential(server, info, signed, client_id, secret)
304
+ saved = _save(name, server, record, store, console)
305
+ revoked = _revoke_superseded(http, saved["replaced"], console)
306
+ chosen_org = None if secret is not None else _choose_org(name, org, values, console, state)
307
+ claims = oidc.claims(record.access_token)
308
+ result = {
309
+ "profile": name,
310
+ "server": server,
311
+ "auth": record.kind,
312
+ "user": claims.get("preferred_username"),
313
+ "sub": record.sub,
314
+ "token_store": store.kind,
315
+ "current": saved["current"],
316
+ "revoked_superseded": revoked,
317
+ "org": chosen_org,
318
+ }
319
+ next_command = "databay status" if saved["current"] else f"databay profile use {name}"
320
+ emit(console, {"result": result, "next": next_command}, ())
321
+
322
+
323
+ @click.command("logout", short_help="Sign out: revoke and delete the profile's credential")
324
+ @common_options
325
+ @click.pass_context
326
+ def logout(ctx: click.Context, **values: Any) -> None: # noqa: ANN401
327
+ """
328
+ Revoke the profile's refresh token at Keycloak, then delete its stored credential.
329
+
330
+ Every profile that named the credential loses it. When the revocation fails the credential is
331
+ still deleted here, and the command exits 1 with revoke_failed.
332
+ """
333
+ from databay.cli.output import emit # noqa: PLC0415
334
+ from databay.credentials import open_store # noqa: PLC0415
335
+ from databay.errors import DatabayError, DatabayUsageError # noqa: PLC0415
336
+ from databay.locks import locked # noqa: PLC0415
337
+ from databay.profiles import edit_config, load_profile # noqa: PLC0415
338
+
339
+ console, state = console_for(ctx, values)
340
+ profile = load_profile(values.get("profile"))
341
+ state["context"] = {"server": profile.server, "profile": profile.name}
342
+ if profile.auth == "env":
343
+ raise DatabayUsageError(f"profile {profile.name} reads $DATABAY_TOKEN and stores nothing to sign out of")
344
+ key = profile.credential
345
+ if key is None or profile.token_store is None:
346
+ emit(console, {"result": {"credential": None, "profiles": [], "revoked": False}}, ())
347
+ return
348
+ store = open_store(profile.token_store)
349
+ failure: DatabayError | None = None
350
+ with locked(key):
351
+ record = _previous(store, key, console)
352
+ if record is not None:
353
+ with http_client(state) as http:
354
+ try:
355
+ revoke(http, record)
356
+ except DatabayError as exc:
357
+ failure = exc
358
+ store.delete(key)
359
+
360
+ def change(data: dict[str, Any]) -> list[str]:
361
+ cleared = []
362
+ for other_name, entry in data["profiles"].items():
363
+ if entry.get("credential") == key:
364
+ entry.pop("credential")
365
+ entry.pop("token_store", None)
366
+ cleared.append(other_name)
367
+ return sorted(cleared)
368
+
369
+ cleared = edit_config(change)
370
+ revoked = failure is None and record is not None and record.refresh_token is not None
371
+ emit(console, {"result": {"credential": key, "profiles": cleared, "revoked": revoked}}, ())
372
+ if failure is not None:
373
+ raise DatabayError(
374
+ f"signed out locally, but Keycloak refused to revoke the session: {failure.message}",
375
+ code="revoke_failed",
376
+ details={"cause": failure.code, "profiles": cleared},
377
+ )
databay/cli/build.py ADDED
@@ -0,0 +1,89 @@
1
+ """
2
+ Click commands from generated ``Operation`` data.
3
+
4
+ Each operation becomes ``databay <noun> <verb>``; a discriminated body, or a body's discriminated
5
+ property, becomes one subcommand per variant (``databay source precheck postgres``, ``databay source
6
+ create postgres``). The flags come from ``databay.cli.flags`` and the
7
+ help text from ``databay.reference``, the same functions ``commands.md`` is rendered with.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import TYPE_CHECKING, Any
13
+
14
+ import click
15
+
16
+ from databay.cli.flags import Flag, command_flags
17
+ from databay.reference import click_help
18
+
19
+ if TYPE_CHECKING:
20
+ from databay.operation import Operation
21
+
22
+ CLICK_TYPES: dict[str, Any] = {"integer": click.INT, "number": click.FLOAT, "string": click.STRING}
23
+
24
+
25
+ def _click_type(flag: Flag) -> Any: # noqa: ANN401 — a click ParamType
26
+ if flag.choices:
27
+ return click.Choice(flag.choices)
28
+ if flag.role == "timeout":
29
+ return click.FloatRange(min=0, min_open=True)
30
+ param = flag.param
31
+ if param is None:
32
+ return click.STRING
33
+ kind = param.item_kind if param.kind == "list" else param.kind
34
+ return CLICK_TYPES.get(kind or "string", click.STRING)
35
+
36
+
37
+ def click_parameter(flag: Flag) -> click.Parameter:
38
+ if flag.takes == "argument":
39
+ return click.Argument([flag.dest], required=flag.required, type=_click_type(flag), metavar=flag.name)
40
+ help_text = flag.help + (" [required]" if flag.required else "")
41
+ if flag.takes == "switch":
42
+ return click.Option([flag.name, flag.dest], is_flag=True, default=False, help=help_text)
43
+ if flag.takes == "bool":
44
+ negative = "--no-" + flag.name.removeprefix("--")
45
+ return click.Option([f"{flag.name}/{negative}", flag.dest], default=None, help=help_text)
46
+ return click.Option(
47
+ [flag.name, flag.dest],
48
+ type=_click_type(flag),
49
+ multiple=flag.takes == "multiple",
50
+ default=None,
51
+ help=help_text,
52
+ metavar=flag.metavar,
53
+ )
54
+
55
+
56
+ def operation_command(op: Operation, variant: str | None = None) -> click.Command:
57
+ flags = command_flags(op, variant)
58
+
59
+ @click.pass_context
60
+ def callback(ctx: click.Context, **values: Any) -> None: # noqa: ANN401
61
+ from databay.cli.runner import run # noqa: PLC0415 — the runner loads httpx and pydantic
62
+
63
+ run(ctx, op, variant, flags, values)
64
+
65
+ name = variant if variant is not None else op.verb
66
+ return click.Command(
67
+ name,
68
+ callback=callback,
69
+ params=[click_parameter(flag) for flag in flags],
70
+ help=click_help(op, variant),
71
+ short_help=op.summary,
72
+ no_args_is_help=False,
73
+ )
74
+
75
+
76
+ def verb_command(op: Operation) -> click.Command:
77
+ if op.body is None or op.body.discriminator is None:
78
+ return operation_command(op)
79
+ group = click.Group(op.verb, help=click_help(op), short_help=op.summary)
80
+ for key, _, _ in op.body.variants:
81
+ group.add_command(operation_command(op, key))
82
+ return group
83
+
84
+
85
+ def noun_group(noun: str, operations: tuple[Operation, ...]) -> click.Group:
86
+ group = click.Group(noun, help=f"The {noun} commands.")
87
+ for op in operations:
88
+ group.add_command(verb_command(op))
89
+ return group
File without changes
@@ -0,0 +1,19 @@
1
+ """``databay alerting-webhook``: imported only when that noun runs.
2
+
3
+ Generated by python/tools/gen_client.py from contract/core.json; do not edit.
4
+ """
5
+
6
+ from databay.cli.build import noun_group
7
+ from databay.operations import alerting_webhook as _ops
8
+
9
+ group = noun_group(
10
+ 'alerting-webhook',
11
+ (
12
+ _ops.ALERTING_WEBHOOK_CREATE,
13
+ _ops.ALERTING_WEBHOOK_DELETE,
14
+ _ops.ALERTING_WEBHOOK_GET,
15
+ _ops.ALERTING_WEBHOOK_LIST,
16
+ _ops.ALERTING_WEBHOOK_NOTIFY,
17
+ _ops.ALERTING_WEBHOOK_UPDATE,
18
+ ),
19
+ )
@@ -0,0 +1,14 @@
1
+ """``databay alerting-webhook-tag``: imported only when that noun runs.
2
+
3
+ Generated by python/tools/gen_client.py from contract/core.json; do not edit.
4
+ """
5
+
6
+ from databay.cli.build import noun_group
7
+ from databay.operations import alerting_webhook_tag as _ops
8
+
9
+ group = noun_group(
10
+ 'alerting-webhook-tag',
11
+ (
12
+ _ops.ALERTING_WEBHOOK_TAG_LIST,
13
+ ),
14
+ )
@@ -0,0 +1,14 @@
1
+ """``databay alerting-webhook-type``: imported only when that noun runs.
2
+
3
+ Generated by python/tools/gen_client.py from contract/core.json; do not edit.
4
+ """
5
+
6
+ from databay.cli.build import noun_group
7
+ from databay.operations import alerting_webhook_type as _ops
8
+
9
+ group = noun_group(
10
+ 'alerting-webhook-type',
11
+ (
12
+ _ops.ALERTING_WEBHOOK_TYPE_LIST,
13
+ ),
14
+ )
@@ -0,0 +1,15 @@
1
+ """``databay alerting-webhook-undelivered``: imported only when that noun runs.
2
+
3
+ Generated by python/tools/gen_client.py from contract/core.json; do not edit.
4
+ """
5
+
6
+ from databay.cli.build import noun_group
7
+ from databay.operations import alerting_webhook_undelivered as _ops
8
+
9
+ group = noun_group(
10
+ 'alerting-webhook-undelivered',
11
+ (
12
+ _ops.ALERTING_WEBHOOK_UNDELIVERED_LIST,
13
+ _ops.ALERTING_WEBHOOK_UNDELIVERED_REPLAY,
14
+ ),
15
+ )
@@ -0,0 +1,17 @@
1
+ """``databay announcement``: imported only when that noun runs.
2
+
3
+ Generated by python/tools/gen_client.py from contract/core.json; do not edit.
4
+ """
5
+
6
+ from databay.cli.build import noun_group
7
+ from databay.operations import announcement as _ops
8
+
9
+ group = noun_group(
10
+ 'announcement',
11
+ (
12
+ _ops.ANNOUNCEMENT_LIST,
13
+ _ops.ANNOUNCEMENT_MARK_ALL_READ,
14
+ _ops.ANNOUNCEMENT_MARK_READ,
15
+ _ops.ANNOUNCEMENT_MARK_UNREAD,
16
+ ),
17
+ )
@@ -0,0 +1,18 @@
1
+ """``databay api-key``: imported only when that noun runs.
2
+
3
+ Generated by python/tools/gen_client.py from contract/core.json; do not edit.
4
+ """
5
+
6
+ from databay.cli.build import noun_group
7
+ from databay.operations import api_key as _ops
8
+
9
+ group = noun_group(
10
+ 'api-key',
11
+ (
12
+ _ops.API_KEY_CREATE,
13
+ _ops.API_KEY_GET,
14
+ _ops.API_KEY_LIST,
15
+ _ops.API_KEY_REVOKE,
16
+ _ops.API_KEY_ROTATE,
17
+ ),
18
+ )
@@ -0,0 +1,16 @@
1
+ """``databay api-key-project``: imported only when that noun runs.
2
+
3
+ Generated by python/tools/gen_client.py from contract/core.json; do not edit.
4
+ """
5
+
6
+ from databay.cli.build import noun_group
7
+ from databay.operations import api_key_project as _ops
8
+
9
+ group = noun_group(
10
+ 'api-key-project',
11
+ (
12
+ _ops.API_KEY_PROJECT_ADD,
13
+ _ops.API_KEY_PROJECT_BI_CONNECTION,
14
+ _ops.API_KEY_PROJECT_REMOVE,
15
+ ),
16
+ )
@@ -0,0 +1,14 @@
1
+ """``databay backfill-reason-code``: imported only when that noun runs.
2
+
3
+ Generated by python/tools/gen_client.py from contract/core.json; do not edit.
4
+ """
5
+
6
+ from databay.cli.build import noun_group
7
+ from databay.operations import backfill_reason_code as _ops
8
+
9
+ group = noun_group(
10
+ 'backfill-reason-code',
11
+ (
12
+ _ops.BACKFILL_REASON_CODE_LIST,
13
+ ),
14
+ )
@@ -0,0 +1,17 @@
1
+ """``databay backfill-run``: imported only when that noun runs.
2
+
3
+ Generated by python/tools/gen_client.py from contract/core.json; do not edit.
4
+ """
5
+
6
+ from databay.cli.build import noun_group
7
+ from databay.operations import backfill_run as _ops
8
+
9
+ group = noun_group(
10
+ 'backfill-run',
11
+ (
12
+ _ops.BACKFILL_RUN_CANCEL,
13
+ _ops.BACKFILL_RUN_CREATE,
14
+ _ops.BACKFILL_RUN_GET,
15
+ _ops.BACKFILL_RUN_LIST,
16
+ ),
17
+ )
@@ -0,0 +1,16 @@
1
+ """``databay canvas``: imported only when that noun runs.
2
+
3
+ Generated by python/tools/gen_client.py from contract/core.json; do not edit.
4
+ """
5
+
6
+ from databay.cli.build import noun_group
7
+ from databay.operations import canvas as _ops
8
+
9
+ group = noun_group(
10
+ 'canvas',
11
+ (
12
+ _ops.CANVAS_ACTIVATE,
13
+ _ops.CANVAS_LIST,
14
+ _ops.CANVAS_VOLUMES,
15
+ ),
16
+ )
@@ -0,0 +1,15 @@
1
+ """``databay canvas-graph``: imported only when that noun runs.
2
+
3
+ Generated by python/tools/gen_client.py from contract/core.json; do not edit.
4
+ """
5
+
6
+ from databay.cli.build import noun_group
7
+ from databay.operations import canvas_graph as _ops
8
+
9
+ group = noun_group(
10
+ 'canvas-graph',
11
+ (
12
+ _ops.CANVAS_GRAPH_GET,
13
+ _ops.CANVAS_GRAPH_PUT,
14
+ ),
15
+ )
@@ -0,0 +1,15 @@
1
+ """``databay canvas-table-check``: imported only when that noun runs.
2
+
3
+ Generated by python/tools/gen_client.py from contract/core.json; do not edit.
4
+ """
5
+
6
+ from databay.cli.build import noun_group
7
+ from databay.operations import canvas_table_check as _ops
8
+
9
+ group = noun_group(
10
+ 'canvas-table-check',
11
+ (
12
+ _ops.CANVAS_TABLE_CHECK_GET,
13
+ _ops.CANVAS_TABLE_CHECK_START,
14
+ ),
15
+ )