fromscratch-pytsdb 0.4.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 (49) hide show
  1. fromscratch_pytsdb-0.4.0/.github/workflows/ci.yml +14 -0
  2. fromscratch_pytsdb-0.4.0/.github/workflows/docs.yml +16 -0
  3. fromscratch_pytsdb-0.4.0/.github/workflows/release.yml +58 -0
  4. fromscratch_pytsdb-0.4.0/.gitignore +11 -0
  5. fromscratch_pytsdb-0.4.0/.pre-commit-config.yaml +56 -0
  6. fromscratch_pytsdb-0.4.0/CLAUDE.md +52 -0
  7. fromscratch_pytsdb-0.4.0/LICENSE +21 -0
  8. fromscratch_pytsdb-0.4.0/PKG-INFO +159 -0
  9. fromscratch_pytsdb-0.4.0/README.md +127 -0
  10. fromscratch_pytsdb-0.4.0/conftest.py +21 -0
  11. fromscratch_pytsdb-0.4.0/docs/concepts/aggregation.md +42 -0
  12. fromscratch_pytsdb-0.4.0/docs/concepts/datapoints.md +73 -0
  13. fromscratch_pytsdb-0.4.0/docs/concepts/downsampling.md +42 -0
  14. fromscratch_pytsdb-0.4.0/docs/concepts/queries.md +50 -0
  15. fromscratch_pytsdb-0.4.0/docs/concepts/retention.md +41 -0
  16. fromscratch_pytsdb-0.4.0/docs/concepts/saving-safely.md +25 -0
  17. fromscratch_pytsdb-0.4.0/docs/concepts/series.md +30 -0
  18. fromscratch_pytsdb-0.4.0/docs/index.md +46 -0
  19. fromscratch_pytsdb-0.4.0/docs/property-testing.md +374 -0
  20. fromscratch_pytsdb-0.4.0/mkdocs.yml +82 -0
  21. fromscratch_pytsdb-0.4.0/overrides/main.html +7 -0
  22. fromscratch_pytsdb-0.4.0/pyproject.toml +156 -0
  23. fromscratch_pytsdb-0.4.0/src/pytsdb/__init__.py +69 -0
  24. fromscratch_pytsdb-0.4.0/src/pytsdb/_version.py +24 -0
  25. fromscratch_pytsdb-0.4.0/src/pytsdb/aggregation.py +140 -0
  26. fromscratch_pytsdb-0.4.0/src/pytsdb/atomic.py +66 -0
  27. fromscratch_pytsdb-0.4.0/src/pytsdb/database.py +350 -0
  28. fromscratch_pytsdb-0.4.0/src/pytsdb/datapoint.py +142 -0
  29. fromscratch_pytsdb-0.4.0/src/pytsdb/downsampling.py +51 -0
  30. fromscratch_pytsdb-0.4.0/src/pytsdb/errors.py +62 -0
  31. fromscratch_pytsdb-0.4.0/src/pytsdb/persistence.py +81 -0
  32. fromscratch_pytsdb-0.4.0/src/pytsdb/py.typed +0 -0
  33. fromscratch_pytsdb-0.4.0/src/pytsdb/query.py +92 -0
  34. fromscratch_pytsdb-0.4.0/src/pytsdb/retention.py +99 -0
  35. fromscratch_pytsdb-0.4.0/src/pytsdb/series.py +192 -0
  36. fromscratch_pytsdb-0.4.0/tests/__init__.py +1 -0
  37. fromscratch_pytsdb-0.4.0/tests/strategies.py +39 -0
  38. fromscratch_pytsdb-0.4.0/tests/test_aggregation.py +113 -0
  39. fromscratch_pytsdb-0.4.0/tests/test_atomic.py +75 -0
  40. fromscratch_pytsdb-0.4.0/tests/test_database.py +277 -0
  41. fromscratch_pytsdb-0.4.0/tests/test_datapoint.py +167 -0
  42. fromscratch_pytsdb-0.4.0/tests/test_downsampling.py +114 -0
  43. fromscratch_pytsdb-0.4.0/tests/test_model.py +352 -0
  44. fromscratch_pytsdb-0.4.0/tests/test_persistence.py +211 -0
  45. fromscratch_pytsdb-0.4.0/tests/test_properties.py +284 -0
  46. fromscratch_pytsdb-0.4.0/tests/test_query.py +114 -0
  47. fromscratch_pytsdb-0.4.0/tests/test_retention.py +94 -0
  48. fromscratch_pytsdb-0.4.0/tests/test_series.py +157 -0
  49. fromscratch_pytsdb-0.4.0/uv.lock +920 -0
@@ -0,0 +1,14 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ ci:
14
+ uses: robcox2910/robcox2910.github.io/.github/workflows/python-ci.yml@v1
@@ -0,0 +1,16 @@
1
+ name: Deploy Docs
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ workflow_dispatch: {}
7
+
8
+ jobs:
9
+ docs:
10
+ uses: robcox2910/robcox2910.github.io/.github/workflows/docs.yml@v1
11
+ permissions:
12
+ contents: read
13
+ pages: write
14
+ id-token: write
15
+ with:
16
+ coverage-report: false
@@ -0,0 +1,58 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*.*.*"
7
+
8
+ permissions:
9
+ contents: write
10
+ id-token: write
11
+
12
+ jobs:
13
+ build:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v7
17
+ with:
18
+ fetch-depth: 0
19
+
20
+ - name: Install uv
21
+ uses: astral-sh/setup-uv@v7
22
+ with:
23
+ version: "latest"
24
+
25
+ - name: Set up Python
26
+ run: uv python install 3.14
27
+
28
+ - name: Build distributions
29
+ run: uv build
30
+
31
+ - name: Upload build artifacts
32
+ uses: actions/upload-artifact@v7
33
+ with:
34
+ name: dist
35
+ path: dist/
36
+
37
+ - name: Upload to GitHub Release
38
+ uses: softprops/action-gh-release@v3
39
+ with:
40
+ files: dist/*
41
+
42
+ publish:
43
+ needs: build
44
+ runs-on: ubuntu-latest
45
+ environment:
46
+ name: pypi
47
+ url: https://pypi.org/project/fromscratch-pytsdb/
48
+ permissions:
49
+ id-token: write
50
+ steps:
51
+ - name: Download build artifacts
52
+ uses: actions/download-artifact@v8
53
+ with:
54
+ name: dist
55
+ path: dist/
56
+
57
+ - name: Publish to PyPI
58
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .coverage
8
+ htmlcov/
9
+ .ruff_cache/
10
+ _version.py
11
+ .hypothesis/
@@ -0,0 +1,56 @@
1
+ default_language_version:
2
+ python: python3.14
3
+
4
+ repos:
5
+ - repo: https://github.com/pre-commit/pre-commit-hooks
6
+ rev: v5.0.0
7
+ hooks:
8
+ # Pages paste real pytest output, trailing spaces included.
9
+ - id: trailing-whitespace
10
+ exclude: ^docs/property-testing\.md$
11
+ - id: end-of-file-fixer
12
+ exclude: ^docs/property-testing\.md$
13
+ - id: check-yaml
14
+ args: ['--unsafe']
15
+ - id: check-added-large-files
16
+ - id: check-json
17
+ - id: check-toml
18
+ - id: check-merge-conflict
19
+ - id: debug-statements
20
+
21
+ - repo: https://github.com/astral-sh/ruff-pre-commit
22
+ rev: v0.15.4
23
+ hooks:
24
+ - id: ruff
25
+ args: [--fix, --exit-non-zero-on-fix]
26
+ - id: ruff-format
27
+
28
+ - repo: https://github.com/RobertCraigie/pyright-python
29
+ rev: v1.1.408
30
+ hooks:
31
+ - id: pyright
32
+
33
+ - repo: local
34
+ hooks:
35
+ - id: pip-audit
36
+ name: pip-audit
37
+ language: system
38
+ pass_filenames: false
39
+ files: ^(pyproject\.toml|uv\.lock)$
40
+ entry: >-
41
+ bash -c 'f=$(mktemp) &&
42
+ uv export --frozen --no-emit-project --all-extras --no-hashes -o "$f" >/dev/null &&
43
+ uvx pip-audit==2.10.0 -r "$f" --disable-pip --no-deps;
44
+ status=$?; rm -f "$f"; exit $status'
45
+
46
+ - repo: https://github.com/rhysd/actionlint
47
+ rev: v1.7.7
48
+ hooks:
49
+ - id: actionlint
50
+
51
+ - repo: https://github.com/commitizen-tools/commitizen
52
+ rev: v4.13.8
53
+ hooks:
54
+ - id: commitizen
55
+ stages: [commit-msg]
56
+ additional_dependencies: [typing_extensions]
@@ -0,0 +1,52 @@
1
+ # Claude Code Instructions
2
+
3
+ <!-- standard:start -->
4
+ ## Code Style
5
+
6
+ - All public functions, methods, classes, and modules **must** have a docstring
7
+ - Use imperative mood: "Return the result." not "Returns the result."
8
+ - One-line docstrings for simple functions; multi-line with summary + details for complex ones
9
+ - Follow the Google docstring convention (D211, D212 selected via ruff)
10
+
11
+ ## Linting
12
+
13
+ - Ruff enforces a comprehensive rule set -- run `uv run ruff check .` before committing
14
+ - Pyright runs in strict mode -- run `uv run pyright src tests` before committing
15
+ - All violations must be fixed, not ignored. Exceptions are the repo-owned `ignore` and `per-file-ignores` lists in `pyproject.toml`, which belong to this repo; the series sync never edits them
16
+
17
+ ## Testing
18
+
19
+ - TDD workflow: Red-Green-Refactor
20
+ - Coverage is enforced by pytest-cov; the floor in `pyproject.toml` is repo-owned
21
+ - Test classes and methods also require docstrings
22
+ - Use named constants instead of magic values (PLR2004)
23
+ - Run `uv run pytest` before committing
24
+ - Property-based tests (Hypothesis) live in `tests/test_properties.py` and `tests/test_model.py`; the shared profiles are in the root `conftest.py` (owned by `series sync`). Run `uv run pytest --hypothesis-profile=thorough` before a release.
25
+
26
+ ## Commits
27
+
28
+ - Use conventional commits: `feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `ci:`, `chore:`
29
+ - All changes go via PR from a feature branch (main is protected); merge when CI is green
30
+
31
+ ## Dependencies
32
+
33
+ - Package manager: uv
34
+ - Run `uv sync --all-extras` after modifying dependencies
35
+ - `uv.lock` is committed; CI and pre-commit audit it with pip-audit through `uvx`, so pip-audit is not a project dependency
36
+ - Sibling projects, where used, are pinned release tags in `[tool.uv.sources]` (git + tag); never use `path` sources
37
+
38
+ ## Releasing
39
+
40
+ - A release is a `vX.Y.Z` tag on main after the PR is merged
41
+ - Docs-only and tooling-only changes do not get a release tag
42
+ - Docs are built with `mkdocs build --strict` and deployed to GitHub Pages by the docs workflow
43
+ <!-- standard:end -->
44
+
45
+ ## Project notes
46
+
47
+ - Pyright in strict mode -- run `uv run pyright src tests` before committing
48
+ - All violations must be fixed, not ignored (except D203/D213 incompatible pair)
49
+ - Minimum 90% coverage enforced by pytest-cov
50
+ - Use conventional commits (enforced by commitizen): `feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `ci:`, `chore:`
51
+ - All changes go via PR from a feature branch (main is protected)
52
+ - Add production deps to `[project.dependencies]`
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rob Cox
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.
@@ -0,0 +1,159 @@
1
+ Metadata-Version: 2.5
2
+ Name: fromscratch-pytsdb
3
+ Version: 0.4.0
4
+ Summary: An educational time-series database library built from scratch in Python
5
+ Project-URL: Homepage, https://github.com/robcox2910/pytsdb
6
+ Project-URL: Repository, https://github.com/robcox2910/pytsdb
7
+ Project-URL: Issues, https://github.com/robcox2910/pytsdb/issues
8
+ Project-URL: Documentation, https://robcox2910.github.io/pytsdb/
9
+ Author: Rob Cox
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: database,education,metrics,time-series
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Education
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Education
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.14
22
+ Provides-Extra: dev
23
+ Requires-Dist: commitizen>=4.4.0; extra == 'dev'
24
+ Requires-Dist: hypothesis>=6.170; extra == 'dev'
25
+ Requires-Dist: pip-audit>=2.10.0; extra == 'dev'
26
+ Requires-Dist: pre-commit>=4.0.1; extra == 'dev'
27
+ Requires-Dist: pyright>=1.1.400; extra == 'dev'
28
+ Requires-Dist: pytest-cov>=6.0.0; extra == 'dev'
29
+ Requires-Dist: pytest>=8.3.4; extra == 'dev'
30
+ Requires-Dist: ruff>=0.9.3; extra == 'dev'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # pytsdb
34
+
35
+ An educational time-series database built from scratch in Python. Part of the **PyLearn** platform for kids aged 12+.
36
+
37
+ A time-series database stores data points that each have a **timestamp**, a **value**, and optional **tags** -- like a weather station logbook that records temperature readings throughout the day.
38
+
39
+ ## Features
40
+
41
+ - **DataPoint** -- timestamped values with optional tags (e.g. sensor="kitchen")
42
+ - **TimeSeries** -- ordered collection of data points for one metric
43
+ - **TimeSeriesDB** -- container managing multiple named series with full CRUD
44
+ - **Time Range Queries** -- efficient binary-search lookups between two timestamps
45
+ - **Tag Filtering** -- filter data points by tag key-value pairs
46
+ - **Aggregation** -- compute avg, min, max, sum, count over configurable time windows
47
+ - **Downsampling** -- reduce resolution (e.g. minute data to hourly averages)
48
+ - **Retention Policies** -- auto-delete data older than N days
49
+ - **Persistence** -- save/load the entire database to JSON files
50
+ - **Rate of Change** -- compute how fast a value is changing over time
51
+
52
+ ## Quick Start
53
+
54
+ ```python
55
+ from datetime import datetime, timedelta, timezone
56
+ from pytsdb import AggFunc, TimeSeriesDB
57
+
58
+ # Create a database and a series
59
+ db = TimeSeriesDB()
60
+ db.create_series("temperature")
61
+
62
+ # Add some readings
63
+ db.add_point("temperature", value=68.0,
64
+ timestamp=datetime(2024, 7, 4, 8, 0, tzinfo=timezone.utc),
65
+ tags={"sensor": "kitchen"})
66
+ db.add_point("temperature", value=72.5,
67
+ timestamp=datetime(2024, 7, 4, 10, 0, tzinfo=timezone.utc),
68
+ tags={"sensor": "kitchen"})
69
+ db.add_point("temperature", value=75.0,
70
+ timestamp=datetime(2024, 7, 4, 12, 0, tzinfo=timezone.utc),
71
+ tags={"sensor": "bedroom"})
72
+
73
+ # Query by time range and tags
74
+ kitchen = db.query("temperature", tags={"sensor": "kitchen"})
75
+ print(f"Kitchen readings: {len(kitchen)}") # 2
76
+
77
+ # Aggregate: hourly averages
78
+ hourly = db.aggregate("temperature", window=timedelta(hours=1), func=AggFunc.AVG)
79
+
80
+ # Downsample to 2-hour buckets
81
+ coarse = db.downsample("temperature", target_interval=timedelta(hours=2))
82
+
83
+ # Auto-expire old data
84
+ db.set_retention("temperature", max_age=timedelta(days=30))
85
+ db.apply_retention()
86
+
87
+ # Save to disk
88
+ from pytsdb import save, load
89
+ save(db, "my_data.json")
90
+ db2 = load("my_data.json")
91
+ ```
92
+
93
+ ## Concepts
94
+
95
+ See the [docs/](docs/) folder for kid-friendly explanations with real-world analogies:
96
+
97
+ - [What is a Time-Series DB?](docs/index.md) -- weather station logbook analogy
98
+ - [Data Points](docs/concepts/datapoints.md) -- thermometer readings
99
+ - [Time Series](docs/concepts/series.md) -- a week of readings
100
+ - [Queries](docs/concepts/queries.md) -- checking yesterday's weather
101
+ - [Aggregation](docs/concepts/aggregation.md) -- weekly average temperature
102
+ - [Downsampling](docs/concepts/downsampling.md) -- monthly summaries
103
+ - [Retention](docs/concepts/retention.md) -- throwing away old newspapers
104
+
105
+ <!-- docs:start -->
106
+ ## Documentation
107
+
108
+ Full docs (with kid-friendly explanations of every idea): https://robcox2910.github.io/pytsdb/
109
+ <!-- docs:end -->
110
+
111
+ ## Development
112
+
113
+ ```bash
114
+ uv sync --all-extras
115
+ uv run ruff check src/ tests/
116
+ uv run ruff format src/ tests/
117
+ uv run pyright src tests
118
+ uv run pytest
119
+ ```
120
+
121
+ ## Releasing
122
+
123
+ Other projects in the series ([PyStack](https://github.com/robcox2910/pystack)) use a pinned release of this one,
124
+ not whatever is on `main`. To publish a change for them:
125
+
126
+ 1. Merge the change to `main`.
127
+ 2. Tag it: `git tag -a vX.Y.Z -m vX.Y.Z && git push origin vX.Y.Z`.
128
+ A change to the public API bumps the minor version (`v0.1.0` → `v0.2.0`);
129
+ anything else bumps the patch (`v0.1.0` → `v0.1.1`).
130
+ 3. In each project that uses it, edit the tag in `[tool.uv.sources]`, run
131
+ `uv lock --upgrade-package <name>`, and open a pull request so CI checks
132
+ the pair together.
133
+
134
+ <!-- series:start -->
135
+ ## Related Projects
136
+
137
+ This project is one of a series that builds real systems "from scratch in Python" to teach how they work. See the [series home](https://robcox2910.github.io/) for the full story.
138
+
139
+ | Project | What it teaches | Repository | Docs |
140
+ | --- | --- | --- | --- |
141
+ | PyOS | Operating systems | [py-os](https://github.com/robcox2910/py-os) | [Docs](https://robcox2910.github.io/py-os/) |
142
+ | Pebble | Compilers and programming languages | [pebble-lang](https://github.com/robcox2910/pebble-lang) | [Docs](https://robcox2910.github.io/pebble-lang/) |
143
+ | PyDB | Relational databases | [pydb](https://github.com/robcox2910/pydb) | [Docs](https://robcox2910.github.io/pydb/) |
144
+ | PyKV | Key-value stores | [pykv](https://github.com/robcox2910/pykv) | [Docs](https://robcox2910.github.io/pykv/) |
145
+ | PyDocDB | Document databases | [pydoc-db](https://github.com/robcox2910/pydoc-db) | [Docs](https://robcox2910.github.io/pydoc-db/) |
146
+ | PyGraphDB | Graph databases | [pygraphdb](https://github.com/robcox2910/pygraphdb) | [Docs](https://robcox2910.github.io/pygraphdb/) |
147
+ | **PyTSDB** | **Time-series databases** | [pytsdb](https://github.com/robcox2910/pytsdb) | [Docs](https://robcox2910.github.io/pytsdb/) |
148
+ | PyVecDB | Vector databases | [pyvecdb](https://github.com/robcox2910/pyvecdb) | [Docs](https://robcox2910.github.io/pyvecdb/) |
149
+ | PyColDB | Column-family databases | [pycoldb](https://github.com/robcox2910/pycoldb) | [Docs](https://robcox2910.github.io/pycoldb/) |
150
+ | PyCrypt | Cryptography | [pycrypt](https://github.com/robcox2910/pycrypt) | [Docs](https://robcox2910.github.io/pycrypt/) |
151
+ | PyNet | Computer networking | [pynet](https://github.com/robcox2910/pynet) | [Docs](https://robcox2910.github.io/pynet/) |
152
+ | PyWeb | HTTP web servers | [pyweb](https://github.com/robcox2910/pyweb) | [Docs](https://robcox2910.github.io/pyweb/) |
153
+ | PyGit | Version control | [pygit](https://github.com/robcox2910/pygit) | [Docs](https://robcox2910.github.io/pygit/) |
154
+ | PySearch | Search engines | [pysearch](https://github.com/robcox2910/pysearch) | [Docs](https://robcox2910.github.io/pysearch/) |
155
+ | PyMQ | Message queues | [pymq](https://github.com/robcox2910/pymq) | [Docs](https://robcox2910.github.io/pymq/) |
156
+ | PyLLM | Language models (the brain) | [pyllm](https://github.com/robcox2910/pyllm) | [Docs](https://robcox2910.github.io/pyllm/) |
157
+ | PyScheduler | Job scheduling | [pyscheduler](https://github.com/robcox2910/pyscheduler) | [Docs](https://robcox2910.github.io/pyscheduler/) |
158
+ | PyStack | Full-stack integration | [pystack](https://github.com/robcox2910/pystack) | [Docs](https://robcox2910.github.io/pystack/) |
159
+ <!-- series:end -->
@@ -0,0 +1,127 @@
1
+ # pytsdb
2
+
3
+ An educational time-series database built from scratch in Python. Part of the **PyLearn** platform for kids aged 12+.
4
+
5
+ A time-series database stores data points that each have a **timestamp**, a **value**, and optional **tags** -- like a weather station logbook that records temperature readings throughout the day.
6
+
7
+ ## Features
8
+
9
+ - **DataPoint** -- timestamped values with optional tags (e.g. sensor="kitchen")
10
+ - **TimeSeries** -- ordered collection of data points for one metric
11
+ - **TimeSeriesDB** -- container managing multiple named series with full CRUD
12
+ - **Time Range Queries** -- efficient binary-search lookups between two timestamps
13
+ - **Tag Filtering** -- filter data points by tag key-value pairs
14
+ - **Aggregation** -- compute avg, min, max, sum, count over configurable time windows
15
+ - **Downsampling** -- reduce resolution (e.g. minute data to hourly averages)
16
+ - **Retention Policies** -- auto-delete data older than N days
17
+ - **Persistence** -- save/load the entire database to JSON files
18
+ - **Rate of Change** -- compute how fast a value is changing over time
19
+
20
+ ## Quick Start
21
+
22
+ ```python
23
+ from datetime import datetime, timedelta, timezone
24
+ from pytsdb import AggFunc, TimeSeriesDB
25
+
26
+ # Create a database and a series
27
+ db = TimeSeriesDB()
28
+ db.create_series("temperature")
29
+
30
+ # Add some readings
31
+ db.add_point("temperature", value=68.0,
32
+ timestamp=datetime(2024, 7, 4, 8, 0, tzinfo=timezone.utc),
33
+ tags={"sensor": "kitchen"})
34
+ db.add_point("temperature", value=72.5,
35
+ timestamp=datetime(2024, 7, 4, 10, 0, tzinfo=timezone.utc),
36
+ tags={"sensor": "kitchen"})
37
+ db.add_point("temperature", value=75.0,
38
+ timestamp=datetime(2024, 7, 4, 12, 0, tzinfo=timezone.utc),
39
+ tags={"sensor": "bedroom"})
40
+
41
+ # Query by time range and tags
42
+ kitchen = db.query("temperature", tags={"sensor": "kitchen"})
43
+ print(f"Kitchen readings: {len(kitchen)}") # 2
44
+
45
+ # Aggregate: hourly averages
46
+ hourly = db.aggregate("temperature", window=timedelta(hours=1), func=AggFunc.AVG)
47
+
48
+ # Downsample to 2-hour buckets
49
+ coarse = db.downsample("temperature", target_interval=timedelta(hours=2))
50
+
51
+ # Auto-expire old data
52
+ db.set_retention("temperature", max_age=timedelta(days=30))
53
+ db.apply_retention()
54
+
55
+ # Save to disk
56
+ from pytsdb import save, load
57
+ save(db, "my_data.json")
58
+ db2 = load("my_data.json")
59
+ ```
60
+
61
+ ## Concepts
62
+
63
+ See the [docs/](docs/) folder for kid-friendly explanations with real-world analogies:
64
+
65
+ - [What is a Time-Series DB?](docs/index.md) -- weather station logbook analogy
66
+ - [Data Points](docs/concepts/datapoints.md) -- thermometer readings
67
+ - [Time Series](docs/concepts/series.md) -- a week of readings
68
+ - [Queries](docs/concepts/queries.md) -- checking yesterday's weather
69
+ - [Aggregation](docs/concepts/aggregation.md) -- weekly average temperature
70
+ - [Downsampling](docs/concepts/downsampling.md) -- monthly summaries
71
+ - [Retention](docs/concepts/retention.md) -- throwing away old newspapers
72
+
73
+ <!-- docs:start -->
74
+ ## Documentation
75
+
76
+ Full docs (with kid-friendly explanations of every idea): https://robcox2910.github.io/pytsdb/
77
+ <!-- docs:end -->
78
+
79
+ ## Development
80
+
81
+ ```bash
82
+ uv sync --all-extras
83
+ uv run ruff check src/ tests/
84
+ uv run ruff format src/ tests/
85
+ uv run pyright src tests
86
+ uv run pytest
87
+ ```
88
+
89
+ ## Releasing
90
+
91
+ Other projects in the series ([PyStack](https://github.com/robcox2910/pystack)) use a pinned release of this one,
92
+ not whatever is on `main`. To publish a change for them:
93
+
94
+ 1. Merge the change to `main`.
95
+ 2. Tag it: `git tag -a vX.Y.Z -m vX.Y.Z && git push origin vX.Y.Z`.
96
+ A change to the public API bumps the minor version (`v0.1.0` → `v0.2.0`);
97
+ anything else bumps the patch (`v0.1.0` → `v0.1.1`).
98
+ 3. In each project that uses it, edit the tag in `[tool.uv.sources]`, run
99
+ `uv lock --upgrade-package <name>`, and open a pull request so CI checks
100
+ the pair together.
101
+
102
+ <!-- series:start -->
103
+ ## Related Projects
104
+
105
+ This project is one of a series that builds real systems "from scratch in Python" to teach how they work. See the [series home](https://robcox2910.github.io/) for the full story.
106
+
107
+ | Project | What it teaches | Repository | Docs |
108
+ | --- | --- | --- | --- |
109
+ | PyOS | Operating systems | [py-os](https://github.com/robcox2910/py-os) | [Docs](https://robcox2910.github.io/py-os/) |
110
+ | Pebble | Compilers and programming languages | [pebble-lang](https://github.com/robcox2910/pebble-lang) | [Docs](https://robcox2910.github.io/pebble-lang/) |
111
+ | PyDB | Relational databases | [pydb](https://github.com/robcox2910/pydb) | [Docs](https://robcox2910.github.io/pydb/) |
112
+ | PyKV | Key-value stores | [pykv](https://github.com/robcox2910/pykv) | [Docs](https://robcox2910.github.io/pykv/) |
113
+ | PyDocDB | Document databases | [pydoc-db](https://github.com/robcox2910/pydoc-db) | [Docs](https://robcox2910.github.io/pydoc-db/) |
114
+ | PyGraphDB | Graph databases | [pygraphdb](https://github.com/robcox2910/pygraphdb) | [Docs](https://robcox2910.github.io/pygraphdb/) |
115
+ | **PyTSDB** | **Time-series databases** | [pytsdb](https://github.com/robcox2910/pytsdb) | [Docs](https://robcox2910.github.io/pytsdb/) |
116
+ | PyVecDB | Vector databases | [pyvecdb](https://github.com/robcox2910/pyvecdb) | [Docs](https://robcox2910.github.io/pyvecdb/) |
117
+ | PyColDB | Column-family databases | [pycoldb](https://github.com/robcox2910/pycoldb) | [Docs](https://robcox2910.github.io/pycoldb/) |
118
+ | PyCrypt | Cryptography | [pycrypt](https://github.com/robcox2910/pycrypt) | [Docs](https://robcox2910.github.io/pycrypt/) |
119
+ | PyNet | Computer networking | [pynet](https://github.com/robcox2910/pynet) | [Docs](https://robcox2910.github.io/pynet/) |
120
+ | PyWeb | HTTP web servers | [pyweb](https://github.com/robcox2910/pyweb) | [Docs](https://robcox2910.github.io/pyweb/) |
121
+ | PyGit | Version control | [pygit](https://github.com/robcox2910/pygit) | [Docs](https://robcox2910.github.io/pygit/) |
122
+ | PySearch | Search engines | [pysearch](https://github.com/robcox2910/pysearch) | [Docs](https://robcox2910.github.io/pysearch/) |
123
+ | PyMQ | Message queues | [pymq](https://github.com/robcox2910/pymq) | [Docs](https://robcox2910.github.io/pymq/) |
124
+ | PyLLM | Language models (the brain) | [pyllm](https://github.com/robcox2910/pyllm) | [Docs](https://robcox2910.github.io/pyllm/) |
125
+ | PyScheduler | Job scheduling | [pyscheduler](https://github.com/robcox2910/pyscheduler) | [Docs](https://robcox2910.github.io/pyscheduler/) |
126
+ | PyStack | Full-stack integration | [pystack](https://github.com/robcox2910/pystack) | [Docs](https://robcox2910.github.io/pystack/) |
127
+ <!-- series:end -->
@@ -0,0 +1,21 @@
1
+ """Shared Hypothesis profiles for the series.
2
+
3
+ Two profiles are registered, and ``ci`` is loaded by default:
4
+
5
+ - ``ci`` runs 100 examples with no deadline, a fixed seed and no example
6
+ database, so the same code gives the same result on every machine and a
7
+ green build can be trusted.
8
+ - ``thorough`` runs 2000 examples with a random seed, to hunt for the rare
9
+ counterexample before a release.
10
+
11
+ Run the long one with ``uv run pytest --hypothesis-profile=thorough``.
12
+
13
+ This file is owned by ``series sync``. Edit it in the series home repo
14
+ (robcox2910.github.io), not here, or the next sync will put it back.
15
+ """
16
+
17
+ from hypothesis import settings
18
+
19
+ settings.register_profile("ci", max_examples=100, deadline=None, derandomize=True, database=None)
20
+ settings.register_profile("thorough", max_examples=2000, deadline=None, derandomize=False)
21
+ settings.load_profile("ci")
@@ -0,0 +1,42 @@
1
+ # Aggregation -- Weekly Average Temperature
2
+
3
+ ## The Analogy
4
+
5
+ You recorded the temperature every hour for a whole week. That's 168 readings! Your teacher asks: "What was the average temperature each day?"
6
+
7
+ Instead of reading all 168 numbers, you:
8
+ 1. **Group** them by day (7 groups of 24 readings)
9
+ 2. **Summarise** each group (add them up and divide by 24)
10
+
11
+ That's aggregation! You pick a **window** (one day) and a **function** (average) and let the computer crunch the numbers.
12
+
13
+ ## Available Functions
14
+
15
+ | Function | What it does | Example |
16
+ |----------|-------------------------------|----------------------------------|
17
+ | AVG | Average of all values | "Average temperature this hour" |
18
+ | MIN | Smallest value | "Coldest reading today" |
19
+ | MAX | Largest value | "Hottest reading today" |
20
+ | SUM | Total of all values | "Total rainfall this week" |
21
+ | COUNT | Number of readings | "How many readings per hour?" |
22
+
23
+ ## In Code
24
+
25
+ ```python
26
+ from datetime import timedelta
27
+ from pytsdb import TimeSeriesDB, AggFunc
28
+
29
+ db = TimeSeriesDB()
30
+ db.create_series("temp")
31
+ # ... add hourly readings ...
32
+
33
+ # Get the average temperature per hour
34
+ hourly_avg = db.aggregate("temp", window=timedelta(hours=1), func=AggFunc.AVG)
35
+
36
+ # Get the max temperature per day
37
+ daily_max = db.aggregate("temp", window=timedelta(days=1), func=AggFunc.MAX)
38
+ ```
39
+
40
+ ## How Time Buckets Work
41
+
42
+ Every data point's timestamp is mapped to a **bucket**. For a 1-hour window, all points between 10:00 and 10:59 go into the "10:00" bucket. The function is then applied to each bucket independently.
@@ -0,0 +1,73 @@
1
+ # Data Points -- Thermometer Readings
2
+
3
+ ## The Analogy
4
+
5
+ Every time you check a thermometer and write down the number, you're creating a **data point**. A data point is the smallest unit of information in a time-series database.
6
+
7
+ It has three parts:
8
+
9
+ 1. **Timestamp** -- *When* did you look at the thermometer? (e.g. "July 4, 2024 at 2:00 PM")
10
+ 2. **Value** -- *What* did it read? (e.g. 72.5°F)
11
+ 3. **Tags** -- *Extra labels* that help you find it later (e.g. sensor="kitchen", floor="1")
12
+
13
+ ## In Code
14
+
15
+ ```python
16
+ from datetime import datetime, timezone
17
+ from pytsdb import DataPoint
18
+
19
+ reading = DataPoint(
20
+ timestamp=datetime(2024, 7, 4, 14, 0, tzinfo=timezone.utc),
21
+ value=72.5,
22
+ tags={"sensor": "kitchen", "floor": "1"},
23
+ )
24
+
25
+ print(reading.value) # 72.5
26
+ print(reading.tags) # {'sensor': 'kitchen', 'floor': '1'}
27
+ ```
28
+
29
+ ## Why Are Tags Useful?
30
+
31
+ Imagine you have 10 temperature sensors around your house. Without tags, you'd need 10 separate series. With tags, you can store everything in one series and filter:
32
+
33
+ ```python
34
+ # "Show me only the kitchen readings"
35
+ reading.matches_tags({"sensor": "kitchen"}) # True
36
+ reading.matches_tags({"sensor": "garage"}) # False
37
+ ```
38
+
39
+ ## Fun Fact
40
+
41
+ Data points in pytsdb are **frozen** (immutable) -- once created, you can't change them. This is like writing in pen instead of pencil. It keeps your data trustworthy!
42
+
43
+ This even applies to the tags. When you create a data point, it takes its own private copy of the tags dictionary, so editing the dict you passed in later can't sneak in and change the point:
44
+
45
+ ```python
46
+ labels = {"sensor": "kitchen"}
47
+ reading = DataPoint(value=72.5, tags=labels)
48
+
49
+ labels["sensor"] = "garage" # edit the original dict
50
+ print(reading.tags) # {'sensor': 'kitchen'} -- still in pen!
51
+ ```
52
+
53
+ A series works the same way in the other direction. Every point it hands back -- from `query`, `first`, `last` or looping over it -- is a fresh copy, so scribbling on a returned point's tags can't change what the database has stored.
54
+
55
+ ## Watch Out: Timestamps Need a Timezone
56
+
57
+ A data point's timestamp must be **timezone-aware** so pytsdb always knows *which* clock you mean. A bare `datetime(2024, 1, 1)` has no timezone, so pytsdb raises a friendly error instead of letting it cause confusing crashes later:
58
+
59
+ ```python
60
+ from datetime import datetime, timezone
61
+
62
+ DataPoint(timestamp=datetime(2024, 1, 1)) # ValueError!
63
+ DataPoint(timestamp=datetime(2024, 1, 1, tzinfo=timezone.utc)) # works
64
+ ```
65
+
66
+ ## Watch Out: NaN Isn't a Reading
67
+
68
+ `float("nan")` means "not a number". It isn't bigger, smaller or equal to anything -- not even itself -- so a NaN reading would make "the highest reading" and "in time order" meaningless. pytsdb refuses it with a `ValueError`. So is anything that isn't a real number (a string, `None`, `True`). Infinity is fine.
69
+
70
+ ```python
71
+ DataPoint(value=float("nan")) # ValueError!
72
+ DataPoint(value=float("inf")) # works
73
+ ```