Refactor into python module, multiple files
This commit is contained in:
parent
81d8423c88
commit
99ed1d462d
6 changed files with 181 additions and 141 deletions
0
repartir_taches/__init__.py
Normal file
0
repartir_taches/__init__.py
Normal file
|
@ -1,10 +1,9 @@
|
||||||
import yaml
|
|
||||||
import argparse
|
|
||||||
import csv
|
|
||||||
from collections import defaultdict
|
|
||||||
import typing as t
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import random
|
from collections import defaultdict
|
||||||
|
import csv
|
||||||
|
import typing as t
|
||||||
|
from ruamel import yaml
|
||||||
|
from .util import levenshtein_distance, UnionFind
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -27,55 +26,6 @@ class Category(t.NamedTuple):
|
||||||
intro: str
|
intro: str
|
||||||
|
|
||||||
|
|
||||||
def levenshtein_distance(s1, s2):
|
|
||||||
"""Shamelessly stolen from https://stackoverflow.com/a/32558749"""
|
|
||||||
if len(s1) > len(s2):
|
|
||||||
s1, s2 = s2, s1
|
|
||||||
|
|
||||||
distances = range(len(s1) + 1)
|
|
||||||
for i2, c2 in enumerate(s2):
|
|
||||||
distances_ = [i2 + 1]
|
|
||||||
for i1, c1 in enumerate(s1):
|
|
||||||
if c1 == c2:
|
|
||||||
distances_.append(distances[i1])
|
|
||||||
else:
|
|
||||||
distances_.append(
|
|
||||||
1 + min((distances[i1], distances[i1 + 1], distances_[-1]))
|
|
||||||
)
|
|
||||||
distances = distances_
|
|
||||||
return distances[-1]
|
|
||||||
|
|
||||||
|
|
||||||
class UnionFind:
|
|
||||||
parent_of: list[int]
|
|
||||||
_group_size: list[int]
|
|
||||||
|
|
||||||
def __init__(self, elt_count: int):
|
|
||||||
self.parent_of = list(range(elt_count))
|
|
||||||
self._group_size = [1] * elt_count
|
|
||||||
|
|
||||||
def root(self, elt: int) -> int:
|
|
||||||
if self.parent_of[elt] == elt:
|
|
||||||
return elt
|
|
||||||
self.parent_of[elt] = self.root(self.parent_of[elt])
|
|
||||||
return self.parent_of[elt]
|
|
||||||
|
|
||||||
def union(self, elt1: int, elt2: int) -> None:
|
|
||||||
elt1 = self.root(elt1)
|
|
||||||
elt2 = self.root(elt2)
|
|
||||||
if elt1 == elt2:
|
|
||||||
return
|
|
||||||
if self._group_size[elt1] > self._group_size[elt2]:
|
|
||||||
self.union(elt2, elt1)
|
|
||||||
else:
|
|
||||||
self._group_size[elt2] += self._group_size[elt1]
|
|
||||||
self._group_size[elt1] = 0
|
|
||||||
self.parent_of[self.root(elt1)] = self.root(elt2)
|
|
||||||
|
|
||||||
def group_size(self, elt: int) -> int:
|
|
||||||
return self._group_size[self.root(elt)]
|
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
tasks_path: str
|
tasks_path: str
|
||||||
people_path: str
|
people_path: str
|
||||||
|
@ -199,89 +149,3 @@ class Config:
|
||||||
req_letters += 1
|
req_letters += 1
|
||||||
|
|
||||||
self.choristes.sort()
|
self.choristes.sort()
|
||||||
|
|
||||||
|
|
||||||
def constituer_groupes(choristes: list[str]) -> list[list[str]]:
|
|
||||||
"""Répartir aléatoirement les gens en groupes"""
|
|
||||||
TAILLE_GROUPE: int = 4
|
|
||||||
nb_choristes = len(choristes)
|
|
||||||
|
|
||||||
groupes: list[list[str]] = []
|
|
||||||
|
|
||||||
random.shuffle(choristes)
|
|
||||||
pos = 0
|
|
||||||
for _ in range(nb_choristes // TAILLE_GROUPE):
|
|
||||||
groupes.append(choristes[pos : pos + TAILLE_GROUPE])
|
|
||||||
pos += TAILLE_GROUPE
|
|
||||||
|
|
||||||
reste = choristes[pos:]
|
|
||||||
|
|
||||||
if len(reste) == TAILLE_GROUPE - 1:
|
|
||||||
groupes.append(reste)
|
|
||||||
else:
|
|
||||||
for gid, pers in enumerate(reste):
|
|
||||||
groupes[gid].append(pers)
|
|
||||||
|
|
||||||
for groupe in groupes:
|
|
||||||
groupe.sort()
|
|
||||||
random.shuffle(groupes)
|
|
||||||
|
|
||||||
return groupes
|
|
||||||
|
|
||||||
|
|
||||||
def assigner_taches(task: Category | Task, group_count: int, cur_group: int = 0) -> int:
|
|
||||||
"""Assigne les tâches aux groupes (round-robin)"""
|
|
||||||
if isinstance(task, Task):
|
|
||||||
task.assigned = list(
|
|
||||||
map(
|
|
||||||
lambda x: x % group_count,
|
|
||||||
range(cur_group, cur_group + task.nb_groups),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return (cur_group + task.nb_groups) % group_count
|
|
||||||
for subtask in task.tasks:
|
|
||||||
cur_group = assigner_taches(subtask, group_count, cur_group)
|
|
||||||
return cur_group
|
|
||||||
|
|
||||||
|
|
||||||
def export_short(config: Config, groupes: list[list[str]]) -> str:
|
|
||||||
"""Exporte la liste des tâches au format court (pour vérification)"""
|
|
||||||
|
|
||||||
def export_taskcat(grp: Task | Category) -> str:
|
|
||||||
if isinstance(grp, Task):
|
|
||||||
return f'* {grp.qualified_name}: {", ".join(map(lambda x: str(x+1), grp.assigned))}'
|
|
||||||
out = "\n" + "#" * (2 + grp.depth) + f" {grp.name}"
|
|
||||||
if grp.time:
|
|
||||||
out += f" ({grp.time})"
|
|
||||||
out += "\n\n"
|
|
||||||
if grp.intro:
|
|
||||||
out += grp.intro + "\n\n"
|
|
||||||
out += "\n".join(map(export_taskcat, grp.tasks))
|
|
||||||
return out
|
|
||||||
|
|
||||||
out = "## Groupes\n\n"
|
|
||||||
for g_id, group in enumerate(groupes):
|
|
||||||
out += f"* Groupe {g_id+1} : " + ", ".join(group) + "\n"
|
|
||||||
|
|
||||||
out += "\n## Tâches\n"
|
|
||||||
|
|
||||||
out += "\n".join(map(export_taskcat, config.taches.tasks))
|
|
||||||
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def export_latex(config: Config, groupes: list[list[str]]) -> str:
|
|
||||||
"""Exporter la liste des tâches en LaTeX (à insérer dans un template)"""
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser("Répartition des tâches")
|
|
||||||
parser.add_argument("taches", help="Fichier yaml contenant les tâches")
|
|
||||||
parser.add_argument("choristes", help="Fichier CSV contenant les choristes")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
config = Config(args.taches, args.choristes)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
88
repartir_taches/entrypoint.py
Normal file
88
repartir_taches/entrypoint.py
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
import argparse
|
||||||
|
import typing as t
|
||||||
|
import random
|
||||||
|
|
||||||
|
from .config import Task, Category, Config
|
||||||
|
|
||||||
|
|
||||||
|
def constituer_groupes(choristes: list[str]) -> list[list[str]]:
|
||||||
|
"""Répartir aléatoirement les gens en groupes"""
|
||||||
|
TAILLE_GROUPE: int = 4
|
||||||
|
nb_choristes = len(choristes)
|
||||||
|
|
||||||
|
groupes: list[list[str]] = []
|
||||||
|
|
||||||
|
random.shuffle(choristes)
|
||||||
|
pos = 0
|
||||||
|
for _ in range(nb_choristes // TAILLE_GROUPE):
|
||||||
|
groupes.append(choristes[pos : pos + TAILLE_GROUPE])
|
||||||
|
pos += TAILLE_GROUPE
|
||||||
|
|
||||||
|
reste = choristes[pos:]
|
||||||
|
|
||||||
|
if len(reste) == TAILLE_GROUPE - 1:
|
||||||
|
groupes.append(reste)
|
||||||
|
else:
|
||||||
|
for gid, pers in enumerate(reste):
|
||||||
|
groupes[gid].append(pers)
|
||||||
|
|
||||||
|
for groupe in groupes:
|
||||||
|
groupe.sort()
|
||||||
|
random.shuffle(groupes)
|
||||||
|
|
||||||
|
return groupes
|
||||||
|
|
||||||
|
|
||||||
|
def assigner_taches(task: Category | Task, group_count: int, cur_group: int = 0) -> int:
|
||||||
|
"""Assigne les tâches aux groupes (round-robin)"""
|
||||||
|
if isinstance(task, Task):
|
||||||
|
task.assigned = list(
|
||||||
|
map(
|
||||||
|
lambda x: x % group_count,
|
||||||
|
range(cur_group, cur_group + task.nb_groups),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return (cur_group + task.nb_groups) % group_count
|
||||||
|
for subtask in task.tasks:
|
||||||
|
cur_group = assigner_taches(subtask, group_count, cur_group)
|
||||||
|
return cur_group
|
||||||
|
|
||||||
|
|
||||||
|
def export_short(config: Config, groupes: list[list[str]]) -> str:
|
||||||
|
"""Exporte la liste des tâches au format court (pour vérification)"""
|
||||||
|
|
||||||
|
def export_taskcat(grp: Task | Category) -> str:
|
||||||
|
if isinstance(grp, Task):
|
||||||
|
assert grp.assigned is not None
|
||||||
|
return f'* {grp.qualified_name}: {", ".join(map(lambda x: str(x+1), grp.assigned))}'
|
||||||
|
out = "\n" + "#" * (2 + grp.depth) + f" {grp.name}"
|
||||||
|
if grp.time:
|
||||||
|
out += f" ({grp.time})"
|
||||||
|
out += "\n\n"
|
||||||
|
if grp.intro:
|
||||||
|
out += grp.intro + "\n\n"
|
||||||
|
out += "\n".join(map(export_taskcat, grp.tasks))
|
||||||
|
return out
|
||||||
|
|
||||||
|
out = "## Groupes\n\n"
|
||||||
|
for g_id, group in enumerate(groupes):
|
||||||
|
out += f"* Groupe {g_id+1} : " + ", ".join(group) + "\n"
|
||||||
|
|
||||||
|
out += "\n## Tâches\n"
|
||||||
|
|
||||||
|
out += "\n".join(map(export_taskcat, config.taches.tasks))
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def export_latex(config: Config, groupes: list[list[str]]) -> str:
|
||||||
|
"""Exporter la liste des tâches en LaTeX (à insérer dans un template)"""
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser("Répartition des tâches")
|
||||||
|
parser.add_argument("taches", help="Fichier yaml contenant les tâches")
|
||||||
|
parser.add_argument("choristes", help="Fichier CSV contenant les choristes")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
config = Config(args.taches, args.choristes)
|
0
repartir_taches/py.typed
Normal file
0
repartir_taches/py.typed
Normal file
59
repartir_taches/util.py
Normal file
59
repartir_taches/util.py
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
""" Utility functions and classes """
|
||||||
|
|
||||||
|
|
||||||
|
def levenshtein_distance(s1, s2):
|
||||||
|
"""Compute the Levenshtein distance (edit distance) between two strings
|
||||||
|
|
||||||
|
Shamelessly stolen from https://stackoverflow.com/a/32558749"""
|
||||||
|
if len(s1) > len(s2):
|
||||||
|
s1, s2 = s2, s1
|
||||||
|
|
||||||
|
distances = range(len(s1) + 1)
|
||||||
|
for i2, c2 in enumerate(s2):
|
||||||
|
distances_ = [i2 + 1]
|
||||||
|
for i1, c1 in enumerate(s1):
|
||||||
|
if c1 == c2:
|
||||||
|
distances_.append(distances[i1])
|
||||||
|
else:
|
||||||
|
distances_.append(
|
||||||
|
1 + min((distances[i1], distances[i1 + 1], distances_[-1]))
|
||||||
|
)
|
||||||
|
distances = distances_
|
||||||
|
return distances[-1]
|
||||||
|
|
||||||
|
|
||||||
|
class UnionFind:
|
||||||
|
"""A union-find implementation"""
|
||||||
|
|
||||||
|
parent_of: list[int]
|
||||||
|
_group_size: list[int]
|
||||||
|
|
||||||
|
def __init__(self, elt_count: int):
|
||||||
|
self.parent_of = list(range(elt_count))
|
||||||
|
self._group_size = [1] * elt_count
|
||||||
|
|
||||||
|
def root(self, elt: int) -> int:
|
||||||
|
"""Find the element representing :elt: (root of component)
|
||||||
|
|
||||||
|
Compresses paths along the way"""
|
||||||
|
if self.parent_of[elt] == elt:
|
||||||
|
return elt
|
||||||
|
self.parent_of[elt] = self.root(self.parent_of[elt])
|
||||||
|
return self.parent_of[elt]
|
||||||
|
|
||||||
|
def union(self, elt1: int, elt2: int) -> None:
|
||||||
|
"""Unites two components"""
|
||||||
|
elt1 = self.root(elt1)
|
||||||
|
elt2 = self.root(elt2)
|
||||||
|
if elt1 == elt2:
|
||||||
|
return
|
||||||
|
if self._group_size[elt1] > self._group_size[elt2]:
|
||||||
|
self.union(elt2, elt1)
|
||||||
|
else:
|
||||||
|
self._group_size[elt2] += self._group_size[elt1]
|
||||||
|
self._group_size[elt1] = 0
|
||||||
|
self.parent_of[self.root(elt1)] = self.root(elt2)
|
||||||
|
|
||||||
|
def group_size(self, elt: int) -> int:
|
||||||
|
"""Get the number of elements in the component of :elt:"""
|
||||||
|
return self._group_size[self.root(elt)]
|
29
setup.py
Executable file
29
setup.py
Executable file
|
@ -0,0 +1,29 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def parse_requirements():
|
||||||
|
reqs = []
|
||||||
|
with open("requirements.txt", "r") as handle:
|
||||||
|
for line in handle:
|
||||||
|
reqs.append(line)
|
||||||
|
return reqs
|
||||||
|
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name="repartir_taches",
|
||||||
|
version="0.0.1",
|
||||||
|
description="Réparttion des tâches pour le WE chorale",
|
||||||
|
url="https://git.tobast.fr/RainbowSwingers/WE-repartir-taches",
|
||||||
|
packages=find_packages(),
|
||||||
|
include_package_data=True,
|
||||||
|
package_data={"staticdeps": ["py.typed"]},
|
||||||
|
install_requires=parse_requirements(),
|
||||||
|
entry_points={
|
||||||
|
"console_scripts": [
|
||||||
|
("repartir_taches = repartir_taches.entrypoint:main"),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
Loading…
Reference in a new issue