fromscratch-pydocdb 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.
- fromscratch_pydocdb-0.3.0/.github/workflows/ci.yml +14 -0
- fromscratch_pydocdb-0.3.0/.github/workflows/docs.yml +16 -0
- fromscratch_pydocdb-0.3.0/.github/workflows/release.yml +58 -0
- fromscratch_pydocdb-0.3.0/.gitignore +11 -0
- fromscratch_pydocdb-0.3.0/.pre-commit-config.yaml +56 -0
- fromscratch_pydocdb-0.3.0/CLAUDE.md +52 -0
- fromscratch_pydocdb-0.3.0/LICENSE +21 -0
- fromscratch_pydocdb-0.3.0/PKG-INFO +146 -0
- fromscratch_pydocdb-0.3.0/README.md +114 -0
- fromscratch_pydocdb-0.3.0/conftest.py +21 -0
- fromscratch_pydocdb-0.3.0/docs/concepts/aggregation.md +91 -0
- fromscratch_pydocdb-0.3.0/docs/concepts/collections.md +62 -0
- fromscratch_pydocdb-0.3.0/docs/concepts/documents.md +45 -0
- fromscratch_pydocdb-0.3.0/docs/concepts/indexing.md +58 -0
- fromscratch_pydocdb-0.3.0/docs/concepts/nested.md +60 -0
- fromscratch_pydocdb-0.3.0/docs/concepts/persistence.md +56 -0
- fromscratch_pydocdb-0.3.0/docs/concepts/projection.md +40 -0
- fromscratch_pydocdb-0.3.0/docs/concepts/queries.md +50 -0
- fromscratch_pydocdb-0.3.0/docs/index.md +44 -0
- fromscratch_pydocdb-0.3.0/docs/property-testing.md +381 -0
- fromscratch_pydocdb-0.3.0/mkdocs.yml +83 -0
- fromscratch_pydocdb-0.3.0/overrides/main.html +7 -0
- fromscratch_pydocdb-0.3.0/pyproject.toml +156 -0
- fromscratch_pydocdb-0.3.0/src/pydocdb/__init__.py +48 -0
- fromscratch_pydocdb-0.3.0/src/pydocdb/_version.py +24 -0
- fromscratch_pydocdb-0.3.0/src/pydocdb/aggregation.py +377 -0
- fromscratch_pydocdb-0.3.0/src/pydocdb/atomic.py +66 -0
- fromscratch_pydocdb-0.3.0/src/pydocdb/collection.py +467 -0
- fromscratch_pydocdb-0.3.0/src/pydocdb/document.py +145 -0
- fromscratch_pydocdb-0.3.0/src/pydocdb/errors.py +58 -0
- fromscratch_pydocdb-0.3.0/src/pydocdb/indexing.py +126 -0
- fromscratch_pydocdb-0.3.0/src/pydocdb/operators.py +187 -0
- fromscratch_pydocdb-0.3.0/src/pydocdb/persistence.py +125 -0
- fromscratch_pydocdb-0.3.0/src/pydocdb/projection.py +101 -0
- fromscratch_pydocdb-0.3.0/src/pydocdb/py.typed +0 -0
- fromscratch_pydocdb-0.3.0/src/pydocdb/query.py +188 -0
- fromscratch_pydocdb-0.3.0/tests/__init__.py +1 -0
- fromscratch_pydocdb-0.3.0/tests/strategies.py +82 -0
- fromscratch_pydocdb-0.3.0/tests/test_aggregation.py +231 -0
- fromscratch_pydocdb-0.3.0/tests/test_atomic.py +75 -0
- fromscratch_pydocdb-0.3.0/tests/test_collection.py +357 -0
- fromscratch_pydocdb-0.3.0/tests/test_document.py +127 -0
- fromscratch_pydocdb-0.3.0/tests/test_errors.py +46 -0
- fromscratch_pydocdb-0.3.0/tests/test_indexing.py +267 -0
- fromscratch_pydocdb-0.3.0/tests/test_model.py +242 -0
- fromscratch_pydocdb-0.3.0/tests/test_operators.py +156 -0
- fromscratch_pydocdb-0.3.0/tests/test_persistence.py +172 -0
- fromscratch_pydocdb-0.3.0/tests/test_projection.py +93 -0
- fromscratch_pydocdb-0.3.0/tests/test_properties.py +211 -0
- fromscratch_pydocdb-0.3.0/tests/test_query.py +116 -0
- fromscratch_pydocdb-0.3.0/uv.lock +920 -0
|
@@ -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-pydocdb/
|
|
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,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 80% 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,146 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fromscratch-pydocdb
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: An educational document store library built from scratch in Python
|
|
5
|
+
Project-URL: Homepage, https://github.com/robcox2910/pydoc-db
|
|
6
|
+
Project-URL: Repository, https://github.com/robcox2910/pydoc-db
|
|
7
|
+
Project-URL: Issues, https://github.com/robcox2910/pydoc-db/issues
|
|
8
|
+
Project-URL: Documentation, https://robcox2910.github.io/pydoc-db/
|
|
9
|
+
Author: Rob Cox
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: database,document,education,nosql,store
|
|
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
|
+
# PyDocDB
|
|
34
|
+
|
|
35
|
+
An educational document store (like MongoDB) built from scratch in Python. Part of the **PyLearn** platform for kids aged 12+.
|
|
36
|
+
|
|
37
|
+
PyDocDB stores JSON-like documents without requiring a fixed schema -- each document can have different fields, just like how every index card in a filing cabinet can have different information on it.
|
|
38
|
+
|
|
39
|
+
## Features
|
|
40
|
+
|
|
41
|
+
- **Documents** -- Dict-like objects with auto-generated unique IDs
|
|
42
|
+
- **Collections** -- Named groups of documents with full CRUD operations
|
|
43
|
+
- **Query Language** -- Filter with operators: `$eq`, `$gt`, `$lt`, `$gte`, `$lte`, `$ne`, `$in`, `$exists`, `$and`, `$or`, `$not`
|
|
44
|
+
- **Nested Field Access** -- Query nested fields with dot notation (e.g., `"address.city"`)
|
|
45
|
+
- **Projection** -- Return only specific fields from documents
|
|
46
|
+
- **Indexing** -- Create indexes on fields for faster lookups
|
|
47
|
+
- **Persistence** -- Save/load collections to JSON files on disk
|
|
48
|
+
- **Aggregation** -- Pipeline with `$match`, `$group`, `$sort`, `$limit`, `$count`
|
|
49
|
+
|
|
50
|
+
## Quick Start
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from pydocdb import Collection, save_collection, load_collection, aggregate
|
|
54
|
+
|
|
55
|
+
# Create a collection (like labeling a new folder in a filing cabinet)
|
|
56
|
+
students = Collection("students")
|
|
57
|
+
|
|
58
|
+
# Insert documents (like filing index cards)
|
|
59
|
+
students.insert_one({"name": "Alice", "age": 14, "grade": "9th"})
|
|
60
|
+
students.insert_one({"name": "Bob", "age": 13, "hobby": "chess"})
|
|
61
|
+
students.insert_one({"name": "Charlie", "age": 15, "grade": "10th"})
|
|
62
|
+
|
|
63
|
+
# Find documents with queries
|
|
64
|
+
teenagers = students.find({"age": {"$gte": 13}})
|
|
65
|
+
alice = students.find_one({"name": "Alice"})
|
|
66
|
+
|
|
67
|
+
# Update and delete
|
|
68
|
+
students.update_one({"name": "Alice"}, {"age": 15})
|
|
69
|
+
students.delete_one({"name": "Bob"})
|
|
70
|
+
|
|
71
|
+
# Use projection to get only specific fields
|
|
72
|
+
names = students.find({}, projection={"name": 1, "_id": 0})
|
|
73
|
+
|
|
74
|
+
# Aggregate data
|
|
75
|
+
result = aggregate(students.to_list(), [
|
|
76
|
+
{"$group": {"_id": "$grade", "count": {"$sum": 1}}},
|
|
77
|
+
{"$sort": {"count": -1}},
|
|
78
|
+
])
|
|
79
|
+
|
|
80
|
+
# Save to disk and load later
|
|
81
|
+
save_collection(students, "students.json")
|
|
82
|
+
loaded = load_collection("students.json")
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Installation
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
uv add pydocdb
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Development
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
git clone https://github.com/robcox2910/pydoc-db.git
|
|
95
|
+
cd pydoc-db
|
|
96
|
+
uv sync --all-extras
|
|
97
|
+
uv run pytest
|
|
98
|
+
uv run ruff check src/ tests/
|
|
99
|
+
uv run pyright src tests
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Releasing
|
|
103
|
+
|
|
104
|
+
Other projects in the series ([PyStack](https://github.com/robcox2910/pystack)) use a pinned release of this one,
|
|
105
|
+
not whatever is on `main`. To publish a change for them:
|
|
106
|
+
|
|
107
|
+
1. Merge the change to `main`.
|
|
108
|
+
2. Tag it: `git tag -a vX.Y.Z -m vX.Y.Z && git push origin vX.Y.Z`.
|
|
109
|
+
A change to the public API bumps the minor version (`v0.1.0` → `v0.2.0`);
|
|
110
|
+
anything else bumps the patch (`v0.1.0` → `v0.1.1`).
|
|
111
|
+
3. In each project that uses it, edit the tag in `[tool.uv.sources]`, run
|
|
112
|
+
`uv lock --upgrade-package <name>`, and open a pull request so CI checks
|
|
113
|
+
the pair together.
|
|
114
|
+
|
|
115
|
+
<!-- series:start -->
|
|
116
|
+
## Related Projects
|
|
117
|
+
|
|
118
|
+
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.
|
|
119
|
+
|
|
120
|
+
| Project | What it teaches | Repository | Docs |
|
|
121
|
+
| --- | --- | --- | --- |
|
|
122
|
+
| PyOS | Operating systems | [py-os](https://github.com/robcox2910/py-os) | [Docs](https://robcox2910.github.io/py-os/) |
|
|
123
|
+
| Pebble | Compilers and programming languages | [pebble-lang](https://github.com/robcox2910/pebble-lang) | [Docs](https://robcox2910.github.io/pebble-lang/) |
|
|
124
|
+
| PyDB | Relational databases | [pydb](https://github.com/robcox2910/pydb) | [Docs](https://robcox2910.github.io/pydb/) |
|
|
125
|
+
| PyKV | Key-value stores | [pykv](https://github.com/robcox2910/pykv) | [Docs](https://robcox2910.github.io/pykv/) |
|
|
126
|
+
| **PyDocDB** | **Document databases** | [pydoc-db](https://github.com/robcox2910/pydoc-db) | [Docs](https://robcox2910.github.io/pydoc-db/) |
|
|
127
|
+
| PyGraphDB | Graph databases | [pygraphdb](https://github.com/robcox2910/pygraphdb) | [Docs](https://robcox2910.github.io/pygraphdb/) |
|
|
128
|
+
| PyTSDB | Time-series databases | [pytsdb](https://github.com/robcox2910/pytsdb) | [Docs](https://robcox2910.github.io/pytsdb/) |
|
|
129
|
+
| PyVecDB | Vector databases | [pyvecdb](https://github.com/robcox2910/pyvecdb) | [Docs](https://robcox2910.github.io/pyvecdb/) |
|
|
130
|
+
| PyColDB | Column-family databases | [pycoldb](https://github.com/robcox2910/pycoldb) | [Docs](https://robcox2910.github.io/pycoldb/) |
|
|
131
|
+
| PyCrypt | Cryptography | [pycrypt](https://github.com/robcox2910/pycrypt) | [Docs](https://robcox2910.github.io/pycrypt/) |
|
|
132
|
+
| PyNet | Computer networking | [pynet](https://github.com/robcox2910/pynet) | [Docs](https://robcox2910.github.io/pynet/) |
|
|
133
|
+
| PyWeb | HTTP web servers | [pyweb](https://github.com/robcox2910/pyweb) | [Docs](https://robcox2910.github.io/pyweb/) |
|
|
134
|
+
| PyGit | Version control | [pygit](https://github.com/robcox2910/pygit) | [Docs](https://robcox2910.github.io/pygit/) |
|
|
135
|
+
| PySearch | Search engines | [pysearch](https://github.com/robcox2910/pysearch) | [Docs](https://robcox2910.github.io/pysearch/) |
|
|
136
|
+
| PyMQ | Message queues | [pymq](https://github.com/robcox2910/pymq) | [Docs](https://robcox2910.github.io/pymq/) |
|
|
137
|
+
| PyLLM | Language models (the brain) | [pyllm](https://github.com/robcox2910/pyllm) | [Docs](https://robcox2910.github.io/pyllm/) |
|
|
138
|
+
| PyScheduler | Job scheduling | [pyscheduler](https://github.com/robcox2910/pyscheduler) | [Docs](https://robcox2910.github.io/pyscheduler/) |
|
|
139
|
+
| PyStack | Full-stack integration | [pystack](https://github.com/robcox2910/pystack) | [Docs](https://robcox2910.github.io/pystack/) |
|
|
140
|
+
<!-- series:end -->
|
|
141
|
+
|
|
142
|
+
<!-- docs:start -->
|
|
143
|
+
## Documentation
|
|
144
|
+
|
|
145
|
+
Full docs (with kid-friendly explanations of every idea): https://robcox2910.github.io/pydoc-db/
|
|
146
|
+
<!-- docs:end -->
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# PyDocDB
|
|
2
|
+
|
|
3
|
+
An educational document store (like MongoDB) built from scratch in Python. Part of the **PyLearn** platform for kids aged 12+.
|
|
4
|
+
|
|
5
|
+
PyDocDB stores JSON-like documents without requiring a fixed schema -- each document can have different fields, just like how every index card in a filing cabinet can have different information on it.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Documents** -- Dict-like objects with auto-generated unique IDs
|
|
10
|
+
- **Collections** -- Named groups of documents with full CRUD operations
|
|
11
|
+
- **Query Language** -- Filter with operators: `$eq`, `$gt`, `$lt`, `$gte`, `$lte`, `$ne`, `$in`, `$exists`, `$and`, `$or`, `$not`
|
|
12
|
+
- **Nested Field Access** -- Query nested fields with dot notation (e.g., `"address.city"`)
|
|
13
|
+
- **Projection** -- Return only specific fields from documents
|
|
14
|
+
- **Indexing** -- Create indexes on fields for faster lookups
|
|
15
|
+
- **Persistence** -- Save/load collections to JSON files on disk
|
|
16
|
+
- **Aggregation** -- Pipeline with `$match`, `$group`, `$sort`, `$limit`, `$count`
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from pydocdb import Collection, save_collection, load_collection, aggregate
|
|
22
|
+
|
|
23
|
+
# Create a collection (like labeling a new folder in a filing cabinet)
|
|
24
|
+
students = Collection("students")
|
|
25
|
+
|
|
26
|
+
# Insert documents (like filing index cards)
|
|
27
|
+
students.insert_one({"name": "Alice", "age": 14, "grade": "9th"})
|
|
28
|
+
students.insert_one({"name": "Bob", "age": 13, "hobby": "chess"})
|
|
29
|
+
students.insert_one({"name": "Charlie", "age": 15, "grade": "10th"})
|
|
30
|
+
|
|
31
|
+
# Find documents with queries
|
|
32
|
+
teenagers = students.find({"age": {"$gte": 13}})
|
|
33
|
+
alice = students.find_one({"name": "Alice"})
|
|
34
|
+
|
|
35
|
+
# Update and delete
|
|
36
|
+
students.update_one({"name": "Alice"}, {"age": 15})
|
|
37
|
+
students.delete_one({"name": "Bob"})
|
|
38
|
+
|
|
39
|
+
# Use projection to get only specific fields
|
|
40
|
+
names = students.find({}, projection={"name": 1, "_id": 0})
|
|
41
|
+
|
|
42
|
+
# Aggregate data
|
|
43
|
+
result = aggregate(students.to_list(), [
|
|
44
|
+
{"$group": {"_id": "$grade", "count": {"$sum": 1}}},
|
|
45
|
+
{"$sort": {"count": -1}},
|
|
46
|
+
])
|
|
47
|
+
|
|
48
|
+
# Save to disk and load later
|
|
49
|
+
save_collection(students, "students.json")
|
|
50
|
+
loaded = load_collection("students.json")
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
uv add pydocdb
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Development
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
git clone https://github.com/robcox2910/pydoc-db.git
|
|
63
|
+
cd pydoc-db
|
|
64
|
+
uv sync --all-extras
|
|
65
|
+
uv run pytest
|
|
66
|
+
uv run ruff check src/ tests/
|
|
67
|
+
uv run pyright src tests
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Releasing
|
|
71
|
+
|
|
72
|
+
Other projects in the series ([PyStack](https://github.com/robcox2910/pystack)) use a pinned release of this one,
|
|
73
|
+
not whatever is on `main`. To publish a change for them:
|
|
74
|
+
|
|
75
|
+
1. Merge the change to `main`.
|
|
76
|
+
2. Tag it: `git tag -a vX.Y.Z -m vX.Y.Z && git push origin vX.Y.Z`.
|
|
77
|
+
A change to the public API bumps the minor version (`v0.1.0` → `v0.2.0`);
|
|
78
|
+
anything else bumps the patch (`v0.1.0` → `v0.1.1`).
|
|
79
|
+
3. In each project that uses it, edit the tag in `[tool.uv.sources]`, run
|
|
80
|
+
`uv lock --upgrade-package <name>`, and open a pull request so CI checks
|
|
81
|
+
the pair together.
|
|
82
|
+
|
|
83
|
+
<!-- series:start -->
|
|
84
|
+
## Related Projects
|
|
85
|
+
|
|
86
|
+
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.
|
|
87
|
+
|
|
88
|
+
| Project | What it teaches | Repository | Docs |
|
|
89
|
+
| --- | --- | --- | --- |
|
|
90
|
+
| PyOS | Operating systems | [py-os](https://github.com/robcox2910/py-os) | [Docs](https://robcox2910.github.io/py-os/) |
|
|
91
|
+
| Pebble | Compilers and programming languages | [pebble-lang](https://github.com/robcox2910/pebble-lang) | [Docs](https://robcox2910.github.io/pebble-lang/) |
|
|
92
|
+
| PyDB | Relational databases | [pydb](https://github.com/robcox2910/pydb) | [Docs](https://robcox2910.github.io/pydb/) |
|
|
93
|
+
| PyKV | Key-value stores | [pykv](https://github.com/robcox2910/pykv) | [Docs](https://robcox2910.github.io/pykv/) |
|
|
94
|
+
| **PyDocDB** | **Document databases** | [pydoc-db](https://github.com/robcox2910/pydoc-db) | [Docs](https://robcox2910.github.io/pydoc-db/) |
|
|
95
|
+
| PyGraphDB | Graph databases | [pygraphdb](https://github.com/robcox2910/pygraphdb) | [Docs](https://robcox2910.github.io/pygraphdb/) |
|
|
96
|
+
| PyTSDB | Time-series databases | [pytsdb](https://github.com/robcox2910/pytsdb) | [Docs](https://robcox2910.github.io/pytsdb/) |
|
|
97
|
+
| PyVecDB | Vector databases | [pyvecdb](https://github.com/robcox2910/pyvecdb) | [Docs](https://robcox2910.github.io/pyvecdb/) |
|
|
98
|
+
| PyColDB | Column-family databases | [pycoldb](https://github.com/robcox2910/pycoldb) | [Docs](https://robcox2910.github.io/pycoldb/) |
|
|
99
|
+
| PyCrypt | Cryptography | [pycrypt](https://github.com/robcox2910/pycrypt) | [Docs](https://robcox2910.github.io/pycrypt/) |
|
|
100
|
+
| PyNet | Computer networking | [pynet](https://github.com/robcox2910/pynet) | [Docs](https://robcox2910.github.io/pynet/) |
|
|
101
|
+
| PyWeb | HTTP web servers | [pyweb](https://github.com/robcox2910/pyweb) | [Docs](https://robcox2910.github.io/pyweb/) |
|
|
102
|
+
| PyGit | Version control | [pygit](https://github.com/robcox2910/pygit) | [Docs](https://robcox2910.github.io/pygit/) |
|
|
103
|
+
| PySearch | Search engines | [pysearch](https://github.com/robcox2910/pysearch) | [Docs](https://robcox2910.github.io/pysearch/) |
|
|
104
|
+
| PyMQ | Message queues | [pymq](https://github.com/robcox2910/pymq) | [Docs](https://robcox2910.github.io/pymq/) |
|
|
105
|
+
| PyLLM | Language models (the brain) | [pyllm](https://github.com/robcox2910/pyllm) | [Docs](https://robcox2910.github.io/pyllm/) |
|
|
106
|
+
| PyScheduler | Job scheduling | [pyscheduler](https://github.com/robcox2910/pyscheduler) | [Docs](https://robcox2910.github.io/pyscheduler/) |
|
|
107
|
+
| PyStack | Full-stack integration | [pystack](https://github.com/robcox2910/pystack) | [Docs](https://robcox2910.github.io/pystack/) |
|
|
108
|
+
<!-- series:end -->
|
|
109
|
+
|
|
110
|
+
<!-- docs:start -->
|
|
111
|
+
## Documentation
|
|
112
|
+
|
|
113
|
+
Full docs (with kid-friendly explanations of every idea): https://robcox2910.github.io/pydoc-db/
|
|
114
|
+
<!-- docs: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,91 @@
|
|
|
1
|
+
# Aggregation
|
|
2
|
+
|
|
3
|
+
## The "Sorting and Counting Your Card Collection" Analogy
|
|
4
|
+
|
|
5
|
+
Imagine you have a huge pile of trading cards. You might want to:
|
|
6
|
+
|
|
7
|
+
1. **Pick out** only the rare cards (`$match`)
|
|
8
|
+
2. **Group** them by type -- Fire, Water, Grass (`$group`)
|
|
9
|
+
3. **Sort** the groups by how many cards each has (`$sort`)
|
|
10
|
+
4. **Look at only** the top 3 groups (`$limit`)
|
|
11
|
+
5. **Count** how many groups there are (`$count`)
|
|
12
|
+
|
|
13
|
+
That's an aggregation pipeline! Each step transforms the data, and the output of one step becomes the input for the next.
|
|
14
|
+
|
|
15
|
+
## Pipeline Stages
|
|
16
|
+
|
|
17
|
+
### $match -- Filter Documents
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from pydocdb import Collection, aggregate
|
|
21
|
+
|
|
22
|
+
products = Collection("products")
|
|
23
|
+
products.insert_many([
|
|
24
|
+
{"name": "Widget", "category": "A", "price": 10},
|
|
25
|
+
{"name": "Gadget", "category": "A", "price": 20},
|
|
26
|
+
{"name": "Doohickey", "category": "B", "price": 30},
|
|
27
|
+
])
|
|
28
|
+
|
|
29
|
+
# Get only category A products
|
|
30
|
+
result = aggregate(products.to_list(), [
|
|
31
|
+
{"$match": {"category": "A"}}
|
|
32
|
+
])
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### $group -- Group and Summarize
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
# Group by category and sum prices
|
|
39
|
+
result = aggregate(products.to_list(), [
|
|
40
|
+
{"$group": {
|
|
41
|
+
"_id": "$category",
|
|
42
|
+
"total_price": {"$sum": "$price"},
|
|
43
|
+
"count": {"$sum": 1},
|
|
44
|
+
"avg_price": {"$avg": "$price"},
|
|
45
|
+
}}
|
|
46
|
+
])
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Available accumulators: `$sum`, `$avg`, `$min`, `$max`
|
|
50
|
+
|
|
51
|
+
### $sort -- Order Results
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
# Sort by price, highest first
|
|
55
|
+
result = aggregate(products.to_list(), [
|
|
56
|
+
{"$sort": {"price": -1}} # -1 = descending, 1 = ascending
|
|
57
|
+
])
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### $limit -- Take Only N Results
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
# Get the top 2 most expensive
|
|
64
|
+
result = aggregate(products.to_list(), [
|
|
65
|
+
{"$sort": {"price": -1}},
|
|
66
|
+
{"$limit": 2}
|
|
67
|
+
])
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### $count -- Count Documents
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
# Count how many products there are
|
|
74
|
+
result = aggregate(products.to_list(), [
|
|
75
|
+
{"$count": "total_products"}
|
|
76
|
+
])
|
|
77
|
+
# [{"total_products": 3}]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Multi-Stage Pipelines
|
|
81
|
+
|
|
82
|
+
The real power comes from chaining stages together:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
result = aggregate(products.to_list(), [
|
|
86
|
+
{"$match": {"price": {"$gte": 15}}}, # Step 1: Filter expensive items
|
|
87
|
+
{"$group": {"_id": "$category", "total": {"$sum": "$price"}}}, # Step 2: Group by category
|
|
88
|
+
{"$sort": {"total": -1}}, # Step 3: Sort by total, highest first
|
|
89
|
+
{"$limit": 5}, # Step 4: Only top 5
|
|
90
|
+
])
|
|
91
|
+
```
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Collections
|
|
2
|
+
|
|
3
|
+
## The Folder in a Filing Cabinet Analogy
|
|
4
|
+
|
|
5
|
+
A collection is like a **labeled folder** in a filing cabinet. The folder called "Students" holds all the student index cards. The folder called "Products" holds all the product cards. Each folder is independent -- adding a card to "Students" doesn't affect "Products."
|
|
6
|
+
|
|
7
|
+
## CRUD Operations
|
|
8
|
+
|
|
9
|
+
CRUD stands for **C**reate, **R**ead, **U**pdate, **D**elete -- the four basic things you can do with data.
|
|
10
|
+
|
|
11
|
+
### Create (Insert)
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from pydocdb import Collection
|
|
15
|
+
|
|
16
|
+
students = Collection("students")
|
|
17
|
+
|
|
18
|
+
# Insert one document
|
|
19
|
+
alice = students.insert_one({"name": "Alice", "age": 14})
|
|
20
|
+
|
|
21
|
+
# Insert many at once
|
|
22
|
+
students.insert_many([
|
|
23
|
+
{"name": "Bob", "age": 13},
|
|
24
|
+
{"name": "Charlie", "age": 15},
|
|
25
|
+
])
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Read (Find)
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
# Find all documents
|
|
32
|
+
all_students = students.find()
|
|
33
|
+
|
|
34
|
+
# Find with a query
|
|
35
|
+
teens = students.find({"age": {"$gte": 13}})
|
|
36
|
+
|
|
37
|
+
# Find just one
|
|
38
|
+
alice = students.find_one({"name": "Alice"})
|
|
39
|
+
|
|
40
|
+
# Find by ID
|
|
41
|
+
doc = students.find_by_id("some-id-here")
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Update
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
# Update one document
|
|
48
|
+
students.update_one({"name": "Alice"}, {"age": 15})
|
|
49
|
+
|
|
50
|
+
# Update many documents
|
|
51
|
+
students.update_many({"grade": "9th"}, {"year": 2024})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Delete
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
# Delete one document
|
|
58
|
+
students.delete_one({"name": "Alice"})
|
|
59
|
+
|
|
60
|
+
# Delete many documents
|
|
61
|
+
students.delete_many({"graduated": True})
|
|
62
|
+
```
|