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/errors.py ADDED
@@ -0,0 +1,222 @@
1
+ """
2
+ The SDK's exceptions: one per error code, each with its exit code and recovery.
3
+
4
+ A server answer's ``code`` becomes the generated exception for that code (``databay.error_codes``),
5
+ a subclass of ``DatabayApiError``. Answers that do not fit the error contract, and failures the
6
+ client finds itself, are the named classes below; ``databay.error_catalog`` holds their codes.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+ from typing import Any
13
+
14
+ from databay.error_catalog import LOCAL_CODES, server_exit
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class Recovery:
19
+ action: str
20
+ side_effects: str
21
+ max_retries: int | None
22
+
23
+
24
+ class DatabayError(Exception):
25
+ """
26
+ Every error the SDK raises. ``code`` is stable; ``exit_code`` is the CLI's exit for it.
27
+
28
+ ``details`` holds the extra top-level fields of a server error body (``revision``, ``quota``, …),
29
+ and, for a client error, the values that explain it (the version, the state).
30
+ """
31
+
32
+ code: str = "internal_error"
33
+
34
+ def __init__(
35
+ self,
36
+ message: str,
37
+ *,
38
+ code: str | None = None,
39
+ status: int | None = None,
40
+ hint: str | None = None,
41
+ next_command: str | None = None,
42
+ details: dict[str, Any] | None = None,
43
+ attempts: int | None = None,
44
+ support_ref: str | None = None,
45
+ docs: str | None = None,
46
+ ) -> None:
47
+ super().__init__(message)
48
+ if code is not None:
49
+ self.code = code
50
+ self.message = message
51
+ self.status = status
52
+ self._hint = hint
53
+ self.next_command = next_command
54
+ self.details = details or {}
55
+ self.attempts = attempts
56
+ self.support_ref = support_ref
57
+ self.docs = docs
58
+ #: An answer the server did give before this failure: a not-ready verdict's body, or the
59
+ #: answer of an operation whose wait then failed.
60
+ self.result: Any = None
61
+
62
+ @property
63
+ def exit_code(self) -> int:
64
+ return LOCAL_CODES[self.code].exit_code
65
+
66
+ @property
67
+ def recovery(self) -> Recovery:
68
+ local = LOCAL_CODES[self.code]
69
+ return Recovery(local.action, local.side_effects, local.max_retries)
70
+
71
+ @property
72
+ def hint(self) -> str:
73
+ return self._hint or LOCAL_CODES[self.code].hint
74
+
75
+ def to_dict(self) -> dict[str, Any]:
76
+ """The error object the CLI prints (``{"error": ...}``)."""
77
+ recovery = self.recovery
78
+ body: dict[str, Any] = {
79
+ "code": self.code,
80
+ "message": self.message,
81
+ "hint": self.hint,
82
+ "next": self.next_command,
83
+ "docs": self.docs,
84
+ "support_ref": self.support_ref,
85
+ "status": self.status,
86
+ "attempts": self.attempts,
87
+ "details": self.details,
88
+ "recovery": {
89
+ "action": recovery.action,
90
+ "side_effects": recovery.side_effects,
91
+ "max_retries": recovery.max_retries,
92
+ },
93
+ }
94
+ return body
95
+
96
+
97
+ class DatabayApiError(DatabayError):
98
+ """A refusal the server answered with a registered ``code``; the generated classes subclass it."""
99
+
100
+ def _info(self) -> Any: # noqa: ANN401 — an ErrorInfo from the generated registry
101
+ from databay.error_codes import REGISTRY # noqa: PLC0415 — loaded only on an error
102
+
103
+ return REGISTRY[self.code]
104
+
105
+ @property
106
+ def exit_code(self) -> int:
107
+ return server_exit(self.code, self._info().action)
108
+
109
+ @property
110
+ def recovery(self) -> Recovery:
111
+ info = self._info()
112
+ return Recovery(info.action, info.side_effects, info.max_retries)
113
+
114
+ @property
115
+ def hint(self) -> str:
116
+ return self._hint or self._info().hint
117
+
118
+
119
+ def api_error(code: str, message: str, **kwargs: Any) -> DatabayApiError: # noqa: ANN401
120
+ """The generated exception for a registered server ``code``; a code missing from the registry is a ``KeyError``."""
121
+ from databay.error_codes import EXCEPTIONS # noqa: PLC0415 — loaded only on an error
122
+
123
+ return EXCEPTIONS[code](message, **kwargs)
124
+
125
+
126
+ class DatabayUsageError(DatabayError):
127
+ """The command line or the SDK call is incomplete or inconsistent (exit 2)."""
128
+
129
+ code = "usage_invalid"
130
+
131
+
132
+ class DatabayProfileError(DatabayError):
133
+ """No profile, a broken config file, or a token the profile names is not there (exit 3)."""
134
+
135
+ code = "no_profile"
136
+
137
+
138
+ class DatabayCancelled(DatabayError):
139
+ code = "cancelled"
140
+
141
+
142
+ class DatabayUncodedError(DatabayError):
143
+ """A JSON error body without a ``code``: the server broke the error contract."""
144
+
145
+ code = "uncoded_error"
146
+
147
+
148
+ class DatabayFeatureDisabled(DatabayError):
149
+ """A gated route's feature is off for this caller; ``feature`` and ``source`` say which and why."""
150
+
151
+ code = "feature_disabled"
152
+
153
+ def __init__(self, message: str, *, feature: str, source: str, **kwargs: Any) -> None: # noqa: ANN401
154
+ from databay.feature_header import FEATURE_SOURCE_HINTS # noqa: PLC0415 — loaded only on an error
155
+
156
+ super().__init__(
157
+ message, hint=FEATURE_SOURCE_HINTS[source], details={"feature": feature, "source": source}, **kwargs
158
+ )
159
+ self.feature = feature
160
+ self.source = source
161
+
162
+
163
+ class DatabayUnknownCodeError(DatabayError):
164
+ """A ``code`` this client version does not know: a newer server added it."""
165
+
166
+ code = "unknown_error_code"
167
+
168
+
169
+ class DatabayServerUnavailable(DatabayError):
170
+ """A non-JSON error body: a proxy or a maintenance page answered, not Databay."""
171
+
172
+ code = "server_unavailable"
173
+
174
+
175
+ class DatabayNetworkError(DatabayError):
176
+ code = "network_error"
177
+
178
+
179
+ class DatabayResponseInvalid(DatabayError):
180
+ """A 2xx body that does not fit the generated model; it names the field, never the body."""
181
+
182
+ code = "response_invalid"
183
+
184
+
185
+ class DatabayVersionError(DatabayError):
186
+ code = "server_version_mismatch"
187
+
188
+
189
+ class DatabayWaitError(DatabayError):
190
+ """
191
+ A wait that did not succeed.
192
+
193
+ Its code is ``wait_pending`` (timed out, resumable), ``wait_failed``, ``unexpected_wait_state``,
194
+ ``wait_target_gone`` or ``wait_feature_disabled``. ``outcome`` is the wait's last state, which the
195
+ CLI prints on stdout.
196
+ """
197
+
198
+ code = "wait_failed"
199
+
200
+ def __init__(self, message: str, *, outcome: Any = None, **kwargs: Any) -> None: # noqa: ANN401
201
+ super().__init__(message, **kwargs)
202
+ self.outcome = outcome
203
+
204
+
205
+ class DatabayNotReady(DatabayError):
206
+ """A verdict route answered a not-ready verdict; ``result`` is the whole answer."""
207
+
208
+ code = "not_ready"
209
+
210
+ def __init__(self, message: str, *, result: Any = None, **kwargs: Any) -> None: # noqa: ANN401
211
+ super().__init__(message, **kwargs)
212
+ self.result = result
213
+
214
+
215
+ @dataclass(frozen=True, slots=True)
216
+ class ErrorInfo:
217
+ """One server code's registry entry (``contract/error_codes.json``)."""
218
+
219
+ hint: str
220
+ action: str
221
+ side_effects: str
222
+ max_retries: int | None
@@ -0,0 +1,40 @@
1
+ """
2
+ The ``X-Databay-Feature`` header on the 404 a feature gate answers.
3
+
4
+ A gated feature that is off answers ``404 {"detail": "Not Found"}``, and an authenticated caller also
5
+ gets this header naming the feature and why it is off. Standard library only: the server's tests load
6
+ this file to check that the header the gate sends parses here.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ from typing import NamedTuple
13
+
14
+ FEATURE_HEADER = "x-databay-feature"
15
+
16
+ #: What the caller can do, per the documented ``source``. A source not listed here is not parsed.
17
+ FEATURE_SOURCE_HINTS: dict[str, str] = {
18
+ "plan": "Your organization's plan does not include this feature. Ask whoever runs this server about changing it.",
19
+ "override": "This feature was turned off for your organization. Ask whoever runs this server to turn it back on.",
20
+ "ceiling": "This server has the feature turned off for everyone on it. Ask whoever runs the server to turn it on.",
21
+ "prerequisite": "This feature needs another feature that is off. Ask whoever runs this server which one.",
22
+ "not_ready": "This feature is still being set up on this server. Ask whoever runs it when it will be ready.",
23
+ "refused": "This server turned the feature off because a check it runs did not pass. Ask whoever runs the server.",
24
+ "legacy": "No plan covers your account. Check that it belongs to an organization, or ask whoever runs the server.",
25
+ }
26
+
27
+ _FEATURE_OFF = re.compile(
28
+ rf"disabled; feature=(?P<feature>[a-z][a-z0-9_]*); source=(?P<source>{'|'.join(FEATURE_SOURCE_HINTS)})"
29
+ )
30
+
31
+
32
+ class FeatureOff(NamedTuple):
33
+ feature: str
34
+ source: str
35
+
36
+
37
+ def parse_feature_header(value: str | None) -> FeatureOff | None:
38
+ """The feature a header names, or ``None`` when it is absent or not the documented shape."""
39
+ match = _FEATURE_OFF.fullmatch(value) if value is not None else None
40
+ return FeatureOff(match["feature"], match["source"]) if match is not None else None
databay/invoke.py ADDED
@@ -0,0 +1,225 @@
1
+ """
2
+ Calling one operation: the project, the request, the answer's model, and the verdict.
3
+
4
+ ``Session`` is what the SDK resources and the CLI commands share. It checks the server once
5
+ (``databay.compat``), places the project where the operation takes it, sends the request and
6
+ validates the answer against the generated model before anyone reads it.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import importlib
12
+ from dataclasses import dataclass, field
13
+ from typing import TYPE_CHECKING, Any
14
+ from urllib.parse import quote
15
+
16
+ from databay import compat
17
+ from databay.errors import DatabayNotReady, DatabayResponseInvalid, DatabayUsageError, api_error
18
+ from databay.transport import IDEMPOTENCY_HEADER, PROJECT_HEADER, Request
19
+
20
+ if TYPE_CHECKING:
21
+ from databay.compat import ServerInfo
22
+ from databay.operation import Operation, Verdict
23
+ from databay.transport import Answer, Transport
24
+
25
+
26
+ @dataclass(slots=True)
27
+ class CallValues:
28
+ """
29
+ The caller's inputs to one operation, by wire name.
30
+
31
+ ``project_explicit`` is a project id the caller passed as the operation's own project
32
+ parameter; ``project_flag`` is ``--project`` (or the SDK's ``project=``).
33
+ """
34
+
35
+ path: dict[str, str] = field(default_factory=dict)
36
+ query: dict[str, Any] = field(default_factory=dict)
37
+ headers: dict[str, str] = field(default_factory=dict)
38
+ body: Any = None
39
+ has_body: bool = False
40
+ project_explicit: str | None = None
41
+ project_flag: str | None = None
42
+ idempotency_key: str | None = None
43
+
44
+
45
+ @dataclass(slots=True)
46
+ class Result:
47
+ raw: Any
48
+ parsed: Any
49
+ project: str | None
50
+ request_id: str
51
+ #: The path values the request was sent with, which a wait may bind.
52
+ path: dict[str, str]
53
+
54
+
55
+ def resolve_project(op: Operation, values: CallValues, active: str | None) -> str | None:
56
+ """
57
+ The project ``op`` is sent for.
58
+
59
+ An explicit id and ``--project`` that differ are ``project_conflict``; otherwise the explicit
60
+ id, then ``--project``, then the profile's active project. None of them is ``no_active_project``.
61
+ """
62
+ if op.project is None:
63
+ return None
64
+ explicit, flag = values.project_explicit, values.project_flag
65
+ if explicit and flag and explicit != flag:
66
+ raise api_error(
67
+ "project_conflict",
68
+ f"the project id argument {explicit} and --project {flag} differ",
69
+ details={"argument": explicit, "flag": flag},
70
+ )
71
+ chosen = explicit or flag or active
72
+ if not chosen:
73
+ raise DatabayUsageError(
74
+ f"`{op.command}` acts on a project: pass --project <project_id> or set the profile's active_project",
75
+ code="no_active_project",
76
+ )
77
+ return chosen
78
+
79
+
80
+ def pointer(document: Any, path: str) -> Any: # noqa: ANN401 — any JSON value
81
+ """The value at a JSON pointer (``/a/b``); ``KeyError`` when a step is missing."""
82
+ value = document
83
+ for raw_step in path.lstrip("/").split("/"):
84
+ step = raw_step.replace("~1", "/").replace("~0", "~")
85
+ if isinstance(value, list):
86
+ value = value[int(step)]
87
+ elif isinstance(value, dict):
88
+ value = value[step]
89
+ else:
90
+ raise KeyError(path)
91
+ return value
92
+
93
+
94
+ def json_text(value: Any) -> str: # noqa: ANN401
95
+ """How a declaration spells a value: a string as is, anything else as JSON (``true``, ``null``, ``3``)."""
96
+ import json # noqa: PLC0415
97
+
98
+ return value if isinstance(value, str) else json.dumps(value)
99
+
100
+
101
+ def build_request(op: Operation, values: CallValues, project: str | None) -> Request:
102
+ path_values = dict(values.path)
103
+ query = dict(values.query)
104
+ headers = dict(values.headers)
105
+ body = values.body
106
+ location = op.project_location
107
+ if location is not None and project is not None:
108
+ where, name = location
109
+ if where == "header":
110
+ headers[PROJECT_HEADER] = project
111
+ elif where == "path":
112
+ path_values[name] = project
113
+ elif where == "query":
114
+ query[name] = project
115
+ else:
116
+ body = {**(body or {}), name: project}
117
+ missing = [p.name for p in op.path_params if p.name not in path_values]
118
+ if missing:
119
+ raise DatabayUsageError(f"`{op.command}` needs {', '.join(missing)}")
120
+ path = op.path
121
+ for name, value in path_values.items():
122
+ path = path.replace("{" + name + "}", quote(str(value), safe=""))
123
+ if values.idempotency_key is not None:
124
+ headers[IDEMPOTENCY_HEADER] = values.idempotency_key
125
+ pairs = [
126
+ (name, json_text(item))
127
+ for name, value in query.items()
128
+ for item in (value if isinstance(value, list) else [value])
129
+ ]
130
+ return Request(
131
+ method=op.method.upper(),
132
+ path=path,
133
+ query=pairs,
134
+ headers=headers,
135
+ json_body=body,
136
+ has_body=body is not None or values.has_body,
137
+ secret_fields=op.secret_fields,
138
+ )
139
+
140
+
141
+ def model_type(reference: str) -> Any: # noqa: ANN401 — a model class or a type alias
142
+ module, _, name = reference.partition(":")
143
+ return getattr(importlib.import_module(f"databay.models.{module}"), name)
144
+
145
+
146
+ def validate(op: Operation, raw: Any, reference: str | None) -> Any: # noqa: ANN401
147
+ """``raw`` as the generated model, or ``response_invalid`` naming the field, never the body."""
148
+ if reference is None:
149
+ return raw
150
+ from pydantic import TypeAdapter, ValidationError # noqa: PLC0415
151
+
152
+ try:
153
+ return TypeAdapter(model_type(reference)).validate_python(raw)
154
+ except ValidationError as exc:
155
+ first = exc.errors(include_input=False, include_url=False)[0]
156
+ where = ".".join(str(part) for part in first["loc"]) or "(the body)"
157
+ raise DatabayResponseInvalid(
158
+ f"`{op.command}`: the answer does not fit {reference.partition(':')[2]} at {where} ({first['type']})",
159
+ details={"operation": op.operation_id, "field": where, "error": first["type"]},
160
+ ) from None
161
+
162
+
163
+ def decode(op: Operation, answer: Answer) -> tuple[Any, Any]:
164
+ """The answer as ``(raw JSON or text, validated value)``."""
165
+ kind = op.response.kind
166
+ if kind == "empty" or answer.status == 204: # noqa: PLR2004
167
+ return None, None
168
+ if kind == "text":
169
+ text = answer.text()
170
+ return text, text
171
+ try:
172
+ raw = answer.json()
173
+ except ValueError:
174
+ raise DatabayResponseInvalid(
175
+ f"`{op.command}`: HTTP {answer.status} answered a body that is not JSON",
176
+ details={"operation": op.operation_id},
177
+ ) from None
178
+ return raw, validate(op, raw, op.response.model)
179
+
180
+
181
+ def check_verdict(op: Operation, verdict: Verdict, raw: Any) -> None: # noqa: ANN401
182
+ try:
183
+ value = json_text(pointer(raw, verdict.field))
184
+ except (KeyError, IndexError, ValueError):
185
+ raise DatabayResponseInvalid(
186
+ f"`{op.command}`: the answer has no {verdict.field}", details={"operation": op.operation_id}
187
+ ) from None
188
+ if value in verdict.ok:
189
+ return
190
+ if value in verdict.not_ready:
191
+ raise DatabayNotReady(
192
+ f"`{op.command}`: {verdict.field.lstrip('/')} is {value}", result=raw, details={"value": value}
193
+ )
194
+ raise DatabayResponseInvalid(
195
+ f"`{op.command}`: {verdict.field.lstrip('/')} is {value!r}, which this CLI does not classify; upgrade it",
196
+ details={"operation": op.operation_id, "value": value},
197
+ )
198
+
199
+
200
+ @dataclass
201
+ class Session:
202
+ """One caller on one server: a transport, the profile's active project and the server's version."""
203
+
204
+ transport: Transport
205
+ active_project: str | None = None
206
+ profile: str | None = None
207
+ #: The server's client-config and whether it came from the cache, once read.
208
+ server: tuple[ServerInfo, bool] | None = None
209
+
210
+ def ensure_compatible(self, op: Operation) -> None:
211
+ self.server = compat.ensure_compatible(self.transport, self.server, op)
212
+
213
+ def call(self, op: Operation, values: CallValues, *, check_verdict_: bool = True) -> Result:
214
+ project = resolve_project(op, values, self.active_project)
215
+ request = build_request(op, values, project)
216
+ self.ensure_compatible(op)
217
+ answer = self.transport.send(request)
218
+ raw, parsed = decode(op, answer)
219
+ if check_verdict_ and op.verdict is not None:
220
+ check_verdict(op, op.verdict, raw)
221
+ path = {p.name: values.path[p.name] for p in op.path_params if p.name in values.path}
222
+ location = op.project_location
223
+ if location is not None and location[0] == "path" and project is not None:
224
+ path[location[1]] = project
225
+ return Result(raw=raw, parsed=parsed, project=project, request_id=answer.request_id, path=path)
databay/locks.py ADDED
@@ -0,0 +1,41 @@
1
+ """
2
+ Exclusive file locks between databay processes: one per credential, one for ``config.toml``.
3
+
4
+ ``fcntl.flock`` on ``<config dir>/locks/<name>.lock``. The kernel drops the lock when the process
5
+ exits, so a killed process never leaves a credential locked.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import fcntl
11
+ import hashlib
12
+ import os
13
+ from contextlib import contextmanager
14
+ from typing import TYPE_CHECKING
15
+
16
+ from databay.paths import config_dir
17
+
18
+ if TYPE_CHECKING:
19
+ from collections.abc import Iterator
20
+ from pathlib import Path
21
+
22
+ CONFIG_LOCK = "config"
23
+
24
+
25
+ def lock_path(name: str) -> Path:
26
+ """The lock file for ``name``; a credential key is hashed, since it holds a URL."""
27
+ stem = name if name == CONFIG_LOCK else hashlib.sha256(name.encode()).hexdigest()
28
+ return config_dir() / "locks" / f"{stem}.lock"
29
+
30
+
31
+ @contextmanager
32
+ def locked(name: str) -> Iterator[None]:
33
+ """Hold the exclusive lock ``name`` for the block; waits while another process holds it."""
34
+ path = lock_path(name)
35
+ path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
36
+ descriptor = os.open(path, os.O_RDWR | os.O_CREAT, 0o600)
37
+ try:
38
+ fcntl.flock(descriptor, fcntl.LOCK_EX)
39
+ yield
40
+ finally:
41
+ os.close(descriptor)