timeseries-cache
A persistent SQLite cache for time-series API data. It fetches only missing UTC date ranges and reuses saved rows across runs. The package itself uses only the Python standard library.
Install
Requires Python 3.10 or newer.
uv add timeseries-cache
Use the package
Declare a Schema with a timezone-aware datetime time dimension, then create a
Cache and call load(selection, start, end, fetcher). The fetcher receives
(start, end, refresh) and returns rows with the declared dimensions and a
value. load returns rows, a status (live, cache, stale, or
unavailable), and any missing ranges. Pass refresh=True to fetch a range
again. Cache files are stored in the directory passed to Cache.
Here is a runnable example using generated hourly observations. Replace the fetcher's body with your API call and return rows in the same format.
from datetime import datetime, timedelta, timezone
from timeseries_cache import Cache, Schema
schema = Schema({"timestamp": datetime, "sensor": str}, "timestamp")
cache = Cache("data/cache", schema)
selection = {"sensor": "A"}
def fetch(start, end, refresh):
timestamp = start
while timestamp < end:
yield {"timestamp": timestamp, **selection, "value": 12.5}
timestamp += timedelta(hours=1)
start = datetime(2024, 1, 1, tzinfo=timezone.utc)
end = start + timedelta(days=1)
first = cache.load(selection, start, end, fetch)
second = cache.load(selection, start, end, fetch)
print(first.status, second.status, len(second.rows))
# With an empty cache: live cache 24
The file data/cache/timeseries_cache.sqlite persists across Python sessions.
Requesting a larger interval fetches only the uncovered parts. read() returns
stored rows without fetching; missing() returns uncovered intervals.
| Result status | Meaning |
|---|---|
live |
The requested fetches succeeded. |
cache |
No fetch was needed. |
stale |
Refreshing failed or was unavailable, but stored coverage exists. |
unavailable |
Some interval remains uncovered. |
Always inspect result.error and result.missing_ranges when a load is not
successful. A failed fetch can still return previously cached rows.
Public API
Import Cache, Schema, and CacheResult from timeseries_cache.
| Call | Purpose |
|---|---|
Schema(dimensions, time_dimension, value_type=float, partition_dimension=None) |
Define row fields and their types. |
Cache(directory, schema) |
Create or reopen directory/timeseries_cache.sqlite. |
cache.load(selection, start, end, fetcher=None, *, options=None, refresh=False, max_age=None) |
Fetch missing ranges and return a CacheResult. |
cache.read(selection, start, end, *, options=None) |
Read saved rows, sorted by time. |
cache.missing(selection, start, end, *, options=None, max_age=None) |
List uncovered or expired intervals as (start, end) datetime pairs. |
cache.replace(selection, start, end, rows, *, options=None) |
Validate and atomically replace one interval. |
cache.fetched_ranges(selection, start, end, *, options=None) |
Inspect overlapping coverage records: start, end, fetched_at, and outcome. |
CacheResult contains rows, status, missing_ranges, profile_id,
fetched_ranges, and error. Rows are ordinary dictionaries. Modifying returned
rows does not change stored data. Coverage record dates are ISO 8601 strings;
row dates and missing-range bounds are UTC datetime objects.
Schema and row types
Dimensions must include the time field and at least one other field. Their names
must be identifiers made from ASCII letters, digits, and underscores, starting
with a letter or underscore. value is reserved for the observation.
| Declared type | Accepted input |
|---|---|
str, int, bool |
That exact Python type; booleans do not count as integers. |
float |
A finite integer or float, stored as a float; booleans are rejected. |
datetime |
A timezone-aware datetime, normalized to UTC; dimensions only. |
dict, list |
JSON-serializable containers with string dictionary keys. |
The value field also accepts None, regardless of value_type. Convert API
timestamp strings to aware datetimes before returning rows. Each timestamp must
be unique within a selection; represent multiple metrics as a metric dimension
and load each metric separately. Nested containers follow JSON semantics, so
tuples inside them become lists on reading.
Cache contract
- One
selectionfixes every non-time dimension. For multiple series, callloadfor each selection. - A fetcher must return the complete response for
[start, end), including all pages/chunks, or raise. An empty iterable means the interval was checked successfully. Missing observations alone cannot reveal a failed download. - Every row contains exactly the schema dimensions and
value. Timestamps must be timezone-aware. Missing values useNone; NaN and infinity are rejected. The start boundary is included; the end boundary is excluded. - Include inputs that affect results in
options, such as provider URL, aggregation or model version. Use separate directories for separate accounts. The cache cannot identify which API a callback uses. JSON dictionaries must have string keys at every nesting level. refresh=Truereloads the whole interval.max_age=timedelta(hours=1)reloads only missing or expired intervals. Either option passesTrueto the fetcher when fetching, allowing it to bypass upstream caches.- If fetching cannot proceed, covered data returns
stale; uncovered intervals returnunavailable.missing_rangesdescribes absent coverage, independent of age. Successful zero-row responses still count as coverage.
Errors and freshness
Invalid schemas, selections, periods, and load arguments raise ValueError.
replace raises for invalid rows or failed writes; a failed replacement leaves
the previous interval intact. load captures exceptions from the fetcher, row
validation, and replacement in result.error, stops fetching further gaps, and
returns the available data. Earlier successful gaps remain saved. Database
failures while opening or reading still raise. KeyboardInterrupt and
SystemExit propagate.
Use result.error is not None to check for failure: an exception can have an
empty message. Coverage means a range was fetched successfully, not that every
possible timestamp has an observation. missing_ranges therefore cannot detect
missing samples inside a successful response.
The age of coverage starts after the response has been fully consumed.
max_age must be a nonnegative timedelta; zero requests a refetch of existing
coverage. Expiry affects reuse without deleting rows. A failed refresh can still
return those rows with status stale.
Storage and concurrency
By default, SQLite stores observations in cache_rows and fetched intervals in
cache_ranges. Setting Schema.partition_dimension creates separate pairs of
tables for each selected partition value. Each record is scoped by schema and
profile identifiers derived from the schema, selection, and options. Existing
0.1.x database files can be reopened without migration.
Connections are opened and closed per operation; there is no close() call to
manage. SQLite uses WAL mode and a 30-second lock timeout. Each replacement
commits rows and coverage together. Readers and writers can use the same local
database, but concurrent loads may fetch the same gap more than once. For
overlapping replacements, the last committed write wins. A complete load is
several operations, not a single database snapshot.
Use a local filesystem for the database. Responses are validated in memory before writing, and results are returned as a list, so split very large requests into smaller intervals. Files persist until you remove them; expiry does not limit disk usage.
Development
uv sync --locked --extra test
uv run --no-sync python -m pytest -q
uv run --no-sync ruff check .
uv run --no-sync ruff format --check .
uv run --no-sync mypy
Tests use temporary databases and mocked fetchers. No credentials or live API requests are needed. The CI workflow checks installed packages on Python 3.10–3.15 and on Linux, macOS, and Windows, using uv's GitHub Actions integration.
Build the wheel and source archive with:
uv build --no-sources
The version is defined in src/timeseries_cache/__init__.py; Hatch reads it when
building. Record release changes in CHANGELOG.md. The wheel
includes type information and the MIT license; tests ship in the source archive.
License
Released under the MIT License.
The full license text is included in the LICENSE file.
Metadata
Release files for timeseries-cache 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| timeseries_cache-0.1.2.tar.gz | 17.0 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| timeseries_cache-0.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 30.8 kB
Release files / timeseries_cache-0.1.2.tar.gz
| Download URL | timeseries_cache-0.1.2.tar.gz |
|---|---|
| Size | 17.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2b7945342ff16fc47ca671f45f2fe1f33d81dba0ad15dbafa7fae40eb2f9c373
|
|
BLAKE2b-256 checksum How to use checksums |
3a2cf8c79d96c9ddc4fdcc67b22b6c21305caeef3dd43744fc6fbe601a484803
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.2 {"installer":{"name":"uv","version":"0.12.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Pop!_OS","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / timeseries_cache-0.1.2-py3-none-any.whl
| Download URL | timeseries_cache-0.1.2-py3-none-any.whl |
|---|---|
| Size | 13.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
cecdd65bad9b0d4dbd0000d863a06575d76bf858399c0f0837307fc913bf230b
|
|
BLAKE2b-256 checksum How to use checksums |
a6c41196765b6b7e1a0ad53435c465c6643f1e5b7d2bca9f0ded78820a55e0a6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.2 {"installer":{"name":"uv","version":"0.12.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Pop!_OS","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for timeseries-cache 0.1.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.