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.
- duckstring-0.5.0/CHANGELOG.md +93 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/MANIFEST.in +2 -0
- {duckstring-0.4.0/src/duckstring.egg-info → duckstring-0.5.0}/PKG-INFO +18 -2
- {duckstring-0.4.0 → duckstring-0.5.0}/README.md +4 -1
- {duckstring-0.4.0 → duckstring-0.5.0}/pyproject.toml +25 -1
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/__init__.py +0 -2
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/alerts/event.py +14 -3
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/alerts/webhook.py +4 -1
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/app.py +71 -4
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/auth.py +17 -0
- duckstring-0.5.0/src/duckstring/catchment/cloud.py +156 -0
- duckstring-0.5.0/src/duckstring/catchment/cloud_backends.py +156 -0
- duckstring-0.5.0/src/duckstring/catchment/cloud_deploy.py +77 -0
- duckstring-0.5.0/src/duckstring/catchment/dialback.py +41 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/driver.py +1440 -61
- duckstring-0.5.0/src/duckstring/catchment/ec2_launcher.py +372 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/egress_worker.py +28 -5
- duckstring-0.5.0/src/duckstring/catchment/fargate_launcher.py +298 -0
- duckstring-0.5.0/src/duckstring/catchment/flight_sql.py +98 -0
- duckstring-0.5.0/src/duckstring/catchment/launcher.py +332 -0
- duckstring-0.5.0/src/duckstring/catchment/pg_wire.py +317 -0
- duckstring-0.5.0/src/duckstring/catchment/pool_launcher.py +397 -0
- duckstring-0.5.0/src/duckstring/catchment/registry.py +85 -0
- duckstring-0.5.0/src/duckstring/catchment/relay.py +217 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/routes/__init__.py +4 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/routes/alerts.py +3 -1
- duckstring-0.5.0/src/duckstring/catchment/routes/catchment.py +476 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/routes/data.py +168 -22
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/routes/deploy.py +138 -8
- duckstring-0.5.0/src/duckstring/catchment/routes/duck.py +68 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/routes/metrics.py +36 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/routes/orchestrate.py +85 -3
- duckstring-0.5.0/src/duckstring/catchment/routes/pool.py +38 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/routes/secrets.py +17 -1
- duckstring-0.5.0/src/duckstring/catchment/routes/serving.py +104 -0
- duckstring-0.5.0/src/duckstring/catchment/schema/017_alert_renotify.sql +7 -0
- duckstring-0.5.0/src/duckstring/catchment/schema/018_duck.sql +11 -0
- duckstring-0.5.0/src/duckstring/catchment/schema/019_lineage.sql +18 -0
- duckstring-0.5.0/src/duckstring/catchment/schema/020_column_lineage.sql +15 -0
- duckstring-0.5.0/src/duckstring/catchment/schema/021_cloud.sql +44 -0
- duckstring-0.5.0/src/duckstring/catchment/schema/022_retire_duck_size.sql +25 -0
- duckstring-0.5.0/src/duckstring/catchment/schema/023_pool_provider.sql +7 -0
- duckstring-0.5.0/src/duckstring/catchment/schema/024_serving.sql +29 -0
- duckstring-0.5.0/src/duckstring/catchment/schema/025_dbt.sql +3 -0
- duckstring-0.5.0/src/duckstring/catchment/schema/026_deploy_config.sql +7 -0
- duckstring-0.5.0/src/duckstring/catchment/schema/027_persist.sql +14 -0
- duckstring-0.5.0/src/duckstring/catchment/schema/028_persist_timing.sql +3 -0
- duckstring-0.5.0/src/duckstring/catchment/serving.py +209 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/404.html +1 -1
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/__next.__PAGE__.txt +2 -2
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/__next._full.txt +3 -3
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/__next._head.txt +1 -1
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/__next._index.txt +2 -2
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/__next._tree.txt +2 -2
- duckstring-0.5.0/src/duckstring/catchment/static/_next/static/chunks/0_8xp0_1~w7l7.css +1 -0
- duckstring-0.5.0/src/duckstring/catchment/static/_next/static/chunks/15o1yi9qh60on.js +2 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/_not-found/__next._full.txt +2 -2
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/_not-found/__next._head.txt +1 -1
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/_not-found/__next._index.txt +2 -2
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/_not-found/__next._not-found.__PAGE__.txt +1 -1
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/_not-found/__next._not-found.txt +1 -1
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/_not-found/__next._tree.txt +2 -2
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/_not-found.html +1 -1
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/_not-found.txt +2 -2
- duckstring-0.5.0/src/duckstring/catchment/static/index.html +1 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/index.txt +3 -3
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/__init__.py +20 -1
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/_http.py +4 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/alert.py +9 -3
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/catchment.py +35 -1
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/control.py +29 -0
- duckstring-0.5.0/src/duckstring/cli/duck.py +185 -0
- duckstring-0.5.0/src/duckstring/cli/lineage.py +111 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/pond.py +50 -7
- duckstring-0.5.0/src/duckstring/cli/serve.py +94 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/core.py +57 -7
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/dataplane.py +446 -15
- duckstring-0.5.0/src/duckstring/dbt_mode.py +149 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/catalog/src/pond.py +4 -1
- duckstring-0.5.0/src/duckstring/demo/gh_actors/README.md +10 -0
- duckstring-0.5.0/src/duckstring/demo/gh_actors/pond.toml +6 -0
- duckstring-0.5.0/src/duckstring/demo/gh_actors/src/pond.py +25 -0
- duckstring-0.5.0/src/duckstring/demo/gh_events/README.md +21 -0
- duckstring-0.5.0/src/duckstring/demo/gh_events/pond.toml +4 -0
- duckstring-0.5.0/src/duckstring/demo/gh_events/src/pond.py +109 -0
- duckstring-0.5.0/src/duckstring/demo/gh_pushes/README.md +10 -0
- duckstring-0.5.0/src/duckstring/demo/gh_pushes/pond.toml +7 -0
- duckstring-0.5.0/src/duckstring/demo/gh_pushes/src/pond.py +23 -0
- duckstring-0.5.0/src/duckstring/demo/gh_repo_activity/README.md +14 -0
- duckstring-0.5.0/src/duckstring/demo/gh_repo_activity/pond.toml +7 -0
- duckstring-0.5.0/src/duckstring/demo/gh_repo_activity/src/pond.py +24 -0
- duckstring-0.5.0/src/duckstring/demo/gh_stars/README.md +10 -0
- duckstring-0.5.0/src/duckstring/demo/gh_stars/pond.toml +6 -0
- duckstring-0.5.0/src/duckstring/demo/gh_stars/src/pond.py +19 -0
- duckstring-0.5.0/src/duckstring/demo/gh_trending/README.md +10 -0
- duckstring-0.5.0/src/duckstring/demo/gh_trending/pond.toml +7 -0
- duckstring-0.5.0/src/duckstring/demo/gh_trending/src/pond.py +23 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/priced/src/pond.py +1 -1
- duckstring-0.5.0/src/duckstring/demo/shop_analytics/.gitignore +8 -0
- duckstring-0.5.0/src/duckstring/demo/shop_analytics/README.md +24 -0
- duckstring-0.5.0/src/duckstring/demo/shop_analytics/dbt/dbt_project.yml +12 -0
- duckstring-0.5.0/src/duckstring/demo/shop_analytics/dbt/models/orders_clean.sql +8 -0
- duckstring-0.5.0/src/duckstring/demo/shop_analytics/dbt/models/revenue_by_product.sql +8 -0
- duckstring-0.5.0/src/duckstring/demo/shop_analytics/dbt/models/sources.yml +9 -0
- duckstring-0.5.0/src/duckstring/demo/shop_analytics/dbt/models/top_products.sql +8 -0
- duckstring-0.5.0/src/duckstring/demo/shop_analytics/pond.toml +11 -0
- duckstring-0.5.0/src/duckstring/demo/shop_orders/.gitignore +3 -0
- duckstring-0.5.0/src/duckstring/demo/shop_orders/README.md +9 -0
- duckstring-0.5.0/src/duckstring/demo/shop_orders/pond.toml +4 -0
- duckstring-0.5.0/src/duckstring/demo/shop_orders/src/pond.py +32 -0
- duckstring-0.5.0/src/duckstring/demo/tpcds_category_revenue/README.md +13 -0
- duckstring-0.5.0/src/duckstring/demo/tpcds_category_revenue/pond.toml +7 -0
- duckstring-0.5.0/src/duckstring/demo/tpcds_category_revenue/src/pond.py +26 -0
- duckstring-0.5.0/src/duckstring/demo/tpcds_items/README.md +11 -0
- duckstring-0.5.0/src/duckstring/demo/tpcds_items/pond.toml +4 -0
- duckstring-0.5.0/src/duckstring/demo/tpcds_items/src/pond.py +80 -0
- duckstring-0.5.0/src/duckstring/demo/tpcds_priced/README.md +11 -0
- duckstring-0.5.0/src/duckstring/demo/tpcds_priced/pond.toml +8 -0
- duckstring-0.5.0/src/duckstring/demo/tpcds_priced/src/pond.py +26 -0
- duckstring-0.5.0/src/duckstring/demo/tpcds_sales/README.md +22 -0
- duckstring-0.5.0/src/duckstring/demo/tpcds_sales/pond.toml +4 -0
- duckstring-0.5.0/src/duckstring/demo/tpcds_sales/src/pond.py +76 -0
- duckstring-0.5.0/src/duckstring/demo/tpcds_store_revenue/README.md +10 -0
- duckstring-0.5.0/src/duckstring/demo/tpcds_store_revenue/pond.toml +7 -0
- duckstring-0.5.0/src/duckstring/demo/tpcds_store_revenue/src/pond.py +25 -0
- duckstring-0.5.0/src/duckstring/demo/tpcds_stores/README.md +10 -0
- duckstring-0.5.0/src/duckstring/demo/tpcds_stores/pond.toml +4 -0
- duckstring-0.5.0/src/duckstring/demo/tpcds_stores/src/pond.py +48 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/duck/__main__.py +130 -14
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/duck/client.py +8 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/duck/core.py +19 -3
- duckstring-0.5.0/src/duckstring/duck/dbt_executor.py +170 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/duck/executor.py +109 -19
- duckstring-0.5.0/src/duckstring/duck/pool_agent.py +151 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/egress/base.py +5 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/egress/object_store.py +87 -10
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/engine/__init__.py +2 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/engine/catchment.py +33 -4
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/engine/core.py +14 -0
- duckstring-0.5.0/src/duckstring/flock/__init__.py +298 -0
- duckstring-0.5.0/src/duckstring/flock/engines/__init__.py +0 -0
- duckstring-0.5.0/src/duckstring/flock/engines/athena.py +297 -0
- duckstring-0.5.0/src/duckstring/flock/equivalence.py +75 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/iceberg_plane.py +27 -16
- duckstring-0.5.0/src/duckstring/schema_contract.py +145 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/storage.py +141 -20
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/trickle/builder.py +59 -3
- duckstring-0.5.0/src/duckstring/trickle/capture.py +384 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/trickle/io.py +19 -2
- duckstring-0.5.0/src/duckstring/trickle/lineage.py +305 -0
- {duckstring-0.4.0 → duckstring-0.5.0/src/duckstring.egg-info}/PKG-INFO +18 -2
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring.egg-info/SOURCES.txt +92 -7
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring.egg-info/requires.txt +16 -0
- duckstring-0.4.0/src/duckstring/catchment/launcher.py +0 -103
- duckstring-0.4.0/src/duckstring/catchment/registry.py +0 -40
- duckstring-0.4.0/src/duckstring/catchment/routes/catchment.py +0 -161
- duckstring-0.4.0/src/duckstring/catchment/routes/duck.py +0 -32
- duckstring-0.4.0/src/duckstring/catchment/static/_next/static/chunks/0p30fzde51na5.css +0 -1
- duckstring-0.4.0/src/duckstring/catchment/static/_next/static/chunks/0qdnkaffkg.2-.js +0 -2
- duckstring-0.4.0/src/duckstring/catchment/static/index.html +0 -1
- duckstring-0.4.0/src/duckstring/schema_contract.py +0 -67
- duckstring-0.4.0/src/duckstring/utils.py +0 -3
- {duckstring-0.4.0 → duckstring-0.5.0}/LICENSE +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/setup.cfg +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/__main__.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/acc.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/agg.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/alerts/__init__.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/alerts/base.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/alerts/email.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/__init__.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/alert_worker.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/asgi.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/dag.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/data_lease.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/db.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/poller.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/routes/draw.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/routes/duct.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/routes/view.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/schema/001_init.sql +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/schema/002_ducts.sql +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/schema/003_pull_m.sql +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/schema/004_identity.sql +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/schema/005_pond_version_schema.sql +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/schema/006_refresh.sql +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/schema/007_catchment_key.sql +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/schema/008_spout.sql +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/schema/009_spout_state.sql +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/schema/010_spout_wake.sql +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/schema/011_spout_window.sql +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/schema/012_spout_node.sql +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/schema/013_changed_f.sql +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/schema/014_alert.sql +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/schema/016_alert_scope_major.sql +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/secrets.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/state_sync.py +0 -0
- {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
- {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
- {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
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/_next/static/chunks/03~yq9q893hmn.js +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/_next/static/chunks/07lhk_q6pmm3r.js +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/_next/static/chunks/0dbhjjzl8qfwv.js +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/_next/static/chunks/0jvmviuftg5e2.css +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/_next/static/chunks/0kczw6usu-y-f.js +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/_next/static/chunks/11kjahy2ntf0n.js +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/_next/static/chunks/turbopack-03~mbvk_uplk_.js +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/favicon.svg +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/logo-mark.svg +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/catchment/static/logo.svg +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/bulk.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/config.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/data.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/deploy.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/duct.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/puddle.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/secret.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/spout.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/status.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/trigger.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/cli/window.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/catalog/.gitignore +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/catalog/README.md +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/catalog/pond.toml +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/orders/.gitignore +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/orders/README.md +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/orders/pond.toml +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/orders/src/pond.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/priced/.gitignore +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/priced/README.md +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/priced/pond.toml +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/priced/src/puddles.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/products/.gitignore +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/products/README.md +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/products/pond.toml +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/products/src/pond.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/reports/.gitignore +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/reports/README.md +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/reports/pond.toml +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/reports/src/pond.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/revenue/.gitignore +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/revenue/README.md +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/revenue/pond.toml +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/revenue/src/pond.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/revenue/src/puddles.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/sales/.gitignore +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/sales/README.md +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/sales/pond.toml +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/sales/src/pond.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/sales/src/puddles.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/transactions/.gitignore +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/transactions/README.md +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/transactions/pond.toml +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/demo/transactions/src/pond.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/duck/__init__.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/egress/__init__.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/egress/credentials.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/egress/destination.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/egress/postgres.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/engine/pond.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/engine/worker.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/iceberg_catalog.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/keys.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/local/__init__.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/local/hydrate.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/local/project.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/local/runner.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/objects.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/trickle/__init__.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/trickle/acc.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/trickle/agg.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/trickle/context.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/trickle_builder.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring/trickle_io.py +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring.egg-info/dependency_links.txt +0 -0
- {duckstring-0.4.0 → duckstring-0.5.0}/src/duckstring.egg-info/entry_points.txt +0 -0
- {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,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: duckstring
|
|
3
|
-
Version: 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
|
-
|
|
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
|
-
|
|
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.
|
|
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]
|
|
@@ -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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|