fromscratch-pycoldb 0.4.0__py3-none-any.whl
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_pycoldb-0.4.0.dist-info/METADATA +155 -0
- fromscratch_pycoldb-0.4.0.dist-info/RECORD +18 -0
- fromscratch_pycoldb-0.4.0.dist-info/WHEEL +4 -0
- fromscratch_pycoldb-0.4.0.dist-info/licenses/LICENSE +21 -0
- pycoldb/__init__.py +65 -0
- pycoldb/_version.py +24 -0
- pycoldb/atomic.py +66 -0
- pycoldb/column.py +73 -0
- pycoldb/column_family.py +294 -0
- pycoldb/compaction.py +81 -0
- pycoldb/errors.py +86 -0
- pycoldb/keyspace.py +200 -0
- pycoldb/persistence.py +191 -0
- pycoldb/py.typed +0 -0
- pycoldb/query.py +140 -0
- pycoldb/row.py +249 -0
- pycoldb/tombstone.py +73 -0
- pycoldb/ttl.py +291 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fromscratch-pycoldb
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: An educational column-family store library built from scratch in Python
|
|
5
|
+
Project-URL: Homepage, https://github.com/robcox2910/pycoldb
|
|
6
|
+
Project-URL: Repository, https://github.com/robcox2910/pycoldb
|
|
7
|
+
Project-URL: Issues, https://github.com/robcox2910/pycoldb/issues
|
|
8
|
+
Project-URL: Documentation, https://robcox2910.github.io/pycoldb/
|
|
9
|
+
Author: Rob Cox
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: column-family,database,education,wide-column
|
|
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
|
+
# PyColDB
|
|
34
|
+
|
|
35
|
+
An educational column-family store (wide-column database) built from scratch in Python. Part of the **PyLearn** platform for kids aged 12+.
|
|
36
|
+
|
|
37
|
+
## What is a Column-Family Store?
|
|
38
|
+
|
|
39
|
+
A column-family store is like a spreadsheet where every row can have different columns. Instead of forcing every record to have the same fields, each row picks and chooses what data it stores. Real-world examples include Apache Cassandra and HBase.
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
- **Columns** -- Name-value pairs with timestamps
|
|
44
|
+
- **Rows** -- Flexible records where each row has its own set of columns
|
|
45
|
+
- **Column Families** -- Named groups of rows (like gradebooks)
|
|
46
|
+
- **Keyspaces** -- Top-level containers for column families (like a school)
|
|
47
|
+
- **CRUD** -- Insert, read, update, and delete columns within rows
|
|
48
|
+
- **Range Queries** -- Get rows by key range (e.g., keys from "A" to "M")
|
|
49
|
+
- **Column Filtering** -- Get only specific columns from a row
|
|
50
|
+
- **TTL** -- Columns can expire after a time-to-live
|
|
51
|
+
- **Tombstones** -- Soft deletion with markers (like Cassandra)
|
|
52
|
+
- **Persistence** -- Save and load to JSON files
|
|
53
|
+
- **Compaction** -- Remove expired and deleted entries to reclaim space
|
|
54
|
+
|
|
55
|
+
## Quick Start
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from pycoldb import Keyspace, Column
|
|
59
|
+
|
|
60
|
+
# Create a keyspace (like a school)
|
|
61
|
+
school = Keyspace(name="school")
|
|
62
|
+
|
|
63
|
+
# Create a column family (like a gradebook)
|
|
64
|
+
school.create_column_family("grades")
|
|
65
|
+
|
|
66
|
+
# Add grades -- each student can have different columns!
|
|
67
|
+
school.put("grades", "alice", Column(name="math", value="A+"))
|
|
68
|
+
school.put("grades", "alice", Column(name="science", value="A"))
|
|
69
|
+
school.put("grades", "bob", Column(name="math", value="B"))
|
|
70
|
+
school.put("grades", "bob", Column(name="music", value="A"))
|
|
71
|
+
|
|
72
|
+
# Read a grade
|
|
73
|
+
grade = school.get_column("grades", "alice", "math")
|
|
74
|
+
print(f"Alice's math grade: {grade.value}") # A+
|
|
75
|
+
|
|
76
|
+
# Range query -- get students from "alice" to "bob"
|
|
77
|
+
from pycoldb import range_query
|
|
78
|
+
cf = school.get_column_family("grades")
|
|
79
|
+
rows = range_query(cf, start_key="alice", end_key="bob")
|
|
80
|
+
for row in rows:
|
|
81
|
+
print(f"{row.key}: {row.column_names()}")
|
|
82
|
+
|
|
83
|
+
# Save to disk
|
|
84
|
+
from pycoldb import save_keyspace, load_keyspace
|
|
85
|
+
save_keyspace(school, "school.json")
|
|
86
|
+
restored = load_keyspace("school.json")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Installation
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# From source
|
|
93
|
+
git clone https://github.com/robcox2910/pycoldb.git
|
|
94
|
+
cd pycoldb
|
|
95
|
+
uv sync --all-extras
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Development
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
uv run ruff check src/ tests/ # Lint
|
|
102
|
+
uv run ruff format src/ tests/ # Format
|
|
103
|
+
uv run pyright src tests # Type check
|
|
104
|
+
uv run pytest # Test (90%+ coverage)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Releasing
|
|
108
|
+
|
|
109
|
+
Other projects in the series ([PyStack](https://github.com/robcox2910/pystack)) use a pinned release of this one,
|
|
110
|
+
not whatever is on `main`. To publish a change for them:
|
|
111
|
+
|
|
112
|
+
1. Merge the change to `main`.
|
|
113
|
+
2. Tag it: `git tag -a vX.Y.Z -m vX.Y.Z && git push origin vX.Y.Z`.
|
|
114
|
+
A change to the public API bumps the minor version (`v0.1.0` → `v0.2.0`);
|
|
115
|
+
anything else bumps the patch (`v0.1.0` → `v0.1.1`).
|
|
116
|
+
3. In each project that uses it, edit the tag in `[tool.uv.sources]`, run
|
|
117
|
+
`uv lock --upgrade-package <name>`, and open a pull request so CI checks
|
|
118
|
+
the pair together.
|
|
119
|
+
|
|
120
|
+
<!-- series:start -->
|
|
121
|
+
## Related Projects
|
|
122
|
+
|
|
123
|
+
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.
|
|
124
|
+
|
|
125
|
+
| Project | What it teaches | Repository | Docs |
|
|
126
|
+
| --- | --- | --- | --- |
|
|
127
|
+
| PyOS | Operating systems | [py-os](https://github.com/robcox2910/py-os) | [Docs](https://robcox2910.github.io/py-os/) |
|
|
128
|
+
| Pebble | Compilers and programming languages | [pebble-lang](https://github.com/robcox2910/pebble-lang) | [Docs](https://robcox2910.github.io/pebble-lang/) |
|
|
129
|
+
| PyDB | Relational databases | [pydb](https://github.com/robcox2910/pydb) | [Docs](https://robcox2910.github.io/pydb/) |
|
|
130
|
+
| PyKV | Key-value stores | [pykv](https://github.com/robcox2910/pykv) | [Docs](https://robcox2910.github.io/pykv/) |
|
|
131
|
+
| PyDocDB | Document databases | [pydoc-db](https://github.com/robcox2910/pydoc-db) | [Docs](https://robcox2910.github.io/pydoc-db/) |
|
|
132
|
+
| PyGraphDB | Graph databases | [pygraphdb](https://github.com/robcox2910/pygraphdb) | [Docs](https://robcox2910.github.io/pygraphdb/) |
|
|
133
|
+
| PyTSDB | Time-series databases | [pytsdb](https://github.com/robcox2910/pytsdb) | [Docs](https://robcox2910.github.io/pytsdb/) |
|
|
134
|
+
| PyVecDB | Vector databases | [pyvecdb](https://github.com/robcox2910/pyvecdb) | [Docs](https://robcox2910.github.io/pyvecdb/) |
|
|
135
|
+
| **PyColDB** | **Column-family databases** | [pycoldb](https://github.com/robcox2910/pycoldb) | [Docs](https://robcox2910.github.io/pycoldb/) |
|
|
136
|
+
| PyCrypt | Cryptography | [pycrypt](https://github.com/robcox2910/pycrypt) | [Docs](https://robcox2910.github.io/pycrypt/) |
|
|
137
|
+
| PyNet | Computer networking | [pynet](https://github.com/robcox2910/pynet) | [Docs](https://robcox2910.github.io/pynet/) |
|
|
138
|
+
| PyWeb | HTTP web servers | [pyweb](https://github.com/robcox2910/pyweb) | [Docs](https://robcox2910.github.io/pyweb/) |
|
|
139
|
+
| PyGit | Version control | [pygit](https://github.com/robcox2910/pygit) | [Docs](https://robcox2910.github.io/pygit/) |
|
|
140
|
+
| PySearch | Search engines | [pysearch](https://github.com/robcox2910/pysearch) | [Docs](https://robcox2910.github.io/pysearch/) |
|
|
141
|
+
| PyMQ | Message queues | [pymq](https://github.com/robcox2910/pymq) | [Docs](https://robcox2910.github.io/pymq/) |
|
|
142
|
+
| PyLLM | Language models (the brain) | [pyllm](https://github.com/robcox2910/pyllm) | [Docs](https://robcox2910.github.io/pyllm/) |
|
|
143
|
+
| PyScheduler | Job scheduling | [pyscheduler](https://github.com/robcox2910/pyscheduler) | [Docs](https://robcox2910.github.io/pyscheduler/) |
|
|
144
|
+
| PyStack | Full-stack integration | [pystack](https://github.com/robcox2910/pystack) | [Docs](https://robcox2910.github.io/pystack/) |
|
|
145
|
+
<!-- series:end -->
|
|
146
|
+
|
|
147
|
+
<!-- docs:start -->
|
|
148
|
+
## Documentation
|
|
149
|
+
|
|
150
|
+
Full docs (with kid-friendly explanations of every idea): https://robcox2910.github.io/pycoldb/
|
|
151
|
+
<!-- docs:end -->
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
pycoldb/__init__.py,sha256=RAeXrOTOZuneYqnMZGyNXD3GMq6Opx7epLuMhb9fusw,1997
|
|
2
|
+
pycoldb/_version.py,sha256=HGBlVvZpQ8N2kh1n_LY5PM4s9ljcIgdbOCgXN-uFje4,520
|
|
3
|
+
pycoldb/atomic.py,sha256=QLLPGZqpA-TgVV5lDEKCLB7vVr05bxj97_Fwykz_gi4,2345
|
|
4
|
+
pycoldb/column.py,sha256=zpsjnqekaO7hN6nyG-e4AyagpkBUNOLsDUv7n1Ar8WA,2108
|
|
5
|
+
pycoldb/column_family.py,sha256=ptZ8aWXUP4iVH5QJHrvqE4AvPR6zvgqlgNdgHFKHzHo,10306
|
|
6
|
+
pycoldb/compaction.py,sha256=1gCKPCtPQTuzFFNhckpx6R_kkAXAY_asUzLHqKRW1Wg,2596
|
|
7
|
+
pycoldb/errors.py,sha256=S6SZy5GZvvOV_R-1D68GoyB9RyOnIU0aifAL1QHw0Pk,2658
|
|
8
|
+
pycoldb/keyspace.py,sha256=SYRTjZ8ILh1L8xp1qdh_drNxHPeiDCzCWoV0sb169_w,6279
|
|
9
|
+
pycoldb/persistence.py,sha256=7ZNqERzSQLTdnpkHPG1uVBECNBVQDbX6-k8uQkhGS7o,7925
|
|
10
|
+
pycoldb/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
pycoldb/query.py,sha256=lyGa0gL0UVF8-FRrSfJ-ai75aZAI-3WE6DzcMYpPOZw,4626
|
|
12
|
+
pycoldb/row.py,sha256=0w892qWqnlhIiK75OkLOf6QbUT78BwuYjbJEf65r5B4,9515
|
|
13
|
+
pycoldb/tombstone.py,sha256=_Xt_4B2t66xLLr1W1zekvR0ZmvFj58zxZ0Za3e46Zc0,2292
|
|
14
|
+
pycoldb/ttl.py,sha256=XtbI3E19ImboY2v9NslsUlh3IArI9z7HK-rw7Lzk41I,8993
|
|
15
|
+
fromscratch_pycoldb-0.4.0.dist-info/METADATA,sha256=JuKzTc4tidccOR4LH3Ibr1-RxpqSamHE6DH_dMFxOLM,7304
|
|
16
|
+
fromscratch_pycoldb-0.4.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
17
|
+
fromscratch_pycoldb-0.4.0.dist-info/licenses/LICENSE,sha256=NtHM2ak--oE71VCcY50kcCoC1cKS0LRUsxq_xA89RNc,1064
|
|
18
|
+
fromscratch_pycoldb-0.4.0.dist-info/RECORD,,
|
|
@@ -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.
|
pycoldb/__init__.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""PyColDB -- An educational column-family store built from scratch in Python.
|
|
2
|
+
|
|
3
|
+
A column-family store (also called a wide-column store) is a type of NoSQL
|
|
4
|
+
database where each row can have different columns. Think of it like a
|
|
5
|
+
spreadsheet where every row is allowed to have different column headers!
|
|
6
|
+
|
|
7
|
+
Real-world column-family stores include Apache Cassandra and HBase. This
|
|
8
|
+
library teaches the core concepts in a simple, understandable way.
|
|
9
|
+
|
|
10
|
+
Quick Start:
|
|
11
|
+
>>> from pycoldb import Keyspace, Column
|
|
12
|
+
>>> school = Keyspace(name="school")
|
|
13
|
+
>>> school.create_column_family("grades")
|
|
14
|
+
ColumnFamily(name='grades', ...)
|
|
15
|
+
>>> school.put("grades", "alice", Column(name="math", value="A+"))
|
|
16
|
+
>>> school.get_column("grades", "alice", "math").value
|
|
17
|
+
'A+'
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from pycoldb.column import Column
|
|
21
|
+
from pycoldb.column_family import ColumnFamily
|
|
22
|
+
from pycoldb.compaction import compact_all, compact_column_family
|
|
23
|
+
from pycoldb.errors import (
|
|
24
|
+
ColumnFamilyAlreadyExistsError,
|
|
25
|
+
ColumnFamilyNotFoundError,
|
|
26
|
+
ColumnNotFoundError,
|
|
27
|
+
InvalidTTLError,
|
|
28
|
+
KeyspaceAlreadyExistsError,
|
|
29
|
+
KeyspaceNotFoundError,
|
|
30
|
+
PersistenceError,
|
|
31
|
+
PyColDBError,
|
|
32
|
+
RowNotFoundError,
|
|
33
|
+
)
|
|
34
|
+
from pycoldb.keyspace import Keyspace
|
|
35
|
+
from pycoldb.persistence import load_keyspace, save_keyspace
|
|
36
|
+
from pycoldb.query import column_filter, query_with_filter, range_query
|
|
37
|
+
from pycoldb.row import Row
|
|
38
|
+
from pycoldb.tombstone import Tombstone
|
|
39
|
+
from pycoldb.ttl import TTLEntry, TTLManager
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"Column",
|
|
43
|
+
"ColumnFamily",
|
|
44
|
+
"ColumnFamilyAlreadyExistsError",
|
|
45
|
+
"ColumnFamilyNotFoundError",
|
|
46
|
+
"ColumnNotFoundError",
|
|
47
|
+
"InvalidTTLError",
|
|
48
|
+
"Keyspace",
|
|
49
|
+
"KeyspaceAlreadyExistsError",
|
|
50
|
+
"KeyspaceNotFoundError",
|
|
51
|
+
"PersistenceError",
|
|
52
|
+
"PyColDBError",
|
|
53
|
+
"Row",
|
|
54
|
+
"RowNotFoundError",
|
|
55
|
+
"TTLEntry",
|
|
56
|
+
"TTLManager",
|
|
57
|
+
"Tombstone",
|
|
58
|
+
"column_filter",
|
|
59
|
+
"compact_all",
|
|
60
|
+
"compact_column_family",
|
|
61
|
+
"load_keyspace",
|
|
62
|
+
"query_with_filter",
|
|
63
|
+
"range_query",
|
|
64
|
+
"save_keyspace",
|
|
65
|
+
]
|
pycoldb/_version.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.4.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 4, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
pycoldb/atomic.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Safe saving -- never leave a half-written file behind.
|
|
2
|
+
|
|
3
|
+
The usual way to save a file is: open it, wipe it, write the new
|
|
4
|
+
contents. If the computer crashes after the wipe but before the writing
|
|
5
|
+
finishes, your data is gone -- the old version was wiped and the new one
|
|
6
|
+
never arrived.
|
|
7
|
+
|
|
8
|
+
So we save like a careful student copying out their homework:
|
|
9
|
+
|
|
10
|
+
1. Write the new version on a **fresh sheet** (a temporary file sitting
|
|
11
|
+
right next to the real one).
|
|
12
|
+
2. Make sure the **ink is dry** -- ``fsync`` forces the bytes out of the
|
|
13
|
+
computer's memory and onto the disk.
|
|
14
|
+
3. **Swap** the fresh sheet in for the old one in a single instant step
|
|
15
|
+
(``Path.replace``). There is never a moment with no complete file: you
|
|
16
|
+
see either the old version or the new one, never half of each.
|
|
17
|
+
|
|
18
|
+
Real databases (SQLite, PostgreSQL, Redis) all use this same trick.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import contextlib
|
|
22
|
+
import os
|
|
23
|
+
import stat
|
|
24
|
+
import tempfile
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
# Permissions for a brand-new file: owner can read/write, others can read.
|
|
28
|
+
_NEW_FILE_MODE = 0o644
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def atomic_write_text(path: Path, text: str) -> None:
|
|
32
|
+
"""Replace the file at *path* with *text*, so a crash can never leave it half-written.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
path: The file to create or replace (its folder must already exist).
|
|
36
|
+
text: The new contents, saved as UTF-8.
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
mode = stat.S_IMODE(path.stat().st_mode) if path.exists() else _NEW_FILE_MODE
|
|
40
|
+
fd, temp_name = tempfile.mkstemp(dir=path.parent, prefix=f".{path.name}.", suffix=".tmp")
|
|
41
|
+
temp_path = Path(temp_name)
|
|
42
|
+
try:
|
|
43
|
+
with os.fdopen(fd, "w", encoding="utf-8") as temp_file:
|
|
44
|
+
temp_file.write(text)
|
|
45
|
+
temp_file.flush()
|
|
46
|
+
os.fsync(temp_file.fileno())
|
|
47
|
+
temp_path.chmod(mode)
|
|
48
|
+
temp_path.replace(path)
|
|
49
|
+
except BaseException:
|
|
50
|
+
temp_path.unlink(missing_ok=True) # don't leave the scrap sheet lying around
|
|
51
|
+
raise
|
|
52
|
+
_sync_folder(path.parent)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _sync_folder(folder: Path) -> None:
|
|
56
|
+
"""Make the swap itself durable by syncing the folder's list of files.
|
|
57
|
+
|
|
58
|
+
Some systems (like Windows) can't sync a folder; there the swap is
|
|
59
|
+
still safe, just not guaranteed to survive a power cut.
|
|
60
|
+
"""
|
|
61
|
+
with contextlib.suppress(OSError):
|
|
62
|
+
fd = os.open(folder, os.O_RDONLY)
|
|
63
|
+
try:
|
|
64
|
+
os.fsync(fd)
|
|
65
|
+
finally:
|
|
66
|
+
os.close(fd)
|
pycoldb/column.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Column data structure for the column-family store.
|
|
2
|
+
|
|
3
|
+
Think of a column like a single cell in a spreadsheet. Each cell has:
|
|
4
|
+
- A **name** (the column header, like "Math Grade")
|
|
5
|
+
- A **value** (the actual data, like "A+")
|
|
6
|
+
- A **timestamp** (when the data was written, like a date stamp)
|
|
7
|
+
|
|
8
|
+
Unlike a regular spreadsheet, every cell remembers exactly when it was
|
|
9
|
+
last updated. This is super useful for knowing which data is newest!
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
>>> from pycoldb.column import Column
|
|
13
|
+
>>> grade = Column(name="math", value="A+", timestamp=1000)
|
|
14
|
+
>>> grade.name
|
|
15
|
+
'math'
|
|
16
|
+
>>> grade.value
|
|
17
|
+
'A+'
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import time
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Column:
|
|
27
|
+
"""A single named value with a timestamp.
|
|
28
|
+
|
|
29
|
+
A column is the smallest unit of data in a column-family store.
|
|
30
|
+
Think of it as one cell in a spreadsheet that also remembers
|
|
31
|
+
when it was last changed.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
name: The column name (like a spreadsheet header).
|
|
35
|
+
value: The stored data (can be any JSON-serializable value).
|
|
36
|
+
timestamp: When this column was last written (epoch seconds).
|
|
37
|
+
If not provided, uses the current time.
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
name: str
|
|
42
|
+
value: str | int | float | bool | None
|
|
43
|
+
timestamp: float = field(default_factory=time.time)
|
|
44
|
+
|
|
45
|
+
def to_dict(self) -> dict[str, str | int | float | bool | None]:
|
|
46
|
+
"""Convert this column to a dictionary for serialization.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
A dictionary with name, value, and timestamp keys.
|
|
50
|
+
|
|
51
|
+
"""
|
|
52
|
+
return {
|
|
53
|
+
"name": self.name,
|
|
54
|
+
"value": self.value,
|
|
55
|
+
"timestamp": self.timestamp,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def from_dict(cls, data: dict[str, str | int | float | bool | None]) -> Column:
|
|
60
|
+
"""Create a Column from a dictionary.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
data: Dictionary with name, value, and timestamp keys.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
A new Column instance.
|
|
67
|
+
|
|
68
|
+
"""
|
|
69
|
+
return cls(
|
|
70
|
+
name=str(data["name"]),
|
|
71
|
+
value=data["value"],
|
|
72
|
+
timestamp=float(data.get("timestamp", 0.0) or 0.0),
|
|
73
|
+
)
|