Ajout Fonctionnalités et tests

preparer_dossier_travail()
formatter_vers_ArcGIS() pour le fichier CSV
This commit is contained in:
David Castex 2025-05-22 16:43:13 +02:00
parent fc70c9f910
commit 8f1a42785c
6 changed files with 344 additions and 43 deletions

View File

@ -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

291
banbou.py
View File

@ -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)
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
# TODO surcharger la fonction print native pour cet affichage
def afficher(liste):
def formatter_vers_ArcGIS(self, fichier):
"""
Affiche le nom des fichiers de la liste des _Fichiers
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
"""
for courant in liste:
print(courant.nom + "." + courant.extension)
# 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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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
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