duckstring 0.4.0__tar.gz → 0.5.0__tar.gz

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 (277) hide show
  1. duckstring-0.5.0/CHANGELOG.md +93 -0
  2. {duckstring-0.4.0 → duckstring-0.5.0}/MANIFEST.in +2 -0
  3. {duckstring-0.4.0/src/duckstring.egg-info → duckstring-0.5.0}/PKG-INFO +18 -2
  4. {duckstring-0.4.0 → duckstring-0.5.0}/README.md +4 -1
  5. {duckstring-0.4.0 → duckstring-0.5.0}/pyproject.toml +25 -1
  6. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/__init__.py +0 -2
  7. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/alerts/event.py +14 -3
  8. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/alerts/webhook.py +4 -1
  9. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/app.py +71 -4
  10. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/auth.py +17 -0
  11. duckstring-0.5.0/src/duckstring/catchment/cloud.py +156 -0
  12. duckstring-0.5.0/src/duckstring/catchment/cloud_backends.py +156 -0
  13. duckstring-0.5.0/src/duckstring/catchment/cloud_deploy.py +77 -0
  14. duckstring-0.5.0/src/duckstring/catchment/dialback.py +41 -0
  15. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/driver.py +1440 -61
  16. duckstring-0.5.0/src/duckstring/catchment/ec2_launcher.py +372 -0
  17. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/egress_worker.py +28 -5
  18. duckstring-0.5.0/src/duckstring/catchment/fargate_launcher.py +298 -0
  19. duckstring-0.5.0/src/duckstring/catchment/flight_sql.py +98 -0
  20. duckstring-0.5.0/src/duckstring/catchment/launcher.py +332 -0
  21. duckstring-0.5.0/src/duckstring/catchment/pg_wire.py +317 -0
  22. duckstring-0.5.0/src/duckstring/catchment/pool_launcher.py +397 -0
  23. duckstring-0.5.0/src/duckstring/catchment/registry.py +85 -0
  24. duckstring-0.5.0/src/duckstring/catchment/relay.py +217 -0
  25. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/routes/__init__.py +4 -0
  26. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/routes/alerts.py +3 -1
  27. duckstring-0.5.0/src/duckstring/catchment/routes/catchment.py +476 -0
  28. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/routes/data.py +168 -22
  29. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/routes/deploy.py +138 -8
  30. duckstring-0.5.0/src/duckstring/catchment/routes/duck.py +68 -0
  31. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/routes/metrics.py +36 -0
  32. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/routes/orchestrate.py +85 -3
  33. duckstring-0.5.0/src/duckstring/catchment/routes/pool.py +38 -0
  34. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/routes/secrets.py +17 -1
  35. duckstring-0.5.0/src/duckstring/catchment/routes/serving.py +104 -0
  36. duckstring-0.5.0/src/duckstring/catchment/schema/017_alert_renotify.sql +7 -0
  37. duckstring-0.5.0/src/duckstring/catchment/schema/018_duck.sql +11 -0
  38. duckstring-0.5.0/src/duckstring/catchment/schema/019_lineage.sql +18 -0
  39. duckstring-0.5.0/src/duckstring/catchment/schema/020_column_lineage.sql +15 -0
  40. duckstring-0.5.0/src/duckstring/catchment/schema/021_cloud.sql +44 -0
  41. duckstring-0.5.0/src/duckstring/catchment/schema/022_retire_duck_size.sql +25 -0
  42. duckstring-0.5.0/src/duckstring/catchment/schema/023_pool_provider.sql +7 -0
  43. duckstring-0.5.0/src/duckstring/catchment/schema/024_serving.sql +29 -0
  44. duckstring-0.5.0/src/duckstring/catchment/schema/025_dbt.sql +3 -0
  45. duckstring-0.5.0/src/duckstring/catchment/schema/026_deploy_config.sql +7 -0
  46. duckstring-0.5.0/src/duckstring/catchment/schema/027_persist.sql +14 -0
  47. duckstring-0.5.0/src/duckstring/catchment/schema/028_persist_timing.sql +3 -0
  48. duckstring-0.5.0/src/duckstring/catchment/serving.py +209 -0
  49. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/404.html +1 -1
  50. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/__next.__PAGE__.txt +2 -2
  51. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/__next._full.txt +3 -3
  52. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/__next._head.txt +1 -1
  53. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/__next._index.txt +2 -2
  54. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/__next._tree.txt +2 -2
  55. duckstring-0.5.0/src/duckstring/catchment/static/_next/static/chunks/0_8xp0_1~w7l7.css +1 -0
  56. duckstring-0.5.0/src/duckstring/catchment/static/_next/static/chunks/15o1yi9qh60on.js +2 -0
  57. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/_not-found/__next._full.txt +2 -2
  58. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/_not-found/__next._head.txt +1 -1
  59. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/_not-found/__next._index.txt +2 -2
  60. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/_not-found/__next._not-found.__PAGE__.txt +1 -1
  61. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/_not-found/__next._not-found.txt +1 -1
  62. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/_not-found/__next._tree.txt +2 -2
  63. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/_not-found.html +1 -1
  64. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/_not-found.txt +2 -2
  65. duckstring-0.5.0/src/duckstring/catchment/static/index.html +1 -0
  66. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/index.txt +3 -3
  67. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/__init__.py +20 -1
  68. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/_http.py +4 -0
  69. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/alert.py +9 -3
  70. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/catchment.py +35 -1
  71. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/control.py +29 -0
  72. duckstring-0.5.0/src/duckstring/cli/duck.py +185 -0
  73. duckstring-0.5.0/src/duckstring/cli/lineage.py +111 -0
  74. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/pond.py +50 -7
  75. duckstring-0.5.0/src/duckstring/cli/serve.py +94 -0
  76. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/core.py +57 -7
  77. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/dataplane.py +446 -15
  78. duckstring-0.5.0/src/duckstring/dbt_mode.py +149 -0
  79. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/catalog/src/pond.py +4 -1
  80. duckstring-0.5.0/src/duckstring/demo/gh_actors/README.md +10 -0
  81. duckstring-0.5.0/src/duckstring/demo/gh_actors/pond.toml +6 -0
  82. duckstring-0.5.0/src/duckstring/demo/gh_actors/src/pond.py +25 -0
  83. duckstring-0.5.0/src/duckstring/demo/gh_events/README.md +21 -0
  84. duckstring-0.5.0/src/duckstring/demo/gh_events/pond.toml +4 -0
  85. duckstring-0.5.0/src/duckstring/demo/gh_events/src/pond.py +109 -0
  86. duckstring-0.5.0/src/duckstring/demo/gh_pushes/README.md +10 -0
  87. duckstring-0.5.0/src/duckstring/demo/gh_pushes/pond.toml +7 -0
  88. duckstring-0.5.0/src/duckstring/demo/gh_pushes/src/pond.py +23 -0
  89. duckstring-0.5.0/src/duckstring/demo/gh_repo_activity/README.md +14 -0
  90. duckstring-0.5.0/src/duckstring/demo/gh_repo_activity/pond.toml +7 -0
  91. duckstring-0.5.0/src/duckstring/demo/gh_repo_activity/src/pond.py +24 -0
  92. duckstring-0.5.0/src/duckstring/demo/gh_stars/README.md +10 -0
  93. duckstring-0.5.0/src/duckstring/demo/gh_stars/pond.toml +6 -0
  94. duckstring-0.5.0/src/duckstring/demo/gh_stars/src/pond.py +19 -0
  95. duckstring-0.5.0/src/duckstring/demo/gh_trending/README.md +10 -0
  96. duckstring-0.5.0/src/duckstring/demo/gh_trending/pond.toml +7 -0
  97. duckstring-0.5.0/src/duckstring/demo/gh_trending/src/pond.py +23 -0
  98. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/priced/src/pond.py +1 -1
  99. duckstring-0.5.0/src/duckstring/demo/shop_analytics/.gitignore +8 -0
  100. duckstring-0.5.0/src/duckstring/demo/shop_analytics/README.md +24 -0
  101. duckstring-0.5.0/src/duckstring/demo/shop_analytics/dbt/dbt_project.yml +12 -0
  102. duckstring-0.5.0/src/duckstring/demo/shop_analytics/dbt/models/orders_clean.sql +8 -0
  103. duckstring-0.5.0/src/duckstring/demo/shop_analytics/dbt/models/revenue_by_product.sql +8 -0
  104. duckstring-0.5.0/src/duckstring/demo/shop_analytics/dbt/models/sources.yml +9 -0
  105. duckstring-0.5.0/src/duckstring/demo/shop_analytics/dbt/models/top_products.sql +8 -0
  106. duckstring-0.5.0/src/duckstring/demo/shop_analytics/pond.toml +11 -0
  107. duckstring-0.5.0/src/duckstring/demo/shop_orders/.gitignore +3 -0
  108. duckstring-0.5.0/src/duckstring/demo/shop_orders/README.md +9 -0
  109. duckstring-0.5.0/src/duckstring/demo/shop_orders/pond.toml +4 -0
  110. duckstring-0.5.0/src/duckstring/demo/shop_orders/src/pond.py +32 -0
  111. duckstring-0.5.0/src/duckstring/demo/tpcds_category_revenue/README.md +13 -0
  112. duckstring-0.5.0/src/duckstring/demo/tpcds_category_revenue/pond.toml +7 -0
  113. duckstring-0.5.0/src/duckstring/demo/tpcds_category_revenue/src/pond.py +26 -0
  114. duckstring-0.5.0/src/duckstring/demo/tpcds_items/README.md +11 -0
  115. duckstring-0.5.0/src/duckstring/demo/tpcds_items/pond.toml +4 -0
  116. duckstring-0.5.0/src/duckstring/demo/tpcds_items/src/pond.py +80 -0
  117. duckstring-0.5.0/src/duckstring/demo/tpcds_priced/README.md +11 -0
  118. duckstring-0.5.0/src/duckstring/demo/tpcds_priced/pond.toml +8 -0
  119. duckstring-0.5.0/src/duckstring/demo/tpcds_priced/src/pond.py +26 -0
  120. duckstring-0.5.0/src/duckstring/demo/tpcds_sales/README.md +22 -0
  121. duckstring-0.5.0/src/duckstring/demo/tpcds_sales/pond.toml +4 -0
  122. duckstring-0.5.0/src/duckstring/demo/tpcds_sales/src/pond.py +76 -0
  123. duckstring-0.5.0/src/duckstring/demo/tpcds_store_revenue/README.md +10 -0
  124. duckstring-0.5.0/src/duckstring/demo/tpcds_store_revenue/pond.toml +7 -0
  125. duckstring-0.5.0/src/duckstring/demo/tpcds_store_revenue/src/pond.py +25 -0
  126. duckstring-0.5.0/src/duckstring/demo/tpcds_stores/README.md +10 -0
  127. duckstring-0.5.0/src/duckstring/demo/tpcds_stores/pond.toml +4 -0
  128. duckstring-0.5.0/src/duckstring/demo/tpcds_stores/src/pond.py +48 -0
  129. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/duck/__main__.py +130 -14
  130. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/duck/client.py +8 -0
  131. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/duck/core.py +19 -3
  132. duckstring-0.5.0/src/duckstring/duck/dbt_executor.py +170 -0
  133. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/duck/executor.py +109 -19
  134. duckstring-0.5.0/src/duckstring/duck/pool_agent.py +151 -0
  135. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/egress/base.py +5 -0
  136. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/egress/object_store.py +87 -10
  137. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/engine/__init__.py +2 -0
  138. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/engine/catchment.py +33 -4
  139. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/engine/core.py +14 -0
  140. duckstring-0.5.0/src/duckstring/flock/__init__.py +298 -0
  141. duckstring-0.5.0/src/duckstring/flock/engines/__init__.py +0 -0
  142. duckstring-0.5.0/src/duckstring/flock/engines/athena.py +297 -0
  143. duckstring-0.5.0/src/duckstring/flock/equivalence.py +75 -0
  144. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/iceberg_plane.py +27 -16
  145. duckstring-0.5.0/src/duckstring/schema_contract.py +145 -0
  146. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/storage.py +141 -20
  147. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/trickle/builder.py +59 -3
  148. duckstring-0.5.0/src/duckstring/trickle/capture.py +384 -0
  149. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/trickle/io.py +19 -2
  150. duckstring-0.5.0/src/duckstring/trickle/lineage.py +305 -0
  151. {duckstring-0.4.0 → duckstring-0.5.0/src/duckstring.egg-info}/PKG-INFO +18 -2
  152. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring.egg-info/SOURCES.txt +92 -7
  153. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring.egg-info/requires.txt +16 -0
  154. duckstring-0.4.0/src/duckstring/catchment/launcher.py +0 -103
  155. duckstring-0.4.0/src/duckstring/catchment/registry.py +0 -40
  156. duckstring-0.4.0/src/duckstring/catchment/routes/catchment.py +0 -161
  157. duckstring-0.4.0/src/duckstring/catchment/routes/duck.py +0 -32
  158. duckstring-0.4.0/src/duckstring/catchment/static/_next/static/chunks/0p30fzde51na5.css +0 -1
  159. duckstring-0.4.0/src/duckstring/catchment/static/_next/static/chunks/0qdnkaffkg.2-.js +0 -2
  160. duckstring-0.4.0/src/duckstring/catchment/static/index.html +0 -1
  161. duckstring-0.4.0/src/duckstring/schema_contract.py +0 -67
  162. duckstring-0.4.0/src/duckstring/utils.py +0 -3
  163. {duckstring-0.4.0 → duckstring-0.5.0}/LICENSE +0 -0
  164. {duckstring-0.4.0 → duckstring-0.5.0}/setup.cfg +0 -0
  165. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/__main__.py +0 -0
  166. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/acc.py +0 -0
  167. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/agg.py +0 -0
  168. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/alerts/__init__.py +0 -0
  169. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/alerts/base.py +0 -0
  170. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/alerts/email.py +0 -0
  171. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/__init__.py +0 -0
  172. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/alert_worker.py +0 -0
  173. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/asgi.py +0 -0
  174. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/dag.py +0 -0
  175. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/data_lease.py +0 -0
  176. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/db.py +0 -0
  177. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/poller.py +0 -0
  178. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/routes/draw.py +0 -0
  179. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/routes/duct.py +0 -0
  180. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/routes/view.py +0 -0
  181. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/schema/001_init.sql +0 -0
  182. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/schema/002_ducts.sql +0 -0
  183. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/schema/003_pull_m.sql +0 -0
  184. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/schema/004_identity.sql +0 -0
  185. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/schema/005_pond_version_schema.sql +0 -0
  186. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/schema/006_refresh.sql +0 -0
  187. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/schema/007_catchment_key.sql +0 -0
  188. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/schema/008_spout.sql +0 -0
  189. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/schema/009_spout_state.sql +0 -0
  190. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/schema/010_spout_wake.sql +0 -0
  191. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/schema/011_spout_window.sql +0 -0
  192. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/schema/012_spout_node.sql +0 -0
  193. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/schema/013_changed_f.sql +0 -0
  194. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/schema/014_alert.sql +0 -0
  195. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/schema/016_alert_scope_major.sql +0 -0
  196. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/secrets.py +0 -0
  197. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/state_sync.py +0 -0
  198. {duckstring-0.4.0/src/duckstring/catchment/static/_next/static/K01gZbd5BTz315upsZeJJ → duckstring-0.5.0/src/duckstring/catchment/static/_next/static/7b3BoSoT7Zw6j1d3GFlHs}/_buildManifest.js +0 -0
  199. {duckstring-0.4.0/src/duckstring/catchment/static/_next/static/K01gZbd5BTz315upsZeJJ → duckstring-0.5.0/src/duckstring/catchment/static/_next/static/7b3BoSoT7Zw6j1d3GFlHs}/_clientMiddlewareManifest.js +0 -0
  200. {duckstring-0.4.0/src/duckstring/catchment/static/_next/static/K01gZbd5BTz315upsZeJJ → duckstring-0.5.0/src/duckstring/catchment/static/_next/static/7b3BoSoT7Zw6j1d3GFlHs}/_ssgManifest.js +0 -0
  201. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/_next/static/chunks/03~yq9q893hmn.js +0 -0
  202. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/_next/static/chunks/07lhk_q6pmm3r.js +0 -0
  203. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/_next/static/chunks/0dbhjjzl8qfwv.js +0 -0
  204. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/_next/static/chunks/0jvmviuftg5e2.css +0 -0
  205. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/_next/static/chunks/0kczw6usu-y-f.js +0 -0
  206. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/_next/static/chunks/11kjahy2ntf0n.js +0 -0
  207. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/_next/static/chunks/turbopack-03~mbvk_uplk_.js +0 -0
  208. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/favicon.svg +0 -0
  209. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/logo-mark.svg +0 -0
  210. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/logo.svg +0 -0
  211. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/bulk.py +0 -0
  212. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/config.py +0 -0
  213. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/data.py +0 -0
  214. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/deploy.py +0 -0
  215. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/duct.py +0 -0
  216. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/puddle.py +0 -0
  217. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/secret.py +0 -0
  218. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/spout.py +0 -0
  219. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/status.py +0 -0
  220. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/trigger.py +0 -0
  221. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/window.py +0 -0
  222. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/catalog/.gitignore +0 -0
  223. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/catalog/README.md +0 -0
  224. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/catalog/pond.toml +0 -0
  225. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/orders/.gitignore +0 -0
  226. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/orders/README.md +0 -0
  227. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/orders/pond.toml +0 -0
  228. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/orders/src/pond.py +0 -0
  229. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/priced/.gitignore +0 -0
  230. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/priced/README.md +0 -0
  231. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/priced/pond.toml +0 -0
  232. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/priced/src/puddles.py +0 -0
  233. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/products/.gitignore +0 -0
  234. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/products/README.md +0 -0
  235. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/products/pond.toml +0 -0
  236. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/products/src/pond.py +0 -0
  237. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/reports/.gitignore +0 -0
  238. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/reports/README.md +0 -0
  239. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/reports/pond.toml +0 -0
  240. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/reports/src/pond.py +0 -0
  241. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/revenue/.gitignore +0 -0
  242. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/revenue/README.md +0 -0
  243. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/revenue/pond.toml +0 -0
  244. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/revenue/src/pond.py +0 -0
  245. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/revenue/src/puddles.py +0 -0
  246. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/sales/.gitignore +0 -0
  247. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/sales/README.md +0 -0
  248. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/sales/pond.toml +0 -0
  249. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/sales/src/pond.py +0 -0
  250. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/sales/src/puddles.py +0 -0
  251. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/transactions/.gitignore +0 -0
  252. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/transactions/README.md +0 -0
  253. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/transactions/pond.toml +0 -0
  254. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/transactions/src/pond.py +0 -0
  255. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/duck/__init__.py +0 -0
  256. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/egress/__init__.py +0 -0
  257. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/egress/credentials.py +0 -0
  258. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/egress/destination.py +0 -0
  259. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/egress/postgres.py +0 -0
  260. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/engine/pond.py +0 -0
  261. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/engine/worker.py +0 -0
  262. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/iceberg_catalog.py +0 -0
  263. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/keys.py +0 -0
  264. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/local/__init__.py +0 -0
  265. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/local/hydrate.py +0 -0
  266. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/local/project.py +0 -0
  267. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/local/runner.py +0 -0
  268. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/objects.py +0 -0
  269. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/trickle/__init__.py +0 -0
  270. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/trickle/acc.py +0 -0
  271. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/trickle/agg.py +0 -0
  272. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/trickle/context.py +0 -0
  273. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/trickle_builder.py +0 -0
  274. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/trickle_io.py +0 -0
  275. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring.egg-info/dependency_links.txt +0 -0
  276. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring.egg-info/entry_points.txt +0 -0
  277. {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring.egg-info/top_level.txt +0 -0
@@ -0,0 +1,93 @@
1
+ # Changelog
2
+
3
+ Notable changes per release. Versions before 0.5.0 are recorded in the git history and the `v*` tags.
4
+
5
+ ## 0.5.0 — 2026-08-03
6
+
7
+ The cloud release: a Catchment can now run its Ducks on AWS, keep its data on S3, and serve that data
8
+ over standard wire protocols. Everything below is opt-in — a Catchment with no cloud configuration
9
+ behaves exactly as it did in 0.4.0, and a `pond.toml` asking for remote compute still runs anywhere.
10
+
11
+ ### Cloud compute
12
+
13
+ - **Duck launchers**: a Pond's Duck runs on the Catchment's own box (default), on **Fargate** (the
14
+ default remote backend — serverless containers, task-role IAM), or on **EC2** (the escape hatch, for
15
+ sizes and GPUs Fargate does not offer). Built-in **S/M/L/XL preset pools** resolve with zero setup.
16
+ - **Duck Pools**: named remote-compute pools (`duckstring duck pool add`), with per-Pond compute
17
+ declared in `pond.toml` and overridable operationally (`duckstring duck set`).
18
+ - **The Pool agent**: one shared machine per named Pool hosting many co-resident Ducks.
19
+ - **Auto-relay**: a Catchment on a laptop behind NAT can run cloud Ducks with no manual tunnel — the
20
+ first remote spawn provisions a small always-reachable box and holds a reverse tunnel to it.
21
+ - **Remote failures are observable**: boot output is teed to the console (EC2) / CloudWatch (Fargate)
22
+ and the tail is attached to the Pond's failure, so a Duck that dies before dialling back still says
23
+ why. Spawn-failure reasons (missing image, missing AMI, bad IAM) surface on the Pond rather than as a
24
+ generic crash.
25
+ - Per-provider **startup grace** so a cold machine is not judged by the steady-state silence window.
26
+
27
+ ### Data plane on S3
28
+
29
+ - A Catchment's data root can be an object store (`duckstring catchment settings --data-root s3://…`),
30
+ with an S3-compatible **endpoint override** for MinIO/Ceph/R2.
31
+ - **Switch, adopt, or migrate** an existing plane, with a background copy and live progress.
32
+ - **Local-first publish + async Persist**: a run publishes locally and mirrors durably in the
33
+ background, so compute never waits on the object store; `persisted_f` is the durable watermark and
34
+ freshness gating is Pool-aware.
35
+ - **S3-resident state**: a merge base is read as a view rather than hydrated, and retention pruning is
36
+ floor-anchored rather than inferred from absence.
37
+
38
+ ### The Flock — over-envelope compute
39
+
40
+ A comprehensive `pond.trickle(...)` recompute that exceeds the Duck's memory envelope can be dispatched
41
+ to a serverless engine (**Athena** ships first), while the Duck keeps merge/diff/publish.
42
+
43
+ **DuckDB is the authority.** Dispatch decides *where* work runs, never *what* is published, enforced
44
+ structurally: an engine-owned allow-list of expressions proven equivalent on both engines (division and
45
+ CAST are excluded — the two engines genuinely disagree), a `conform` step that casts the engine's result
46
+ to DuckDB's own schema and rejects a differing column set, and degradation to local compute on any
47
+ failure. Dispatch counters ride `/metrics`, since a silently degrading Flock is otherwise invisible.
48
+
49
+ ### Data serving
50
+
51
+ - A sandboxed, warm, read-only query surface over published data: catalog = catchment, schema = pond.
52
+ - **Postgres wire** and **Arrow Flight SQL** adapters, plus `/api/serve` and a CLI.
53
+ - A **Catalog UI** unifying the query surface, with role-governed access.
54
+
55
+ ### Lineage
56
+
57
+ - Table-level (observed per run), column-level (static per version, with a sqlglot upgrade for SQL
58
+ outputs), and row-level temporal provenance (`duckstring trace`).
59
+ - **OpenLineage** events emitted on run completion for catalog integration.
60
+
61
+ ### dbt-mode Ponds
62
+
63
+ Deploy a dbt project as a Pond with no `@ripple` code: each dbt model becomes a Ripple with its own
64
+ freshness, failure and retry tracking, and `ref()` becomes the intra-Pond graph. Opt in with the
65
+ `duckstring[dbt]` extra.
66
+
67
+ ### Correctness and operability
68
+
69
+ - **Version contract**: a lossless widening of a column type is accepted (a data-dependent branch could
70
+ legitimately wedge a live Pond permanently); `reset-contract` is the escape hatch for a genuine
71
+ narrowing; contract failures carry a dedicated sub-reason end-to-end.
72
+ - **Reads reject a stale local publish** the Catchment knows is behind, rather than serving it.
73
+ - Pond Runs the Pond has moved past are closed rather than stranded at `running`.
74
+ - A Pool machine that disappears is detected and relaunched, instead of wedging every Pond on it.
75
+ - The **serving sandbox** no longer spills into the shared system temp — DuckDB exempts its temp
76
+ directory from the external-access lock, which on Linux exposed everything under `/tmp` to a
77
+ read-level user.
78
+ - An idle Duck backs off instead of polling ten times a second.
79
+ - Opt-in **re-notify cadence** for alert channels while a failure or freshness episode persists.
80
+ - Incremental object-store egress (`mode=append`) mirrors the published collection per delivery.
81
+
82
+ ### Testing
83
+
84
+ - The object-store data plane runs against a **real S3 API** in CI (MinIO, which rejects unsigned
85
+ requests — the class of bug moto cannot catch).
86
+ - Real-data demo sets: **TPC-DS** and **GHArchive** (the latter offline against a committed fixture).
87
+ - Python matrix, the dbt extra, real-Postgres egress, and frontend tests in CI.
88
+
89
+ ### Documentation
90
+
91
+ - A full **AWS guide** (`guides/cloud.md`): the cloud-enable gate, the three IAM roles, security-group
92
+ asymmetry, the AMI Python constraint, the build-your-own-image policy, and the Flock's authority rules.
93
+ - New guides for lineage, dbt-mode, and querying data.
@@ -1,5 +1,7 @@
1
1
  # The sdist ships the installable package only — keep internal design notes and the test suite
2
2
  # out of the PyPI source tarball (they're in the git repo for contributors).
3
+ include CHANGELOG.md
4
+
3
5
  prune tests
4
6
  prune plans
5
7
  prune experiment
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: duckstring
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Build data pipelines the way you build software: version each transform, declare its dependencies, and Duckstring resolves the execution DAG automatically.
5
5
  Author-email: Duckstring <dev@duckstring.com>
6
6
  License: Apache License
@@ -225,15 +225,29 @@ Requires-Dist: pyiceberg>=0.7
225
225
  Requires-Dist: pyarrow>=14
226
226
  Requires-Dist: pytz
227
227
  Provides-Extra: iceberg
228
+ Provides-Extra: dbt
229
+ Requires-Dist: dbt-duckdb>=1.9; extra == "dbt"
230
+ Provides-Extra: lineage
231
+ Requires-Dist: sqlglot>=23; extra == "lineage"
232
+ Provides-Extra: aws
233
+ Requires-Dist: s3fs>=2023.1; extra == "aws"
234
+ Requires-Dist: boto3>=1.34; extra == "aws"
228
235
  Provides-Extra: dev
229
236
  Requires-Dist: pytest>=8.0; extra == "dev"
237
+ Requires-Dist: moto[server]>=5.0; extra == "dev"
230
238
  Requires-Dist: pytest-timeout>=0.5; extra == "dev"
231
239
  Requires-Dist: ruff>=0.3; extra == "dev"
232
240
  Requires-Dist: mypy>=1.8; extra == "dev"
241
+ Requires-Dist: dbt-duckdb>=1.9; extra == "dev"
242
+ Requires-Dist: sqlglot>=23; extra == "dev"
243
+ Requires-Dist: s3fs>=2023.1; extra == "dev"
244
+ Requires-Dist: boto3>=1.34; extra == "dev"
245
+ Requires-Dist: pg8000>=1.31; extra == "dev"
233
246
  Dynamic: license-file
234
247
 
235
248
  # Duckstring
236
- *There is no DAG.*
249
+
250
+ Build data pipelines the way you build software: version each transform, declare its dependencies, and Duckstring resolves the execution DAG automatically.
237
251
 
238
252
  Duckstring treats data transformations as software packages. Upstream dependencies are declared per Pond (unit operation), defining the DAG without the need for its direct management.
239
253
 
@@ -297,6 +311,8 @@ Ponds execute on demand signals sent to an Outlet, in two flavours — **push**
297
311
 
298
312
  A Tide keeps an Outlet no staler than a bound (`duckstring trigger tide reports 1d`); a Wave keeps it as fresh as the pipeline can supply. See [Triggers](https://docs.duckstring.com/guides/triggers) for the full semantics.
299
313
 
314
+ *There is no DAG.*
315
+
300
316
  ## Going further
301
317
 
302
318
  Full documentation lives at **[docs.duckstring.com](https://docs.duckstring.com)**:
@@ -1,5 +1,6 @@
1
1
  # Duckstring
2
- *There is no DAG.*
2
+
3
+ Build data pipelines the way you build software: version each transform, declare its dependencies, and Duckstring resolves the execution DAG automatically.
3
4
 
4
5
  Duckstring treats data transformations as software packages. Upstream dependencies are declared per Pond (unit operation), defining the DAG without the need for its direct management.
5
6
 
@@ -63,6 +64,8 @@ Ponds execute on demand signals sent to an Outlet, in two flavours — **push**
63
64
 
64
65
  A Tide keeps an Outlet no staler than a bound (`duckstring trigger tide reports 1d`); a Wave keeps it as fresh as the pipeline can supply. See [Triggers](https://docs.duckstring.com/guides/triggers) for the full semantics.
65
66
 
67
+ *There is no DAG.*
68
+
66
69
  ## Going further
67
70
 
68
71
  Full documentation lives at **[docs.duckstring.com](https://docs.duckstring.com)**:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "duckstring"
3
- version = "0.4.0"
3
+ version = "0.5.0"
4
4
  description = "Build data pipelines the way you build software: version each transform, declare its dependencies, and Duckstring resolves the execution DAG automatically."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -35,11 +35,35 @@ dependencies = [
35
35
  [project.optional-dependencies]
36
36
  # Retained for back-compat: the Iceberg deps are in core now (Iceberg is the default plane).
37
37
  iceberg = []
38
+ # dbt-mode Ponds (plans/dbt.md): deploy a dbt project as a Pond, each model a Ripple. Heavyweight and
39
+ # opt-in — only pulled when a team actually runs dbt-mode. dbt-duckdb pulls dbt-core transitively.
40
+ dbt = [
41
+ "dbt-duckdb>=1.9",
42
+ ]
43
+ # Column lineage through .sql() escape hatches (plans/lineage.md Phase 3): a real SQL parser, opt-in --
44
+ # without it those outputs are honestly "opaque", never guessed. Pure python, light.
45
+ lineage = [
46
+ "sqlglot>=23",
47
+ ]
48
+ # AWS / cloud (plans/cloud-config.md): the object-store data plane + egress (s3fs — fsspec's small-file
49
+ # work; DuckDB does the bulk Parquet I/O over httpfs on its own), and the remote-compute launchers
50
+ # (EC2/Fargate/relay) + the Athena Flock engine (boto3). Opt-in — a local/offline Catchment needs none.
51
+ aws = [
52
+ "s3fs>=2023.1",
53
+ "boto3>=1.34",
54
+ ]
38
55
  dev = [
39
56
  "pytest>=8.0",
57
+ # A local S3 API, so the object-store data plane is a normal test rather than a live-AWS session.
58
+ "moto[server]>=5.0",
40
59
  "pytest-timeout>=0.5",
41
60
  "ruff>=0.3",
42
61
  "mypy>=1.8",
62
+ "dbt-duckdb>=1.9",
63
+ "sqlglot>=23",
64
+ "s3fs>=2023.1",
65
+ "boto3>=1.34",
66
+ "pg8000>=1.31",
43
67
  ]
44
68
 
45
69
  [tool.setuptools]
@@ -5,7 +5,6 @@ from .core import (
5
5
  Catchment,
6
6
  Pond,
7
7
  Puddle,
8
- Ripple,
9
8
  puddle,
10
9
  ripple,
11
10
  )
@@ -19,7 +18,6 @@ __all__ = [
19
18
  "Catchment",
20
19
  "Pond",
21
20
  "Puddle",
22
- "Ripple",
23
21
  "puddle",
24
22
  "ripple",
25
23
  "__version__",
@@ -14,6 +14,12 @@ from datetime import datetime, timezone
14
14
  # The event vocabulary. `freshness` is tick-driven (the SLA sweep); the rest are transition-driven.
15
15
  KNOWN_EVENTS = ("failure", "contract", "spout", "recovery", "freshness")
16
16
 
17
+ # Machine-consumer kinds: valid subscriptions, but deliberately EXCLUDED from the `all` expansion —
18
+ # an ops Slack channel subscribed to `all` must never receive raw catalog events. `openlineage` carries
19
+ # a standard OpenLineage RunEvent per completed Pond Run (plans/lineage.md Phase 4); the webhook
20
+ # notifier posts it verbatim (no Slack `text` wrapper).
21
+ EXPLICIT_EVENTS = ("openlineage",)
22
+
17
23
  # Default severity per kind (a channel filters by kind, not severity — this is only for the payload/label).
18
24
  SEVERITY = {
19
25
  "failure": "error",
@@ -21,13 +27,15 @@ SEVERITY = {
21
27
  "spout": "error",
22
28
  "freshness": "warning",
23
29
  "recovery": "info",
30
+ "openlineage": "info",
24
31
  }
25
32
 
26
33
 
27
34
  def normalise_events(events: str | None) -> tuple[str, ...]:
28
35
  """Parse a channel's ``events`` CSV (or ``all``/empty) into a validated tuple of known kinds.
29
36
 
30
- ``all`` / empty → every kind. Raises :class:`ValueError` naming an unknown kind."""
37
+ ``all`` / empty → every *notification* kind (machine-consumer kinds like ``openlineage`` need an
38
+ explicit subscription). Raises :class:`ValueError` naming an unknown kind."""
31
39
  if not events or events.strip().lower() == "all":
32
40
  return KNOWN_EVENTS
33
41
  out = []
@@ -35,8 +43,11 @@ def normalise_events(events: str | None) -> tuple[str, ...]:
35
43
  kind = raw.strip().lower()
36
44
  if not kind:
37
45
  continue
38
- if kind not in KNOWN_EVENTS:
39
- raise ValueError(f"unknown alert event {kind!r} — choose from {', '.join(KNOWN_EVENTS)} (or 'all')")
46
+ if kind not in KNOWN_EVENTS and kind not in EXPLICIT_EVENTS:
47
+ raise ValueError(
48
+ f"unknown alert event {kind!r} — choose from "
49
+ f"{', '.join(KNOWN_EVENTS + EXPLICIT_EVENTS)} (or 'all')"
50
+ )
40
51
  out.append(kind)
41
52
  if not out:
42
53
  return KNOWN_EVENTS
@@ -26,7 +26,10 @@ class WebhookNotifier:
26
26
  from ..egress import credentials
27
27
 
28
28
  url = credentials.resolve(self.dest.raw) # resolve any ${env:}/${secret:} token — never logged
29
- body = {"text": event.summary(), **event.to_payload()} # `text` for Slack; the rest for generic receivers
29
+ if event.kind == "openlineage" and event.detail.get("event"):
30
+ body = event.detail["event"] # a standard OpenLineage RunEvent — posted verbatim, no wrapper
31
+ else:
32
+ body = {"text": event.summary(), **event.to_payload()} # `text` for Slack; the rest generic
30
33
  try:
31
34
  resp = httpx.post(url, json=body, timeout=_TIMEOUT)
32
35
  resp.raise_for_status()
@@ -12,7 +12,7 @@ from fastapi.staticfiles import StaticFiles
12
12
  from . import auth
13
13
  from .db import connect, ensure_identity, migrate
14
14
  from .driver import Driver
15
- from .launcher import NoopLauncher, SubprocessLauncher
15
+ from .launcher import NoopLauncher, SubprocessLauncher, load_launcher_class
16
16
  from .routes import router
17
17
 
18
18
  _STATIC_DIR = Path(__file__).parent / "static"
@@ -41,16 +41,71 @@ async def _lifespan(app: FastAPI):
41
41
  # keys, so rotating those never disrupts running Ducks).
42
42
  # base_url None = unknown (the platform picked the bind address): the launcher defers spawns
43
43
  # until the dial-back middleware learns the address from the first request.
44
- launcher = SubprocessLauncher(
45
- app.state.root, base_url, token=app.state.duck_token, data_root=app.state.data_root
46
- )
44
+ # DUCKSTRING_DUCK_LAUNCHER=module:Class (prereqs D6) swaps the implementation — same
45
+ # constructor contract, same interface; the Duck still dials back over the duck channel.
46
+ if os.environ.get("DUCKSTRING_DUCK_LAUNCHER"):
47
+ # A fully-custom launcher (D6) takes over entirely — same constructor contract as before.
48
+ launcher_cls = load_launcher_class(os.environ["DUCKSTRING_DUCK_LAUNCHER"])
49
+ launcher = launcher_cls(
50
+ app.state.root, base_url, token=app.state.duck_token, data_root=app.state.data_root
51
+ )
52
+ else:
53
+ # The dispatching launcher: `catchment`-targeted Ponds on this box (subprocess), pool/
54
+ # dedicated ones on EC2 — the same code whether the Catchment is local or hosted. The EC2
55
+ # backend is only built when cloud is enabled (remote data root + AWS creds); otherwise
56
+ # remote targets degrade to local (plans/cloud-config.md).
57
+ from .cloud_backends import build_remote_backends
58
+ from .launcher import DispatchingLauncher
59
+
60
+ local = SubprocessLauncher(
61
+ app.state.root, base_url, token=app.state.duck_token, data_root=app.state.data_root
62
+ )
63
+ # The remote backends (Fargate/EC2 + the shared dial-back) exist only when cloud is enabled
64
+ # (remote data root + AWS creds); otherwise remote targets degrade to local. The same builder
65
+ # re-attaches them live when creds / the data root change at runtime (see cloud_backends).
66
+ remotes, dialback = build_remote_backends(
67
+ app.state.root, base_url, app.state.duck_token, app.state.data_root, app.state.secret_store)
68
+ launcher = DispatchingLauncher(local, remotes, dialback=dialback, default_provider="fargate")
47
69
  driver = Driver(app.state.db, app.state.root, base_url, launcher, data_root=app.state.data_root)
48
70
  app.state.driver = driver
49
71
  app.state.launcher = launcher
50
72
 
73
+ # Boot credential check: the cloud gate is presence-based (deterministic, network-free), so a
74
+ # present-but-rejected key would show "enabled" while every remote launch fails. Validate the creds
75
+ # actually authenticate in the background — the result caches on app.state.cloud_creds (surfaced on
76
+ # /api/status so the UI can persistently warn) and logs a warning. Non-blocking, never fails boot.
77
+ # Validity is surfaced, not folded into the gate (a transient STS blip must not strand a running setup).
78
+ from .cloud_backends import refresh_credential_status
79
+ app.state.cloud_creds = None
80
+ refresh_credential_status(app.state, force=True)
81
+
51
82
  # Restore: resume any Pond Runs that were in flight when the Catchment last stopped.
52
83
  driver.resume_incomplete()
53
84
 
85
+ # Data-serving wire adapters (plans/data-serving.md): the Postgres wire (default) + Arrow Flight SQL,
86
+ # each on its own port when configured, sharing the sandboxed serving core. Put TLS + a network ACL
87
+ # in front for a hosted Catchment (the password is an API key).
88
+ app.state.serve_wires = []
89
+ pg_port = os.environ.get("DUCKSTRING_SERVE_PG_PORT")
90
+ if pg_port:
91
+ from .pg_wire import PgWireServer
92
+
93
+ pg = PgWireServer(driver, api_key=app.state.api_key,
94
+ host=os.environ.get("DUCKSTRING_SERVE_HOST", "127.0.0.1"), port=int(pg_port))
95
+ pg.start()
96
+ app.state.serve_wires.append(pg)
97
+ flight_port = os.environ.get("DUCKSTRING_SERVE_FLIGHT_PORT")
98
+ if flight_port:
99
+ try:
100
+ from .flight_sql import FlightSqlServer
101
+
102
+ fl = FlightSqlServer(driver, api_key=app.state.api_key,
103
+ host=os.environ.get("DUCKSTRING_SERVE_HOST", "127.0.0.1"), port=int(flight_port))
104
+ fl.start()
105
+ app.state.serve_wires.append(fl)
106
+ except ImportError:
107
+ pass # Flight needs pyarrow.flight; skip if unavailable
108
+
54
109
  from .alert_worker import run_alert_worker
55
110
  from .egress_worker import run_egress_worker
56
111
  from .poller import run_poller
@@ -97,6 +152,8 @@ async def _lifespan(app: FastAPI):
97
152
  except asyncio.CancelledError:
98
153
  pass
99
154
  launcher.shutdown_all()
155
+ for wire in getattr(app.state, "serve_wires", []):
156
+ wire.stop()
100
157
  if app.state.data_lease is not None:
101
158
  from .data_lease import release_lease
102
159
 
@@ -134,6 +191,12 @@ def create_app(
134
191
  migrate(con)
135
192
  ensure_identity(con, name or os.environ.get("DUCKSTRING_CATCHMENT_NAME"))
136
193
 
194
+ # The data root can be a persisted Catchment setting (attached via the API after the Catchment is
195
+ # made — plans/cloud-config.md), behind an explicit argument and the env for platform hosting.
196
+ if not data_root:
197
+ from .cloud import DATA_ROOT_KEY, get_setting
198
+ data_root = get_setting(con, DATA_ROOT_KEY)
199
+
137
200
  # Writer lease on an external data root — refuse to start if a *different* live Catchment owns it (two
138
201
  # Catchments racing one lake's Iceberg catalog would dangle its pointer). A same-id restart reclaims
139
202
  # instantly; only engaged for an external DUCKSTRING_DATA_ROOT, so the local default is untouched.
@@ -167,6 +230,10 @@ def create_app(
167
230
  from .secrets import SecretStore
168
231
  app.state.secret_store = SecretStore(root)
169
232
  credentials.set_secret_provider(app.state.secret_store.get)
233
+ # Give the AWS_* secrets teeth: load them into the environment so botocore's chain uses them (the
234
+ # enable-by-secret cloud flow). Real env wins.
235
+ from . import cloud as _cloud
236
+ _cloud.load_aws_env(app.state.secret_store)
170
237
  # The address Ducks dial back to: explicit argument (the CLI passes its bind address), or the
171
238
  # environment, or None — unknown, because the host platform picks the bind address (e.g. Posit
172
239
  # Connect). When None it is learned from the first request's ASGI scope below.
@@ -141,6 +141,23 @@ def get_principal(request: Request) -> Principal:
141
141
  return Principal(level=None, is_duck=False)
142
142
 
143
143
 
144
+ def level_for_key(con: sqlite3.Connection | None, api_key: str | None, supplied: str) -> Level | None:
145
+ """The access :class:`Level` for a raw key — the reusable core of :func:`get_principal`, for
146
+ non-HTTP front doors (the pg / Flight wire servers, where the key arrives as a password). Open mode
147
+ (no keys configured) → FULL; a bad/absent key → None."""
148
+ if not auth_configured(con, api_key):
149
+ return Level.FULL
150
+ if supplied:
151
+ if con is not None:
152
+ h = hash_key(supplied)
153
+ for level, khash in _key_hashes(con).items():
154
+ if secrets.compare_digest(khash, h):
155
+ return NAME_TO_LEVEL[level]
156
+ if api_key and secrets.compare_digest(supplied, api_key):
157
+ return Level.FULL
158
+ return None
159
+
160
+
144
161
  # ─── Route guards (declared on each route as `dependencies=[...]`) ────────────
145
162
 
146
163
 
@@ -0,0 +1,156 @@
1
+ """Catchment-level cloud config (plans/cloud-config.md, increment 2).
2
+
3
+ The persisted ``catchment_setting`` key/value store (migration 021) plus the **cloud-enable gate**:
4
+ remote compute needs shared object storage a remote box can read, so the gate is *S3 data root +
5
+ AWS credentials present*. The data-plane target is a **persisted setting** (settable after the
6
+ Catchment is made), but **set-once in practice** — switching it once data exists would strand that
7
+ data, so the setter refuses a populated Catchment (a migration is deferred).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import functools
13
+ import os
14
+ import sqlite3
15
+
16
+ DATA_ROOT_KEY = "data_root"
17
+ _REMOTE_SCHEMES = ("s3", "gs")
18
+
19
+
20
+ # ─── the generic key/value setting store ─────────────────────────────────────────
21
+
22
+ def get_setting(con: sqlite3.Connection, key: str) -> str | None:
23
+ row = con.execute("SELECT value FROM catchment_setting WHERE key = ?", (key,)).fetchone()
24
+ return row[0] if row else None
25
+
26
+
27
+ def set_setting(con: sqlite3.Connection, key: str, value: str | None) -> None:
28
+ if value is None:
29
+ con.execute("DELETE FROM catchment_setting WHERE key = ?", (key,))
30
+ else:
31
+ con.execute(
32
+ "INSERT INTO catchment_setting (key, value) VALUES (?, ?) "
33
+ "ON CONFLICT(key) DO UPDATE SET value = excluded.value",
34
+ (key, value),
35
+ )
36
+ con.commit()
37
+
38
+
39
+ def all_settings(con: sqlite3.Connection) -> dict[str, str]:
40
+ return dict(con.execute("SELECT key, value FROM catchment_setting").fetchall())
41
+
42
+
43
+ def has_published_data(con: sqlite3.Connection) -> bool:
44
+ """Whether the Catchment has ever published output — a successful Pond Run. The set-once guard
45
+ keys off this: while it is False the data root is freely settable; once True, switching it would
46
+ strand the published data, so the setter refuses."""
47
+ row = con.execute("SELECT 1 FROM pond_run WHERE status = 'success' LIMIT 1").fetchone()
48
+ return row is not None
49
+
50
+
51
+ # ─── the cloud-enable gate ──────────────────────────────────────────────────────
52
+
53
+ def is_remote(data_root: str | None) -> bool:
54
+ """A data root a remote box can read — an object store (s3://, gs://). A local path / None means
55
+ only Catchment Ducks are launchable (no shared storage)."""
56
+ return bool(data_root) and data_root.split("://", 1)[0].lower() in _REMOTE_SCHEMES
57
+
58
+
59
+ def aws_configured(secret_store=None) -> bool:
60
+ """AWS credentials the control plane can use to launch/submit: env creds/profile/role, or an
61
+ ``AWS_*`` secret in the write-only store (worker boxes themselves use instance roles — creds here
62
+ are the control-plane half)."""
63
+ if any(os.environ.get(k) for k in ("AWS_ACCESS_KEY_ID", "AWS_PROFILE", "AWS_ROLE_ARN",
64
+ "AWS_WEB_IDENTITY_TOKEN_FILE")):
65
+ return True
66
+ if secret_store is not None:
67
+ try:
68
+ if any(n["name"].startswith("AWS_") for n in secret_store.names()):
69
+ return True
70
+ except Exception:
71
+ pass
72
+ # Fall back to the botocore credential chain (a `[default]` profile, SSO, an instance role) so a
73
+ # plain `aws configure` enables cloud — not only explicit env vars. Cached (process-lifetime).
74
+ return _chain_has_credentials()
75
+
76
+
77
+ @functools.lru_cache(maxsize=1)
78
+ def _chain_has_credentials() -> bool:
79
+ try:
80
+ import boto3
81
+ return boto3.Session().get_credentials() is not None
82
+ except Exception:
83
+ return False
84
+
85
+
86
+ AWS_SECRET_PREFIX = "AWS_"
87
+
88
+
89
+ def load_aws_env(secret_store=None) -> None:
90
+ """Inject any ``AWS_*`` secrets into the process environment so botocore's default credential chain
91
+ actually picks them up. ``aws_configured`` already *counts* an ``AWS_*`` secret as creds (the gate);
92
+ this gives that its teeth — every boto3 client is built off the ambient chain, so without this a
93
+ secret-stored key would flip the gate green but real launches would fail with no ambient creds.
94
+
95
+ Real environment variables win (``setdefault``), so an operator's explicit env is never overridden.
96
+ Called at startup and after a matching secret is set (see routes/secrets)."""
97
+ if secret_store is None:
98
+ return
99
+ try:
100
+ names = [n["name"] for n in secret_store.names() if n["name"].startswith(AWS_SECRET_PREFIX)]
101
+ except Exception:
102
+ return
103
+ changed = False
104
+ for name in names:
105
+ if name not in os.environ:
106
+ value = secret_store.get(name)
107
+ if value is not None:
108
+ os.environ[name] = value
109
+ changed = True
110
+ if changed:
111
+ _chain_has_credentials.cache_clear()
112
+
113
+
114
+ def validate_credentials(region: str | None = None) -> str | None:
115
+ """A *live* credential check: None if the control-plane AWS creds authenticate (STS
116
+ GetCallerIdentity), else a short error string. Deliberately **separate** from the presence-based
117
+ ``aws_configured`` gate — the gate must stay deterministic and network-free (a transient STS blip
118
+ must never flip cloud off and strand a running setup), so validity is surfaced (a boot warning, the
119
+ Verify button), not folded into the gate. A region is required only to sign the request."""
120
+ region = region or os.environ.get("AWS_REGION") or os.environ.get("AWS_DEFAULT_REGION") or "us-east-1"
121
+ try:
122
+ import boto3
123
+ boto3.client("sts", region_name=region).get_caller_identity()
124
+ return None
125
+ except Exception as exc:
126
+ return str(exc) or exc.__class__.__name__
127
+
128
+
129
+ def cloud_status(data_root: str | None, secret_store=None, cred_status: dict | None = None) -> dict:
130
+ """The gate + its reasons — surfaced on /api/status and the settings endpoint so the UI can grey
131
+ out remote-compute options and explain why. ``cred_status`` (the cached live-validation result, see
132
+ cloud_backends) adds ``creds_valid``/``creds_error`` so the UI can persistently warn when the gate is
133
+ enabled but the credentials don't authenticate (``creds_valid is None`` = not yet checked / gate off
134
+ → no warning)."""
135
+ from . import cloud_deploy
136
+
137
+ remote = is_remote(data_root)
138
+ aws = aws_configured(secret_store)
139
+ enabled = remote and aws
140
+ out = {
141
+ "data_root": data_root,
142
+ "data_root_remote": remote,
143
+ "aws_configured": aws,
144
+ "cloud_enabled": enabled,
145
+ # Provider readiness — Cloud enabled AND the provider's env deployment config is adequate to launch
146
+ # WITHOUT per-pool input. Fargate readiness is what gates the S/M/L/XL presets (they rely on the
147
+ # env); a per-pool/dedicated Duck can still carry its own config independently of these.
148
+ "fargate_enabled": bool(enabled and not cloud_deploy.missing_fields("fargate", None)),
149
+ "ec2_enabled": bool(enabled and not cloud_deploy.missing_fields("ec2", None)),
150
+ "creds_valid": None,
151
+ "creds_error": None,
152
+ }
153
+ if cred_status is not None:
154
+ out["creds_valid"] = cred_status.get("valid")
155
+ out["creds_error"] = cred_status.get("error")
156
+ return out