config.py#

"""
Centralized application configuration via pydantic-settings.

This module defines the single source of truth for runtime configuration.
Values are loaded from process environment variables and an optional ``.env``
file, validated with Pydantic, and exposed through a module-level
``Settings`` instance. This consolidation avoids ad-hoc ``os.getenv`` calls
across the codebase and improves type safety and discoverability.

See Also
--------
app.database : Engine/session factories that consume ``settings.DATABASE_URL``.
app.utils : Logging helpers and general utilities that may use config values.

Notes
-----
- Primary role: provide a validated ``Settings`` model and a ready-to-use
  ``settings`` instance consumed throughout the application.
- Key dependencies: ``pydantic_settings.BaseSettings``, process environment,
  and an optional ``.env`` file at the project root.
- Invariants: ``DATABASE_URL`` must be present; extra environment variables
  are ignored to minimize coupling; sensible defaults exist for non-critical
  options.

Examples
--------
>>> from app.config import settings
>>> isinstance(settings.DATABASE_URL, str)
True
>>> # Override via environment or instantiate directly for tests
>>> from app.config import Settings
True
"""

import logging
from pathlib import Path
from typing import Optional

from pydantic import Extra, Field
from pydantic_settings import BaseSettings

# Constants
ENV_FILE: Path = Path(".env")
ENV_FILE_ENCODING: str = "utf-8"
POLLING_INTERVAL_MINUTES_DEFAULT: int = 10
DATA_INGEST_INTERVAL_MINUTES_DEFAULT: int = 1
USER_AGENT_DEFAULT: str = "SkolDataApp/1.0 (kontakt@skoldata.se)"
DOCUMENTATION_URL_DEFAULT: str = "http://localhost:8001"

logger = logging.getLogger(__name__)


class Settings(BaseSettings):
    """Application configuration loaded from environment variables.

    The settings object is the single source of truth for runtime configuration
    across the application. It is based on ``pydantic-settings`` to provide
    type checking, default values, and ``.env`` support.

    Parameters
    ----------
    DATABASE_URL : str
        Connection string for the database. This field is required.
    polling_interval_minutes : int, optional
        Interval for polling jobs, in minutes. Defaults to ``10``.
    data_ingest_interval_minutes : int, optional
        Interval for data ingestion, in minutes. Defaults to ``1``.
    user_agent : str, optional
        User-Agent header for outbound HTTP requests. Defaults to a project
        specific value.
    my_lat : float, optional
        Optional latitude used for a central point in weather queries.
    my_long : float, optional
        Optional longitude used for a central point in weather queries.

    Attributes
    ----------
    DATABASE_URL : str
        Connection string for the database. Required (no default).
    polling_interval_minutes : int
        Interval for polling jobs, in minutes.
    data_ingest_interval_minutes : int
        Interval for data ingestion, in minutes.
    user_agent : str
        User-Agent header for outbound HTTP requests.
    my_lat : float | None
        Optional latitude used for a central point in weather queries.
    my_long : float | None
        Optional longitude used for a central point in weather queries.

    Raises
    ------
    pydantic.ValidationError
        If required values are missing or invalid when the model is
        instantiated (including at import time for the module-level
        ``settings`` instance).

    Examples
    --------
    >>> from app.config import Settings
    >>> s = Settings(DATABASE_URL="sqlite:///:memory:")
    >>> isinstance(s.polling_interval_minutes, int)
    True

    See Also
    --------
    - app.database: Consumers of ``Settings`` values for engine/session setup.

    Notes
    -----
    - Extra environment variables are ignored (``extra=ignore``).
    - Values are read at import time when ``settings = Settings()`` is executed.
    """

    DATABASE_URL: str = Field(..., description="Connection string for the database")
    polling_interval_minutes: int = POLLING_INTERVAL_MINUTES_DEFAULT
    data_ingest_interval_minutes: int = DATA_INGEST_INTERVAL_MINUTES_DEFAULT
    user_agent: str = USER_AGENT_DEFAULT
    my_lat: Optional[float] = None
    my_long: Optional[float] = None
    documentation_url: str = Field(
        DOCUMENTATION_URL_DEFAULT,
        description="URL where the project's rendered documentation is hosted (Sphinx)",
    )

    class Config:
        """Pydantic configuration for environment file settings.

        Notes
        -----
        - ``env_file`` points to the project ``.env`` to simplify local runs.
        - Encoding is set explicitly to avoid platform-specific defaults.
        - Extra variables are ignored to prevent accidental coupling.
        """

        env_file = ENV_FILE
        env_file_encoding = ENV_FILE_ENCODING
        extra = Extra.ignore


# Instantiate Settings, loading from environment variables or the `.env` file.
settings = Settings()

__all__ = ["settings", "Settings"]