neobanbou/NEOBANBOU.py

1244 lines
45 KiB
Python

## Version en un seul fichier du script banbou ##
import subprocess
# Vérifier s'il existe une version plus récente de pip
try:
result = subprocess.run(['pip', 'install', '--upgrade', 'pip', '--disable-pip-version-check'], capture_output=True, text=True)
if result.returncode == 0:
print("pip a été mis à jour avec succès.")
except subprocess.CalledProcessError:
print("La mise à jour de pip a échoué.")
bibliotheques = ['os', 'getpass', 're', 'shutil', 'datetime', 'openpyxl']
for bibliotheque in bibliotheques:
try:
__import__(bibliotheque)
except ImportError:
# Installer la bibliothèque
print(f"{bibliotheque} n'est pas installée. Installation en cours...")
subprocess.check_call(['pip', 'install', bibliotheque])
print(f"{bibliotheque} a été installée avec succès.")
import os, getpass
import re # module RegEx
import shutil, datetime
import openpyxl as xls
### CONSTANTES ###
# chemin absolue en dur du modèle
prenom = getpass.getuser()
MODELE = f"C:\\Users\\{prenom}\\Desktop\\Banbou\\VISA_BANBOU.xlsx"
# nom du dossier créé pour l'opérateur
TRAVAIL = "Travail"
# séparateur d'un CSV
SEP = ";"
# regex, motifs à chercher dans une chaine. Explication :
# (MOTIF)+ matche 1 OU PLUS occurence du mot MOTIF
# (?:) le groupe ne sera pas capturé, du coup dans python le groupe ne créé pas de chaine vide si pas de match du groupe en question
# | OR
EXPRESSION = r"(?:fiche)+|(?:relev)+|(?:topo)+|(?:info)+|(?:ouvr)+|(?:\.doc)+|(?:\.dwg)+|(?:\.csv)+|(?:\.odt)+|(?:\.pdf)+|(?:\.doc)+"
# Masques Binaires représentants les controles de chaques catégorie traitées
# les bits sont allumés à chaque position où un test existe
FAIL_CSV = 7 # Explication : il existe pour le moment 3 tests pour les CSVs donc : "0000 0000 0000 0111"
FAIL_DWG = 16 # un seul test pour les DWGs : "0000 0000 0001 0000"
FAIL_PDF = 256 # un seul test pour les PDFs : "0000 0001 0000 0000"
FAIL_INFO = 4096 # un seul test pour les fiches infos : "0001 0000 0000 0000"
# Valeurs constantes définie par IGN
PROJECTIONS = [
{
"nom" : "GPS",
"E0" : 0,
"N0" : 0,
"offsetE0" : 180,
"offsetN0" : 90,
"EPSG" : 4326 # Correspond à la projection des GPS
},
{
"nom" : "Lambert93",
"E0" : 700000,
"N0" : 6600000,
"offsetE0" : 600000,
"offsetN0" : 600000,
"EPSG" : 2154 # Attention la plus recente est 9794
},
{
"nom" : "CC42",
"E0" : 1700000,
"N0" : 1200000,
"offsetE0" : 600000,
"offsetN0" : 111000,
"EPSG" : 3942
},
{
"nom" : "CC43",
"E0" : 1700000,
"N0" : 2200000,
"offsetE0" : 600000,
"offsetN0" : 111000,
"EPSG" : 3943
},
{
"nom" : "CC44",
"E0" : 1700000,
"N0" : 3200000,
"offsetE0" : 600000,
"offsetN0" : 111000,
"EPSG" : 3944
},
{
"nom" : "CC45",
"E0" : 1700000,
"N0" : 4200000,
"offsetE0" : 600000,
"offsetN0" : 111000,
"EPSG" : 3945
},
{
"nom" : "CC46",
"E0" : 1700000,
"N0" : 5200000,
"offsetE0" : 600000,
"offsetN0" : 111000,
"EPSG" : 3946
},
{
"nom" : "CC47",
"E0" : 1700000,
"N0" : 6200000,
"offsetE0" : 600000,
"offsetN0" : 111000,
"EPSG" : 3947
},
{
"nom" : "CC48",
"E0" : 1700000,
"N0" : 7200000,
"offsetE0" : 600000,
"offsetN0" : 111000,
"EPSG" : 3948
},
{
"nom" : "CC49",
"E0" : 1700000,
"N0" : 8200000,
"offsetE0" : 600000,
"offsetN0" : 111000,
"EPSG" : 3949
},
{
"nom" : "CC50",
"E0" : 1700000,
"N0" : 9200000,
"offsetE0" : 600000,
"offsetN0" : 111000,
"EPSG" : 3950
}
# TODO : ajouter les projections DOMTOMs
]
### CLASSES ET FONCTIONS
class _Fichier:
def __init__(self,
#nom_original="Pas de nom original",
chemin="Pas de chemin",
extension="Pas d'extension",
nom= "Pas de nom",
implication="Non-conforme",
categorie="Pas de catégorie",
taille=0):
#self.nom_original = nom_original # - son nom original
self.chemin = chemin # - son chemin absolue (dossier+fichier+extension)
self.extension = extension # - son extension (ecrit en minuscule)
self.nom = nom # - son nom formaté
self.implication = implication # - son implication dans le projet {"Necessaire", "Non-conforme", "A-ignorer"}
self.categorie = categorie # - sa categorie {"CSV", "DWG", "SHEMAS", "TOPO", "Non-definie"}
self.taille = taille # taille en octets
def afficher(self):
"""
Affiche dans la sortie standard les éléments du fichier
"""
print("\nfichier.afficher()")
print("nom :".ljust(16) + self.nom)
#print("nom orig :".ljust(16) + self.nom_original)
print("ext :".ljust(16) + self.extension)
print("chemin :".ljust(16) + self.chemin)
print("implication :".ljust(16) + self.implication)
print("categorie :".ljust(16) + self.categorie)
print("taille :".ljust(16) + str(self.taille) )
def lire(dossier, fichier):
"""
Lit le nom du fichier et du dossier
Construit un élement _Fichier ET met à jour TOUS ses attributs
:param user_input: chemin absolue du dossier, nom du fichier
:return: un element _Fichier
"""
print("\nfichier.lire()")
# initialiser un _Fichier
ce_Fichier = _Fichier()
# son chemin absolue
ce_Fichier.chemin = dossier + "\\" + fichier
# déterminer son nom original et son extension
# vérifie qu'il y est au moins un point dans le nom
if "." in fichier :
*nom_original, ce_Fichier.extension = fichier.rsplit(".", maxsplit=1)
# reconstitue une chaine simple depuis la liste
nom_original = "".join(nom_original)
# lettrer l'extension en minuscule
ce_Fichier.extension = ce_Fichier.extension.casefold()
# formatter et écrire le nom
ce_Fichier.nom = formater(nom_original)
# déterminer son implication et sa categorie
ce_Fichier.implication, ce_Fichier.categorie = impliquer(fichier)
# calculer sa taille
ce_Fichier.taille = os.path.getsize(ce_Fichier.chemin)
print(f"implication : {ce_Fichier.implication} {ce_Fichier.categorie}")
else:
print(f'"{fichier}" est un fichier sans extension --> Non-conforme.')
ce_Fichier.nom = fichier
ce_Fichier.implication = "Non-conforme"
#TODO: voir si il manque pas des données à lire ici pour pas bloquer le reste du prog
return ce_Fichier
class _Notification:
def __init__(self,
categorie="Pas de catégorie",
texte="Pas de texte"):
self.categorie = categorie # vu comme une énumération de l'ensemble {"CSV", "DWG", "PDF", "FRONT"} seule ces valeurs sont donc possibles et traitées par le script
self.texte = texte
def controler(id_point, point_x, point_y):
"""
regarde si les coordonnées d'un 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.
NOTE : On ne controle pas l'élevation (coord Z), ces projections ne prennent pas en compte l'élevation.
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"
"""
#print("\nprojection.controler()")
# projection à retourner
projection = "Mauvaise projection"
# NOTE : Une partie des ensembles de coordonnées de CC47 et de Lambert93 s'intersectionne :
# Lorsqu'un point de coordonnées (X, Y) se situe dans la plage 1100000, 1300000 pour X et
# la plage 3089000, 3311000 pour Y, alors ce point est à la fois valide en CC47 et en Lambert93
# Avant de déterminer la projection, je vais donc vérifier si le point à controler n'est pas dans cette plage
# Et avertir si necessaire
if 6089000 < point_y < 6311000:
if 1100000 < point_x < 1300000:
print(f"AVERTISSEMENT : ID {ID_POINT} : SES COORDONNÉES PEUVENT ÊTRE INTERPRÉTER CORRECTEMENT COMME DU CC47 ET DU LAMBERT93.")
print("DANS LA PLUPART DES CAS, VOUS DEVREZ PROJETER LE FICHIER CSV EN CC47.")
# définie la projection conique Nord en regardant dans quelle intervalle la valeur se situe
for P in PROJECTIONS:
borne_basse = P["N0"] - P["offsetN0"]
borne_haute = P["N0"] + P["offsetN0"]
if borne_basse < point_y < borne_haute :
projection = P["nom"]
#print(f"Proj. conique Nord trouvé : {projection}")
print(f" --> {projection:10}")
# Controle la longitude
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 1100000 < point_x < 2300000 :
longitude_correcte = True
case "GPS":
if -180 < point_x < 180:
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
## Analyse lexicale
def tokeniser(ligne): #Pas utilisé, je le laisse au cas ou.
"""
Tokenise une ligne de texte.
Lit en entrée une "ligne" d'un fichier texte, découpe la ligne en mots,
chaque mot étant séparé par le séparateur "sep".
Attendu : si un champ est vide alors la liste reçoit quand même un
item pour representer ce champ (une chaine vide "" qui peut servir
pour tester sa présence)
Retourne une liste des mots trouvés, ou une liste vide si aucun mot.
"""
sortie = []
# enleve les espaces superflues avant et après la chaine
tempo = ligne.strip()
tempo = tempo.split(sep=SEP)
#affichage test
print("tokeniser")
print(tempo)
# enlever les mots vides
for i in tempo:
print
if i not in '':
sortie.append(i)
return sortie
## Conversion, Formatage
def formater(chaine):
"""Formate selon nomenclature.
Formate la chaine de caractères passée en paramètre :
Enlève tous les accents français. Enlève la cédille du C.
Remplace les espaces ' ', les traits d'union '-' et les ''' apostrophes
par des tirets bas '_'.
Ne traite pas pour le moment le AE et OE ligaturé.
Si plusieurs '_' se suivent, les réduire à un seul.
"""
#TODO : gérer les accents sur majuscules
#TODO : amélioration ou exercice, utiliser la méthode str.translate() et maketrans
resultat = ""
precedent = None
for c in chaine:
match c:
case "à" | "â" | "ä":
resultat+= "a"
case "é" | "è" | "ê" | "ë":
resultat+= "e"
case "î" | "ï":
resultat+= "i"
case "ô" | "ö":
resultat+= "o"
case "ù" | "û" | "ü":
resultat+= "u"
case "ÿ":
resultat+= "y"
case "ç":
resultat+= "c"
case " " | "-" | "_" | "'":
if('_' not in precedent ):
resultat+= "_"
c = "_"
case _:
resultat+= c
precedent = c
return resultat
## Analyse semantique
def impliquer(chaine):
"""
Définir l'implication d'une chaine (attendu : un nom de fichier avec son extension).
Retourne un tuple de str (Necessité, Catégorie).
La necessité peut prendre une des valeurs suivantes :
{"Necessaire", "A-ignorer}.
La catégorie peut prendre une des valeurs suivantes:
{"CSV", "DWG", "SHEMAS", "TOPO", "Non-definie"}.
Il est attendu que lorsqu'un fichier obtient une valeur de catégorie
Alors il est aussi considéré "Necessaire".
Les fichiers "Necessaire" seront les fichiers copiés dans le
répertoire "Travail".
La catégorie est définie par une recherche de motif dans le nom du fichier.
# TODO: voir si elle peut pas être définie en lisant les métadonnées des fichiers
Les fichiers nécessaires sont les DWGs, les CSVs pour les datas.
Pour les shémas et relevés de topo :
- PDFs, DOCs, ODTs (Doc LibreOffice),
"""
print(f"\nimpliquer({chaine})")
# mettre la chaine en minuscule
chaine = chaine.casefold()
# cherche la PREMIERE correspondance du motif (alors que findall() cherche toutes les correspondances)
motif = re.search(EXPRESSION, chaine) # re.search() retourne un objet Match si trouvé sinon None
if motif:
# print("Il y a un match")
motif = motif.group() # Match.group() permet de retourner la valeur matché
else:
# print("Pas de match")
pass
match motif :
# On va traiter les mots clés d'abord
case "fiche" | "relev" | "topo" | "ouvr" | "info" | ".doc" | ".odt" :
return ("Necessaire", "TOPO")
case ".dwg" :
return ("Necessaire", "DWG")
case ".csv" :
return ("Necessaire", "CSV")
case ".pdf" :
return ("Necessaire", "SHEMAS")
case _ :
return ("A-ignorer","Non-definie")
## Fonctions Input Utilisateur
def saisir_annee_topo():
"""
demande à l'utilisateur de saisir l'année du relevé topographique.
Demande à l'utilisateur l'année affichée dans le relevé topograhique
du projet.
Lit la valeur saisi dans l'entrée standard.
Les fichiers n'étant pas encore créés dans le dossier "Travail" à
ce moment du programme, l'utilisateur doit donc ouvrir la fiche
du relevé topographique dans le dossier original.
Pour ne pas être bloquant (cas ou l'année n'est pas trouvable),
la saisi de '0' permet de ne rien mettre.
Retourne la valeur saisi.
"""
print("\nVeuillez saisir l'année de la date du relevé géo référencé.")
print("Pour cela ouvrez le relevé topographique du dossier/sous-dossier original.")
correct = False
while(not correct):
saisi = input("Saisir 4 chiffres (ou 0 pour aucune date) : ")
if saisi in "0":
return ""
correct = saisi.isdecimal() and len(saisi) == 4
# Confirmation demandé
confirm = input(f"{saisi} : Etes vous sur (Y/N) ? ")
if confirm not in ["Y", "y", "O", "o"] :
saisir_annee_topo()
return saisi
class _Projet:
def __init__(self,
nom="Pas de nom",
date="Pas de date",
racine="Pas de chemin",
fichiers=[],
nb_fichiers=0,
taille=0,
rapport="Pas de fichier",
nb_shemas=0,
nb_releves=0,
nb_csvs=0,
points=[],
nb_points=0,
nb_dwgs=0,
notifs=[],
controles=0):
self.nom = nom # nom du projet
self.date = date # date du traitement
self.racine = racine # chemin racine du projet
self.fichiers = fichiers # liste de _Fichier
self.nb_fichiers = nb_fichiers # nb de fichiers dans "Travail"
self.taille = taille # taille des fichiers dans "Travail"
self.rapport = rapport # chemin vers le visa
self.nb_shemas = nb_shemas # nb de plans de la mise en place
self.nb_releves = nb_releves # nb de rapports de relevés topo
self.nb_csvs = nb_csvs # nb de fichiers CSV
self.points = points # liste des points du CSV
self.nb_points = nb_points # nb de points des CSVs TODO: REDONDANT avec len(points)
self.nb_dwgs = nb_dwgs # nb de fichiers DWG
self.notifs = notifs # liste contenant les notifs du VISA
self.controles = controles # variable drapeau contenant toutes les validations de controles
# (voir Document pour plus d'info sur sa représentation)
def notifier(self, categorie, texte):
"""
Ajoute une notification au _Projet
La catégorie est soit "PDF", "CSV", "DWG" pour notifier dans la
feuille correspondante. Une chaine personnalisée notifiera dans la
feuille FRONT.
"""
print("\n_Projet.notifier()")
print(f"Incident ajouté :{texte} ")
incident = _Notification( categorie, texte)
self.notifs.append(incident)
def lister_fichiers(self, dossier):
"""
Construit une liste avec les fichiers de 'dossier'
- Parcours le dossier et ses sous-dossiers, ajoute TOUS les fichiers
dans une liste de _Fichier.
- Chaque _Fichier ajouté a ses attributs mis a jour.
- Les nombres des différentes catégories de fichiers nécessaires
au projet sont mis a jour.
:param user_input: nom complet du dossier
:return: liste d'élements de type _Fichier
"""
print("\n_Projet.lister_fichiers()")
self.fichiers = []
print("Création liste de _Fichier...")
for dossier_courant, list_sousdossiers, list_fichiers in os.walk(dossier):
for fichier_courant in list_fichiers:
ce_Fichier = lire(dossier_courant, fichier_courant)
self.fichiers.append(ce_Fichier)
print(f'"{fichier_courant}" ajouté.')
# mettre à jour les nbs des catégories de fichiers necessaires
if ce_Fichier.implication in "Necessaire":
### sa categorie {"CSV", "DWG", "SHEMAS", "TOPO", "Non-definie"}
match ce_Fichier.categorie :
case "CSV":
self.nb_csvs += 1
case "DWG":
self.nb_dwgs += 1
case "SHEMAS":
self.nb_shemas += 1
case "TOPO":
self.nb_releves += 1
case _:
pass
print("Fin création liste.")
# TODO surcharger la fonction print native pour cet affichage
def afficher_liste(liste):
"""
Affiche le nom des fichiers de la liste des _Fichiers
"""
print("\nprojet.afficher_liste()")
for courant in liste:
print("\n" + courant.nom + "." + courant.extension)
def enraciner(projet):
"""
récupère le repertoire de travail (working directory) courant
met à jour l'attribut 'racine' d'un projet
"""
print("\nprojet.enraciner()")
projet.racine = os.getcwd()
print("Racine : ".ljust(16), f"{projet.racine}")
def calculer_taille(projet):
"""
calcule la taille des fichiers necessaires d'un liste
d'élements _Fichier.
Met à jour l'attribut 'taille' dans le projet
"""
print("\nprojet.calculer_taille()")
taille = 0.0
for courant in projet.fichiers:
if courant.implication in "Necessaire":
taille += courant.taille
projet.taille = taille
# 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}.")
def dater(projet):
"""
recupère la date du jour
met à jour l'attribut 'date' du projet
"""
print("\nprojet.dater()")
projet.date = datetime.datetime.today().strftime('%Y%m%d')
print(f"Date : {projet.date}")
def nommer(projet):
"""
Met à jour l'attribut 'nom' en composant un nom.
Le nom est constitué du NOM du dossier racine ET de la DATE
courante formatté.
"""
print("\nprojet.nommer()")
projet.nom = f"{os.path.basename(os.getcwd())}_{projet.date}"
projet.nom = formater(projet.nom)
print(f'Nom : "{projet.nom}"')
def preparer_dossier_travail(projet):
"""
Créer un dossier "Travail" dans la racine du working directory et
le peuple des fichiers nécessaires
Préfixe et suffixe les fichiers le nécessitant (le CSV et le DWG)
"""
print("\nprojet.preparer_dossier_travail()")
#travail = "Travail"
# création du dossier "Travail" et de ses sous-dossiers "
_chemin = f"{projet.racine}\\{TRAVAIL}"
#nomemclaturer les noms des sousdossiers
sousdossier = f"{_chemin}\\{projet.nom}"
# pour le moment tous les sous dossiers sont
dossierCSVs = _chemin
dossierDWGs = _chemin
dossierPDFs = _chemin
dossierTOPOs = _chemin
print(f'Chemin du dossier de préparation : "{_chemin}"')
try:
os.mkdir(_chemin)
print (f'Dossier "{TRAVAIL}" créé.')
os.mkdir(sousdossier) # dossier pour le projet ARCGIS
if projet.nb_csvs > 1:
dossierCSVs = f"{sousdossier}_CSV"
os.mkdir(dossierCSV)
print (f'Dossier "CSV" créé.')
if projet.nb_dwgs > 1:
dossierDWGs = f"{sousdossier}_DWG"
os.mkdir(dossierDWGs)
print (f'Dossier "DWG" créé.')
if projet.nb_shemas > 1:
dossierPDFs = f"{sousdossier}_PDF"
os.mkdir(dossierPDFs)
print (f'Dossier "PDF" créé.')
if projet.nb_releves > 1:
dossierTOPOs = f"{sousdossier}_TOPO"
os.mkdir(dossierTOPOs)
print (f'Dossier "TOPO" créé.')
except FileExistsError as erreur:
print(f'AVERTISSEMENT: LE DOSSIER "{TRAVAIL}" EXISTE DÉJA. SUPPRIMER LE, PUIS RELANCER LE SCRIPT SVP.')
purger_et_finir_programme(projet)
except OSError as erreur:
print(f"FICHIER NON TROUVÉ. SUREMENT UN PB DE CHEMIN EN AMONT.")
purger_et_finir_programme(projet)
# peuplement du dossier Travail avec les fichiers necessaires
print("Copie des fichiers nécessaires et nomenclature de leurs noms...")
# demande l'annnée du revelé topo à l'utilisateur
annee_topo = saisir_annee_topo()
# nom du dossier projet
dossier = f"{os.path.basename(os.getcwd())}"
dossier = formater(dossier)
print(f"dossier : {dossier}")
# les suffixes ajoutés en cas de plusieurs fichiers dans la même catégorie
letterCSV = "\0" if projet.nb_csvs <= 1 else "A" # condition ternaire en python
letterDWG = "\0" if projet.nb_dwgs <= 1 else "A"
letterSHEMAS = "\0" if projet.nb_shemas <= 1 else "A"
letterTOPO = "\0" if projet.nb_releves <= 1 else "A"
for fichier in projet.fichiers:
if fichier.implication in "Necessaire":
source = fichier.chemin
print(f"source : {source}")
# fabrication du nom du fichier de destination
dest = _chemin + "\\"
# 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.
#{"CSV", "DWG", "SHEMAS", "TOPO", "Non-definie"}
#ANCIENNE VERSION
#-----------------------------------------------------------------
# match fichier.categorie :
# case "CSV":
# dest = f"{dossierCSVs}\\Point_{fichier.nom}_IN"
# case "DWG":
# dest = f"{dossierDWGs}\\Plan_{fichier.nom}"
# case "SHEMAS":
# dest = f"{dossierPDFs}\\{fichier.nom}"
# case "TOPO":
# dest = f"{dossierTOPOs}\\{fichier.nom}"
# case _:
# pass # TODO: voir pour les autres cas, normalement il n'y en a pas pour le moment
#-----------------------------------------------------------------
#{"CSV", "DWG", "SHEMAS", "TOPO", "Non-definie"}
match fichier.categorie :
case "CSV":
if projet.nb_csvs <= 1 :
dest = f"{dossierCSVs}\\{dossier}_IN_Points_{annee_topo}"
else :
dest = f"{dossierCSVs}\\{dossier}_IN_Points_{annee_topo}_{letterCSV}"
letterCSV = chr(ord(letterCSV)+1) # on incremente la lettre
case "DWG":
if projet.nb_dwgs <= 1 :
dest = f"{dossierDWGs}\\{dossier}_Plan_{annee_topo}"
else :
dest = f"{dossierDWGs}\\{dossier}_Plan_{annee_topo}_{letterDWG}"
letterDWG = chr(ord(letterDWG)+1)
case "SHEMAS":
if projet.nb_shemas <= 1 :
dest = f"{dossierPDFs}\\{dossier}_Plan_{annee_topo}"
else :
dest = f"{dossierPDFs}\\{dossier}_Plan_{annee_topo}_{letterSHEMAS}"
letterSHEMAS = chr(ord(letterSHEMAS)+1)
case "TOPO":
if projet.nb_releves <= 1 :
dest = f"{dossierTOPOs}\\{dossier}_Infos_{annee_topo}"
else :
dest = f"{dossierTOPOs}\\{dossier}_Infos_{annee_topo}_{letterTOPO}"
letterTOPO = chr(ord(letterTOPO)+1)
case _:
pass # TODO: voir pour les autres cas, normalement il n'y en a pas pour le moment
# renomme le fichier en conséquence
fichier.nom = os.path.basename(dest)
dest = dest + f".{fichier.extension}"
print(f"dest : {dest}")
if fichier.implication in "Necessaire":
try:
shutil.copyfile( source , dest)
print("" + fichier.nom.ljust(40,".") + "copié")
except shutil.SameFileError as err :
print(f"Le fichier existe déjà.")
def controler_longueur_noms(projet):
"""
controle la longueur des noms des fichiers du projet.
Notifie si necessaire les noms trop longs
"""
#print("\nprojet.controler_longueur_noms()")
for fichier in projet.fichiers:
match fichier.extension:
case "dwg":
if len(fichier.nom) > 38 : # 40 - 2 si lettres de suffixe "_A", "_B", etc
projet.notifier("DWG", f'"{fichier.nom}.dwg" : Nom trop long.')
case "csv" :
if len(fichier.nom) > 43 :
projet.notifier("CSV", f'"{fichier.nom}.csv" : Nom trop long.')
case "pdf" :
if len(fichier.nom) > 43 :
projet.notifier("PDF", f'"{fichier.nom}.pdf" : Nom trop long.')
case _:
if len(fichier.nom) > 45 :
projet.notifier("FRONT", f'"{fichier.nom}.{fichier.extension}" : Nom trop long.')
def formater_vers_ArcGIS(projet, fichier_entree, fichier_sortie):
"""
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.
Change le séparateur décimal ',' en '.' (necessaire car sinon je ne peux convertir les strings en float)
Notifie si nécessaire les points de controles qui ne passent pas.
Met à jour l'attribut nb_points
Contraintes :
- Le fichier CSV NÉCESSITE des ';' comme séparateur d'élements. (Normalement c'est toujours le cas de toutes façon)
- Le fichier doit avoir exactement 5 colonnes. TODO : vois si plus de souplesse avec le catchage de paramètres restants
param user_input: nom complet d'un fichier csv
NOTE: Ne pas formatter le fichier original, mais celui qui est deja copié dans le dossier Travail
"""
print("\nprojet.formatter_vers_ArcGIS()")
# titres des colonnes correctement formattés.
titres = "id_point;TYPE;X;Y;Z\n"
sortie = [titres] # le fichier de sortie ( representé comme une liste de lignes ) contient un premiere ligne de titre
df = open(fichier_entree, "r")
ligne = df.readline()
# analyser la premiere ligne et formatter cette ligne
# 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)
print(f"Mots[0] = {mots[0]}")
if mots[0].isnumeric():
#insérer ligne
sortie.append(ligne)
projet.nb_points += 1
# 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 :
# lit et controle la ligne courante
courant = lire_et_controler_ligne(projet, ligne_courante)
# ajoute la ligne
if courant not in "":
sortie.append(f"{courant}\n")
# compter ce point
projet.nb_points += 1
df.close()
# je reouvre le descripteur en mode w only pour ecrire le fichier
df = open(fichier_sortie, "w")
# écriture
for ligne in sortie:
#print(f"{ligne}")
df.write(ligne) # NOTE: write()
df.close()
print("Fin formatage du CSV.")
def lire_et_controler_ligne(projet, ligne):
"""
Analyse une chaine de caractère (concrètement, une ligne du CSV)/
remplace les séparateurs décimaux ',' par '.'
Notifie (dans le projet lié) les points de controles qui ne passent pas.
Met à jour l'attribut nb_points du projet lié
Met à jour la variable de controles du projet lié
Contraintes :
- Le fichier CSV NÉCESSITE des ';' comme séparateur d'élements.
:return: une chaine de carac formatté pour ArcGIS
"""
#print("\nprojet.lire_et_controler_ligne()")
#print(f'ENTREE : "{ligne}"')
# enlever les espaces
ligne = ligne.replace(" ", "")
ligne = ligne.replace(",", ".")
ligne = ligne.replace("\n", "")
# si la ligne n'est pas vides alors
# TODO : changer le match en if ligne not in ["", ";;;;"]:
match ligne:
case ";;;;;;" | ";;;;;" | ";;;;" | ";;;" | ";;" | ";" | "" :
ligne = ""
case _:
# controler la projection de ce point
# NOTE: split() tokenise la ligne en items (des strings), si un champ est manquant, alors il tokenise une chaine vide( "" )
# On peut donc tester cette chaine vide pour savoir si une champ est manquant.abs
# Je vais dont controler la présence du champ ID, car il apparait que parfois il est manquant. Si tel est le cas,
# Notifier et terminer le programme --> dossier FAIL
#
# NOTE: split() n'enleve pas le \n de la fin de ligne dans la chaine --> ligne = ligne.replace("\n", "") que j'ai rajouter au dessus
try :
id_point, type_, point_x, point_y, point_z, *autres = ligne.split(sep=";")
except ValueError:
print("LE FICHIER SOURCE CSV EST MAL FORMATÉ, IL DOIT MANQUER DES CHAMPS ET DE SÉPARATEURS DE CHAMPS.")
purger_et_finir_programme(projet)
#PRINT de controle
print(f'ID:"{id_point}", TYPE:"{type_}", X:"{point_x}", Y:"{point_y}", Z:"{point_z}", *autres={autres}')
# controler qu'il n'y est pas un champ vide pour id_point, TYPE, X et Y. (Z étant optionnel)
# Si tel est le cas, on quitte FAIL
if id_point in "":
print('AVERTISSEMENT : COLONNE "ID_POINT" MANQUANTE.')
purger_et_finir_programme(projet)
if type_ in "":
print('AVERTISSEMENT : COLONNE "TYPE" MANQUANTE.')
purger_et_finir_programme(projet)
if point_x in "":
print('AVERTISSEMENT : COLONNE "X" MANQUANTE.')
purger_et_finir_programme(projet)
if point_y in "":
print('AVERTISSEMENT : COLONNE "Y" MANQUANTE.')
purger_et_finir_programme(projet)
if point_z in "":
print('AVERTISSEMENT : COLONNE "Z" MANQUANTE.')
purger_et_finir_programme(projet)
# controler que id_point est un entier
# Si c'est un flottant alors ya un pb, on quitte FAIL
try :
if float(id_point)/int(id_point) != 1.0 :
print("AVERTISSEMENT : LA PREMIÈRE COLONNE EST UN FLOTTANT. ATTENDU UN ID_POINT DE VALEUR ENTIÈRE.")
print('IL DOIT MANQUER LA COLONNE "ID_POINT AINSI QUE LE SÉPARATEUR DE CHAMP ";".')
purger_et_finir_programme(projet)
except ValueError :
print("AVERTISSEMENT : LA COLONNE ID_POINT N'A PAS UN NOMBRE.")
print(f'ATTENDU UN ENTIER. TROUVÉ ID_POINT DE TYPE "{type(id_point)}"')
purger_et_finir_programme(projet)
# A partir d'ici il est attendu que la tokenisation se soit bien déroulée
try :
projo = controler(id_point, float(point_x), float(point_y))
except ValueError :
print("AVERTISSEMENT : UNE COLLONNE (X OU Y) EST MAL CONVERTIE.")
print(f'ATTENDU VALEURS DÉCIMALES. TROUVÉ X : DE TYPE "{type(point_x)}", Y DE TYPE "{type(point_y)}"')
purger_et_finir_programme(projet)
# notifier si pas bonne projection
if projo in "Mauvaise projection":
incident = _Notification("CSV", f"Point ID {id_point} : Mauvaise projection")
projet.notifs.append(incident)
projet.controles |= 2**2
# controle la présence d'ancien type de point et notifie
if type_ in ["AUT", "ABCE"] :
incident = _Notification("CSV", f"Point ID {id_point} : Ancien TYPE : {type_}")
projet.notifs.append(incident)
#print(f'SORTIE : "{ligne}"')
return ligne
def remplir(modele, projet):
"""
Complète le Visa du projet à partir d'un fichier modele.
Controle le nb de fichiers de chaque catégorie et notifie.
Parcours la liste de notifications du projet et ajoute chaque
notification dans la feuille concerné du classeur
A défaut, une notification de la liste qui n'a pas de catégorie
avec sa propre feuille sera ajoutée à la première feuille (FRONT)
Enregistre le Visa dans le repertoire de Travail
Contraintes :
- le modele du VISA doit être NÉCESSAIREMENT dans le même
dossier que le script. TODO : traité l'exception si fichier non trouvé
- le modele du VISA NE doit PAS avoir eu de modification notable
dans son design ( emplacements des cellules utilisées pour
notifier, nom des feuilles, etc)
"""
print("\nvisa.remplir()")
# creation d'un workbook à partir d'un modele
classeur = xls.load_workbook(modele)
## definition personnalisé vers chaque feuille du classeur
FRONT = classeur[classeur.sheetnames[0]]
PDF = classeur[classeur.sheetnames[1]]
CSV = classeur[classeur.sheetnames[2]]
DWG = classeur[classeur.sheetnames[3]]
# Nom dossier
# Date de reception
# Date Visa
FRONT['C1'].value = f'Analyse : {projet.nom}\nDate de réception :\nDate visa : {projet.date}'
# Taille du dossier
taille = projet.taille
# Affichage adapté à la bonne unité
if taille < 1024 :
unite = "octets"
elif taille < 1024**2 :
unite = "Ko"
taille /= 1024
else :
unite = "Mo"
taille /= 1024**2
FRONT['C10'].value = f"Taille totale : {taille:.2f} {unite}."
# TODO: factoriser les instructions ci dessous (pas sur...)
### FRONT DWG
# Je ne peux pas controler automatiquement la projection du DWG (c'est un binaire illisible hors de AutoCAD)
# J'ajoute donc automatiquement la mention comme dans le script précedent.
FRONT['C3'].value = f"{projet.nb_dwgs} fichier(s) DWG présent(s).\nProjection de tous les fichiers DWG : RFG93-CC43 (EPSG:3943)."
# Controle le nb de fichiers DWG
if projet.nb_dwgs == 0:
projet.controles |= 2**4 # allume le bit 5ième bit
FRONT['C3'].value = "Pas de fichier DWG présent."
# Regarde si des fails ont été détectés sur les DWGs
fail = projet.controles & FAIL_DWG # operation logique avec le masque
# notifie en consequence
if fail :
FRONT['B3'].value = "FAIL"
else :
FRONT['B3'].value = "OK"
### FRONT PDF
FRONT['C4'].value = f"{projet.nb_shemas} fichier(s) PDF présent(s)."
# Controle le nb de fichiers PDF
if projet.nb_shemas == 0:
projet.controles |= 2**8 # allume le 9ième bit
FRONT['C4'] = "Pas de fichier PDF présent."
# Regarde si des fails ont été détectés sur les PDFs
fail = projet.controles & FAIL_PDF
# notifie en consequence
if fail :
FRONT['B4'].value = "FAIL"
else :
FRONT['B4'].value = "OK"
### FRONT CSV
FRONT['C5'].value = f"1 fichier(s) CSV présent(s).\n{projet.nb_points} point(s) en RFG93-CC43 (EPSG:3943)."
# Controle le nb de fichiers CSV
if projet.nb_csvs == 0:
projet.controles |= 2**0 # allume le 1er bit
FRONT['C5'].value = "Pas de fichier CSV présent."
# Regarde si des fails ont été détectés sur les CSVs
fail = projet.controles & FAIL_CSV
# notifie en consequence
if fail :
FRONT['B5'].value = "FAIL"
else :
FRONT['B5'].value = "OK"
# FRONT FICHE INFO TOPOLOGIE
FRONT['C6'].value = f"{projet.nb_releves} fiche(s) Info. présente(s)."
# Controle le nb de fichiers Fiche Topologique
if projet.nb_releves == 0:
projet.controles |= 2**12 # allume le 13ième bit
FRONT['C6'].value = "Pas de fiche d'Info. Topologie présente."
# Regarde si des fails ont été détectés sur les Fiches Topo
fail = projet.controles & FAIL_INFO
# notifie en consequence
if fail :
FRONT['B6'].value = "FAIL"
else :
FRONT['B6'].value = "OK"
# Notifier dans les autres Feuilles
# lignes en cours à remplir pour chaque feuille
# initialisé aux lignes où on va commencer à notifier
A = 2 # "B2" # Pour la feuille PDF
B = 2 # "B2" # Pour la feuille CSV
C = 2 # "B2" # Pour la feuille DWG
D = 12 # "C12" # Pour la feuille FRONT
cell = "" # cellule courante qui va être notifier
for notif in projet.notifs :
match notif.categorie :
case "PDF" :
cell = PDF["B"+str(A)]
A += 1
case "CSV" :
cell = CSV["B"+str(B)]
B += 1
case "DWG" :
cell = DWG["B"+ str(C)]
C += 1
case _:
cell = FRONT["C"+str(D)]
D += 1
cell.value = notif.texte
# Ici le VISA doit être correctement rempli
# on sauvegarde dans un fichier le classeur
classeur.save(f"Travail\\{projet.nom}_VISA.xlsx")
def purger_et_finir_programme(projet):
"""
Efface le dossier "Travail" si il existe puis termine le programme.
Cette fonction est utilisée quand des expections sont levées et
qu'elles nécessitent la fermeture prématurée du programme.
"""
print("\npurger_et_finir_programme()")
print("\nFIN DE PROGRAMME.\n")
# Ne fonctionne pas car des descripteurs de fichiers sont utilisés au moment de la demande
## shutil.rmtree(f"{projet.racine}\\{TRAVAIL}" , ignore_errors=True)
quit()
## NOTE : Pour traiter le cas des sous-dossiers lors du dézippage
# il vaut mieux que l'opérateur copie le fichier banbou.py dans chaque
# sous-dossiers puis l'execute.
## Donc Il est attendu que :
# - le fichier script se situe DANS le dossier contenant que
# les fichiers originaux.
# - le modele du visa soit dans un autre dossier BIEN SPÉCIFIÉ
# (je vais remettre un chemin absolue en dur "Desktop\Banbou"
# car suivant l'opérateur, le dézippage crée des sous-sous-dossiers,
# et serait problématique pour un chemin relatif)
# Création entité projet et mise à jour de ses attributs
ce_Projet = _Projet()
enraciner(ce_Projet)
dater(ce_Projet)
nommer(ce_Projet)
# controle de la longueur du nom du projet
if len(ce_Projet.nom) >=46 :
print(f"\n{ce_Projet.nom} EST UN NOM DE DOSSIER TROP LONG (+ DE 46 CARACTÈRES). VEUILLEZ RACCOURCIR SON NOM.")
purger_et_finir_programme(ce_Projet)
# explorer le dossier Trouvé et fabriquer la liste des fichiers nécessaires
ce_Projet.lister_fichiers(ce_Projet.racine)
# calcule la taille des fichiers nécessaires
calculer_taille(ce_Projet)
# Ici le projet doit avoir toutes les données nécessaires pour fabriquer
# et remplir les dossiers attendus
preparer_dossier_travail(ce_Projet)
# Règle nouvellement ajouté : il faut normalement UN SEUL fichier CSV,
# l'opérateur ne devrait plus fusionner manuellement des fichiers CSV.
if ce_Projet.nb_csvs > 1:
print("\nAVERTISSEMENT : IL Y A PLUSIEURS FICHIERS CSV.")
print("VEUILLEZ NE GARDER QU'UN SEUL FICHIER CSV DANS LE DOSSIER ORIGINAL.")
purger_et_finir_programme(ce_Projet)
# On formatte le/les fichier/s CSV pour ArcGIS
for fichier in ce_Projet.fichiers:
if fichier.categorie in "CSV":
entree = ce_Projet.racine + "\\Travail\\" + fichier.nom + ".csv"
sortie = ce_Projet.racine + "\\Travail\\" + fichier.nom + ".csv"
formater_vers_ArcGIS(ce_Projet, entree, sortie)
# On controle les noms des fichiers copiés
controler_longueur_noms(ce_Projet)
# On fabrique le VISA
#modele = ce_Projet.racine + "\\VISA_BANBOU.xlsx"
remplir(MODELE, ce_Projet)
#Fin de Programme Attendu
print("\nProgramme terminé correctement. ")