Skip to main content

PageStore

PageStore stores named time series in immutable, checksummed array pages. The new API uses a filesystem catalog with per-signal coordination and no SQLite index. The design and remaining milestones are documented in architecture.md.

Installation and development

Requires Python 3.10 or newer. Install from PyPI with:

python -m pip install pagestore

NumPy is installed automatically. From a checkout, install with:

python -m pip install .

For an editable development installation and the test suite:

python -m pip install -e ".[testing]"
python -m pytest -q

sh mktest also writes an HTML coverage report to htmlcov/. Build and validate the source distribution and wheel with:

python -m pip install build twine
python -m build
python -m twine check dist/*

Packaging is configured in pyproject.toml; the version comes from pagestore/version.py. Both distributions include the legacy namespace.

Store and read

from pagestore import DB

with DB("./measurements") as db:
    db.store({"temperature": ([1, 2, 3], [10.0, 11.0, 12.0])})
    db.store({"temperature": ([3, 4], [12.5, 13.0])})
    timestamps, values = db.get_signal("temperature", 2, 4)
    # timestamps: [2, 3, 4]; values: [11.0, 12.5, 13.0]
    print(db.info("temperature"))

Bounds are inclusive. Writes sort timestamps, keep the last incoming duplicate, and replace existing records at equal timestamps. skip=1 selects every second record across the entire selected interval; max_count limits the final result. get("regexp") searches names using re.search; get(["exact/name"]) selects exact names. Unknown exact names raise SignalNotFoundError.

Numeric timestamps remain numeric measurement axes. Integer timestamps are int64; floating timestamps are float64. A signal's timestamp kind is fixed by its first nonempty write, and subsequent conversions must be exact.

Datetimes and timezones

with DB("./measurements", timezone="utc") as db:
    db.store({"beam": (["2026-01-01 12:00:00.123456789"], [42.0])},
             timezone="cern")
    timestamps, values = db.get_signal(
        "beam", "2026-01-01T11:00:00Z", "2026-01-01T12:00:00Z")

timezone="utc" is the default, matching NXCALS's timestamp convention. "cern" means Europe/Zurich; "local" uses the machine's timezone including DST rules. IANA names also work. Method-level timezone= overrides the instance default for parsing naive ISO strings and Python datetimes. Explicit offsets and aware datetimes retain their own meaning. Ambiguous or nonexistent naive times at DST transitions are rejected; supply an offset or an aware datetime with fold.

Datetimes are stored and returned as UTC numpy.datetime64[ns], with exact nanosecond precision. NumPy datetime arrays are already UTC. For raw NXCALS int64 nanoseconds, convert explicitly without copying:

timestamps = timestamps_ns.view("datetime64[ns]")

Parallel bulk loads

Assign different signals to different workers; each worker opens its own DB. ingest consumes an iterator of Batch objects or (timestamps, values) tuples. It buffers approximately one commit group, rather than the entire extraction.

from pagestore import Batch, DB

def load_signal(path, name, extracted_batches):
    with DB(path) as db:
        return db.ingest(name, extracted_batches)

Each signal defaults to 8 MiB per serialized page, including metadata and hashes. Set max_page_size=64 * 1024**2 on store or ingest for larger records or datasets, or call db.configure_signal(name, max_page_size=...) later. One indivisible record larger than the limit gets its own oversized page. Changes to the size policy affect new pages.

Fresh, disjoint writes read and rewrite no existing measurement pages. Ingestion rejects overlaps by default; use on_overlap="replace" for corrections. Commits default to roughly 64 MiB. A final checkpoint makes the page set independently recoverable. Earlier groups remain committed if a later group fails:

from pagestore import IngestError

try:
    result = db.ingest("signal", batches)
except IngestError as error:
    print(error.progress, error.cause)

Progress includes acknowledged counts, generation, commit ID, batch index, and record offset in the sorted, deduplicated batch. A cursor inside an unsorted source batch must be applied after the same normalization. StoreError similarly reports successful signals if a later signal fails. There is no multi-signal transaction. A RecoveryIncompleteError cause means HEAD is committed but the recovery copies need completion; call db.repair_recovery(name) without replaying the upsert. An unknown publication outcome carries its commit ID for reconciliation.

Records and ownership

Dense values have shape (records, *record_shape). Record dtype and shape may change over time. Reads covering different schemas return a one-dimensional object array, preserving each record's NumPy dtype and shape. Supported stored types are bool, integers through 64 bits, float16/32/64, complex64/128, and fixed-width byte/Unicode strings. Arbitrary Python objects and structured dtypes are rejected.

Use RaggedArray.from_arrays(records) for arrays with a variable first record dimension and equal dtype/trailing dimensions, then pass Batch(times, ragged). All stored multibyte arrays use explicit little-endian encoding. Eager results own writable, native-endian memory. Streaming batches can retain read-only mapped pages even after the stream or database is closed:

with db.iter_signal("signal", skip=1) as stream:
    for batch in stream:
        process(batch.timestamps, batch.values)

Integrity and recovery

Each page contains two header copies, section hashes, and an embedded whole-page SHA-256. Paired recovery pages describe confirmed commits. Ordinary mapped reads validate metadata; explicitly verify all payload bytes with db.check(full=True).

from pagestore.maintenance import recover

report = recover("./damaged-or-flattened-pages", "./recovered")
assert report.complete, report.errors

Recovery is offline and creates a separate directory. Finalized .pg files alone are sufficient, including the recovery pages; their original directory structure is unnecessary. Recovery can reconstruct damaged metadata from a surviving copy when the reconstructed page matches its expected hash. Missing/corrupt measurement payloads require another copy or backup. The report identifies incomplete recovery, repaired pages, and unconfirmed orphan pages that were excluded from live data. Use db.checkpoint(name) to create a fresh recovery checkpoint. Obsolete files are currently retained; automatic reclamation is not implemented.

Filesystems and current scope

A plain path selects filesystem storage. Mount detection selects local flock or directory locks for NFS/generic mounts; db.backend_info exposes the decision. file:///path?profile=generic is an optional explicit override. Lock coordination is persisted so clients cannot silently use incompatible protocols. Directory locks are never stolen based on age; after a crashed writer, establish that it is dead and reconcile the committed state before removing its lock directory.

This first implementation is tested on a local filesystem, including multiple processes and the directory-lock protocol. NFS still needs qualification on the deployment servers. Writable EOS, native root://, and s3:// adapters are not implemented and fail explicitly. Instances are thread-confined. Use mode="r" for read-only access or mode="x" to require a new database.

Run python -m pytest -q for the test suite. Run PYTHONPATH=. python examples/benchmark.py --workers 4 --records 1048576 for a synthetic fresh-signal/direct-write comparison on the chosen filesystem. It does not measure NXCALS service latency or establish a network time-to-first-data guarantee.

Legacy API

from pagestore.legacy import PageStore, Page, Data, DataSet

The old SQLite/pickle implementation and existing file format remain available only through this namespace. Its original README and examples are retained. Open only trusted legacy files; the new DB neither opens nor modifies that format. Copy legacy measurements explicitly into a separate new database when needed.

Metadata

Release files for pagestore 0.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pagestore 0.0.0
File Size Uploaded
pagestore-0.0.0.tar.gz 83.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pagestore 0.0.0
File Interpreter ABI Platform
pagestore-0.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 126.7 kB

Release files / pagestore-0.0.0.tar.gz

Download URL pagestore-0.0.0.tar.gz
Size 83.1 kB
Tags Source
SHA-256 checksum
How to use checksums
4e288705b8628df3688594a6d7e9debb9096d05e7ab31376143362b3b310172e
BLAKE2b-256 checksum
How to use checksums
faa3a4ed4811adbfce6956ba81217d320a22107324a01936b065095debf62d2e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.6

Release files / pagestore-0.0.0-py3-none-any.whl

Download URL pagestore-0.0.0-py3-none-any.whl
Size 43.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b60446299d557598f71357a6e496acfccb8c6f8e308c685e7c612b4f5bb95b20
BLAKE2b-256 checksum
How to use checksums
8c770dad86877f5c6a15e967be04709bed078746cc0423e7b329e826ea697656
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.6

Release history Release notifications | RSS feed

This release

0.0.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page