PyStore

repository·main·Indexed 20 days ago

https://github.com/ranaroussi/pystore

A high-performance datastore for Pandas time-series data (version 1.0.1) that leverages Parquet, Dask, and PyArrow. It organizes data into a hierarchy of Stores, Collections, and Items, supporting rapid querying, asynchronous I/O via async_pystore, schema evolution, and data validation. Features include streaming appends, batch operations, point-in-time snapshots for versioning, and integration with Dask distributed schedulers.

Tokens
19K
Snippets
74
Records
82
Agent score
69%

What's inside pystore

  1. How PyStore collections and namespaces work

    main

    PyStore organizes data into namespaced collections. A collection acts as a bucket for data, often categorized by source, user, or frequency (e.g., EOD for End-Of-Day or ONEMINUTE for minute bars).

    Internally, each collection maps to a directory containing partitioned Parquet files for each individual item (e.g., a specific stock symbol). This structure allows for efficient querying and partitioning of large datasets.

  2. Install PyStore via pip or conda

    main

    Install PyStore using pip or conda. If you are using pip, it is recommended to use the --upgrade and --no-cache-dir flags to ensure a clean installation.

    Important: You must have the Snappy compression library installed on your system before installing PyStore.

    # Using pip
    pip install pystore --upgrade --no-cache-dir
    
    # Using conda
    conda install -c ranaroussi pystore
  3. Basic usage: Store, Read, and Append data

    main

    PyStore follows a hierarchy: Store $\rightarrow$ Collection $\rightarrow$ Item.

    1. Set Path: Use pystore.set_path() to define where data is stored. Defaults to ~/pystore or the PYSTORE_PATH environment variable.
    2. Connect: Use pystore.store("name") to connect to a datastore.
    3. Collection: Use store.collection("name") to access a namespace (e.g., by frequency like EOD or ONEMINUTE).
    4. Write: Use collection.write("item_name", dataframe, metadata={...}) to save data.
    5. Read: Use collection.item("item_name") to retrieve an item. You can access the data as a Dask dataframe via .data, metadata via .metadata, or convert it to a Pandas DataFrame via .to_pandas().
    6. Append: Use collection.append("item_name", dataframe) to add new rows to an existing item.
    import pystore
    import yfinance as yf
    
    pystore.set_path("~/pystore")
    store = pystore.store("mydatastore")
    collection = store.collection("NASDAQ")
    
    # Load and write data
    aapl = yf.download("AAPL", multi_level_index=False)
    collection.write("AAPL", aapl[:100], metadata={"source": "yfinance"})
    
    # Read data
    item = collection.item("AAPL")
    df = item.to_pandas()
    
    # Append data
    collection.append("AAPL", aapl[100:])
  4. Install Snappy dependencies

    main

    PyStore requires the Snappy compression library. Follow the instructions for your operating system:

    Ubuntu/Debian (APT): sudo apt install libsnappy-dev

    CentOS/RHEL (RPM): sudo yum install libsnappy-devel

    macOS: Install the C library via Homebrew, then install the Python wrapper:

    brew install snappy
    # Then use conda:
    conda install python-snappy -c conda-forge
    # OR use pip:
    CPPFLAGS="-I/usr/local/include -L/usr/local/lib" pip install python-snappy

    Windows: Refer to Snappy for Windows and community guides for installation.

    # macOS example
    brew install snappy
    CPPFLAGS="-I/usr/local/include -L/usr/local/lib" pip install python-snappy
  5. Manage data items with the Collection class

    main

    The Collection class is the primary interface for managing groups of data items within a PyStore datastore. It provides methods to create, read, append, and delete items, which are essentially time-series datasets stored as Parquet files. You can interact with individual items using the .item(name) method, which returns an Item object for fine-grained control.

    from pystore import Datastore
    from pystore.collection import Collection
    
    datastore = Datastore('~/my_data')
    collection = datastore.get_collection('my_collection')
    
    # Access an item
    item = collection.item('sensor_data')
    # Perform operations on the item
    data = item.to_pandas()
  6. Configure Schema Evolution Strategies

    main

    PyStore provides several EvolutionStrategy options to control how schema changes (like adding columns or changing types) are handled when updating data. Use these strategies when initializing SchemaEvolution:

    • STRICT: No schema changes are allowed. Any change triggers a validation error.
    • ADD_ONLY: Only allows adding new columns. Removing or modifying existing columns is forbidden.
    • COMPATIBLE: Allows adding columns and performing 'compatible' type changes (e.g., widening an integer type or converting to object).
    • FLEXIBLE: Allows most changes automatically.
    from pystore.schema_evolution import SchemaEvolution, EvolutionStrategy
    
    # Example: Initialize with a compatible strategy
    evolution = SchemaEvolution(strategy=EvolutionStrategy.COMPATIBLE)
  7. How collection-level locking works

    main

    The CollectionLock (accessed via with_lock) provides a way to synchronize access to a collection across different processes or threads.

    It works by attempting to create a specific directory (.lock_<lock_name>) within the collection's path.

    • Acquisition: Uses os.makedirs(..., exist_ok=False) to atomically create the lock directory. If the directory exists, the lock is held by another process, and the requester will retry until the timeout is reached.
    • Ownership: A lock_id (UUID) is written to a lock_id file inside the lock directory. This ensures that only the process that created the lock can successfully release it.
    • Release: The process verifies its lock_id matches the one in the file before removing the directory.
  8. Use Snapshots for data versioning and recovery

    main

    Snapshots provide a point-in-time, named reference for all current items in a collection. This allows you to recover data if an accidental overwrite or corruption occurs.

    • Create: collection.create_snapshot('name') captures the current state.
    • List: collection.list_snapshots() shows existing snapshots.
    • Retrieve: Use collection.item(name, snapshot='snapshot_name') to load data from a specific snapshot instead of the current version.
    • Delete: Use collection.delete_snapshot('name') to remove a specific snapshot, or collection.delete_snapshots() to remove all.
    # Create a snapshot
    collection.create_snapshot('v1_backup')
    
    # Load an item from that snapshot
    snap_item = collection.item('AAPL', snapshot='v1_backup')
    snap_df = snap_item.to_pandas()
    
    # Restore data from snapshot
    collection.write('AAPL', snap_df, overwrite=True)
  9. How transactions and batch transactions work

    main

    PyStore provides two levels of transactional management:

    1. Transaction: Provides full atomicity and rollback capabilities. It creates backups of existing items before modifying them. If an error occurs, it restores the backups to ensure the collection remains in a consistent state.
    2. BatchTransaction: An optimization layer. It does not manage its own backups/rollbacks directly; instead, it collects all operations in memory and then executes them by wrapping them in a single standard Transaction. This is significantly more efficient for multiple append operations on the same item, as it performs a single pd.concat and a single write operation per item.

    When to use which:

    • Use transaction when you need immediate atomicity for a sequence of distinct operations.
    • Use batch_transaction when you are performing many updates (especially appends) to the same items and want to minimize filesystem and Pandas overhead.
  10. Integrate validation into a Collection class

    main

    If you have a custom Collection class, you can inject validation support using add_validation_to_collection(collection_class). This monkey-patches the class to add:

    • set_validator(validator: DataValidator): Attaches a validator to the collection instance.
    • get_validator() -> Optional[DataValidator]: Retrieves the attached validator.
    • write(item, data, ...): Validates data (if it's a DataFrame) before writing.
    • append(item, data, ...): Validates data (if it's a DataFrame) before appending.
    from pystore.validation import add_validation_to_collection, create_validator, ColumnExistsRule
    
    class MyCollection:
        def write(self, item, data, metadata={}, **kwargs): 
            pass
        def append(self, item, data, **kwargs): 
            pass
    
    # Inject validation logic
    add_validation_to_collection(MyCollection)
    
    collection = MyCollection()
    validator = create_validator()
    validator.add_rule(ColumnExistsRule(['price']))
    
    # Attach the validator
    collection.set_validator(validator)
    
    # Now, calling write() will trigger validation
    collection.write('item1', pd.DataFrame({'wrong_col': [1]})) # Raises ValidationError
    from pystore.validation import add_validation_to_collection, create_validator, ColumnExistsRule
    
    class MyCollection:
        def write(self, item, data, metadata={}, **kwargs): 
            pass
        def append(self, item, data, **kwargs): 
            pass
    
    # Inject validation logic
    add_validation_to_collection(MyCollection)
    
    collection = MyCollection()
    validator = create_validator()
    validator.add_rule(ColumnExistsRule(['price']))
    
    # Attach the validator
    collection.set_validator(validator)
    
    # Now, calling write() will trigger validation
    collection.write('item1', pd.DataFrame({'wrong_col': [1]})) # Raises ValidationError
  11. Enable schema evolution for a Collection

    main

    To integrate schema evolution into your PyStore workflow, use enable_schema_evolution(collection_class, strategy). This injects methods into your collection class to manage schema metadata for individual items.

    Once enabled, you can:

    • item.enable_schema_evolution(item_name, strategy): Initialize evolution for a specific item.
    • item.get_item_evolution(item_name): Retrieve the evolution manager for an item.
    • item.set_item_evolution(item_name, evolution_obj): Save a specific evolution configuration to an item's metadata.
    # Assuming 'MyCollection' is your PyStore collection class
    from pystore.schema_evolution import enable_schema_evolution, EvolutionStrategy
    
    enable_schema_evolution(MyCollection, strategy=EvolutionStrategy.COMPATIBLE)
    
    # Now your collection instance can use these methods
    my_collection.enable_schema_evolution('my_item_id', strategy=EvolutionStrategy.COMPATIBLE)
  12. Perform optimized batch operations with `batch_transaction`

    main

    For scenarios involving many operations on the same items, use batch_transaction(collection). This is an optimized version that aggregates multiple append operations for the same item into a single pd.concat call before executing them within a standard atomic transaction. This reduces the overhead of multiple individual write/append calls.

    Key Methods in BatchTransaction:

    • write(item, data, **kwargs): Stages a write for item.
    • append(item, data, **kwargs): Stages an append for item. Multiple calls to append for the same item will be combined.
    • delete(item): Stages a deletion for item.
    from pystore import batch_transaction
    
    with batch_transaction(collection) as batch:
        batch.write('item1', df1)
        batch.write('item2', df2)
        batch.append('item3', df3)
        batch.append('item3', df4)  # These two appends will be combined into one
    # All operations are committed together