Skip to content

scenario

scenario

Scenario model for ETM scenario management.

Classes

SavedScenarioError

Bases: Exception

Base saved scenario error

Scenario

Scenario(**data)

Bases: Base

Pydantic model for a MyETM SavedScenario.

A SavedScenario wraps an ETEngine session scenario and persists it in MyETM. The response includes both SavedScenario metadata and the full nested Scenario.

Source code in src/pyetm/models/base.py
def __init__(self, **data: Any) -> None:
    """
    Initialize the model, converting validation errors to warnings.
    """
    super(BaseModel, self).__setattr__("__pydantic_private__", {})

    # Initialize all private attributes with their defaults
    private_dict: Dict[str, Any] = self.__pydantic_private__  # type: ignore[assignment]
    for attr_name, attr_info in self.__class__.__private_attributes__.items():
        if (
            hasattr(attr_info, "default_factory")
            and attr_info.default_factory is not None
        ):
            # Call factory - signature varies between pydantic versions
            private_dict[attr_name] = attr_info.default_factory()
        elif hasattr(attr_info, "default"):
            private_dict[attr_name] = attr_info.default
        else:
            private_dict[attr_name] = None

    try:
        super().__init__(**data)
    except ValidationError as e:
        # Check if data is None or empty - this indicates API error
        if not data or data is None:
            # Re-raise with clearer message
            raise ValueError(
                f"Cannot create {self.__class__.__name__} with empty data. "
                "This usually indicates an authentication or API error."
            ) from e

        # If validation fails, create model without validation and collect warnings
        # Use model_construct to bypass validation
        temp_instance = self.__class__.model_construct(**data)

        # Copy the constructed data to this instance
        for field_name, field_value in temp_instance.__dict__.items():
            if not field_name.startswith("_"):
                object.__setattr__(self, field_name, field_value)

        # Ensure required Pydantic slot attributes exist to prevent AttributeError
        for slot in ("__pydantic_fields_set__", "__pydantic_extra__"):
            try:
                value = object.__getattribute__(temp_instance, slot)
                object.__setattr__(self, slot, value)
            except AttributeError:
                # Initialize missing slot attributes with defaults
                if slot == "__pydantic_extra__":
                    object.__setattr__(self, slot, {})
                elif slot == "__pydantic_fields_set__":
                    object.__setattr__(self, slot, set())

        # Convert validation errors to warnings
        for error in e.errors():
            field_path = ".".join(str(part) for part in error.get("loc", []))
            message = error.get("msg", "Validation failed")
            self._warning_collector.add(field_path, message, "error")
Attributes
session property
session

Get the current underlying ETEngine Scenario for this SavedScenario.

Returns:

Name Type Description
Scenario 'Session'

The current ETEngine scenario session (cached after first access)

inputs property
inputs

Get inputs from the underlying session.

sortables property
sortables

Get sortables from the underlying session.

custom_curves property
custom_curves

Get custom curves from the underlying session.

hourly_output_curves property
hourly_output_curves

Get output curves from the underlying session.

annual_exports property
annual_exports

Get annual exports from the underlying session.

couplings property
couplings

Get couplings from the underlying session.

version property
version

Get ETM version from the underlying session.

start_year property
start_year

Get start year from the underlying session.

template_id property
template_id

Get template ID from the underlying session.

keep_compatible property
keep_compatible

Get keep_compatible flag from the underlying session.

scaling property
scaling

Get scaling from the underlying session.

url property
url

Get URL from the underlying session.

Functions
new classmethod
new(title, session_id=None, area_code=None, end_year=None, client=None, user_values=None, custom_curves=None, sortables=None, private=False, **kwargs)

Create a SavedScenario in MyETM - either from an existing session or by creating a new one.

Provide EITHER session_id OR (area_code + end_year), not both.

Parameters:

Name Type Description Default
title str

Title for the saved scenario (required)

required
session_id Optional[int]

ID of existing Session to save (optional)

None
area_code Optional[str]

Region code for new session, e.g., "nl2023", "de" (optional)

None
end_year Optional[int]

End year for new session (optional)

None
client Optional[BaseClient]

Optional BaseClient instance

None
user_values Optional[Dict[str, Any]]

Optional dict of user input values to apply after creation

None
custom_curves Optional[Dict[str, Any]]

Optional dict of custom curves to upload after creation

None
sortables Optional[Dict[str, Any]]

Optional dict of sortables to apply after creation

None
private bool

Whether the scenario should be private (default: False)

False
**kwargs Any

Additional parameters (e.g., description)

{}

Returns:

Type Description
'Scenario'

SavedScenario instance

Raises:

Type Description
SavedScenarioError

If creation fails

ValueError

If parameter combination is invalid

Example
Create new scenario (creates new session + saves it)

scenario = Scenario.new( ... title="High Solar 2050", ... area_code="nl2023", ... end_year=2050 ... )

Save existing session

scenario = Scenario.new( ... title="My Scenario", ... session_id=existing_session.id ... )

Source code in src/pyetm/models/scenario.py
@classmethod
def new(
    cls,
    title: str,
    session_id: Optional[int] = None,
    area_code: Optional[str] = None,
    end_year: Optional[int] = None,
    client: Optional[BaseClient] = None,
    user_values: Optional[Dict[str, Any]] = None,
    custom_curves: Optional[Dict[str, Any]] = None,
    sortables: Optional[Dict[str, Any]] = None,
    private: bool = False,
    **kwargs: Any,
) -> "Scenario":
    """
    Create a SavedScenario in MyETM - either from an existing session or by creating a new one.

    Provide EITHER session_id OR (area_code + end_year), not both.

    Args:
        title: Title for the saved scenario (required)
        session_id: ID of existing Session to save (optional)
        area_code: Region code for new session, e.g., "nl2023", "de" (optional)
        end_year: End year for new session (optional)
        client: Optional BaseClient instance
        user_values: Optional dict of user input values to apply after creation
        custom_curves: Optional dict of custom curves to upload after creation
        sortables: Optional dict of sortables to apply after creation
        private: Whether the scenario should be private (default: False)
        **kwargs: Additional parameters (e.g., description)

    Returns:
        SavedScenario instance

    Raises:
        SavedScenarioError: If creation fails
        ValueError: If parameter combination is invalid

    Example:
        >>> # Create new scenario (creates new session + saves it)
        >>> scenario = Scenario.new(
        ...     title="High Solar 2050",
        ...     area_code="nl2023",
        ...     end_year=2050
        ... )

        >>> # Save existing session
        >>> scenario = Scenario.new(
        ...     title="My Scenario",
        ...     session_id=existing_session.id
        ... )
    """
    # Validate authentication
    from pyetm.config.settings import get_settings

    if not get_settings().etm_api_token:
        raise PermissionError(
            "Creating SavedScenarios requires authentication. "
            "Set ETM_API_TOKEN or create a Session instead. "
            "Get your token at https://energytransitionmodel.com/api_access"
        )

    # Validation
    if session_id is not None and (area_code is not None or end_year is not None):
        raise ValueError(
            "Provide either session_id OR (area_code + end_year), not both"
        )
    if session_id is None and (area_code is None or end_year is None):
        raise ValueError(
            "Must provide either session_id OR both area_code and end_year"
        )

    # Create new session if area_code and end_year are provided
    if area_code is not None and end_year is not None:
        from pyetm.models.session import Session

        session = Session.new(area_code=area_code, end_year=end_year)
        session_id = session.id

    # Create SavedScenario using the session_id
    params = {
        "scenario_id": session_id,
        "title": title,
        "private": private,
        **kwargs,
    }
    return cls._create_from_params(
        params, client, user_values, custom_curves, sortables
    )
load classmethod
load(saved_scenario_id, client=None)

Load an existing SavedScenario from MyETM by its ID.

Parameters:

Name Type Description Default
saved_scenario_id int

The ID of the saved scenario to load

required
client Optional[BaseClient]

Optional BaseClient instance

None

Returns:

Type Description
'Scenario'

SavedScenario instance

Raises:

Type Description
SavedScenarioError

If loading fails

Source code in src/pyetm/models/scenario.py
@classmethod
def load(
    cls, saved_scenario_id: int, client: Optional[BaseClient] = None
) -> "Scenario":
    """
    Load an existing SavedScenario from MyETM by its ID.

    Args:
        saved_scenario_id: The ID of the saved scenario to load
        client: Optional BaseClient instance

    Returns:
        SavedScenario instance

    Raises:
        SavedScenarioError: If loading fails
    """
    if client is None:
        client = get_client()

    # Create a simple object with id attribute for the runner
    template = type("T", (), {"id": saved_scenario_id})()
    result = FetchSavedScenarioRunner.run(client, template)

    if not result.success:
        for error in result.errors:
            if "not found" in error.lower():
                raise SavedScenarioError(
                    f"Scenario {saved_scenario_id} does not exist on this ETM environment"
                )
        raise SavedScenarioError(
            f"Could not load saved scenario {saved_scenario_id}: {result.errors}"
        )

    saved_scenario = cls.model_validate(result.data)

    # Store client for future operations
    saved_scenario._client = client

    for warning in result.errors:
        saved_scenario.add_warning("base", warning)

    # Auto-display load warnings (use getattr for safety)
    scenario_id_str = getattr(saved_scenario, "id", saved_scenario_id)
    scenario_title = getattr(saved_scenario, "title", "Unknown")
    saved_scenario.auto_show_warnings(
        f"SavedScenario #{scenario_id_str} (title='{scenario_title}')"
    )

    return saved_scenario
update
update(client=None, **kwargs)

Update this SavedScenario

Parameters:

Name Type Description Default
client Optional[BaseClient]

Optional BaseClient instance

None
**kwargs Any

Fields to update (title, private, discarded)

{}
Source code in src/pyetm/models/scenario.py
def update(self, client: Optional[BaseClient] = None, **kwargs: Any) -> None:
    """
    Update this SavedScenario

    Args:
        client: Optional BaseClient instance
        **kwargs: Fields to update (title, private, discarded)
    """
    if client is None:
        client = get_client()
    result = UpdateSavedScenarioRunner.run(client, self.id, kwargs)

    if not result.success:
        raise SavedScenarioError(
            f"Could not update saved scenario: {result.errors}"
        )

    for warning in result.errors:
        self.add_warning("update", warning)

    if result.data:
        for field, value in result.data.items():
            if hasattr(self, field):
                setattr(self, field, value)

    for field, value in kwargs.items():
        if hasattr(self, field) and (not result.data or field not in result.data):
            setattr(self, field, value)
discard
discard(client=None)

Discard this SavedScenario from MyETM (soft-delete, recoverable).

The scenario is marked as discarded and hidden from listings, but can be recovered through the MyETM web interface within 60 days. After 60 days, MyETM automatically removes discarded scenarios permanently.

This is the safe, recoverable deletion method. Use delete() for permanent deletion.

Parameters:

Name Type Description Default
client Optional[BaseClient]

Optional BaseClient instance

None

Raises:

Type Description
SavedScenarioError

If discard fails

Example

scenario = Scenario.load(123) scenario.discard() # Soft-delete, recoverable for 60 days

Source code in src/pyetm/models/scenario.py
def discard(self, client: Optional[BaseClient] = None) -> None:
    """
    Discard this SavedScenario from MyETM (soft-delete, recoverable).

    The scenario is marked as discarded and hidden from listings, but can be
    recovered through the MyETM web interface within 60 days. After 60 days,
    MyETM automatically removes discarded scenarios permanently.

    This is the safe, recoverable deletion method. Use delete() for permanent deletion.

    Args:
        client: Optional BaseClient instance

    Raises:
        SavedScenarioError: If discard fails

    Example:
        scenario = Scenario.load(123)
        scenario.discard()  # Soft-delete, recoverable for 60 days
    """
    if client is None:
        client = get_client()

    result = DiscardSavedScenarioRunner.run(client, self.id)

    if not result.success:
        raise SavedScenarioError(
            f"Could not discard saved scenario: {result.errors}"
        )
delete
delete(client=None)

Permanently delete this SavedScenario AND its underlying Session (hard delete with cascade).

WARNING: This is a PERMANENT deletion and CANNOT be undone. This will: 1. Permanently delete the SavedScenario from MyETM 2. Permanently delete the underlying Session from ETEngine

All scenario data will be irreversibly lost. For recoverable deletion, use discard() instead.

Parameters:

Name Type Description Default
client Optional[BaseClient]

Optional BaseClient instance

None

Raises:

Type Description
SavedScenarioError

If deletion fails

Example

scenario = Scenario.load(123) scenario.delete() # PERMANENT deletion - cannot be recovered

Source code in src/pyetm/models/scenario.py
def delete(self, client: Optional[BaseClient] = None) -> None:
    """
    Permanently delete this SavedScenario AND its underlying Session (hard delete with cascade).

    WARNING: This is a PERMANENT deletion and CANNOT be undone. This will:
    1. Permanently delete the SavedScenario from MyETM
    2. Permanently delete the underlying Session from ETEngine

    All scenario data will be irreversibly lost. For recoverable deletion,
    use discard() instead.

    Args:
        client: Optional BaseClient instance

    Raises:
        SavedScenarioError: If deletion fails

    Example:
        scenario = Scenario.load(123)
        scenario.delete()  # PERMANENT deletion - cannot be recovered
    """
    if client is None:
        client = get_client()

    result = DestroySavedScenarioRunner.run(
        client,
        saved_scenario_id=self.id,
        scenario_id=self.scenario_id
    )

    if not result.success:
        raise SavedScenarioError(
            f"Could not permanently delete saved scenario: {result.errors}"
        )
user_values
user_values()

Get user values from the underlying session.

Source code in src/pyetm/models/scenario.py
def user_values(self) -> Dict[str, Any]:
    """Get user values from the underlying session."""
    return self.session.user_values()
update_user_values
update_user_values(update_inputs, skip_upload=False)

Update user values on the underlying session.

Source code in src/pyetm/models/scenario.py
def update_user_values(
    self, update_inputs: Dict[str, Any], skip_upload: bool = False
) -> None:
    """Update user values on the underlying session."""
    self.session.update_user_values(update_inputs, skip_upload=skip_upload)
remove_user_values
remove_user_values(input_keys)

Remove user values on the underlying session.

Source code in src/pyetm/models/scenario.py
def remove_user_values(self, input_keys: Union[List[str], Set[str]]) -> None:
    """Remove user values on the underlying session."""
    self.session.remove_user_values(input_keys)
set_user_values_from_dataframe
set_user_values_from_dataframe(dataframe)

Set user values from dataframe on the underlying session.

Source code in src/pyetm/models/scenario.py
def set_user_values_from_dataframe(self, dataframe: pd.DataFrame) -> None:
    """Set user values from dataframe on the underlying session."""
    self.session.set_user_values_from_dataframe(dataframe)
update_sortables
update_sortables(update_sortables)

Update sortables on the underlying session.

Source code in src/pyetm/models/scenario.py
def update_sortables(self, update_sortables: Dict[str, List[Any]]) -> None:
    """Update sortables on the underlying session."""
    self.session.update_sortables(update_sortables)
remove_sortables
remove_sortables(sortable_names)

Remove sortables on the underlying session.

Source code in src/pyetm/models/scenario.py
def remove_sortables(self, sortable_names: Union[List[str], Set[str]]) -> None:
    """Remove sortables on the underlying session."""
    self.session.remove_sortables(sortable_names)
set_sortables_from_dataframe
set_sortables_from_dataframe(dataframe, skip_upload=False)

Set sortables from dataframe on the underlying session.

Source code in src/pyetm/models/scenario.py
def set_sortables_from_dataframe(
    self, dataframe: pd.DataFrame, skip_upload: bool = False
) -> None:
    """Set sortables from dataframe on the underlying session."""
    self.session.set_sortables_from_dataframe(dataframe, skip_upload=skip_upload)
update_custom_curves
update_custom_curves(custom_curves, skip_upload=False)

Update custom curves on the underlying session.

Source code in src/pyetm/models/scenario.py
def update_custom_curves(
    self, custom_curves: Any, skip_upload: bool = False
) -> None:
    """Update custom curves on the underlying session."""
    self.session.update_custom_curves(custom_curves, skip_upload=skip_upload)
remove_custom_curves
remove_custom_curves(curve_keys)

Remove custom curves from the underlying session.

Source code in src/pyetm/models/scenario.py
def remove_custom_curves(
    self, curve_keys: Union[List[str], Set[str]]
) -> None:
    """Remove custom curves from the underlying session."""
    self.session.remove_custom_curves(curve_keys)
custom_curve_series
custom_curve_series(curve_name)

Get a custom curve series from the underlying session.

Source code in src/pyetm/models/scenario.py
def custom_curve_series(self, curve_name: str) -> Optional[pd.Series[Any]]:
    """Get a custom curve series from the underlying session."""
    return self.session.custom_curve_series(curve_name)
custom_curves_series
custom_curves_series()

Yield all custom curve series from the underlying session.

Source code in src/pyetm/models/scenario.py
def custom_curves_series(self) -> Any:  # Returns generator
    """Yield all custom curve series from the underlying session."""
    return self.session.custom_curves_series()
get_hourly_curve
get_hourly_curve(identifier)

Get a single hourly output curve by name or carrier type alias.

Carrier types ('electricity', 'heat', 'hydrogen', 'methane') are treated as convenient aliases for their primary curves.

Parameters:

Name Type Description Default
identifier str

Curve name (e.g., 'merit_order') or carrier type alias

required

Returns:

Type Description
Optional[DataFrame]

DataFrame with hourly data, or None if not found

Source code in src/pyetm/models/scenario.py
def get_hourly_curve(self, identifier: str) -> Optional[pd.DataFrame]:
    """
    Get a single hourly output curve by name or carrier type alias.

    Carrier types ('electricity', 'heat', 'hydrogen', 'methane') are treated
    as convenient aliases for their primary curves.

    Args:
        identifier: Curve name (e.g., 'merit_order') or carrier type alias

    Returns:
        DataFrame with hourly data, or None if not found
    """
    return self.session.get_hourly_curve(identifier)
get_hourly_curves
get_hourly_curves(identifiers)

Get multiple hourly output curves by names or carrier type aliases.

Parameters:

Name Type Description Default
identifiers list[str]

List of curve names and/or carrier type aliases

required

Returns:

Type Description
dict[str, DataFrame]

Dictionary mapping curve names to DataFrames

Source code in src/pyetm/models/scenario.py
def get_hourly_curves(
    self, identifiers: list[str]
) -> dict[str, pd.DataFrame]:
    """
    Get multiple hourly output curves by names or carrier type aliases.

    Args:
        identifiers: List of curve names and/or carrier type aliases

    Returns:
        Dictionary mapping curve names to DataFrames
    """
    return self.session.get_hourly_curves(identifiers)
all_hourly_output_curves
all_hourly_output_curves()

Yield all output curves from the underlying session.

Source code in src/pyetm/models/scenario.py
def all_hourly_output_curves(self) -> Any:  # Returns generator
    """Yield all output curves from the underlying session."""
    return self.session.all_hourly_output_curves()
clear_hourly_curves_cache
clear_hourly_curves_cache()

Clear all hourly output curves cache files and LRU cache.

Returns:

Type Description
int

Number of files successfully removed

Source code in src/pyetm/models/scenario.py
def clear_hourly_curves_cache(self) -> int:
    """Clear all hourly output curves cache files and LRU cache.

    Returns:
        Number of files successfully removed
    """
    return self.session.clear_hourly_curves_cache()
clear_custom_curves_cache
clear_custom_curves_cache()

Clear all custom curves cache files.

Returns:

Type Description
int

Number of files successfully removed

Source code in src/pyetm/models/scenario.py
def clear_custom_curves_cache(self) -> int:
    """Clear all custom curves cache files.

    Returns:
        Number of files successfully removed
    """
    return self.session.clear_custom_curves_cache()
clear_all_curve_caches
clear_all_curve_caches()

Clear all curve caches (hourly output curves and custom curves).

Returns:

Type Description
tuple[int, int]

Tuple of (hourly_curves_removed, custom_curves_removed)

Source code in src/pyetm/models/scenario.py
def clear_all_curve_caches(self) -> tuple[int, int]:
    """Clear all curve caches (hourly output curves and custom curves).

    Returns:
        Tuple of (hourly_curves_removed, custom_curves_removed)
    """
    return self.session.clear_all_curve_caches()
clear_session_cache
clear_session_cache()

Clear entire session temp directory (all cached files).

This removes all cached files for this session and clears all associated in-memory caches.

Source code in src/pyetm/models/scenario.py
def clear_session_cache(self) -> None:
    """Clear entire session temp directory (all cached files).

    This removes all cached files for this session and clears
    all associated in-memory caches.
    """
    self.session.clear_session_cache()
get_annual_export
get_annual_export(export_name)

Get a single annual export by name from the underlying session.

Source code in src/pyetm/models/scenario.py
def get_annual_export(self, export_name: str) -> Optional[pd.DataFrame]:
    """Get a single annual export by name from the underlying session."""
    return self.session.get_annual_export(export_name)
get_annual_exports
get_annual_exports(export_names)

Get multiple annual exports from the underlying session.

Source code in src/pyetm/models/scenario.py
def get_annual_exports(
    self, export_names: AnnualExportType | list[AnnualExportType]
) -> dict[str, pd.DataFrame]:
    """Get multiple annual exports from the underlying session."""
    return self.session.get_annual_exports(export_names)
update_couplings
update_couplings(coupling_groups, action='couple', force=False)

Update couplings on the underlying session.

Source code in src/pyetm/models/scenario.py
def update_couplings(
    self, coupling_groups: List[str], action: str = "couple", force: bool = False
) -> None:
    """Update couplings on the underlying session."""
    self.session.update_couplings(coupling_groups, action, force)
add_queries
add_queries(gquery_keys)

Add queries to the underlying session.

Source code in src/pyetm/models/scenario.py
def add_queries(self, gquery_keys: Union[list[str], set[str]]) -> None:
    """Add queries to the underlying session."""
    self.session.add_queries(gquery_keys)
execute_queries
execute_queries()

Execute queries on the underlying session.

Source code in src/pyetm/models/scenario.py
def execute_queries(self) -> None:
    """Execute queries on the underlying session."""
    self.session.execute_queries()
results
results(columns=None)

Get query results from the underlying session.

Source code in src/pyetm/models/scenario.py
def results(self, columns: Any = None) -> pd.DataFrame:
    """Get query results from the underlying session."""
    return self.session.results(columns)
queries_requested
queries_requested()

Check if queries have been requested on the underlying session.

Source code in src/pyetm/models/scenario.py
def queries_requested(self) -> bool:
    """Check if queries have been requested on the underlying session."""
    return self.session.queries_requested()
set_export_config
set_export_config(config)

Set export config on the underlying session.

Source code in src/pyetm/models/scenario.py
def set_export_config(self, config: "ExportConfig" | None) -> None:
    """Set export config on the underlying session."""
    self.session.set_export_config(config)
get_export_config
get_export_config()

Get export config from the underlying session.

Source code in src/pyetm/models/scenario.py
def get_export_config(self) -> "ExportConfig" | None:
    """Get export config from the underlying session."""
    return self.session.get_export_config()
show_all_warnings
show_all_warnings()

Show all warnings from the underlying session.

Source code in src/pyetm/models/scenario.py
def show_all_warnings(self) -> None:
    """Show all warnings from the underlying session."""
    self.session.show_all_warnings()
identifier
identifier()

Get identifier in priority order: saved title, short_name, session title, saved id, session id.

Source code in src/pyetm/models/scenario.py
def identifier(self) -> Union[str, int]:
    """Get identifier in priority order: saved title, short_name, session title, saved id, session id."""
    if self.title:
        return self.title
    if self.session.short_name:
        return self.session.short_name
    if self.session.title:
        return self.session.title
    if self.id:
        return self.id
    return self.session.id
set_short_name
set_short_name(short_name)

Set short name on the underlying session.

Source code in src/pyetm/models/scenario.py
def set_short_name(self, short_name: str) -> None:
    """Set short name on the underlying session."""
    self.session.set_short_name(short_name)
update_metadata
update_metadata(**kwargs)

Update metadata on the underlying session.

Source code in src/pyetm/models/scenario.py
def update_metadata(self, **kwargs: Any) -> Optional[Dict[str, Any]]:
    """Update metadata on the underlying session."""
    return self.session.update_metadata(**kwargs)
copy_with_preset
copy_with_preset(**overrides)

Create a copy of the underlying session with a linked preset and save it to MyETM.

Source code in src/pyetm/models/scenario.py
def copy_with_preset(self, **overrides: Any) -> "Scenario":
    """
    Create a copy of the underlying session with a linked preset and save it to MyETM.
    """
    # Separate SavedScenario parameters from Session copy parameters
    title = overrides.pop("title", f"Copy of {self.title}")
    private = overrides.pop("private", None)

    # Copy the underlying session with preset link, passing session-related overrides
    copied_session = self.session.copy_with_preset(**overrides)

    # Save the copied session to MyETM as a new SavedScenario
    save_params = {"title": title}
    if private is not None:
        save_params["private"] = private

    return cast("Scenario", copied_session.save(**save_params))
copy
copy(user_values=None, custom_curves=None, sortables=None, **overrides)

Create a copy with no template link to the original scenario and save it to MyETM.

Parameters:

Name Type Description Default
user_values Optional[Dict[str, Any]]

Optional dict of user input values to apply after copying

None
custom_curves Optional[Dict[str, Any]]

Optional dict of custom curves to upload after copying

None
sortables Optional[Dict[str, Any]]

Optional dict of sortables to apply after copying

None
**overrides Any

Additional parameters to override (title, private, etc.)

{}

Returns:

Type Description
'Scenario'

Copied SavedScenario instance

Source code in src/pyetm/models/scenario.py
def copy(
    self,
    user_values: Optional[Dict[str, Any]] = None,
    custom_curves: Optional[Dict[str, Any]] = None,
    sortables: Optional[Dict[str, Any]] = None,
    **overrides: Any,
) -> "Scenario":
    """
    Create a copy with no template link to the original scenario and save it to MyETM.

    Args:
        user_values: Optional dict of user input values to apply after copying
        custom_curves: Optional dict of custom curves to upload after copying
        sortables: Optional dict of sortables to apply after copying
        **overrides: Additional parameters to override (title, private, etc.)

    Returns:
        Copied SavedScenario instance
    """
    # Separate SavedScenario parameters from Session copy parameters
    title = overrides.pop("title", f"Copy of {self.title}")
    private = overrides.pop("private", None)

    # Copy the underlying session (no preset link), passing session-related overrides
    copied_session = self.session.copy(**overrides)

    # Save the copied session to MyETM as a new SavedScenario
    save_params = {"title": title}
    if private is not None:
        save_params["private"] = private

    copied_scenario = copied_session.save(**save_params)

    # Apply data parameters if provided
    if user_values or custom_curves or sortables:
        from pyetm.clients import BaseClient, get_client

        client = get_client()
        Scenario._apply_data_to_scenario(
            copied_scenario, user_values, custom_curves, sortables, client
        )

    return cast("Scenario", copied_scenario)
interpolate classmethod
interpolate(scenarios, *end_years, titles=None, client=None, **kwargs)

Interpolate one or more saved scenarios to target years and save to MyETM.

Source code in src/pyetm/models/scenario.py
@classmethod
def interpolate(
    cls,
    scenarios: Union["Scenario", List["Scenario"]],
    *end_years: int,
    titles: Optional[List[str]] = None,
    client: Optional[BaseClient] = None,
    **kwargs: Any,
) -> List["Scenario"]:
    """
    Interpolate one or more saved scenarios to target years and save to MyETM.
    """
    end_years_list = list(end_years)

    if titles is not None and len(titles) != len(end_years_list):
        raise ValueError(
            f"Length of titles ({len(titles)}) must match length of "
            f"end_years ({len(end_years_list)})"
        )

    # Get underlying sessions and perform interpolation
    from pyetm.models.session import Session

    scenario_list = scenarios if isinstance(scenarios, list) else [scenarios]
    sessions = [sc.session for sc in scenario_list]
    interpolated_sessions = Session.interpolate(sessions, *end_years, client=client)

    # Save each interpolated session as a SavedScenario
    saved_scenarios_list = []
    for i, session in enumerate(interpolated_sessions):
        # Generate title if not provided
        if titles:
            title = titles[i]
        else:
            title = f"Interpolated to {session.end_year}"

        saved = session.save(client=client, title=title, **kwargs)
        saved_scenarios_list.append(saved)

    return saved_scenarios_list
to_excel
to_excel(path, **export_options)

Export this saved scenario to Excel.

Source code in src/pyetm/models/scenario.py
def to_excel(self, path: PathLike[str] | str, **export_options: Any) -> None:
    """Export this saved scenario to Excel."""
    self.session.to_excel(path, **export_options)
collect_export_data
collect_export_data(**export_options)

Returns ExportDataCollection containing pandas DataFrames and dictionaries that can be exported to any file format (Parquet, CSV, JSON, etc.).

Source code in src/pyetm/models/scenario.py
def collect_export_data(self, **export_options: Any) -> "ExportDataCollection":
    """
    Returns ExportDataCollection containing pandas DataFrames and dictionaries
    that can be exported to any file format (Parquet, CSV, JSON, etc.).
    """
    return self.session.collect_export_data(**export_options)
list_users
list_users(client=None)

Fetch all users with access to this saved scenario.

Source code in src/pyetm/models/scenario.py
def list_users(self, client: Optional[BaseClient] = None) -> List[Dict[str, Any]]:
    """
    Fetch all users with access to this saved scenario.
    """
    if client is None:
        client = get_client()

    result = SavedScenarioUsersIndexRunner.run(client, self.id)

    if not result.success:
        raise SavedScenarioError(f"Could not fetch users: {result.errors}")

    if result.data is None:
        return []

    for user in result.data:
        user["role"] = user["role"].replace("scenario_", "", 1)

    return result.data
update_users
update_users(email, role, client=None, skip_upload=False)

Add, update, or remove a user's access to this saved scenario. - skip_upload: If True, store data locally without uploading (can be applied later)

Source code in src/pyetm/models/scenario.py
def update_users(
    self,
    email: str,
    role: str,
    client: Optional[BaseClient] = None,
    skip_upload: bool = False,
) -> None:
    """
    Add, update, or remove a user's access to this saved scenario.
    - skip_upload: If True, store data locally without uploading (can be applied later)
    """
    if client is None:
        client = get_client()

    role = self._normalize_role(role)

    if skip_upload:
        self._pending_users[email] = role
        return

    if role == "remove":
        self._remove_user(email, client)
        return

    if self._user_exists(email, client):
        self._update_user_role(email, role, client)
    else:
        self._add_user(email, role, client)
apply_pending_users
apply_pending_users(client=None)

Apply all pending user updates that were loaded with skip_upload=True.

Source code in src/pyetm/models/scenario.py
def apply_pending_users(self, client: Optional[BaseClient] = None) -> int:
    """
    Apply all pending user updates that were loaded with skip_upload=True.
    """
    if not self._pending_users:
        return 0

    if client is None:
        client = get_client()

    count = 0
    for email, role in list(self._pending_users.items()):
        try:
            self.update_users(email, role, client=client, skip_upload=False)
            count += 1
        except Exception as e:
            from pyetm.models.scenario import SavedScenarioError
            import logging

            logging.getLogger(__name__).warning(
                "Failed to apply pending user '%s' with role '%s': %s",
                email,
                role,
                e,
            )

    # Clear pending users after applying
    self._pending_users.clear()
    return count

Functions