duckstring 0.6.1__tar.gz → 0.7.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.6.1 → duckstring-0.7.0}/CHANGELOG.md +180 -0
- {duckstring-0.6.1/src/duckstring.egg-info → duckstring-0.7.0}/PKG-INFO +17 -7
- {duckstring-0.6.1 → duckstring-0.7.0}/README.md +8 -4
- {duckstring-0.6.1 → duckstring-0.7.0}/pyproject.toml +32 -8
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/app.py +3 -2
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/auth.py +44 -3
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/cloud.py +4 -2
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/db.py +3 -1
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/driver.py +492 -308
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/egress_worker.py +39 -28
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/flight_sql.py +25 -15
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/pg_wire.py +21 -14
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/poller.py +31 -20
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/routes/__init__.py +2 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/routes/data.py +131 -49
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/routes/deploy.py +38 -21
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/routes/draw.py +1 -2
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/routes/duck.py +2 -2
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/routes/metrics.py +10 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/routes/orchestrate.py +60 -57
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/routes/serving.py +60 -10
- duckstring-0.7.0/src/duckstring/catchment/routes/tap.py +64 -0
- duckstring-0.7.0/src/duckstring/catchment/schema/030_flock_accept.sql +5 -0
- duckstring-0.7.0/src/duckstring/catchment/schema/031_refresh.sql +83 -0
- duckstring-0.7.0/src/duckstring/catchment/schema/032_tap.sql +16 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/serving.py +42 -15
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/404.html +1 -1
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/__next.__PAGE__.txt +2 -2
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/__next._full.txt +2 -2
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/__next._head.txt +1 -1
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/__next._index.txt +1 -1
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/__next._tree.txt +1 -1
- duckstring-0.7.0/src/duckstring/catchment/static/_next/static/chunks/0h6ulcnnp2foi.js +2 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/_not-found/__next._full.txt +1 -1
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/_not-found/__next._head.txt +1 -1
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/_not-found/__next._index.txt +1 -1
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/_not-found/__next._not-found.__PAGE__.txt +1 -1
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/_not-found/__next._not-found.txt +1 -1
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/_not-found/__next._tree.txt +1 -1
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/_not-found.html +1 -1
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/_not-found.txt +1 -1
- duckstring-0.7.0/src/duckstring/catchment/static/index.html +1 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/index.txt +2 -2
- duckstring-0.7.0/src/duckstring/catchment/taps.py +236 -0
- duckstring-0.7.0/src/duckstring/changes.py +122 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/cli/__init__.py +22 -41
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/cli/catchment.py +9 -7
- duckstring-0.7.0/src/duckstring/cli/control.py +346 -0
- duckstring-0.7.0/src/duckstring/cli/data.py +351 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/cli/deploy.py +19 -2
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/cli/duck.py +10 -5
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/cli/duct.py +16 -13
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/cli/lineage.py +23 -15
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/cli/pond.py +1 -1
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/cli/puddle.py +3 -1
- duckstring-0.7.0/src/duckstring/cli/refresh.py +202 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/cli/spout.py +5 -3
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/cli/status.py +10 -10
- duckstring-0.7.0/src/duckstring/cli/tap.py +135 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/cli/window.py +61 -49
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/core.py +309 -18
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/dataplane.py +145 -43
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_events/src/pond.py +1 -1
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_repo_activity/README.md +2 -2
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/orders/src/pond.py +1 -1
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/priced/src/puddles.py +4 -4
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/products/src/pond.py +1 -1
- duckstring-0.7.0/src/duckstring/demo/reports/pond.toml +11 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/revenue/README.md +1 -1
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/revenue/src/puddles.py +2 -2
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/shop_analytics/README.md +1 -1
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/sql_reports/pond.toml +4 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_category_revenue/README.md +2 -2
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_category_revenue/src/pond.py +2 -2
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_items/src/pond.py +2 -2
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_sales/src/pond.py +1 -1
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_stores/src/pond.py +1 -1
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/transactions/src/pond.py +1 -1
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/duck/__init__.py +2 -2
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/duck/__main__.py +6 -3
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/duck/client.py +7 -4
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/duck/core.py +30 -4
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/duck/dbt_executor.py +4 -4
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/duck/executor.py +17 -13
- duckstring-0.7.0/src/duckstring/egress/attached.py +178 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/egress/base.py +6 -1
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/egress/destination.py +4 -3
- duckstring-0.7.0/src/duckstring/egress/ducklake.py +142 -0
- duckstring-0.7.0/src/duckstring/egress/postgres.py +55 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/engine/__init__.py +32 -28
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/engine/catchment.py +430 -240
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/engine/core.py +115 -54
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/engine/worker.py +15 -15
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/environments.py +31 -14
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/flock/__init__.py +164 -81
- duckstring-0.7.0/src/duckstring/flock/engines/athena.py +154 -0
- duckstring-0.7.0/src/duckstring/flock/engines/motherduck.py +62 -0
- duckstring-0.7.0/src/duckstring/flock/engines/remote_duckdb.py +225 -0
- duckstring-0.7.0/src/duckstring/flock/engines/sql_engine.py +206 -0
- duckstring-0.7.0/src/duckstring/flock/engines/starrocks.py +305 -0
- duckstring-0.7.0/src/duckstring/flock/translate.py +289 -0
- duckstring-0.7.0/src/duckstring/flock/verify.py +123 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/local/runner.py +8 -5
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/objects.py +75 -7
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/schema_contract.py +19 -2
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/sql_ripples.py +3 -2
- duckstring-0.7.0/src/duckstring/trickle/__init__.py +17 -0
- {duckstring-0.6.1 → duckstring-0.7.0/src/duckstring.egg-info}/PKG-INFO +17 -7
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring.egg-info/SOURCES.txt +21 -16
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring.egg-info/requires.txt +9 -2
- duckstring-0.6.1/src/duckstring/catchment/static/_next/static/chunks/09y-jd_5pt5my.js +0 -2
- duckstring-0.6.1/src/duckstring/catchment/static/index.html +0 -1
- duckstring-0.6.1/src/duckstring/cli/bulk.py +0 -135
- duckstring-0.6.1/src/duckstring/cli/control.py +0 -227
- duckstring-0.6.1/src/duckstring/cli/data.py +0 -273
- duckstring-0.6.1/src/duckstring/cli/serve.py +0 -97
- duckstring-0.6.1/src/duckstring/cli/trigger.py +0 -119
- duckstring-0.6.1/src/duckstring/demo/reports/pond.toml +0 -7
- duckstring-0.6.1/src/duckstring/egress/postgres.py +0 -170
- duckstring-0.6.1/src/duckstring/flock/engines/athena.py +0 -298
- duckstring-0.6.1/src/duckstring/flock/equivalence.py +0 -75
- duckstring-0.6.1/src/duckstring/trickle/__init__.py +0 -71
- duckstring-0.6.1/src/duckstring/trickle/acc.py +0 -122
- duckstring-0.6.1/src/duckstring/trickle/agg.py +0 -184
- duckstring-0.6.1/src/duckstring/trickle/builder.py +0 -1176
- duckstring-0.6.1/src/duckstring/trickle/capture.py +0 -363
- duckstring-0.6.1/src/duckstring/trickle/context.py +0 -53
- duckstring-0.6.1/src/duckstring/trickle/io.py +0 -2548
- duckstring-0.6.1/src/duckstring/trickle/lineage.py +0 -305
- {duckstring-0.6.1 → duckstring-0.7.0}/LICENSE +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/MANIFEST.in +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/setup.cfg +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/__init__.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/__main__.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/acc.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/agg.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/alerts/__init__.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/alerts/base.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/alerts/email.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/alerts/event.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/alerts/webhook.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/__init__.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/alert_worker.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/asgi.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/cloud_backends.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/cloud_deploy.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/dag.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/data_lease.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/dialback.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/ec2_launcher.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/fargate_launcher.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/launcher.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/pool_launcher.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/registry.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/relay.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/routes/alerts.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/routes/catchment.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/routes/duct.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/routes/pool.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/routes/secrets.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/routes/view.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/001_init.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/002_ducts.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/003_pull_m.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/004_identity.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/005_pond_version_schema.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/006_refresh.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/007_catchment_key.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/008_spout.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/009_spout_state.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/010_spout_wake.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/011_spout_window.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/012_spout_node.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/013_changed_f.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/014_alert.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/016_alert_scope_major.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/017_alert_renotify.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/018_duck.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/019_lineage.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/020_column_lineage.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/021_cloud.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/022_retire_duck_size.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/023_pool_provider.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/024_serving.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/025_dbt.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/026_deploy_config.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/027_persist.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/028_persist_timing.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/029_source_pins.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/secrets.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/state_sync.py +0 -0
- {duckstring-0.6.1/src/duckstring/catchment/static/_next/static/ZjcXhxxBm5wFt-ERTERYy → duckstring-0.7.0/src/duckstring/catchment/static/_next/static/FqgYlcTGJfIEhRPz1Nc4p}/_buildManifest.js +0 -0
- {duckstring-0.6.1/src/duckstring/catchment/static/_next/static/ZjcXhxxBm5wFt-ERTERYy → duckstring-0.7.0/src/duckstring/catchment/static/_next/static/FqgYlcTGJfIEhRPz1Nc4p}/_clientMiddlewareManifest.js +0 -0
- {duckstring-0.6.1/src/duckstring/catchment/static/_next/static/ZjcXhxxBm5wFt-ERTERYy → duckstring-0.7.0/src/duckstring/catchment/static/_next/static/FqgYlcTGJfIEhRPz1Nc4p}/_ssgManifest.js +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/_next/static/chunks/03~yq9q893hmn.js +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/_next/static/chunks/07lhk_q6pmm3r.js +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/_next/static/chunks/0_8xp0_1~w7l7.css +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/_next/static/chunks/0dbhjjzl8qfwv.js +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/_next/static/chunks/0jvmviuftg5e2.css +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/_next/static/chunks/0kczw6usu-y-f.js +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/_next/static/chunks/11kjahy2ntf0n.js +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/_next/static/chunks/turbopack-03~mbvk_uplk_.js +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/favicon.svg +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/logo-mark.svg +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/logo.svg +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/cli/_http.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/cli/alert.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/cli/config.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/cli/secret.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/dbt_mode.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/catalog/.gitignore +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/catalog/README.md +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/catalog/pond.toml +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/catalog/src/pond.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_actors/README.md +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_actors/pond.toml +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_actors/src/pond.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_events/README.md +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_events/pond.toml +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_pushes/README.md +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_pushes/pond.toml +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_pushes/src/pond.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_repo_activity/pond.toml +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_repo_activity/src/pond.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_stars/README.md +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_stars/pond.toml +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_stars/src/pond.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_trending/README.md +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_trending/pond.toml +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_trending/src/pond.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/orders/.gitignore +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/orders/README.md +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/orders/pond.toml +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/priced/.gitignore +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/priced/README.md +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/priced/pond.toml +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/priced/src/pond.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/products/.gitignore +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/products/README.md +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/products/pond.toml +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/reports/.gitignore +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/reports/README.md +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/reports/src/pond.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/revenue/.gitignore +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/revenue/pond.toml +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/revenue/src/pond.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/sales/.gitignore +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/sales/README.md +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/sales/pond.toml +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/sales/src/pond.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/sales/src/puddles.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/shop_analytics/.gitignore +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/shop_analytics/dbt/dbt_project.yml +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/shop_analytics/dbt/models/orders_clean.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/shop_analytics/dbt/models/revenue_by_product.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/shop_analytics/dbt/models/sources.yml +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/shop_analytics/dbt/models/top_products.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/shop_analytics/pond.toml +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/shop_orders/.gitignore +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/shop_orders/README.md +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/shop_orders/pond.toml +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/shop_orders/src/pond.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/sql_reports/README.md +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/sql_reports/sql/monthly_summary.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/sql_sales/README.md +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/sql_sales/data/tier_bands.csv +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/sql_sales/pond.toml +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/sql_sales/sql/daily_sales.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/sql_sales/sql/price_tiers.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/sql_sales/sql/sale_line.sql +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/sql_sales/src/puddles.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_category_revenue/pond.toml +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_items/README.md +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_items/pond.toml +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_priced/README.md +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_priced/pond.toml +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_priced/src/pond.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_sales/README.md +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_sales/pond.toml +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_store_revenue/README.md +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_store_revenue/pond.toml +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_store_revenue/src/pond.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_stores/README.md +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_stores/pond.toml +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/transactions/.gitignore +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/transactions/README.md +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/transactions/pond.toml +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/discover.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/duck/pool_agent.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/egress/__init__.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/egress/credentials.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/egress/object_store.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/engine/pond.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/flock/engines/__init__.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/keys.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/local/__init__.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/local/hydrate.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/local/project.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/pondignore.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/storage.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/trickle_builder.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/trickle_io.py +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring.egg-info/dependency_links.txt +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring.egg-info/entry_points.txt +0 -0
- {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring.egg-info/top_level.txt +0 -0
|
@@ -2,6 +2,186 @@
|
|
|
2
2
|
|
|
3
3
|
Notable changes per release. Versions before 0.5.0 are recorded in the git history and the `v*` tags.
|
|
4
4
|
|
|
5
|
+
## 0.7.0 — 2026-10-22
|
|
6
|
+
|
|
7
|
+
A single trigger, Refresh, replaces Tap, Wave, Pulse and Tide, and a Tap is now an app's access point to a
|
|
8
|
+
Pond. Also DuckDB 2.0 support and Spouts that deliver to a DuckLake. The old trigger commands and routes are
|
|
9
|
+
removed with no compatibility period, so read each section's Upgrading notes. Needs `duckstring-trickle` 0.2.1.
|
|
10
|
+
|
|
11
|
+
### One trigger: Refresh
|
|
12
|
+
|
|
13
|
+
Tap, Wave, Pulse and Tide are replaced by a single trigger, **Refresh**, which asks for a Pond's data to be at
|
|
14
|
+
least as fresh as now. A recurring Refresh repeats it:
|
|
15
|
+
|
|
16
|
+
- `duckstring refresh add reports --every 1d`: a day after the later of the last request and the last refresh of
|
|
17
|
+
the data (the old Tide).
|
|
18
|
+
- `duckstring refresh add reports --every 1d --start 06:00 [--on MON,TUE,WED,THU,FRI] [--until T]`: at set times,
|
|
19
|
+
on a window's grid, whatever else refreshes the Pond. New. A Catchment that was down asks once, for the latest
|
|
20
|
+
occurrence, when it returns.
|
|
21
|
+
- `duckstring refresh add reports --continuous` (or `--every 0s`): as fresh as the pipeline allows, paced to its
|
|
22
|
+
slowest step (the old Wave).
|
|
23
|
+
|
|
24
|
+
`duckstring refresh now reports` refreshes once (the old Pulse). A Pond can have any number of scheduled Refreshes and
|
|
25
|
+
one `--every` or continuous Refresh; `refresh ls` lists them with when each next asks, and `refresh rm` removes
|
|
26
|
+
one. In the web UI, a Pond's trigger buttons became a Refresh section (Refresh Now, and Recurring Refresh for
|
|
27
|
+
Continuous, Every N and schedules), and its recurring Refreshes and Taps are listed in a strip at the bottom of its
|
|
28
|
+
box on the canvas.
|
|
29
|
+
|
|
30
|
+
Upgrading:
|
|
31
|
+
|
|
32
|
+
- **Existing Waves become continuous Refreshes and Tides become `--every` Refreshes** (migration `031`).
|
|
33
|
+
- **`POST /api/ponds/{name}/refresh` changed meaning.** It used to mark a Pond for a rebuild; it now refreshes the
|
|
34
|
+
Pond (at `demand` level). The rebuild flag moved to `POST /api/ponds/{name}/rebuild`, and the command to
|
|
35
|
+
`duckstring control rebuild` (and the batch operation `refresh` to `rebuild`).
|
|
36
|
+
Scripts that called `/refresh` for a rebuild need updating.
|
|
37
|
+
- **Pull is gone from the engine.** A Tap's "anything newer" request no longer exists: a Refresh always asks for
|
|
38
|
+
data as of now. Wake now runs once if the Sources are fresher at that moment, rather than waiting for them.
|
|
39
|
+
- `duckstring trigger window POND add|list|remove` is now `duckstring window add|ls|rm POND`.
|
|
40
|
+
- **The old commands and routes are removed, with no compatibility period**: `duckstring trigger …`, `control
|
|
41
|
+
refresh`, `do --refresh`, and `POST /tap`, `/pulse`, `/wave`, `/tide` and `/untrigger`. Scripts need the new ones.
|
|
42
|
+
- **Both ends of a duct need this release.** A Draw sends no requests to an upstream Catchment older than Refresh
|
|
43
|
+
(there, `/refresh` would mark the Pond for a rebuild) and logs that it needs upgrading.
|
|
44
|
+
- **`catchment open --tap-on-get` is removed** (it went with Tap, and fired on every reader, including the web
|
|
45
|
+
UI's data viewer). An open Pond stays open. Taps, below, replace it.
|
|
46
|
+
- `/api/status` lists each Pond's `refreshes` (with `next_due`), `demand_f` and `rebuild_pending`, in place of
|
|
47
|
+
`trigger`, `has_pull`, `target_f` and `refresh_pending`.
|
|
48
|
+
- The Orchestration Theory page was removed from the docs; the Scheduling guide and the `refresh` and `window`
|
|
49
|
+
CLI reference cover what it said that users need.
|
|
50
|
+
|
|
51
|
+
### Taps: freshness that follows usage
|
|
52
|
+
|
|
53
|
+
A **Tap** is an access point on a Pond for one app: `duckstring refresh tap add reports --name dashboard --fresh-within
|
|
54
|
+
5m`. It has its own key, shown once, which reads the Pond's exposed tables (as a read-level key does) over HTTP
|
|
55
|
+
(`/api/query`, `/api/serve/query`), the Postgres wire or Arrow Flight, and is refused everywhere else. A query through it sends the Pond a Refresh when the newest request on the Pond, or the
|
|
56
|
+
data, is at least `--fresh-within` old (the `--every` clock, checked per query; `0`, the default, refreshes on every
|
|
57
|
+
query unless a request is already waiting), so the pipeline keeps up while the app is in use and goes quiet when it
|
|
58
|
+
isn't. A query is always answered at once from what is published. Queries with any other key never send demand.
|
|
59
|
+
|
|
60
|
+
- `refresh tap ls|rm|rotate POND`; the routes `/api/ponds/{name}/taps` (full). The web UI lists a Pond's Taps at the
|
|
61
|
+
foot of its Refresh section, adds them in a modal, and shows a Tap's details, Rotate and Delete when clicked.
|
|
62
|
+
- `/metrics` gains `duckstring_tap_queries_total` and `duckstring_tap_refreshes_total`; `/api/status` lists each
|
|
63
|
+
Pond's `taps` (never keys).
|
|
64
|
+
- **`GET /api/ponds/{name}/freshness`** says how fresh a Pond's data is: `freshness` (the point in time it
|
|
65
|
+
reflects), `changed` (when its output last changed), `refreshing`, and when each table and Object last changed. A
|
|
66
|
+
Tap's key can call it for its own Pond, and `Catchment.freshness()` wraps it. It replaces a route of the same name
|
|
67
|
+
that the HTTP reference described as the Pond's freshness but that listed a Trickle table's run freshnesses for
|
|
68
|
+
the data viewer; that list moved to `/api/ponds/{name}/tables/{table}/runs`.
|
|
69
|
+
- The Python `Catchment` client takes a Tap's key as `api_key`, and then needs no `pond`.
|
|
70
|
+
- The demo `reports` exposes `monthly_summary` (`[serve] tables`), so a Tap on it works out of the box.
|
|
71
|
+
- Migration `032` (`pond_tap`).
|
|
72
|
+
|
|
73
|
+
### The CLI, regrouped
|
|
74
|
+
|
|
75
|
+
The top level goes from 21 commands to 13: `status`, `query`, `get`, `refresh`, `window`, `control`, `data`, `pond`,
|
|
76
|
+
`catchment`, `duck`, `spout`, `secret` and `alert`. Tables and Objects are named `POND.NAME` everywhere, as in the
|
|
77
|
+
Python API. No old forms are kept:
|
|
78
|
+
|
|
79
|
+
| Before | Now |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `query POND [TABLE] [--sql SQL]` | `query POND.TABLE`, or `query SQL [--pond POND]` |
|
|
82
|
+
| `serve query SQL` | `query SQL` (with `--csv`, `--json` and `--parquet` too) |
|
|
83
|
+
| `get POND TABLE [--path]`, `get-object POND NAME [--out]` | `get POND.NAME [--out]` |
|
|
84
|
+
| `objects POND`, `serve status` | `data ls [POND]` (with each table's exposure and last change) |
|
|
85
|
+
| `delete-table POND TABLE`, `delete-object POND NAME` | `data delete POND.NAME` |
|
|
86
|
+
| `serve expose POND TABLE` | `data expose POND.TABLE` |
|
|
87
|
+
| `lineage [POND] [--table T]`, `trace POND.TABLE` | `data lineage [POND[.TABLE]]`, `data trace POND.TABLE` |
|
|
88
|
+
| `serve promote POND` | `pond promote POND` |
|
|
89
|
+
| `puddle ls\|show\|query` | `pond puddle ls\|show\|query` |
|
|
90
|
+
| `catchment open\|close` | `catchment duct open\|close` |
|
|
91
|
+
| `do PONDS --kill …` | `control kill PONDS …`: every `control` verb takes several Ponds and `--all`, `--tree`, `--between`, `--downstream`; `--wipe` is `control wipe`, `--remove` is `pond remove` |
|
|
92
|
+
|
|
93
|
+
`POST /api/serve/query` takes a `format` (`csv`, `json` or `parquet`) to return a file.
|
|
94
|
+
|
|
95
|
+
### Orchestration
|
|
96
|
+
|
|
97
|
+
- **Requests that arrive faster than the pipeline can serve them are paced to its slowest step** (the throttle),
|
|
98
|
+
always on, with no setting. A step that receives two requests during one run is held there, and one request goes
|
|
99
|
+
upstream each time it starts a new run.
|
|
100
|
+
- **A Refresh no longer refreshes an optional Source after its first build.** Demand propagates only to a Pond's
|
|
101
|
+
required Sources: a Pond never waits for an optional Source, so asking it ran it for nothing. An optional Source
|
|
102
|
+
that has never run still gets one request, so its Sink has output to read, and a Pond whose Sources are all
|
|
103
|
+
optional still asks every one of them. If an optional Source needs a cadence of its own, give it its own Refresh.
|
|
104
|
+
- **A repeat request for the freshness a Pond is already running is ignored.** It used to start a second
|
|
105
|
+
Run at the same freshness, which the no-change skip then completed as a pass, ending the Run in flight
|
|
106
|
+
early. A duct re-sending its downstream's request could cause it.
|
|
107
|
+
- **A join's Sources read together under saturation.** When some of a Pond's Sources are throttled and others
|
|
108
|
+
aren't, its requests reach the faster Sources only once the throttled ones start their next run, so a join
|
|
109
|
+
isn't held back by a fast Source that read early.
|
|
110
|
+
- **A Pond already throttled through another Refresh's Source still sends its first request.** It used to hold
|
|
111
|
+
that request until its own first completion, which the request itself was needed for.
|
|
112
|
+
- **Sleep keeps other Ponds' demand.** Sleeping a Pond (or a path with `--upstream`) clears its own demand, but a
|
|
113
|
+
pending request from another Pond it was serving is now sent to it again; before, that request stranded.
|
|
114
|
+
- **Demand is no longer lost at a halted Source.** A Pond that stops being blocked (after Clear, a recovery or a
|
|
115
|
+
redeploy) re-sends its newest unmet request upstream. Before, a request waiting on a Source whose Run had failed
|
|
116
|
+
or been killed stranded after Clear until something else demanded the path.
|
|
117
|
+
|
|
118
|
+
### Redeploys and contract failures
|
|
119
|
+
|
|
120
|
+
- **A redeploy takes effect while a recurring Refresh keeps the Pond busy.** A warm Duck used to keep running the
|
|
121
|
+
code it started with until it went idle, which a continuous Refresh never let it do, so a fix redeploy changed
|
|
122
|
+
nothing. A Duck now says which version it runs when it asks for work; a Duck on a replaced version finishes the
|
|
123
|
+
Runs it holds and exits, and a Duck on the new version picks up the next Run.
|
|
124
|
+
- **A Run in flight across a redeploy is recorded against the version that started it.** Its events were recorded
|
|
125
|
+
against the new version, which the database refused, and the Duck retried the refused event forever: the Pond
|
|
126
|
+
showed as running for good, and nothing it did afterwards was recorded.
|
|
127
|
+
- **A contract violation fails the Pond.** The Duck reported the Run's last Ripple as a success before it found the
|
|
128
|
+
output broke the version's contract, so the Catchment counted the Run done and dropped the failure that followed:
|
|
129
|
+
the Pond never showed as failed, and its Sinks ran on output that was never published. A Duck from before this
|
|
130
|
+
release still reports the old way, so redeploy Ducks with the Catchment.
|
|
131
|
+
|
|
132
|
+
### DuckDB 2.0
|
|
133
|
+
|
|
134
|
+
Duckstring and Trickle support DuckDB 2.0 as well as 1.5 (`duckdb>=1.5,<3`). DuckDB 2.0 needs Python 3.11 or
|
|
135
|
+
newer, so on Python 3.10 DuckDB stays at 1.5.
|
|
136
|
+
|
|
137
|
+
Upgrading:
|
|
138
|
+
|
|
139
|
+
- **Ripple code may need a change.** In DuckDB 2.0 a query run with `execute()` holds its connection until its rows
|
|
140
|
+
are read to the end, so `pond.con.execute(...).fetchone()` followed by another statement fails, and relations
|
|
141
|
+
no longer have `fetchone()`. Read with `fetchall()`. A Pond whose lock keeps DuckDB 1.5 stays on 1.5 while the
|
|
142
|
+
Catchment runs 2.0, so Ponds can move over one at a time.
|
|
143
|
+
- A column that is NULL in every row and has no other type (`SELECT NULL AS note`) is published as `INTEGER`, as
|
|
144
|
+
DuckDB 1.5 typed it. DuckDB 2.0 would give it its own NULL type, which refuses later values.
|
|
145
|
+
- Tables written with `if_changed=True` republish once after the upgrade, since change fingerprints are tied to
|
|
146
|
+
the DuckDB version.
|
|
147
|
+
- A Pond database written by DuckDB 2.0 can't be opened by 1.5. A Pond moved back to 1.5 rebuilds its database
|
|
148
|
+
from what it published, as it would after losing it.
|
|
149
|
+
- On DuckDB 2.0 an integer `//` or `%` by zero raises. A Flock job on StarRocks now fails in the same place and
|
|
150
|
+
runs locally, rather than publishing NULL.
|
|
151
|
+
- Trickle 0.2.1 is required.
|
|
152
|
+
|
|
153
|
+
### DuckLake Spouts
|
|
154
|
+
|
|
155
|
+
A Spout can deliver to a [DuckLake](https://ducklake.select):
|
|
156
|
+
`duckstring spout add reports --to 'ducklake:sqlite:/srv/lake/meta.sqlite?data_path=s3://lakehouse/files/'`. The
|
|
157
|
+
destination is DuckDB's own attach string for the lake (SQLite, Postgres or DuckDB catalogs), with `data_path`,
|
|
158
|
+
`schema` and the data path's credentials as options. Any table can be delivered: a merge Trickle by its primary
|
|
159
|
+
key, an append Trickle's new rows, a plain table replaced whole. A run's tables go in one transaction, so the
|
|
160
|
+
lake gains one snapshot per delivery, authored `duckstring`. Delivery is exactly-once. Duckstring never
|
|
161
|
+
migrates a lake's catalog, and leaves compaction and snapshot expiry to the lake's owner.
|
|
162
|
+
|
|
163
|
+
Also for Spouts:
|
|
164
|
+
|
|
165
|
+
- `spout resync` now re-delivers in full to Postgres too. Before, a destination that keeps its own record of
|
|
166
|
+
what it has received (Postgres) treated a resync as already delivered.
|
|
167
|
+
- A Postgres table first filled by a full load no longer gains a `_duckstring_f` column that later deliveries
|
|
168
|
+
left empty, and a column a table gains is added to the destination table.
|
|
169
|
+
- A Spout delivering all of a Pond's tables no longer delivers a merge Trickle's changelog as a table of its own.
|
|
170
|
+
|
|
171
|
+
### Contract checks on remote Ducks
|
|
172
|
+
|
|
173
|
+
A merge or append Trickle with an integer `SUM` column failed its version contract on the first run of a fresh
|
|
174
|
+
Duck (every run on Fargate, EC2 or a pool, after its idle shutdown). `SUM` of an integer is `HUGEINT`, which
|
|
175
|
+
Parquet stores as `DOUBLE`, so a Duck that rebuilt its database from what it had published held `DOUBLE` where the
|
|
176
|
+
contract had recorded `HUGEINT`. The check now compares types as published. Before this release, contract
|
|
177
|
+
failures found at that point were dropped without a trace, which hid it.
|
|
178
|
+
|
|
179
|
+
### Documentation and Playground
|
|
180
|
+
|
|
181
|
+
The Concepts and Guides have screenshots and recordings of the web UI and the Playground, and the landing page
|
|
182
|
+
leads with the Playground. The Playground has a `chain` example (`?example=chain`), the 1 s → 3 s → 1 s chain the
|
|
183
|
+
Orchestration page uses.
|
|
184
|
+
|
|
5
185
|
## 0.6.1 — 2026-10-08
|
|
6
186
|
|
|
7
187
|
### Performance
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: duckstring
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: An open source data engineering platform for DuckDB: versioned transformations, pull orchestration, incremental processing and a built-in catalog.
|
|
5
5
|
Author-email: Duckstring <dev@duckstring.com>
|
|
6
6
|
License: Apache License
|
|
@@ -212,7 +212,8 @@ Project-URL: Playground, https://playground.duckstring.com
|
|
|
212
212
|
Requires-Python: >=3.10
|
|
213
213
|
Description-Content-Type: text/markdown
|
|
214
214
|
License-File: LICENSE
|
|
215
|
-
Requires-Dist: duckdb<
|
|
215
|
+
Requires-Dist: duckdb<3,>=1.5
|
|
216
|
+
Requires-Dist: duckstring-trickle<0.3,>=0.2.1
|
|
216
217
|
Requires-Dist: rich>=13.7
|
|
217
218
|
Requires-Dist: typer>=0.12
|
|
218
219
|
Requires-Dist: fastapi>=0.111
|
|
@@ -232,6 +233,10 @@ Requires-Dist: sqlglot>=23; extra == "lineage"
|
|
|
232
233
|
Provides-Extra: aws
|
|
233
234
|
Requires-Dist: s3fs>=2023.1; extra == "aws"
|
|
234
235
|
Requires-Dist: boto3>=1.34; extra == "aws"
|
|
236
|
+
Requires-Dist: sqlglot<31,>=30.21; extra == "aws"
|
|
237
|
+
Provides-Extra: starrocks
|
|
238
|
+
Requires-Dist: pymysql>=1.1; extra == "starrocks"
|
|
239
|
+
Requires-Dist: sqlglot<31,>=30.21; extra == "starrocks"
|
|
235
240
|
Provides-Extra: dev
|
|
236
241
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
237
242
|
Requires-Dist: moto[server]>=5.0; extra == "dev"
|
|
@@ -239,10 +244,11 @@ Requires-Dist: pytest-timeout>=0.5; extra == "dev"
|
|
|
239
244
|
Requires-Dist: ruff>=0.3; extra == "dev"
|
|
240
245
|
Requires-Dist: mypy>=1.8; extra == "dev"
|
|
241
246
|
Requires-Dist: dbt-duckdb>=1.9; extra == "dev"
|
|
242
|
-
Requires-Dist: sqlglot
|
|
247
|
+
Requires-Dist: sqlglot<31,>=30.21; extra == "dev"
|
|
243
248
|
Requires-Dist: s3fs>=2023.1; extra == "dev"
|
|
244
249
|
Requires-Dist: boto3>=1.34; extra == "dev"
|
|
245
250
|
Requires-Dist: pg8000>=1.31; extra == "dev"
|
|
251
|
+
Requires-Dist: pymysql>=1.1; extra == "dev"
|
|
246
252
|
Dynamic: license-file
|
|
247
253
|
|
|
248
254
|
# Duckstring
|
|
@@ -274,9 +280,13 @@ The pipeline follows from those declarations, so it never has to be drawn by han
|
|
|
274
280
|
Schedules are set on the Ponds whose output is actually used, at the end of the pipeline. Duckstring works back from there, running each upstream Pond only as often as something downstream needs it, so a path nobody consumes never runs. A daily job is one command:
|
|
275
281
|
|
|
276
282
|
```bash
|
|
277
|
-
duckstring
|
|
283
|
+
duckstring refresh add reports --every 1d
|
|
278
284
|
```
|
|
279
285
|
|
|
286
|
+
Data can also stay fresh just because it's being used: a [Tap](https://docs.duckstring.com/guides/serving_apps_with_taps) gives an app its own key, and its queries refresh the Pond when the data is older than you allow, so nothing runs while the app is idle.
|
|
287
|
+
|
|
288
|
+
The [Playground](https://playground.duckstring.com) runs the demo pipeline in your browser, with a short guided tour of how it's scheduled.
|
|
289
|
+
|
|
280
290
|
## Incremental processing
|
|
281
291
|
|
|
282
292
|
A *Trickle* stores a table's changes as a Z-set: rows weighted +1 when added and -1 when removed. Joins and aggregations written with the Trickle builder recompute only the keys that changed since the last run, and handle updates and deletes correctly across any shape of join:
|
|
@@ -325,8 +335,8 @@ This starts a local Catchment, with its web UI at http://127.0.0.1:7474. Leave i
|
|
|
325
335
|
mkdir demo && cd demo
|
|
326
336
|
duckstring pond demo # transactions, products → sales → reports
|
|
327
337
|
duckstring pond deploy --all --yes
|
|
328
|
-
duckstring
|
|
329
|
-
duckstring query reports
|
|
338
|
+
duckstring refresh now reports # run the pipeline once, from the end
|
|
339
|
+
duckstring query reports.monthly_summary
|
|
330
340
|
```
|
|
331
341
|
|
|
332
342
|
The [Quickstart](https://docs.duckstring.com/quickstart) goes through the same steps with explanations.
|
|
@@ -343,7 +353,7 @@ The [Quickstart](https://docs.duckstring.com/quickstart) goes through the same s
|
|
|
343
353
|
|
|
344
354
|
## Documentation
|
|
345
355
|
|
|
346
|
-
[docs.duckstring.com](https://docs.duckstring.com) has the concepts, task guides, and a reference for the CLI, Python API, `pond.toml` and HTTP API. To try pull orchestration without installing anything,
|
|
356
|
+
[docs.duckstring.com](https://docs.duckstring.com) has the concepts, task guides, and a reference for the CLI, Python API, `pond.toml` and HTTP API. To try pull orchestration without installing anything, the [Playground](https://playground.duckstring.com) simulates it in the browser, with a guided tour.
|
|
347
357
|
|
|
348
358
|
## Status
|
|
349
359
|
|
|
@@ -27,9 +27,13 @@ The pipeline follows from those declarations, so it never has to be drawn by han
|
|
|
27
27
|
Schedules are set on the Ponds whose output is actually used, at the end of the pipeline. Duckstring works back from there, running each upstream Pond only as often as something downstream needs it, so a path nobody consumes never runs. A daily job is one command:
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
|
-
duckstring
|
|
30
|
+
duckstring refresh add reports --every 1d
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
Data can also stay fresh just because it's being used: a [Tap](https://docs.duckstring.com/guides/serving_apps_with_taps) gives an app its own key, and its queries refresh the Pond when the data is older than you allow, so nothing runs while the app is idle.
|
|
34
|
+
|
|
35
|
+
The [Playground](https://playground.duckstring.com) runs the demo pipeline in your browser, with a short guided tour of how it's scheduled.
|
|
36
|
+
|
|
33
37
|
## Incremental processing
|
|
34
38
|
|
|
35
39
|
A *Trickle* stores a table's changes as a Z-set: rows weighted +1 when added and -1 when removed. Joins and aggregations written with the Trickle builder recompute only the keys that changed since the last run, and handle updates and deletes correctly across any shape of join:
|
|
@@ -78,8 +82,8 @@ This starts a local Catchment, with its web UI at http://127.0.0.1:7474. Leave i
|
|
|
78
82
|
mkdir demo && cd demo
|
|
79
83
|
duckstring pond demo # transactions, products → sales → reports
|
|
80
84
|
duckstring pond deploy --all --yes
|
|
81
|
-
duckstring
|
|
82
|
-
duckstring query reports
|
|
85
|
+
duckstring refresh now reports # run the pipeline once, from the end
|
|
86
|
+
duckstring query reports.monthly_summary
|
|
83
87
|
```
|
|
84
88
|
|
|
85
89
|
The [Quickstart](https://docs.duckstring.com/quickstart) goes through the same steps with explanations.
|
|
@@ -96,7 +100,7 @@ The [Quickstart](https://docs.duckstring.com/quickstart) goes through the same s
|
|
|
96
100
|
|
|
97
101
|
## Documentation
|
|
98
102
|
|
|
99
|
-
[docs.duckstring.com](https://docs.duckstring.com) has the concepts, task guides, and a reference for the CLI, Python API, `pond.toml` and HTTP API. To try pull orchestration without installing anything,
|
|
103
|
+
[docs.duckstring.com](https://docs.duckstring.com) has the concepts, task guides, and a reference for the CLI, Python API, `pond.toml` and HTTP API. To try pull orchestration without installing anything, the [Playground](https://playground.duckstring.com) simulates it in the browser, with a guided tour.
|
|
100
104
|
|
|
101
105
|
## Status
|
|
102
106
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "duckstring"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.7.0"
|
|
4
4
|
description = "An open source data engineering platform for DuckDB: versioned transformations, pull orchestration, incremental processing and a built-in catalog."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -10,10 +10,13 @@ authors = [
|
|
|
10
10
|
]
|
|
11
11
|
|
|
12
12
|
dependencies = [
|
|
13
|
-
# The tested range: CI runs the suite on the floor and the latest release, and against the
|
|
14
|
-
# in an allowed-to-fail job. Raise the ceiling once the suite passes on the next major's release
|
|
15
|
-
# (plans/duckdb-2
|
|
16
|
-
"duckdb>=1.5,<
|
|
13
|
+
# The tested range: CI runs the suite on the floor (1.5, an LTS) and the latest release, and against the
|
|
14
|
+
# pre-release in an allowed-to-fail job. Raise the ceiling once the suite passes on the next major's release
|
|
15
|
+
# (plans/duckdb-2.md). A Pond environment gets this Duckstring, so these bounds hold there too.
|
|
16
|
+
"duckdb>=1.5,<3",
|
|
17
|
+
# The incremental engine (packages/trickle), released separately. A compatible range: while it's 0.x a
|
|
18
|
+
# minor release can change its API or storage format, so Duckstring moves to one deliberately.
|
|
19
|
+
"duckstring-trickle>=0.2.1,<0.3",
|
|
17
20
|
"rich>=13.7",
|
|
18
21
|
"typer>=0.12",
|
|
19
22
|
"fastapi>=0.111",
|
|
@@ -48,10 +51,19 @@ lineage = [
|
|
|
48
51
|
]
|
|
49
52
|
# AWS / cloud (plans/cloud-config.md): the object-store data plane + egress (s3fs — fsspec's small-file
|
|
50
53
|
# work; DuckDB does the bulk Parquet I/O over httpfs on its own), and the remote-compute launchers
|
|
51
|
-
# (EC2/Fargate/relay) + the Athena Flock engine (boto3). Opt-in
|
|
54
|
+
# (EC2/Fargate/relay) + the Athena Flock engine (boto3, and sqlglot to translate its jobs). Opt-in: a
|
|
55
|
+
# local/offline Catchment needs none.
|
|
52
56
|
aws = [
|
|
53
57
|
"s3fs>=2023.1",
|
|
54
58
|
"boto3>=1.34",
|
|
59
|
+
"sqlglot>=30.21,<31",
|
|
60
|
+
]
|
|
61
|
+
# The StarRocks Flock engine (plans/flock-coverage.md): the MySQL-protocol client, and sqlglot to translate
|
|
62
|
+
# jobs. sqlglot is pinned to one major because what an engine is trusted with is measured against its
|
|
63
|
+
# translations: re-run tests/test_flock_starrocks_conformance.py before widening it.
|
|
64
|
+
starrocks = [
|
|
65
|
+
"pymysql>=1.1",
|
|
66
|
+
"sqlglot>=30.21,<31",
|
|
55
67
|
]
|
|
56
68
|
dev = [
|
|
57
69
|
"pytest>=8.0",
|
|
@@ -61,10 +73,11 @@ dev = [
|
|
|
61
73
|
"ruff>=0.3",
|
|
62
74
|
"mypy>=1.8",
|
|
63
75
|
"dbt-duckdb>=1.9",
|
|
64
|
-
"sqlglot>=
|
|
76
|
+
"sqlglot>=30.21,<31",
|
|
65
77
|
"s3fs>=2023.1",
|
|
66
78
|
"boto3>=1.34",
|
|
67
79
|
"pg8000>=1.31",
|
|
80
|
+
"pymysql>=1.1",
|
|
68
81
|
]
|
|
69
82
|
|
|
70
83
|
[tool.setuptools]
|
|
@@ -77,6 +90,12 @@ package-dir = { "" = "src" }
|
|
|
77
90
|
[tool.setuptools.packages.find]
|
|
78
91
|
where = ["src"]
|
|
79
92
|
|
|
93
|
+
[tool.uv.workspace]
|
|
94
|
+
members = ["packages/trickle"]
|
|
95
|
+
|
|
96
|
+
[tool.uv.sources]
|
|
97
|
+
duckstring-trickle = { workspace = true }
|
|
98
|
+
|
|
80
99
|
[project.urls]
|
|
81
100
|
Homepage = "https://duckstring.com"
|
|
82
101
|
Documentation = "https://docs.duckstring.com"
|
|
@@ -88,7 +107,7 @@ duckstring = "duckstring.cli:main"
|
|
|
88
107
|
ds = "duckstring.cli:main"
|
|
89
108
|
|
|
90
109
|
[tool.pytest.ini_options]
|
|
91
|
-
testpaths = ["tests"]
|
|
110
|
+
testpaths = ["tests", "packages/trickle/tests"]
|
|
92
111
|
# Some suites share fixtures by importing `tests.<module>`; put the repo root on the path so a bare
|
|
93
112
|
# `pytest` (what CI runs) resolves them the same as `python -m pytest`.
|
|
94
113
|
pythonpath = ["."]
|
|
@@ -104,3 +123,8 @@ extend-exclude = ["experiment"]
|
|
|
104
123
|
[tool.ruff.lint]
|
|
105
124
|
select = ["E", "F", "I", "B"]
|
|
106
125
|
ignore = ["B008"] # typer.Option/Argument in defaults is intentional
|
|
126
|
+
|
|
127
|
+
[tool.ruff.lint.isort]
|
|
128
|
+
# Explicit, because ruff versions disagree on modules that only resolve through an alias: newer ones class
|
|
129
|
+
# duckstring.trickle.* (now packages/trickle, aliased) as third-party. `trickle` itself is third-party here.
|
|
130
|
+
known-first-party = ["duckstring"]
|
|
@@ -9,6 +9,7 @@ from pathlib import Path
|
|
|
9
9
|
from fastapi import FastAPI
|
|
10
10
|
from fastapi.staticfiles import StaticFiles
|
|
11
11
|
|
|
12
|
+
from ..trickle_io import first_row
|
|
12
13
|
from . import auth
|
|
13
14
|
from .db import connect, ensure_identity, migrate
|
|
14
15
|
from .driver import Driver
|
|
@@ -19,7 +20,7 @@ _STATIC_DIR = Path(__file__).parent / "static"
|
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
async def _scheduler(driver: Driver) -> None:
|
|
22
|
-
"""Drive clock processes (
|
|
23
|
+
"""Drive clock processes (recurring Refreshes, window boundaries) at next_wake."""
|
|
23
24
|
while True:
|
|
24
25
|
nw = driver.next_wake()
|
|
25
26
|
now = datetime.now(timezone.utc)
|
|
@@ -205,7 +206,7 @@ def create_app(
|
|
|
205
206
|
from ..storage import get_storage
|
|
206
207
|
from .data_lease import acquire_lease
|
|
207
208
|
|
|
208
|
-
cid = con.execute("SELECT value FROM catchment_meta WHERE key = 'id'")
|
|
209
|
+
cid = first_row(con.execute("SELECT value FROM catchment_meta WHERE key = 'id'"))
|
|
209
210
|
owner_id = cid[0] if cid else "unknown"
|
|
210
211
|
lease_store = get_storage(data_root)
|
|
211
212
|
acquire_lease(lease_store, owner_id) # raises LeaseConflict → the server does not start
|
|
@@ -4,9 +4,14 @@ The single-key model is split into a **total-ordered** ladder — ``read ⊂ dem
|
|
|
4
4
|
authorization check stays one integer comparison:
|
|
5
5
|
|
|
6
6
|
- **read** — read & query data only (no ducts, no demand).
|
|
7
|
-
- **demand** — read + create demand (
|
|
7
|
+
- **demand** — read + create demand (refresh now, recurring Refreshes) + connect a downstream duct.
|
|
8
8
|
- **full** — everything: deploy, the control verbs, windows, ducts, key rotation.
|
|
9
9
|
|
|
10
|
+
Besides the ladder, a **Tap** key (plans/taps.md) is a principal of its own: it names one Tap on one Pond line,
|
|
11
|
+
can query only that Tap's tables, and its queries may send that Pond a Refresh. It is accepted only on the
|
|
12
|
+
routes guarded by ``read_or_tap`` (the query routes) and refused (403) everywhere else. Tap keys are recognised
|
|
13
|
+
in open mode too, since what they add is the Refresh, not access.
|
|
14
|
+
|
|
10
15
|
Each route declares a *minimum* level via the ready-made dependencies ``read`` / ``demand`` / ``full``
|
|
11
16
|
(or ``duck`` for the internal worker channel). A request's level is the matched key's level. Routes are
|
|
12
17
|
**fail-closed**: :func:`audit_routes` runs at app construction and raises if any ``/api`` route (bar the
|
|
@@ -31,6 +36,7 @@ from enum import IntEnum
|
|
|
31
36
|
|
|
32
37
|
from fastapi import Depends, HTTPException, Request
|
|
33
38
|
|
|
39
|
+
from ..trickle_io import first_row
|
|
34
40
|
from .db import locked
|
|
35
41
|
|
|
36
42
|
# Routes that need no credential at all (parity with the old open `/api/health`).
|
|
@@ -79,7 +85,7 @@ def generate(con: sqlite3.Connection, levels: list[str] | None = None) -> dict[s
|
|
|
79
85
|
def ensure_duck_token(con: sqlite3.Connection) -> str:
|
|
80
86
|
"""Mint the internal Duck dial-back token once (persisted in ``catchment_meta``), then return it.
|
|
81
87
|
Stable across restarts so a Duck that outlived the Catchment keeps authenticating."""
|
|
82
|
-
row = con.execute("SELECT value FROM catchment_meta WHERE key = 'duck_token'")
|
|
88
|
+
row = first_row(con.execute("SELECT value FROM catchment_meta WHERE key = 'duck_token'"))
|
|
83
89
|
if row is not None:
|
|
84
90
|
return row[0]
|
|
85
91
|
token = secrets.token_urlsafe(32)
|
|
@@ -128,10 +134,26 @@ def auth_configured(con: sqlite3.Connection | None, api_key: str | None) -> bool
|
|
|
128
134
|
# ─── Request-time resolution ─────────────────────────────────────────────────
|
|
129
135
|
|
|
130
136
|
|
|
137
|
+
@dataclass(frozen=True)
|
|
138
|
+
class TapScope:
|
|
139
|
+
"""The Tap a key belongs to: its Pond line (a pond key) and its name."""
|
|
140
|
+
pond: str
|
|
141
|
+
name: str
|
|
142
|
+
|
|
143
|
+
|
|
131
144
|
@dataclass
|
|
132
145
|
class Principal:
|
|
133
146
|
level: Level | None # the access level of a user credential (None = unauthenticated)
|
|
134
147
|
is_duck: bool # the internal worker token (only valid on the duck channel)
|
|
148
|
+
tap: TapScope | None = None # a Tap's key: queries only that Tap's tables (level is None)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def tap_for_key(driver, supplied: str) -> TapScope | None:
|
|
152
|
+
"""The Tap a raw key belongs to, if any (the Driver holds the Taps' key hashes in memory)."""
|
|
153
|
+
if not supplied or driver is None:
|
|
154
|
+
return None
|
|
155
|
+
lookup = getattr(driver, "tap_for_hash", None)
|
|
156
|
+
return lookup(hash_key(supplied)) if lookup is not None else None
|
|
135
157
|
|
|
136
158
|
|
|
137
159
|
def _extract_token(request: Request) -> str:
|
|
@@ -152,6 +174,10 @@ def get_principal(request: Request) -> Principal:
|
|
|
152
174
|
if duck_token and supplied and secrets.compare_digest(supplied, duck_token):
|
|
153
175
|
return Principal(level=None, is_duck=True)
|
|
154
176
|
|
|
177
|
+
tap = tap_for_key(getattr(state, "driver", None), supplied)
|
|
178
|
+
if tap is not None:
|
|
179
|
+
return Principal(level=None, is_duck=False, tap=tap)
|
|
180
|
+
|
|
155
181
|
if not auth_configured(con, api_key):
|
|
156
182
|
return Principal(level=Level.FULL, is_duck=False) # open mode — no credential required
|
|
157
183
|
|
|
@@ -167,6 +193,16 @@ def get_principal(request: Request) -> Principal:
|
|
|
167
193
|
return Principal(level=None, is_duck=False)
|
|
168
194
|
|
|
169
195
|
|
|
196
|
+
def principal_for_key(driver, api_key: str | None, supplied: str) -> Principal | None:
|
|
197
|
+
"""The principal for a raw key on a non-HTTP front door (the pg / Flight wires): a Tap, else the key's
|
|
198
|
+
access level (open mode → FULL). None for a bad or absent key."""
|
|
199
|
+
tap = tap_for_key(driver, supplied)
|
|
200
|
+
if tap is not None:
|
|
201
|
+
return Principal(level=None, is_duck=False, tap=tap)
|
|
202
|
+
level = level_for_key(getattr(driver, "db", None), api_key, supplied)
|
|
203
|
+
return Principal(level=level, is_duck=False) if level is not None else None
|
|
204
|
+
|
|
205
|
+
|
|
170
206
|
def level_for_key(con: sqlite3.Connection | None, api_key: str | None, supplied: str) -> Level | None:
|
|
171
207
|
"""The access :class:`Level` for a raw key — the reusable core of :func:`get_principal`, for
|
|
172
208
|
non-HTTP front doors (the pg / Flight wire servers, where the key arrives as a password). Open mode
|
|
@@ -187,10 +223,14 @@ def level_for_key(con: sqlite3.Connection | None, api_key: str | None, supplied:
|
|
|
187
223
|
# ─── Route guards (declared on each route as `dependencies=[...]`) ────────────
|
|
188
224
|
|
|
189
225
|
|
|
190
|
-
def _level_guard(required: Level):
|
|
226
|
+
def _level_guard(required: Level, allow_tap: bool = False):
|
|
191
227
|
def dep(principal: Principal = Depends(get_principal)) -> Principal:
|
|
192
228
|
if principal.is_duck: # the worker token is for the duck channel only
|
|
193
229
|
raise HTTPException(status_code=403, detail="endpoint not available to the worker token")
|
|
230
|
+
if principal.tap is not None:
|
|
231
|
+
if allow_tap:
|
|
232
|
+
return principal
|
|
233
|
+
raise HTTPException(status_code=403, detail="a Tap's key can only query its Tap")
|
|
194
234
|
if principal.level is None:
|
|
195
235
|
raise HTTPException(status_code=401, detail="Invalid or missing API key")
|
|
196
236
|
if principal.level < required:
|
|
@@ -219,6 +259,7 @@ def _duck_guard():
|
|
|
219
259
|
read = _level_guard(Level.READ)
|
|
220
260
|
demand = _level_guard(Level.DEMAND)
|
|
221
261
|
full = _level_guard(Level.FULL)
|
|
262
|
+
read_or_tap = _level_guard(Level.READ, allow_tap=True) # the query routes: a read key or a Tap's key
|
|
222
263
|
duck = _duck_guard()
|
|
223
264
|
|
|
224
265
|
|
|
@@ -13,6 +13,8 @@ import functools
|
|
|
13
13
|
import os
|
|
14
14
|
import sqlite3
|
|
15
15
|
|
|
16
|
+
from ..trickle_io import first_row
|
|
17
|
+
|
|
16
18
|
DATA_ROOT_KEY = "data_root"
|
|
17
19
|
_REMOTE_SCHEMES = ("s3", "gs")
|
|
18
20
|
|
|
@@ -20,7 +22,7 @@ _REMOTE_SCHEMES = ("s3", "gs")
|
|
|
20
22
|
# ─── the generic key/value setting store ─────────────────────────────────────────
|
|
21
23
|
|
|
22
24
|
def get_setting(con: sqlite3.Connection, key: str) -> str | None:
|
|
23
|
-
row = con.execute("SELECT value FROM catchment_setting WHERE key = ?", (key,))
|
|
25
|
+
row = first_row(con.execute("SELECT value FROM catchment_setting WHERE key = ?", (key,)))
|
|
24
26
|
return row[0] if row else None
|
|
25
27
|
|
|
26
28
|
|
|
@@ -44,7 +46,7 @@ def has_published_data(con: sqlite3.Connection) -> bool:
|
|
|
44
46
|
"""Whether the Catchment has ever published output — a successful Pond Run. The set-once guard
|
|
45
47
|
keys off this: while it is False the data root is freely settable; once True, switching it would
|
|
46
48
|
strand the published data, so the setter refuses."""
|
|
47
|
-
row = con.execute("SELECT 1 FROM pond_run WHERE status = 'success' LIMIT 1")
|
|
49
|
+
row = first_row(con.execute("SELECT 1 FROM pond_run WHERE status = 'success' LIMIT 1"))
|
|
48
50
|
return row is not None
|
|
49
51
|
|
|
50
52
|
|
|
@@ -5,13 +5,15 @@ from contextlib import nullcontext
|
|
|
5
5
|
from datetime import datetime, timezone
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
+
from ..trickle_io import first_row
|
|
9
|
+
|
|
8
10
|
_SCHEMA_DIR = Path(__file__).parent / "schema"
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
def ensure_identity(con: sqlite3.Connection, name: str | None = None) -> None:
|
|
12
14
|
"""Mint this Catchment's stable id on first start (never changes), and set/refresh its optional
|
|
13
15
|
display name. Call after migrate()."""
|
|
14
|
-
row = con.execute("SELECT value FROM catchment_meta WHERE key = 'id'")
|
|
16
|
+
row = first_row(con.execute("SELECT value FROM catchment_meta WHERE key = 'id'"))
|
|
15
17
|
if row is None:
|
|
16
18
|
con.execute("INSERT INTO catchment_meta (key, value) VALUES ('id', ?)", (str(uuid.uuid4()),))
|
|
17
19
|
if name:
|