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/oidc.py ADDED
@@ -0,0 +1,205 @@
1
+ """
2
+ The OAuth requests the CLI sends to the identity provider (Keycloak): discovery, token grants, revocation.
3
+
4
+ Every grant answer is checked the same way: a 2xx with ``access_token`` and ``expires_in``, or an
5
+ OAuth error (``{"error": ...}``) raised as ``OAuthRefusal`` naming it. Tokens never appear in an
6
+ error message or an event.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import base64
12
+ import hashlib
13
+ import json
14
+ import secrets
15
+ import time
16
+ from dataclasses import dataclass
17
+ from typing import TYPE_CHECKING, Any
18
+
19
+ from databay.errors import DatabayError, DatabayNetworkError, DatabayResponseInvalid, DatabayServerUnavailable
20
+
21
+ if TYPE_CHECKING:
22
+ import httpx
23
+
24
+ DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code"
25
+ LOGIN_SCOPE = "openid offline_access"
26
+ OFFLINE_SCOPE = "offline_access"
27
+
28
+
29
+ class OAuthRefusal(DatabayError):
30
+ """The provider answered an OAuth error; ``error`` is its ``error`` field (``invalid_grant``, ``slow_down``, …)."""
31
+
32
+ code = "login_failed"
33
+
34
+ def __init__(self, message: str, *, error: str, **kwargs: Any) -> None: # noqa: ANN401
35
+ super().__init__(message, **kwargs)
36
+ self.error = error
37
+
38
+
39
+ @dataclass(frozen=True, slots=True)
40
+ class Endpoints:
41
+ issuer: str
42
+ authorization: str
43
+ token: str
44
+ device: str
45
+ #: ``None`` when the provider advertises no revocation endpoint.
46
+ revocation: str | None
47
+
48
+
49
+ @dataclass(frozen=True, slots=True)
50
+ class Grant:
51
+ access_token: str
52
+ expires_at: float
53
+ refresh_token: str | None
54
+ scope: tuple[str, ...]
55
+
56
+
57
+ def pkce() -> tuple[str, str]:
58
+ """A code verifier and its S256 challenge."""
59
+ verifier = secrets.token_urlsafe(64)
60
+ digest = hashlib.sha256(verifier.encode()).digest()
61
+ return verifier, base64.urlsafe_b64encode(digest).rstrip(b"=").decode()
62
+
63
+
64
+ def claims(jwt: str) -> dict[str, Any]:
65
+ """
66
+ The payload of a JWT the token endpoint just issued to this client.
67
+
68
+ Read, not verified: the token came straight from the provider over the connection the CLI
69
+ opened, and the server checks every token it is sent.
70
+ """
71
+ try:
72
+ payload = jwt.split(".")[1]
73
+ data = json.loads(base64.urlsafe_b64decode(payload + "=" * (-len(payload) % 4)))
74
+ except (IndexError, ValueError) as exc:
75
+ raise DatabayResponseInvalid("the access token the provider issued is not a JWT") from exc
76
+ if not isinstance(data, dict) or not isinstance(data.get("sub"), str):
77
+ raise DatabayResponseInvalid("the access token the provider issued has no sub")
78
+ return data
79
+
80
+
81
+ def _post(http: httpx.Client, url: str, data: dict[str, str]) -> httpx.Response:
82
+ import httpx # noqa: PLC0415
83
+
84
+ try:
85
+ return http.post(url, data=data, headers={"Accept": "application/json"})
86
+ except httpx.TransportError as exc:
87
+ raise DatabayNetworkError(f"POST {url}: could not reach the identity provider ({type(exc).__name__})") from exc
88
+
89
+
90
+ def _transient(status: int) -> bool:
91
+ return status >= 500 or status == 429 # noqa: PLR2004
92
+
93
+
94
+ def _json(response: httpx.Response, where: str) -> dict[str, Any]:
95
+ try:
96
+ body = response.json()
97
+ except ValueError:
98
+ body = None
99
+ if not isinstance(body, dict):
100
+ if _transient(response.status_code):
101
+ raise DatabayServerUnavailable(f"{where}: HTTP {response.status_code} from the identity provider")
102
+ raise DatabayResponseInvalid(f"{where}: HTTP {response.status_code} without a JSON object")
103
+ return body
104
+
105
+
106
+ def _refusal(response: httpx.Response, body: dict[str, Any], where: str) -> DatabayError:
107
+ """The error a non-200 answer is: transient for a 5xx or 429 whatever its body, else the OAuth error it names."""
108
+ error = body.get("error")
109
+ if _transient(response.status_code):
110
+ return DatabayServerUnavailable(
111
+ f"{where}: HTTP {response.status_code} from the identity provider"
112
+ + (f" ({error})" if isinstance(error, str) else ""),
113
+ status=response.status_code,
114
+ )
115
+ if not isinstance(error, str):
116
+ raise DatabayResponseInvalid(f"{where}: HTTP {response.status_code} without an OAuth error")
117
+ description = body.get("error_description")
118
+ text = f"{where}: the identity provider answered {error}" + (
119
+ f" ({description})" if isinstance(description, str) else ""
120
+ )
121
+ return OAuthRefusal(text, error=error, status=response.status_code)
122
+
123
+
124
+ def discover(http: httpx.Client, issuer: str) -> Endpoints:
125
+ """The issuer's endpoints from its OpenID discovery document."""
126
+ import httpx # noqa: PLC0415
127
+
128
+ url = f"{issuer}/.well-known/openid-configuration"
129
+ try:
130
+ response = http.get(url, headers={"Accept": "application/json"})
131
+ except httpx.TransportError as exc:
132
+ raise DatabayNetworkError(f"GET {url}: could not reach the identity provider ({type(exc).__name__})") from exc
133
+ if response.status_code != 200: # noqa: PLR2004
134
+ raise DatabayServerUnavailable(f"GET {url}: HTTP {response.status_code}")
135
+ body = _json(response, f"GET {url}")
136
+ required = ("issuer", "authorization_endpoint", "token_endpoint", "device_authorization_endpoint")
137
+ missing = [name for name in required if not isinstance(body.get(name), str)]
138
+ if missing:
139
+ raise DatabayResponseInvalid(f"GET {url}: the discovery document has no {missing}")
140
+ if body["issuer"] != issuer:
141
+ raise DatabayResponseInvalid(
142
+ f"GET {url}: the discovery document names issuer {body['issuer']}, not {issuer}",
143
+ details={"issuer": body["issuer"], "expected": issuer},
144
+ )
145
+ revocation = body.get("revocation_endpoint")
146
+ return Endpoints(
147
+ issuer=issuer,
148
+ authorization=body["authorization_endpoint"],
149
+ token=body["token_endpoint"],
150
+ device=body["device_authorization_endpoint"],
151
+ revocation=revocation if isinstance(revocation, str) else None,
152
+ )
153
+
154
+
155
+ def grant(http: httpx.Client, token_endpoint: str, data: dict[str, str], *, now: float | None = None) -> Grant:
156
+ """POST one grant to the token endpoint; an OAuth error is ``OAuthRefusal``."""
157
+ issued_at = time.time() if now is None else now
158
+ where = f"{data['grant_type']} grant"
159
+ response = _post(http, token_endpoint, data)
160
+ body = _json(response, where)
161
+ if response.status_code != 200: # noqa: PLR2004
162
+ raise _refusal(response, body, where)
163
+ access, expires_in = body.get("access_token"), body.get("expires_in")
164
+ if not isinstance(access, str) or not isinstance(expires_in, int | float):
165
+ raise DatabayResponseInvalid(f"{where}: the answer has no access_token and expires_in")
166
+ refresh = body.get("refresh_token")
167
+ scope = body.get("scope")
168
+ return Grant(
169
+ access_token=access,
170
+ expires_at=issued_at + float(expires_in),
171
+ refresh_token=refresh if isinstance(refresh, str) else None,
172
+ scope=tuple(scope.split()) if isinstance(scope, str) else (),
173
+ )
174
+
175
+
176
+ def start_device(http: httpx.Client, endpoint: str, client_id: str, challenge: str) -> dict[str, Any]:
177
+ """Start a device authorization; the answer holds the user code and the verification links."""
178
+ where = "device authorization"
179
+ response = _post(
180
+ http,
181
+ endpoint,
182
+ {"client_id": client_id, "scope": LOGIN_SCOPE, "code_challenge": challenge, "code_challenge_method": "S256"},
183
+ )
184
+ body = _json(response, where)
185
+ if response.status_code != 200: # noqa: PLR2004
186
+ raise _refusal(response, body, where)
187
+ missing = [
188
+ name
189
+ for name, kind in (("device_code", str), ("user_code", str), ("verification_uri", str), ("expires_in", int))
190
+ if not isinstance(body.get(name), kind)
191
+ ]
192
+ if missing:
193
+ raise DatabayResponseInvalid(f"{where}: the answer has no {missing}")
194
+ return body
195
+
196
+
197
+ def revoke(http: httpx.Client, endpoint: str, client_id: str, refresh_token: str) -> None:
198
+ """Revoke ``refresh_token`` (RFC 7009); a refusal raises ``OAuthRefusal`` or a transport error."""
199
+ where = "token revocation"
200
+ response = _post(
201
+ http, endpoint, {"client_id": client_id, "token": refresh_token, "token_type_hint": "refresh_token"}
202
+ )
203
+ if response.status_code == 200: # noqa: PLR2004
204
+ return
205
+ raise _refusal(response, _json(response, where), where)
databay/operation.py ADDED
@@ -0,0 +1,191 @@
1
+ """
2
+ What one exposed API operation is to the SDK and CLI, as plain data.
3
+
4
+ The generated ``databay.operations.<noun>`` modules hold one ``Operation`` per route, built from the
5
+ route's ``x-databay-client`` declaration in ``contract/core.json``. Both the SDK resources and the
6
+ CLI commands read them, so the two never disagree on a path, a flag or a wait. This module imports
7
+ only the standard library: ``python/tools/gen_client.py`` builds these objects too.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass
13
+ from typing import Literal
14
+
15
+ Location = Literal["path", "query", "header", "body"]
16
+ #: How a value is read from the command line: ``json`` takes a JSON document (objects and arrays
17
+ #: of objects), ``list`` repeats the flag once per item.
18
+ ValueKind = Literal["string", "integer", "number", "boolean", "json", "list"]
19
+ ResponseKind = Literal["json", "text", "empty"]
20
+
21
+
22
+ @dataclass(frozen=True, slots=True)
23
+ class Param:
24
+ """One input of an operation: a path, query or header parameter, or a body field."""
25
+
26
+ name: str
27
+ location: Location
28
+ kind: ValueKind
29
+ required: bool
30
+ description: str = ""
31
+ #: Marked ``format: password`` in the spec: never a value flag, redacted in verbose logs.
32
+ secret: bool = False
33
+ #: The values an enum takes; for a ``list`` of an enum, the values of one item.
34
+ choices: tuple[str, ...] = ()
35
+ #: The kind of one item of a ``list``.
36
+ item_kind: ValueKind | None = None
37
+ #: The schema allows ``null``: the SDK may send ``None`` for it.
38
+ nullable: bool = False
39
+
40
+ @property
41
+ def flag(self) -> str:
42
+ """The CLI flag without dashes: the name in kebab case."""
43
+ return self.name.replace("_", "-").lower()
44
+
45
+ @property
46
+ def identifier(self) -> str:
47
+ """The Python identifier the SDK and the CLI callback use for it."""
48
+ return python_identifier(self.name)
49
+
50
+
51
+ @dataclass(frozen=True, slots=True)
52
+ class Body:
53
+ """
54
+ An operation's JSON body.
55
+
56
+ A plain body has ``fields``. A discriminated body has one variant per value of
57
+ ``discriminator``, each with its own fields; the CLI makes each variant a subcommand. With
58
+ ``variant_field`` set, the variants are that one property of a plain body: a variant's fields
59
+ and the discriminator nest under it on the wire, and ``fields`` are the properties beside it.
60
+ """
61
+
62
+ schema: str | None
63
+ fields: tuple[Param, ...] = ()
64
+ discriminator: str | None = None
65
+ variants: tuple[tuple[str, str, tuple[Param, ...]], ...] = ()
66
+ variant_field: str | None = None
67
+
68
+ def variant(self, key: str) -> tuple[str, tuple[Param, ...]]:
69
+ for variant_key, schema, fields in self.variants:
70
+ if variant_key == key:
71
+ return schema, fields
72
+ msg = f"{self.schema} has no variant {key!r}"
73
+ raise KeyError(msg)
74
+
75
+ def fields_of(self, variant: str | None) -> tuple[Param, ...]:
76
+ """Every field a command of ``variant`` takes as a flag: the body's own, then the variant's."""
77
+ return self.fields if variant is None else (*self.fields, *self.variant(variant)[1])
78
+
79
+
80
+ @dataclass(frozen=True, slots=True)
81
+ class Condition:
82
+ """How a status value that needs a second field is classified, by that field's value."""
83
+
84
+ field: str
85
+ pending: tuple[str, ...] = ()
86
+ success: tuple[str, ...] = ()
87
+ failure: tuple[str, ...] = ()
88
+
89
+
90
+ @dataclass(frozen=True, slots=True)
91
+ class Wait:
92
+ """
93
+ How the client waits on what an operation started, by polling an exposed status operation.
94
+
95
+ ``bind`` maps each status-operation parameter to a JSON pointer into this operation's response,
96
+ or to ``path:<name>``, one of this operation's own path parameters.
97
+ """
98
+
99
+ status_operation: str
100
+ bind: tuple[tuple[str, str], ...]
101
+ field: str
102
+ pending: tuple[str, ...]
103
+ success: tuple[str, ...]
104
+ failure: tuple[str, ...]
105
+ error_field: str | None = None
106
+ conditional: tuple[tuple[str, Condition], ...] = ()
107
+ timeout_s: float | None = None
108
+
109
+
110
+ @dataclass(frozen=True, slots=True)
111
+ class Verdict:
112
+ """A 2xx body field whose ``not_ready`` values make the operation's answer a refusal."""
113
+
114
+ field: str
115
+ ok: tuple[str, ...]
116
+ not_ready: tuple[str, ...]
117
+
118
+
119
+ @dataclass(frozen=True, slots=True)
120
+ class Response:
121
+ kind: ResponseKind
122
+ #: ``module:Class`` under ``databay.models`` for a JSON body, or ``None`` when it has no schema.
123
+ model: str | None = None
124
+ #: The top-level JSON type the schema allows: ``object``, ``array`` or ``any``.
125
+ shape: str = "any"
126
+ #: For an object answer that holds the operation's list (``{"keys": [...]}``): the JSON pointer
127
+ #: of that list, whose items the declared ``columns`` name.
128
+ rows: str | None = None
129
+
130
+
131
+ @dataclass(frozen=True, slots=True)
132
+ class Operation:
133
+ operation_id: str
134
+ noun: str
135
+ verb: str
136
+ method: str
137
+ path: str
138
+ summary: str
139
+ description: str
140
+ project: str | None
141
+ destructive: bool
142
+ idempotent: bool
143
+ since: str
144
+ params: tuple[Param, ...]
145
+ body: Body | None
146
+ response: Response
147
+ examples: tuple[str, ...]
148
+ columns: tuple[str, ...] = ()
149
+ wait: Wait | None = None
150
+ verdict: Verdict | None = None
151
+ #: Set on a status operation that a ``wait`` polls: the wait it can resume (``--wait`` on it).
152
+ resumes: Wait | None = None
153
+
154
+ @property
155
+ def secret_fields(self) -> frozenset[str]:
156
+ """Names of every secret body field, across all variants: what verbose logs redact."""
157
+ if self.body is None:
158
+ return frozenset()
159
+ fields = list(self.body.fields) + [p for _, _, variant in self.body.variants for p in variant]
160
+ return frozenset(p.name for p in fields if p.secret)
161
+
162
+ @property
163
+ def project_location(self) -> tuple[str, str] | None:
164
+ """``(where, name)`` for ``path:``/``query:``/``body:`` locations; ``("header", "")``; ``None``."""
165
+ if self.project is None:
166
+ return None
167
+ where, _, name = self.project.partition(":")
168
+ return where, name
169
+
170
+ @property
171
+ def offers_wait(self) -> bool:
172
+ """Whether the call can wait (``--wait``). A create answers at once; its ``wait`` only names ``next``."""
173
+ return self.resumes is not None or (self.wait is not None and self.verb != "create")
174
+
175
+ @property
176
+ def path_params(self) -> tuple[Param, ...]:
177
+ return tuple(p for p in self.params if p.location == "path")
178
+
179
+ @property
180
+ def command(self) -> str:
181
+ return f"databay {self.noun} {self.verb}"
182
+
183
+
184
+ def python_identifier(name: str) -> str:
185
+ """A wire name as a Python identifier: lower case, ``-`` and spaces as ``_``, a keyword suffixed ``_``."""
186
+ import keyword # noqa: PLC0415 — only the generator and the first call pay for it
187
+
188
+ ident = "".join(c if c.isalnum() else "_" for c in name.lower())
189
+ if not ident or ident[0].isdigit():
190
+ ident = f"_{ident}"
191
+ return f"{ident}_" if keyword.iskeyword(ident) else ident
File without changes