Add intermed file

Does not yet display assignment fairness
Contributes towards #5
This commit is contained in:
Théophile Bastian 2023-03-14 17:08:55 +01:00
parent 2464eba49d
commit 44cb69583f
2 changed files with 207 additions and 17 deletions

View file

@ -7,6 +7,7 @@ import jinja2 as j2
from .config import Task, Category, Config from .config import Task, Category, Config
from .partition import TaskId, partition from .partition import TaskId, partition
from . import intermed_file
from . import util from . import util
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -201,6 +202,16 @@ def main() -> None:
"--to-short-md", "--to-short-md",
help="Exporter vers un fichier Markdown (pour vérification uniquement)", help="Exporter vers un fichier Markdown (pour vérification uniquement)",
) )
parser.add_argument(
"--to-intermed",
type=Path,
help="Exporter vers un fichier éditable manuellement, et finaliser plus tard",
)
parser.add_argument(
"--use-intermed",
type=Path,
help="Importer un fichier éditable manuellement précédemment généré",
)
parser.add_argument( parser.add_argument(
"-g", "-g",
"--debug", "--debug",
@ -218,24 +229,36 @@ def main() -> None:
if args.bare_tasks: if args.bare_tasks:
util.write_to_file(args.bare_tasks, export_bare_tasks_md(config)) util.write_to_file(args.bare_tasks, export_bare_tasks_md(config))
retry: int = 0 groupes: list[Group]
MAX_RETRY: int = 4 if args.use_intermed:
while retry < MAX_RETRY: intermed = intermed_file.IntermedFile.from_file(
try: config.choristes, config.taches, args.use_intermed
groupes = repartition(config) )
break intermed.sanity_check()
except AssignError as exn: groupes = intermed.to_assignment(config)
retry += 1 else:
logger.warning( retry: int = 0
"[essai %d/%d] Échec de répartition des tâches : %s", MAX_RETRY: int = 4
retry, while retry < MAX_RETRY:
MAX_RETRY, try:
exn, groupes = repartition(config)
) break
if retry == MAX_RETRY: except AssignError as exn:
logger.critical("Échec de répartition des tâches.") retry += 1
raise exn from exn logger.warning(
"[essai %d/%d] Échec de répartition des tâches : %s",
retry,
MAX_RETRY,
exn,
)
if retry == MAX_RETRY:
logger.critical("Échec de répartition des tâches.")
raise exn from exn
if args.to_intermed:
intermed = intermed_file.IntermedFile.from_assignment(config, groupes)
intermed.sanity_check()
intermed.write(args.to_intermed)
if args.to_tex: if args.to_tex:
util.write_to_file(args.to_tex, export_latex(config, groupes)) util.write_to_file(args.to_tex, export_latex(config, groupes))
if args.to_short_md: if args.to_short_md:

View file

@ -0,0 +1,167 @@
""" Fichier intermédiaire aisément éditable à la main """
from pathlib import Path
import typing as t
from dataclasses import dataclass
import dataclasses
import ruamel
from . import config
Assignment: t.TypeAlias = dict[str, list[int]]
class BadIntermedFile(Exception):
"""Raised when an intermediary file doesn't contain what is expected"""
@dataclass
class _IntermedFileData:
assignation: Assignment
nb_groupes: int
groupes: list[list[str]]
class IntermedFile:
"""Fichier intermédiaire aisément éditable à la main"""
choristes: list[str]
tasks: config.Category
data: _IntermedFileData
def __init__(
self,
choristes: list[str],
tasks: config.Category,
assignment: dict[str, list[int]],
nb_groups: int,
groups: list[list[str]],
):
self.choristes = choristes
self.tasks = tasks
self.data = _IntermedFileData(
assignation=assignment,
nb_groupes=nb_groups,
groupes=groups,
)
def sanity_check(self):
"""Check that the data is consistent -- does not check that the assignment is
fair"""
def check_taches(task: config.Category | config.Task):
if isinstance(task, config.Task):
if task.qualified_name not in self.data.assignation:
raise BadIntermedFile(
f"Aucun groupe assigné pour {task.qualified_name}"
)
nb_assigned = len(self.data.assignation[task.qualified_name])
if nb_assigned != task.nb_groups:
raise BadIntermedFile(
f"{nb_assigned} groupes assignés pour {task.qualified_name}, "
f"il en faut {task.nb_groups}"
)
else:
for child in task.tasks:
check_taches(child)
if len(self.data.groupes) != self.data.nb_groupes:
raise BadIntermedFile("Nombre de groupes incohérent")
choristes_seen: dict[str, int] = {}
for grp_id, group in enumerate(self.data.groupes):
for choriste in group:
if choriste in choristes_seen:
raise BadIntermedFile(
f"{choriste} est dans plusieurs groupes : "
f"{choristes_seen[choriste]} et {grp_id}."
)
if choriste not in self.choristes:
raise BadIntermedFile(
f"{choriste} est dans le groupe {grp_id}, mais ne fait pas "
"partie de la liste des choristes"
)
choristes_seen[choriste] = grp_id
missing_choristes = set(self.choristes) - set(choristes_seen.keys())
if missing_choristes:
raise BadIntermedFile(
"Ces choristes ne sont dans aucun groupe : "
+ ", ".join(missing_choristes)
)
check_taches(self.tasks)
def write(self, to_path: Path):
"""Write the intermediary file to this path"""
yaml = ruamel.yaml.YAML()
yaml.dump(dataclasses.asdict(self.data), to_path)
def to_assignment(self, conf: config.Config) -> list[list[str]]:
"""Use this intermediary file to make an assignment. Assigns groups to tasks
in the config, and returns the groups"""
def assign_tasks(task: config.Category | config.Task):
if isinstance(task, config.Task):
task.assigned = self.data.assignation[task.qualified_name]
task.assigned.sort()
else:
for child in task.tasks:
assign_tasks(child)
assign_tasks(conf.taches)
return self.data.groupes
@classmethod
def from_assignment(
cls, conf: config.Config, groups: list[list[str]]
) -> "IntermedFile":
def make_assignment(task: config.Category | config.Task, out: Assignment):
if isinstance(task, config.Task):
assert task.qualified_name not in out
assert task.assigned is not None
# Internal groups are 0-indexed
out[task.qualified_name] = list(map(lambda x: x + 1, task.assigned))
else:
for child in task.tasks:
make_assignment(child, out)
assignment: Assignment = {}
make_assignment(conf.taches, assignment)
return cls(
choristes=conf.choristes,
tasks=conf.taches,
assignment=assignment,
nb_groups=len(groups),
groups=groups,
)
@classmethod
def from_file(
cls, choristes: list[str], tasks: config.Category, file: Path
) -> "IntermedFile":
yaml = ruamel.yaml.YAML()
with file.open("r") as handle:
raw_data = yaml.load(handle)
for expected_field in dataclasses.fields(_IntermedFileData):
if expected_field.name not in raw_data:
raise BadIntermedFile(
f"Le champ '{expected_field.name}' est absent du fichier !"
)
try:
data = _IntermedFileData(**raw_data)
except TypeError as exn:
raise BadIntermedFile(
"Impossible d'interpréter ce fichier comme un fichier intermédiaire."
) from exn
# Internal groups are 0-indexed
for grp in data.assignation.values():
for pos in range(len(grp)):
grp[pos] -= 1
return cls(
choristes=choristes,
tasks=tasks,
assignment=data.assignation,
nb_groups=data.nb_groupes,
groups=data.groupes,
)