API reference¶
Everything most code needs is importable from pyrosome itself, e.g.
from pyrosome import ParquetDal.
Data access¶
- class pyrosome.Dal(*args, **kwargs)[source]¶
Bases:
ProtocolVersioned, append-only storage for data frames, whatever the file format.
Example
>>> from pathlib import Path >>> import polars as pl >>> from pyrosome import CsvDal, Dal, ParquetDal >>> def save(dal: Dal) -> str: ... return dal.write(pl.DataFrame({"x": [1]}), "example").name >>> save(ParquetDal(Path("parquet"))), save(CsvDal(Path("csv"))) ('v0', 'v0')
- read(*keys)[source]¶
Read data from the latest version of the data described by keys.
- Parameters:
*keys (str) – Path components naming the profile.
- Returns:
A lazy scan over every data file in the latest version.
- Raises:
FileNotFoundError – If there is no data for keys.
- Return type:
LazyFrame
- class pyrosome.ParquetDal(data_directory)[source]¶
Bases:
_FileDalData Access Layer storing versioned data as parquet files.
Each profile, named by one or more keys, is a directory under
data_directory. Everywritecreates a new version andappendadds files to the latest version, so existing data is never modified in place.Example
>>> from pathlib import Path >>> import polars as pl >>> from pyrosome import ParquetDal >>> dal = ParquetDal(Path("data")) >>> dal.write(pl.DataFrame({"x": [1, 2]}), "example").name 'v0' >>> dal.append(pl.DataFrame({"x": [3]}), "example").name '1.parquet' >>> dal.read("example").collect()["x"].to_list() [1, 2, 3]
Create a data access layer. Nothing is read or written until used.
- Parameters:
data_directory (Path) – The directory where data files are stored.
- Returns:
None.
- append(data, *keys)¶
Add a new file to the latest data directory without touching existing files.
Columns are reordered and cast to match the existing data. If there is no existing data this warns and writes instead.
- Parameters:
data (DataFrame | LazyFrame) – The data to append. Its columns must match the existing data.
*keys (str) – Path components naming the profile.
- Returns:
The path of the new file, or the new versioned directory if there was no existing data.
- Raises:
ValueError – If the columns do not match the existing data, or for any reason
writeraises it.polars.exceptions.InvalidOperationError – If a column cannot be cast to the existing type.
FileNotFoundError – If the latest version directory is missing.
TypeError – If the profile’s manifest file is corrupt.
RuntimeError – If the latest data spans more than one directory.
polars.exceptions.PolarsError – If the data cannot be computed or written.
OSError – If the data directory cannot be written to.
- Return type:
- get_profile_dir(*keys)¶
Get path to the profile directory described by keys.
- Parameters:
*keys (str) – Path components naming the profile.
- Returns:
The profile directory.
- Raises:
ValueError – If there are no keys or they lead outside the data directory.
- Return type:
- read(*keys)¶
Read data from the latest version of the data described by keys.
- Parameters:
*keys (str) – Path components naming the profile.
- Returns:
A lazy scan over every data file in the latest version.
- Raises:
FileNotFoundError – If there is no data for keys.
ValueError – If there are no keys or they lead outside the data directory.
TypeError – If the profile’s manifest file is corrupt.
polars.exceptions.PolarsError – If the data files cannot be scanned.
- Return type:
LazyFrame
- write(data, *keys)¶
Write the data as a new version.
Readers keep seeing the previous version until the write is complete.
- Parameters:
data (DataFrame | LazyFrame) – The data to write.
*keys (str) – Path components naming the profile.
- Returns:
The versioned directory the data was written to.
- Raises:
ValueError – If there are no keys, they lead outside the data directory, or the profile’s manifest names an invalid version.
TypeError – If the profile’s manifest file is corrupt.
pyrosome.simple_file_lock.FileAlreadyLockedError – If another process holds the manifest lock for longer than
pyrosome.dal.DataManifestFile.LOCK_TIMEOUT.pyrosome.simple_file_lock.FileLockPermissionError – If the manifest cannot be locked.
polars.exceptions.PolarsError – If the data cannot be computed or written.
OSError – If the profile directory cannot be created or written to.
- Return type:
- class pyrosome.CsvDal(data_directory)[source]¶
Bases:
_FileDalSame as a ParquetDal but uses csv files instead of parquet files.
Broadly recommend using the parquet version, but this exists for user convenience. Column types are inferred when the files are read back.
Example
>>> from pathlib import Path >>> import polars as pl >>> from pyrosome import CsvDal >>> csv_dal = CsvDal(Path("data")) >>> output_dir = csv_dal.write(pl.DataFrame({"x": [1, 2]}), "example_csv") >>> (output_dir / "0.csv").read_text().splitlines() ['x', '1', '2']
Create a data access layer. Nothing is read or written until used.
- Parameters:
data_directory (Path) – The directory where data files are stored.
- Returns:
None.
- append(data, *keys)¶
Add a new file to the latest data directory without touching existing files.
Columns are reordered and cast to match the existing data. If there is no existing data this warns and writes instead.
- Parameters:
data (DataFrame | LazyFrame) – The data to append. Its columns must match the existing data.
*keys (str) – Path components naming the profile.
- Returns:
The path of the new file, or the new versioned directory if there was no existing data.
- Raises:
ValueError – If the columns do not match the existing data, or for any reason
writeraises it.polars.exceptions.InvalidOperationError – If a column cannot be cast to the existing type.
FileNotFoundError – If the latest version directory is missing.
TypeError – If the profile’s manifest file is corrupt.
RuntimeError – If the latest data spans more than one directory.
polars.exceptions.PolarsError – If the data cannot be computed or written.
OSError – If the data directory cannot be written to.
- Return type:
- get_profile_dir(*keys)¶
Get path to the profile directory described by keys.
- Parameters:
*keys (str) – Path components naming the profile.
- Returns:
The profile directory.
- Raises:
ValueError – If there are no keys or they lead outside the data directory.
- Return type:
- read(*keys)¶
Read data from the latest version of the data described by keys.
- Parameters:
*keys (str) – Path components naming the profile.
- Returns:
A lazy scan over every data file in the latest version.
- Raises:
FileNotFoundError – If there is no data for keys.
ValueError – If there are no keys or they lead outside the data directory.
TypeError – If the profile’s manifest file is corrupt.
polars.exceptions.PolarsError – If the data files cannot be scanned.
- Return type:
LazyFrame
- write(data, *keys)¶
Write the data as a new version.
Readers keep seeing the previous version until the write is complete.
- Parameters:
data (DataFrame | LazyFrame) – The data to write.
*keys (str) – Path components naming the profile.
- Returns:
The versioned directory the data was written to.
- Raises:
ValueError – If there are no keys, they lead outside the data directory, or the profile’s manifest names an invalid version.
TypeError – If the profile’s manifest file is corrupt.
pyrosome.simple_file_lock.FileAlreadyLockedError – If another process holds the manifest lock for longer than
pyrosome.dal.DataManifestFile.LOCK_TIMEOUT.pyrosome.simple_file_lock.FileLockPermissionError – If the manifest cannot be locked.
polars.exceptions.PolarsError – If the data cannot be computed or written.
OSError – If the profile directory cannot be created or written to.
- Return type:
- pyrosome.get_accessor(file_ending: Literal['parquet']) type[ParquetDal][source]¶
- pyrosome.get_accessor(file_ending: Literal['csv']) type[CsvDal]
Get the accessor class that stores files with the given file ending.
- Parameters:
file_ending (str) – The file format to store, without a leading dot.
- Returns:
ParquetDalfor"parquet"orCsvDalfor"csv".- Raises:
ValueError – If file_ending is not a supported format.
- Return type:
type[ParquetDal | CsvDal]
Filtering reads¶
- class pyrosome.SimpleIo(data_directory)[source]¶
Bases:
objectParquet storage whose
readcan also filter rows by column values.Example
>>> from pathlib import Path >>> import polars as pl >>> from pyrosome import SimpleIo >>> io = SimpleIo(Path("data")) >>> df = pl.DataFrame({"x": [1, 2, 2], "y": ["a", "b", None]}) >>> io.write(df, "example").name 'v0' >>> io.read("example", x=2, y="b").collect().rows() [(2, 'b')] >>> io.read("example", y=None).collect().rows() [(2, None)]
writeandappendalso take pandas data frames:>>> import pandas as pd >>> io.append(pd.DataFrame({"x": [3], "y": ["d"]}), "example").name '1.parquet' >>> io.read("example", x=3).collect().rows() [(3, 'd')]
Create the inner dal. Nothing is read or written until used.
- Parameters:
data_directory (Path | __annotationlib_name_1__ | os.PathLike[__annotationlib_name_2__]) – The directory where data files are stored.
- Returns:
None.
- dal: ParquetDal¶
The data access layer reads and writes go through.
- available_profiles(*keys)[source]¶
List the profiles directly below keys that have been written.
A profile is a directory holding a manifest. Nested profiles, and directories that only group other profiles, are not listed; pass their path as keys to list what they hold.
Example
>>> from pathlib import Path >>> import polars as pl >>> from pyrosome import SimpleIo >>> io = SimpleIo(Path("data")) >>> df = pl.DataFrame({"x": [1]}) >>> _ = io.write(df, "shop", "orders") >>> _ = io.write(df, "shop", "totals") >>> io.available_profiles("shop") ('orders', 'totals') >>> io.available_profiles("missing") ()
- Parameters:
*keys (str) – Path components naming the directory to look in. With no keys, the data directory itself.
- Returns:
The names of the profiles below keys, sorted. Empty if the directory doesn’t exist.
- Raises:
ValueError – If keys lead outside the data directory.
OSError – If the directory cannot be listed.
- Return type:
- read(*keys, **filters)[source]¶
Read a lazy frame, keeping only rows where each column equals a value.
Filters are equality only. Pass a column whose name isn’t a valid keyword argument by unpacking a dict, e.g.
io.read("sales", **{"unit price": 3}).- Parameters:
*keys (str) – Path components naming the profile.
**filters (FilterValue) – Column names and the value each column must equal. None matches nulls.
- Returns:
A lazy scan over the latest version, keeping only rows that match every filter.
- Raises:
polars.exceptions.ColumnNotFoundError – If a filter names a column the data doesn’t have.
TypeError – If a filter value is a float or another unsupported type, or the profile’s manifest file is corrupt.
FileNotFoundError – If there is no data for keys.
ValueError – If there are no keys or they lead outside the data directory.
polars.exceptions.PolarsError – If the data files cannot be scanned.
- Return type:
LazyFrame
- write(data, *keys)[source]¶
Write the data as a new version.
A pandas data frame’s named index levels are stored as columns; an unnamed index is dropped.
- Parameters:
data (DataFrame | LazyFrame | pd.DataFrame) – The data to write, as a polars or pandas frame.
*keys (str) – Path components naming the profile.
- Returns:
The versioned directory the data was written to.
- Raises:
ImportError – If data is a pandas frame needing pyarrow to convert, and pyarrow is not installed.
ValueError – If there are no keys, they lead outside the data directory, or the profile’s manifest names an invalid version.
TypeError – If the profile’s manifest file is corrupt.
pyrosome.simple_file_lock.FileAlreadyLockedError – If another process holds the manifest lock for longer than
pyrosome.dal.DataManifestFile.LOCK_TIMEOUT.pyrosome.simple_file_lock.FileLockPermissionError – If the manifest cannot be locked.
polars.exceptions.PolarsError – If the data cannot be computed or written.
OSError – If the profile directory cannot be created or written to.
- Return type:
Path
- append(data, *keys)[source]¶
Add a new file to the latest version without touching existing files.
A pandas data frame’s named index levels are stored as columns; an unnamed index is dropped.
- Parameters:
data (DataFrame | LazyFrame | pd.DataFrame) – The data to append, as a polars or pandas frame. Its columns must match the existing data.
*keys (str) – Path components naming the profile.
- Returns:
The path of the new file, or the new versioned directory if there was no existing data.
- Raises:
ImportError – If data is a pandas frame needing pyarrow to convert, and pyarrow is not installed.
ValueError – If the columns do not match the existing data, or for any reason
writeraises it.polars.exceptions.InvalidOperationError – If a column cannot be cast to the existing type.
FileNotFoundError – If the latest version directory is missing.
TypeError – If the profile’s manifest file is corrupt.
RuntimeError – If the latest data spans more than one directory.
polars.exceptions.PolarsError – If the data cannot be computed or written.
OSError – If the data directory cannot be written to.
- Return type:
Path
- pyrosome.api.FilterValue = FilterValue¶
A value
pyrosome.SimpleIo.readcan filter a column by.datetime.datetimeis adatetime.date, so both are accepted. Floats are not: equality on floats is unreliable, since0.1 + 0.2 != 0.3.
Datasets¶
- class pyrosome.Dataset(dal, *keys, **filters)[source]¶
Bases:
objectRepresents a dataset.
A dataset is defined by a set of keys, which point to specific folders in the data directory. This usually will be a system that contains profiles, but a user could add more nesting if they so desired.
Every profile read through a dataset is filtered by the dataset’s filters, so every profile is expected to have the filtered columns.
Subclasses can derive profiles from other profiles: a method named
_calc_<profile>(keys joined by underscores) returns the profile’s data, andget_cache_profilewrites it when the profile has no data yet. The method is called on the datasetwithout_filters, so it computes the profile for every row and the cache is shared by all subsets.Example
>>> from pathlib import Path >>> import polars as pl >>> from pyrosome import Dataset, SimpleIo >>> class Shop(Dataset): ... def _calc_totals(self) -> pl.LazyFrame: ... orders = self.get_profile("orders") ... return orders.group_by("region").agg(pl.col("amount").sum()) >>> io = SimpleIo(Path("data")) >>> orders = pl.DataFrame({"region": ["eu", "us", "eu"], "amount": [1, 2, 3]}) >>> io.write(orders, "shop", "orders").name 'v0' >>> eu = Shop(io, "shop", region="eu") >>> eu.get_profile("orders").collect()["amount"].to_list() [1, 3] >>> eu.get_cache_profile("totals").collect().rows() [('eu', 4)] >>> io.read("shop", "totals").collect().height 2
Set up the dataset. Nothing is read until a profile is requested.
Subclasses should keep this signature:
get_subsetandwithout_filterscall it to make datasets of the same class.- Parameters:
dal (SimpleIo) – The storage the dataset’s profiles are read from.
*keys (str) – Path components naming the dataset’s directory.
**filters (FilterValue) – Column names and the value each column must equal in every profile read through this dataset. None matches nulls.
- Returns:
None.
- filters: Mapping[str, FilterValue]¶
Row-wise filters to apply when loading the data.
- get_profile(*keys)[source]¶
Get a profile defined by keys, filtered by the dataset’s filters.
- Parameters:
*keys (str) – Path components naming the profile, below the dataset’s keys.
- Returns:
A lazy scan over the profile’s latest version, keeping only rows that match every filter.
- Raises:
polars.exceptions.ColumnNotFoundError – If a filter names a column the profile doesn’t have.
TypeError – If a filter value is a float or another unsupported type, or the profile’s manifest file is corrupt.
FileNotFoundError – If the profile has no data.
ValueError – If there are no keys at all, or they lead outside the data directory.
polars.exceptions.PolarsError – If the data files cannot be scanned.
- Return type:
pl.LazyFrame
- get_cache_profile(*keys, refresh=False)[source]¶
Get a profile, or calculate and cache it if not found.
The profile is calculated by the
_calc_<profile>method on the datasetwithout_filters, and written as a new version, so the cached profile holds every row. The dataset’s filters are applied when it is read.- Parameters:
- Returns:
A lazy scan over the profile’s latest version, keeping only rows that match every filter.
- Raises:
AttributeError – If the profile has to be calculated and there is no
_calc_<profile>method.polars.exceptions.ColumnNotFoundError – If a filter names a column the profile doesn’t have.
TypeError – If a filter value is a float or another unsupported type, or the profile’s manifest file is corrupt.
FileNotFoundError – If a profile the calculation reads has no data.
ValueError – If there are no keys at all, or they lead outside the data directory.
ImportError – If the calculation returns a pandas frame needing pyarrow to convert, and pyarrow is not installed.
pyrosome.simple_file_lock.FileAlreadyLockedError – If another process holds the manifest lock for longer than
pyrosome.dal.DataManifestFile.LOCK_TIMEOUT.pyrosome.simple_file_lock.FileLockPermissionError – If the manifest cannot be locked.
polars.exceptions.PolarsError – If the data cannot be computed, written or scanned.
OSError – If the profile directory cannot be created or written to.
Exception – Anything the
_calc_<profile>method raises.
- Return type:
pl.LazyFrame
- without_filters()[source]¶
Get this dataset without any filters.
- Returns:
A dataset of the same class, with the same keys and no filters.
- Return type:
- get_subset(**new_filters)[source]¶
Apply additional filters to further reduce this dataset.
- Parameters:
**new_filters (FilterValue) – Column names and the value each column must equal, on top of this dataset’s filters.
- Returns:
A dataset of the same class, with the same keys and the combined filters.
- Raises:
ValueError – If this dataset already filters one of the columns.
- Return type:
Internals¶
These live in submodules and are rarely needed directly.
Primary API for writing/reading/appending.
- class pyrosome.dal.DataManifestFile(profile_path)[source]¶
Bases:
objectA manifest file that records the latest version of a profile.
Create a handle on the manifest in profile_path. Nothing is read yet.
- Parameters:
profile_path (Path) – Path to the profile directory.
- Returns:
None.
- MANIFEST_FILE_NAME: Final[str] = 'manifest.json'¶
Name of the manifest file inside the profile directory.
- LOCK_TIMEOUT: Final[float] = 10.0¶
Seconds to wait for another process to finish writing the manifest.
- exists()[source]¶
Does the manifest file even exist yet?
- Returns:
Whether the manifest file exists.
- Return type:
- get_latest_data_dir()[source]¶
Get the latest data directory described by the manifest file.
- Returns:
The latest data directory.
- Raises:
FileNotFoundError – If the manifest file or the latest data directory does not exist.
TypeError – If the manifest file is corrupt.
- Return type:
- reserve_new_data_dir()[source]¶
Create and return the next version of the data directory.
The manifest is not updated; call
commit_data_dironce the data is written.
- commit_data_dir(data_dir)[source]¶
Point the manifest at data_dir, unless it already points at a newer version.
- Parameters:
data_dir (Path) – The finished data directory.
- Returns:
None.
- Raises:
ValueError – If data_dir or the version in the manifest has an invalid name.
TypeError – If the manifest file is corrupt.
pyrosome.simple_file_lock.FileAlreadyLockedError – If another process holds the manifest lock for longer than
LOCK_TIMEOUT.pyrosome.simple_file_lock.FileLockPermissionError – If the manifest cannot be locked.
OSError – If the manifest file cannot be written.
- Return type:
None
Create a simple lock on a file.
Nothing can read or write to a file while it is locked. This prevents multiple processes from reading or writing to the same file at the same time, which could cause data corruption.
- exception pyrosome.simple_file_lock.LockError[source]¶
Bases:
ExceptionAn error that occurs during file locking.
- exception pyrosome.simple_file_lock.FileAlreadyLockedError[source]¶
Bases:
LockErrorAn error that occurs when a file is already locked and cannot be locked again.
- exception pyrosome.simple_file_lock.FileLockPermissionError[source]¶
Bases:
LockErrorAn error that occurs when a file cannot be locked due to permission issues.
- exception pyrosome.simple_file_lock.FileLockedError[source]¶
Bases:
LockErrorAn error that occurs when a file is locked and cannot be accessed.
- class pyrosome.simple_file_lock.SimpleWriteLock(path, timeout=0.0, poll_interval=0.05)[source]¶
Bases:
objectSimple lock that prevents other processes accessing a file being written to.
The lock is a
<file>.write_lockfile next to the locked file, so it works across processes but is only advisory: code that ignores the lock is not blocked.Example
>>> from pathlib import Path >>> from pyrosome.simple_file_lock import SimpleWriteLock, file_is_locked >>> target = Path("notes.txt") >>> with SimpleWriteLock(target, timeout=1.0): ... file_is_locked(target) True >>> file_is_locked(target) False
Create a lock for path. The lock is only taken when entering the context.
- Parameters:
- Returns:
None.
- Raises:
ValueError – If path has an empty name, such as
/.
- timeout¶
Seconds to keep retrying if another process holds the lock.
- pyrosome.simple_file_lock.file_is_locked(path)[source]¶
Check if a file is locked.
- Parameters:
path (Path) – The file to check.
- Returns:
Whether a lock is currently held on path.
- Raises:
ValueError – If path has an empty name, such as
/.- Return type: