Skip to content

users_pack

users_pack

User data packing utilities.

Classes

UsersPack

Bases: Packable

UsersPack handles the import, export, and management of scenario user roles, using a grid format similar to SLIDER_SETTINGS.

Functions
to_dataframe
to_dataframe(columns=None)

Export user roles to a DataFrame in grid format.

Returns:

Type Description
DataFrame

DataFrame with user emails as index, scenarios as columns

Source code in src/pyetm/models/packables/users_pack.py
def to_dataframe(self, columns: Any = None) -> pd.DataFrame:
    """
    Export user roles to a DataFrame in grid format.

    Returns:
        DataFrame with user emails as index, scenarios as columns
    """
    if not self.scenarios:
        return pd.DataFrame()

    # Collect all users across all scenarios
    user_roles: Dict[str, Dict[Any, str]] = {}  # email -> {scenario_key: role}

    for scenario in self.scenarios:
        scenario_key = self._get_scenario_display_key(scenario)

        try:
            result = ScenarioUsersIndexRunner.run(
                client=scenario.client, scenario_id=scenario.id  # type: ignore[attr-defined]
            )

            if not result.success:
                logger.warning(
                    "Failed to fetch users for scenario '%s': %s",
                    scenario.identifier(),
                    result.errors,
                )
                continue

            users = result.data or []
            for user in users:
                email = user.get("email")
                role = user.get("role")

                if not email or not role:
                    continue

                # Convert API role to short name for display
                short_role = self._api_role_to_short(role)

                if email not in user_roles:
                    user_roles[email] = {}
                user_roles[email][scenario_key] = short_role

        except Exception as e:
            logger.warning(
                "Error fetching users for scenario '%s': %s",
                scenario.identifier(),
                e,
            )

    if not user_roles:
        return pd.DataFrame()

    # Build DataFrame with multi-level columns
    frames = []
    labels = []

    for scenario in self.scenarios:
        scenario_key = self._get_scenario_display_key(scenario)
        # Create a series with all emails and their roles for this scenario
        scenario_roles = {
            email: roles.get(scenario_key, np.nan) for email, roles in user_roles.items()
        }
        df = pd.DataFrame({"role": scenario_roles})
        frames.append(df)
        labels.append(scenario_key)

    return (
        pd.concat(frames, axis=1, keys=labels, names=["scenario", "field"])
        if frames
        else pd.DataFrame()
    )
from_dataframe
from_dataframe(df, update_set=None)

Import user roles from DataFrame and apply to scenarios.

Parameters:

Name Type Description Default
df Any

DataFrame in grid format (emails × scenarios)

required
update_set Optional[set[str]]

Set of update types to apply (checks for "users")

None
Source code in src/pyetm/models/packables/users_pack.py
def from_dataframe(self, df: Any, update_set: Optional[set[str]] = None) -> None:
    """
    Import user roles from DataFrame and apply to scenarios.

    Args:
        df: DataFrame in grid format (emails × scenarios)
        update_set: Set of update types to apply (checks for "users")
    """
    if df is None or getattr(df, "empty", False):
        return

    # Check if we should upload to API or just load locally
    skip_upload = not self._should_include_upload(update_set)

    try:
        # Extract grid data using base class helper
        data_df = self._extract_grid_data(df)
        if data_df is None:
            return

        # Prepare grid data using base class helper
        data_df, user_emails, scenario_columns = self._prepare_grid_data(data_df)

        for column_name in scenario_columns:
            # Resolve scenario with automatic warning
            scenario = self._resolve_scenario_with_warning(column_name, "USERS sheet")
            if scenario is None:
                continue

            # Process each user/role pair for this scenario
            for email in user_emails:
                role_value = data_df.loc[email, column_name]

                if pd.isna(role_value):
                    continue

                try:
                    scenario.update_users(email, role_value, skip_upload=skip_upload)
                    if not skip_upload:
                        logger.info(
                            "Updated user '%s' with role '%s' for scenario '%s'",
                            email,
                            role_value,
                            scenario.identifier(),
                        )
                except Exception as e:
                    logger.warning(
                        "Failed to apply role for user '%s' in scenario '%s': %s",
                        email,
                        scenario.identifier(),
                        e,
                    )

    except Exception as e:
        logger.warning("Failed to parse USERS sheet: %s", e)