pipolars.core.types - PIPolars 0.1.0 documentation

Skip to content
Back to top

# Source code for pipolars.core.types

    """Type definitions for PIPolars library.

    This module contains all the type definitions, enums, and data classes used
    throughout the library for type safety and better IDE support.
    """

    from __future__ import annotations

    from dataclasses import dataclass, field
    from datetime import datetime
    from enum import Enum, IntEnum
    from typing import Any, TypeAlias, Union

    import polars as pl

    # Type aliases for common types
    PITimestamp: TypeAlias = Union[datetime, str, "AFTime"]
    TagName: TypeAlias = str
    TagPath: TypeAlias = str

    class RetrievalMode(str, Enum):
        """Data retrieval modes for PI Point queries.

        These modes determine how data is retrieved from the PI Data Archive.
        """

        RECORDED = "recorded"
        """Return actual recorded values as stored in the archive."""

        INTERPOLATED = "interpolated"
        """Return interpolated values at regular intervals."""

        PLOT = "plot"
        """Return values optimized for plotting (reduced data density)."""

        SUMMARY = "summary"
        """Return summary statistics (min, max, avg, etc.)."""

        COMPRESSED = "compressed"
        """Return compressed data using exception/compression settings."""

    class SummaryType(IntEnum):
        """Summary calculation types for PI data.

        These correspond to OSIsoft AF SDK AFSummaryTypes enumeration.
        """

        NONE = 0
        TOTAL = 1
        AVERAGE = 2
        MINIMUM = 4
        MAXIMUM = 8
        RANGE = 16
        STD_DEV = 32
        POP_STD_DEV = 64
        COUNT = 128
        PERCENT_GOOD = 8192
        TOTAL_WITH_UOM = 16384
        ALL = 24831
        ALL_FOR_NON_NUMERIC = 8320

    class TimestampMode(str, Enum):
        """Timestamp handling modes for summary calculations."""

        AUTO = "auto"
        """Automatically determine timestamp placement."""

        START = "start"
        """Use interval start time."""

        END = "end"
        """Use interval end time."""

        MIDDLE = "middle"
        """Use interval midpoint."""

    class DataQuality(IntEnum):
        """PI data quality flags.

        These flags indicate the quality and reliability of PI values.
        """

        GOOD = 0
        """Value is good and reliable."""

        SUBSTITUTED = 1
        """Value was manually substituted."""

        QUESTIONABLE = 2
        """Value quality is questionable."""

        BAD = 3
        """Value is bad or unreliable."""

        NO_DATA = 4
        """No data available for the requested time."""

        CALC_FAILED = 5
        """Calculation failed to produce a value."""

    class DigitalState(str, Enum):
        """Common PI digital states."""

        NO_DATA = "No Data"
        BAD_INPUT = "Bad Input"
        CALC_OFF = "Calc Off"
        COMM_FAIL = "Comm Fail"
        CONFIGURE = "Configure"
        I_O_TIMEOUT = "I/O Timeout"
        NO_SAMPLE = "No Sample"
        SHUTDOWN = "Shutdown"
        SCAN_OFF = "Scan Off"
        OVER_RANGE = "Over Range"
        UNDER_RANGE = "Under Range"

    class PointType(str, Enum):
        """PI Point data types."""

        FLOAT16 = "float16"
        FLOAT32 = "float32"
        FLOAT64 = "float64"
        INT16 = "int16"
        INT32 = "int32"
        DIGITAL = "digital"
        TIMESTAMP = "timestamp"
        STRING = "string"
        BLOB = "blob"

    class BoundaryType(str, Enum):
        """Boundary handling for time range queries."""

        INSIDE = "inside"
        """Only include values strictly inside the time range."""

        OUTSIDE = "outside"
        """Include boundary values outside the range."""

        INTERPOLATED = "interpolated"
        """Interpolate values at boundaries."""

    @dataclass(frozen=True, slots=True)
    class AFTime:
        """Represents a PI AF Time specification.

        Supports both absolute timestamps and relative time expressions
        like "*" (now), "*-1d" (1 day ago), "t" (today), etc.

        Examples:
            >>> AFTime("*")  # Now
            >>> AFTime("*-1h")  # 1 hour ago
            >>> AFTime("2024-01-01")  # Absolute date
            >>> AFTime("t")  # Today at midnight
            >>> AFTime("y")  # Yesterday at midnight
        """

        expression: str
        """The time expression string."""

        def __str__(self) -> str:
            return self.expression

        @classmethod
        def now(cls) -> AFTime:
            """Create an AFTime representing the current time."""
            return cls("*")

        @classmethod
        def today(cls) -> AFTime:
            """Create an AFTime representing today at midnight."""
            return cls("t")

        @classmethod
        def yesterday(cls) -> AFTime:
            """Create an AFTime representing yesterday at midnight."""
            return cls("y")

        @classmethod
        def ago(cls, **kwargs: int) -> AFTime:
            """Create an AFTime relative to now.

            Args:
                **kwargs: Time units (days, hours, minutes, seconds)

            Returns:
                AFTime representing the relative time.

            Example:
                >>> AFTime.ago(days=1, hours=2)  # 1 day and 2 hours ago
            """
            parts = []
            if days := kwargs.get("days"):
                parts.append(f"{days}d")
            if hours := kwargs.get("hours"):
                parts.append(f"{hours}h")
            if minutes := kwargs.get("minutes"):
                parts.append(f"{minutes}m")
            if seconds := kwargs.get("seconds"):
                parts.append(f"{seconds}s")

            offset = "".join(parts) if parts else "0s"
            return cls(f"*-{offset}")

        @classmethod
        def from_datetime(cls, dt: datetime) -> AFTime:
            """Create an AFTime from a Python datetime object."""
            return cls(dt.isoformat())

    @dataclass(slots=True)
    class PIValue:
        """Represents a single PI value with timestamp and quality.

        This is the fundamental data unit returned from PI queries.
        """

        timestamp: datetime
        """The timestamp of the value."""

        value: Any
        """The actual value (can be numeric, string, or digital state)."""

        quality: DataQuality = DataQuality.GOOD
        """The quality flag for this value."""

        is_good: bool = field(init=False)
        """Convenience property indicating if the value is good quality."""

        def __post_init__(self) -> None:
            self.is_good = self.quality == DataQuality.GOOD

        def to_dict(self) -> dict[str, Any]:
            """Convert to a dictionary for DataFrame construction."""
            return {
                "timestamp": self.timestamp,
                "value": self.value,
                "quality": self.quality.value,
            }

    @dataclass(frozen=True, slots=True)
    class TimeRange:
        """Represents a time range for PI queries.

        Attributes:
            start: Start time of the range
            end: End time of the range
        """

        start: PITimestamp
        end: PITimestamp

        @classmethod
        def last(cls, **kwargs: int) -> TimeRange:
            """Create a time range from now to the past.

            Example:
                >>> TimeRange.last(days=7)  # Last 7 days
                >>> TimeRange.last(hours=24)  # Last 24 hours
            """
            return cls(
                start=AFTime.ago(**kwargs),
                end=AFTime.now(),
            )

        @classmethod
        def today(cls) -> TimeRange:
            """Create a time range for today."""
            return cls(start=AFTime.today(), end=AFTime.now())

    @dataclass(frozen=True, slots=True)
    class PointConfig:
        """Configuration for a PI Point (tag).

        Contains metadata about a PI Point retrieved from the server.
        """

        name: str
        """The PI Point name (tag name)."""

        point_id: int
        """The unique point ID in the PI Data Archive."""

        point_type: PointType
        """The data type of the point."""

        description: str = ""
        """Description of the point."""

        engineering_units: str = ""
        """Engineering units for the point."""

        zero: float = 0.0
        """Zero value for scaling."""

        span: float = 100.0
        """Span value for scaling."""

        display_digits: int = -5
        """Number of display digits."""

        typical_value: float | None = None
        """Typical value for this point."""

    @dataclass(frozen=True, slots=True)
    class SummaryResult:
        """Result of a summary calculation.

        Contains the calculated summary values for a time range.
        """

        tag: str
        """The PI Point name."""

        start: datetime
        """Start time of the summary period."""

        end: datetime
        """End time of the summary period."""

        average: float | None = None
        minimum: float | None = None
        maximum: float | None = None
        total: float | None = None
        count: int | None = None
        std_dev: float | None = None
        range: float | None = None
        percent_good: float | None = None

    # Polars schema definitions for PI data
    PI_VALUE_SCHEMA: dict[str, pl.DataType] = {
        "timestamp": pl.Datetime("us", "UTC"),
        "value": pl.Float64(),
        "quality": pl.Int8(),
    }

    PI_VALUE_WITH_TAG_SCHEMA: dict[str, pl.DataType] = {
        "tag": pl.Utf8(),
        "timestamp": pl.Datetime("us", "UTC"),
        "value": pl.Float64(),
        "quality": pl.Int8(),
    }

    SUMMARY_SCHEMA: dict[str, pl.DataType] = {
        "tag": pl.Utf8(),
        "start": pl.Datetime("us", "UTC"),
        "end": pl.Datetime("us", "UTC"),
        "average": pl.Float64(),
        "minimum": pl.Float64(),
        "maximum": pl.Float64(),
        "total": pl.Float64(),
        "count": pl.Int64(),
        "std_dev": pl.Float64(),
        "percent_good": pl.Float64(),
    }