Skip to main content

SQLite-backed local caching helpers for time series data.

Project description

Liteseries

An SQLite-backed replicator and cache for timeseries data.

Liteseries works as an invisible layer around a timeseries endpoint. Your function still returns the rows you would normally get from the vendor, while liteseries stores them in SQLite for the next matching call. When a request reaches past the saved range, it reads the local slice first and asks the vendor only for the missing forward period. Liteseries is stable for single-threaded use, with parallel reads available where the Arrow backend supports them.

from liteseries import close_ls, launch_ls, ls_cache, threadpool_shutdown_ls
  • launch_ls() opens the local SQLite/ADBC runtime for the current thread.
  • ls_cache(...) decorates endpoint functions that return pyarrow.Table objects.
  • close_ls() closes the runtime connection when your process is done with it.
  • threadpool_shutdown_ls(...) for use in a multithreaded environment.

Usage

Install the package, then call launch_ls() once before cached functions run:

from __future__ import annotations

from datetime import UTC, datetime, time, timedelta

import pyarrow as pa

from liteseries import close_ls, launch_ls, ls_cache


def unix_micros(year: int, month: int, day: int) -> int:
    """Return a UTC timestamp in the microsecond units liteseries stores."""
    return int(datetime(year, month, day, tzinfo=UTC).timestamp() * 1_000_000)


launch_ls()

By default, launch_ls() looks for an existing .sqlite file near the entry script, prefers one with liteseries in its name, and otherwise creates liteseries_db.sqlite. Pass a path when you want to choose the file yourself:

launch_ls("cache/prices.sqlite")

You can also set LITESERIES_DB before launch when configuration belongs in the environment:

$env:LITESERIES_DB = "D:\market-cache\prices.sqlite"

Wrap An Endpoint

Your endpoint function should accept keyword arguments for the requested time range and key values, then return a pyarrow.Table. The time column is expected to use Unix microseconds, and None means an open-ended side of the range.

This example uses a local data source so the shape is easy to see. A real function can call an HTTP API, a vendor SDK, or a file reader.

def vendor_prices(start, end, symbol, interval) -> pa.Table:
    """Fetch rows from the source system and return only endpoint columns."""
    rows = [
        (unix_micros(2026, 1, 2), 101.25, 102.10),
        (unix_micros(2026, 1, 3), 102.00, 103.40),
        (unix_micros(2026, 1, 4), 103.25, 103.05),
    ]

    if start is not None:
        rows = [row for row in rows if row[0] >= start]
    if end is not None:
        rows = [row for row in rows if row[0] <= end]

    return pa.table(
        {
            "ts": [row[0] for row in rows],
            "open": [row[1] for row in rows],
            "close": [row[2] for row in rows],
        }
    )

Decorate it with ls_cache. columns is the full SQLite schema, including key columns that may come from function arguments instead of the returned Arrow table. column_keys identify separate series inside one table, while table_keys can split families such as intervals into separate SQLite tables.

@ls_cache(
    columns=("symbol", "ts", "open", "close"),
    time_keys=("start", "end"),
    time_col="ts",
    column_keys=("symbol",),
    out_cols=("ts", "open", "close"),
    table="prices",
    refresh_period=timedelta(days=1),
    active_in=time(0, tzinfo=UTC),
)
def prices(start, end, symbol):
    """Return cached prices, refreshing from the endpoint when needed."""
    return vendor_prices(start, end, symbol, interval="1d")

Now call the wrapped function with keyword arguments:

try:
    first = prices(
        start=unix_micros(2026, 1, 2),
        end=unix_micros(2026, 1, 4),
        symbol="MSFT",
    )

    second = prices(
        start=unix_micros(2026, 1, 2),
        end=unix_micros(2026, 1, 4),
        symbol="MSFT",
    )

    assert second.equals(first)
finally:
    close_ls()

On the first call, liteseries creates the data table and its metadata table, fills missing key columns, writes the Arrow rows, and returns the requested slice. On later calls, it serves rows from SQLite unless the request reaches past the cached freshness boundary.

Refresh Windows

refresh_period and active_in tell liteseries when it is worth checking the endpoint again.

For daily or slower data, pass one time that marks when the latest period is expected to be complete:

@ls_cache(
    columns=("symbol", "ts", "open", "close"),
    time_keys=("start", "end"),
    time_col="ts",
    column_keys=("symbol",),
    table="daily_prices",
    refresh_period=timedelta(days=1),
    active_in=time(16, 1, tzinfo=UTC),
)
def daily_prices(start, end, symbol):
    """Cache daily bars after the market close boundary has passed."""
    return vendor_prices(start, end, symbol, interval="1d")

For intraday data, pass a (start_time, end_time) window. Liteseries advances the freshness boundary by refresh_period steps inside that window:

@ls_cache(
    columns=("symbol", "ts", "open", "close"),
    time_keys=("start", "end"),
    time_col="ts",
    column_keys=("symbol",),
    table_keys=("interval",),
    out_cols=("ts", "open", "close"),
    table="intraday_prices",
    refresh_period=timedelta(minutes=5),
    active_in=(time(9, 30, tzinfo=UTC), time(16, 0, tzinfo=UTC)),
)
def intraday_prices(start, end, symbol, interval):
    """Cache one interval per SQLite table, keyed by symbol within each table."""
    return vendor_prices(start, end, symbol, interval=interval)

Columns And Names

Use plain Python-style column and table names when you can. That is the default fast path. If your endpoint already produces names with spaces or quotes, set LITESERIES_PROTECTNAMES=true before importing liteseries so SQL identifiers are double-quoted:

$env:LITESERIES_PROTECTNAMES = "true"

out_cols controls the columns returned by the decorated function. This is handy when the database needs key columns such as symbol, but callers only want the time-series values.

Empty Tails

Some providers stop returning rows after a symbol expires or a contract ends. Pass expires_after when liteseries should stop asking forward for that key after repeated empty tail checks:

@ls_cache(
    columns=("symbol", "ts", "open", "close"),
    time_keys=("start", "end"),
    time_col="ts",
    column_keys=("symbol",),
    table="expired_prices",
    refresh_period=timedelta(hours=1),
    active_in=(time(0, tzinfo=UTC), time(23, 59, tzinfo=UTC)),
    expires_after=timedelta(days=7),
)
def expired_prices(start, end, symbol):
    """Cache sparse series without querying forever beyond the final row."""
    return vendor_prices(start, end, symbol, interval="1h")

For a live provider example with pandas/yfinance conversion, see demo.py.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

liteseries-1.0.0.tar.gz (1.3 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

liteseries-1.0.0-py3-none-any.whl (16.1 kB view details)

Uploaded Python 3

File details

Details for the file liteseries-1.0.0.tar.gz.

File metadata

  • Download URL: liteseries-1.0.0.tar.gz
  • Upload date:
  • Size: 1.3 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: Hatch/1.17.0 {"ci":null,"cpu":"AMD64","implementation":{"name":"CPython","version":"3.13.9"},"installer":{"name":"hatch","version":"1.17.0"},"openssl_version":"OpenSSL 3.5.4 30 Sep 2025","python":"3.13.9","system":{"name":"Windows","release":"11"}} HTTPX2/2.3.0

File hashes

Hashes for liteseries-1.0.0.tar.gz
Algorithm Hash digest
SHA256 b83011576a8475a8d29e4712d1dcc6d1447068f741734f18373e7797cdc24ef6
MD5 c537b79f1a51da74ba9a0fcedb1bcf43
BLAKE2b-256 17511ca5ccac91323cf54376315a6bd12a7d6a6a46c6428ebded3ccb545610c5

See more details on using hashes here.

File details

Details for the file liteseries-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: liteseries-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 16.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: Hatch/1.17.0 {"ci":null,"cpu":"AMD64","implementation":{"name":"CPython","version":"3.13.9"},"installer":{"name":"hatch","version":"1.17.0"},"openssl_version":"OpenSSL 3.5.4 30 Sep 2025","python":"3.13.9","system":{"name":"Windows","release":"11"}} HTTPX2/2.3.0

File hashes

Hashes for liteseries-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 55dfe70a01fd89cad217e74eff0c5050f724b5c8a009e297387aeb0199f39639
MD5 d3521b2cb69ebe97097f3b2bbca1ed2f
BLAKE2b-256 f9a56dce1ff5a07be3a8e9f01c5ea39a66c42ee5fa3dbc85217b0a750b8756ea

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page