duckstring 0.1.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. duckstring-0.3.0/MANIFEST.in +8 -0
  2. {duckstring-0.1.0 → duckstring-0.3.0}/PKG-INFO +64 -201
  3. duckstring-0.3.0/README.md +82 -0
  4. duckstring-0.3.0/pyproject.toml +78 -0
  5. duckstring-0.3.0/src/duckstring/__init__.py +26 -0
  6. duckstring-0.3.0/src/duckstring/acc.py +13 -0
  7. duckstring-0.3.0/src/duckstring/agg.py +12 -0
  8. duckstring-0.3.0/src/duckstring/catchment/app.py +123 -0
  9. duckstring-0.3.0/src/duckstring/catchment/asgi.py +30 -0
  10. duckstring-0.3.0/src/duckstring/catchment/dag.py +84 -0
  11. duckstring-0.3.0/src/duckstring/catchment/db.py +57 -0
  12. duckstring-0.3.0/src/duckstring/catchment/driver.py +1498 -0
  13. duckstring-0.3.0/src/duckstring/catchment/launcher.py +94 -0
  14. duckstring-0.3.0/src/duckstring/catchment/poller.py +238 -0
  15. duckstring-0.3.0/src/duckstring/catchment/registry.py +27 -0
  16. duckstring-0.3.0/src/duckstring/catchment/routes/__init__.py +22 -0
  17. duckstring-0.3.0/src/duckstring/catchment/routes/catchment.py +129 -0
  18. duckstring-0.3.0/src/duckstring/catchment/routes/data.py +532 -0
  19. duckstring-0.3.0/src/duckstring/catchment/routes/deploy.py +267 -0
  20. duckstring-0.3.0/src/duckstring/catchment/routes/draw.py +110 -0
  21. duckstring-0.3.0/src/duckstring/catchment/routes/duck.py +31 -0
  22. duckstring-0.3.0/src/duckstring/catchment/routes/duct.py +98 -0
  23. duckstring-0.3.0/src/duckstring/catchment/routes/orchestrate.py +302 -0
  24. duckstring-0.3.0/src/duckstring/catchment/routes/view.py +97 -0
  25. duckstring-0.3.0/src/duckstring/catchment/schema/001_init.sql +158 -0
  26. duckstring-0.3.0/src/duckstring/catchment/schema/002_ducts.sql +41 -0
  27. duckstring-0.3.0/src/duckstring/catchment/schema/003_pull_m.sql +5 -0
  28. duckstring-0.3.0/src/duckstring/catchment/schema/004_identity.sql +15 -0
  29. duckstring-0.3.0/src/duckstring/catchment/schema/005_pond_version_schema.sql +12 -0
  30. duckstring-0.3.0/src/duckstring/catchment/schema/006_refresh.sql +7 -0
  31. duckstring-0.3.0/src/duckstring/catchment/static/404.html +1 -0
  32. duckstring-0.3.0/src/duckstring/catchment/static/__next.__PAGE__.txt +7 -0
  33. duckstring-0.3.0/src/duckstring/catchment/static/__next._full.txt +16 -0
  34. duckstring-0.3.0/src/duckstring/catchment/static/__next._head.txt +6 -0
  35. duckstring-0.3.0/src/duckstring/catchment/static/__next._index.txt +5 -0
  36. duckstring-0.3.0/src/duckstring/catchment/static/__next._tree.txt +3 -0
  37. duckstring-0.3.0/src/duckstring/catchment/static/_next/static/Z066xVKlXhWUYRNRq4sUP/_buildManifest.js +16 -0
  38. duckstring-0.1.0/src/duckstring/catchment/static/_next/static/chunks/10u3y4bw1ayzs.js → duckstring-0.3.0/src/duckstring/catchment/static/_next/static/chunks/07lhk_q6pmm3r.js +1 -1
  39. duckstring-0.3.0/src/duckstring/catchment/static/_next/static/chunks/0d-69yy-ja-ef.css +1 -0
  40. duckstring-0.3.0/src/duckstring/catchment/static/_next/static/chunks/0dbhjjzl8qfwv.js +1 -0
  41. duckstring-0.3.0/src/duckstring/catchment/static/_next/static/chunks/0jvmviuftg5e2.css +1 -0
  42. duckstring-0.3.0/src/duckstring/catchment/static/_next/static/chunks/0kczw6usu-y-f.js +5 -0
  43. duckstring-0.3.0/src/duckstring/catchment/static/_next/static/chunks/0l6vjq3ae5vov.js +2 -0
  44. duckstring-0.3.0/src/duckstring/catchment/static/_next/static/chunks/11kjahy2ntf0n.js +31 -0
  45. duckstring-0.3.0/src/duckstring/catchment/static/_next/static/chunks/turbopack-03~mbvk_uplk_.js +1 -0
  46. duckstring-0.3.0/src/duckstring/catchment/static/_not-found/__next._full.txt +16 -0
  47. duckstring-0.3.0/src/duckstring/catchment/static/_not-found/__next._head.txt +6 -0
  48. duckstring-0.3.0/src/duckstring/catchment/static/_not-found/__next._index.txt +5 -0
  49. {duckstring-0.1.0 → duckstring-0.3.0}/src/duckstring/catchment/static/_not-found/__next._not-found.__PAGE__.txt +2 -2
  50. duckstring-0.3.0/src/duckstring/catchment/static/_not-found/__next._not-found.txt +5 -0
  51. {duckstring-0.1.0 → duckstring-0.3.0}/src/duckstring/catchment/static/_not-found/__next._tree.txt +2 -2
  52. duckstring-0.3.0/src/duckstring/catchment/static/_not-found.html +1 -0
  53. duckstring-0.3.0/src/duckstring/catchment/static/_not-found.txt +16 -0
  54. {duckstring-0.1.0 → duckstring-0.3.0}/src/duckstring/catchment/static/favicon.svg +1 -1
  55. duckstring-0.3.0/src/duckstring/catchment/static/index.html +1 -0
  56. duckstring-0.3.0/src/duckstring/catchment/static/index.txt +16 -0
  57. duckstring-0.3.0/src/duckstring/catchment/static/logo-mark.svg +15 -0
  58. {duckstring-0.1.0 → duckstring-0.3.0}/src/duckstring/catchment/static/logo.svg +1 -1
  59. duckstring-0.3.0/src/duckstring/cli/__init__.py +80 -0
  60. duckstring-0.3.0/src/duckstring/cli/_http.py +83 -0
  61. duckstring-0.3.0/src/duckstring/cli/catchment.py +368 -0
  62. duckstring-0.3.0/src/duckstring/cli/config.py +122 -0
  63. duckstring-0.3.0/src/duckstring/cli/control.py +164 -0
  64. duckstring-0.3.0/src/duckstring/cli/data.py +133 -0
  65. duckstring-0.3.0/src/duckstring/cli/deploy.py +150 -0
  66. duckstring-0.3.0/src/duckstring/cli/duct.py +190 -0
  67. duckstring-0.3.0/src/duckstring/cli/pond.py +234 -0
  68. duckstring-0.3.0/src/duckstring/cli/puddle.py +141 -0
  69. duckstring-0.3.0/src/duckstring/cli/status.py +290 -0
  70. duckstring-0.3.0/src/duckstring/cli/trigger.py +110 -0
  71. duckstring-0.3.0/src/duckstring/cli/window.py +178 -0
  72. duckstring-0.3.0/src/duckstring/core.py +525 -0
  73. duckstring-0.3.0/src/duckstring/dataplane.py +518 -0
  74. duckstring-0.3.0/src/duckstring/demo/catalog/.gitignore +8 -0
  75. duckstring-0.3.0/src/duckstring/demo/catalog/README.md +12 -0
  76. duckstring-0.3.0/src/duckstring/demo/catalog/pond.toml +4 -0
  77. duckstring-0.3.0/src/duckstring/demo/catalog/src/pond.py +61 -0
  78. duckstring-0.3.0/src/duckstring/demo/orders/.gitignore +8 -0
  79. duckstring-0.3.0/src/duckstring/demo/orders/README.md +11 -0
  80. duckstring-0.3.0/src/duckstring/demo/orders/pond.toml +4 -0
  81. duckstring-0.3.0/src/duckstring/demo/orders/src/pond.py +52 -0
  82. duckstring-0.3.0/src/duckstring/demo/priced/.gitignore +8 -0
  83. duckstring-0.3.0/src/duckstring/demo/priced/README.md +20 -0
  84. duckstring-0.3.0/src/duckstring/demo/priced/pond.toml +7 -0
  85. duckstring-0.3.0/src/duckstring/demo/priced/src/pond.py +23 -0
  86. duckstring-0.3.0/src/duckstring/demo/priced/src/puddles.py +59 -0
  87. duckstring-0.3.0/src/duckstring/demo/products/.gitignore +8 -0
  88. duckstring-0.3.0/src/duckstring/demo/products/README.md +9 -0
  89. duckstring-0.3.0/src/duckstring/demo/products/pond.toml +4 -0
  90. duckstring-0.3.0/src/duckstring/demo/products/src/pond.py +66 -0
  91. duckstring-0.3.0/src/duckstring/demo/reports/.gitignore +8 -0
  92. duckstring-0.3.0/src/duckstring/demo/reports/README.md +11 -0
  93. duckstring-0.3.0/src/duckstring/demo/reports/pond.toml +7 -0
  94. duckstring-0.3.0/src/duckstring/demo/reports/src/pond.py +24 -0
  95. duckstring-0.3.0/src/duckstring/demo/revenue/.gitignore +8 -0
  96. duckstring-0.3.0/src/duckstring/demo/revenue/README.md +12 -0
  97. duckstring-0.3.0/src/duckstring/demo/revenue/pond.toml +7 -0
  98. duckstring-0.3.0/src/duckstring/demo/revenue/src/pond.py +25 -0
  99. duckstring-0.3.0/src/duckstring/demo/revenue/src/puddles.py +35 -0
  100. duckstring-0.3.0/src/duckstring/demo/sales/.gitignore +8 -0
  101. duckstring-0.3.0/src/duckstring/demo/sales/README.md +11 -0
  102. duckstring-0.3.0/src/duckstring/demo/sales/pond.toml +7 -0
  103. duckstring-0.3.0/src/duckstring/demo/sales/src/pond.py +67 -0
  104. duckstring-0.3.0/src/duckstring/demo/sales/src/puddles.py +47 -0
  105. duckstring-0.3.0/src/duckstring/demo/transactions/.gitignore +8 -0
  106. duckstring-0.3.0/src/duckstring/demo/transactions/README.md +9 -0
  107. duckstring-0.3.0/src/duckstring/demo/transactions/pond.toml +4 -0
  108. duckstring-0.3.0/src/duckstring/demo/transactions/src/pond.py +46 -0
  109. duckstring-0.3.0/src/duckstring/duck/__init__.py +8 -0
  110. duckstring-0.3.0/src/duckstring/duck/__main__.py +178 -0
  111. duckstring-0.3.0/src/duckstring/duck/client.py +46 -0
  112. duckstring-0.3.0/src/duckstring/duck/core.py +173 -0
  113. duckstring-0.3.0/src/duckstring/duck/executor.py +187 -0
  114. duckstring-0.3.0/src/duckstring/engine/__init__.py +99 -0
  115. duckstring-0.3.0/src/duckstring/engine/catchment.py +635 -0
  116. duckstring-0.3.0/src/duckstring/engine/core.py +236 -0
  117. duckstring-0.3.0/src/duckstring/engine/pond.py +122 -0
  118. duckstring-0.3.0/src/duckstring/engine/worker.py +177 -0
  119. duckstring-0.3.0/src/duckstring/iceberg_catalog.py +204 -0
  120. duckstring-0.3.0/src/duckstring/iceberg_plane.py +288 -0
  121. duckstring-0.3.0/src/duckstring/keys.py +28 -0
  122. duckstring-0.3.0/src/duckstring/local/__init__.py +12 -0
  123. duckstring-0.3.0/src/duckstring/local/hydrate.py +114 -0
  124. duckstring-0.3.0/src/duckstring/local/project.py +55 -0
  125. duckstring-0.3.0/src/duckstring/local/runner.py +181 -0
  126. duckstring-0.3.0/src/duckstring/schema_contract.py +67 -0
  127. duckstring-0.3.0/src/duckstring/trickle/__init__.py +71 -0
  128. duckstring-0.3.0/src/duckstring/trickle/acc.py +139 -0
  129. duckstring-0.3.0/src/duckstring/trickle/agg.py +209 -0
  130. duckstring-0.3.0/src/duckstring/trickle/builder.py +1069 -0
  131. duckstring-0.3.0/src/duckstring/trickle/context.py +53 -0
  132. duckstring-0.3.0/src/duckstring/trickle/io.py +2283 -0
  133. duckstring-0.3.0/src/duckstring/trickle_builder.py +13 -0
  134. duckstring-0.3.0/src/duckstring/trickle_io.py +15 -0
  135. {duckstring-0.1.0 → duckstring-0.3.0}/src/duckstring.egg-info/PKG-INFO +64 -201
  136. duckstring-0.3.0/src/duckstring.egg-info/SOURCES.txt +146 -0
  137. duckstring-0.3.0/src/duckstring.egg-info/requires.txt +22 -0
  138. duckstring-0.1.0/README.md +0 -232
  139. duckstring-0.1.0/pyproject.toml +0 -46
  140. duckstring-0.1.0/src/duckstring/__init__.py +0 -13
  141. duckstring-0.1.0/src/duckstring/catchment/app.py +0 -9
  142. duckstring-0.1.0/src/duckstring/catchment/routes/__init__.py +0 -0
  143. duckstring-0.1.0/src/duckstring/catchment/routes/catchment.py +0 -0
  144. duckstring-0.1.0/src/duckstring/catchment/static/404.html +0 -1
  145. duckstring-0.1.0/src/duckstring/catchment/static/__next.__PAGE__.txt +0 -6
  146. duckstring-0.1.0/src/duckstring/catchment/static/__next._full.txt +0 -15
  147. duckstring-0.1.0/src/duckstring/catchment/static/__next._head.txt +0 -6
  148. duckstring-0.1.0/src/duckstring/catchment/static/__next._index.txt +0 -5
  149. duckstring-0.1.0/src/duckstring/catchment/static/__next._tree.txt +0 -2
  150. duckstring-0.1.0/src/duckstring/catchment/static/_next/static/chunks/07mne3zcixcz2.js +0 -3
  151. duckstring-0.1.0/src/duckstring/catchment/static/_next/static/chunks/0d-5~.f~0wtco.js +0 -5
  152. duckstring-0.1.0/src/duckstring/catchment/static/_next/static/chunks/0d3tkj33oycf2.js +0 -1
  153. duckstring-0.1.0/src/duckstring/catchment/static/_next/static/chunks/0fsi8lw5utejl.js +0 -1
  154. duckstring-0.1.0/src/duckstring/catchment/static/_next/static/chunks/0j18t3ydystkv.css +0 -1
  155. duckstring-0.1.0/src/duckstring/catchment/static/_next/static/chunks/11qk_vpw_zwkf.js +0 -31
  156. duckstring-0.1.0/src/duckstring/catchment/static/_next/static/chunks/turbopack-0e47rfx~.~3-8.js +0 -1
  157. duckstring-0.1.0/src/duckstring/catchment/static/_next/static/v34k52t11dNGCtVyPJ3io/_buildManifest.js +0 -11
  158. duckstring-0.1.0/src/duckstring/catchment/static/_not-found/__next._full.txt +0 -16
  159. duckstring-0.1.0/src/duckstring/catchment/static/_not-found/__next._head.txt +0 -6
  160. duckstring-0.1.0/src/duckstring/catchment/static/_not-found/__next._index.txt +0 -5
  161. duckstring-0.1.0/src/duckstring/catchment/static/_not-found/__next._not-found.txt +0 -5
  162. duckstring-0.1.0/src/duckstring/catchment/static/_not-found.html +0 -1
  163. duckstring-0.1.0/src/duckstring/catchment/static/_not-found.txt +0 -16
  164. duckstring-0.1.0/src/duckstring/catchment/static/dev/cache/turbopack/ee6e79b1/CURRENT +0 -0
  165. duckstring-0.1.0/src/duckstring/catchment/static/dev/logs/next-development.log +0 -0
  166. duckstring-0.1.0/src/duckstring/catchment/static/index.html +0 -1
  167. duckstring-0.1.0/src/duckstring/catchment/static/index.txt +0 -15
  168. duckstring-0.1.0/src/duckstring/cli/__init__.py +0 -16
  169. duckstring-0.1.0/src/duckstring/cli/catchment.py +0 -33
  170. duckstring-0.1.0/src/duckstring/core.py +0 -17
  171. duckstring-0.1.0/src/duckstring.egg-info/SOURCES.txt +0 -51
  172. duckstring-0.1.0/src/duckstring.egg-info/requires.txt +0 -10
  173. {duckstring-0.1.0 → duckstring-0.3.0}/LICENSE +0 -0
  174. {duckstring-0.1.0 → duckstring-0.3.0}/setup.cfg +0 -0
  175. {duckstring-0.1.0 → duckstring-0.3.0}/src/duckstring/__main__.py +0 -0
  176. {duckstring-0.1.0 → duckstring-0.3.0}/src/duckstring/catchment/__init__.py +0 -0
  177. {duckstring-0.1.0/src/duckstring/catchment/static/_next/static/v34k52t11dNGCtVyPJ3io → duckstring-0.3.0/src/duckstring/catchment/static/_next/static/Z066xVKlXhWUYRNRq4sUP}/_clientMiddlewareManifest.js +0 -0
  178. {duckstring-0.1.0/src/duckstring/catchment/static/_next/static/v34k52t11dNGCtVyPJ3io → duckstring-0.3.0/src/duckstring/catchment/static/_next/static/Z066xVKlXhWUYRNRq4sUP}/_ssgManifest.js +0 -0
  179. {duckstring-0.1.0 → duckstring-0.3.0}/src/duckstring/catchment/static/_next/static/chunks/03~yq9q893hmn.js +0 -0
  180. {duckstring-0.1.0 → duckstring-0.3.0}/src/duckstring/utils.py +0 -0
  181. {duckstring-0.1.0 → duckstring-0.3.0}/src/duckstring.egg-info/dependency_links.txt +0 -0
  182. {duckstring-0.1.0 → duckstring-0.3.0}/src/duckstring.egg-info/entry_points.txt +0 -0
  183. {duckstring-0.1.0 → duckstring-0.3.0}/src/duckstring.egg-info/top_level.txt +0 -0
@@ -0,0 +1,8 @@
1
+ # The sdist ships the installable package only — keep internal design notes and the test suite
2
+ # out of the PyPI source tarball (they're in the git repo for contributors).
3
+ prune tests
4
+ prune plans
5
+ prune experiment
6
+ prune docs
7
+ prune frontend
8
+ prune playground
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: duckstring
3
- Version: 0.1.0
4
- Summary: Duckstring is a Python-native data-mesh orchestration framework designed to be as naturally extensible as installing packages.
3
+ Version: 0.3.0
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
7
7
  Version 2.0, January 2004
@@ -205,6 +205,10 @@ License: Apache License
205
205
  See the License for the specific language governing permissions and
206
206
  limitations under the License.
207
207
 
208
+ Project-URL: Homepage, https://duckstring.com
209
+ Project-URL: Documentation, https://docs.duckstring.com
210
+ Project-URL: Repository, https://github.com/duckstring-dev/duckstring
211
+ Project-URL: Playground, https://playground.duckstring.com
208
212
  Requires-Python: >=3.10
209
213
  Description-Content-Type: text/markdown
210
214
  License-File: LICENSE
@@ -212,242 +216,101 @@ Requires-Dist: duckdb>=0.10
212
216
  Requires-Dist: rich>=13.7
213
217
  Requires-Dist: typer>=0.12
214
218
  Requires-Dist: fastapi>=0.111
219
+ Requires-Dist: python-multipart>=0.0.9
215
220
  Requires-Dist: uvicorn[standard]>=0.29
221
+ Requires-Dist: httpx>=0.25
222
+ Requires-Dist: tomli>=2.0; python_version < "3.11"
223
+ Requires-Dist: tomli-w>=1.0
224
+ Requires-Dist: pyiceberg>=0.7
225
+ Requires-Dist: pyarrow>=14
226
+ Requires-Dist: pytz
227
+ Provides-Extra: iceberg
216
228
  Provides-Extra: dev
217
229
  Requires-Dist: pytest>=8.0; extra == "dev"
230
+ Requires-Dist: pytest-timeout>=0.5; extra == "dev"
218
231
  Requires-Dist: ruff>=0.3; extra == "dev"
219
232
  Requires-Dist: mypy>=1.8; extra == "dev"
220
233
  Dynamic: license-file
221
234
 
222
235
  # Duckstring
236
+ *There is no DAG.*
223
237
 
224
- Duckstring is a data pipeline framework built around modular, versioned nodes called **Ponds**. Each Pond specifies its immediate parents (with version), allowing for the formation of a DAG much like one would install packages.
238
+ 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.
225
239
 
226
- Pond execution is orchestrated within an environment - a **Catchment** - that controls storage and other global settings. It uses a pull-based system modelled after Kanban, with **Outlets** (terminal Ponds) sending demand upstream. This allows each Pond to be modified and deployed independently, with any paths in the DAG that are not attached to any Outlet automatically skipped.
240
+ Ponds are upgraded and deployed to Duckstring's pull-based Catchment (orchestrator) atomically - like upgrading a package - with the earlier version continuing to execute until there are no consumers dependent on it. Upstream defines constraints on what it can consume, downstream defines when it's needed, and the Catchment optimally executes the sequence of Ponds supplying it with the best currency and frequency as possible.
227
241
 
228
- Duckstring is built on the philosophy that most data pipelines are not truly "big data" and with good design can execute on a single compute node. It is primarily designed for batch and incremental workloads for tables on the order of tens of millions of rows (e.g. <50M).
229
-
230
- The default engine is DuckDB, though this is configurable. Duckstring is however an independent project and is not affiliated with, endorsed by, or maintained by the DuckDB project.
231
-
232
- -- **Note**: As the project is in development, most of the notes below should be read as *indended functionality*, and most features are not yet implemented.
242
+ You should not need to manage the DAG. You should not need global governance. You should know yourself and your suppliers and trust that you'll get what you need when you need it.
233
243
 
234
244
  ## Core Concepts
235
245
 
236
- - **Catchment**: Control environment - a FastAPI application
237
- - **Pond**: Versioned transformation unit with declared upstream dependencies - the main element of version control
238
- - **Inlet**: Pond with external dependencies and no upstream Ponds
239
- - **Outlet**: Pond with no downstream Ponds (e.g. outputs final data products)
240
- - **Ripple**: Unit operation within a Pond (e.g. a single transformation producing a table)
241
-
242
- ## Installation
243
-
244
- ```bash
245
- pip install duckstring
246
- ```
247
-
248
- ## Quickstart
249
-
250
- ### 1) Connect to a Catchment
251
-
252
- A Catchment is the execution environment, receiving Ponds and managing runs. It runs either as a local daemon or as a remote server, allowing you to start locally and seamlessly upgrade to a hosted/cloud server if you need to later.
253
-
254
- #### Start a Catchment Server
255
-
256
- To run a Catchment locally, run:
257
-
258
- ```bash
259
- duckstring catchment start --name dev --port 5000 --root ~/.duckstring/dev
260
- ```
261
-
262
- This will start a server with name 'dev' at port 5000 (the default, if none specified) and store Catchment details at `~/.duckstring/dev` (default is `~/.duckstring/{name}`). If any of these options are omitted you will be prompted on start.
263
-
264
- #### Connect to a Remote Server
265
-
266
- Alternatively, you can connect to a server running a Catchment:
267
-
268
- ```bash
269
- duckstring catchment connect --name dev --path https://path.to.catchment
270
- ```
271
-
272
- This will prompt for any necessary auth, and will add the Catchment under the specified name.
273
-
274
- #### Connect to *duckstring.com*
275
-
276
- There are future plans for a dedicated Catchment service at https://duckstring.com. If you're interested, please [contact me](mailto:dev@duckstring.com).
277
-
278
- ### 2) Define Pond(s)
279
-
280
- #### Demo Ponds
281
-
282
- If you want to see an example sequence of Ponds in action immediately, create three project directories and run one of these commands in each:
283
-
284
- ```bash
285
- duckstring pond demo inlet
286
- duckstring pond demo pond
287
- duckstring pond demo outlet
288
- ```
289
-
290
- It's recommended to do this before attempting to make your own so that you can get a feel for the structure.
291
-
292
- #### Custom Pond
293
-
294
- Create a project directory and run:
295
-
296
- ```bash
297
- duckstring pond init example_pond
298
- ```
299
-
300
- This will create a duckstring pond structure:
301
-
302
- ```text
303
- root/
304
- |-- src/
305
- | |-- pond.py
306
- |-- pond.toml
307
- |-- __main__.py
308
- |-- .gitignore
309
- |-- README.md
310
- ```
311
-
312
- Here `pond.py` contains the code for a single Ripple operation (currently blank), and `pond.toml` specifies the Pond name "example_pond" and version (defaulting to "0.1.0").
313
-
314
- ### 3) Deploy to Catchment
315
-
316
- #### From Local
317
-
318
- From a Pond's project root run:
319
-
320
- ```bash
321
- duckstring deploy dev
322
- ```
323
-
324
- This will read the pond name, version and type (Inlet, Pond, Outlet) from `pond.toml` and deploy the project contents to the Catchment specified by name (here `dev`).
325
-
326
- Alternatively, you can import the Pond using the Catchment UI.
327
-
328
- #### From Git
329
-
330
- If you are using git with a remote, you can deploy with:
246
+ The main elements:
331
247
 
332
- ```bash
333
- duckstring deploy dev --git {branch|commit|tag}
334
- ```
335
-
336
- This will use the current branch/commit/tag to define the Pond. Upon each execution the Catchment will clone the repository and run it.
337
-
338
- This can also be specified using the Catchment UI.
339
-
340
- ### 3) Execute
341
-
342
- Ponds are executed by sending a Demand signal from an Outlet. This propagates backwards through the DAG until it reaches each upstream Inlet, causing them to execute, with children beginning upon completion of all of their parents.
343
-
344
- These examples will use the Pond `outlet`, version `1.0.0`, as the execution reference. All examples may also be alternatively executed using the Catchment UI.
345
-
346
- #### Pulse
347
-
348
- To initiate a single run:
349
-
350
- ```bash
351
- duckstring pulse dev outlet
352
- ```
353
-
354
- The `pulse` mode emits a Demand signal from `outlet`, and when it begins execution, sends a Stop signal. This causes it to execute exactly once.
355
-
356
- This will automatically run against the maximum version available for that Pond. To use a specific version:
357
-
358
- ```bash
359
- duckstring pulse dev outlet --version 1
360
- ```
361
-
362
- #### Wave
363
-
364
- To continuously run:
365
-
366
- ```bash
367
- duckstring wave dev outlet
368
- ```
369
-
370
- The `wave` mode emits a Demand signal from `outlet`, and when it begins execution, sends *another* Demand signal. This causes it to execute continuously, as frequently as the DAG allows (i.e. at a period equal to the execution time of the slowest Ripple in any Pond).
371
-
372
- #### Tide
373
-
374
- To run at a scheduled frequency:
375
-
376
- ```bash
377
- duckstring tide dev outlet 15 2 * * * --local
378
- ```
379
-
380
- This would run at 2:15am every day local time, using cron syntax. Omitting the `--local` flag defaults to UTC.
381
-
382
- ### 4) Monitor
383
-
384
- To print out a summary of current processes in the Catchment:
385
-
386
- ```bash
387
- duckstring status dev
388
- ```
389
-
390
- This will print to CLI a summary for each Pond that is either currently executing or has Demand.
391
-
392
- To include all Ponds:
248
+ - **Catchment**: Control environment (FastAPI + UI + CLI)
249
+ - **Pond**: Versioned container with declared upstream dependencies
250
+ - **Ripple**: Unit operation within a Pond (e.g. a single transformation producing a table)
251
+ - **Trickle**: DBSP-based incremental engine operating within a Ripple
393
252
 
394
- ```bash
395
- duckstring status dev --all
396
- ```
253
+ Ponds are typed or referred to in context:
397
254
 
398
- ### 5) Retrieve Data
255
+ - **Source**: A parent Pond
256
+ - **Sink**: A child Pond
257
+ - **Inlet**: A Pond with external dependencies and no Sources
258
+ - **Outlet**: A Pond with no Sinks (e.g. outputs final data products)
399
259
 
400
- #### Get
260
+ To see the orchestration model in action without installing anything, try the [Duckstring Playground](https://playground.duckstring.com).
401
261
 
402
- The simplest way to retrieve data is to load by the Ripple name. This returns the entire contents of the directory, and does not require that the data be in a tabular format (e.g. SQL-compatible).
262
+ ## Quickstart
403
263
 
404
264
  ```bash
405
- duckstring get dev outlet daily
406
- ```
407
-
408
- This writes a directory `./ponds/outlet/daily` with the 'daily' Ripple's contents. You may also override the default location:
265
+ pip install duckstring
409
266
 
410
- ```bash
411
- duckstring get dev outlet daily --path ./daily_output
267
+ # Start a local Catchment (the runtime + web UI) — leave it running
268
+ duckstring catchment init --name dev
412
269
  ```
413
270
 
414
- #### SQL Query
415
-
416
- If the target is an SQL-compatible table (e.g. DuckDB or Parquet), an SQL statement may be sent directly, outputting the result to the command line:
271
+ Then, in another terminal:
417
272
 
418
273
  ```bash
419
- duckstring query dev outlet --sql "SELECT * FROM daily WHERE id=1;"
420
- ```
274
+ # Create the demo pipeline (transactions, products sales reports) and deploy it
275
+ mkdir demo && cd demo
276
+ duckstring pond demo
277
+ duckstring pond deploy --all -y
421
278
 
422
- Alternatively, include a file path:
279
+ # Run it end to end, once
280
+ duckstring trigger pulse reports
423
281
 
424
- ```bash
425
- duckstring query dev outlet --sql @path/to/query.sql
282
+ # Look around
283
+ duckstring status # live state of every Pond
284
+ duckstring query reports monthly_summary # peek at an output table
426
285
  ```
427
286
 
428
- Omitting the `--sql` statement queries with a default SELECT * LIMIT 10 on the specified table:
287
+ The Catchment also serves a live web UI at `http://127.0.0.1:7474` the Pond graph, freshness, run history, and the full trigger/control surface.
429
288
 
430
- ```bash
431
- duckstring query dev outlet daily
432
- ```
289
+ ## Execution
433
290
 
434
- ##### Write to file
291
+ Ponds execute on demand signals sent to an Outlet, in two flavours — **push** runs the lineage forward to a target freshness; **pull** propagates demand upstream so every Pond re-runs as its Sources update, naturally throttled to the bottleneck. Each comes as a one-shot or a standing trigger:
435
292
 
436
- To output to a file, include a flag for the file format, followed by the file name:
293
+ | | Once | Continuously |
294
+ |---|---|---|
295
+ | **Push** | Pulse | Tide |
296
+ | **Pull** | Tap | Wave |
437
297
 
438
- `--csv`: Comma-separated values
439
- `--json`: JSON records
440
- `--parquet`: Parquet file
298
+ 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.
441
299
 
442
- This writes by default to `./ponds/outlet/daily/{filename}`. To overrite the default location you may use the `--path` flag.
300
+ ## Going further
443
301
 
444
- For example, to execute an sql statement from file `query.sql` and write the result to CSV at the current directory:
302
+ Full documentation lives at **[docs.duckstring.com](https://docs.duckstring.com)**:
445
303
 
446
- ```bash
447
- duckstring query dev outlet --sql @query.sql --csv daily.csv --path .
448
- ```
304
+ - [Quickstart](https://docs.duckstring.com/getting-started/quickstart) — the path above, with explanations
305
+ - [Theory](https://docs.duckstring.com/theory) the freshness-based orchestration model in depth
306
+ - [Versioning](https://docs.duckstring.com/concepts/versioning) — SemVer on Ponds, concurrent major versions, atomic upgrades
307
+ - [Local testing](https://docs.duckstring.com/guides/local-testing) — Puddles: test a Pond before deploying it
308
+ - [Fault tolerance](https://docs.duckstring.com/guides/fault-tolerance) — retry budgets, failure states, recovery
309
+ - [Running a Catchment](https://docs.duckstring.com/guides/running-a-catchment) — hosting, authentication, platform deployment
310
+ - [CLI](https://docs.duckstring.com/reference/cli) / [HTTP API](https://docs.duckstring.com/reference/http-api) — full references
449
311
 
450
- ## Further Reading
312
+ There are future plans for a hosted Catchment service at [duckstring.com](https://duckstring.com). If you're interested, please [get in touch](mailto:dev@duckstring.com).
451
313
 
452
- For more detail on each component, please read the corresponding documentation in `docs/`.
314
+ ## License
453
315
 
316
+ [Apache 2.0](LICENSE)
@@ -0,0 +1,82 @@
1
+ # Duckstring
2
+ *There is no DAG.*
3
+
4
+ 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
+ Ponds are upgraded and deployed to Duckstring's pull-based Catchment (orchestrator) atomically - like upgrading a package - with the earlier version continuing to execute until there are no consumers dependent on it. Upstream defines constraints on what it can consume, downstream defines when it's needed, and the Catchment optimally executes the sequence of Ponds supplying it with the best currency and frequency as possible.
7
+
8
+ You should not need to manage the DAG. You should not need global governance. You should know yourself and your suppliers and trust that you'll get what you need when you need it.
9
+
10
+ ## Core Concepts
11
+
12
+ The main elements:
13
+
14
+ - **Catchment**: Control environment (FastAPI + UI + CLI)
15
+ - **Pond**: Versioned container with declared upstream dependencies
16
+ - **Ripple**: Unit operation within a Pond (e.g. a single transformation producing a table)
17
+ - **Trickle**: DBSP-based incremental engine operating within a Ripple
18
+
19
+ Ponds are typed or referred to in context:
20
+
21
+ - **Source**: A parent Pond
22
+ - **Sink**: A child Pond
23
+ - **Inlet**: A Pond with external dependencies and no Sources
24
+ - **Outlet**: A Pond with no Sinks (e.g. outputs final data products)
25
+
26
+ To see the orchestration model in action without installing anything, try the [Duckstring Playground](https://playground.duckstring.com).
27
+
28
+ ## Quickstart
29
+
30
+ ```bash
31
+ pip install duckstring
32
+
33
+ # Start a local Catchment (the runtime + web UI) — leave it running
34
+ duckstring catchment init --name dev
35
+ ```
36
+
37
+ Then, in another terminal:
38
+
39
+ ```bash
40
+ # Create the demo pipeline (transactions, products → sales → reports) and deploy it
41
+ mkdir demo && cd demo
42
+ duckstring pond demo
43
+ duckstring pond deploy --all -y
44
+
45
+ # Run it end to end, once
46
+ duckstring trigger pulse reports
47
+
48
+ # Look around
49
+ duckstring status # live state of every Pond
50
+ duckstring query reports monthly_summary # peek at an output table
51
+ ```
52
+
53
+ The Catchment also serves a live web UI at `http://127.0.0.1:7474` — the Pond graph, freshness, run history, and the full trigger/control surface.
54
+
55
+ ## Execution
56
+
57
+ Ponds execute on demand signals sent to an Outlet, in two flavours — **push** runs the lineage forward to a target freshness; **pull** propagates demand upstream so every Pond re-runs as its Sources update, naturally throttled to the bottleneck. Each comes as a one-shot or a standing trigger:
58
+
59
+ | | Once | Continuously |
60
+ |---|---|---|
61
+ | **Push** | Pulse | Tide |
62
+ | **Pull** | Tap | Wave |
63
+
64
+ 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
+ ## Going further
67
+
68
+ Full documentation lives at **[docs.duckstring.com](https://docs.duckstring.com)**:
69
+
70
+ - [Quickstart](https://docs.duckstring.com/getting-started/quickstart) — the path above, with explanations
71
+ - [Theory](https://docs.duckstring.com/theory) — the freshness-based orchestration model in depth
72
+ - [Versioning](https://docs.duckstring.com/concepts/versioning) — SemVer on Ponds, concurrent major versions, atomic upgrades
73
+ - [Local testing](https://docs.duckstring.com/guides/local-testing) — Puddles: test a Pond before deploying it
74
+ - [Fault tolerance](https://docs.duckstring.com/guides/fault-tolerance) — retry budgets, failure states, recovery
75
+ - [Running a Catchment](https://docs.duckstring.com/guides/running-a-catchment) — hosting, authentication, platform deployment
76
+ - [CLI](https://docs.duckstring.com/reference/cli) / [HTTP API](https://docs.duckstring.com/reference/http-api) — full references
77
+
78
+ There are future plans for a hosted Catchment service at [duckstring.com](https://duckstring.com). If you're interested, please [get in touch](mailto:dev@duckstring.com).
79
+
80
+ ## License
81
+
82
+ [Apache 2.0](LICENSE)
@@ -0,0 +1,78 @@
1
+ [project]
2
+ name = "duckstring"
3
+ version = "0.3.0"
4
+ description = "Build data pipelines the way you build software: version each transform, declare its dependencies, and Duckstring resolves the execution DAG automatically."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = { file = "LICENSE" }
8
+ authors = [
9
+ { name = "Duckstring", email = "dev@duckstring.com" }
10
+ ]
11
+
12
+ dependencies = [
13
+ "duckdb>=0.10",
14
+ "rich>=13.7",
15
+ "typer>=0.12",
16
+ "fastapi>=0.111",
17
+ "python-multipart>=0.0.9",
18
+ "uvicorn[standard]>=0.29",
19
+ "httpx>=0.25",
20
+ "tomli>=2.0; python_version < '3.11'",
21
+ "tomli-w>=1.0",
22
+ # The default data plane: an Apache Iceberg base layer (snapshots + schema metadata over the
23
+ # Parquet files we already write). Core deps because Iceberg is default-on; set
24
+ # DUCKSTRING_DATA_PLANE=parquet to opt out (the lighter fallback, also for offline Catchments that
25
+ # can't fetch DuckDB's iceberg extension). NB: we use our own file-backed catalog
26
+ # (duckstring.iceberg_catalog) rather than pyiceberg's SqlCatalog, so SQLAlchemy is NOT pulled in.
27
+ "pyiceberg>=0.7",
28
+ "pyarrow>=14",
29
+ # DuckDB requires pytz to convert TIMESTAMPTZ columns to Python datetimes on fetch
30
+ # (our _duckstring_f freshness column is TIMESTAMPTZ, read back in dataplane/trickle export).
31
+ # It is otherwise only present transitively (pandas), so a clean install lacks it.
32
+ "pytz",
33
+ ]
34
+
35
+ [project.optional-dependencies]
36
+ # Retained for back-compat: the Iceberg deps are in core now (Iceberg is the default plane).
37
+ iceberg = []
38
+ dev = [
39
+ "pytest>=8.0",
40
+ "pytest-timeout>=0.5",
41
+ "ruff>=0.3",
42
+ "mypy>=1.8",
43
+ ]
44
+
45
+ [tool.setuptools]
46
+ package-dir = { "" = "src" }
47
+
48
+ [tool.setuptools.package-data]
49
+ "duckstring.catchment" = ["static/**/*", "schema/*.sql"]
50
+ "duckstring" = ["demo/**/*", "demo/**/.gitignore"]
51
+
52
+ [tool.setuptools.packages.find]
53
+ where = ["src"]
54
+
55
+ [project.urls]
56
+ Homepage = "https://duckstring.com"
57
+ Documentation = "https://docs.duckstring.com"
58
+ Repository = "https://github.com/duckstring-dev/duckstring"
59
+ Playground = "https://playground.duckstring.com"
60
+
61
+ [project.scripts]
62
+ duckstring = "duckstring.cli:main"
63
+ ds = "duckstring.cli:main"
64
+
65
+ [tool.pytest.ini_options]
66
+ testpaths = ["tests"]
67
+ # Tight enough to catch an accidental real sleep (sim tests finish in ms), loose enough to absorb
68
+ # cold-runner I/O on CI (SQLite migrate / uvicorn startup can exceed 1s on shared runners).
69
+ timeout = 5
70
+
71
+ [tool.ruff]
72
+ line-length = 128
73
+ target-version = "py310"
74
+ extend-exclude = ["experiment"]
75
+
76
+ [tool.ruff.lint]
77
+ select = ["E", "F", "I", "B"]
78
+ ignore = ["B008"] # typer.Option/Argument in defaults is intentional
@@ -0,0 +1,26 @@
1
+ from importlib.metadata import PackageNotFoundError
2
+ from importlib.metadata import version as _pkg_version
3
+
4
+ from .core import (
5
+ Catchment,
6
+ Pond,
7
+ Puddle,
8
+ Ripple,
9
+ puddle,
10
+ ripple,
11
+ )
12
+
13
+ try:
14
+ __version__ = _pkg_version("duckstring")
15
+ except PackageNotFoundError: # running from a source tree without an installed dist
16
+ __version__ = "0.0.0"
17
+
18
+ __all__ = [
19
+ "Catchment",
20
+ "Pond",
21
+ "Puddle",
22
+ "Ripple",
23
+ "puddle",
24
+ "ripple",
25
+ "__version__",
26
+ ]
@@ -0,0 +1,13 @@
1
+ """Backwards-compatible alias — scan (order-dependent) metric specs live in :mod:`duckstring.trickle.acc`.
2
+
3
+ Kept so ``from duckstring import acc`` works alongside ``from duckstring import agg``. See
4
+ :mod:`duckstring.trickle_io` for the rationale.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from .trickle import acc as _acc
10
+
11
+
12
+ def __getattr__(name): # PEP 562: forward all names to the relocated module
13
+ return getattr(_acc, name)
@@ -0,0 +1,12 @@
1
+ """Backwards-compatible alias — aggregate metric specs now live in :mod:`duckstring.trickle.agg`.
2
+
3
+ Kept so ``from duckstring import agg`` continues to work. See :mod:`duckstring.trickle_io` for the rationale.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from .trickle import agg as _agg
9
+
10
+
11
+ def __getattr__(name): # PEP 562: forward all names to the relocated module
12
+ return getattr(_agg, name)
@@ -0,0 +1,123 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import os
5
+ import secrets
6
+ from contextlib import asynccontextmanager
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+
10
+ from fastapi import FastAPI
11
+ from fastapi.responses import JSONResponse
12
+ from fastapi.staticfiles import StaticFiles
13
+
14
+ from .db import connect, ensure_identity, migrate
15
+ from .driver import Driver
16
+ from .launcher import NoopLauncher, SubprocessLauncher
17
+ from .routes import router
18
+
19
+ _STATIC_DIR = Path(__file__).parent / "static"
20
+
21
+
22
+ async def _scheduler(driver: Driver) -> None:
23
+ """Drive clock processes (Tide deadlines, window boundaries, Wave-on-idle) at next_wake."""
24
+ while True:
25
+ nw = driver.next_wake()
26
+ now = datetime.now(timezone.utc)
27
+ delay = (nw - now).total_seconds() if nw else 1.0
28
+ await asyncio.sleep(max(0.05, min(delay, 5.0)))
29
+ try:
30
+ driver.scheduler_tick()
31
+ except Exception as exc: # keep the loop alive
32
+ print(f"[catchment] scheduler error: {exc}", flush=True)
33
+
34
+
35
+ @asynccontextmanager
36
+ async def _lifespan(app: FastAPI):
37
+ base_url = app.state.base_url
38
+ if os.environ.get("DUCKSTRING_DISABLE_DUCKS"):
39
+ launcher = NoopLauncher()
40
+ else:
41
+ # Ducks dial back over the same authenticated surface — they present the API key as their token.
42
+ # base_url None = unknown (the platform picked the bind address): the launcher defers spawns
43
+ # until the dial-back middleware learns the address from the first request.
44
+ launcher = SubprocessLauncher(app.state.root, base_url, token=app.state.api_key or "")
45
+ driver = Driver(app.state.db, app.state.root, base_url, launcher)
46
+ app.state.driver = driver
47
+ app.state.launcher = launcher
48
+
49
+ # Restore: resume any Pond Runs that were in flight when the Catchment last stopped.
50
+ driver.resume_incomplete()
51
+
52
+ from .poller import run_poller
53
+
54
+ # The poller wakes immediately when a Draw acquires demand (so it solicits its upstream at once),
55
+ # instead of waiting for its next cycle. The driver signals across threads via the running loop.
56
+ wake = asyncio.Event()
57
+ loop = asyncio.get_running_loop()
58
+ driver.set_notify(lambda: loop.call_soon_threadsafe(wake.set))
59
+
60
+ scheduler = asyncio.create_task(_scheduler(driver))
61
+ poller = asyncio.create_task(run_poller(driver, app.state.root, wake))
62
+ try:
63
+ yield
64
+ finally:
65
+ for task in (scheduler, poller):
66
+ task.cancel()
67
+ try:
68
+ await task
69
+ except asyncio.CancelledError:
70
+ pass
71
+ launcher.shutdown_all()
72
+
73
+
74
+ def create_app(
75
+ root: Path, api_key: str | None = None, base_url: str | None = None, name: str | None = None
76
+ ) -> FastAPI:
77
+ root.mkdir(parents=True, exist_ok=True)
78
+ con = connect(root / "duck.db")
79
+ migrate(con)
80
+ ensure_identity(con, name or os.environ.get("DUCKSTRING_CATCHMENT_NAME"))
81
+
82
+ app = FastAPI(title="Duckstring Catchment", lifespan=_lifespan)
83
+ app.state.root = root
84
+ app.state.db = con
85
+ # API key: explicit argument, or the environment (useful for containers/remote serving). When
86
+ # set, every /api request (except /api/health) must present it — Bearer header or X-Duck-Token.
87
+ app.state.api_key = api_key or os.environ.get("DUCKSTRING_API_KEY") or None
88
+ # The address Ducks dial back to: explicit argument (the CLI passes its bind address), or the
89
+ # environment, or None — unknown, because the host platform picks the bind address (e.g. Posit
90
+ # Connect). When None it is learned from the first request's ASGI scope below.
91
+ app.state.base_url = base_url or os.environ.get("DUCKSTRING_CATCHMENT_URL") or None
92
+
93
+ @app.middleware("http")
94
+ async def _learn_dialback_address(request, call_next):
95
+ launcher = getattr(app.state, "launcher", None)
96
+ if launcher is not None and getattr(launcher, "base_url", "") is None:
97
+ server = request.scope.get("server") # the server's bound (host, port) per the ASGI spec
98
+ if server and server[1]: # a unix socket has port None — nothing TCP to dial
99
+ host = "127.0.0.1" if server[0] in ("0.0.0.0", "::") else server[0]
100
+ url = f"http://{host}:{server[1]}"
101
+ app.state.base_url = url
102
+ app.state.driver.base_url = url
103
+ launcher.set_base_url(url) # spawns any Ducks that were waiting on the address
104
+ return await call_next(request)
105
+
106
+ @app.middleware("http")
107
+ async def _require_api_key(request, call_next):
108
+ key = app.state.api_key
109
+ path = request.url.path
110
+ if key and path.startswith("/api") and path != "/api/health":
111
+ auth = request.headers.get("authorization", "")
112
+ supplied = auth[7:] if auth.lower().startswith("bearer ") else ""
113
+ supplied = supplied or request.headers.get("x-duck-token", "")
114
+ if not secrets.compare_digest(supplied, key):
115
+ return JSONResponse({"detail": "Invalid or missing API key"}, status_code=401)
116
+ return await call_next(request)
117
+
118
+ app.include_router(router, prefix="/api")
119
+
120
+ if _STATIC_DIR.exists():
121
+ app.mount("/", StaticFiles(directory=_STATIC_DIR, html=True), name="frontend")
122
+
123
+ return app