From 8f1a42785c9967ab524fd995dc488737c2458830 Mon Sep 17 00:00:00 2001 From: David Castex Date: Thu, 22 May 2025 16:43:13 +0200 Subject: [PATCH] =?UTF-8?q?Ajout=20Fonctionnalit=C3=A9s=20et=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit preparer_dossier_travail() formatter_vers_ArcGIS() pour le fichier CSV --- README.md | 9 +- banbou.py | 299 ++++++++++++++++--- test/test_controler_projection.py | 33 ++ test/test_creer_liste.py | 12 +- test/test_projet_calculer_taille.py | 18 ++ test/test_projet_preparer_dossier_travail.py | 16 +- 6 files changed, 344 insertions(+), 43 deletions(-) create mode 100644 test/test_controler_projection.py diff --git a/README.md b/README.md index 6101da5..4367c78 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # NeoBanbou -Script Python d'assistance dans le controle des dossiers de mise en place de fibre optiques. +Script Python d'assistance dans le controle des dossiers de plan de recollement de fibre optiques. ## Lancer les Tests @@ -12,6 +12,13 @@ Pour lancer les tests, il faudra préalablement installer un environnement de tr python -m venv venv ``` +Puis basculer vers cet environnement (a faire à chaque fois qu'on ouvre un shell) +```bash + source venv/Scripts/activate +``` +Sous Windows ce sera `env\Scripts\activate.bat` + + J'ai utiliser Pytest pour les tests et son module pytest-mock ```bash diff --git a/banbou.py b/banbou.py index 16d7bd7..0642000 100644 --- a/banbou.py +++ b/banbou.py @@ -1,4 +1,4 @@ -## Script Banbou pour le prétraitement des données des dossiers d'installation +## Script Banbou pour le prétraitement des données des dossiers de recollement ## de fibre optique. import os, shutil @@ -55,7 +55,7 @@ def creer_liste(dossier): """ Construit une liste avec les fichiers de 'dossier' - Parcourt le dossier et ses sous-dossier, ajoute les fichiers + Parcourt le dossier et ses sous-dossier, ajoute TOUS les fichiers dans une liste de _Fichier. Chaque _Fichier ajouté a ses attributs mis a jour. Retourne la liste. @@ -66,12 +66,163 @@ def creer_liste(dossier): for dossier_courant, list_sousdossiers, list_fichiers in os.walk(dossier): for fichier_courant in list_fichiers: + print(f"fichier courant : {fichier_courant}") ce_Fichier = _Fichier.lire(dossier_courant, fichier_courant) liste.append(ce_Fichier) return liste +# TODO surcharger la fonction print native pour cet affichage +def afficher(liste): + """ + Affiche le nom des fichiers de la liste des _Fichiers + """ + for courant in liste: + print(courant.nom + "." + courant.extension) + + + + + +def controller_projection(id_point, point_x, point_y): + """ + regarde si les coordonnées du point sont bien dans une des projections + autorisées + + Contrainte : + - Pour le moment, ne peut reconnaitre que des projections + de points situés en France métropolitaine (Lambert93 et les 9 zones CC). + - Ce sont toutes des projections coniques depuis le pole nord --> la longitude (axe Ouest Est) n'est pas déformé par ces projections et donc reste valide. + Mais je vais aussi contraindre la longitude à la France métropolitaine, pour pas qu'un point situé en Russie par exemple apparaisse comme valide. + + param user_input: un point de type (int, float, float) TODO: avoir si conversion nécessaire ou faire en chaine de carac + return: Une chaine nommant la projection trouvé sinon "Mauvaise projection" + """ + # Valeurs constantes définie par IGN + projections_conformes = [ + { + "nom" : "GPS", + "E0" : 0, + "N0" : 0, + "fenetreE0" : 180, + "fenetreN0" : 90, + "EPSG" : 4326 # Correspond à la projection des GPS + }, + { + "nom" : "Lambert93", + "E0" : 700000, + "N0" : 6600000, + "fenetreE0" : 600000, + "fenetreN0" : 600000, + "EPSG" : 2154 # Attention la plus recente est 9794 + }, + { + "nom" : "CC42", + "E0" : 1700000, + "N0" : 1200000, + "fenetreE0" : 600000, + "fenetreN0" : 111000, + "EPSG" : 3942 + }, + { + "nom" : "CC43", + "E0" : 1700000, + "N0" : 2200000, + "fenetreE0" : 600000, + "fenetreN0" : 111000, + "EPSG" : 3943 + }, + { + "nom" : "CC44", + "E0" : 1700000, + "N0" : 3200000, + "fenetreE0" : 600000, + "fenetreN0" : 111000, + "EPSG" : 3944 + }, + { + "nom" : "CC45", + "E0" : 1700000, + "N0" : 4200000, + "fenetreE0" : 600000, + "fenetreN0" : 111000, + "EPSG" : 3945 + }, + { + "nom" : "CC46", + "E0" : 1700000, + "N0" : 5200000, + "fenetreE0" : 600000, + "fenetreN0" : 111000, + "EPSG" : 3946 + }, + { + "nom" : "CC47", + "E0" : 1700000, + "N0" : 6200000, + "fenetreE0" : 600000, + "fenetreN0" : 111000, + "EPSG" : 3947 + }, + { + "nom" : "CC48", + "E0" : 1700000, + "N0" : 7200000, + "fenetreE0" : 600000, + "fenetreN0" : 111000, + "EPSG" : 3948 + }, + { + "nom" : "CC49", + "E0" : 1700000, + "N0" : 8200000, + "fenetreE0" : 600000, + "fenetreN0" : 111000, + "EPSG" : 3949 + }, + { + "nom" : "CC50", + "E0" : 1700000, + "N0" : 9200000, + "fenetreE0" : 600000, + "fenetreN0" : 111000, + "EPSG" : 3950 + } + # TODO : ajouter les projections DOMTOMs + ] + + # projection à retourner + projection = "Mauvaise projection" + + # définie la projection conique Nord en regardant dans quelle intervalle la valeur se situe + for elem in projections_conformes: + borne_basse = elem["N0"] - elem["fenetreN0"] + borne_haute = elem["N0"] + elem["fenetreN0"] + if borne_basse < point_y < borne_haute : + projection = elem["nom"] + + print(f"Proj. trouvé : {projection}") + + # Controle la longitude + # Si c'est Lambert93 alors + # TODO: refaire cette partie avec des variables et non des entiers literraux + longitude_correcte = False + match projection: + case "Lambert93": + if 100000 < point_x < 1300000 : + longitude_correcte = True + case "CC42" | "CC43" | "CC44" | "CC45" | "CC46" | "CC47" | "CC48" | "CC49" | "CC50": + if 900000 < point_x < 2300000 : + longitude_correcte = True + case _: + pass + + if not longitude_correcte : + projection = "Mauvaise projection" + print(f"Avertissement : Longitude du point id {id_point} pas en métropole.") + + return projection @@ -99,7 +250,7 @@ class _Fichier: implication="Non-conforme", taille=0): self.nom_original = nom_original # - son nom original - self.chemin = chemin # - son chemin + self.chemin = chemin # - son chemin absolue (dossier+fichier) self.extension = extension # - son extension self.nom = nom # - son nom formaté self.implication = implication # - son implication dans le projet @@ -152,7 +303,7 @@ class _Fichier: - PDFs, DOCs, ODTs (Doc LibreOffice), """ match self.extension: - case "pdf" | "dwg" | "csv" | "doc" | "odt" | "PDF" | "DWG" | "CSV" | "DOC" | "ODT": + case "pdf" | "dwg" | "csv" | "doc" | "docs" | "odt" | "PDF" | "DWG" | "CSV" | "DOC" | "ODT" | "DOCS": self.implication = "Necessaire" case _: self.implication = "A-ignorer" @@ -175,7 +326,9 @@ class _Projet: nb_shemas=0, nb_releves=0, nb_csv=0, - nb_dwgs=0): + nb_dwgs=0, + nb_points = 0, + liste_notifs=[]): self.nom = nom # nom du projet self.date = date # date du traitement self.racine = racine # chemin racine du projet @@ -187,6 +340,8 @@ class _Projet: self.nb_releves = nb_releves # nb de rapports de relevés topo self.nb_csv = nb_csv # nb de fichiers CSV self.nb_dwgs = nb_dwgs # nb de fichiers DWG + self.nb_points = nb_points # nb de points des CSVs + self.liste_notifs = liste_notifs # liste contenant les notifs du VISA def enraciner(self): @@ -204,12 +359,21 @@ class _Projet: d'élements _Fichier. Met à jour l'attribut 'taille' dans le projet """ - taille = 0 + taille = 0.0 for courant in self.liste: if courant.implication in "Necessaire": taille += courant.taille self.taille = taille - print(f"Taille totale : {self.taille / 1024**2:.2} Mo.\n") + # Affichage adapté à la bonne unité + if taille < 1024 : + unite = "octets" + elif taille < 1024**2 : + unite = "Ko" + taille /= 1024 + else : + unite = "Mo" + taille /= 1024**2 + print(f"Taille totale : {taille:.2f} {unite}.\n") def dater_projet(self): """ @@ -249,39 +413,104 @@ class _Projet: # peuplement du dossier Travail avec les fichiers necessaires for fichier in self.liste: - print("TEST001") + source = fichier.chemin + print(f"source : {source}") + # lors du peuplement préfixer et suffixer les noms des fichiers concernés comme suit : "Plan_nomfichierdwg.dwg" et "Point_nomfichiercsv_IN.csv" + # Confirmation par Audrey qu'il n'y a nécessairement qu'un fichier CSV par projet --> Le script s'occupe de le renommer. + # Mais parfois il peut y avoir plusieurs DWGs dont un seul est utile --> PASS -- Je laisse l'opérateur choisir lequel utiliser et le renommer manuellement. + match fichier.extention: + case "csv": + dest = self.racine + "\\" + travail + "\\" + "Point_" + fichier.nom + "_IN" + "." + fichier.extension + + case _: + dest = self.racine + "\\" + travail + "\\" + fichier.nom + "." + fichier.extension + + print(f"dest : {dest}") if fichier.implication in "Necessaire": - print("TEST002") - shutil.copyfile(fichier.chemin + "\\" + fichier.nom_original, self.racine + "\\" + travail + "\\" + fichier.nom) - - - - - - - - - - - - - -# TODO surcharger la fonction print native pour cet affichage -def afficher(liste): - """ - Affiche le nom des fichiers de la liste des _Fichiers - """ - for courant in liste: - print(courant.nom + "." + courant.extension) - - - - + try: + shutil.copyfile( source , dest) + print(fichier.nom.ljust(40,".") + "copié") + except shutil.SameFileError as err : + print(f"Le fichier {fichier.nom + fichier.extension} existe déjà.") + #TODO : + # Confirmation par Audrey qu'il n'y a necessairement qu'un fichier CSV par projet --> On peut le renommer. + # Mais parfois il peut y avoir plusieurs DWGs dont un seul est utile --> PASS -- Je laisse l'opérateur choisir lequel utiliser et le renommer manuellement. + shutil.move() + #TODO : verifier aussi la longueur des noms de fichiers. Notifier si nécessaire + def formatter_vers_ArcGIS(self, fichier): + """ + Lit et formatte un fichier CSV pour son importation dans ArcGIS. + + Lit la 1ère ligne et s'assure de la présence des titres de colonnes, + formatte cette 1ère ligne, supprime les lignes vides puis change le séparateur d'élements en + ',' -- séparateur adapté pour ArcGIS -- + Notifie si nécessaire les points de controles qui ne passent pas. + Renomme le fichier formatté en "Point_fichier_IN.csv" + Contraintes : + - Le fichier CSV NÉCESSITE des ';' comme séparateur d'élements. + - Le fichier doit avoir exactement 5 colonnes. + param user_input: nom complet d'un fichier csv + return: Le nombre de points du csv + """ + # titres des colonnes correctement formattés. + titres = "id_point;TYPE;X;Y;Z" + + # On va traiter ligne par ligne le fichier, puis cette ligne sera ajouté + # a une liste. Finalement, on écrasera chaque ligne du fichier avec cette liste + df = open(fichier) + sortie = [] + + # analyser la premier ligne et formatter cette ligne + ligne = df.readline() + # Si le premier mot est un nombre Alors il manque les titres, dans + # ce cas insérer une ligne. + mots = ligne.split(sep=";", maxsplit=1) + if mots[0].isalpha(): + #insérer ligne + sortie.append(titres) + else: + sortie.append(ligne) + + # la tête de lecture du descripteur de fichier ne reset pas sa position + # donc on peut continuer le parcours des lignes directement + for ligne_courante in df : + # enlever les espaces + ligne_courante = ligne_courante.replace(" ", "") + # si la ligne n'est pas vides alors + # TODO : changer le match en if ligne_courante not in ["", ";;;;"]: + match ligne_courante: + case "" | ";;;;": + pass + case _: + # controler la projection de ce point + id_point, type_, point_x, point_y, *autres = ligne_courante.split(sep=";") + projection = controller_projection(id_point, float(point_x), float(point_y)) + #TODO: notifier si pas bonne projection + + # puis ajouter la ligne a la liste + sortie.append(ligne_courante) + # compter ce point + self.nb_points += 1 + df.close() + + # je reouvre le descripteur en mode w only pour ecrire le fichier + + df + + +# structure représetant des messages à mentionner dans le rapport VISA +# chaque notification possède un texte et sa catégorie concernée. +class _Notification: + def __init__(self, + categorie="Pas de catégorie", + texte="Pas de texte"): + self.categorie = categorie + self.texte = texte @@ -298,7 +527,7 @@ print("Répertoire courant : ".center(18), racine) pas_de_dossier = True for a in os.scandir(): - print("courant scandir() = ", a, " ", a.is_dir()) + print("courant scandir() = ", a.name.ljust(25), " ", a.is_dir()) if a.is_dir(): #il y a un dossier pas_de_dossier = False diff --git a/test/test_controler_projection.py b/test/test_controler_projection.py new file mode 100644 index 0000000..05d49d8 --- /dev/null +++ b/test/test_controler_projection.py @@ -0,0 +1,33 @@ +from banbou import controller_projection + +def test_projection_correcte_metropole() : + sut = ("1004", 1585702, 2185080) + + proj = controller_projection(*sut) + + assert proj == "CC43" + +def test_projection_correcte_avec_des_flottants() : + sut = ("1004", 1585702.06, 2185080.612) + + proj = controller_projection(*sut) + + assert proj == "CC43" + +def test_lattitude_incorrecte() : + sut = ("1004", 1000000, 0) + + proj = controller_projection(*sut) + + assert proj == "Mauvaise projection" + +def test_longitude_incorrecte(capsys) : + sut = ("1004", 1000000, 0) + + proj = controller_projection(*sut) + out, err = capsys.readouterr() + expected_out = "Proj. trouvé : Mauvaise projection\n" + "Avertissement : Longitude du point id 1004 pas en métropole.\n" + + assert proj == "Mauvaise projection" + assert out == expected_out + diff --git a/test/test_creer_liste.py b/test/test_creer_liste.py index b751e26..16c8f6a 100644 --- a/test/test_creer_liste.py +++ b/test/test_creer_liste.py @@ -30,11 +30,13 @@ def test_doit_creer_liste_depuis_un_dossier_complexe(mocker, capsys): sut = creer_liste(dossier) - afficher(sut) - out, err = capsys.readouterr() - expected_out = "DIS.pdf\n" + "DIS_AXR08_PT802161.csv\n" + "DIS_AXR08_PT802161.dwg\n" + "DIS_AXR08_PT802161.pdf\n" + "fichier_a_ignorer_lors_de_la_copie.txt\n" - - assert out == expected_out + # ATTENTION pour ce test VÉRIFIEZ PRÉALABLEMENT qu'un vieux dossier "Travail ne soit pas déja + # créé par les tests précédents. Si tel est le cas EFFACER le dossier "Travail" + assert f"{sut[0].nom}.{sut[0].extension}" == "DIS.pdf" + assert f"{sut[1].nom}.{sut[1].extension}" == "DIS_AXR08_PT802161.csv" + assert f"{sut[2].nom}.{sut[2].extension}" == "DIS_AXR08_PT802161.dwg" + assert f"{sut[3].nom}.{sut[3].extension}" == "DIS_AXR08_PT802161.pdf" + assert f"{sut[4].nom}.{sut[4].extension}" == "fichier_a_ignorer_lors_de_la_copie.txt" diff --git a/test/test_projet_calculer_taille.py b/test/test_projet_calculer_taille.py index b5dfe17..ce0ad2d 100644 --- a/test/test_projet_calculer_taille.py +++ b/test/test_projet_calculer_taille.py @@ -52,7 +52,25 @@ def test_ne_doit_pas_calculer_fichier_non_necessaire(mocker): assert sut.taille == expected_value +def test_doit_afficher_un_arrondi_deux_decimale(mocker, capsys): + class MockResponse: + def __init__(self): + self.taille = 2089457 + self.implication = "Necessaire" + + mocker.patch("banbou._Fichier", return_value = MockResponse()) + + liste = [banbou._Fichier()] + sut = _Projet() + sut.liste = liste + + expected_value = "Taille totale : 1.99 Mo.\n\n" + + sut.calculer_taille() + out, err = capsys.readouterr() + + assert out == expected_value diff --git a/test/test_projet_preparer_dossier_travail.py b/test/test_projet_preparer_dossier_travail.py index d705761..a6abb7d 100644 --- a/test/test_projet_preparer_dossier_travail.py +++ b/test/test_projet_preparer_dossier_travail.py @@ -1,5 +1,5 @@ from banbou import _Projet -import banbou +import banbou, os # Je n'ai pas mocker l'environnement donc prévoir d'effacer # les anciens fichiers et dossier créés par les tests précédents @@ -15,4 +15,16 @@ def test_doit_copier_les_fichiers_necessaires(mocker): sut.racine = dossier sut.liste = banbou.creer_liste(dossier) sut.preparer_dossier_travail() -###ENCOURS \ No newline at end of file + + out = os.walk(dossier + "\\Travail") + liste = [i for i in out] + + # valeurs attendues + sous_dossier_attendue = [] # pas de sous dossier + fichiers_attendues = ['DIS.pdf', 'DIS_AXR08_PT802161.csv', 'DIS_AXR08_PT802161.dwg', 'DIS_AXR08_PT802161.pdf'] + + + assert liste[0][1] == sous_dossier_attendue + assert liste[0][2] == fichiers_attendues + + # TODO: effacer le dossier de "Travail" pour pas interférer avec les autres tests