diff --git a/CHANGELOG.md b/CHANGELOG.md index bc6effc..bbe52e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +### [v6.0] 2020-07-29 +- Lecture de la liste des membres à partir d'un fichier Excel +- Écriture de la liste des membres décédés dans un fichier Excel +- Vider le texte de l'IHM lorsqu'on clique sur OK +- Fichier BAT pour installer et lancer l'IHM sous Windows +- BUG: le traitement rend la main à l'IHM losque terminé +- BUG: la barre de progression est remise à zero entre chaque traitement + ### [v5.0] 2020-07-17 - Mise en place d'une interface graphique - Ajout d'un fichier install.py diff --git a/install.py b/install.py index 307696c..7e71ccb 100644 --- a/install.py +++ b/install.py @@ -26,6 +26,27 @@ except ImportError: stdout=subprocess.PIPE, stderr=subprocess.PIPE) print(p.communicate()) -url = "https://git.roflcopter.fr/sdjgeek/purge-registres-deces-insee/-/archive/v5.0/purge-registres-deces-insee-v5.0.zip" -myfile = requests.get(url) -open('purge-registres-deces-insee-v5.0.zip', 'wb').write(myfile.content) +try: + import pandas +except ImportError: + print("Installing pandas") + p = subprocess.Popen([sys.executable, "-m", "pip", "install", "-U", "pandas"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print(p.communicate()) + +try: + import xlrd +except ImportError: + print("Installing xlrd") + p = subprocess.Popen([sys.executable, "-m", "pip", "install", "-U", "xlrd"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print(p.communicate()) + +try: + import numpy +except ImportError: + print("Installing numpy") + p = subprocess.Popen([sys.executable, "-m", "pip", "install", "-U", "numpy"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print(p.communicate()) + diff --git a/purge-registres-deces-insee/excel_in.py b/purge-registres-deces-insee/excel_in.py new file mode 100644 index 0000000..bc54af6 --- /dev/null +++ b/purge-registres-deces-insee/excel_in.py @@ -0,0 +1,54 @@ +""" +Copyright (c) 2020 Sdj Geek +Voir le fichier LICENSE + +Classe d'accès aux données du site de l'Église + +""" + +import os +import pandas as pd +import numpy as np + +from membre_base import MembreBase, MembreProvider + + +class ExcelIn(MembreProvider): + + def __init__(self, excel_path): + self.excel_path = excel_path + self.dataframe = None + + def __len__(self): + return len(self.dataframe) + + class Member(MembreBase): + + def __init__(self, provider, row): + super().__init__(provider) + self.r_id = str(row["id"]) + self.r_first_name = str(row["first_name"]).strip().split(' ')[0].upper() + self.r_last_name = str(row["last_name"]).strip().split(' ')[0].upper() + self.r_maiden_name = str(row["maiden_name"]).strip().split(' ')[0].upper() if type(row["maiden_name"]) == np.str else None + self.r_annee = str(row["annee"]) + self.r_mois = str(row["mois"]) + self.r_jour = str(row["jour"]) + self.r_ville = "" + self.r_sexe = "F" if str(row["status"]) == "Female" else "M" + + def get_name(self): + return os.path.basename(self.excel_path) + + def load(self): + self.dataframe = pd.read_excel(self.excel_path, skiprows=[0, 1], usecols="B:E,G:I,K", header=None, + names=["last_name", "first_name", "maiden_name", "id", "jour", "mois", "annee", + "status"]) + return len(self.dataframe) + + def get_member_list(self): + for index, row in self.dataframe.iterrows(): + try: + yield self.Member(self, row) + except ValueError: + print(f"Error with member [{row}]") + continue diff --git a/purge-registres-deces-insee/excel_out.py b/purge-registres-deces-insee/excel_out.py new file mode 100644 index 0000000..60bd01d --- /dev/null +++ b/purge-registres-deces-insee/excel_out.py @@ -0,0 +1,30 @@ +""" +Copyright (c) 2020 Sdj Geek +Voir le fichier LICENSE + +Classe d'accès aux données INSEE dans la base SQLite + +""" + +import pandas as pd + + +class ExcelOut: + + def __init__(self, out_path): + self.out_path = out_path + self.data = list() + + def add_member(self, member): + self.data.append({ + 'nom_registres': member.get_nom_registres(), + 'nom_insee': member.get_nom_insee(), + 'mrn': member.r_id, + 'death_year': member.i_annee_deces, + 'death_month': member.i_mois_deces, + 'death_day': member.i_jour_deces + }) + + def generate_output(self): + df = pd.DataFrame(self.data) + df.to_excel(self.out_path) diff --git a/purge-registres-deces-insee/gui_trouver_decedes.py b/purge-registres-deces-insee/gui_trouver_decedes.py index 69c716a..0e4fd54 100644 --- a/purge-registres-deces-insee/gui_trouver_decedes.py +++ b/purge-registres-deces-insee/gui_trouver_decedes.py @@ -37,38 +37,50 @@ class MainApplication(tk.Frame): self.entry_bdd_insee.grid(row=0, column=1, sticky='ew') self.button_bdd_insee = tk.Button(self.frame, text="...", command=self.command_button_bdd_insee) self.button_bdd_insee.grid(row=0, column=2, sticky='w') + + # Sélection fichier Excel + self.label_fichier_excel = tk.Label(self.frame, text="Fichier Excel") + self.label_fichier_excel.grid(row=1, column=0, sticky='e') + self.value_fichier_excel = tk.StringVar() + self.entry_fichier_excel = tk.Entry(self.frame, state='disabled', textvariable=self.value_fichier_excel) + self.entry_fichier_excel.grid(row=1, column=1, sticky='ew') + self.button_fichier_excel = tk.Button(self.frame, text="...", command=self.command_button_fichier_excel) + self.button_fichier_excel.grid(row=1, column=2, sticky='w') # Sélection répertoire sortie self.label_dir_out = tk.Label(self.frame, text="Répertoire de sortie") - self.label_dir_out.grid(row=1, column=0, sticky='e') + self.label_dir_out.grid(row=2, column=0, sticky='e') self.value_dir_out = tk.StringVar() self.entry_dir_out = tk.Entry(self.frame, state='disabled', textvariable=self.value_dir_out) - self.entry_dir_out.grid(row=1, column=1, sticky='ew') + self.entry_dir_out.grid(row=2, column=1, sticky='ew') self.button_dir_out = tk.Button(self.frame, text="...", command=self.command_button_dir_out) - self.button_dir_out.grid(row=1, column=2, sticky='w') + self.button_dir_out.grid(row=2, column=2, sticky='w') # Sélection des unités à traiter self.label_units = tk.Label(self.frame, text="Unités à purger") - self.label_units.grid(row=2, column=0, sticky='e') + self.label_units.grid(row=3, column=0, sticky='e') self.value_units = tk.StringVar() self.entry_units = tk.Entry(self.frame, textvariable=self.value_units) - self.entry_units.grid(row=2, column=1, sticky='ew', columnspan="2") + self.entry_units.grid(row=3, column=1, sticky='ew', columnspan="2") # Bouton validation self.button_valid = tk.Button(self.frame, text="Ok", command=self.command_button_valid) - self.button_valid.grid(row=3, column=0, columnspan=3) + self.button_valid.grid(row=4, column=0, columnspan=3) # Barre de progression self.progressbar = ttk.Progressbar(self.frame, orient=tk.HORIZONTAL, mode='determinate') - self.progressbar.grid(row=4, column=0, columnspan=3, sticky='ew') + self.progressbar.grid(row=5, column=0, columnspan=3, sticky='ew') # Affichage des logs self.text_log = tk.Text(self.frame, state='disabled') - self.text_log.grid(row=0, column=3, rowspan=5, sticky='nesw') + self.text_log.grid(row=0, column=3, rowspan=6, sticky='nesw') def command_button_bdd_insee(self): self.value_bdd_insee.set(tkfiledialog.askopenfilename(title="Fichier de l'INSEE")) + def command_button_fichier_excel(self): + self.value_fichier_excel.set(tkfiledialog.askopenfilename(title="Fichier Excel")) + def command_button_dir_out(self): self.value_dir_out.set(tkfiledialog.askdirectory(title="Répertoire de sortie")) @@ -77,6 +89,11 @@ class MainApplication(tk.Frame): self.text_log.insert(tk.END, text) self.text_log.configure(state='disabled') + def clear_log(self): + self.text_log.configure(state='normal') + self.text_log.delete('1.0', tk.END) + self.text_log.configure(state='disabled') + def watch(self): if self.run: if self.pipe.poll(): @@ -84,47 +101,52 @@ class MainApplication(tk.Frame): if message.get('step', False): self.progressbar.step(message['step']) elif message.get('text', False): - print(message['text']) self.add_log(message['text']) - elif message.get('running', False): + elif 'running' in message: self.run = message['running'] elif message.get('set_max', False): + self.progressbar["value"] = 0 self.progressbar['maximum'] = message['set_max'] self.parent.after(100, self.watch) else: - print("Recherche terminée") self.add_log("\nRecherche terminée\n") self.button_valid.configure(state='normal') def command_button_valid(self): self.button_valid.configure(state='disabled') + self.clear_log() self.run = True (conn1, conn2) = Pipe() self.pipe = conn1 - Worker(conn2, self.value_bdd_insee.get(), self.value_dir_out.get(), self.value_units.get().split(',')).start() + unite = self.value_units.get().split(',') + if unite == ['']: + unite = None + Worker(conn2, self.value_bdd_insee.get(), self.value_fichier_excel.get(), self.value_dir_out.get(), unite).start() self.watch() class Worker(Process): - def __init__(self, pipe, bdd_insee, dir_out, units): + def __init__(self, pipe, bdd_insee, fichier_excel, dir_out, units): Process.__init__(self) self.pipe = pipe self.bdd_insee = bdd_insee + self.fichier_excel = fichier_excel self.dir_out = dir_out self.units = units def tracker(self, step=None, text=None, set_max=None, running=None): - if step: + if step is not None: self.pipe.send({'step': step}) - elif text: + elif text is not None: self.pipe.send({'text': text}) - elif set_max: + elif set_max is not None: self.pipe.send({'set_max': set_max}) - elif done: + elif running is not None: self.pipe.send({'running': running}) def run(self): trouver_decedes(chemin_base_donnees=self.bdd_insee, + excel_path=self.fichier_excel, chemin_repertoire_sortie=self.dir_out, numeros_unites=self.units, tracker=self.tracker) diff --git a/purge-registres-deces-insee/membre_base.py b/purge-registres-deces-insee/membre_base.py new file mode 100644 index 0000000..14e179e --- /dev/null +++ b/purge-registres-deces-insee/membre_base.py @@ -0,0 +1,91 @@ +""" +Copyright (c) 2020 Sdj Geek +Voir le fichier LICENSE + +""" + +from abc import ABC, abstractmethod + + +class MembreBase(ABC): + + def __init__(self, provider): + self.provider = provider + # Données issues des registres + self.r_id = None + self.r_first_name = None + self.r_last_name = None + self.r_maiden_name = None + self.r_annee = None + self.r_mois = None + self.r_jour = None + self.r_ville = None + self.r_sexe = None + + # Données issues de l'INSEE + self.i_first_name = None + self.i_last_name = None + self.i_annee_naissance = None + self.i_mois_naissance = None + self.i_jour_naissance = None + self.i_ville_naissance = None + self.i_annee_deces = None + self.i_mois_deces = None + self.i_jour_deces = None + self.i_ville_deces = None + + def get_nom_registres(self): + nom_registres = f"{self.r_last_name}, {self.r_first_name}" + if self.r_maiden_name: + nom_registres = f"{nom_registres} née {self.r_maiden_name.upper()}" + return nom_registres + + def get_nom_insee(self): + return f"{self.i_last_name}, {self.i_first_name}" + + def set_insee(self, insee): + self.i_first_name = insee.first_name + self.i_last_name = insee.last_name + self.i_annee_naissance = insee.annee_naissance + self.i_mois_naissance = insee.mois_naissance + self.i_jour_naissance = insee.jour_naissance + self.i_ville_naissance = insee.code_lieu_naissance + self.i_annee_deces = insee.annee_deces + self.i_mois_deces = insee.mois_deces + self.i_jour_deces = insee.jour_deces + self.i_ville_deces = insee.code_lieu_deces + + def get_texte_decede(self): + if self.r_sexe == "F": + feminin = "e" + elif self.r_sexe == "M": + feminin = "" + else: + feminin = "(e)" + return f""" +Le membre {self.get_nom_registres()} ({self.r_id}), +né{feminin} le {self.r_jour:0>2}/{self.r_mois:0>2}/{self.r_annee:0>4} à {self.r_ville.upper()} +semble être décédé{feminin}. +Dans le fichier de l'INSEE on peut trouver {self.get_nom_insee()} +né{feminin} le {self.i_jour_naissance:0>2}/{self.i_mois_naissance:0>2}/{self.i_annee_naissance:0>4} à {self.i_ville_naissance} +décédé{feminin} le {self.i_jour_deces:0>2}/{self.i_mois_deces:0>2}/{self.i_annee_deces:0>4} à {self.i_ville_deces} +""" + + +class MembreProvider(ABC): + + @abstractmethod + def load(self): + pass + + @abstractmethod + def get_name(self): + pass + + @abstractmethod + def __len__(self): + pass + + @abstractmethod + def get_member_list(self): + pass diff --git a/purge-registres-deces-insee/site_eglise.py b/purge-registres-deces-insee/site_eglise.py index 48e5731..9a69a3c 100644 --- a/purge-registres-deces-insee/site_eglise.py +++ b/purge-registres-deces-insee/site_eglise.py @@ -9,29 +9,77 @@ Classe d'accès aux données du site de l'Église import requests import browser_cookie3 +from membre_base import MembreBase, MembreProvider -class SiteEglise: - def __init__(self, cookie_path=None): +class SiteEglise(MembreProvider): + + def __init__(self, unite, cookie_path=None): """ - + :param unite: numéro de l'unité (paroisse, branche) :param cookie_path: chemin vers le répertoire où inscrire les fichiers de sortie """ + self.unite = unite self.cookie_jar = browser_cookie3.firefox(cookie_file=cookie_path) + self.as_json = None - def get_member_list(self, unit_number): - """Recevoir la liste des membres + def __len__(self): + return len(self.as_json) - :param unit_number: numéro de l'unité (paroisse, branche) - :returns: la liste des membres sous forme d'objet JSON + class Membre(MembreBase): - """ + def __init__(self, provider, data): + super().__init__(provider) + self.completed = False + # Données fournies + self.r_id = data['legacyCmisId'] + self.r_last_name, self.r_first_name = data['nameListPreferredLocal'].upper().split(',') + self.r_last_name = self.r_last_name.strip().split(' ')[0] + self.r_first_name = self.r_first_name.strip().split(' ')[0] + self.r_annee, self.r_mois, self.r_jour = data['birth']['date']['date'].split('-') + self.r_sexe = data['sex'] + # Rechercher le nom de jeune fille si besoin + if data['isSpouse']: + self.complete() + + def complete(self): + if not self.completed: + member_profile = self.provider.get_member_profile(self.r_id) + self.r_maiden_name = member_profile['individual']['maidenNameGroup']['name1']['family'].strip().split(' ')[0].upper() + self.r_ville = member_profile['individual']['birthPlace'] + if not self.r_ville: + self.r_ville = "" + self.completed = True + + def set_insee(self, insee): + self.complete() + super().set_insee(insee) + + def get_name(self): + return f"Unité_{self.unite}" + + def load(self): r = requests.get('https://lcr.churchofjesuschrist.org/services/umlu/report/member-list', - params={'lang': "fra", 'unitNumber': unit_number}, + params={'lang': "fra", 'unitNumber': self.unite}, headers={'Accept': "application/json"}, cookies=self.cookie_jar) r.raise_for_status() - return r.json() + self.as_json = r.json() + return len(self.as_json) + + def get_member_list(self): + """Recevoir la liste des membres + + + :returns: la liste des membres sous forme d'objet JSON + + """ + for member in self.as_json: + try: + yield self.Membre(self, member) + except ValueError: + print(f"Error with member [{member['nameListPreferredLocal']}, {member['birth']['date']['date']}]") + continue def get_member_profile(self, member_id): """Recevoir les informations sur un membre diff --git a/purge-registres-deces-insee/trouver_decedes.py b/purge-registres-deces-insee/trouver_decedes.py index 8b4ec39..48b0dbb 100755 --- a/purge-registres-deces-insee/trouver_decedes.py +++ b/purge-registres-deces-insee/trouver_decedes.py @@ -15,91 +15,83 @@ import os from bdd_insee import BddInsee from site_eglise import SiteEglise +from excel_in import ExcelIn +from excel_out import ExcelOut -def default_tracker(step=None, text=None, set_max=None): +def default_tracker(step=None, text=None, set_max=None, running=None): if text: print(text) -def trouver_decedes(chemin_base_donnees, numeros_unites, chemin_repertoire_sortie, cookie_path=None, tracker=None): +def trouver_decedes(chemin_base_donnees, numeros_unites, chemin_repertoire_sortie, cookie_path=None, excel_path=None, + tracker=None): """Recherche les personnes décédées dans les registres :param chemin_base_donnees: chemin vers le fichier SQLite :param numeros_unites: liste des numéros d'unités à analyser :param chemin_repertoire_sortie: chemin vers le répertoire où inscrire les fichiers de sortie :param cookie_path: chemin vers la base de donnée des cookies + :param excel_path: chemin vers le fichier Excel contenant la liste des membres à rechercher + :param tracker: Objet permettant de suivre l'avancée du traitement """ # Tracker par défaut if tracker is None: tracker = default_tracker - # Initialiser les accès aux données (INSEE et site de l'Église) + # Initialiser les accès aux données INSEE base_insee = BddInsee(chemin_base_donnees) - site_eglise = SiteEglise(cookie_path) - # Boucler sur la liste des unités - for unite in numeros_unites: - tracker(text=f"Unité {unite}") + # Initialiser les fournisseurs de liste de membres + fournisseurs_membres = list() + if excel_path: + fournisseurs_membres.append(ExcelIn(excel_path)) + if numeros_unites: + for unite in numeros_unites: + fournisseurs_membres.append(SiteEglise(unite, cookie_path)) + # Boucler sur la liste fournisseurs + for member_provider in fournisseurs_membres: + tracker(set_max=member_provider.load()) + tracker(text=f"Recherche dans {member_provider.get_name()}\n") # Récupérer la liste des membres - members = site_eglise.get_member_list(unite) - - # Préparer le fichier de sortie - output_file = os.path.join(chemin_repertoire_sortie, f"liste_membres_decedes_unite_{unite}.txt") - with open(output_file, 'w') as out_file: - out_file.write("Les lieux dans le fichier de l'INSEE sont donnés en Code Officiel Géographique en vigueur au moment de la prise en compte du décès\n") - # Boucler sur la liste des membres - tracker(set_max=len(members)) - for member in members: - # Lire les noms et date de naissance - name_registre = member['nameListPreferredLocal'] - full_birthdate = member['birth']['date']['date'] - maiden_name = None - ville_registre = None - # S'il s'agit d'une femme mariée, trouver son nom de jeune fille - if member['isSpouse']: - member_profile = site_eglise.get_member_profile(member['legacyCmisId']) - maiden_name = member_profile['individual']['maidenNameGroup']['name1']['family'] - ville_registre = member_profile['individual']['birthPlace'] - try: - last_name, first_name = name_registre.split(',') - annee_registre, mois_registre, jour_registre = full_birthdate.split('-') - except ValueError: - print(f"Error with member [{name_registre}, {full_birthdate}]") - continue - first_name = first_name.strip().split(' ')[0] - last_name = last_name.strip().split(' ')[0] - query = base_insee.find_person(first_name, last_name, maiden_name, annee_registre, mois_registre, jour_registre) - name_registre = name_registre.upper() - if maiden_name: - name_registre = f"{name_registre} née {maiden_name.upper()}" - if member['sex'] == "F": - feminin = "e" - else: - feminin = "" - for person in query: - if not ville_registre: - member_profile = site_eglise.get_member_profile(member['legacyCmisId']) - ville_registre = member_profile['individual']['birthPlace'] - if not ville_registre: - ville_registre = "" - text = f""" -Le membre {name_registre}, -né{feminin} le {jour_registre:0>2}/{mois_registre:0>2}/{annee_registre:0>4} à {ville_registre.upper()} -semble être décédé{feminin}. -Dans le fichier de l'INSEE on peut trouver {person.last_name}, {person.first_name} -né{feminin} le {person.jour_naissance:0>2}/{person.mois_naissance:0>2}/{person.annee_naissance:0>4} à {person.code_lieu_naissance} -décédé{feminin} le {person.jour_deces:0>2}/{person.mois_deces:0>2}/{person.annee_deces:0>4} à {person.code_lieu_deces} -""" - tracker(text=text) - out_file.write(text) - tracker(step=1) + members = member_provider.get_member_list() + # Préparer les fichiers de sortie + output_base_name = os.path.join(chemin_repertoire_sortie, f"liste_membres_decedes_{member_provider.get_name()}") + output_txt = open(output_base_name + ".txt", 'w') + output_xls = ExcelOut(output_base_name + ".xlsx") + output_txt.write("Les lieux dans le fichier de l'INSEE sont donnés en Code Officiel Géographique en vigueur au moment de la prise en compte du décès\n") + # Boucler sur la liste des membres + for member in members: + query = base_insee.find_person(member.r_first_name, member.r_last_name, member.r_maiden_name, + member.r_annee, member.r_mois, member.r_jour) + for person in query: + member.set_insee(person) + text = member.get_texte_decede() + tracker(text=text) + output_txt.write(text) + output_xls.add_member(member) + tracker(step=1) + # Clore les fichiers de sortie + output_txt.close() + output_xls.generate_output() + tracker(running=False) if __name__ == "__main__": parser = argparse.ArgumentParser(description='Recherche des personnes décédées dans les registres.') parser.add_argument('chemin_base_donnees', type=str, help="chemin vers la base de données") parser.add_argument('chemin_repertoire_sortie', type=str, help="chemin vers le répertoire de sortie") - parser.add_argument('numeros_unites', type=str, help="numéros des l'unités à traiter, séparés par des virgules (ex: 123,753,469)") - parser.add_argument('--cookie', '-c', type=str, help='chemin vers la base de donnée des cookies de Firefox') + parser.add_argument('--numeros_unites', '-u', type=str, help="numéros des l'unités à traiter, séparés par des virgules (ex: 123,753,469)") + parser.add_argument('--cookie', '-c', type=str, help="chemin vers la base de donnée des cookies de Firefox") + parser.add_argument('--excel', '-e', type=str, help="chemin vers le fichier Excel contenant la liste des membres à rechercher") args = parser.parse_args() - trouver_decedes(args.chemin_base_donnees, args.numeros_unites.split(','), args.chemin_repertoire_sortie, args.cookie) + + if args.numeros_unites: + numeros_unites = args.numeros_unites.split(',') + else: + numeros_unites = list() + + trouver_decedes(chemin_base_donnees=args.chemin_base_donnees, + numeros_unites=numeros_unites, + chemin_repertoire_sortie=args.chemin_repertoire_sortie, + cookie_path=args.cookie, + excel_path=args.excel) diff --git a/requirements.txt b/requirements.txt index b0630ba..63d8669 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,6 @@ peewee requests -browser_cookie3 \ No newline at end of file +browser_cookie3 +pandas +xlrd +numpy diff --git a/start_windows.bat b/start_windows.bat new file mode 100644 index 0000000..9e6033a --- /dev/null +++ b/start_windows.bat @@ -0,0 +1,2 @@ +python install.py +python purge-registres-deces-insee\gui_trouver_decedes.py \ No newline at end of file