pipolars.core.config - PIPolars 0.1.0 documentation

Skip to content
Back to top

# Source code for pipolars.core.config

    """Configuration management for PIPolars library.

    This module provides configuration classes using Pydantic for validation
    and supports loading from environment variables and configuration files.
    """

    from __future__ import annotations

    import sys
    from datetime import timedelta
    from enum import Enum
    from pathlib import Path
    from typing import Any

    if sys.version_info >= (3, 11):
        import tomllib
    else:
        import tomli as tomllib  # type: ignore[import-not-found]

    from pydantic import Field, SecretStr, field_validator, model_validator
    from pydantic_settings import BaseSettings, SettingsConfigDict

    class AuthMethod(str, Enum):
        """Authentication methods for PI System connection."""

        WINDOWS = "windows"
        """Use Windows integrated authentication (NTLM/Kerberos)."""

        EXPLICIT = "explicit"
        """Use explicit username/password authentication."""

    class CacheBackend(str, Enum):
        """Cache storage backends."""

        NONE = "none"
        """No caching."""

        MEMORY = "memory"
        """In-memory cache (lost on restart)."""

        SQLITE = "sqlite"
        """SQLite database cache."""

        ARROW = "arrow"
        """Apache Arrow IPC file cache."""

    class PIServerConfig(BaseSettings):
        """Configuration for PI Data Archive connection.

        Attributes:
            host: PI Server hostname or IP address
            port: PI Server port (default: 5450)
            timeout: Connection timeout in seconds
            auth_method: Authentication method to use
            username: Username for explicit authentication
            password: Password for explicit authentication
        """

        model_config = SettingsConfigDict(
            env_prefix="PI_SERVER_",
            env_file=".env",
            extra="ignore",
        )

        host: str = Field(description="PI Server hostname or IP address")
        port: int = Field(default=5450, ge=1, le=65535)
        timeout: int = Field(default=30, ge=1, le=300, description="Connection timeout in seconds")
        auth_method: AuthMethod = Field(default=AuthMethod.WINDOWS)
        username: str | None = Field(default=None, description="Username for explicit auth")
        password: SecretStr | None = Field(default=None, description="Password for explicit auth")

        @model_validator(mode="after")
        def validate_explicit_auth(self) -> PIServerConfig:
            """Validate that username/password are provided for explicit auth."""
            if self.auth_method == AuthMethod.EXPLICIT:
                if not self.username or not self.password:
                    raise ValueError(
                        "Username and password are required for explicit authentication"
                    )
            return self

    class AFServerConfig(BaseSettings):
        """Configuration for AF Server connection.

        Attributes:
            host: AF Server hostname (if different from PI Server)
            database: Default AF Database name
            timeout: Connection timeout in seconds
        """

        model_config = SettingsConfigDict(
            env_prefix="AF_SERVER_",
            env_file=".env",
            extra="ignore",
        )

        host: str | None = Field(default=None, description="AF Server hostname")
        database: str | None = Field(default=None, description="Default AF Database name")
        timeout: int = Field(default=30, ge=1, le=300)

    class CacheConfig(BaseSettings):
        """Configuration for data caching.

        Attributes:
            backend: Cache storage backend to use
            path: Path for file-based cache backends
            max_size_mb: Maximum cache size in megabytes
            ttl_hours: Time-to-live for cached data in hours
            compression: Enable compression for cached data
        """

        model_config = SettingsConfigDict(
            env_prefix="PIPOLARS_CACHE_",
            env_file=".env",
            extra="ignore",
        )

        backend: CacheBackend = Field(default=CacheBackend.NONE)
        path: Path = Field(default=Path.home() / ".pipolars" / "cache")
        max_size_mb: int = Field(default=1024, ge=0, description="Max cache size in MB")
        ttl_hours: int = Field(default=24, ge=0, description="Cache TTL in hours")
        compression: bool = Field(default=True, description="Enable compression")

        @property
        def ttl(self) -> timedelta:
            """Get TTL as a timedelta."""
            return timedelta(hours=self.ttl_hours)

    class QueryConfig(BaseSettings):
        """Configuration for PI queries.

        Attributes:
            max_points_per_query: Maximum number of points in a single query
            default_page_size: Default page size for paginated queries
            max_values_per_request: Maximum values per request
            parallel_requests: Number of parallel requests for bulk operations
            retry_attempts: Number of retry attempts for failed requests
            retry_delay: Delay between retries in seconds
        """

        model_config = SettingsConfigDict(
            env_prefix="PIPOLARS_QUERY_",
            env_file=".env",
            extra="ignore",
        )

        max_points_per_query: int = Field(default=1000, ge=1, le=10000)
        default_page_size: int = Field(default=10000, ge=100, le=1000000)
        max_values_per_request: int = Field(default=150000, ge=1000, le=1000000)
        parallel_requests: int = Field(default=4, ge=1, le=32)
        retry_attempts: int = Field(default=3, ge=0, le=10)
        retry_delay: float = Field(default=1.0, ge=0.1, le=60.0)

    class PolarsConfig(BaseSettings):
        """Configuration for Polars DataFrame output.

        Attributes:
            timestamp_column: Name of the timestamp column
            value_column: Name of the value column
            quality_column: Name of the quality column
            tag_column: Name of the tag column (for multi-tag queries)
            include_quality: Include quality column by default
            timezone: Default timezone for timestamps
        """

        model_config = SettingsConfigDict(
            env_prefix="PIPOLARS_POLARS_",
            env_file=".env",
            extra="ignore",
        )

        timestamp_column: str = Field(default="timestamp")
        value_column: str = Field(default="value")
        quality_column: str = Field(default="quality")
        tag_column: str = Field(default="tag")
        include_quality: bool = Field(default=False)
        timezone: str = Field(default="UTC")

        @field_validator("timezone")
        @classmethod
        def validate_timezone(cls, v: str) -> str:
            """Validate that the timezone is valid."""
            import zoneinfo

            try:
                zoneinfo.ZoneInfo(v)
            except KeyError as e:
                raise ValueError(f"Invalid timezone: {v}") from e
            return v

    class PIConfig(BaseSettings):
        """Main configuration class for PIPolars library.

        This class aggregates all configuration sections and provides
        a unified interface for configuration management.

        Example:
            >>> config = PIConfig(
            ...     server=PIServerConfig(host="my-pi-server"),
            ...     cache=CacheConfig(backend=CacheBackend.SQLITE),
            ... )
            >>> client = PIClient(config=config)
        """

        model_config = SettingsConfigDict(
            env_prefix="PIPOLARS_",
            env_file=".env",
            env_nested_delimiter="__",
            extra="ignore",
        )

        server: PIServerConfig = Field(default_factory=lambda: PIServerConfig(host="localhost"))
        af: AFServerConfig = Field(default_factory=AFServerConfig)
        cache: CacheConfig = Field(default_factory=CacheConfig)
        query: QueryConfig = Field(default_factory=QueryConfig)
        polars: PolarsConfig = Field(default_factory=PolarsConfig)

        # Global settings
        debug: bool = Field(default=False, description="Enable debug mode")
        log_level: str = Field(default="INFO", description="Logging level")

        @classmethod
        def from_file(cls, path: str | Path) -> PIConfig:
            """Load configuration from a TOML or JSON file.

            Args:
                path: Path to the configuration file

            Returns:
                PIConfig instance with loaded configuration
            """
            import json

            path = Path(path)

            if path.suffix == ".toml":
                with path.open("rb") as f:
                    data = tomllib.load(f)
            elif path.suffix == ".json":
                with path.open() as f:
                    data = json.load(f)
            else:
                raise ValueError(f"Unsupported config file format: {path.suffix}")

            return cls.model_validate(data)

        def to_dict(self) -> dict[str, Any]:
            """Convert configuration to a dictionary.

            Sensitive fields like passwords are masked.
            """
            data = self.model_dump()
            # Mask sensitive fields
            if data.get("server", {}).get("password"):
                data["server"]["password"] = "***"
            return data