TinyFlux

repository·master·Indexed 41 days ago

https://github.com/citrusvanilla/tinyflux

A lightweight, pure-Python time series database inspired by TinyDB, optimized for small analytics workflows and IoT data stores. It features high-performance atomic writes, efficient time-based range queries, and a schemaless design using CSV storage. TinyFlux supports data organization via measurements, tags for efficient retrieval, and numeric fields, while maintaining a small memory footprint by avoiding loading entire datasets into memory.

Tokens
18.4K
Snippets
71
Records
89
Agent score
51%

What's inside TinyFlux

  1. Overview of the TinyFlux API surface

    master

    TinyFlux provides a structured API for managing time-series data through several specialized modules. To get started with writing and querying data, refer to the getting-started guide. The API is organized into the following functional areas:

    • TinyFlux Database API: Core database management and lifecycle.
    • Point API: Handling individual data points.
    • Queries API: Executing searches and data retrieval.
    • Measurement API: Managing measurements (the logical grouping of data).
    • Index API: Managing indices for optimized querying.
    • Storages API: Managing the underlying storage backends.
    • Utils API: General utility functions.
  2. What is a Point in TinyFlux?

    master

    A Point is the fundamental unit of data in TinyFlux, analogous to a row in a relational database or a document in a document-oriented database. TinyFlux is schemaless, meaning tags and fields can be added or removed from any point dynamically.

    A Point consists of four main attributes:

    AttributePython TypeDescription
    measurementstrLike a table name. Defaults to _default if not specified.
    timedatetimeThe timestamp of the data. TinyFlux converts all times to UTC.
    tagsDict[str, str]A collection of string keys and string values used for metadata.
    fieldsDict[str, Union[float, int]]A collection of string keys and numeric values.
    from tinyflux import Point
    from datetime import datetime, timezone
    
    p1 = Point(
        measurement="air_quality",
        time=datetime.now(timezone.utc),
        tags={"city": "Los Angeles", "parameter": "PM2.5"},
        fields={"aqi": 112.0}
    )
  3. Understand TinyFlux memory management and data handling

    master

    TinyFlux is designed to maintain a small memory footprint by avoiding loading entire datasets into memory.

    • Indexing: It uses an internal index that is typically ~80% smaller than the full dataset, providing high query performance without the memory cost of an in-memory database.
    • Updates and Removals: When performing updates or removals, TinyFlux evaluates items one at a time. It writes to temporary storage and then replaces the original storage with the updated version. It does not rewrite data in time-ascending order to avoid expensive memory or disk operations.
    • Time-based Searches: Queries are optimized by searching a sorted container of timestamps held in memory, which allows for fast retrieval of candidate Points without exhaustive scans.
  4. Understand the difference between Tags and Fields

    master

    TinyFlux separates data into tags and fields to optimize performance and enforce type consistency:

    • Tags (dict[str, str]): Use these for attributes with low cardinality (values that don't change frequently, like city or country). TinyFlux uses tags to index data efficiently.
    • Fields (dict[str, int|float]): Use these for attributes with high cardinality (values that vary significantly, like temperature or pressure). Fields are not indexed.

    Note: TinyFlux does not support complex types (like nested dictionaries or lists) as values. For document-based storage with complex types, use TinyDB instead.

  5. Use Field Sets for numeric data

    master

    A field set (or "fields") is a collection of field key and field value pairs belonging to a Point.

    • Schema: TinyFlux is schemaless; a Point can have zero or more fields.
    • Types: Field keys are strings, while field values are numeric (float or int in Python).
    • Indexing: Unlike tags, field keys and values do not map to Points in a TinyFlux index. This is because the number of distinct field values is typically too high for efficient indexing.
    • On Disk: Field sets occupy side-by-side columns. By default, field keys are prefixed with _field_.
    # Example of adding a field set to a Point
    Point(
        fields={
            "num_restaurants": 12,
            "num_boba_shops": 3,
        }
    )
  6. What are measurements in TinyFlux?

    master

    A measurement in TinyFlux is an abstraction analogous to a "table" in a traditional Relational Database Management System (RDBMS). It allows you to partition data within the same database file. When you access TinyFlux through a measurement, you use the same API as the main database, but all operations are automatically filtered to that specific measurement.

    By default, TinyFlux uses a measurement named _default if no specific measurement is targeted.

  7. Handle timezones in TinyFlux

    master

    TinyFlux uses Python datetime objects for timestamps. Internally, all timestamps are stored as ISO formatted strings in UTC.

    Best Practice

    To avoid data corruption or unexpected shifts, follow these rules:

    1. Always use timezone-aware datetime objects set to the UTC timezone for the time attribute of a Point.
    2. Store local timezone information as a tag if you need to reconstruct the original local time after retrieval. TinyFlux does not store the original timezone automatically.

    Common Pitfalls

    • Naive Datetimes: If you provide a timezone-naive datetime, TinyFlux assumes it is in the local timezone of the machine running the code and converts it to UTC. This can lead to incorrect data if the machine's timezone doesn't match your data's intended timezone.
    • Non-UTC Aware Datetimes: If you provide a timezone-aware datetime that is not UTC, TinyFlux will cast it to UTC for storage, and the original timezone information will be lost.
    • Non-datetime types: Providing anything other than a datetime object to the time parameter will raise a ValueError.
    from datetime import datetime, timezone
    from zoneinfo import ZoneInfo
    from tinyflux import TinyFlux, Point
    
    # 1. Create a UTC timestamp
    t = datetime.now(timezone.utc)
    
    # 2. Store the point with UTC time and the local timezone as a tag
    p = Point(
        time=t,
        tags={"room": "bedroom", "timezone": "America/Los_Angeles"},
        fields={"temp": 72.0}
    )
    
    # 3. After retrieval, cast back to local time using the tag
    la_timezone = ZoneInfo(p.tags["timezone"])
    local_time = p.time.astimezone(la_timezone)
  8. Maintain index validity by inserting in time-order

    master

    TinyFlux builds an index upon initialization. If you insert a Point that is out of chronological order (i.e., its timestamp is earlier than existing points), the index becomes invalid (db.index.valid will return False).

    If auto-index is set to True (the default), TinyFlux will attempt to rebuild the index during the next read operation, which can cause significant latency. To avoid this, always attempt to insert Points into TinyFlux in time-order.

    from tinyflux import TinyFlux, Point
    from datetime import datetime, timedelta, timezone
    
    db = TinyFlux("my_db.csv")
    t = datetime.now(timezone.utc)
    
    # Valid insertion (in order)
    db.insert(Point(time=t))
    print(db.index.valid)  # True
    
    # Invalid insertion (out of order)
    db.insert(Point(time=t - timedelta(hours=1)))
    print(db.index.valid)  # False
  9. Understand TinyFlux write performance and indexing behavior

    master

    TinyFlux is optimized for high-speed, write-heavy workloads. It uses a default CSV store designed for nearly instantaneous appends, regardless of the file size.

    Important Indexing Note: TinyFlux maintains an index for performance, but this index is invalidated if an insert occurs with a timestamp that precedes the most recent insert (out-of-order data). TinyFlux will not automatically rebuild the index during a write operation if invalidation occurs.

  10. Configure Tag Sets for efficient retrieval

    master

    A tag set (or "tags") is a collection of tag key and tag value pairs belonging to a Point.

    • Schema: TinyFlux is schemaless; a Point can have zero or more tags.
    • Types: Both tag keys and tag values are strings.
    • Indexing: Tag keys and values are mapped to Points using a hashmap in a TinyFlux index, which allows for highly efficient retrieval.
    • Best Practice: In a well-designed database, the number of distinct tag values should be significantly lower than the number of distinct field values.
    • On Disk: Tag sets occupy side-by-side columns (one for the key, one for the value). By default, tag keys are prefixed with _tag_.
    # Example of adding a tag set to a Point
    Point(
        tags={
            "city": "LA",
            "neighborhood": "Chinatown",
            "food": "good",
        }
    )
  11. How to update tags and fields using Callables

    master

    When updating tags or fields, you can pass a Callable to perform complex logic. The callable receives the current dictionary of tags or fields and must return the updated dictionary.

    Note on Deletion: Using a Callable to update tags or fields will not remove keys from the dictionary, even if you omit them from the returned dictionary. This follows the behavior of the Python dict.update() method. To actually remove keys, you must use the unset_tags or unset_fields arguments.

    >>> # Update a specific tag value within the tag set
    >>> Measurement = MeasurementQuery()
    >>> def california_updater(tags):
    ...     if "state" in tags and tags["state"] == "CA":
    ...         return {**tags, "state": "California"}
    ...     else:
    ...         return tags
    >>> db.update(Measurement == "US Metros", tags=california_updater)
  12. Understand TinyFlux Timestamps

    master

    A timestamp is the time associated with a Point.

    • In Python: It is represented as a datetime object.
    • Storage: When inserted into a TinyFlux database, it is automatically cast to a timezone-aware object in the UTC timezone.
    • On Disk: It is serialized as an ISO 8601 formatted string and occupies the first column of the default CSV storage class.
    # Example of a Point with a timestamp
    from tinyflux import Point
    from datetime import datetime, timezone
    
    p = Point(time=datetime.now(timezone.utc), measurement="test")