"""Represents the configuration of a multiple expression
:author: Michel Bierlaire
:date: Sun Apr 2 14:15:17 2023
"""
from __future__ import annotations
import logging
from typing import NamedTuple, Iterable
import biogeme.exceptions as excep
logger = logging.getLogger(__name__)
[docs]
class SelectionTuple(NamedTuple):
controller: str
selection: str
SEPARATOR = ';'
SELECTION_SEPARATOR = ':'
[docs]
class Configuration:
"""Represents the configuration of a multiple expression. It is
internally represented as a sorted list of tuples.
"""
def __init__(self, selections: Iterable[SelectionTuple] | None = None):
"""Ctor.
:param selections: list of tuples, where each of
them associates a controller name with the selected configuration
:type selections: list(SelectionTuple)
"""
if selections is None:
self.__selections: list[SelectionTuple] | None = None
else:
self.selections: list[SelectionTuple] = list(selections)
@property
def selections(self) -> list[SelectionTuple]:
return self.__selections
@selections.setter
def selections(self, the_list: list[SelectionTuple]):
self.__selections = sorted(the_list)
self.__check_list_validity()
self.string_id = self.get_string_id()
[docs]
@classmethod
def from_string(cls, string_id: str) -> Configuration:
"""Ctor from a string representation
:param string_id: string ID
:type string_id: str
"""
terms = string_id.split(SEPARATOR)
the_config = {}
for term in terms:
try:
controller, selection = term.split(SELECTION_SEPARATOR)
except ValueError as exc:
error_msg = (
f'{exc}: Invalid syntax for ID {term}. Expecting a separator '
f'[{SELECTION_SEPARATOR}]'
)
raise excep.BiogemeError(error_msg)
the_config[controller] = selection
return cls.from_dict(the_config)
[docs]
@classmethod
def from_dict(cls, dict_of_selections: dict[str, str]) -> Configuration:
"""Ctor from dict
:param dict_of_selections: dict associating a catalog name
with the selected configuration
:type dict_of_selections: dict(str: str)
"""
the_list = (
SelectionTuple(controller=controller, selection=selection)
for controller, selection in dict_of_selections.items()
)
return cls(selections=the_list)
[docs]
@classmethod
def from_tuple_of_configurations(
cls, tuple_of_configurations: tuple[Configuration, ...]
) -> Configuration:
"""Ctor from tuple of configurations that are merged together.
In the presence of two different selections for the same
catalog, the first one is selected,
and the others ignored.
:param tuple_of_configurations: tuple of configurations to merge
:type tuple_of_configurations: tuple(Configuration)
"""
known_controllers = set()
selections = []
for configuration in tuple_of_configurations:
if configuration.selections is not None:
for selection in configuration.selections:
if selection.controller not in known_controllers:
selections.append(selection)
known_controllers.add(selection.controller)
return cls(selections)
[docs]
def set_of_controllers(self) -> set[str]:
return {selection.controller for selection in self.selections}
def __eq__(self, other: Configuration) -> bool:
return self.string_id == other.string_id
def __hash__(self) -> int:
return hash(self.string_id)
def __repr__(self) -> str:
return repr(self.string_id)
def __str__(self) -> str:
return str(self.string_id)
[docs]
def get_string_id(self) -> str:
"""The string ID is a unique string representation of the configuration
:return: string ID
:rtype: str
"""
terms = [
f'{selection.controller}{SELECTION_SEPARATOR}{selection.selection}'
for selection in self.selections
]
return SEPARATOR.join(terms)
[docs]
def get_html(self) -> str:
html = '<p>Specification</p><p><ul>\n'
for selection_tuple in self.selections:
html += (
f'<li>{selection_tuple.controller}: '
f'{selection_tuple.selection}</li>\n'
)
html += '</ul></p>\n'
return html
[docs]
def get_selection(self, controller_name: str) -> str | None:
"""Retrieve the selection of a given controller
:param controller_name: name of the controller
:type controller_name: str
:return: name of the selected config, or None if controller is not known
:rtype: str
"""
for selection in self.selections:
if selection.controller == controller_name:
return selection.selection
return None
def __check_list_validity(self) -> None:
"""Check the validity of the list.
:raise BiogemeError: if the same catalog appears more than once in the list
"""
unique_items = set()
for item in self.__selections:
if item.controller in unique_items:
error_msg = (
f'Controller {item.controller} appears more than once in the '
f'configuration: {self.__selections}'
)
raise excep.BiogemeError(error_msg)
unique_items.add(item.controller)