Quix Streams

repository·main·Indexed 23 days ago

https://github.com/quixio/quix-streams

An open-source Python framework for real-time data engineering, operational analytics, and machine learning on Apache Kafka data streams. It provides a pure Python Streaming DataFrame API for building scalable data pipelines and event-driven microservices, featuring a Source API for data ingestion and a Sink API for writing to external destinations.

Tokens
178.4K
Snippets
429
Records
808
Agent score
82%

What's inside quix-streams

  1. Overview of Quix Streams

    main

    Quix Streams is a cloud-native Python library designed for processing data in Kafka. It provides a lightweight interface that combines Kafka's scalability and resiliency with a Pythonic, Pandas-like API.

    Key capabilities include:

    • No heavy infrastructure: Runs without a JVM, external orchestrator, or server-side engine.
    • Python Ecosystem Integration: Works seamlessly with pandas, scikit-learn, TensorFlow, and PyTorch.
    • Stateful Processing: Supports stateful operations using RocksDB and aggregations over tumbling and hopping time windows.
    • Serialization: Supports multiple formats including JSON and Quix-specific serialization.
    • Reliability: Provides "at-least-once" Kafka processing guarantees.
    • Deployment: Designed for container orchestration (e.g., Kubernetes) but also supports local development and Jupyter Notebooks.
  2. What is a StreamingDataFrame?

    main

    The StreamingDataFrame is the primary object used for building ETL (Extract, Transform, Load) pipelines in Quix Streams. It allows you to define data processing pipelines declaratively, which are then executed at runtime on incoming Kafka message values.

    Key Characteristics:

    • Declarative Pipeline: Operations are defined upfront and executed as data flows through.
    • Pandas-like Interface: Provides functions and an interface similar to Pandas DataFrames and Series.
    • Stateful Processing: Built-in support for managing state during stream processing.
    • Columnar Data: While it supports simple types like int and str, it is optimized for "columnar" data structures such as dictionaries or JSON objects.

    Typically, you create a StreamingDataFrame from a quixstreams.app.Application instance using sdf = app.dataframe().

    sdf = StreamingDataFrame()
    sdf = sdf.apply(a_func)
    sdf = sdf.filter(another_func)
    sdf = sdf.to_topic(topic_obj)
  3. What is a StreamingSeries?

    main

    A StreamingSeries represents a single column of values within a StreamingDataFrame. They are typically generated when you access a column using sdf["column_name"] or as a result of operations performed on a StreamingDataFrame.

    StreamingSeries allow you to perform column-level operations such as:

    • Basic Arithmetic: add, subtract, modulo, etc.
    • Comparisons: greater than, equals, is/not, etc.
    • Existence Checks: checking for the presence of columns or specific values.
    • Chaining: combining multiple operations together in a single pipeline.

    In most cases, you will interact with StreamingSeries implicitly through StreamingDataFrame operations, similar to how one uses Pandas.

    # Example of implicit StreamingSeries usage
    sdf = StreamingDataFrame()
    sdf["new_sum_field"] = sdf["column_c"] + sdf["column_d"] + 2
  4. What is branching in Quix Streams?

    main

    Branching occurs when a StreamingDataFrame (SDF) diverges from a single point into two or more independent processing points. This allows you to create a directed acyclic graph (DAG) of operations rather than a single linear chain.

    Key Use Cases

    • Multiple Topic Output: Handle different data structures or schemas by transforming the same source data in different ways before producing to different topics.
    • Conditional Operations: Natively support logic where different operations are applied to different subsets of data (e.g., using .filter() to create a branch that only processes specific records).
    • Consolidating Applications: Combine multiple related processing applications into a single application to reduce overhead and manage overlapping transformations in one place.
  5. Understand the FileSink base class

    main

    The FileSink is the base class for all community file-based sinks. It inherits from BatchingSink and implements the logic for grouping messages by topic and partition.

    Each batch is serialized using the configured format before being written to the destination. The specific destination (local, Azure, S3) is determined by the subclass implementation.

    Core Methods:

    • setup(): An abstract method used to authenticate and validate the connection.
    • write(batch: SinkBatch): Writes a batch of data using the configured format.
  6. How the InfluxDB v3 Source works

    main

    The InfluxDB3Source extracts data from specified InfluxDB v3 measurements and dumps them to a Kafka topic.

    Processing Logic:

    • It processes measurements sequentially.
    • It uses a tumbling window approach based on the time_delta parameter.
    • It starts querying from start_date and moves towards end_date.
    • Once a measurement is complete, it moves to the next one.

    Important Behavior:

    • If end_date is not provided, the source will run indefinitely for a single measurement and will never process other measurements.
  7. Reproduce Topic-Partition hierarchy with LocalFileSource

    main

    If your files were produced by a Quix Streams *FileSink, they likely follow a topic-partition structure (e.g., topic_name/partition_id/file.ext). To reproduce messages to their exact original partitions, you must:

    1. Subclass LocalFileSource and implement the file_partition_counter() -> int method to return the number of partition folders.
    2. Set has_partition_folders=True in the constructor.
    3. Use a key_setter that extracts the original Kafka key.

    This ensures the resulting Kafka topic has the correct partition count and that messages are distributed according to their original partitioning.

    from quixstreams.sources.community.file.local import LocalFileSource
    
    class MyLocalFileSource(LocalFileSource):
        def file_partition_counter(self) -> int:
            # Returns the number of partition folders (e.g., '0', '1')
            return len([f for f in self._filepath.iterdir()])
    
    def my_key_setter(record: dict) -> str:
        return record["original_key_field"]
    
    source = MyLocalFileSource(
        filepath='my_topic/',
        has_partition_folders=True,
        key_setter=my_key_setter,
        # ... other required args
    )
  8. Requirements for stateful operations on concatenated dataframes

    main

    If you intend to perform stateful operations (such as windowed aggregations) on a StreamingDataFrame created via .concat(), you must satisfy two critical requirements:

    1. Identical Partition Counts: The underlying source topics must have the same number of partitions. If they do not, the application will raise an error.
    2. Consistent Partitioning Algorithm: The message keys must be distributed using the same partitioning algorithm across all source topics. If they differ, the same keys might be routed to different state stores, leading to incorrect aggregation results.
  9. Optimize performance when branching

    main

    Branching requires cloning the current value at the branching node $N-1$ times (where $N$ is the number of branches). This cloning is performed using pickle, which can be a performance bottleneck or fail if data is not serializable.

    To minimize performance loss:

    1. Reduce value size: Use column projection to keep only necessary columns before branching. Smaller values result in lower clone costs.
    2. Filter upfront: Filter values before creating branches to reduce the total volume of data that needs to be cloned.
  10. Handle stream timeouts with QuixTSDataLakeSink

    main

    You can detect periods of inactivity for specific Kafka message keys using the stream_timeout_ms and on_stream_timeout parameters in QuixTSDataLakeSink.

    To enable this feature, you must provide both parameters. The sink uses a StreamTimeoutTracker to track the last-seen timestamp per key. When a key remains silent for the specified duration, the on_stream_timeout callback is invoked once per silence period.

  11. Use persistent state with stateful processing

    main

    To use persistent state for a specific message key during processing, pass stateful=True to StreamingDataFrame.apply(), StreamingDataFrame.update(), or StreamingDataFrame.filter().

    When stateful=True is used, your custom function must accept a second argument of type State. The State object allows you to manage key-value data that is automatically scoped to the current Kafka message key.

    State API:

    • .get(key, default=None): Retrieve a value.
    • .set(key, value): Store a value.
    • .delete(key): Remove a key.
    • .exists(key): Check if a key exists.

    Keys and values must be JSON-serializable. Under the hood, keys are prefixed by the Kafka message key to ensure isolation between different message keys.

    from quixstreams import State
    
    sdf = app.dataframe(...)
    
    
    def add_max_temperature(value: dict, state: State):
        """
        Calculate max observed temperature and add it to the current value
        """
        current_max = state.get('max_temperature')
        if current_max is None:
            max_temperature = value['temperature']
        else:
            max_temperature = max(value['temperature'], current_max)
        state.set('max_temperature', max_temperature)
        value['max_temperature'] = max_temperature
    
    
    sdf = sdf.update(add_max_temperature, stateful=True)