From: Benoît Fontaine Date: Thu, 16 May 2019 09:39:17 +0000 (+0200) Subject: - Add black formating (see https://github.com/python/black) X-Git-Url: https://git.nutra.tk/v2?a=commitdiff_plain;h=8a4b5965277173db45a1522d4d19bc0a1ecddb4a;p=gamesguru%2Fgetmyancestors.git - Add black formating (see https://github.com/python/black) - Add docstrings --- diff --git a/.gitignore b/.gitignore index d4ab07c..0ba9ab0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.pyc *.ged *.log +*.settings .vscode/ scratchpad.py diff --git a/__init__.py b/__init__.py index 56fafa5..ec1fe35 100644 --- a/__init__.py +++ b/__init__.py @@ -1,2 +1,2 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- +# coding: utf-8 diff --git a/fstogedcom.py b/fstogedcom.py index 712d48a..af98196 100644 --- a/fstogedcom.py +++ b/fstogedcom.py @@ -1,17 +1,17 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- +# coding: utf-8 # global import +import re +import os +import sys +import time +import asyncio +import tempfile from tkinter import Tk, StringVar, IntVar, filedialog, messagebox, Menu, TclError, PhotoImage from tkinter.ttk import Frame, Label, Entry, Button, Checkbutton, Treeview, Notebook from threading import Thread from diskcache import Cache -import time -import tempfile -import asyncio -import re -import os -import sys # local import from getmyancestors import Session, Tree, Indi, Fam @@ -19,10 +19,9 @@ from mergemyancestors import Gedcom from translation import translations -tmp_dir = os.path.join(tempfile.gettempdir(), 'fstogedcom') -global cache +tmp_dir = os.path.join(tempfile.gettempdir(), "fstogedcom") cache = Cache(tmp_dir) -lang = cache.get('lang') +lang = cache.get("lang") def _(string): @@ -31,104 +30,138 @@ def _(string): return string -# Entry widget with right-clic menu to copy/cut/paste class EntryWithMenu(Entry): + """ Entry widget with right-clic menu to copy/cut/paste """ + def __init__(self, master, **kw): super(EntryWithMenu, self).__init__(master, **kw) - self.bind('', self.click_right) + self.bind("", self.click_right) def click_right(self, event): + """ open menu """ menu = Menu(self, tearoff=0) try: self.selection_get() - state = 'normal' + state = "normal" except TclError: - state = 'disabled' - menu.add_command(label=_('Copy'), command=self.copy, state=state) - menu.add_command(label=_('Cut'), command=self.cut, state=state) - menu.add_command(label=_('Paste'), command=self.paste) + state = "disabled" + menu.add_command(label=_("Copy"), command=self.copy, state=state) + menu.add_command(label=_("Cut"), command=self.cut, state=state) + menu.add_command(label=_("Paste"), command=self.paste) menu.post(event.x_root, event.y_root) def copy(self): + """ copy in clipboard """ self.clipboard_clear() text = self.selection_get() self.clipboard_append(text) def cut(self): + """ move in clipboard """ self.copy() - self.delete('sel.first', 'sel.last') + self.delete("sel.first", "sel.last") def paste(self): + """ paste from clipboard """ try: - text = self.selection_get(selection='CLIPBOARD') - self.insert('insert', text) + text = self.selection_get(selection="CLIPBOARD") + self.insert("insert", text) except TclError: pass -# List of files to merge class FilesToMerge(Treeview): + """ List of GEDCOM files to merge """ + def __init__(self, master, **kwargs): - super(FilesToMerge, self).__init__(master, selectmode='extended', height=5, **kwargs) - self.heading('#0', text=_('Files')) - self.column('#0', width=300) + super(FilesToMerge, self).__init__(master, selectmode="extended", height=5, **kwargs) + self.heading("#0", text=_("Files")) + self.column("#0", width=300) self.files = dict() - self.bind('', self.popup) + self.bind("", self.popup) def add_file(self, filename): + """ add a GEDCOM file """ if any(f.name == filename for f in self.files.values()): - messagebox.showinfo(_('Error'), message=_('File already exist: ') + os.path.basename(filename)) + messagebox.showinfo( + _("Error"), message=_("File already exist: ") + os.path.basename(filename) + ) return if not os.path.exists(filename): - messagebox.showinfo(_('Error'), message=_('File not found: ') + os.path.basename(filename)) + messagebox.showinfo( + _("Error"), message=_("File not found: ") + os.path.basename(filename) + ) return - file = open(filename, 'r', encoding='utf-8') - new_id = self.insert('', 0, text=os.path.basename(filename)) + file = open(filename, "r", encoding="utf-8") + new_id = self.insert("", 0, text=os.path.basename(filename)) self.files[new_id] = file def popup(self, event): + """ open menu to remove item """ item = self.identify_row(event.y) if item: menu = Menu(self, tearoff=0) - menu.add_command(label=_('Remove'), command=self.delete_item(item)) + menu.add_command(label=_("Remove"), command=self.delete_item(item)) menu.post(event.x_root, event.y_root) def delete_item(self, item): + """ return a function to remove a file """ + def delete(): self.files[item].close() self.files.pop(item) self.delete(item) + return delete -# Merge widget class Merge(Frame): + """ Merge GEDCOM widget """ def __init__(self, master, **kwargs): super(Merge, self).__init__(master, **kwargs) - warning = Label(self, font=('a', 7), wraplength=300, justify='center', text=_('Warning: This tool should only be used to merge GEDCOM files from this software. If you use other GEDCOM files, the result is not guaranteed.')) + warning = Label( + self, + font=("a", 7), + wraplength=300, + justify="center", + text=_( + "Warning: This tool should only be used to merge GEDCOM files from this software. " + "If you use other GEDCOM files, the result is not guaranteed." + ), + ) self.files_to_merge = FilesToMerge(self) - self.btn_add_file = Button(self, text=_('Add files'), command=self.add_files) + self.btn_add_file = Button(self, text=_("Add files"), command=self.add_files) buttons = Frame(self, borderwidth=20) - self.btn_quit = Button(buttons, text=_('Quit'), command=self.quit) - self.btn_save = Button(buttons, text=_('Merge'), command=self.save) + self.btn_quit = Button(buttons, text=_("Quit"), command=self.quit) + self.btn_save = Button(buttons, text=_("Merge"), command=self.save) warning.pack() self.files_to_merge.pack() self.btn_add_file.pack() - self.btn_quit.pack(side='left', padx=(0, 40)) - self.btn_save.pack(side='right', padx=(40, 0)) - buttons.pack(side='bottom') + self.btn_quit.pack(side="left", padx=(0, 40)) + self.btn_save.pack(side="right", padx=(40, 0)) + buttons.pack(side="bottom") def add_files(self): - for filename in filedialog.askopenfilenames(title=_('Open'), defaultextension='.ged', filetypes=(('GEDCOM', '.ged'), (_('All files'), '*.*'))): + """ open file explorer to pick a file """ + for filename in filedialog.askopenfilenames( + title=_("Open"), + defaultextension=".ged", + filetypes=(("GEDCOM", ".ged"), (_("All files"), "*.*")), + ): self.files_to_merge.add_file(filename) def save(self): + """ merge GEDCOM files """ if not self.files_to_merge.files: - messagebox.showinfo(_('Error'), message=_('Please add GEDCOM files')) + messagebox.showinfo(_("Error"), message=_("Please add GEDCOM files")) return - filename = filedialog.asksaveasfilename(title=_('Save as'), defaultextension='.ged', filetypes=(('GEDCOM', '.ged'), (_('All files'), '*.*'))) + filename = filedialog.asksaveasfilename( + title=_("Save as"), + defaultextension=".ged", + filetypes=(("GEDCOM", ".ged"), (_("All files"), "*.*")), + ) tree = Tree() indi_counter = 0 @@ -191,86 +224,99 @@ class Merge(Frame): # compute number for family relationships and print GEDCOM file tree.reset_num() - with open(filename, 'w', encoding='utf-8') as file: + with open(filename, "w", encoding="utf-8") as file: tree.print(file) - messagebox.showinfo(_('Info'), message=_('Files successfully merged')) + messagebox.showinfo(_("Info"), message=_("Files successfully merged")) - # prevent exception on quit during download def quit(self): + """ prevent exception on quit during download """ super(Merge, self).quit() os._exit(1) -# Sign In widget class SignIn(Frame): + """ Sign In widget """ def __init__(self, master, **kwargs): super(SignIn, self).__init__(master, **kwargs) self.username = StringVar() self.password = StringVar() - label_username = Label(self, text=_('Username:')) + label_username = Label(self, text=_("Username:")) entry_username = EntryWithMenu(self, textvariable=self.username, width=30) - label_password = Label(self, text=_('Password:')) - entry_password = EntryWithMenu(self, show='●', textvariable=self.password, width=30) + label_password = Label(self, text=_("Password:")) + entry_password = EntryWithMenu(self, show="●", textvariable=self.password, width=30) label_username.grid(row=0, column=0, pady=15, padx=(0, 5)) entry_username.grid(row=0, column=1) label_password.grid(row=1, column=0, padx=(0, 5)) entry_password.grid(row=1, column=1) entry_username.focus_set() - entry_username.bind('', self.enter) - entry_password.bind('', self.enter) + entry_username.bind("", self.enter) + entry_password.bind("", self.enter) def enter(self, evt): - if evt.keysym in {'Return', 'KP_Enter'}: + """ enter event """ + if evt.keysym in {"Return", "KP_Enter"}: self.master.master.command_in_thread(self.master.master.login)() -# List of starting individuals class StartIndis(Treeview): + """ List of starting individuals """ + def __init__(self, master, **kwargs): - super(StartIndis, self).__init__(master, selectmode='extended', height=5, columns=('fid',), **kwargs) - self.heading('#0', text=_('Name')) - self.column('#0', width=250) - self.column('fid', width=80) + super(StartIndis, self).__init__( + master, selectmode="extended", height=5, columns=("fid",), **kwargs + ) + self.heading("#0", text=_("Name")) + self.column("#0", width=250) + self.column("fid", width=80) self.indis = dict() - self.heading('fid', text='Id') - self.bind('', self.popup) + self.heading("fid", text="Id") + self.bind("", self.popup) def add_indi(self, fid): + """ add an individual fid """ if not fid: - return + return None if fid in self.indis.values(): - messagebox.showinfo(_('Error'), message=_('ID already exist')) - return - if not re.match(r'[A-Z0-9]{4}-[A-Z0-9]{3}', fid): - messagebox.showinfo(_('Error'), message=_('Invalid FamilySearch ID: ') + fid) - return + messagebox.showinfo(_("Error"), message=_("ID already exist")) + return None + if not re.match(r"[A-Z0-9]{4}-[A-Z0-9]{3}", fid): + messagebox.showinfo(_("Error"), message=_("Invalid FamilySearch ID: ") + fid) + return None fs = self.master.master.master.fs - data = fs.get_url('/platform/tree/persons/%s.json' % fid) - if data and 'persons' in data: - if 'names' in data['persons'][0]: - for name in data['persons'][0]['names']: - if name['preferred']: - self.indis[self.insert('', 0, text=name['nameForms'][0]['fullText'], values=fid)] = fid + data = fs.get_url("/platform/tree/persons/%s.json" % fid) + if data and "persons" in data: + if "names" in data["persons"][0]: + for name in data["persons"][0]["names"]: + if name["preferred"]: + self.indis[ + self.insert("", 0, text=name["nameForms"][0]["fullText"], values=fid) + ] = fid return True - messagebox.showinfo(_('Error'), message=_('Individual not found')) + messagebox.showinfo(_("Error"), message=_("Individual not found")) + return None def popup(self, event): + """ open menu to remove item """ item = self.identify_row(event.y) if item: menu = Menu(self, tearoff=0) - menu.add_command(label=_('Remove'), command=self.delete_item(item)) + menu.add_command(label=_("Remove"), command=self.delete_item(item)) menu.post(event.x_root, event.y_root) def delete_item(self, item): + """ return a function to remove a fid """ + def delete(): self.indis.pop(item) self.delete(item) + return delete -# Options form class Options(Frame): + """ Options form """ + def __init__(self, master, ordinances=False, **kwargs): super(Options, self).__init__(master, **kwargs) self.ancestors = IntVar() @@ -283,40 +329,49 @@ class Options(Frame): self.fid = StringVar() btn = Frame(self) entry_fid = EntryWithMenu(btn, textvariable=self.fid, width=16) - entry_fid.bind('', self.enter) - label_ancestors = Label(self, text=_('Number of generations to ascend')) + entry_fid.bind("", self.enter) + label_ancestors = Label(self, text=_("Number of generations to ascend")) entry_ancestors = EntryWithMenu(self, textvariable=self.ancestors, width=5) - label_descendants = Label(self, text=_('Number of generations to descend')) + label_descendants = Label(self, text=_("Number of generations to descend")) entry_descendants = EntryWithMenu(self, textvariable=self.descendants, width=5) - btn_add_indi = Button(btn, text=_('Add a FamilySearch ID'), command=self.add_indi) - btn_spouses = Checkbutton(self, text='\t' + _('Add spouses and couples information'), variable=self.spouses) - btn_ordinances = Checkbutton(self, text='\t' + _('Add Temple information'), variable=self.ordinances) - btn_contributors = Checkbutton(self, text='\t' + _('Add list of contributors in notes'), variable=self.contributors) + btn_add_indi = Button(btn, text=_("Add a FamilySearch ID"), command=self.add_indi) + btn_spouses = Checkbutton( + self, text="\t" + _("Add spouses and couples information"), variable=self.spouses + ) + btn_ordinances = Checkbutton( + self, text="\t" + _("Add Temple information"), variable=self.ordinances + ) + btn_contributors = Checkbutton( + self, text="\t" + _("Add list of contributors in notes"), variable=self.contributors + ) self.start_indis.grid(row=0, column=0, columnspan=3) - entry_fid.grid(row=0, column=0, sticky='w') - btn_add_indi.grid(row=0, column=1, sticky='w') - btn.grid(row=1, column=0, columnspan=2, sticky='w') - entry_ancestors.grid(row=2, column=0, sticky='w') - label_ancestors.grid(row=2, column=1, sticky='w') - entry_descendants.grid(row=3, column=0, sticky='w') - label_descendants.grid(row=3, column=1, sticky='w') - btn_spouses.grid(row=4, column=0, columnspan=2, sticky='w') + entry_fid.grid(row=0, column=0, sticky="w") + btn_add_indi.grid(row=0, column=1, sticky="w") + btn.grid(row=1, column=0, columnspan=2, sticky="w") + entry_ancestors.grid(row=2, column=0, sticky="w") + label_ancestors.grid(row=2, column=1, sticky="w") + entry_descendants.grid(row=3, column=0, sticky="w") + label_descendants.grid(row=3, column=1, sticky="w") + btn_spouses.grid(row=4, column=0, columnspan=2, sticky="w") if ordinances: - btn_ordinances.grid(row=5, column=0, columnspan=3, sticky='w') - btn_contributors.grid(row=6, column=0, columnspan=3, sticky='w') + btn_ordinances.grid(row=5, column=0, columnspan=3, sticky="w") + btn_contributors.grid(row=6, column=0, columnspan=3, sticky="w") entry_ancestors.focus_set() def add_indi(self): + """ add a fid """ if self.start_indis.add_indi(self.fid.get()): - self.fid.set('') + self.fid.set("") def enter(self, evt): - if evt.keysym in {'Return', 'KP_Enter'}: + """ enter event """ + if evt.keysym in {"Return", "KP_Enter"}: self.add_indi() -# Main widget class Download(Frame): + """ Main widget """ + def __init__(self, master, **kwargs): super(Download, self).__init__(master, borderwidth=20, **kwargs) self.fs = None @@ -327,7 +382,9 @@ class Download(Frame): self.info_tree = False self.start_time = None info = Frame(self, borderwidth=10) - self.info_label = Label(info, wraplength=350, borderwidth=20, justify='center', font=('a', 10, 'bold')) + self.info_label = Label( + info, wraplength=350, borderwidth=20, justify="center", font=("a", 10, "bold") + ) self.info_indis = Label(info) self.info_fams = Label(info) self.info_sources = Label(info) @@ -343,81 +400,104 @@ class Download(Frame): self.form = Frame(self) self.sign_in = SignIn(self.form) self.options = None - self.title = Label(self, text=_('Sign In to FamilySearch'), font=('a', 12, 'bold')) + self.title = Label(self, text=_("Sign In to FamilySearch"), font=("a", 12, "bold")) buttons = Frame(self) - self.btn_quit = Button(buttons, text=_('Quit'), command=Thread(target=self.quit).start) - self.btn_valid = Button(buttons, text=_('Sign In'), command=self.command_in_thread(self.login)) + self.btn_quit = Button(buttons, text=_("Quit"), command=Thread(target=self.quit).start) + self.btn_valid = Button( + buttons, text=_("Sign In"), command=self.command_in_thread(self.login) + ) self.title.pack() self.sign_in.pack() self.form.pack() - self.btn_quit.pack(side='left', padx=(0, 40)) - self.btn_valid.pack(side='right', padx=(40, 0)) + self.btn_quit.pack(side="left", padx=(0, 40)) + self.btn_valid.pack(side="right", padx=(40, 0)) info.pack() - buttons.pack(side='bottom') + buttons.pack(side="bottom") self.pack() self.update_needed = False def info(self, text): + """ dislay informations """ self.info_label.config(text=text) def save(self): - filename = filedialog.asksaveasfilename(title=_('Save as'), defaultextension='.ged', filetypes=(('GEDCOM', '.ged'), (_('All files'), '*.*'))) + """ save the GEDCOM file """ + filename = filedialog.asksaveasfilename( + title=_("Save as"), + defaultextension=".ged", + filetypes=(("GEDCOM", ".ged"), (_("All files"), "*.*")), + ) if not filename: return - with open(filename, 'w', encoding='utf-8') as file: + with open(filename, "w", encoding="utf-8") as file: self.tree.print(file) def login(self): + """ log in FamilySearch """ global _ username = self.sign_in.username.get() password = self.sign_in.password.get() if not (username and password): - messagebox.showinfo(message=_('Please enter your FamilySearch username and password.')) + messagebox.showinfo(message=_("Please enter your FamilySearch username and password.")) return - self.btn_valid.config(state='disabled') - self.info(_('Login to FamilySearch...')) - self.logfile = open('download.log', 'w', encoding='utf-8') - self.fs = Session(self.sign_in.username.get(), self.sign_in.password.get(), verbose=True, logfile=self.logfile, timeout=1) + self.btn_valid.config(state="disabled") + self.info(_("Login to FamilySearch...")) + self.logfile = open("download.log", "w", encoding="utf-8") + self.fs = Session( + self.sign_in.username.get(), + self.sign_in.password.get(), + verbose=True, + logfile=self.logfile, + timeout=1, + ) if not self.fs.logged: - messagebox.showinfo(_('Error'), message=_('The username or password was incorrect')) - self.btn_valid.config(state='normal') - self.info('') + messagebox.showinfo(_("Error"), message=_("The username or password was incorrect")) + self.btn_valid.config(state="normal") + self.info("") return self.tree = Tree(self.fs) _ = self.fs._ - self.title.config(text=_('Options')) - cache.delete('lang') - cache.add('lang', self.fs.lang) - lds_account = self.fs.get_url('/platform/tree/persons/%s/ordinances.json' % self.fs.get_userid()) != 'error' + self.title.config(text=_("Options")) + cache.delete("lang") + cache.add("lang", self.fs.lang) + lds_account = ( + self.fs.get_url("/platform/tree/persons/%s/ordinances.json" % self.fs.get_userid()) + != "error" + ) self.options = Options(self.form, lds_account) - self.info('') + self.info("") self.sign_in.destroy() self.options.pack() self.master.change_lang() - self.btn_valid.config(command=self.command_in_thread(self.download), state='normal', text=_('Download')) + self.btn_valid.config( + command=self.command_in_thread(self.download), state="normal", text=_("Download") + ) self.options.start_indis.add_indi(self.fs.get_userid()) self.update_needed = False def quit(self): + """ prevent exception during download """ self.update_needed = False if self.logfile: self.logfile.close() super(Download, self).quit() - # prevent exception during download os._exit(1) def download(self): - todo = [self.options.start_indis.indis[key] for key in sorted(self.options.start_indis.indis)] + """ download family tree """ + todo = [ + self.options.start_indis.indis[key] for key in sorted(self.options.start_indis.indis) + ] for fid in todo: - if not re.match(r'[A-Z0-9]{4}-[A-Z0-9]{3}', fid): - messagebox.showinfo(_('Error'), message=_('Invalid FamilySearch ID: ') + fid) + if not re.match(r"[A-Z0-9]{4}-[A-Z0-9]{3}", fid): + messagebox.showinfo(_("Error"), message=_("Invalid FamilySearch ID: ") + fid) return self.start_time = time.time() self.options.destroy() self.form.destroy() - self.title.config(text='FamilySearch to GEDCOM') - self.btn_valid.config(state='disabled') - self.info(_('Downloading starting individuals...')) + self.title.config(text="FamilySearch to GEDCOM") + self.btn_valid.config(state="disabled") + self.info(_("Downloading starting individuals...")) self.info_tree = True self.tree.add_indis(todo) todo = set(todo) @@ -426,7 +506,7 @@ class Download(Frame): if not todo: break done |= todo - self.info(_('Downloading %s. of generations of ancestors...') % (i + 1)) + self.info(_("Downloading %s. of generations of ancestors...") % (i + 1)) todo = self.tree.add_parents(todo) - done todo = set(self.tree.indi.keys()) @@ -435,11 +515,11 @@ class Download(Frame): if not todo: break done |= todo - self.info(_('Downloading %s. of generations of descendants...') % (i + 1)) + self.info(_("Downloading %s. of generations of descendants...") % (i + 1)) todo = self.tree.add_children(todo) - done if self.options.spouses.get(): - self.info(_('Downloading spouses and marriage information...')) + self.info(_("Downloading spouses and marriage information...")) todo = set(self.tree.indi.keys()) self.tree.add_spouses(todo) ordi = self.options.ordinances.get() @@ -461,34 +541,44 @@ class Download(Frame): await future loop = asyncio.get_event_loop() - self.info(_('Downloading notes') + (((',' if cont else _(' and')) + _(' ordinances')) if ordi else '') + (_(' and contributors') if cont else '') + '...') + self.info( + _("Downloading notes") + + ((("," if cont else _(" and")) + _(" ordinances")) if ordi else "") + + (_(" and contributors") if cont else "") + + "..." + ) loop.run_until_complete(download_stuff(loop)) self.tree.reset_num() - self.btn_valid.config(command=self.save, state='normal', text=_('Save')) - self.info(text=_('Success ! Click below to save your GEDCOM file')) + self.btn_valid.config(command=self.save, state="normal", text=_("Save")) + self.info(text=_("Success ! Click below to save your GEDCOM file")) self.update_info_tree() self.update_needed = False def command_in_thread(self, func): + """ command to update widget in a new Thread """ + def res(): self.update_needed = True Thread(target=self.update_gui).start() Thread(target=func).start() + return res def update_info_tree(self): + """ update informations """ if self.info_tree and self.start_time and self.tree: - self.info_indis.config(text=_('Individuals: %s') % len(self.tree.indi)) - self.info_fams.config(text=_('Families: %s') % len(self.tree.fam)) - self.info_sources.config(text=_('Sources: %s') % len(self.tree.sources)) - self.info_notes.config(text=_('Notes: %s') % len(self.tree.notes)) + self.info_indis.config(text=_("Individuals: %s") % len(self.tree.indi)) + self.info_fams.config(text=_("Families: %s") % len(self.tree.fam)) + self.info_sources.config(text=_("Sources: %s") % len(self.tree.sources)) + self.info_notes.config(text=_("Notes: %s") % len(self.tree.notes)) t = round(time.time() - self.start_time) minutes = t // 60 seconds = t % 60 - self.time.config(text=_('Elapsed time: %s:%s') % (minutes, '00%s'[len(str(seconds)):] % seconds)) + self.time.config(text=_("Elapsed time: %s:%s") % (minutes, str(seconds).zfill(2))) def update_gui(self): + """ update widget """ while self.update_needed: self.update_info_tree() self.master.update() @@ -496,27 +586,30 @@ class Download(Frame): class FStoGEDCOM(Notebook): + """ Main notebook """ + def __init__(self, master, **kwargs): super(FStoGEDCOM, self).__init__(master, width=400, **kwargs) self.download = Download(self) self.merge = Merge(self) - self.add(self.download, text=_('Download GEDCOM')) - self.add(self.merge, text=_('Merge GEDCOMs')) + self.add(self.download, text=_("Download GEDCOM")) + self.add(self.merge, text=_("Merge GEDCOMs")) self.pack() def change_lang(self): - self.tab(self.index(self.download), text=_('Download GEDCOM')) - self.tab(self.index(self.merge), text=_('Merge GEDCOMs')) - self.download.btn_quit.config(text=_('Quit')) - self.merge.btn_quit.config(text=_('Quit')) - self.merge.btn_save.config(text=_('Merge')) - self.merge.btn_add_file.config(text=_('Add files')) + """ update text with user's language """ + self.tab(self.index(self.download), text=_("Download GEDCOM")) + self.tab(self.index(self.merge), text=_("Merge GEDCOMs")) + self.download.btn_quit.config(text=_("Quit")) + self.merge.btn_quit.config(text=_("Quit")) + self.merge.btn_save.config(text=_("Merge")) + self.merge.btn_add_file.config(text=_("Add files")) -if __name__ == '__main__': +if __name__ == "__main__": root = Tk() - root.title('FamilySearch to GEDCOM') - if sys.platform != 'darwin': - root.iconphoto(True, PhotoImage(file='fstogedcom.png')) + root.title("FamilySearch to GEDCOM") + if sys.platform != "darwin": + root.iconphoto(True, PhotoImage(file="fstogedcom.png")) fstogedcom = FStoGEDCOM(root) fstogedcom.mainloop() diff --git a/getmyancestors.py b/getmyancestors.py index 2f9f7dd..7e54b73 100755 --- a/getmyancestors.py +++ b/getmyancestors.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- +# coding: utf-8 """ getmyancestors.py - Retrieve GEDCOM data from FamilySearch Tree Copyright (C) 2014-2016 Giulio Genovese (giulio.genovese@gmail.com) @@ -23,74 +23,73 @@ # global import from __future__ import print_function +import re import sys -import argparse -import getpass import time +import getpass import asyncio -import re +import argparse +import requests # local import from translation import translations -try: - import requests -except ImportError: - sys.stderr.write('You need to install the requests module first\n') - sys.stderr.write('(run this in your terminal: "python3 -m pip install requests" or "python3 -m pip install --user requests")\n') - exit(2) - try: import babelfish except ImportError: - sys.stderr.write('You need to install the babelfish module first\n') - sys.stderr.write('(run this in your terminal: "python3 -m pip install babelfish" or "python3 -m pip install --user babelfish")\n') - exit(2) + sys.stderr.write("You need to install the babelfish module first\n") + sys.stderr.write( + '(run this in your terminal: "python3 -m pip install babelfish" ' + 'or "python3 -m pip install --user babelfish")\n' + ) + sys.exit(2) -MAX_PERSONS = 200 # is subject to change: see https://www.familysearch.org/developers/docs/api/tree/Persons_resource +# is subject to change: see https://www.familysearch.org/developers/docs/api/tree/Persons_resource +MAX_PERSONS = 200 FACT_TAGS = { - 'http://gedcomx.org/Birth': 'BIRT', - 'http://gedcomx.org/Christening': 'CHR', - 'http://gedcomx.org/Death': 'DEAT', - 'http://gedcomx.org/Burial': 'BURI', - 'http://gedcomx.org/PhysicalDescription': 'DSCR', - 'http://gedcomx.org/Occupation': 'OCCU', - 'http://gedcomx.org/MilitaryService': '_MILT', - 'http://gedcomx.org/Marriage': 'MARR', - 'http://gedcomx.org/Divorce': 'DIV', - 'http://gedcomx.org/Annulment': 'ANUL', - 'http://gedcomx.org/CommonLawMarriage': '_COML', - 'http://gedcomx.org/BarMitzvah': 'BARM', - 'http://gedcomx.org/BatMitzvah': 'BASM', - 'http://gedcomx.org/Naturalization': 'NATU', - 'http://gedcomx.org/Residence': 'RESI', - 'http://gedcomx.org/Religion': 'RELI', - 'http://familysearch.org/v1/TitleOfNobility': 'TITL', - 'http://gedcomx.org/Cremation': 'CREM', - 'http://gedcomx.org/Caste': 'CAST', - 'http://gedcomx.org/Nationality': 'NATI', + "http://gedcomx.org/Birth": "BIRT", + "http://gedcomx.org/Christening": "CHR", + "http://gedcomx.org/Death": "DEAT", + "http://gedcomx.org/Burial": "BURI", + "http://gedcomx.org/PhysicalDescription": "DSCR", + "http://gedcomx.org/Occupation": "OCCU", + "http://gedcomx.org/MilitaryService": "_MILT", + "http://gedcomx.org/Marriage": "MARR", + "http://gedcomx.org/Divorce": "DIV", + "http://gedcomx.org/Annulment": "ANUL", + "http://gedcomx.org/CommonLawMarriage": "_COML", + "http://gedcomx.org/BarMitzvah": "BARM", + "http://gedcomx.org/BatMitzvah": "BASM", + "http://gedcomx.org/Naturalization": "NATU", + "http://gedcomx.org/Residence": "RESI", + "http://gedcomx.org/Religion": "RELI", + "http://familysearch.org/v1/TitleOfNobility": "TITL", + "http://gedcomx.org/Cremation": "CREM", + "http://gedcomx.org/Caste": "CAST", + "http://gedcomx.org/Nationality": "NATI", } FACT_EVEN = { - 'http://gedcomx.org/Stillbirth': 'Stillborn', - 'http://familysearch.org/v1/Affiliation': 'Affiliation', - 'http://gedcomx.org/Clan': 'Clan Name', - 'http://gedcomx.org/NationalId': 'National Identification', - 'http://gedcomx.org/Ethnicity': 'Race', - 'http://familysearch.org/v1/TribeName': 'Tribe Name' + "http://gedcomx.org/Stillbirth": "Stillborn", + "http://familysearch.org/v1/Affiliation": "Affiliation", + "http://gedcomx.org/Clan": "Clan Name", + "http://gedcomx.org/NationalId": "National Identification", + "http://gedcomx.org/Ethnicity": "Race", + "http://familysearch.org/v1/TribeName": "Tribe Name", } ORDINANCES_STATUS = { - 'http://familysearch.org/v1/Ready': 'QUALIFIED', - 'http://familysearch.org/v1/Completed': 'COMPLETED', - 'http://familysearch.org/v1/Cancelled': 'CANCELED', - 'http://familysearch.org/v1/InProgress': 'SUBMITTED', - 'http://familysearch.org/v1/NotNeeded': 'INFANT' + "http://familysearch.org/v1/Ready": "QUALIFIED", + "http://familysearch.org/v1/Completed": "COMPLETED", + "http://familysearch.org/v1/Cancelled": "CANCELED", + "http://familysearch.org/v1/InProgress": "SUBMITTED", + "http://familysearch.org/v1/NotNeeded": "INFANT", } def cont(string): + """ parse a GEDCOM line adding CONT and CONT tags if necessary """ level = int(string[:1]) + 1 lines = string.splitlines() res = list() @@ -98,21 +97,30 @@ def cont(string): for line in lines: c_line = line to_conc = list() - while len(c_line.encode('utf-8')) > max_len: + while len(c_line.encode("utf-8")) > max_len: index = min(max_len, len(c_line) - 2) - while (len(c_line[:index].encode('utf-8')) > max_len or re.search(r'[ \t\v]', c_line[index - 1:index + 1])) and index > 1: + while ( + len(c_line[:index].encode("utf-8")) > max_len + or re.search(r"[ \t\v]", c_line[index - 1 : index + 1]) + ) and index > 1: index -= 1 to_conc.append(c_line[:index]) c_line = c_line[index:] max_len = 248 to_conc.append(c_line) - res.append(('\n%s CONC ' % level).join(to_conc)) + res.append(("\n%s CONC " % level).join(to_conc)) max_len = 248 - return ('\n%s CONT ' % level).join(res) + return ("\n%s CONT " % level).join(res) + "\n" -# FamilySearch session class class Session: + """ Create a FamilySearch session + :param username and password: valid FamilySearch credentials + :param verbose: True to active verbose mode + :param logfile: a file object or similar + :param timeout: time before retry a request + """ + def __init__(self, username, password, verbose=False, logfile=sys.stderr, timeout=60): self.username = username self.password = password @@ -123,84 +131,93 @@ class Session: self.counter = 0 self.logged = self.login() - # Write in logfile if verbose enabled def write_log(self, text): + """ write text in the log file """ if self.verbose: - self.logfile.write('[%s]: %s\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), text)) + self.logfile.write("[%s]: %s\n" % (time.strftime("%Y-%m-%d %H:%M:%S"), text)) - # retrieve FamilySearch session ID (https://familysearch.org/developers/docs/guides/oauth2) def login(self): + """ retrieve FamilySearch session ID + (https://familysearch.org/developers/docs/guides/oauth2) + """ while True: try: - url = 'https://www.familysearch.org/auth/familysearch/login' - self.write_log('Downloading: ' + url) - r = requests.get(url, params={'ldsauth': False}, allow_redirects=False) - url = r.headers['Location'] - self.write_log('Downloading: ' + url) + url = "https://www.familysearch.org/auth/familysearch/login" + self.write_log("Downloading: " + url) + r = requests.get(url, params={"ldsauth": False}, allow_redirects=False) + url = r.headers["Location"] + self.write_log("Downloading: " + url) r = requests.get(url, allow_redirects=False) idx = r.text.index('name="params" value="') - span = r.text[idx + 21:].index('"') - params = r.text[idx + 21:idx + 21 + span] - - url = 'https://ident.familysearch.org/cis-web/oauth2/v3/authorization' - self.write_log('Downloading: ' + url) - r = requests.post(url, data={'params': params, 'userName': self.username, 'password': self.password}, allow_redirects=False) - - if 'The username or password was incorrect' in r.text: - self.write_log('The username or password was incorrect') + span = r.text[idx + 21 :].index('"') + params = r.text[idx + 21 : idx + 21 + span] + + url = "https://ident.familysearch.org/cis-web/oauth2/v3/authorization" + self.write_log("Downloading: " + url) + r = requests.post( + url, + data={"params": params, "userName": self.username, "password": self.password}, + allow_redirects=False, + ) + + if "The username or password was incorrect" in r.text: + self.write_log("The username or password was incorrect") return False - if 'Invalid Oauth2 Request' in r.text: - self.write_log('Invalid Oauth2 Request') + if "Invalid Oauth2 Request" in r.text: + self.write_log("Invalid Oauth2 Request") time.sleep(self.timeout) continue - url = r.headers['Location'] - self.write_log('Downloading: ' + url) + url = r.headers["Location"] + self.write_log("Downloading: " + url) r = requests.get(url, allow_redirects=False) - self.fssessionid = r.cookies['fssessionid'] + self.fssessionid = r.cookies["fssessionid"] except requests.exceptions.ReadTimeout: - self.write_log('Read timed out') + self.write_log("Read timed out") continue except requests.exceptions.ConnectionError: - self.write_log('Connection aborted') + self.write_log("Connection aborted") time.sleep(self.timeout) continue except requests.exceptions.HTTPError: - self.write_log('HTTPError') + self.write_log("HTTPError") time.sleep(self.timeout) continue except KeyError: - self.write_log('KeyError') + self.write_log("KeyError") time.sleep(self.timeout) continue except ValueError: - self.write_log('ValueError') + self.write_log("ValueError") time.sleep(self.timeout) continue - self.write_log('FamilySearch session id: ' + self.fssessionid) + self.write_log("FamilySearch session id: " + self.fssessionid) return True - # retrieve JSON structure from FamilySearch URL def get_url(self, url): + """ retrieve JSON structure from a FamilySearch URL """ self.counter += 1 while True: try: - self.write_log('Downloading: ' + url) - # r = requests.get(url, cookies = { 's_vi': self.s_vi, 'fssessionid' : self.fssessionid }, timeout = self.timeout) - r = requests.get('https://familysearch.org' + url, cookies={'fssessionid': self.fssessionid}, timeout=self.timeout) + self.write_log("Downloading: " + url) + r = requests.get( + "https://familysearch.org" + url, + cookies={"fssessionid": self.fssessionid}, + timeout=self.timeout, + ) except requests.exceptions.ReadTimeout: - self.write_log('Read timed out') + self.write_log("Read timed out") continue except requests.exceptions.ConnectionError: - self.write_log('Connection aborted') + self.write_log("Connection aborted") time.sleep(self.timeout) continue - self.write_log('Status code: ' + str(r.status_code)) + self.write_log("Status code: %s" % r.status_code) if r.status_code == 204: return None if r.status_code in {404, 405, 410, 500}: - self.write_log('WARNING: ' + url) + self.write_log("WARNING: " + url) return None if r.status_code == 401: self.login() @@ -208,38 +225,49 @@ class Session: try: r.raise_for_status() except requests.exceptions.HTTPError: - self.write_log('HTTPError') + self.write_log("HTTPError") if r.status_code == 403: - if 'message' in r.json()['errors'][0] and r.json()['errors'][0]['message'] == u'Unable to get ordinances.': - self.write_log('Unable to get ordinances. Try with an LDS account or without option -c.') - return 'error' - else: - - self.write_log('WARNING: code 403 from %s %s' % (url, r.json()['errors'][0]['message'] or '')) - return None + if ( + "message" in r.json()["errors"][0] + and r.json()["errors"][0]["message"] == "Unable to get ordinances." + ): + self.write_log( + "Unable to get ordinances. " + "Try with an LDS account or without option -c." + ) + return "error" + self.write_log( + "WARNING: code 403 from %s %s" + % (url, r.json()["errors"][0]["message"] or "") + ) + return None time.sleep(self.timeout) continue try: return r.json() except Exception as e: - self.write_log('WARNING: corrupted file from %s, error: %s' % (url, e)) + self.write_log("WARNING: corrupted file from %s, error: %s" % (url, e)) return None - # retrieve FamilySearch current user ID def set_current(self): - url = '/platform/users/current.json' + """ retrieve FamilySearch current user ID, name and language """ + url = "/platform/users/current.json" data = self.get_url(url) if data: - self.fid = data['users'][0]['personId'] - self.lang = data['users'][0]['preferredLanguage'] - self.display_name = data['users'][0]['displayName'] + self.fid = data["users"][0]["personId"] + self.lang = data["users"][0]["preferredLanguage"] + self.display_name = data["users"][0]["displayName"] def get_userid(self): + """ get FamilySearch current user ID """ if not self.fid: self.set_current() return self.fid def _(self, string): + """ translate a string into user's language + TODO replace translation file for gettext format + """ if not self.lang: self.set_current() if string in translations and self.lang in translations[string]: @@ -247,12 +275,16 @@ class Session: return string -# some GEDCOM objects class Note: + """ GEDCOM Note class + :param text: the Note content + :param tree: a Tree object + :param num: the GEDCOM identifier + """ counter = 0 - def __init__(self, text='', tree=None, num=None): + def __init__(self, text="", tree=None, num=None): if num: self.num = num else: @@ -264,13 +296,20 @@ class Note: tree.notes.append(self) def print(self, file=sys.stdout): - file.write(cont('0 @N' + str(self.num) + '@ NOTE ' + self.text) + '\n') + """ print Note in GEDCOM format """ + file.write(cont("0 @N%s@ NOTE %s" % (self.num, self.text))) def link(self, file=sys.stdout, level=1): - file.write(str(level) + ' NOTE @N' + str(self.num) + '@\n') + """ print the reference in GEDCOM format """ + file.write("%s NOTE @N%s@\n" % (level, self.num)) class Source: + """ GEDCOM Source class + :param data: FS Source data + :param tree: a Tree object + :param num: the GEDCOM identifier + """ counter = 0 @@ -285,167 +324,198 @@ class Source: self.url = self.citation = self.title = self.fid = None self.notes = set() if data: - self.fid = data['id'] - if 'about' in data: - self.url = data['about'].replace('familysearch.org/platform/memories/memories', 'www.familysearch.org/photos/artifacts') - if 'citations' in data: - self.citation = data['citations'][0]['value'] - if 'titles' in data: - self.title = data['titles'][0]['value'] - if 'notes' in data: - for n in data['notes']: - if n['text']: - self.notes.add(Note(n['text'], self.tree)) + self.fid = data["id"] + if "about" in data: + self.url = data["about"].replace( + "familysearch.org/platform/memories/memories", + "www.familysearch.org/photos/artifacts", + ) + if "citations" in data: + self.citation = data["citations"][0]["value"] + if "titles" in data: + self.title = data["titles"][0]["value"] + if "notes" in data: + for n in data["notes"]: + if n["text"]: + self.notes.add(Note(n["text"], self.tree)) def print(self, file=sys.stdout): - file.write('0 @S' + str(self.num) + '@ SOUR \n') + """ print Source in GEDCOM format """ + file.write("0 @S%s@ SOUR \n" % self.num) if self.title: - file.write(cont('1 TITL ' + self.title) + '\n') + file.write(cont("1 TITL " + self.title)) if self.citation: - file.write(cont('1 AUTH ' + self.citation) + '\n') + file.write(cont("1 AUTH " + self.citation)) if self.url: - file.write(cont('1 PUBL ' + self.url) + '\n') + file.write(cont("1 PUBL " + self.url)) for n in self.notes: n.link(file, 1) - file.write('1 REFN ' + self.fid + '\n') + file.write("1 REFN %s\n" % self.fid) def link(self, file=sys.stdout, level=1): - file.write(str(level) + ' SOUR @S' + str(self.num) + '@\n') + """ print the reference in GEDCOM format """ + file.write("%s SOUR @S%s@\n" % (level, self.num)) class Fact: + """ GEDCOM Fact class + :param data: FS Fact data + :param tree: a tree object + """ def __init__(self, data=None, tree=None): self.value = self.type = self.date = self.place = self.note = self.map = None if data: - if 'value' in data: - self.value = data['value'] - if 'type' in data: - self.type = data['type'] + if "value" in data: + self.value = data["value"] + if "type" in data: + self.type = data["type"] if self.type in FACT_EVEN: self.type = tree.fs._(FACT_EVEN[self.type]) - elif self.type[:6] == u'data:,': + elif self.type[:6] == "data:,": self.type = self.type[6:] elif self.type not in FACT_TAGS: self.type = None - if 'date' in data: - self.date = data['date']['original'] - if 'place' in data: - place = data['place'] - self.place = place['original'] - if 'description' in place and place['description'][1:] in tree.places: - self.map = tree.places[place['description'][1:]] - if 'changeMessage' in data['attribution']: - self.note = Note(data['attribution']['changeMessage'], tree) - if self.type == 'http://gedcomx.org/Death' and not (self.date or self.place): - self.value = 'Y' - - def print(self, file=sys.stdout, key=None): + if "date" in data: + self.date = data["date"]["original"] + if "place" in data: + place = data["place"] + self.place = place["original"] + if "description" in place and place["description"][1:] in tree.places: + self.map = tree.places[place["description"][1:]] + if "changeMessage" in data["attribution"]: + self.note = Note(data["attribution"]["changeMessage"], tree) + if self.type == "http://gedcomx.org/Death" and not (self.date or self.place): + self.value = "Y" + + def print(self, file=sys.stdout): + """ print Fact in GEDCOM format + the GEDCOM TAG depends on the type, defined in FACT_TAGS + """ if self.type in FACT_TAGS: - tmp = '1 ' + FACT_TAGS[self.type] + tmp = "1 " + FACT_TAGS[self.type] if self.value: - tmp += ' ' + self.value + tmp += " " + self.value file.write(cont(tmp)) elif self.type: - file.write('1 EVEN\n2 TYPE ' + self.type) + file.write("1 EVEN\n2 TYPE %s\n" % self.type) if self.value: - file.write('\n' + cont('2 NOTE Description: ' + self.value)) + file.write(cont("2 NOTE Description: " + self.value)) else: return - file.write('\n') if self.date: - file.write(cont('2 DATE ' + self.date) + '\n') + file.write(cont("2 DATE " + self.date)) if self.place: - file.write(cont('2 PLAC ' + self.place) + '\n') + file.write(cont("2 PLAC " + self.place)) if self.map: latitude, longitude = self.map - file.write('3 MAP\n4 LATI ' + latitude + '\n4 LONG ' + longitude + '\n') + file.write("3 MAP\n4 LATI %s\n4 LONG %s\n" % (latitude, longitude)) if self.note: self.note.link(file, 2) class Memorie: + """ GEDCOM Memorie class + :param data: FS Memorie data + """ def __init__(self, data=None): self.description = self.url = None - if data and 'links' in data: - self.url = data['about'] - if 'titles' in data: - self.description = data['titles'][0]['value'] - if 'descriptions' in data: - self.description = ('' if not self.description else self.description + '\n') + data['descriptions'][0]['value'] + if data and "links" in data: + self.url = data["about"] + if "titles" in data: + self.description = data["titles"][0]["value"] + if "descriptions" in data: + self.description = ("" if not self.description else self.description + "\n") + data[ + "descriptions" + ][0]["value"] def print(self, file=sys.stdout): - file.write('1 OBJE\n2 FORM URL\n') + """ print Memorie in GEDCOM format """ + file.write("1 OBJE\n2 FORM URL\n") if self.description: - file.write(cont('2 TITL ' + self.description) + '\n') + file.write(cont("2 TITL " + self.description)) if self.url: - file.write(cont('2 FILE ' + self.url) + '\n') + file.write(cont("2 FILE " + self.url)) class Name: + """ GEDCOM Name class + :param data: FS Name data + :param tree: a Tree object + """ def __init__(self, data=None, tree=None): - self.given = '' - self.surname = '' + self.given = "" + self.surname = "" self.prefix = None self.suffix = None self.note = None if data: - if 'parts' in data['nameForms'][0]: - for z in data['nameForms'][0]['parts']: - if z['type'] == u'http://gedcomx.org/Given': - self.given = z['value'] - if z['type'] == u'http://gedcomx.org/Surname': - self.surname = z['value'] - if z['type'] == u'http://gedcomx.org/Prefix': - self.prefix = z['value'] - if z['type'] == u'http://gedcomx.org/Suffix': - self.suffix = z['value'] - if 'changeMessage' in data['attribution']: - self.note = Note(data['attribution']['changeMessage'], tree) + if "parts" in data["nameForms"][0]: + for z in data["nameForms"][0]["parts"]: + if z["type"] == "http://gedcomx.org/Given": + self.given = z["value"] + if z["type"] == "http://gedcomx.org/Surname": + self.surname = z["value"] + if z["type"] == "http://gedcomx.org/Prefix": + self.prefix = z["value"] + if z["type"] == "http://gedcomx.org/Suffix": + self.suffix = z["value"] + if "changeMessage" in data["attribution"]: + self.note = Note(data["attribution"]["changeMessage"], tree) def print(self, file=sys.stdout, typ=None): - tmp = '1 NAME ' + self.given + ' /' + self.surname + '/' + """ print Name in GEDCOM format + :param typ: type for additional names + """ + tmp = "1 NAME %s /%s/" % (self.given, self.surname) if self.suffix: - tmp += ' ' + self.suffix - file.write(cont(tmp) + '\n') + tmp += " " + self.suffix + file.write(cont(tmp)) if typ: - file.write('2 TYPE ' + typ + '\n') + file.write("2 TYPE %s\n" % typ) if self.prefix: - file.write('2 NPFX ' + self.prefix + '\n') + file.write("2 NPFX %s\n" % self.prefix) if self.note: self.note.link(file, 2) class Ordinance: + """ GEDCOM Ordinance class + :param data: FS Ordinance data + """ def __init__(self, data=None): self.date = self.temple_code = self.status = self.famc = None if data: - if 'date' in data: - self.date = data['date']['formal'] - if 'templeCode' in data: - self.temple_code = data['templeCode'] - self.status = data['status'] + if "date" in data: + self.date = data["date"]["formal"] + if "templeCode" in data: + self.temple_code = data["templeCode"] + self.status = data["status"] def print(self, file=sys.stdout): + """ print Ordinance in Gecom format """ if self.date: - file.write(cont('2 DATE ' + self.date) + '\n') + file.write(cont("2 DATE " + self.date)) if self.temple_code: - file.write('2 TEMP ' + self.temple_code + '\n') + file.write("2 TEMP %s\n" % self.temple_code) if self.status in ORDINANCES_STATUS: - file.write('2 STAT ' + ORDINANCES_STATUS[self.status] + '\n') + file.write("2 STAT %s\n" % ORDINANCES_STATUS[self.status]) if self.famc: - file.write('2 FAMC @F' + str(self.famc.num) + '@\n') + file.write("2 FAMC @F%s@\n" % self.famc.num) -# GEDCOM individual class class Indi: + """ GEDCOM individual class + :param fid' FamilySearch id + :param tree: a tree object + :param num: the GEDCOM identifier + """ counter = 0 - # initialize individual def __init__(self, fid=None, tree=None, num=None): if num: self.num = num @@ -474,159 +544,180 @@ class Indi: self.memories = set() def add_data(self, data): + """ add FS individual data """ if data: - if data['names']: - for x in data['names']: - if x['preferred']: + if data["names"]: + for x in data["names"]: + if x["preferred"]: self.name = Name(x, self.tree) else: - if x['type'] == u'http://gedcomx.org/Nickname': + if x["type"] == "http://gedcomx.org/Nickname": self.nicknames.add(Name(x, self.tree)) - if x['type'] == u'http://gedcomx.org/BirthName': + if x["type"] == "http://gedcomx.org/BirthName": self.birthnames.add(Name(x, self.tree)) - if x['type'] == u'http://gedcomx.org/AlsoKnownAs': + if x["type"] == "http://gedcomx.org/AlsoKnownAs": self.aka.add(Name(x, self.tree)) - if x['type'] == u'http://gedcomx.org/MarriedName': + if x["type"] == "http://gedcomx.org/MarriedName": self.married.add(Name(x, self.tree)) - if 'gender' in data: - if data['gender']['type'] == 'http://gedcomx.org/Male': - self.gender = 'M' - elif data['gender']['type'] == 'http://gedcomx.org/Female': - self.gender = 'F' - elif data['gender']['type'] == 'http://gedcomx.org/Unknown': - self.gender = 'U' - if 'facts' in data: - for x in data['facts']: - if x['type'] == u'http://familysearch.org/v1/LifeSketch': - self.notes.add(Note('=== ' + self.tree.fs._('Life Sketch') + ' ===\n' + x['value'], self.tree)) + if "gender" in data: + if data["gender"]["type"] == "http://gedcomx.org/Male": + self.gender = "M" + elif data["gender"]["type"] == "http://gedcomx.org/Female": + self.gender = "F" + elif data["gender"]["type"] == "http://gedcomx.org/Unknown": + self.gender = "U" + if "facts" in data: + for x in data["facts"]: + if x["type"] == "http://familysearch.org/v1/LifeSketch": + self.notes.add( + Note( + "=== %s ===\n%s" + % (self.tree.fs._("Life Sketch"), x.get("value", "")), + self.tree, + ) + ) else: self.facts.add(Fact(x, self.tree)) - if 'sources' in data: - sources = self.tree.fs.get_url('/platform/tree/persons/%s/sources.json' % self.fid) + if "sources" in data: + sources = self.tree.fs.get_url("/platform/tree/persons/%s/sources.json" % self.fid) if sources: quotes = dict() - for quote in sources['persons'][0]['sources']: - quotes[quote['descriptionId']] = quote['attribution']['changeMessage'] if 'changeMessage' in quote['attribution'] else None - for source in sources['sourceDescriptions']: - if source['id'] not in self.tree.sources: - self.tree.sources[source['id']] = Source(source, self.tree) - self.sources.add((self.tree.sources[source['id']], quotes[source['id']])) - if 'evidence' in data: - url = '/platform/tree/persons/%s/memories.json' % self.fid + for quote in sources["persons"][0]["sources"]: + quotes[quote["descriptionId"]] = ( + quote["attribution"]["changeMessage"] + if "changeMessage" in quote["attribution"] + else None + ) + for source in sources["sourceDescriptions"]: + if source["id"] not in self.tree.sources: + self.tree.sources[source["id"]] = Source(source, self.tree) + self.sources.add((self.tree.sources[source["id"]], quotes[source["id"]])) + if "evidence" in data: + url = "/platform/tree/persons/%s/memories.json" % self.fid memorie = self.tree.fs.get_url(url) - if memorie and 'sourceDescriptions' in memorie: - for x in memorie['sourceDescriptions']: - if x['mediaType'] == 'text/plain': - text = '\n'.join(val.get('value', '') for val in x.get('titles', []) + x.get('descriptions', [])) + if memorie and "sourceDescriptions" in memorie: + for x in memorie["sourceDescriptions"]: + if x["mediaType"] == "text/plain": + text = "\n".join( + val.get("value", "") + for val in x.get("titles", []) + x.get("descriptions", []) + ) self.notes.add(Note(text, self.tree)) else: self.memories.add(Memorie(x)) - # add a fams to the individual def add_fams(self, fams): + """ add family fid (for spouse or parent)""" self.fams_fid.add(fams) - # add a famc to the individual def add_famc(self, famc): + """ add family fid (for child) """ self.famc_fid.add(famc) - # retrieve individual notes def get_notes(self): - notes = self.tree.fs.get_url('/platform/tree/persons/%s/notes.json' % self.fid) + """ retrieve individual notes """ + notes = self.tree.fs.get_url("/platform/tree/persons/%s/notes.json" % self.fid) if notes: - for n in notes['persons'][0]['notes']: - text_note = '=== ' + n['subject'] + ' ===\n' if 'subject' in n else '' - text_note += n['text'] + '\n' if 'text' in n else '' + for n in notes["persons"][0]["notes"]: + text_note = "=== %s ===\n" % n["subject"] if 'subject' in n else "" + text_note += n["text"] + "\n" if "text" in n else "" self.notes.add(Note(text_note, self.tree)) - # retrieve LDS ordinances def get_ordinances(self): + """ retrieve LDS ordinances + need a LDS account + """ res = [] famc = False - url = '/platform/tree/persons/%s/ordinances.json' % self.fid - data = self.tree.fs.get_url(url)['persons'][0]['ordinances'] + url = "/platform/tree/persons/%s/ordinances.json" % self.fid + data = self.tree.fs.get_url(url)["persons"][0]["ordinances"] if data: for o in data: - if o['type'] == u'http://lds.org/Baptism': + if o["type"] == "http://lds.org/Baptism": self.baptism = Ordinance(o) - elif o['type'] == u'http://lds.org/Confirmation': + elif o["type"] == "http://lds.org/Confirmation": self.confirmation = Ordinance(o) - elif o['type'] == u'http://lds.org/Endowment': + elif o["type"] == "http://lds.org/Endowment": self.endowment = Ordinance(o) - elif o['type'] == u'http://lds.org/SealingChildToParents': + elif o["type"] == "http://lds.org/SealingChildToParents": self.sealing_child = Ordinance(o) - if 'father' in o and 'mother' in o: - famc = (o['father']['resourceId'], - o['mother']['resourceId']) - elif o['type'] == u'http://lds.org/SealingToSpouse': + if "father" in o and "mother" in o: + famc = (o["father"]["resourceId"], o["mother"]["resourceId"]) + elif o["type"] == "http://lds.org/SealingToSpouse": res.append(o) return res, famc - # retrieve contributors def get_contributors(self): + """ retrieve contributors """ temp = set() - data = self.tree.fs.get_url('/platform/tree/persons/%s/changes.json' % self.fid) + data = self.tree.fs.get_url("/platform/tree/persons/%s/changes.json" % self.fid) if data: - for entries in data['entries']: - for contributors in entries['contributors']: - temp.add(contributors['name']) + for entries in data["entries"]: + for contributors in entries["contributors"]: + temp.add(contributors["name"]) if temp: - text = '=== ' + self.tree.fs._('Contributors') + ' ===\n' + '\n'.join(sorted(temp)) + text = "=== %s ===\n%s" % (self.tree.fs._("Contributors"), "\n".join(sorted(temp))) for n in self.tree.notes: if n.text == text: self.notes.add(n) return self.notes.add(Note(text, self.tree)) - # print individual information in GEDCOM format def print(self, file=sys.stdout): - file.write('0 @I' + str(self.num) + '@ INDI\n') + """ print individual in GEDCOM format """ + file.write("0 @I%s@ INDI\n" % self.num) if self.name: self.name.print(file) for o in self.nicknames: - file.write(cont('2 NICK ' + o.given + ' ' + o .surname) + '\n') + file.write(cont("2 NICK %s %s" % (o.given, o.surname))) for o in self.birthnames: o.print(file) for o in self.aka: - o.print(file, 'aka') + o.print(file, "aka") for o in self.married: - o.print(file, 'married') + o.print(file, "married") if self.gender: - file.write('1 SEX ' + self.gender + '\n') + file.write("1 SEX %s\n" % self.gender) for o in self.facts: o.print(file) for o in self.memories: o.print(file) if self.baptism: - file.write('1 BAPL\n') + file.write("1 BAPL\n") self.baptism.print(file) if self.confirmation: - file.write('1 CONL\n') + file.write("1 CONL\n") self.confirmation.print(file) if self.endowment: - file.write('1 ENDL\n') + file.write("1 ENDL\n") self.endowment.print(file) if self.sealing_child: - file.write('1 SLGC\n') + file.write("1 SLGC\n") self.sealing_child.print(file) for num in self.fams_num: - file.write('1 FAMS @F' + str(num) + '@\n') + file.write("1 FAMS @F%s@\n" % num) + file.write("1 FAMS @F%s@\n" % num) for num in self.famc_num: - file.write('1 FAMC @F' + str(num) + '@\n') - file.write('1 _FSFTID ' + self.fid + '\n') + file.write("1 FAMC @F%s@\n" % num) + file.write("1 _FSFTID %s\n" % self.fid) for o in self.notes: o.link(file) for source, quote in self.sources: source.link(file, 1) if quote: - file.write(cont('2 PAGE ' + quote) + '\n') + file.write(cont("2 PAGE " + quote)) -# GEDCOM family class class Fam: + """ GEDCOM family class + :param husb: husbant fid + :param wife: wife fid + :param tree: a Tree object + :param num: a GEDCOM identifier + """ + counter = 0 - # initialize family def __init__(self, husb=None, wife=None, tree=None, num=None): if num: self.num = num @@ -644,87 +735,105 @@ class Fam: self.notes = set() self.sources = set() - # add a child to the family def add_child(self, child): + """ add a child fid to the family """ if child not in self.chil_fid: self.chil_fid.add(child) - # retrieve and add marriage information def add_marriage(self, fid): + """ retrieve and add marriage information + :param fid: the marriage fid + """ if not self.fid: self.fid = fid - url = '/platform/tree/couple-relationships/%s.json' % self.fid + url = "/platform/tree/couple-relationships/%s.json" % self.fid data = self.tree.fs.get_url(url) if data: - if 'facts' in data['relationships'][0]: - for x in data['relationships'][0]['facts']: + if "facts" in data["relationships"][0]: + for x in data["relationships"][0]["facts"]: self.facts.add(Fact(x, self.tree)) - if 'sources' in data['relationships'][0]: + if "sources" in data["relationships"][0]: quotes = dict() - for x in data['relationships'][0]['sources']: - quotes[x['descriptionId']] = x['attribution']['changeMessage'] if 'changeMessage' in x['attribution'] else None + for x in data["relationships"][0]["sources"]: + quotes[x["descriptionId"]] = ( + x["attribution"]["changeMessage"] + if "changeMessage" in x["attribution"] + else None + ) new_sources = quotes.keys() - self.tree.sources.keys() if new_sources: - sources = self.tree.fs.get_url('/platform/tree/couple-relationships/%s/sources.json' % self.fid) - for source in sources['sourceDescriptions']: - if source['id'] in new_sources and source['id'] not in self.tree.sources: - self.tree.sources[source['id']] = Source(source, self.tree) + sources = self.tree.fs.get_url( + "/platform/tree/couple-relationships/%s/sources.json" % self.fid + ) + for source in sources["sourceDescriptions"]: + if ( + source["id"] in new_sources + and source["id"] not in self.tree.sources + ): + self.tree.sources[source["id"]] = Source(source, self.tree) for source_fid in quotes: self.sources.add((self.tree.sources[source_fid], quotes[source_fid])) - # retrieve marriage notes def get_notes(self): + """ retrieve marriage notes """ if self.fid: - notes = self.tree.fs.get_url('/platform/tree/couple-relationships/%s/notes.json' % self.fid) + notes = self.tree.fs.get_url( + "/platform/tree/couple-relationships/%s/notes.json" % self.fid + ) if notes: - for n in notes['relationships'][0]['notes']: - text_note = '=== ' + n['subject'] + ' ===\n' if 'subject' in n else '' - text_note += n['text'] + '\n' if 'text' in n else '' + for n in notes["relationships"][0]["notes"]: + text_note = "=== %s ===\n" % n["subject"] if "subject" in n else "" + text_note += n["text"] + "\n" if "text" in n else "" self.notes.add(Note(text_note, self.tree)) - # retrieve contributors def get_contributors(self): + """ retrieve contributors """ if self.fid: temp = set() - data = self.tree.fs.get_url('/platform/tree/couple-relationships/%s/changes.json' % self.fid) + data = self.tree.fs.get_url( + "/platform/tree/couple-relationships/%s/changes.json" % self.fid + ) if data: - for entries in data['entries']: - for contributors in entries['contributors']: - temp.add(contributors['name']) + for entries in data["entries"]: + for contributors in entries["contributors"]: + temp.add(contributors["name"]) if temp: - text = '=== ' + self.tree.fs._('Contributors') + ' ===\n' + '\n'.join(sorted(temp)) + text = "=== %s ===\n%s" % (self.tree.fs._("Contributors"), "\n".join(sorted(temp))) for n in self.tree.notes: if n.text == text: self.notes.add(n) return self.notes.add(Note(text, self.tree)) - # print family information in GEDCOM format def print(self, file=sys.stdout): - file.write('0 @F' + str(self.num) + '@ FAM\n') + """ print family information in GEDCOM format """ + file.write("0 @F%s@ FAM\n" % self.num) if self.husb_num: - file.write('1 HUSB @I' + str(self.husb_num) + '@\n') + file.write("1 HUSB @I%s@\n" % self.husb_num) if self.wife_num: - file.write('1 WIFE @I' + str(self.wife_num) + '@\n') + file.write("1 WIFE @I%s@\n" % self.wife_num) for num in self.chil_num: - file.write('1 CHIL @I' + str(num) + '@\n') + file.write("1 CHIL @I%s@\n" % num) for o in self.facts: o.print(file) if self.sealing_spouse: - file.write('1 SLGS\n') + file.write("1 SLGS\n") self.sealing_spouse.print(file) if self.fid: - file.write('1 _FSFTID ' + self.fid + '\n') + file.write("1 _FSFTID %s\n" % self.fid) for o in self.notes: o.link(file) for source, quote in self.sources: source.link(file, 1) if quote: - file.write(cont('2 PAGE ' + quote) + '\n') + file.write(cont("2 PAGE " + quote)) -# family tree class class Tree: + """ family tree class + :param fs: a Session object + """ + def __init__(self, fs=None): self.fs = fs self.indi = dict() @@ -733,58 +842,72 @@ class Tree: self.sources = dict() self.places = dict() - # add individuals to the family tree def add_indis(self, fids): + """ add individuals to the family tree + :param fids: an iterable of fid + """ + async def add_datas(loop, data): futures = set() - for person in data['persons']: - self.indi[person['id']] = Indi(person['id'], self) - futures.add(loop.run_in_executor(None, self.indi[person['id']].add_data, person)) + for person in data["persons"]: + self.indi[person["id"]] = Indi(person["id"], self) + futures.add(loop.run_in_executor(None, self.indi[person["id"]].add_data, person)) for future in futures: await future new_fids = [fid for fid in fids if fid and fid not in self.indi] loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) - # loop = asyncio.get_event_loop() - while len(new_fids): - data = self.fs.get_url('/platform/tree/persons.json?pids=' + ','.join(new_fids[:MAX_PERSONS])) + while new_fids: + data = self.fs.get_url( + "/platform/tree/persons.json?pids=" + ",".join(new_fids[:MAX_PERSONS]) + ) if data: - if 'places' in data: - for place in data['places']: - if place['id'] not in self.places: - self.places[place['id']] = (str(place['latitude']), str(place['longitude'])) + if "places" in data: + for place in data["places"]: + if place["id"] not in self.places: + self.places[place["id"]] = ( + str(place["latitude"]), + str(place["longitude"]), + ) loop.run_until_complete(add_datas(loop, data)) - if 'childAndParentsRelationships' in data: - for rel in data['childAndParentsRelationships']: - father = rel['father']['resourceId'] if 'father' in rel else None - mother = rel['mother']['resourceId'] if 'mother' in rel else None - child = rel['child']['resourceId'] if 'child' in rel else None + if "childAndParentsRelationships" in data: + for rel in data["childAndParentsRelationships"]: + father = rel["father"]["resourceId"] if "father" in rel else None + mother = rel["mother"]["resourceId"] if "mother" in rel else None + child = rel["child"]["resourceId"] if "child" in rel else None if child in self.indi: self.indi[child].parents.add((father, mother)) if father in self.indi: self.indi[father].children.add((father, mother, child)) if mother in self.indi: self.indi[mother].children.add((father, mother, child)) - if 'relationships' in data: - for rel in data['relationships']: - if rel['type'] == u'http://gedcomx.org/Couple': - person1 = rel['person1']['resourceId'] - person2 = rel['person2']['resourceId'] - relfid = rel['id'] + if "relationships" in data: + for rel in data["relationships"]: + if rel["type"] == "http://gedcomx.org/Couple": + person1 = rel["person1"]["resourceId"] + person2 = rel["person2"]["resourceId"] + relfid = rel["id"] if person1 in self.indi: self.indi[person1].spouses.add((person1, person2, relfid)) if person2 in self.indi: self.indi[person2].spouses.add((person1, person2, relfid)) new_fids = new_fids[MAX_PERSONS:] - # add family to the family tree def add_fam(self, father, mother): - if not (father, mother) in self.fam: + """ add a family to the family tree + :param father: the father fid or None + :param mother: the mother fid or None + """ + if (father, mother) not in self.fam: self.fam[(father, mother)] = Fam(father, mother, self) - # add a children relationship (possibly incomplete) to the family tree def add_trio(self, father, mother, child): + """ add a children relationship to the family tree + :param father: the father fid or None + :param mother: the mother fid or None + :param child: the child fid or None + """ if father in self.indi: self.indi[father].add_fams((father, mother)) if mother in self.indi: @@ -794,32 +917,46 @@ class Tree: self.add_fam(father, mother) self.fam[(father, mother)].add_child(child) - # add parents relationships def add_parents(self, fids): + """ add parents relationships + :param fids: a set of fids + """ parents = set() - for fid in (fids & self.indi.keys()): + for fid in fids & self.indi.keys(): for couple in self.indi[fid].parents: parents |= set(couple) if parents: self.add_indis(parents) - for fid in (fids & self.indi.keys()): + for fid in fids & self.indi.keys(): for father, mother in self.indi[fid].parents: - if mother in self.indi and father in self.indi or not father and mother in self.indi or not mother and father in self.indi: + if ( + mother in self.indi + and father in self.indi + or not father + and mother in self.indi + or not mother + and father in self.indi + ): self.add_trio(father, mother, fid) return set(filter(None, parents)) - # add spouse relationships def add_spouses(self, fids): + """ add spouse relationships + :param fids: a set of fid + """ + async def add(loop, rels): futures = set() for father, mother, relfid in rels: if (father, mother) in self.fam: - futures.add(loop.run_in_executor(None, self.fam[(father, mother)].add_marriage, relfid)) + futures.add( + loop.run_in_executor(None, self.fam[(father, mother)].add_marriage, relfid) + ) for future in futures: await future rels = set() - for fid in (fids & self.indi.keys()): + for fid in fids & self.indi.keys(): rels |= self.indi[fid].spouses loop = asyncio.get_event_loop() if rels: @@ -831,59 +968,75 @@ class Tree: self.add_fam(father, mother) loop.run_until_complete(add(loop, rels)) - # add children relationships def add_children(self, fids): + """ add children relationships + :param fids: a set of fid + """ rels = set() - for fid in (fids & self.indi.keys()): + for fid in fids & self.indi.keys(): rels |= self.indi[fid].children if fid in self.indi else set() children = set() if rels: self.add_indis(set.union(*(set(rel) for rel in rels))) for father, mother, child in rels: - if child in self.indi and (mother in self.indi and father in self.indi or not father and mother in self.indi or not mother and father in self.indi): + if child in self.indi and ( + mother in self.indi + and father in self.indi + or not father + and mother in self.indi + or not mother + and father in self.indi + ): self.add_trio(father, mother, child) children.add(child) return children - # retrieve ordinances def add_ordinances(self, fid): + """ retrieve ordinances + :param fid: an individual fid + """ if fid in self.indi: ret, famc = self.indi[fid].get_ordinances() if famc and famc in self.fam: self.indi[fid].sealing_child.famc = self.fam[famc] for o in ret: - if (fid, o['spouse']['resourceId']) in self.fam: - self.fam[(fid, o['spouse']['resourceId']) - ].sealing_spouse = Ordinance(o) - elif (o['spouse']['resourceId'], fid) in self.fam: - self.fam[(o['spouse']['resourceId'], fid) - ].sealing_spouse = Ordinance(o) + if (fid, o["spouse"]["resourceId"]) in self.fam: + self.fam[(fid, o["spouse"]["resourceId"])].sealing_spouse = Ordinance(o) + elif (o["spouse"]["resourceId"], fid) in self.fam: + self.fam[(o["spouse"]["resourceId"], fid)].sealing_spouse = Ordinance(o) def reset_num(self): + """ reset all GEDCOM identifiers """ for husb, wife in self.fam: self.fam[(husb, wife)].husb_num = self.indi[husb].num if husb else None self.fam[(husb, wife)].wife_num = self.indi[wife].num if wife else None - self.fam[(husb, wife)].chil_num = set([self.indi[chil].num for chil in self.fam[(husb, wife)].chil_fid]) + self.fam[(husb, wife)].chil_num = set( + self.indi[chil].num for chil in self.fam[(husb, wife)].chil_fid + ) for fid in self.indi: - self.indi[fid].famc_num = set([self.fam[(husb, wife)].num for husb, wife in self.indi[fid].famc_fid]) - self.indi[fid].fams_num = set([self.fam[(husb, wife)].num for husb, wife in self.indi[fid].fams_fid]) + self.indi[fid].famc_num = set( + self.fam[(husb, wife)].num for husb, wife in self.indi[fid].famc_fid + ) + self.indi[fid].fams_num = set( + self.fam[(husb, wife)].num for husb, wife in self.indi[fid].fams_fid + ) - # print GEDCOM file def print(self, file=sys.stdout): - file.write('0 HEAD\n') - file.write('1 CHAR UTF-8\n') - file.write('1 GEDC\n') - file.write('2 VERS 5.5.1\n') - file.write('2 FORM LINEAGE-LINKED\n') - file.write('1 SOUR getmyancestors\n') - file.write('2 VERS 1.0\n') - file.write('2 NAME getmyancestors\n') - file.write('1 DATE ' + time.strftime('%d %b %Y') + '\n') - file.write('2 TIME ' + time.strftime('%H:%M:%S') + '\n') - file.write('1 SUBM @SUBM@\n') - file.write('0 @SUBM@ SUBM\n') - file.write('1 NAME ' + self.fs.display_name + '\n') - file.write('1 LANG ' + babelfish.Language.fromalpha2(self.fs.lang).name + '\n') + """ print family tree in GEDCOM format """ + file.write("0 HEAD\n") + file.write("1 CHAR UTF-8\n") + file.write("1 GEDC\n") + file.write("2 VERS 5.5.1\n") + file.write("2 FORM LINEAGE-LINKED\n") + file.write("1 SOUR getmyancestors\n") + file.write("2 VERS 1.0\n") + file.write("2 NAME getmyancestors\n") + file.write("1 DATE %s\n" % time.strftime("%d %b %Y")) + file.write("2 TIME %s\n" % time.strftime("%H:%M:%S")) + file.write("1 SUBM @SUBM@\n") + file.write("0 @SUBM@ SUBM\n") + file.write("1 NAME %s\n" % self.fs.display_name) + file.write("1 LANG %s\n" % babelfish.Language.fromalpha2(self.fs.lang).name) for fid in sorted(self.indi, key=lambda x: self.indi.__getitem__(x).num): self.indi[fid].print(file) @@ -898,28 +1051,70 @@ class Tree: if n.num == notes[i - 1].num: continue n.print(file) - file.write('0 TRLR\n') - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Retrieve GEDCOM data from FamilySearch Tree (4 Jul 2016)', add_help=False, usage='getmyancestors.py -u username -p password [options]') - parser.add_argument('-u', metavar='', type=str, help='FamilySearch username') - parser.add_argument('-p', metavar='', type=str, help='FamilySearch password') - parser.add_argument('-i', metavar='', nargs='+', type=str, help='List of individual FamilySearch IDs for whom to retrieve ancestors') - parser.add_argument('-a', metavar='', type=int, default=4, help='Number of generations to ascend [4]') - parser.add_argument('-d', metavar='', type=int, default=0, help='Number of generations to descend [0]') - parser.add_argument('-m', action="store_true", default=False, help='Add spouses and couples information [False]') - parser.add_argument('-r', action="store_true", default=False, help='Add list of contributors in notes [False]') - parser.add_argument('-c', action="store_true", default=False, help='Add LDS ordinances (need LDS account) [False]') - parser.add_argument("-v", action="store_true", default=False, help="Increase output verbosity [False]") - parser.add_argument('-t', metavar='', type=int, default=60, help='Timeout in seconds [60]') - parser.add_argument('--show-password', action="store_true", default=False, help="Show password in .settings file [False]") + file.write("0 TRLR\n") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Retrieve GEDCOM data from FamilySearch Tree (4 Jul 2016)", + add_help=False, + usage="getmyancestors.py -u username -p password [options]", + ) + parser.add_argument("-u", metavar="", type=str, help="FamilySearch username") + parser.add_argument("-p", metavar="", type=str, help="FamilySearch password") + parser.add_argument( + "-i", + metavar="", + nargs="+", + type=str, + help="List of individual FamilySearch IDs for whom to retrieve ancestors", + ) + parser.add_argument( + "-a", metavar="", type=int, default=4, help="Number of generations to ascend [4]" + ) + parser.add_argument( + "-d", metavar="", type=int, default=0, help="Number of generations to descend [0]" + ) + parser.add_argument( + "-m", action="store_true", default=False, help="Add spouses and couples information [False]" + ) + parser.add_argument( + "-r", action="store_true", default=False, help="Add list of contributors in notes [False]" + ) + parser.add_argument( + "-c", + action="store_true", + default=False, + help="Add LDS ordinances (need LDS account) [False]", + ) + parser.add_argument( + "-v", action="store_true", default=False, help="Increase output verbosity [False]" + ) + parser.add_argument("-t", metavar="", type=int, default=60, help="Timeout in seconds [60]") + parser.add_argument( + "--show-password", + action="store_true", + default=False, + help="Show password in .settings file [False]", + ) try: - parser.add_argument('-o', metavar='', type=argparse.FileType('w', encoding='UTF-8'), default=sys.stdout, help='output GEDCOM file [stdout]') - parser.add_argument('-l', metavar='', type=argparse.FileType('w', encoding='UTF-8'), default=sys.stderr, help='output log file [stderr]') + parser.add_argument( + "-o", + metavar="", + type=argparse.FileType("w", encoding="UTF-8"), + default=sys.stdout, + help="output GEDCOM file [stdout]", + ) + parser.add_argument( + "-l", + metavar="", + type=argparse.FileType("w", encoding="UTF-8"), + default=sys.stderr, + help="output log file [stderr]", + ) except TypeError: - sys.stderr.write('Python >= 3.4 is required to run this script\n') - sys.stderr.write('(see https://docs.python.org/3/whatsnew/3.4.html#argparse)\n') + sys.stderr.write("Python >= 3.4 is required to run this script\n") + sys.stderr.write("(see https://docs.python.org/3/whatsnew/3.4.html#argparse)\n") exit(2) # extract arguments from the command line @@ -932,8 +1127,8 @@ if __name__ == '__main__': if args.i: for fid in args.i: - if not re.match(r'[A-Z0-9]{4}-[A-Z0-9]{3}', fid): - exit('Invalid FamilySearch ID: ' + fid) + if not re.match(r"[A-Z0-9]{4}-[A-Z0-9]{3}", fid): + exit("Invalid FamilySearch ID: " + fid) username = args.u if args.u else input("Enter FamilySearch username: ") password = args.p if args.p else getpass.getpass("Enter FamilySearch password: ") @@ -943,35 +1138,36 @@ if __name__ == '__main__': # Report settings used when getmyancestors.py is executed. setting_list = [ - string for setting in [ + string + for setting in [ [ - str('-' + action.dest + ' ' + action.help), + str("-" + action.dest + " " + action.help), username - if action.dest is 'u' + if action.dest is "u" else password - if action.dest is 'p' and args.show_password - else '******' - if action.dest is 'p' + if action.dest is "p" and args.show_password + else "******" + if action.dest is "p" else str(vars(args)[action.dest].name) - if hasattr(vars(args)[action.dest], 'name') - else str(vars(args)[action.dest])] - for action in vars(parser)['_actions']] for string in setting] - setting_list.insert(0, time.strftime('%X %x %Z')) - setting_list.insert(0, 'time stamp: ') - - formatting = '{:74}{:\t>1}\n' * int(len(setting_list) / 2) - settings_output = (( - formatting - ).format( - *setting_list)) - - if not (args.o.name == ''): - with open( - args.o.name.split('.')[0] + '.settings', 'w') as settings_record: - settings_record.write(settings_output) + if hasattr(vars(args)[action.dest], "name") + else str(vars(args)[action.dest]), + ] + for action in vars(parser)["_actions"] + ] + for string in setting + ] + setting_list.insert(0, time.strftime("%X %x %Z")) + setting_list.insert(0, "time stamp: ") + + formatting = "{:74}{:\t>1}\n" * int(len(setting_list) / 2) + settings_output = (formatting).format(*setting_list) + + if args.o.name != "": + with open(args.o.name.split(".")[0] + ".settings", "w") as settings_record: + settings_record.write(settings_output) # initialize a FamilySearch session and a family tree object - print('Login to FamilySearch...') + print("Login to FamilySearch...") fs = Session(username, password, args.v, args.l, args.t) if not fs.logged: exit(2) @@ -979,12 +1175,15 @@ if __name__ == '__main__': tree = Tree(fs) # check LDS account - if args.c and fs.get_url('/platform/tree/persons/%s/ordinances.json' % fs.get_userid()) == 'error': + if ( + args.c + and fs.get_url("/platform/tree/persons/%s/ordinances.json" % fs.get_userid()) == "error" + ): exit(2) # add list of starting individuals to the family tree todo = args.i if args.i else [fs.get_userid()] - print(_('Downloading starting individuals...')) + print(_("Downloading starting individuals...")) tree.add_indis(todo) # download ancestors @@ -994,7 +1193,7 @@ if __name__ == '__main__': if not todo: break done |= todo - print(_('Downloading %s. of generations of ancestors...') % (i + 1)) + print(_("Downloading %s. of generations of ancestors...") % (i + 1)) todo = tree.add_parents(todo) - done # download descendants @@ -1004,12 +1203,12 @@ if __name__ == '__main__': if not todo: break done |= todo - print(_('Downloading %s. of generations of descendants...') % (i + 1)) + print(_("Downloading %s. of generations of descendants...") % (i + 1)) todo = tree.add_children(todo) - done # download spouses if args.m: - print(_('Downloading spouses and marriage information...')) + print(_("Downloading spouses and marriage information...")) todo = set(tree.indi.keys()) tree.add_spouses(todo) @@ -1030,10 +1229,28 @@ if __name__ == '__main__': await future loop = asyncio.get_event_loop() - print(_('Downloading notes') + (((',' if args.r else _(' and')) + _(' ordinances')) if args.c else '') + (_(' and contributors') if args.r else '') + '...') + print( + _("Downloading notes") + + ((("," if args.r else _(" and")) + _(" ordinances")) if args.c else "") + + (_(" and contributors") if args.r else "") + + "..." + ) loop.run_until_complete(download_stuff(loop)) # compute number for family relationships and print GEDCOM file tree.reset_num() tree.print(args.o) - print(_('Downloaded %s individuals, %s families, %s sources and %s notes in %s seconds with %s HTTP requests.') % (str(len(tree.indi)), str(len(tree.fam)), str(len(tree.sources)), str(len(tree.notes)), str(round(time.time() - time_count)), str(fs.counter))) + print( + _( + "Downloaded %s individuals, %s families, %s sources and %s notes " + "in %s seconds with %s HTTP requests." + ) + % ( + str(len(tree.indi)), + str(len(tree.fam)), + str(len(tree.sources)), + str(len(tree.notes)), + str(round(time.time() - time_count)), + str(fs.counter), + ) + ) diff --git a/mergemyancestors.py b/mergemyancestors.py index 8af9c4e..99809d4 100755 --- a/mergemyancestors.py +++ b/mergemyancestors.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- +# coding: utf-8 """ mergemyancestors.py - Merge GEDCOM data from FamilySearch Tree Copyright (C) 2014-2016 Giulio Genovese (giulio.genovese@gmail.com) @@ -29,7 +29,7 @@ import sys import argparse # local import -from getmyancestors import * +import getmyancestors as gt sys.path.append(os.path.dirname(sys.argv[0])) @@ -38,11 +38,12 @@ def reversed_dict(d): return {val: key for key, val in d.items()} -FACT_TYPES = reversed_dict(FACT_TAGS) -ORDINANCES = reversed_dict(ORDINANCES_STATUS) +FACT_TYPES = reversed_dict(gt.FACT_TAGS) +ORDINANCES = reversed_dict(gt.ORDINANCES_STATUS) class Gedcom: + """ Parse a GEDCOM file into a Tree """ def __init__(self, file, tree): self.f = file @@ -61,31 +62,34 @@ class Gedcom: self.__add_id() def __parse(self): + """ Parse the GEDCOM file into self.tree """ while self.__get_line(): - if self.tag == 'INDI': - self.num = int(self.pointer[2:len(self.pointer) - 1]) - self.indi[self.num] = Indi(tree=self.tree, num=self.num) + if self.tag == "INDI": + self.num = int(self.pointer[2 : len(self.pointer) - 1]) + self.indi[self.num] = gt.Indi(tree=self.tree, num=self.num) self.__get_indi() - elif self.tag == 'FAM': - self.num = int(self.pointer[2:len(self.pointer) - 1]) + elif self.tag == "FAM": + self.num = int(self.pointer[2 : len(self.pointer) - 1]) if self.num not in self.fam: - self.fam[self.num] = Fam(tree=self.tree, num=self.num) + self.fam[self.num] = gt.Fam(tree=self.tree, num=self.num) self.__get_fam() - elif self.tag == 'NOTE': - self.num = int(self.pointer[2:len(self.pointer) - 1]) + elif self.tag == "NOTE": + self.num = int(self.pointer[2 : len(self.pointer) - 1]) if self.num not in self.note: - self.note[self.num] = Note(tree=self.tree, num=self.num) + self.note[self.num] = gt.Note(tree=self.tree, num=self.num) self.__get_note() - elif self.tag == 'SOUR': - self.num = int(self.pointer[2:len(self.pointer) - 1]) + elif self.tag == "SOUR": + self.num = int(self.pointer[2 : len(self.pointer) - 1]) if self.num not in self.sour: - self.sour[self.num] = Source(num=self.num) + self.sour[self.num] = gt.Source(num=self.num) self.__get_source() else: continue def __get_line(self): - # if the flag is set, skip reading a newline + """ Parse a new line + If the flag is set, skip reading a newline + """ if self.flag: self.flag = False return True @@ -94,75 +98,78 @@ class Gedcom: if not words: return False self.level = int(words[0]) - if words[1][0] == '@': + if words[1][0] == "@": self.pointer = words[1] self.tag = words[2] - self.data = ' '.join(words[3:]) + self.data = " ".join(words[3:]) else: self.pointer = None self.tag = words[1] - self.data = ' '.join(words[2:]) + self.data = " ".join(words[2:]) return True def __get_indi(self): + """ Parse an individual """ while self.f and self.__get_line() and self.level > 0: - if self.tag == 'NAME': + if self.tag == "NAME": self.__get_name() - elif self.tag == 'SEX': + elif self.tag == "SEX": self.indi[self.num].gender = self.data - elif self.tag in FACT_TYPES or self.tag == 'EVEN': + elif self.tag in FACT_TYPES or self.tag == "EVEN": self.indi[self.num].facts.add(self.__get_fact()) - elif self.tag == 'BAPL': + elif self.tag == "BAPL": self.indi[self.num].baptism = self.__get_ordinance() - elif self.tag == 'CONL': + elif self.tag == "CONL": self.indi[self.num].confirmation = self.__get_ordinance() - elif self.tag == 'ENDL': + elif self.tag == "ENDL": self.indi[self.num].endowment = self.__get_ordinance() - elif self.tag == 'SLGC': + elif self.tag == "SLGC": self.indi[self.num].sealing_child = self.__get_ordinance() - elif self.tag == 'FAMS': - self.indi[self.num].fams_num.add(int(self.data[2:len(self.data) - 1])) - elif self.tag == 'FAMC': - self.indi[self.num].famc_num.add(int(self.data[2:len(self.data) - 1])) - elif self.tag == '_FSFTID': + elif self.tag == "FAMS": + self.indi[self.num].fams_num.add(int(self.data[2 : len(self.data) - 1])) + elif self.tag == "FAMC": + self.indi[self.num].famc_num.add(int(self.data[2 : len(self.data) - 1])) + elif self.tag == "_FSFTID": self.indi[self.num].fid = self.data - elif self.tag == 'NOTE': - num = int(self.data[2:len(self.data) - 1]) + elif self.tag == "NOTE": + num = int(self.data[2 : len(self.data) - 1]) if num not in self.note: - self.note[num] = Note(tree=self.tree, num=num) + self.note[num] = gt.Note(tree=self.tree, num=num) self.indi[self.num].notes.add(self.note[num]) - elif self.tag == 'SOUR': + elif self.tag == "SOUR": self.indi[self.num].sources.add(self.__get_link_source()) - elif self.tag == 'OBJE': + elif self.tag == "OBJE": self.indi[self.num].memories.add(self.__get_memorie()) self.flag = True def __get_fam(self): + """ Parse a family """ while self.__get_line() and self.level > 0: - if self.tag == 'HUSB': - self.fam[self.num].husb_num = int(self.data[2:len(self.data) - 1]) - elif self.tag == 'WIFE': - self.fam[self.num].wife_num = int(self.data[2:len(self.data) - 1]) - elif self.tag == 'CHIL': - self.fam[self.num].chil_num.add(int(self.data[2:len(self.data) - 1])) + if self.tag == "HUSB": + self.fam[self.num].husb_num = int(self.data[2 : len(self.data) - 1]) + elif self.tag == "WIFE": + self.fam[self.num].wife_num = int(self.data[2 : len(self.data) - 1]) + elif self.tag == "CHIL": + self.fam[self.num].chil_num.add(int(self.data[2 : len(self.data) - 1])) elif self.tag in FACT_TYPES: self.fam[self.num].facts.add(self.__get_fact()) - elif self.tag == 'SLGS': + elif self.tag == "SLGS": self.fam[self.num].sealing_spouse = self.__get_ordinance() - elif self.tag == '_FSFTID': + elif self.tag == "_FSFTID": self.fam[self.num].fid = self.data - elif self.tag == 'NOTE': - num = int(self.data[2:len(self.data) - 1]) + elif self.tag == "NOTE": + num = int(self.data[2 : len(self.data) - 1]) if num not in self.note: - self.note[num] = Note(tree=self.tree, num=num) + self.note[num] = gt.Note(tree=self.tree, num=num) self.fam[self.num].notes.add(self.note[num]) - elif self.tag == 'SOUR': + elif self.tag == "SOUR": self.fam[self.num].sources.add(self.__get_link_source()) self.flag = True def __get_name(self): - parts = self.__get_text().split('/') - name = Name() + """ Parse a name """ + parts = self.__get_text().split("/") + name = gt.Name() added = False name.given = parts[0].strip() name.surname = parts[1].strip() @@ -172,74 +179,77 @@ class Gedcom: self.indi[self.num].name = name added = True while self.__get_line() and self.level > 1: - if self.tag == 'NPFX': + if self.tag == "NPFX": name.prefix = self.data - elif self.tag == 'TYPE': - if self.data == 'aka': + elif self.tag == "TYPE": + if self.data == "aka": self.indi[self.num].aka.add(name) added = True - elif self.data == 'married': + elif self.data == "married": self.indi[self.num].married.add(name) added = True - elif self.tag == 'NICK': - nick = Name() + elif self.tag == "NICK": + nick = gt.Name() nick.given = self.data self.indi[self.num].nicknames.add(nick) - elif self.tag == 'NOTE': - num = int(self.data[2:len(self.data) - 1]) + elif self.tag == "NOTE": + num = int(self.data[2 : len(self.data) - 1]) if num not in self.note: - self.note[num] = Note(tree=self.tree, num=num) + self.note[num] = gt.Note(tree=self.tree, num=num) name.note = self.note[num] if not added: self.indi[self.num].birthnames.add(name) self.flag = True def __get_fact(self): - fact = Fact() - if self.tag != 'EVEN': + """ Parse a fact """ + fact = gt.Fact() + if self.tag != "EVEN": fact.type = FACT_TYPES[self.tag] fact.value = self.data while self.__get_line() and self.level > 1: - if self.tag == 'TYPE': + if self.tag == "TYPE": fact.type = self.data - if self.tag == 'DATE': + if self.tag == "DATE": fact.date = self.__get_text() - elif self.tag == 'PLAC': + elif self.tag == "PLAC": fact.place = self.__get_text() - elif self.tag == 'MAP': + elif self.tag == "MAP": fact.map = self.__get_map() - elif self.tag == 'NOTE': - if self.data[:12] == 'Description:': + elif self.tag == "NOTE": + if self.data[:12] == "Description:": fact.value = self.data[13:] continue - num = int(self.data[2:len(self.data) - 1]) + num = int(self.data[2 : len(self.data) - 1]) if num not in self.note: - self.note[num] = Note(tree=self.tree, num=num) + self.note[num] = gt.Note(tree=self.tree, num=num) fact.note = self.note[num] - elif self.tag == 'CONT': - fact.value += '\n' + self.data - elif self.tag == 'CONC': + elif self.tag == "CONT": + fact.value += "\n" + self.data + elif self.tag == "CONC": fact.value += self.data self.flag = True return fact def __get_map(self): + """ Parse map coordinates """ latitude = None longitude = None while self.__get_line() and self.level > 3: - if self.tag == 'LATI': + if self.tag == "LATI": latitude = self.data - elif self.tag == 'LONG': + elif self.tag == "LONG": longitude = self.data self.flag = True return (latitude, longitude) def __get_text(self): + """ Parse a multiline text """ text = self.data while self.__get_line(): - if self.tag == 'CONT': - text += '\n' + self.data - elif self.tag == 'CONC': + if self.tag == "CONT": + text += "\n" + self.data + elif self.tag == "CONC": text += self.data else: break @@ -247,69 +257,75 @@ class Gedcom: return text def __get_source(self): + """ Parse a source """ while self.__get_line() and self.level > 0: - if self.tag == 'TITL': + if self.tag == "TITL": self.sour[self.num].title = self.__get_text() - elif self.tag == 'AUTH': + elif self.tag == "AUTH": self.sour[self.num].citation = self.__get_text() - elif self.tag == 'PUBL': + elif self.tag == "PUBL": self.sour[self.num].url = self.__get_text() - elif self.tag == 'REFN': + elif self.tag == "REFN": self.sour[self.num].fid = self.data if self.data in self.tree.sources: self.sour[self.num] = self.tree.sources[self.data] else: self.tree.sources[self.data] = self.sour[self.num] - elif self.tag == 'NOTE': - num = int(self.data[2:len(self.data) - 1]) + elif self.tag == "NOTE": + num = int(self.data[2 : len(self.data) - 1]) if num not in self.note: - self.note[num] = Note(tree=self.tree, num=num) + self.note[num] = gt.Note(tree=self.tree, num=num) self.sour[self.num].notes.add(self.note[num]) self.flag = True def __get_link_source(self): - num = int(self.data[2:len(self.data) - 1]) + """ Parse a link to a source """ + num = int(self.data[2 : len(self.data) - 1]) if num not in self.sour: - self.sour[num] = Source(num=num) + self.sour[num] = gt.Source(num=num) page = None while self.__get_line() and self.level > 1: - if self.tag == 'PAGE': + if self.tag == "PAGE": page = self.__get_text() self.flag = True return (self.sour[num], page) def __get_memorie(self): - memorie = Memorie() + """ Parse a memorie """ + memorie = gt.Memorie() while self.__get_line() and self.level > 1: - if self.tag == 'TITL': + if self.tag == "TITL": memorie.description = self.__get_text() - elif self.tag == 'FILE': + elif self.tag == "FILE": memorie.url = self.__get_text() self.flag = True return memorie def __get_note(self): + """ Parse a note """ self.note[self.num].text = self.__get_text() self.flag = True def __get_ordinance(self): - ordinance = Ordinance() + """ Parse an ordinance """ + ordinance = gt.Ordinance() while self.__get_line() and self.level > 1: - if self.tag == 'DATE': + if self.tag == "DATE": ordinance.date = self.__get_text() - elif self.tag == 'TEMP': + elif self.tag == "TEMP": ordinance.temple_code = self.data - elif self.tag == 'STAT': + elif self.tag == "STAT": ordinance.status = ORDINANCES[self.data] - elif self.tag == 'FAMC': - num = int(self.data[2:len(self.data) - 1]) + elif self.tag == "FAMC": + num = int(self.data[2 : len(self.data) - 1]) if num not in self.fam: - self.fam[num] = Fam(tree=self.tree, num=num) + self.fam[num] = gt.Fam(tree=self.tree, num=num) ordinance.famc = self.fam[num] self.flag = True return ordinance def __add_id(self): + """ Reset GEDCOM identifiers """ for num in self.fam: if self.fam[num].husb_num: self.fam[num].husb_fid = self.indi[self.fam[num].husb_num].fid @@ -324,14 +340,32 @@ class Gedcom: self.indi[num].fams_fid.add((self.fam[fams].husb_fid, self.fam[fams].wife_fid)) -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Merge GEDCOM data from FamilySearch Tree (4 Jul 2016)', add_help=False, usage='mergemyancestors.py -i input1.ged input2.ged ... [options]') +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Merge GEDCOM data from FamilySearch Tree (4 Jul 2016)", + add_help=False, + usage="mergemyancestors.py -i input1.ged input2.ged ... [options]", + ) try: - parser.add_argument('-i', metavar='', nargs='+', type=argparse.FileType('r', encoding='UTF-8'), default=sys.stdin, help='input GEDCOM files [stdin]') - parser.add_argument('-o', metavar='', nargs='?', type=argparse.FileType('w', encoding='UTF-8'), default=sys.stdout, help='output GEDCOM files [stdout]') + parser.add_argument( + "-i", + metavar="", + nargs="+", + type=argparse.FileType("r", encoding="UTF-8"), + default=sys.stdin, + help="input GEDCOM files [stdin]", + ) + parser.add_argument( + "-o", + metavar="", + nargs="?", + type=argparse.FileType("w", encoding="UTF-8"), + default=sys.stdout, + help="output GEDCOM files [stdout]", + ) except TypeError: - sys.stderr.write('Python >= 3.4 is required to run this script\n') - sys.stderr.write('(see https://docs.python.org/3/whatsnew/3.4.html#argparse)\n') + sys.stderr.write("Python >= 3.4 is required to run this script\n") + sys.stderr.write("(see https://docs.python.org/3/whatsnew/3.4.html#argparse)\n") exit(2) # extract arguments from the command line @@ -342,7 +376,7 @@ if __name__ == '__main__': parser.print_help() exit(2) - tree = Tree() + tree = gt.Tree() indi_counter = 0 fam_counter = 0 @@ -356,7 +390,7 @@ if __name__ == '__main__': fid = ged.indi[num].fid if fid not in tree.indi: indi_counter += 1 - tree.indi[fid] = Indi(tree=tree, num=indi_counter) + tree.indi[fid] = gt.Indi(tree=tree, num=indi_counter) tree.indi[fid].tree = tree tree.indi[fid].fid = ged.indi[num].fid tree.indi[fid].fams_fid |= ged.indi[num].fams_fid @@ -382,7 +416,7 @@ if __name__ == '__main__': husb, wife = (ged.fam[num].husb_fid, ged.fam[num].wife_fid) if (husb, wife) not in tree.fam: fam_counter += 1 - tree.fam[(husb, wife)] = Fam(husb, wife, tree, fam_counter) + tree.fam[(husb, wife)] = gt.Fam(husb, wife, tree, fam_counter) tree.fam[(husb, wife)].tree = tree tree.fam[(husb, wife)].chil_fid |= ged.fam[num].chil_fid if ged.fam[num].fid: diff --git a/translation.py b/translation.py index 8eead42..3fa7955 100644 --- a/translation.py +++ b/translation.py @@ -1,266 +1,176 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- +# coding: utf-8 translations = { - 'Life Sketch': { - 'de': 'Kurzinfo zum Lebenslauf', - 'eo': '[Ļîƒé Šķéţçĥ---- П國カ내]', - 'es': 'Resumen de la vida de la persona', - 'fr': 'Biographie succincte', - 'it': 'Profilo', - 'ja': '生涯の概要', - 'ko': '인생 개요', - 'pt': 'Resumo da Vida', - 'ru': 'Краткое жизнеописание', - 'zh': '生活簡述' + "Life Sketch": { + "de": "Kurzinfo zum Lebenslauf", + "eo": "[Ļîƒé Šķéţçĥ---- П國カ내]", + "es": "Resumen de la vida de la persona", + "fr": "Biographie succincte", + "it": "Profilo", + "ja": "生涯の概要", + "ko": "인생 개요", + "pt": "Resumo da Vida", + "ru": "Краткое жизнеописание", + "zh": "生活簡述", + }, + "Contributors": { + "de": "Mitwirkende", + "eo": "Contributors", + "es": "Colaboradores", + "fr": "Contributeurs", + "it": "Contributori", + "ja": "貢献者", + "ko": "기여자", + "pt": "Colaboradores", + "ru": "Авторы", + "zh": "贡献者", + }, + "Stillborn": { + "de": "Tot geboren", + "eo": "[Šţîļļƀöŕñ----------- П國カ내]", + "es": "Nacido muerto o mortinato", + "fr": "Mort-né(e)", + "it": "Nato morto", + "ja": "死産", + "ko": "사산아", + "pt": "Natimorto", + "ru": "Мертворожденный", + "zh": "死胎", + }, + "Affiliation": { + "de": "Zugehörigkeit", + "eo": "[Ńƒîļîåţîöñ---- П國カ내]", + "es": "Afiliación", + "fr": "Affiliation", + "it": "Affiliazione", + "ja": "所属", + "ko": "소속", + "pt": "Afiliação", + "ru": "Принадлежность", + "zh": "所屬團體", + }, + "Clan Name": { + "de": "Bezeichnung des Clans", + "eo": "[Çļåñ Ñåɱé----------- П國カ내]", + "es": "Nombre del clan", + "fr": "Nom de clan", + "it": "Nome clan", + "ja": "氏族名", + "ko": "씨족 이름", + "pt": "Nome do Clã", + "ru": "Название клана", + "zh": "氏族名字", + }, + "National Identification": { + "de": "Ausweisnummer", + "eo": "[Ñåţîöñåļ Îðéñţîƒîçåţîöñ----------- П國カ내]", + "es": "Identificación nacional", + "fr": "Numéro national d’identification", + "it": "Documento di identità", + "ja": "国民登録番号", + "ko": "주민등록번호", + "pt": "Carteira de Identidade Nacional", + "ru": "Удостоверение личности", + "zh": "國民身分證", + }, + "Race": { + "de": "Ethnische Zugehörigkeit", + "eo": "[Ŕåçé- П國カ내]", + "es": "Raza", + "fr": "Race", + "it": "Razza", + "ja": "人種", + "ko": "인종", + "pt": "Raça", + "ru": "Раса", + "zh": "種族", + }, + "Tribe Name": { + "de": "Bezeichnung des Stammes", + "eo": "[Ţŕîƀé Ñåɱé------------- П國カ내]", + "es": "Nombre de la tribu", + "fr": "Nom de tribu", + "it": "Nome tribù", + "ja": "部族名", + "ko": "부족명", + "pt": "Nome da Tribo", + "ru": "Название племени", + "zh": "部落名字", + }, + "Downloading starting individuals...": {"fr": "Téléchargement des personnes de départ..."}, + "Downloading %s. of generations of descendants...": { + "fr": "Téléchargement de %s génération(s) de descendants..." + }, + "Downloading spouses and marriage information...": { + "fr": "Téléchargement des conjoints et des informations de mariage..." + }, + "Downloading notes": {"fr": "Téléchargement des notes"}, + " and": {"fr": " et"}, + " ordinances": {"fr": " des ordonnances"}, + " and contributors": {"fr": " et des contributeurs"}, + "Downloaded %s individuals, %s families, %s sources and %s notes in %s seconds with %s HTTP requests.": { + "fr": "%s personnes, %s familles, %s sources et %s notes téléchargés en %s secondes avec %s requêtes HTTP." + }, + "Download ": {"fr": "Téléchargement de la "}, + "Copy": {"fr": "Copier"}, + "Cut": {"fr": "Couper"}, + "Paste": {"fr": "Coller"}, + "Username:": {"fr": "Nom d'utilisateur :"}, + "Password:": {"fr": "Mot de passe :"}, + "ID already exist": {"fr": "Cet identifiant existe déjà"}, + "Invalid FamilySearch ID: ": {"fr": "Identifiant FamilySearch invalide : "}, + "Individual not found": {"fr": "Personne non trouvée"}, + "Remove": {"fr": "Supprimer"}, + "Number of generations to ascend": {"fr": "Nombre de générations d'ancêtres"}, + "Number of generations to descend": {"fr": "Nombre de générations de descendants"}, + "Add a FamilySearch ID": {"fr": "Ajouter un identifiant FamilySearch"}, + "Add spouses and couples information": { + "fr": "Ajouter les conjoints et les informations de mariage" + }, + "Add Temple information": {"fr": "Ajouter les informations du Temple"}, + "Add list of contributors in notes": { + "fr": "Ajouter une liste des contributeurs dans les notes" + }, + "Sign In to FamilySearch": {"fr": "Ouvrir une session FamilySearch"}, + "Sign In": {"fr": "Ouvrir une session"}, + "Quit": {"fr": "Quitter"}, + "Save as": {"fr": "Enregistrer sous"}, + "All files": {"fr": "Tous les fichiers"}, + "Login to FamilySearch...": {"fr": "Connection à FamilySearch..."}, + "The username or password was incorrect": { + "fr": "Le nom d'utilisateur ou le mot de passe est incorrect" + }, + "Options": {"fr": "Options"}, + "Downloading %s. of generations of ancestors...": { + "fr": "Téléchargement de %s génération(s) d'ancêtres..." + }, + "Save": {"fr": "Sauvegarder"}, + "Success ! Click below to save your GEDCOM file": { + "fr": "Succès ! Cliquez ci-dessous pour sauvegarder votre fichier GEDCOM" + }, + "Download GEDCOM": {"fr": "Télécharger un GEDCOM"}, + "Merge GEDCOMs": {"fr": "Fusionner des GEDCOM"}, + "Merge": {"fr": "Fusionner"}, + "Add files": {"fr": "Ajouter des fichiers"}, + "File already exist: ": {"fr": "Ce fichier existe déjà: "}, + "Open": {"fr": "Ouvrir"}, + "File not found: ": {"fr": "Fichier non trouvé: "}, + "Files successfully merged": {"fr": "Fichiers fusionnés avec succès"}, + "Files": {"fr": "Fichiers"}, + "Please add GEDCOM files": {"fr": "Veuillez ajouter des fichiers GEDCOM"}, + "Error": {"fr": "Erreur"}, + "Info": {"fr": "Info"}, + "Name": {"fr": "Nom"}, + "Warning: This tool should only be used to merge GEDCOM files from this software. If you use other GEDCOM files, the result is not guaranteed.": { + "fr": "Attention : Cet outil ne devrait être utilisé qu'avec des fichiers GEDCOM provenants de ce logiciel. Si vous utilisez d'autres fichiers GEDCOM, le résultat n'est pas garanti." + }, + "Individuals: %s": {"fr": "Personnes : %s"}, + "Families: %s": {"fr": "Familles : %s"}, + "Sources: %s": {"fr": "Sources : %s"}, + "Notes: %s": {"fr": "Notes : %s"}, + "Elapsed time: %s:%s": {"fr": "Temps écoulé : %s:%s"}, + "Please enter your FamilySearch username and password.": { + "fr": "Veuillez entrer votre nom d'utilisateur et votre mot de passe FamilySearch." }, - 'Contributors': { - 'de': 'Mitwirkende', - 'eo': 'Contributors', - 'es': 'Colaboradores', - 'fr': 'Contributeurs', - 'it': 'Contributori', - 'ja': '貢献者', - 'ko': '기여자', - 'pt': 'Colaboradores', - 'ru': 'Авторы', - 'zh': '贡献者' - }, - 'Stillborn': { - 'de': 'Tot geboren', - 'eo': '[Šţîļļƀöŕñ----------- П國カ내]', - 'es': 'Nacido muerto o mortinato', - 'fr': 'Mort-né(e)', - 'it': 'Nato morto', - 'ja': '死産', - 'ko': '사산아', - 'pt': 'Natimorto', - 'ru': 'Мертворожденный', - 'zh': '死胎' - }, - 'Affiliation': { - 'de': 'Zugehörigkeit', - 'eo': '[Ńƒîļîåţîöñ---- П國カ내]', - 'es': 'Afiliación', - 'fr': 'Affiliation', - 'it': 'Affiliazione', - 'ja': '所属', - 'ko': '소속', - 'pt': 'Afiliação', - 'ru': 'Принадлежность', - 'zh': '所屬團體' - }, - 'Clan Name': { - 'de': 'Bezeichnung des Clans', - 'eo': '[Çļåñ Ñåɱé----------- П國カ내]', - 'es': 'Nombre del clan', - 'fr': 'Nom de clan', - 'it': 'Nome clan', - 'ja': '氏族名', - 'ko': '씨족 이름', - 'pt': 'Nome do Clã', - 'ru': 'Название клана', - 'zh': '氏族名字' - }, - 'National Identification': { - 'de': 'Ausweisnummer', - 'eo': '[Ñåţîöñåļ Îðéñţîƒîçåţîöñ----------- П國カ내]', - 'es': 'Identificación nacional', - 'fr': 'Numéro national d’identification', - 'it': 'Documento di identità', - 'ja': '国民登録番号', - 'ko': '주민등록번호', - 'pt': 'Carteira de Identidade Nacional', - 'ru': 'Удостоверение личности', - 'zh': '國民身分證' - }, - 'Race': { - 'de': 'Ethnische Zugehörigkeit', - 'eo': '[Ŕåçé- П國カ내]', - 'es': 'Raza', - 'fr': 'Race', - 'it': 'Razza', - 'ja': '人種', - 'ko': '인종', - 'pt': 'Raça', - 'ru': 'Раса', - 'zh': '種族' - }, - 'Tribe Name': { - 'de': 'Bezeichnung des Stammes', - 'eo': '[Ţŕîƀé Ñåɱé------------- П國カ내]', - 'es': 'Nombre de la tribu', - 'fr': 'Nom de tribu', - 'it': 'Nome tribù', - 'ja': '部族名', - 'ko': '부족명', - 'pt': 'Nome da Tribo', - 'ru': 'Название племени', - 'zh': '部落名字' - }, - 'Downloading starting individuals...': { - 'fr': 'Téléchargement des personnes de départ...', - }, - 'Downloading %s. of generations of descendants...': { - 'fr': 'Téléchargement de %s génération(s) de descendants...', - }, - 'Downloading spouses and marriage information...': { - 'fr': 'Téléchargement des conjoints et des informations de mariage...', - }, - 'Downloading notes': { - 'fr': 'Téléchargement des notes', - }, - ' and': { - 'fr': ' et', - }, - ' ordinances': { - 'fr': ' des ordonnances', - }, - ' and contributors': { - 'fr': ' et des contributeurs', - }, - 'Downloaded %s individuals, %s families, %s sources and %s notes in %s seconds with %s HTTP requests.': { - 'fr': '%s personnes, %s familles, %s sources et %s notes téléchargés en %s secondes avec %s requêtes HTTP.', - }, - 'Download ': { - 'fr': 'Téléchargement de la ', - }, - 'Copy': { - 'fr': 'Copier', - }, - 'Cut': { - 'fr': 'Couper', - }, - 'Paste': { - 'fr': 'Coller', - }, - 'Username:': { - 'fr': "Nom d'utilisateur :", - }, - 'Password:': { - 'fr': 'Mot de passe :', - }, - 'ID already exist': { - 'fr': 'Cet identifiant existe déjà', - }, - 'Invalid FamilySearch ID: ': { - 'fr': 'Identifiant FamilySearch invalide : ', - }, - 'Individual not found': { - 'fr': 'Personne non trouvée', - }, - 'Remove': { - 'fr': 'Supprimer', - }, - 'Number of generations to ascend': { - 'fr': "Nombre de générations d'ancêtres", - }, - 'Number of generations to descend': { - 'fr': 'Nombre de générations de descendants', - }, - 'Add a FamilySearch ID': { - 'fr': 'Ajouter un identifiant FamilySearch', - }, - 'Add spouses and couples information': { - 'fr': 'Ajouter les conjoints et les informations de mariage', - }, - 'Add Temple information': { - 'fr': 'Ajouter les informations du Temple', - }, - 'Add list of contributors in notes': { - 'fr': 'Ajouter une liste des contributeurs dans les notes', - }, - 'Sign In to FamilySearch': { - 'fr': 'Ouvrir une session FamilySearch', - }, - 'Sign In': { - 'fr': 'Ouvrir une session', - }, - 'Quit': { - 'fr': 'Quitter', - }, - 'Save as': { - 'fr': 'Enregistrer sous', - }, - 'All files': { - 'fr': 'Tous les fichiers', - }, - 'Login to FamilySearch...': { - 'fr': 'Connection à FamilySearch...', - }, - 'The username or password was incorrect': { - 'fr': "Le nom d'utilisateur ou le mot de passe est incorrect", - }, - 'Options': { - 'fr': 'Options', - }, - 'Downloading %s. of generations of ancestors...': { - 'fr': "Téléchargement de %s génération(s) d'ancêtres...", - }, - 'Save': { - 'fr': 'Sauvegarder', - }, - 'Success ! Click below to save your GEDCOM file': { - 'fr': 'Succès ! Cliquez ci-dessous pour sauvegarder votre fichier GEDCOM', - }, - 'Download GEDCOM': { - 'fr': 'Télécharger un GEDCOM', - }, - 'Merge GEDCOMs': { - 'fr': 'Fusionner des GEDCOM', - }, - 'Merge': { - 'fr': 'Fusionner', - }, - 'Add files': { - 'fr': 'Ajouter des fichiers', - }, - 'File already exist: ': { - 'fr': 'Ce fichier existe déjà: ', - }, - 'Open': { - 'fr': 'Ouvrir', - }, - 'File not found: ': { - 'fr': 'Fichier non trouvé: ', - }, - 'Files successfully merged': { - 'fr': 'Fichiers fusionnés avec succès', - }, - 'Files': { - 'fr': 'Fichiers', - }, - 'Please add GEDCOM files': { - 'fr': 'Veuillez ajouter des fichiers GEDCOM', - }, - 'Error': { - 'fr': 'Erreur', - }, - 'Info': { - 'fr': 'Info', - }, - 'Name': { - 'fr': 'Nom', - }, - 'Warning: This tool should only be used to merge GEDCOM files from this software. If you use other GEDCOM files, the result is not guaranteed.': { - 'fr': "Attention : Cet outil ne devrait être utilisé qu'avec des fichiers GEDCOM provenants de ce logiciel. Si vous utilisez d'autres fichiers GEDCOM, le résultat n'est pas garanti.", - }, - 'Individuals: %s': { - 'fr': 'Personnes : %s', - }, - 'Families: %s': { - 'fr': 'Familles : %s', - }, - 'Sources: %s': { - 'fr': 'Sources : %s', - }, - 'Notes: %s': { - 'fr': 'Notes : %s', - }, - 'Elapsed time: %s:%s': { - 'fr': 'Temps écoulé : %s:%s', - }, - 'Please enter your FamilySearch username and password.': { - 'fr': "Veuillez entrer votre nom d'utilisateur et votre mot de passe FamilySearch." - } }