"""Venue layout metadata library."""
from __future__ import annotations
from collections import Counter
from itertools import chain
from pathlib import Path
from typing import Generic, Iterable, Mapping, TypeVar
from . import yaml_loader
from .matches import StagingOffsets
from .types import (
LayoutData,
Region,
RegionData,
RegionName,
ShepherdData,
ShepherdingArea,
ShepherdingData,
ShepherdName,
TLA,
)
T = TypeVar('T')
T_str = TypeVar('T_str', bound=str)
[docs]class InvalidRegionException(Exception):
"""
An exception that occurs when there are invalid regions mentioned in
the shepherding data.
"""
def __init__(self, region: RegionName, area: str) -> None:
tpl = "Invalid region '{0}' found in shepherding area '{1}'"
super().__init__(tpl.format(region, area))
self.region = region
self.area = area
[docs]class MismatchException(Exception, Generic[T_str]):
"""
An exception that occurs when there are duplicate, extra or missing items.
"""
def __init__(
self,
tpl: str,
duplicates: Iterable[T_str],
extras: Iterable[T_str],
missing: Iterable[T_str],
) -> None:
details = []
for label, teams in (
("duplicates", duplicates),
("extras", extras),
("missing", missing),
):
if teams:
details.append(f"{label}: " + ", ".join(sorted(teams)))
assert details, f"No bad items given to {self.__class__}!"
detail = "; ".join(details)
super().__init__(tpl.format(detail))
self.duplicates = duplicates
self.extras = extras
self.missing = missing
[docs]class LayoutTeamsException(MismatchException[TLA]):
"""
An exception that occurs when there are duplicate, extra or missing
teams in a layout.
"""
def __init__(
self,
duplicate_teams: Iterable[TLA],
extra_teams: Iterable[TLA],
missing_teams: Iterable[TLA],
):
tpl = "Duplicate, extra or missing teams in the layout! ({0})"
super().__init__(tpl, duplicate_teams, extra_teams, missing_teams)
[docs]class ShepherdingAreasException(MismatchException[str]):
"""
An exception that occurs when there are duplicate, extra or missing
shepherding areas in the staging times.
"""
def __init__(
self,
where: str,
duplicate: Iterable[str],
extra: Iterable[str],
missing: Iterable[str],
):
tpl = f"Duplicate, extra or missing shepherding areas {where}! ({{0}})"
super().__init__(tpl, duplicate, extra, missing)
[docs]class Venue:
"""A class providing information about the layout within the venue."""
@staticmethod
def _check_staging_times(
shepherding_areas: Iterable[ShepherdName],
staging_times: StagingOffsets,
) -> None:
"""
Check that the given staging times contain signals for the right
set of shepherding areas.
Will throw a :class:`ShepherdingAreasException` if there are
any missing, extra or duplicate areas found.
:param list shepherding_areas: The reference list of shepherding
areas at the competition.
:param list teams_layout: A dict of staging times, containing at
least a ``signal_shepherds`` key which
is a map of times for each area.
"""
shepherding_areas_set = set(shepherding_areas)
staging_areas_set = set(staging_times['signal_shepherds'].keys())
extra_areas = staging_areas_set - shepherding_areas_set
missing_areas = shepherding_areas_set - staging_areas_set
if extra_areas or missing_areas:
raise ShepherdingAreasException(
"in the staging times",
[],
extra_areas,
missing_areas,
)
@staticmethod
def _get_duplicates(items: Iterable[T]) -> list[T]:
return [item for item, count in Counter(items).items() if count > 1]
[docs] @classmethod
def check_teams(cls, teams: Iterable[TLA], teams_layout: list[RegionData]) -> None:
"""
Check that the given layout of teams contains the same set of
teams as the reference.
Will throw a :class:`LayoutTeamsException` if there are any
missing, extra or duplicate teams found.
:param list teams: The reference list of teams in the competition.
:param list teams_layout: A list of maps with a list of teams
under the ``teams`` key.
"""
layout_teams = list(chain.from_iterable(r['teams'] for r in teams_layout))
duplicate_teams = cls._get_duplicates(layout_teams)
teams_set = set(teams)
layout_teams_set = set(layout_teams)
extra_teams = layout_teams_set - teams_set
missing_teams = teams_set - layout_teams_set
if duplicate_teams or extra_teams or missing_teams:
raise LayoutTeamsException(duplicate_teams, extra_teams, missing_teams)
@staticmethod
def _match_regions_and_shepherds(
shepherds: Iterable[ShepherdData],
teams_layout: list[RegionData],
) -> Iterable[tuple[RegionData, ShepherdData]]:
regions_by_name = {r['name']: r for r in teams_layout}
for shepherd in shepherds:
for region_name in shepherd.get('regions', []):
region = regions_by_name.get(region_name)
if not region:
raise InvalidRegionException(region_name, shepherd['name'])
yield region, shepherd
@staticmethod
def _build_locations(
regions_and_shepherds: Iterable[tuple[RegionData, ShepherdData]],
) -> Mapping[RegionName, Region]:
return {
region['name']: Region({
'name': region['name'],
'display_name': region['display_name'],
'description': region.get('description', ""),
'teams': region['teams'],
'shepherds': ShepherdingArea({
'name': shepherd['name'],
'colour': shepherd['colour'],
}),
})
for region, shepherd in regions_and_shepherds
}
def __init__(
self,
teams: Iterable[TLA],
layout_file: Path,
shepherding_file: Path,
):
layout_data: LayoutData = yaml_loader.load(layout_file)
teams_layout = layout_data['teams']
self.check_teams(teams, teams_layout)
shepherding_data: ShepherdingData = yaml_loader.load(shepherding_file)
shepherds = shepherding_data['shepherds']
self._shepherding_areas = [a['name'] for a in shepherds]
"""
A :class:`list` of shepherding zone names from the shepherding file.
"""
duplicate_areas = self._get_duplicates(self._shepherding_areas)
if duplicate_areas:
raise ShepherdingAreasException(
'in the shepherding data',
duplicate_areas,
[],
[],
)
self.locations = self._build_locations(
self._match_regions_and_shepherds(shepherds, teams_layout),
)
"""
A :class:`dict` of location names (from the layout file) to location
information, including which teams are in that location and the
shepherding region which contains that location.
"""
self._team_locations: dict[TLA, Region] = {}
for location in self.locations.values():
for team in location['teams']:
self._team_locations[team] = location
[docs] def check_staging_times(self, staging_times: StagingOffsets) -> None:
self._check_staging_times(self._shepherding_areas, staging_times)
[docs] def get_team_location(self, team: TLA) -> RegionName:
"""
Get the name of the location allocated to the given team within
the venue.
:param str team: The TLA of the team in question.
:returns: The name of the location allocated to the team.
"""
return self._team_locations[team]['name']