Compare commits
2 commits
2464eba49d
...
05372eef68
Author | SHA1 | Date | |
---|---|---|---|
05372eef68 | |||
44cb69583f |
2 changed files with 236 additions and 17 deletions
|
@ -2,11 +2,13 @@ import argparse
|
|||
import typing as t
|
||||
import random
|
||||
from pathlib import Path
|
||||
from collections import defaultdict
|
||||
import logging
|
||||
import jinja2 as j2
|
||||
|
||||
from .config import Task, Category, Config
|
||||
from .partition import TaskId, partition
|
||||
from . import intermed_file
|
||||
from . import util
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -178,6 +180,32 @@ def export_latex(config: Config, groupes: list[list[str]]) -> str:
|
|||
return template.render(**env)
|
||||
|
||||
|
||||
def log_assignment_toughness(config: Config):
|
||||
"""Prints each group's assigned tasks toughness"""
|
||||
|
||||
grp_tough: dict[int, int] = defaultdict(int)
|
||||
|
||||
def explore_tasks(task: Task | Category):
|
||||
if isinstance(task, Task):
|
||||
assert task.assigned is not None
|
||||
for grp in task.assigned:
|
||||
grp_tough[grp] += task.tough
|
||||
else:
|
||||
for child in task.tasks:
|
||||
explore_tasks(child)
|
||||
|
||||
explore_tasks(config.taches)
|
||||
|
||||
grp_ids = list(grp_tough.keys())
|
||||
grp_ids.sort()
|
||||
tough_lines = []
|
||||
for grp_id in grp_ids:
|
||||
tough_lines.append(f"{grp_id+1:2d} : {grp_tough[grp_id]:3d}")
|
||||
out_str = "Répartition des pénibilités :\n" + "\n".join(tough_lines)
|
||||
|
||||
logger.info(out_str)
|
||||
|
||||
|
||||
def repartition(config: Config) -> list[Group]:
|
||||
"""Crée des groupes et assigne des tâches"""
|
||||
groupes: list[Group] = constituer_groupes(config.choristes)
|
||||
|
@ -201,6 +229,16 @@ def main() -> None:
|
|||
"--to-short-md",
|
||||
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(
|
||||
"-g",
|
||||
"--debug",
|
||||
|
@ -218,24 +256,38 @@ def main() -> None:
|
|||
if args.bare_tasks:
|
||||
util.write_to_file(args.bare_tasks, export_bare_tasks_md(config))
|
||||
|
||||
retry: int = 0
|
||||
MAX_RETRY: int = 4
|
||||
while retry < MAX_RETRY:
|
||||
try:
|
||||
groupes = repartition(config)
|
||||
break
|
||||
except AssignError as exn:
|
||||
retry += 1
|
||||
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
|
||||
groupes: list[Group]
|
||||
if args.use_intermed:
|
||||
intermed = intermed_file.IntermedFile.from_file(
|
||||
config.choristes, config.taches, args.use_intermed
|
||||
)
|
||||
intermed.sanity_check()
|
||||
groupes = intermed.to_assignment(config)
|
||||
else:
|
||||
retry: int = 0
|
||||
MAX_RETRY: int = 4
|
||||
while retry < MAX_RETRY:
|
||||
try:
|
||||
groupes = repartition(config)
|
||||
break
|
||||
except AssignError as exn:
|
||||
retry += 1
|
||||
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
|
||||
|
||||
log_assignment_toughness(config)
|
||||
|
||||
if args.to_intermed:
|
||||
intermed = intermed_file.IntermedFile.from_assignment(config, groupes)
|
||||
intermed.sanity_check()
|
||||
intermed.write(args.to_intermed)
|
||||
if args.to_tex:
|
||||
util.write_to_file(args.to_tex, export_latex(config, groupes))
|
||||
if args.to_short_md:
|
||||
|
|
167
repartir_taches/intermed_file.py
Normal file
167
repartir_taches/intermed_file.py
Normal 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,
|
||||
)
|
Loading…
Reference in a new issue