optimisation migration abs to assiduites (WIP)

This commit is contained in:
iziram 2023-02-09 21:04:53 +01:00
parent e18990d804
commit 53c9658ce1
4 changed files with 305 additions and 112 deletions

View File

@ -81,13 +81,11 @@ class Assiduite(db.Model):
) -> object or int:
"""Créer une nouvelle assiduité pour l'étudiant"""
# Vérification de non duplication des périodes
assiduites: list[Assiduite] = etud.assiduites.all()
if is_period_conflicting(date_debut, date_fin, assiduites):
assiduites: list[Assiduite] = etud.assiduites
if is_period_conflicting(date_debut, date_fin, assiduites, Assiduite):
raise ScoValueError(
"Duplication des assiduités (la période rentrée rentre en conflit avec une assiduité enregistrée)"
)
if moduleimpl is not None:
# Vérification de l'existence du module pour l'étudiant
if moduleimpl.est_inscrit(etud):
@ -114,6 +112,32 @@ class Assiduite(db.Model):
return nouv_assiduite
@classmethod
def fast_create_assiduite(
cls,
etudid: int,
date_debut: datetime,
date_fin: datetime,
etat: EtatAssiduite,
moduleimpl_id: int = None,
description: str = None,
entry_date: datetime = None,
) -> object or int:
"""Créer une nouvelle assiduité pour l'étudiant"""
# Vérification de non duplication des périodes
nouv_assiduite = Assiduite(
date_debut=date_debut,
date_fin=date_fin,
etat=etat,
etudid=etudid,
moduleimpl_id=moduleimpl_id,
desc=description,
entry_date=entry_date,
)
return nouv_assiduite
class Justificatif(db.Model):
"""
@ -185,8 +209,8 @@ class Justificatif(db.Model):
) -> object or int:
"""Créer un nouveau justificatif pour l'étudiant"""
# Vérification de non duplication des périodes
justificatifs: list[Justificatif] = etud.justificatifs.all()
if is_period_conflicting(date_debut, date_fin, justificatifs):
justificatifs: list[Justificatif] = etud.justificatifs
if is_period_conflicting(date_debut, date_fin, justificatifs, Justificatif):
raise ScoValueError(
"Duplication des justificatifs (la période rentrée rentre en conflit avec un justificatif enregistré)"
)
@ -202,11 +226,35 @@ class Justificatif(db.Model):
return nouv_justificatif
@classmethod
def fast_create_justificatif(
cls,
etudid: int,
date_debut: datetime,
date_fin: datetime,
etat: EtatJustificatif,
raison: str = None,
entry_date: datetime = None,
) -> object or int:
"""Créer un nouveau justificatif pour l'étudiant"""
nouv_justificatif = Justificatif(
date_debut=date_debut,
date_fin=date_fin,
etat=etat,
etudid=etudid,
raison=raison,
entry_date=entry_date,
)
return nouv_justificatif
def is_period_conflicting(
date_debut: datetime,
date_fin: datetime,
collection: list[Assiduite or Justificatif],
collection_cls: Assiduite or Justificatif,
) -> bool:
"""
Vérifie si une date n'entre pas en collision
@ -215,12 +263,15 @@ def is_period_conflicting(
date_debut = localize_datetime(date_debut)
date_fin = localize_datetime(date_fin)
unified = [
uni
for uni in collection
if is_period_overlapping(
(date_debut, date_fin), (uni.date_debut, uni.date_fin), bornes=False
)
]
return len(unified) != 0
if (
collection.filter_by(date_debut=date_debut, date_fin=date_fin).first()
is not None
):
return True
count: int = collection.filter(
collection_cls.date_debut < date_fin, collection_cls.date_fin > date_debut
).count()
return count > 0

43
app/profiler.py Normal file
View File

@ -0,0 +1,43 @@
from time import time
from datetime import datetime
class Profiler:
OUTPUT: str = "/tmp/scodoc.profiler.csv"
def __init__(self, tag: str) -> None:
self.tag: str = tag
self.start_time: time = None
self.stop_time: time = None
def start(self):
self.start_time = time()
return self
def stop(self):
self.stop_time = time()
return self
def elapsed(self) -> float:
return self.stop_time - self.start_time
def dates(self) -> tuple[datetime, datetime]:
return datetime.fromtimestamp(self.start_time), datetime.fromtimestamp(
self.stop_time
)
def write(self):
with open(Profiler.OUTPUT, "a") as file:
dates: tuple = self.dates()
date_str = (dates[0].isoformat(), dates[1].isoformat())
file.write(f"\n{self.tag},{self.elapsed() : .2}")
@classmethod
def write_in(cls, msg: str):
with open(cls.OUTPUT, "a") as file:
file.write(f"\n# {msg}")
@classmethod
def clear(cls):
with open(cls.OUTPUT, "w") as file:
file.write("")

View File

@ -39,6 +39,7 @@ from hashlib import md5
import numbers
import os
import re
from shutil import get_terminal_size
import _thread
import time
import unicodedata
@ -88,6 +89,60 @@ ETATS_INSCRIPTION = {
}
def printProgressBar(
iteration,
total,
prefix="",
suffix="",
finish_msg="",
decimals=1,
length=100,
fill="",
autosize=False,
):
"""
Affiche une progress bar à un point donné (mettre dans une boucle pour rendre dynamique)
@params:
iteration - Required : index du point donné (Int)
total - Required : nombre total avant complétion (eg: len(List))
prefix - Optional : Préfix -> écrit à gauche de la barre (Str)
suffix - Optional : Suffix -> écrit à droite de la barre (Str)
decimals - Optional : nombres de chiffres après la virgule (Int)
length - Optional : taille de la barre en nombre de caractères (Int)
fill - Optional : charactère de remplissange de la barre (Str)
autosize - Optional : Choisir automatiquement la taille de la barre en fonction du terminal (Bool)
"""
percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total)))
color = ProgressBarColors.RED
if 50 >= float(percent) > 25:
color = ProgressBarColors.MAGENTA
if 75 >= float(percent) > 50:
color = ProgressBarColors.BLUE
if 90 >= float(percent) > 75:
color = ProgressBarColors.CYAN
if 100 >= float(percent) > 90:
color = ProgressBarColors.GREEN
styling = f"{prefix} |{fill}| {percent}% {suffix}"
if autosize:
cols, _ = get_terminal_size(fallback=(length, 1))
length = cols - len(styling)
filledLength = int(length * iteration // total)
bar = fill * filledLength + "-" * (length - filledLength)
print(f"\r{color}{styling.replace(fill, bar)}{ProgressBarColors.RESET}", end="\r")
# Affiche une nouvelle ligne vide
if iteration == total:
print(f"\n{finish_msg}")
class ProgressBarColors:
BLUE = "\033[94m"
CYAN = "\033[96m"
GREEN = "\033[92m"
MAGENTA = "\033[95m"
RED = "\033[91m"
RESET = "\033[0m"
class BiDirectionalEnum(Enum):
"""Permet la recherche inverse d'un enum
Condition : les clés et les valeurs doivent être uniques

View File

@ -1,77 +1,33 @@
# Script de migration des données de la base "absences" -> "assiduites"/"justificatifs"
import shutil
from app import db
from app.profiler import Profiler
from app.models import (
Assiduite,
Justificatif,
Absence,
Identite,
ModuleImpl,
ModuleImplInscription,
Departement,
)
from app.scodoc.sco_utils import EtatAssiduite, EtatJustificatif, localize_datetime
from app.scodoc.sco_utils import (
EtatAssiduite,
EtatJustificatif,
localize_datetime,
ProgressBarColors,
printProgressBar,
)
from datetime import time, datetime, date
from json import dump
class glob:
class _glob:
DUPLICATIONS_ASSIDUITES: dict[tuple[date, bool, int], Assiduite] = {}
DUPLICATIONS_JUSTIFICATIFS: dict[tuple[date, bool, int], Justificatif] = {}
DUPLICATED: list[Justificatif] = []
class bcolors:
BLUE = "\033[94m"
CYAN = "\033[96m"
GREEN = "\033[92m"
MAGENTA = "\033[95m"
RED = "\033[91m"
RESET = "\033[0m"
def printProgressBar(
iteration,
total,
prefix="",
suffix="",
finish_msg="",
decimals=1,
length=100,
fill="",
autosize=False,
):
"""
Affiche une progress bar à un point donné (mettre dans une boucle pour rendre dynamique)
@params:
iteration - Required : index du point donné (Int)
total - Required : nombre total avant complétion (eg: len(List))
prefix - Optional : Préfix -> écrit à gauche de la barre (Str)
suffix - Optional : Suffix -> écrit à droite de la barre (Str)
decimals - Optional : nombres de chiffres après la virgule (Int)
length - Optional : taille de la barre en nombre de caractères (Int)
fill - Optional : charactère de remplissange de la barre (Str)
autosize - Optional : Choisir automatiquement la taille de la barre en fonction du terminal (Bool)
"""
percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total)))
color = bcolors.RED
if 50 > float(percent) > 25:
color = bcolors.MAGENTA
if 75 > float(percent) > 50:
color = bcolors.BLUE
if 90 > float(percent) > 75:
color = bcolors.CYAN
if 100 >= float(percent) > 90:
color = bcolors.GREEN
styling = f"{prefix} |{fill}| {percent}% {suffix}"
if autosize:
cols, _ = shutil.get_terminal_size(fallback=(length, 1))
length = cols - len(styling)
filledLength = int(length * iteration // total)
bar = fill * filledLength + "-" * (length - filledLength)
print(f"\r{color}{styling.replace(fill, bar)}{bcolors.RESET}", end="\r")
# Affiche une nouvelle ligne vide
if iteration == total:
print(f"\n{finish_msg}")
PROBLEMS: dict[int, list[str]] = {}
CURRENT_ETU: list = []
MODULES: list[tuple[int, int]] = []
COMPTE: list[int, int] = []
def migrate_abs_to_assiduites(
@ -95,6 +51,11 @@ def migrate_abs_to_assiduites(
.entry_date: datetime -> timestamp d'entrée de l'abs
.etudid: relation -> Identite
"""
Profiler.clear()
time_elapsed: Profiler = Profiler("migration")
time_elapsed.start()
if morning is None:
pref_time_morning = time(8, 0)
else:
@ -115,42 +76,77 @@ def migrate_abs_to_assiduites(
absences_query = Absence.query
if dept is not None:
depts_id = [dep.id for dep in Departement.query.filter_by(acronym=dept).all()]
absences_query = absences_query.filter(Absence.etudid.in_(depts_id))
absences: list[Absence] = absences_query.order_by(Absence.jour).all()
glob.DUPLICATED = []
glob.DUPLICATIONS_ASSIDUITES = {}
dept: Departement = Departement.query.filter_by(acronym=dept).first()
if dept is not None:
etuds_id: list[int] = [etud.id for etud in dept.etudiants]
absences_query = absences_query.filter(Absence.etudid.in_(etuds_id))
absences: Absence = absences_query.order_by(Absence.etudid)
absences_len: int = len(absences)
_glob.DUPLICATED = []
_glob.DUPLICATIONS_ASSIDUITES = {}
_glob.DUPLICATIONS_JUSTIFICATIFS = {}
_glob.PROBLEMS = {}
_glob.CURRENT_ETU = []
_glob.MODULES = []
_glob.COMPTE = [0, 0]
absences_len: int = absences.count()
print(
f"{ProgressBarColors.BLUE}{absences_len} absences vont être migrées{ProgressBarColors.RESET}"
)
printProgressBar(0, absences_len, "Progression", "effectué", autosize=True)
for i, abs in enumerate(absences):
if abs.estabs:
generated = _from_abs_to_assiduite(
abs, pref_time_morning, pref_time_noon, pref_time_evening
)
try:
if abs.estabs:
generated = _from_abs_to_assiduite(
abs, pref_time_morning, pref_time_noon, pref_time_evening
)
if not isinstance(generated, str):
db.session.add(generated)
_glob.COMPTE[0] += 1
except Exception as e:
if abs.id not in _glob.PROBLEMS:
_glob.PROBLEMS[abs.id] = []
_glob.PROBLEMS[abs.id].append(e.args[0])
if abs.estjust:
generated = _from_abs_to_justificatif(
abs, pref_time_morning, pref_time_noon, pref_time_evening
try:
if abs.estjust:
generated = _from_abs_to_justificatif(
abs, pref_time_morning, pref_time_noon, pref_time_evening
)
if not isinstance(generated, str):
db.session.add(generated)
_glob.COMPTE[1] += 1
except Exception as e:
if abs.id not in _glob.PROBLEMS:
_glob.PROBLEMS[abs.id] = []
_glob.PROBLEMS[abs.id].append(e.args[0])
if i % 10 == 0:
printProgressBar(
i,
absences_len,
"Progression",
"effectué",
autosize=True,
)
if not isinstance(generated, str):
db.session.add(generated)
printProgressBar(
i,
absences_len,
"Progression",
"effectué",
autosize=True,
)
if i % 1000 == 0:
printProgressBar(
i,
absences_len,
"Progression",
"effectué",
autosize=True,
)
db.session.commit()
dup_assi = glob.DUPLICATED
dup_assi = _glob.DUPLICATED
assi: Assiduite
for assi in dup_assi:
assi.moduleimpl_id = None
@ -164,9 +160,28 @@ def migrate_abs_to_assiduites(
"Progression",
"effectué",
autosize=True,
finish_msg=f"{bcolors.GREEN}Les absences ont bien été migrées.{bcolors.RESET}",
finish_msg=f"{ProgressBarColors.GREEN}Les absences ont bien été migrées.{ProgressBarColors.RESET}",
)
time_elapsed.stop()
print(
f"{ProgressBarColors.GREEN}La migration a pris {time_elapsed.elapsed():.2f} secondes {ProgressBarColors.RESET}"
)
print(
f"{ProgressBarColors.RED}Il y a eu {len(_glob.PROBLEMS)} absences qui n'ont pas pu être migrée."
)
print(
f"Vous retrouverez un fichier json {ProgressBarColors.GREEN}/tmp/scodoc_migration_abs.json{ProgressBarColors.RED} contenant les ids des absences ainsi que les erreurs liées."
)
with open("/tmp/scodoc_migration_abs.json", "w", encoding="utf-8") as file:
dump(_glob.PROBLEMS, file)
print(
f"{ProgressBarColors.CYAN}{_glob.COMPTE[0]} assiduités et {_glob.COMPTE[1]} justificatifs ont été générés.{ProgressBarColors.RESET}"
)
# afficher nombre justificatifs généré par rapport au nombre de justificatifs
def _from_abs_to_assiduite(
_abs: Absence, morning: time, noon: time, evening: time
@ -174,6 +189,7 @@ def _from_abs_to_assiduite(
etat = EtatAssiduite.ABSENT
date_deb: datetime = None
date_fin: datetime = None
if _abs.matin:
date_deb = datetime.combine(_abs.jour, morning)
date_fin = datetime.combine(_abs.jour, noon)
@ -183,37 +199,54 @@ def _from_abs_to_assiduite(
date_deb = localize_datetime(date_deb)
date_fin = localize_datetime(date_fin)
duplicata: Assiduite = glob.DUPLICATIONS_ASSIDUITES.get(
duplicata: Assiduite = _glob.DUPLICATIONS_ASSIDUITES.get(
(_abs.jour, _abs.matin, _abs.etudid)
)
if duplicata is not None:
glob.DUPLICATED.append(duplicata)
_glob.DUPLICATED.append(duplicata)
return "Duplicated"
desc: str = _abs.description
entry_date: datetime = _abs.entry_date
etud: Identite = Identite.query.filter_by(id=_abs.etudid).first()
moduleimpl: ModuleImpl = ModuleImpl.query.filter_by(id=_abs.moduleimpl_id).first()
if _abs.etudid not in _glob.CURRENT_ETU:
etud: Identite = Identite.query.filter_by(id=_abs.etudid).first()
if etud is None:
return "No Etud"
_glob.CURRENT_ETU.append(_abs.etudid)
retour = Assiduite.create_assiduite(
etud=etud,
moduleimpl_id: int = _abs.moduleimpl_id
if (
moduleimpl_id is not None
and (_abs.etudid, _abs.moduleimpl_id) not in _glob.MODULES
):
moduleimpl_inscription: ModuleImplInscription = (
ModuleImplInscription.query.filter_by(
moduleimpl_id=_abs.moduleimpl_id, etudid=_abs.etudid
).first()
)
if moduleimpl_inscription is None:
raise Exception("Moduleimpl_id incorrect ou étudiant non inscrit")
retour = Assiduite.fast_create_assiduite(
etudid=_abs.etudid,
date_debut=date_deb,
date_fin=date_fin,
etat=etat,
moduleimpl=moduleimpl,
moduleimpl_id=moduleimpl_id,
description=desc,
entry_date=entry_date,
)
glob.DUPLICATIONS_ASSIDUITES[(_abs.jour, _abs.matin, _abs.etudid)] = retour
_glob.DUPLICATIONS_ASSIDUITES[(_abs.jour, _abs.matin, _abs.etudid)] = retour
return retour
def _from_abs_to_justificatif(
_abs: Absence, morning: time, noon: time, evening: time
) -> Justificatif:
etat = EtatJustificatif.VALIDE
date_deb: datetime = None
date_fin: datetime = None
@ -227,13 +260,23 @@ def _from_abs_to_justificatif(
date_deb = localize_datetime(date_deb)
date_fin = localize_datetime(date_fin)
duplicata: Justificatif = _glob.DUPLICATIONS_JUSTIFICATIFS.get(
(_abs.jour, _abs.matin, _abs.etudid)
)
if duplicata is not None:
return "Duplicated"
desc: str = _abs.description
entry_date: datetime = _abs.entry_date
etud: Identite = Identite.query.filter_by(id=_abs.etudid).first()
if _abs.etudid not in _glob.CURRENT_ETU:
etud: Identite = Identite.query.filter_by(id=_abs.etudid).first()
if etud is None:
return "No Etud"
_glob.CURRENT_ETU.append(_abs.etudid)
retour = Justificatif.create_justificatif(
etud=etud,
retour = Justificatif.fast_create_justificatif(
etudid=_abs.etudid,
date_debut=date_deb,
date_fin=date_fin,
etat=etat,
@ -241,4 +284,5 @@ def _from_abs_to_justificatif(
entry_date=entry_date,
)
_glob.DUPLICATIONS_JUSTIFICATIFS[(_abs.jour, _abs.matin, _abs.etudid)] = retour
return retour