ducktide 0.0.1__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.
- ducktide-0.0.1/.gitignore +126 -0
- ducktide-0.0.1/LICENSE +21 -0
- ducktide-0.0.1/PKG-INFO +283 -0
- ducktide-0.0.1/README.md +257 -0
- ducktide-0.0.1/pyproject.toml +90 -0
- ducktide-0.0.1/src/ducktide/__init__.py +45 -0
- ducktide-0.0.1/src/ducktide/context.py +117 -0
- ducktide-0.0.1/src/ducktide/db.py +233 -0
- ducktide-0.0.1/src/ducktide/exceptions.py +90 -0
- ducktide-0.0.1/src/ducktide/orm/__init__.py +26 -0
- ducktide-0.0.1/src/ducktide/orm/base.py +268 -0
- ducktide-0.0.1/src/ducktide/orm/example.py +63 -0
- ducktide-0.0.1/src/ducktide/py.typed +0 -0
- ducktide-0.0.1/src/ducktide/table/__init__.py +58 -0
- ducktide-0.0.1/src/ducktide/table/_base.py +107 -0
- ducktide-0.0.1/src/ducktide/table/_io.py +202 -0
- ducktide-0.0.1/src/ducktide/table/_query.py +263 -0
- ducktide-0.0.1/src/ducktide/table/_write.py +147 -0
- ducktide-0.0.1/src/ducktide/time/__init__.py +25 -0
- ducktide-0.0.1/src/ducktide/time/_base.py +141 -0
- ducktide-0.0.1/src/ducktide/time/_ingest.py +204 -0
- ducktide-0.0.1/src/ducktide/time/_io.py +103 -0
- ducktide-0.0.1/src/ducktide/time/_query.py +163 -0
- ducktide-0.0.1/src/ducktide/time/timeseries_db.py +53 -0
- ducktide-0.0.1/src/ducktide/time/timeseries_model.py +124 -0
- ducktide-0.0.1/src/ducktide/time/timeseries_repo.py +49 -0
- ducktide-0.0.1/src/ducktide/utils/__init__.py +9 -0
- ducktide-0.0.1/src/ducktide/utils/path_validation.py +123 -0
- ducktide-0.0.1/src/ducktide/utils/sql.py +316 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
### Python template
|
|
2
|
+
.idea
|
|
3
|
+
.venv
|
|
4
|
+
.ruff_cache
|
|
5
|
+
.ty_cache
|
|
6
|
+
|
|
7
|
+
# HTML outputs from docstring examples (e.g. report.save("output/..."))
|
|
8
|
+
output/
|
|
9
|
+
|
|
10
|
+
### Don't expose API keys, etc.
|
|
11
|
+
.env
|
|
12
|
+
|
|
13
|
+
__marimo__
|
|
14
|
+
|
|
15
|
+
_tests
|
|
16
|
+
_book
|
|
17
|
+
_pdoc
|
|
18
|
+
docs/notebooks/*.html
|
|
19
|
+
docs/reports
|
|
20
|
+
docs/reports.md
|
|
21
|
+
_mkdocs
|
|
22
|
+
_benchmarks
|
|
23
|
+
_jupyter
|
|
24
|
+
_site
|
|
25
|
+
|
|
26
|
+
# LaTeX build artifacts
|
|
27
|
+
docs/paper/*.aux
|
|
28
|
+
docs/paper/*.fdb_latexmk
|
|
29
|
+
docs/paper/*.fls
|
|
30
|
+
docs/paper/*.log
|
|
31
|
+
docs/paper/*.out
|
|
32
|
+
docs/paper/*.toc
|
|
33
|
+
docs/paper/*.pdf
|
|
34
|
+
docs/paper/*.bbl
|
|
35
|
+
docs/paper/*.blg
|
|
36
|
+
|
|
37
|
+
# temp file used by Junie
|
|
38
|
+
.output.txt
|
|
39
|
+
|
|
40
|
+
# folder used for programs, e.g. uv, uvx, task, etc.
|
|
41
|
+
bin
|
|
42
|
+
|
|
43
|
+
# Byte-compiled / optimized / DLL files
|
|
44
|
+
__pycache__/
|
|
45
|
+
*.py[cod]
|
|
46
|
+
*$py.class
|
|
47
|
+
|
|
48
|
+
# Generated presentation files
|
|
49
|
+
presentation.html
|
|
50
|
+
presentation.pdf
|
|
51
|
+
*.pptx
|
|
52
|
+
|
|
53
|
+
# C extensions
|
|
54
|
+
*.so
|
|
55
|
+
|
|
56
|
+
# .DS_Store
|
|
57
|
+
.DS_Store
|
|
58
|
+
|
|
59
|
+
# Distribution / packaging
|
|
60
|
+
.Python
|
|
61
|
+
build/
|
|
62
|
+
develop-eggs/
|
|
63
|
+
dist/
|
|
64
|
+
downloads/
|
|
65
|
+
eggs/
|
|
66
|
+
.eggs/
|
|
67
|
+
lib/
|
|
68
|
+
lib64/
|
|
69
|
+
parts/
|
|
70
|
+
sdist/
|
|
71
|
+
var/
|
|
72
|
+
wheels/
|
|
73
|
+
share/python-wheels/
|
|
74
|
+
*.egg-info/
|
|
75
|
+
.installed.cfg
|
|
76
|
+
*.egg
|
|
77
|
+
MANIFEST
|
|
78
|
+
|
|
79
|
+
# Installer logs
|
|
80
|
+
pip-log.txt
|
|
81
|
+
pip-delete-this-directory.txt
|
|
82
|
+
|
|
83
|
+
# Unit test / coverage reports
|
|
84
|
+
htmlcov/
|
|
85
|
+
.tox/
|
|
86
|
+
.nox/
|
|
87
|
+
.coverage
|
|
88
|
+
.coverage.*
|
|
89
|
+
.cache
|
|
90
|
+
nosetests.xml
|
|
91
|
+
coverage.xml
|
|
92
|
+
coverage.json
|
|
93
|
+
*.cover
|
|
94
|
+
*.py,cover
|
|
95
|
+
.hypothesis/
|
|
96
|
+
.benchmarks/
|
|
97
|
+
.pytest_cache/
|
|
98
|
+
cover/
|
|
99
|
+
|
|
100
|
+
# Security scanning baselines (regenerate as needed)
|
|
101
|
+
.bandit-baseline.json
|
|
102
|
+
|
|
103
|
+
# Translations
|
|
104
|
+
*.mo
|
|
105
|
+
*.pot
|
|
106
|
+
|
|
107
|
+
# Cython debug symbols
|
|
108
|
+
cython_debug/
|
|
109
|
+
|
|
110
|
+
# Makefile -- `local.mk` is deliberately not ignored. The Makefile is template-owned and
|
|
111
|
+
# overwritten by every sync, so `local.mk` is the only place a repo's own make targets can
|
|
112
|
+
# live, and anything CI invokes has to be committed. rhiza's own `make e2e`, which
|
|
113
|
+
# rhiza_e2e.yml runs in all three language jobs, is exactly that case.
|
|
114
|
+
#
|
|
115
|
+
# `local-setup.sh` is not ignored either, and for the same reason one step further out: it
|
|
116
|
+
# is the hook every layer's `install` runs to provision a native binary the project needs
|
|
117
|
+
# (graphviz, libpq, pandoc), so CI invokes it on every job. A gitignored provisioning script
|
|
118
|
+
# is one that works on its author's machine and nowhere else.
|
|
119
|
+
|
|
120
|
+
.bandit-baseline.json
|
|
121
|
+
|
|
122
|
+
# Rust (rust-core bundle). Kept in core rather than the language layer because
|
|
123
|
+
# .gitignore has one owner and git opens it with O_NOFOLLOW, so it cannot be a
|
|
124
|
+
# per-layer file. These entries are inert in a Python repo.
|
|
125
|
+
target/
|
|
126
|
+
**/*.rs.bk
|
ducktide-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 Thomas Schmelzer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
ducktide-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ducktide
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Immutable Pydantic models and append-fast time series on DuckDB + Polars
|
|
5
|
+
Project-URL: Homepage, https://github.com/Jebel-Quant/ducktide
|
|
6
|
+
Project-URL: Repository, https://github.com/Jebel-Quant/ducktide
|
|
7
|
+
Project-URL: Issues, https://github.com/Jebel-Quant/ducktide/issues
|
|
8
|
+
Author: Thomas Schmelzer
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: duckdb,orm,polars,pydantic,repository-pattern,time-series
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Database
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: duckdb<2,>=1.5.5
|
|
22
|
+
Requires-Dist: polars<2,>=1.44.2
|
|
23
|
+
Requires-Dist: pyarrow>=25.0.0
|
|
24
|
+
Requires-Dist: pydantic<3,>=2.13.5
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# ducktide
|
|
28
|
+
|
|
29
|
+
[](https://github.com/Jebel-Quant/ducktide/releases)
|
|
30
|
+
|
|
31
|
+
[](https://github.com/jebel-quant/rhiza/releases/tag/v1.8.0)
|
|
32
|
+
[](LICENSE)
|
|
33
|
+
[](https://www.python.org/)
|
|
34
|
+
[](https://github.com/Jebel-Quant/ducktide/actions/workflows/rhiza_ci.yml)
|
|
35
|
+
[](https://github.com/astral-sh/ruff)
|
|
36
|
+
[](https://github.com/astral-sh/uv)
|
|
37
|
+
[](https://www.codefactor.io/repository/github/Jebel-Quant/ducktide)
|
|
38
|
+
[](https://scorecard.dev/viewer/?uri=github.com/Jebel-Quant/ducktide)
|
|
39
|
+
|
|
40
|
+
Immutable Pydantic models and append-fast time series on DuckDB + Polars.
|
|
41
|
+
|
|
42
|
+
ducktide is a small persistence layer with two halves:
|
|
43
|
+
|
|
44
|
+
- **Entity tables**: frozen Pydantic models persisted through repositories
|
|
45
|
+
(`DB` + `Table`). Models carry no `save()`/`find()`/`delete()` methods;
|
|
46
|
+
all reads and writes go through the table, so domain objects stay plain values.
|
|
47
|
+
- **Time series**: `TimeSeriesDB`, an append-only store for high-volume
|
|
48
|
+
numerical data (prices, volumes, sensor readings). Ingestion only appends rows
|
|
49
|
+
newer than what is already stored, per instrument, so re-ingesting an
|
|
50
|
+
overlapping frame is safe.
|
|
51
|
+
|
|
52
|
+
Both run on DuckDB (in-memory or a single file) and hand data back as Polars
|
|
53
|
+
DataFrames. There is no SQLAlchemy and no server.
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install ducktide
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Requires Python 3.11+.
|
|
62
|
+
|
|
63
|
+
## Entity tables
|
|
64
|
+
|
|
65
|
+
Define a domain model and its table mapping:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
import tempfile
|
|
69
|
+
from functools import partial
|
|
70
|
+
from pathlib import Path
|
|
71
|
+
from typing import ClassVar
|
|
72
|
+
|
|
73
|
+
from ducktide import DB, Table
|
|
74
|
+
from ducktide.orm import DomainModel, ORMModel
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Sensor(DomainModel):
|
|
78
|
+
table_name: ClassVar[str] = "sensor"
|
|
79
|
+
|
|
80
|
+
id: int
|
|
81
|
+
name: str
|
|
82
|
+
site: str
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class SensorORM(ORMModel, Sensor):
|
|
86
|
+
_table_name: ClassVar[str] = "sensor"
|
|
87
|
+
_domain_model: ClassVar[type] = Sensor
|
|
88
|
+
_primary_key: ClassVar[str] = "id"
|
|
89
|
+
_schema: ClassVar[dict[str, str]] = {
|
|
90
|
+
"id": "INTEGER PRIMARY KEY",
|
|
91
|
+
"name": "TEXT NOT NULL",
|
|
92
|
+
"site": "TEXT NOT NULL",
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
db = DB(tables_map={"sensor": partial(Table, model_class=SensorORM)}) # or db_path="sensors.duckdb"
|
|
97
|
+
|
|
98
|
+
db.sensor.bulk_insert(
|
|
99
|
+
[
|
|
100
|
+
Sensor(id=1, name="north", site="berlin"),
|
|
101
|
+
Sensor(id=2, name="south", site="zurich"),
|
|
102
|
+
]
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
db.sensor.select(site="berlin") # [SensorORM(id=1, name='north', site='berlin')]
|
|
106
|
+
db.sensor.get(2) # SensorORM(id=2, name='south', site='zurich')
|
|
107
|
+
db.sensor.to_frame() # polars.DataFrame
|
|
108
|
+
db.sensor.to_parquet(Path(tempfile.mkdtemp()) / "sensors.parquet")
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Time series
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from datetime import date, datetime
|
|
115
|
+
|
|
116
|
+
import polars as pl
|
|
117
|
+
|
|
118
|
+
from ducktide import TimeSeriesDB
|
|
119
|
+
|
|
120
|
+
ts = TimeSeriesDB() # or TimeSeriesDB("prices.duckdb")
|
|
121
|
+
|
|
122
|
+
ts.ingest(
|
|
123
|
+
"prices",
|
|
124
|
+
pl.DataFrame(
|
|
125
|
+
{
|
|
126
|
+
"timestamp": [datetime(2025, 1, 1, 9, 0), datetime(2025, 1, 1, 9, 1)],
|
|
127
|
+
"instrument_id": [1, 1],
|
|
128
|
+
"close": [100.0, 100.5],
|
|
129
|
+
}
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
ts.get_timeseries_frame("prices", instrument_id=1, start=date(2025, 1, 1))
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The table is created on first ingest. The timestamp column defaults to
|
|
137
|
+
`timestamp`; pass `TimeSeriesDB(time_col="ts")` to change it.
|
|
138
|
+
|
|
139
|
+
## Default database context
|
|
140
|
+
|
|
141
|
+
For notebooks and tests you can scope a default database instead of passing it
|
|
142
|
+
around:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from ducktide.context import use_db
|
|
146
|
+
|
|
147
|
+
with use_db(db):
|
|
148
|
+
... # code that calls ducktide.context.get_default_db()
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Development
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
uv sync --group test
|
|
155
|
+
uv run pytest
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Run `make help` to see all available targets:
|
|
159
|
+
|
|
160
|
+
```text
|
|
161
|
+
task section needs does
|
|
162
|
+
book Book test benchmark build the companion
|
|
163
|
+
stress book
|
|
164
|
+
hypothesis-test
|
|
165
|
+
paper
|
|
166
|
+
book-nav Book check that every
|
|
167
|
+
mkdocs nav entry
|
|
168
|
+
resolves in the
|
|
169
|
+
built book
|
|
170
|
+
marimo Book install start the Marimo
|
|
171
|
+
editor
|
|
172
|
+
marimo-validate Book install check that every
|
|
173
|
+
Marimo notebook runs
|
|
174
|
+
serve Book book build the book and
|
|
175
|
+
serve it on port
|
|
176
|
+
8000
|
|
177
|
+
clean Dev remove build
|
|
178
|
+
artifacts and stale
|
|
179
|
+
local branches
|
|
180
|
+
doctor Dev check local
|
|
181
|
+
prerequisites
|
|
182
|
+
setup Dev run the repository's
|
|
183
|
+
own environment
|
|
184
|
+
setup hook
|
|
185
|
+
docker-build Docker build the Docker
|
|
186
|
+
image
|
|
187
|
+
docker-clean Docker remove the Docker
|
|
188
|
+
image
|
|
189
|
+
docker-run Docker docker-build run the Docker
|
|
190
|
+
container
|
|
191
|
+
lfs-install Git LFS configure git-lfs
|
|
192
|
+
for this repository
|
|
193
|
+
lfs-pull Git LFS download the LFS
|
|
194
|
+
files for the
|
|
195
|
+
current branch
|
|
196
|
+
lfs-status Git LFS show the status of
|
|
197
|
+
LFS files
|
|
198
|
+
lfs-track Git LFS list the patterns
|
|
199
|
+
tracked by git-lfs
|
|
200
|
+
failed-workflows GitHub Helpers list recent failing
|
|
201
|
+
workflow runs
|
|
202
|
+
latest-release GitHub Helpers show information
|
|
203
|
+
about the latest
|
|
204
|
+
GitHub release
|
|
205
|
+
view-issues GitHub Helpers list open issues
|
|
206
|
+
view-prs GitHub Helpers list open pull
|
|
207
|
+
requests
|
|
208
|
+
whoami GitHub Helpers check github auth
|
|
209
|
+
status
|
|
210
|
+
workflow-status GitHub Helpers show recent runs for
|
|
211
|
+
the release workflow
|
|
212
|
+
paper Paper compile the LaTeX
|
|
213
|
+
paper to PDF
|
|
214
|
+
paper-clean Paper remove the LaTeX
|
|
215
|
+
build artifacts
|
|
216
|
+
presentation Presentation generate the HTML
|
|
217
|
+
slides with Marp
|
|
218
|
+
presentation-pdf Presentation generate the PDF
|
|
219
|
+
slides with Marp
|
|
220
|
+
presentation-serve Presentation serve the slides
|
|
221
|
+
with Marp's live
|
|
222
|
+
preview
|
|
223
|
+
all Python fmt deps test run every gate, as
|
|
224
|
+
docs-coverage CI does
|
|
225
|
+
security license
|
|
226
|
+
typecheck rhiza-test
|
|
227
|
+
coverage Python install measure coverage and
|
|
228
|
+
write
|
|
229
|
+
_tests/coverage.xml
|
|
230
|
+
deps Python install run deptry over the
|
|
231
|
+
contributed folders
|
|
232
|
+
docs-coverage Python install check docstring
|
|
233
|
+
coverage with
|
|
234
|
+
interrogate
|
|
235
|
+
install Python setup create the venv and
|
|
236
|
+
sync dependencies
|
|
237
|
+
license Python install scan for copyleft
|
|
238
|
+
licences
|
|
239
|
+
security Python install run the bandit
|
|
240
|
+
security scan
|
|
241
|
+
test Python install run all tests
|
|
242
|
+
test-lowest Python install run the tests
|
|
243
|
+
against the oldest
|
|
244
|
+
dependencies the
|
|
245
|
+
manifest allows
|
|
246
|
+
typecheck Python install run ty and/or mypy
|
|
247
|
+
(typechecker = ty |
|
|
248
|
+
mypy | both)
|
|
249
|
+
docs-examples Quality install check the fenced
|
|
250
|
+
examples in the docs
|
|
251
|
+
tree
|
|
252
|
+
fmt Quality run the pre-commit
|
|
253
|
+
hooks over all files
|
|
254
|
+
complexity Quality fail on a block
|
|
255
|
+
above the
|
|
256
|
+
cyclomatic-complexi…
|
|
257
|
+
ceiling
|
|
258
|
+
test-pyproject Quality install run the
|
|
259
|
+
pyproject.toml
|
|
260
|
+
structure checks,
|
|
261
|
+
verbosely
|
|
262
|
+
rhiza-test Quality install run the rhiza
|
|
263
|
+
repository checks
|
|
264
|
+
semgrep Quality run the semgrep
|
|
265
|
+
static analysis
|
|
266
|
+
rules
|
|
267
|
+
todos Quality list every TODO,
|
|
268
|
+
FIXME and HACK
|
|
269
|
+
comment
|
|
270
|
+
update Template sync the rhiza
|
|
271
|
+
template into this
|
|
272
|
+
repository
|
|
273
|
+
benchmark Testing extras install run the performance
|
|
274
|
+
benchmarks
|
|
275
|
+
hypothesis-test Testing extras install run the
|
|
276
|
+
property-based tests
|
|
277
|
+
stress Testing extras install run the stress and
|
|
278
|
+
load tests
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## License
|
|
282
|
+
|
|
283
|
+
MIT
|
ducktide-0.0.1/README.md
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# ducktide
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Jebel-Quant/ducktide/releases)
|
|
4
|
+
|
|
5
|
+
[](https://github.com/jebel-quant/rhiza/releases/tag/v1.8.0)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://www.python.org/)
|
|
8
|
+
[](https://github.com/Jebel-Quant/ducktide/actions/workflows/rhiza_ci.yml)
|
|
9
|
+
[](https://github.com/astral-sh/ruff)
|
|
10
|
+
[](https://github.com/astral-sh/uv)
|
|
11
|
+
[](https://www.codefactor.io/repository/github/Jebel-Quant/ducktide)
|
|
12
|
+
[](https://scorecard.dev/viewer/?uri=github.com/Jebel-Quant/ducktide)
|
|
13
|
+
|
|
14
|
+
Immutable Pydantic models and append-fast time series on DuckDB + Polars.
|
|
15
|
+
|
|
16
|
+
ducktide is a small persistence layer with two halves:
|
|
17
|
+
|
|
18
|
+
- **Entity tables**: frozen Pydantic models persisted through repositories
|
|
19
|
+
(`DB` + `Table`). Models carry no `save()`/`find()`/`delete()` methods;
|
|
20
|
+
all reads and writes go through the table, so domain objects stay plain values.
|
|
21
|
+
- **Time series**: `TimeSeriesDB`, an append-only store for high-volume
|
|
22
|
+
numerical data (prices, volumes, sensor readings). Ingestion only appends rows
|
|
23
|
+
newer than what is already stored, per instrument, so re-ingesting an
|
|
24
|
+
overlapping frame is safe.
|
|
25
|
+
|
|
26
|
+
Both run on DuckDB (in-memory or a single file) and hand data back as Polars
|
|
27
|
+
DataFrames. There is no SQLAlchemy and no server.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install ducktide
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Requires Python 3.11+.
|
|
36
|
+
|
|
37
|
+
## Entity tables
|
|
38
|
+
|
|
39
|
+
Define a domain model and its table mapping:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import tempfile
|
|
43
|
+
from functools import partial
|
|
44
|
+
from pathlib import Path
|
|
45
|
+
from typing import ClassVar
|
|
46
|
+
|
|
47
|
+
from ducktide import DB, Table
|
|
48
|
+
from ducktide.orm import DomainModel, ORMModel
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Sensor(DomainModel):
|
|
52
|
+
table_name: ClassVar[str] = "sensor"
|
|
53
|
+
|
|
54
|
+
id: int
|
|
55
|
+
name: str
|
|
56
|
+
site: str
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class SensorORM(ORMModel, Sensor):
|
|
60
|
+
_table_name: ClassVar[str] = "sensor"
|
|
61
|
+
_domain_model: ClassVar[type] = Sensor
|
|
62
|
+
_primary_key: ClassVar[str] = "id"
|
|
63
|
+
_schema: ClassVar[dict[str, str]] = {
|
|
64
|
+
"id": "INTEGER PRIMARY KEY",
|
|
65
|
+
"name": "TEXT NOT NULL",
|
|
66
|
+
"site": "TEXT NOT NULL",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
db = DB(tables_map={"sensor": partial(Table, model_class=SensorORM)}) # or db_path="sensors.duckdb"
|
|
71
|
+
|
|
72
|
+
db.sensor.bulk_insert(
|
|
73
|
+
[
|
|
74
|
+
Sensor(id=1, name="north", site="berlin"),
|
|
75
|
+
Sensor(id=2, name="south", site="zurich"),
|
|
76
|
+
]
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
db.sensor.select(site="berlin") # [SensorORM(id=1, name='north', site='berlin')]
|
|
80
|
+
db.sensor.get(2) # SensorORM(id=2, name='south', site='zurich')
|
|
81
|
+
db.sensor.to_frame() # polars.DataFrame
|
|
82
|
+
db.sensor.to_parquet(Path(tempfile.mkdtemp()) / "sensors.parquet")
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Time series
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from datetime import date, datetime
|
|
89
|
+
|
|
90
|
+
import polars as pl
|
|
91
|
+
|
|
92
|
+
from ducktide import TimeSeriesDB
|
|
93
|
+
|
|
94
|
+
ts = TimeSeriesDB() # or TimeSeriesDB("prices.duckdb")
|
|
95
|
+
|
|
96
|
+
ts.ingest(
|
|
97
|
+
"prices",
|
|
98
|
+
pl.DataFrame(
|
|
99
|
+
{
|
|
100
|
+
"timestamp": [datetime(2025, 1, 1, 9, 0), datetime(2025, 1, 1, 9, 1)],
|
|
101
|
+
"instrument_id": [1, 1],
|
|
102
|
+
"close": [100.0, 100.5],
|
|
103
|
+
}
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
ts.get_timeseries_frame("prices", instrument_id=1, start=date(2025, 1, 1))
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The table is created on first ingest. The timestamp column defaults to
|
|
111
|
+
`timestamp`; pass `TimeSeriesDB(time_col="ts")` to change it.
|
|
112
|
+
|
|
113
|
+
## Default database context
|
|
114
|
+
|
|
115
|
+
For notebooks and tests you can scope a default database instead of passing it
|
|
116
|
+
around:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from ducktide.context import use_db
|
|
120
|
+
|
|
121
|
+
with use_db(db):
|
|
122
|
+
... # code that calls ducktide.context.get_default_db()
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Development
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
uv sync --group test
|
|
129
|
+
uv run pytest
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Run `make help` to see all available targets:
|
|
133
|
+
|
|
134
|
+
```text
|
|
135
|
+
task section needs does
|
|
136
|
+
book Book test benchmark build the companion
|
|
137
|
+
stress book
|
|
138
|
+
hypothesis-test
|
|
139
|
+
paper
|
|
140
|
+
book-nav Book check that every
|
|
141
|
+
mkdocs nav entry
|
|
142
|
+
resolves in the
|
|
143
|
+
built book
|
|
144
|
+
marimo Book install start the Marimo
|
|
145
|
+
editor
|
|
146
|
+
marimo-validate Book install check that every
|
|
147
|
+
Marimo notebook runs
|
|
148
|
+
serve Book book build the book and
|
|
149
|
+
serve it on port
|
|
150
|
+
8000
|
|
151
|
+
clean Dev remove build
|
|
152
|
+
artifacts and stale
|
|
153
|
+
local branches
|
|
154
|
+
doctor Dev check local
|
|
155
|
+
prerequisites
|
|
156
|
+
setup Dev run the repository's
|
|
157
|
+
own environment
|
|
158
|
+
setup hook
|
|
159
|
+
docker-build Docker build the Docker
|
|
160
|
+
image
|
|
161
|
+
docker-clean Docker remove the Docker
|
|
162
|
+
image
|
|
163
|
+
docker-run Docker docker-build run the Docker
|
|
164
|
+
container
|
|
165
|
+
lfs-install Git LFS configure git-lfs
|
|
166
|
+
for this repository
|
|
167
|
+
lfs-pull Git LFS download the LFS
|
|
168
|
+
files for the
|
|
169
|
+
current branch
|
|
170
|
+
lfs-status Git LFS show the status of
|
|
171
|
+
LFS files
|
|
172
|
+
lfs-track Git LFS list the patterns
|
|
173
|
+
tracked by git-lfs
|
|
174
|
+
failed-workflows GitHub Helpers list recent failing
|
|
175
|
+
workflow runs
|
|
176
|
+
latest-release GitHub Helpers show information
|
|
177
|
+
about the latest
|
|
178
|
+
GitHub release
|
|
179
|
+
view-issues GitHub Helpers list open issues
|
|
180
|
+
view-prs GitHub Helpers list open pull
|
|
181
|
+
requests
|
|
182
|
+
whoami GitHub Helpers check github auth
|
|
183
|
+
status
|
|
184
|
+
workflow-status GitHub Helpers show recent runs for
|
|
185
|
+
the release workflow
|
|
186
|
+
paper Paper compile the LaTeX
|
|
187
|
+
paper to PDF
|
|
188
|
+
paper-clean Paper remove the LaTeX
|
|
189
|
+
build artifacts
|
|
190
|
+
presentation Presentation generate the HTML
|
|
191
|
+
slides with Marp
|
|
192
|
+
presentation-pdf Presentation generate the PDF
|
|
193
|
+
slides with Marp
|
|
194
|
+
presentation-serve Presentation serve the slides
|
|
195
|
+
with Marp's live
|
|
196
|
+
preview
|
|
197
|
+
all Python fmt deps test run every gate, as
|
|
198
|
+
docs-coverage CI does
|
|
199
|
+
security license
|
|
200
|
+
typecheck rhiza-test
|
|
201
|
+
coverage Python install measure coverage and
|
|
202
|
+
write
|
|
203
|
+
_tests/coverage.xml
|
|
204
|
+
deps Python install run deptry over the
|
|
205
|
+
contributed folders
|
|
206
|
+
docs-coverage Python install check docstring
|
|
207
|
+
coverage with
|
|
208
|
+
interrogate
|
|
209
|
+
install Python setup create the venv and
|
|
210
|
+
sync dependencies
|
|
211
|
+
license Python install scan for copyleft
|
|
212
|
+
licences
|
|
213
|
+
security Python install run the bandit
|
|
214
|
+
security scan
|
|
215
|
+
test Python install run all tests
|
|
216
|
+
test-lowest Python install run the tests
|
|
217
|
+
against the oldest
|
|
218
|
+
dependencies the
|
|
219
|
+
manifest allows
|
|
220
|
+
typecheck Python install run ty and/or mypy
|
|
221
|
+
(typechecker = ty |
|
|
222
|
+
mypy | both)
|
|
223
|
+
docs-examples Quality install check the fenced
|
|
224
|
+
examples in the docs
|
|
225
|
+
tree
|
|
226
|
+
fmt Quality run the pre-commit
|
|
227
|
+
hooks over all files
|
|
228
|
+
complexity Quality fail on a block
|
|
229
|
+
above the
|
|
230
|
+
cyclomatic-complexi…
|
|
231
|
+
ceiling
|
|
232
|
+
test-pyproject Quality install run the
|
|
233
|
+
pyproject.toml
|
|
234
|
+
structure checks,
|
|
235
|
+
verbosely
|
|
236
|
+
rhiza-test Quality install run the rhiza
|
|
237
|
+
repository checks
|
|
238
|
+
semgrep Quality run the semgrep
|
|
239
|
+
static analysis
|
|
240
|
+
rules
|
|
241
|
+
todos Quality list every TODO,
|
|
242
|
+
FIXME and HACK
|
|
243
|
+
comment
|
|
244
|
+
update Template sync the rhiza
|
|
245
|
+
template into this
|
|
246
|
+
repository
|
|
247
|
+
benchmark Testing extras install run the performance
|
|
248
|
+
benchmarks
|
|
249
|
+
hypothesis-test Testing extras install run the
|
|
250
|
+
property-based tests
|
|
251
|
+
stress Testing extras install run the stress and
|
|
252
|
+
load tests
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## License
|
|
256
|
+
|
|
257
|
+
MIT
|