Source code for r2x_core.plugin_config

"""Base configuration class for plugins.

This module provides the foundational configuration class that plugin implementations
should inherit from to define model-specific parameters. This applies to parsers,
exporters, and system modifiers.

Classes
-------
PluginConfig
    Base configuration class with support for defaults loading.

Examples
--------
Create a model-specific configuration:

>>> from r2x_core.plugin_config import PluginConfig
>>> from pydantic import field_validator
>>>
>>> class ReEDSConfig(PluginConfig):
...     model_year: int
...     weather_year: int
...     scenario: str = "base"
...
...     @field_validator("model_year")
...     @classmethod
...     def validate_year(cls, v):
...         if v < 2020 or v > 2050:
...             raise ValueError("Year must be between 2020 and 2050")
...         return v
>>>
>>> config = ReEDSConfig(
...     model_year=2030,
...     weather_year=2012,
...     scenario="high_re"
... )

Load constants from JSON:

>>> constants = ReEDSConfig.load_defaults()
>>> # Use constants in your parser/exporter logic

See Also
--------
r2x_core.parser.BaseParser : Uses this configuration class
r2x_core.exporter.BaseExporter : Uses this configuration class
"""

from pathlib import Path
from typing import Any, ClassVar

from loguru import logger
from pydantic import BaseModel


[docs] class PluginConfig(BaseModel): """Base configuration class for plugin inputs and model parameters. Applications should inherit from this class to define model-specific configuration parameters for parsers, exporters, and system modifiers. Subclasses define their own fields for model-specific parameters. Examples -------- Create a model-specific configuration: >>> class ReEDSConfig(PluginConfig): ... '''Configuration for ReEDS parser.''' ... model_year: int ... weather_year: int ... scenario: str = "base" ... >>> config = ReEDSConfig( ... model_year=2030, ... weather_year=2012, ... scenario="high_re" ... ) With validation: >>> from pydantic import field_validator >>> >>> class ValidatedConfig(PluginConfig): ... model_year: int ... ... @field_validator("model_year") ... @classmethod ... def validate_year(cls, v): ... if v < 2020 or v > 2050: ... raise ValueError("Year must be between 2020 and 2050") ... return v See Also -------- r2x_core.parser.BaseParser : Uses this configuration class r2x_core.exporter.BaseExporter : Uses this configuration class pydantic.BaseModel : Parent class providing validation Notes ----- The PluginConfig uses Pydantic for: - Automatic type checking and validation - JSON serialization/deserialization - Field validation and transformation - Default value management Subclasses can add: - Model-specific years (solve_year, weather_year, horizon_year, etc.) - Scenario identifiers - Feature flags - File path overrides - Custom validation logic """ CONFIG_DIR: ClassVar[str] = "config" FILE_MAPPING_NAME: ClassVar[str] = "file_mapping.json" DEFAULTS_FILE_NAME: ClassVar[str] = "defaults.json"
[docs] @classmethod def get_file_mapping_path(cls) -> Path: """Get the path to this plugin's file mapping JSON. This method uses inspect.getfile() to locate the plugin module file, then constructs the path to the file mapping JSON in the config directory. By convention, plugins should store their file_mapping.json in a config/ subdirectory next to the config module. The filename can be customized by overriding the FILE_MAPPING_NAME class variable. Returns ------- Path Absolute path to the file_mapping.json file. Note that this path may not exist if the plugin hasn't created the file yet. Examples -------- Get file mapping path for a config: >>> from r2x_reeds.config import ReEDSConfig >>> mapping_path = ReEDSConfig.get_file_mapping_path() >>> print(mapping_path) /path/to/r2x_reeds/config/file_mapping.json Override the filename in a custom config: >>> class CustomConfig(PluginConfig): ... FILE_MAPPING_NAME = "custom_mapping.json" ... >>> path = CustomConfig.get_file_mapping_path() >>> print(path.name) custom_mapping.json Use with DataStore: >>> from r2x_core import DataStore >>> mapping_path = MyModelConfig.get_file_mapping_path() >>> store = DataStore.from_json(mapping_path, folder="/data/mymodel") See Also -------- load_defaults : Similar pattern for loading constants DataStore.from_plugin_config : Direct DataStore creation from config Notes ----- This method uses inspect.getfile() to locate the module file, then navigates to the config directory. This works for both installed packages and editable installs. """ import inspect # Get the file where the config class is defined module_file = inspect.getfile(cls) module_path = Path(module_file).parent return module_path / cls.CONFIG_DIR / cls.FILE_MAPPING_NAME
[docs] @classmethod def load_defaults(cls, defaults_file: Path | str | None = None) -> dict[str, Any]: """Load default constants from JSON file. Provides a standardized way to load model-specific constants, mappings, and default values from JSON files. If no file path is provided, automatically looks for the file specified by DEFAULTS_FILE_NAME in the config directory. Parameters ---------- defaults_file : Path, str, or None, optional Path to defaults JSON file. If None, looks for the file specified by DEFAULTS_FILE_NAME (default: 'defaults.json') in the CONFIG_DIR subdirectory relative to the config module. Returns ------- dict[str, Any] Dictionary of default constants to use in your parser/exporter logic. Returns empty dict if file doesn't exist. Examples -------- Load defaults automatically: >>> from r2x_reeds.config import ReEDSConfig >>> defaults = ReEDSConfig.load_defaults() >>> config = ReEDSConfig( ... solve_years=2030, ... weather_years=2012, ... ) >>> # Use defaults dict in your parser/exporter logic >>> excluded_techs = defaults.get("excluded_techs", []) Load from custom path: >>> defaults = ReEDSConfig.load_defaults("/path/to/custom_defaults.json") See Also -------- PluginConfig : Base configuration class get_file_mapping_path : Related file discovery method """ import inspect import json if defaults_file is None: # Get the file where the config class is defined module_file = inspect.getfile(cls) module_path = Path(module_file).parent defaults_file = module_path / cls.CONFIG_DIR / cls.DEFAULTS_FILE_NAME else: defaults_file = Path(defaults_file) if not defaults_file.exists(): logger.debug("Defaults file not found: {}", defaults_file) return {} try: with open(defaults_file) as f: data: dict[str, Any] = json.load(f) return data except json.JSONDecodeError as e: logger.error("Failed to parse defaults JSON from {}: {}", defaults_file, e) return {}
[docs] @classmethod def get_cli_schema(cls) -> dict[str, Any]: """Get JSON schema for CLI argument generation. This method generates a CLI-friendly schema from the configuration class, adding metadata useful for building command-line interfaces. It's designed to help tools like r2x-cli dynamically generate argument parsers from configuration classes. Returns ------- dict[str, Any] A JSON schema dictionary enhanced with CLI metadata. Each property includes: - cli_flag: The command-line flag (e.g., "--model-year") - required: Whether the argument is required - All standard Pydantic schema fields (type, description, default, etc.) Examples -------- Generate CLI schema for a configuration class: >>> from r2x_core.plugin_config import PluginConfig >>> >>> class MyConfig(PluginConfig): ... '''My model configuration.''' ... model_year: int ... scenario: str = "base" ... >>> schema = MyConfig.get_cli_schema() >>> print(schema["properties"]["model_year"]["cli_flag"]) --model-year >>> print(schema["properties"]["model_year"]["required"]) True >>> print(schema["properties"]["scenario"]["cli_flag"]) --scenario >>> print(schema["properties"]["scenario"]["required"]) False Use in CLI generation: >>> import argparse >>> parser = argparse.ArgumentParser() >>> schema = MyConfig.get_cli_schema() >>> for field_name, field_info in schema["properties"].items(): ... flag = field_info["cli_flag"] ... required = field_info["required"] ... help_text = field_info.get("description", "") ... parser.add_argument(flag, required=required, help=help_text) See Also -------- load_defaults : Load default constants from JSON file r2x_core.parser.BaseParser.get_file_mapping_path : Get file mapping path pydantic.BaseModel.model_json_schema : Underlying schema generation Notes ----- The CLI flag naming convention converts underscores to hyphens: - model_year -> --model-year - weather_year -> --weather-year - solve_year -> --solve-year This follows common CLI conventions (e.g., argparse, click). The schema includes all Pydantic field information, so CLI tools can: - Determine field types for proper parsing - Extract descriptions for help text - Identify default values - Validate constraints """ base_schema = cls.model_json_schema() cli_schema: dict[str, Any] = { "title": base_schema.get("title", cls.__name__), "description": base_schema.get("description", ""), "properties": {}, "required": base_schema.get("required", []), } for field_name, field_info in base_schema.get("properties", {}).items(): cli_field = field_info.copy() cli_field["cli_flag"] = f"--{field_name.replace('_', '-')}" cli_field["required"] = field_name in cli_schema["required"] cli_schema["properties"][field_name] = cli_field return cli_schema