From 8ba7bae707bd8b76db1a2777d0a6dfe20dac64e6 Mon Sep 17 00:00:00 2001 From: Vladimir Buzalka Date: Sat, 4 Apr 2026 08:57:00 +0200 Subject: [PATCH] notebook vb --- MedicusWithClaude/s03soubory.py | 4 +- MedicusWithClaude/s03soubory_01.py | 397 ++++++++++++++++++ MedicusWithClaude/s03soubory_01_FINAL.py | 397 ++++++++++++++++++ .../s03soubory_01_FINAL_notes.md | 226 ++++++++++ 4 files changed, 1022 insertions(+), 2 deletions(-) create mode 100644 MedicusWithClaude/s03soubory_01.py create mode 100644 MedicusWithClaude/s03soubory_01_FINAL.py create mode 100644 MedicusWithClaude/s03soubory_01_FINAL_notes.md diff --git a/MedicusWithClaude/s03soubory.py b/MedicusWithClaude/s03soubory.py index cca6b4d..d7e5f68 100644 --- a/MedicusWithClaude/s03soubory.py +++ b/MedicusWithClaude/s03soubory.py @@ -8,8 +8,8 @@ conn = fdb.connect( password="masterkey", charset="win1250") -cesta = r"u:\NextcloudOrdinace\Dokumentace_ke_zpracování" -cestazpracovana = r"u:\NextcloudOrdinace\Dokumentace_zpracovaná" +cesta = r"u:\testimport" +cestazpracovana = r"u:\testimportzpracovana" # Konstanty pro detekci sekce Vložené přílohy (RTF kódování win1250) PRILOHY_HEADER = r"Vlo\'9een\'e9 p\'f8\'edlohy:" diff --git a/MedicusWithClaude/s03soubory_01.py b/MedicusWithClaude/s03soubory_01.py new file mode 100644 index 0000000..cc4a3a3 --- /dev/null +++ b/MedicusWithClaude/s03soubory_01.py @@ -0,0 +1,397 @@ +import os, shutil, fdb, time, threading +import re, datetime, funkce, funkce_ext + +# Connect to the Firebird database +conn = fdb.connect( + dsn=r'localhost:c:\medicus 3\data\medicus.fdb', + user='SYSDBA', + password="masterkey", + charset="win1250") + +cesta = r"u:\testimport" +cestazpracovana = r"u:\testimportzpracovana" + +# Konstanty pro detekci sekce Vložené přílohy (RTF kódování win1250) +PRILOHY_HEADER = r"Vlo\'9een\'e9 p\'f8\'edlohy:" +PRILOHY_CLOSING = r'\pard\s10\plain\cs15\f0\fs20 \par' + +# ─── Helper funkce ──────────────────────────────────────────────────────────── + +def restore_files_for_import(retezec): + drop = r"u:\Dropbox\!!!Days\Downloads Z230\Dokumentace" + next = r"u:\NextcloudOrdinace\Dokumentace_ke_zpracování" + if not os.path.exists(drop): + print(f"The directory '{drop}' does not exist.") + return + for item in os.listdir(drop): + item_path = os.path.join(drop, item) + if os.path.isfile(item_path) or os.path.islink(item_path): + os.unlink(item_path) + print(f"Deleted file: {item_path}") + elif os.path.isdir(item_path): + shutil.rmtree(item_path) + print(f"Deleted directory: {item_path}") + for item in os.listdir(next): + item_path = os.path.join(next, item) + if os.path.isfile(item_path) and item_path.endswith(".pdf") and retezec in item_path: + shutil.copy(item_path, os.path.join(drop, item)) + print(f"Copied file: {item_path}") + + +def kontrola_rc(rc, connection): + cur = connection.cursor() + cur.execute("select count(*),idpac from kar where rodcis=? group by idpac", (rc,)) + row = cur.fetchone() + if row: + return row[1] + else: + return False + + +def kontrola_struktury(souborname, connection): + if souborname.endswith('.pdf'): + pattern = re.compile(r'(^\d{9,10}) (\d{4}-\d{2}-\d{2}) (\w+, \w.+?) \[(.+?)\] \[(.*?)\]') + match = pattern.search(souborname) + vpohode = True + if match and len(match.groups()) == 5: + datum = match.group(2) + try: + datetime.datetime.strptime(datum, "%Y-%m-%d").date() + except: + vpohode = False + return vpohode + cur = connection.cursor() + cur.execute("select count(*) from kar where rodcis=?", (match.group(1),)) + row = cur.fetchone()[0] + if row != 1: + vpohode = False + return vpohode + else: + vpohode = False + return vpohode + else: + vpohode = False + return vpohode + return vpohode + + +def vrat_info_o_souboru(souborname, connection): + pattern = re.compile(r'(^\d{9,10}) (\d{4}-\d{2}-\d{2}) (\w+, \w.+?) \[(.+?)\] \[(.*?)\]') + match = pattern.search(souborname) + rc = match.group(1) + datum = datetime.datetime.strptime(match.group(2), "%Y-%m-%d").date() + jmeno = match.group(3) + prvnizavorka = match.group(4) + druhazavorka = match.group(5) + cur = connection.cursor() + cur.execute("select idpac from kar where rodcis=?", (rc,)) + idpac = cur.fetchone()[0] + datumsouboru = datetime.datetime.fromtimestamp(os.path.getctime(os.path.join(cesta, souborname))) + return (rc, idpac, datum, jmeno, prvnizavorka, druhazavorka, souborname, datumsouboru) + + +def prejmenuj_chybny_soubor(souborname, cesta): + if souborname[0] != "♥": + soubornovy = "♥" + souborname + os.rename(os.path.join(cesta, souborname), os.path.join(cesta, soubornovy)) + + +def _pokus_o_zamek(dekurs_id, vysledek): + """Běží ve vlákně: pokusí se zamknout dekurz přes separátní spojení. + Výsledek zapíše do slovníku vysledek: {'ok': True} nebo {'chyba': str}. + Pokud vlákno stále běží po uplynutí timeoutu → záznam je zamčený. + """ + conn_t = None + try: + conn_t = fdb.connect( + dsn=r'localhost:c:\medicus 3\data\medicus.fdb', + user='SYSDBA', password='masterkey', charset='win1250' + ) + cur_t = conn_t.cursor() + cur_t.execute( + "SELECT ID FROM DEKURS WHERE ID = ? FOR UPDATE WITH LOCK", + (dekurs_id,) + ) + cur_t.fetchone() + conn_t.rollback() # Uvolni zámek – sloužil jen k ověření + vysledek['ok'] = True + except Exception as e: + vysledek['chyba'] = str(e) + finally: + if conn_t: + try: + conn_t.close() + except Exception: + pass + + +def zkus_zamknout_dnesni_dekurs(conn, idpac, datum_vlozeni, timeout_sec=2): + """Zjistí zda existuje dnešní dekurz a ověří že není zamčený. + + Vrátí: + (id, rtf) – dnešní dekurz existuje a není zamčený + None – žádný dnešní dekurz (bude se dělat INSERT, zámek není potřeba) + + Vyhodí RuntimeError pokud je záznam zamčený jiným uživatelem (Medicus ho má otevřený). + + Poznámka: NOWAIT transakci fdb neumí spolehlivě nastavit, proto spustíme + pokus o zámek ve vlákně s timeoutem. Pokud vlákno do timeout_sec sekund + neskončí, záznam je zamčený a přeskočíme celou skupinu. + """ + cur = conn.cursor() + + # Krok 1: přečti ID, datum a obsah posledního dekurzu (běžný SELECT) + cur.execute(""" + SELECT FIRST 1 ID, DATUM, DEKURS FROM DEKURS + WHERE IDPAC = ? + ORDER BY ID DESC + """, (idpac,)) + row = cur.fetchone() + if row is None: + print(f" Žádný dekurz pro pacienta IDPAC={idpac}") + return None + + dekurs_id, dekurs_datum, dekurs_rtf = row + print(f" Poslední dekurs: ID={dekurs_id}, datum={dekurs_datum}") + + if dekurs_datum != datum_vlozeni: + print(f" → jiný den ({dekurs_datum} ≠ {datum_vlozeni}), vytvoříme nový (INSERT)") + return None + + # Krok 2: ověř přes vlákno s timeoutem zda záznam není zamčený + print(f" → dnešní den ({datum_vlozeni}) ✓ – ověřuji zámek (timeout {timeout_sec}s)...") + vysledek = {} + t = threading.Thread(target=_pokus_o_zamek, args=(dekurs_id, vysledek), daemon=True) + t.start() + t.join(timeout=timeout_sec) + + if t.is_alive(): + # Vlákno stále čeká na zámek = záznam drží Medicus + raise RuntimeError(f"DEKURZ ID={dekurs_id} je zamčený (Medicus má záznam otevřený)") + + if 'chyba' in vysledek: + raise fdb.DatabaseError(vysledek['chyba']) + + print(f" → záznam volný, pokračuji se zápisem") + return (dekurs_id, dekurs_rtf) + + +def ma_sekci_prilohy(rtf): + return PRILOHY_HEADER in rtf + + +def pridat_do_sekce_prilohy(rtf, bookmark_list, filenameforbookmark_list): + """Přidá více souborů do EXISTUJÍCÍ sekce 'Vložené přílohy'. + + Postup: + 1. Spočítá počet Files: odkazů = N → nové indexy začínají od N + 2. Vloží nové \\pard řádky před uzavírací prázdný řádek sekce + 3. Přidá nové bookmarky na konec {\\info{\\bookmarks ...}} + """ + # 1. Počet existujících Files: odkazů + bkm_match = re.search(r'\{\\info\{\\bookmarks ([^}]*)\}\}', rtf) + if bkm_match: + bkm_entries = [e for e in bkm_match.group(1).split(';') if e.strip()] + n_files = sum(1 for e in bkm_entries if '"Files:' in e) + else: + n_files = 0 + print(f" Počet existujících Files odkazů: {n_files}, přidávám {len(bookmark_list)} nových") + + # 2. Vložit nové \pard řádky před PRILOHY_CLOSING + prilohy_pos = rtf.find(PRILOHY_HEADER) + closing_pos = rtf.find(PRILOHY_CLOSING, prilohy_pos) + if closing_pos == -1: + raise RuntimeError("Nenalezen uzavírací řádek sekce Vložené přílohy!") + + new_pards = '' + for i, fname in enumerate(filenameforbookmark_list): + idx = n_files + i + new_pards += (r'\pard\s10{\*\bkmkstart ' + str(idx) + r'}' + r'\plain\cs32\f0\ul\fs20\cf1 ' + fname + + r'{\*\bkmkend ' + str(idx) + r'}\par' + '\n') + + rtf = rtf[:closing_pos] + new_pards + rtf[closing_pos:] + + # 3. Přidat nové bookmarky na konec {\info{\bookmarks ...}} + def append_bookmarks(m): + entries = [e for e in m.group(1).split(';') if e.strip()] + entries.extend(bookmark_list) + return '{\\info{\\bookmarks ' + ';'.join(entries) + '}}' + + rtf = re.sub(r'\{\\info\{\\bookmarks ([^}]*)\}\}', append_bookmarks, rtf) + return rtf + + +def merge_rtf_prepend(existing_rtf, new_bkm_list, new_body_pards, n_new): + """Vloží novou sekci příloh na ZAČÁTEK stávajícího dekurzu (sekce tam ještě není).""" + rtf = existing_rtf + rtf = re.sub(r'\\bkmkstart (\d+)', + lambda m: '\\bkmkstart ' + str(int(m.group(1)) + n_new), rtf) + rtf = re.sub(r'\\bkmkend (\d+)', + lambda m: '\\bkmkend ' + str(int(m.group(1)) + n_new), rtf) + + new_bkm_str = ';'.join(new_bkm_list) + + def merge_bkm(m): + existing = m.group(1).strip() + combined = new_bkm_str + (';' + existing if existing else '') + return '{\\info{\\bookmarks ' + combined + '}}' + + if re.search(r'\{\\info\{\\bookmarks', rtf): + rtf = re.sub(r'\{\\info\{\\bookmarks ([^}]*)\}\}', merge_bkm, rtf) + else: + rtf = re.sub(r'(\\deflang\d+)', + r'\1{\\info{\\bookmarks ' + new_bkm_str + '}}', rtf, count=1) + + match = re.search(r'\\uc1\\pard', rtf) + if match: + pos = match.start() + rtf = rtf[:pos] + new_body_pards + '\n' + rtf[pos:] + return rtf + + +# Šablona RTF pro nový dekurs +RTF_TEMPLATE = r"""{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029{\info{\bookmarks BOOKMARKNAMES}}{\fonttbl{\f0\fnil\fcharset238 Arial;}{\f5\fnil\fcharset238 Symbol;}} +{\colortbl ;\red0\green0\blue255;\red0\green128\blue0;\red0\green0\blue0;} +{\stylesheet{\s10\fi0\li0\ql\ri0\sb0\sa0 Vlevo;}{\*\cs15\f0\fs20 Norm\'e1ln\'ed;}{\*\cs20\f0\i\fs20 Z\'e1hlav\'ed;}{\*\cs32\f0\ul\fs20\cf1 Odkaz;}} +BOOKMARKSTEXT +\pard\s10\plain\cs15\f0\fs20 \par +}""" + +# ─── Hlavní tělo skriptu ────────────────────────────────────────────────────── + +info = [] +for soubor in os.listdir(cesta): + if os.path.isfile(os.path.join(cesta, soubor)): + print(soubor) + if kontrola_struktury(soubor, conn): + info.append(vrat_info_o_souboru(soubor, conn)) + else: + prejmenuj_chybny_soubor(soubor, cesta) + +info = sorted(info, key=lambda x: (x[0], x[1])) +print(info) + +skupiny = {} +for row in info: + skupiny[row[0]] = [] +for row in info: + skupiny[row[0]].append(row) + +for key in skupiny.keys(): + print(f"\n{'='*60}") + print(f"RC: {key}, souborů: {len(skupiny[key])}") + + idpac = skupiny[key][0][1] + datumzapisu = datetime.datetime.now().date() + caszapisu = datetime.datetime.now().time() + + # ── PRE-CHECK: zkus zamknout dnešní dekurz PŘED zpracováním souborů ────── + print(f"\n>>> Kontrola zámku dekurzu pro IDPAC={idpac}...") + try: + existujici = zkus_zamknout_dnesni_dekurs(conn, idpac, datumzapisu) + except RuntimeError as e: + # Vlákno nepřišlo do timeoutu = záznam drží Medicus + print(f"\n!!! DEKURZ ZAMČEN – soubory skupiny RC={key} přeskočeny.") + print(" Spusťte skript znovu až bude záznam volný.") + continue + except fdb.DatabaseError as e: + chyba = str(e).lower() + if 'deadlock' in chyba or 'lock conflict' in chyba or 'update conflict' in chyba: + print(f"\n!!! DEKURZ ZAMČEN (DB konflikt) – soubory skupiny RC={key} přeskočeny.") + print(" Spusťte skript znovu až bude záznam volný.") + continue + raise # jiná DB chyba – propaguj dál + + cislo = 9 + poradi = 0 + bookmark_list = [] + filenameforbookmark_list = [] + bookmarks_body = '' + + # ── Krok 1: vložit každý soubor do ext DB + přesunout do zpracovaných ──── + for row in skupiny[key]: + fileid = funkce_ext.zapis_file_ext( + vstupconnection=conn, idpac=row[1], + cesta=cesta, souborname=row[6], prvnizavorka=row[4], + soubordate=row[2], souborfiledate=row[7], poznamka=row[5]) + print(f" → FILES.ID = {fileid} ({row[6]})") + + # Přesun souboru do zpracovaných + for attempt in range(3): + try: + dest = os.path.join(cestazpracovana, row[6]) + if not os.path.exists(dest): + shutil.move(os.path.join(cesta, row[6]), dest) + else: + ts = datetime.datetime.now().strftime("%Y-%m-%d %H-%M-%S") + shutil.move(os.path.join(cesta, row[6]), + os.path.join(cestazpracovana, row[6][:-4] + " " + ts + ".pdf")) + print(" Přesun OK!") + break + except Exception as e: + print(f" Attempt {attempt + 1} failed: {e}") + if attempt < 2: + print(" Retrying in 5 seconds...") + time.sleep(5) + else: + print(" Max retries reached. Command failed.") + + filenameforbookmark = row[2].strftime('%Y-%m-%d') + ' ' + row[4] + ': ' + row[5] + bookmark_list.append('"' + filenameforbookmark + '","Files:' + str(fileid) + '",' + str(cislo)) + filenameforbookmark_list.append(filenameforbookmark) + cislo += 7 + + bookmarks_body += (r'\pard\s10{\*\bkmkstart ' + str(poradi) + r'}' + r'\plain\cs32\f0\ul\fs20\cf1 ' + filenameforbookmark + + r'{\*\bkmkend ' + str(poradi) + r'}\par') + poradi += 1 + + # ── Krok 2: sestavit tělo nové sekce příloh ─────────────────────────────── + new_body = (r'\uc1\pard\s10\plain\cs20\f0\i\fs20 Vlo\'9een\'e9 p\'f8\'edlohy:\par' + '\n' + + bookmarks_body + '\n' + + r'\pard\s10\plain\cs15\f0\fs20 \par') + + # ── Krok 3: rozhodovací logika (3 případy) ──────────────────────────────── + cur = conn.cursor() + + if existujici: + dekurs_id, existing_rtf = existujici + + if ma_sekci_prilohy(existing_rtf): + # Případ 1: dnešní dekurz má sekci příloh → přidáme soubory dovnitř + print(f"\n>>> Sekce 'Vložené přílohy' nalezena v DEKURS ID={dekurs_id}") + print(">>> Přidávám soubory DO existující sekce...") + merged_rtf = pridat_do_sekce_prilohy(existing_rtf, bookmark_list, filenameforbookmark_list) + else: + # Případ 2: dnešní dekurz existuje, ale sekci příloh nemá → prepend + print(f"\n>>> DEKURS ID={dekurs_id} nemá sekci příloh → vkládám sekci na začátek...") + merged_rtf = merge_rtf_prepend(existing_rtf, bookmark_list, new_body, len(skupiny[key])) + + print("\n=== Výsledný RTF ===") + print(merged_rtf) + cur.execute("UPDATE DEKURS SET DEKURS = ? WHERE ID = ?", (merged_rtf, dekurs_id)) + conn.commit() + print(f"\n>>> UPDATE DEKURS ID={dekurs_id} – hotovo!") + + else: + # Případ 3: žádný dnešní dekurz → vytvoříme nový + print(f"\n>>> Žádný dekurs pro dnešek → vytvářím nový...") + bookmark_str = ';'.join(bookmark_list) + rtf = RTF_TEMPLATE.replace('BOOKMARKNAMES', bookmark_str) + rtf = rtf.replace('BOOKMARKSTEXT', new_body) + + print("\n=== Výsledný RTF ===") + print(rtf) + + dekursid = funkce.get_dekurs_id(conn) + cur.execute( + "INSERT INTO DEKURS (id, iduzi, idprac, idodd, idpac, datum, cas, dekurs)" + " VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + (dekursid, 6, 2, 2, idpac, datumzapisu, caszapisu, rtf) + ) + conn.commit() + print(f"\n>>> Nový DEKURS ID={dekursid}") + +print("\n=== HOTOVO ===") +conn.close() diff --git a/MedicusWithClaude/s03soubory_01_FINAL.py b/MedicusWithClaude/s03soubory_01_FINAL.py new file mode 100644 index 0000000..cc4a3a3 --- /dev/null +++ b/MedicusWithClaude/s03soubory_01_FINAL.py @@ -0,0 +1,397 @@ +import os, shutil, fdb, time, threading +import re, datetime, funkce, funkce_ext + +# Connect to the Firebird database +conn = fdb.connect( + dsn=r'localhost:c:\medicus 3\data\medicus.fdb', + user='SYSDBA', + password="masterkey", + charset="win1250") + +cesta = r"u:\testimport" +cestazpracovana = r"u:\testimportzpracovana" + +# Konstanty pro detekci sekce Vložené přílohy (RTF kódování win1250) +PRILOHY_HEADER = r"Vlo\'9een\'e9 p\'f8\'edlohy:" +PRILOHY_CLOSING = r'\pard\s10\plain\cs15\f0\fs20 \par' + +# ─── Helper funkce ──────────────────────────────────────────────────────────── + +def restore_files_for_import(retezec): + drop = r"u:\Dropbox\!!!Days\Downloads Z230\Dokumentace" + next = r"u:\NextcloudOrdinace\Dokumentace_ke_zpracování" + if not os.path.exists(drop): + print(f"The directory '{drop}' does not exist.") + return + for item in os.listdir(drop): + item_path = os.path.join(drop, item) + if os.path.isfile(item_path) or os.path.islink(item_path): + os.unlink(item_path) + print(f"Deleted file: {item_path}") + elif os.path.isdir(item_path): + shutil.rmtree(item_path) + print(f"Deleted directory: {item_path}") + for item in os.listdir(next): + item_path = os.path.join(next, item) + if os.path.isfile(item_path) and item_path.endswith(".pdf") and retezec in item_path: + shutil.copy(item_path, os.path.join(drop, item)) + print(f"Copied file: {item_path}") + + +def kontrola_rc(rc, connection): + cur = connection.cursor() + cur.execute("select count(*),idpac from kar where rodcis=? group by idpac", (rc,)) + row = cur.fetchone() + if row: + return row[1] + else: + return False + + +def kontrola_struktury(souborname, connection): + if souborname.endswith('.pdf'): + pattern = re.compile(r'(^\d{9,10}) (\d{4}-\d{2}-\d{2}) (\w+, \w.+?) \[(.+?)\] \[(.*?)\]') + match = pattern.search(souborname) + vpohode = True + if match and len(match.groups()) == 5: + datum = match.group(2) + try: + datetime.datetime.strptime(datum, "%Y-%m-%d").date() + except: + vpohode = False + return vpohode + cur = connection.cursor() + cur.execute("select count(*) from kar where rodcis=?", (match.group(1),)) + row = cur.fetchone()[0] + if row != 1: + vpohode = False + return vpohode + else: + vpohode = False + return vpohode + else: + vpohode = False + return vpohode + return vpohode + + +def vrat_info_o_souboru(souborname, connection): + pattern = re.compile(r'(^\d{9,10}) (\d{4}-\d{2}-\d{2}) (\w+, \w.+?) \[(.+?)\] \[(.*?)\]') + match = pattern.search(souborname) + rc = match.group(1) + datum = datetime.datetime.strptime(match.group(2), "%Y-%m-%d").date() + jmeno = match.group(3) + prvnizavorka = match.group(4) + druhazavorka = match.group(5) + cur = connection.cursor() + cur.execute("select idpac from kar where rodcis=?", (rc,)) + idpac = cur.fetchone()[0] + datumsouboru = datetime.datetime.fromtimestamp(os.path.getctime(os.path.join(cesta, souborname))) + return (rc, idpac, datum, jmeno, prvnizavorka, druhazavorka, souborname, datumsouboru) + + +def prejmenuj_chybny_soubor(souborname, cesta): + if souborname[0] != "♥": + soubornovy = "♥" + souborname + os.rename(os.path.join(cesta, souborname), os.path.join(cesta, soubornovy)) + + +def _pokus_o_zamek(dekurs_id, vysledek): + """Běží ve vlákně: pokusí se zamknout dekurz přes separátní spojení. + Výsledek zapíše do slovníku vysledek: {'ok': True} nebo {'chyba': str}. + Pokud vlákno stále běží po uplynutí timeoutu → záznam je zamčený. + """ + conn_t = None + try: + conn_t = fdb.connect( + dsn=r'localhost:c:\medicus 3\data\medicus.fdb', + user='SYSDBA', password='masterkey', charset='win1250' + ) + cur_t = conn_t.cursor() + cur_t.execute( + "SELECT ID FROM DEKURS WHERE ID = ? FOR UPDATE WITH LOCK", + (dekurs_id,) + ) + cur_t.fetchone() + conn_t.rollback() # Uvolni zámek – sloužil jen k ověření + vysledek['ok'] = True + except Exception as e: + vysledek['chyba'] = str(e) + finally: + if conn_t: + try: + conn_t.close() + except Exception: + pass + + +def zkus_zamknout_dnesni_dekurs(conn, idpac, datum_vlozeni, timeout_sec=2): + """Zjistí zda existuje dnešní dekurz a ověří že není zamčený. + + Vrátí: + (id, rtf) – dnešní dekurz existuje a není zamčený + None – žádný dnešní dekurz (bude se dělat INSERT, zámek není potřeba) + + Vyhodí RuntimeError pokud je záznam zamčený jiným uživatelem (Medicus ho má otevřený). + + Poznámka: NOWAIT transakci fdb neumí spolehlivě nastavit, proto spustíme + pokus o zámek ve vlákně s timeoutem. Pokud vlákno do timeout_sec sekund + neskončí, záznam je zamčený a přeskočíme celou skupinu. + """ + cur = conn.cursor() + + # Krok 1: přečti ID, datum a obsah posledního dekurzu (běžný SELECT) + cur.execute(""" + SELECT FIRST 1 ID, DATUM, DEKURS FROM DEKURS + WHERE IDPAC = ? + ORDER BY ID DESC + """, (idpac,)) + row = cur.fetchone() + if row is None: + print(f" Žádný dekurz pro pacienta IDPAC={idpac}") + return None + + dekurs_id, dekurs_datum, dekurs_rtf = row + print(f" Poslední dekurs: ID={dekurs_id}, datum={dekurs_datum}") + + if dekurs_datum != datum_vlozeni: + print(f" → jiný den ({dekurs_datum} ≠ {datum_vlozeni}), vytvoříme nový (INSERT)") + return None + + # Krok 2: ověř přes vlákno s timeoutem zda záznam není zamčený + print(f" → dnešní den ({datum_vlozeni}) ✓ – ověřuji zámek (timeout {timeout_sec}s)...") + vysledek = {} + t = threading.Thread(target=_pokus_o_zamek, args=(dekurs_id, vysledek), daemon=True) + t.start() + t.join(timeout=timeout_sec) + + if t.is_alive(): + # Vlákno stále čeká na zámek = záznam drží Medicus + raise RuntimeError(f"DEKURZ ID={dekurs_id} je zamčený (Medicus má záznam otevřený)") + + if 'chyba' in vysledek: + raise fdb.DatabaseError(vysledek['chyba']) + + print(f" → záznam volný, pokračuji se zápisem") + return (dekurs_id, dekurs_rtf) + + +def ma_sekci_prilohy(rtf): + return PRILOHY_HEADER in rtf + + +def pridat_do_sekce_prilohy(rtf, bookmark_list, filenameforbookmark_list): + """Přidá více souborů do EXISTUJÍCÍ sekce 'Vložené přílohy'. + + Postup: + 1. Spočítá počet Files: odkazů = N → nové indexy začínají od N + 2. Vloží nové \\pard řádky před uzavírací prázdný řádek sekce + 3. Přidá nové bookmarky na konec {\\info{\\bookmarks ...}} + """ + # 1. Počet existujících Files: odkazů + bkm_match = re.search(r'\{\\info\{\\bookmarks ([^}]*)\}\}', rtf) + if bkm_match: + bkm_entries = [e for e in bkm_match.group(1).split(';') if e.strip()] + n_files = sum(1 for e in bkm_entries if '"Files:' in e) + else: + n_files = 0 + print(f" Počet existujících Files odkazů: {n_files}, přidávám {len(bookmark_list)} nových") + + # 2. Vložit nové \pard řádky před PRILOHY_CLOSING + prilohy_pos = rtf.find(PRILOHY_HEADER) + closing_pos = rtf.find(PRILOHY_CLOSING, prilohy_pos) + if closing_pos == -1: + raise RuntimeError("Nenalezen uzavírací řádek sekce Vložené přílohy!") + + new_pards = '' + for i, fname in enumerate(filenameforbookmark_list): + idx = n_files + i + new_pards += (r'\pard\s10{\*\bkmkstart ' + str(idx) + r'}' + r'\plain\cs32\f0\ul\fs20\cf1 ' + fname + + r'{\*\bkmkend ' + str(idx) + r'}\par' + '\n') + + rtf = rtf[:closing_pos] + new_pards + rtf[closing_pos:] + + # 3. Přidat nové bookmarky na konec {\info{\bookmarks ...}} + def append_bookmarks(m): + entries = [e for e in m.group(1).split(';') if e.strip()] + entries.extend(bookmark_list) + return '{\\info{\\bookmarks ' + ';'.join(entries) + '}}' + + rtf = re.sub(r'\{\\info\{\\bookmarks ([^}]*)\}\}', append_bookmarks, rtf) + return rtf + + +def merge_rtf_prepend(existing_rtf, new_bkm_list, new_body_pards, n_new): + """Vloží novou sekci příloh na ZAČÁTEK stávajícího dekurzu (sekce tam ještě není).""" + rtf = existing_rtf + rtf = re.sub(r'\\bkmkstart (\d+)', + lambda m: '\\bkmkstart ' + str(int(m.group(1)) + n_new), rtf) + rtf = re.sub(r'\\bkmkend (\d+)', + lambda m: '\\bkmkend ' + str(int(m.group(1)) + n_new), rtf) + + new_bkm_str = ';'.join(new_bkm_list) + + def merge_bkm(m): + existing = m.group(1).strip() + combined = new_bkm_str + (';' + existing if existing else '') + return '{\\info{\\bookmarks ' + combined + '}}' + + if re.search(r'\{\\info\{\\bookmarks', rtf): + rtf = re.sub(r'\{\\info\{\\bookmarks ([^}]*)\}\}', merge_bkm, rtf) + else: + rtf = re.sub(r'(\\deflang\d+)', + r'\1{\\info{\\bookmarks ' + new_bkm_str + '}}', rtf, count=1) + + match = re.search(r'\\uc1\\pard', rtf) + if match: + pos = match.start() + rtf = rtf[:pos] + new_body_pards + '\n' + rtf[pos:] + return rtf + + +# Šablona RTF pro nový dekurs +RTF_TEMPLATE = r"""{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029{\info{\bookmarks BOOKMARKNAMES}}{\fonttbl{\f0\fnil\fcharset238 Arial;}{\f5\fnil\fcharset238 Symbol;}} +{\colortbl ;\red0\green0\blue255;\red0\green128\blue0;\red0\green0\blue0;} +{\stylesheet{\s10\fi0\li0\ql\ri0\sb0\sa0 Vlevo;}{\*\cs15\f0\fs20 Norm\'e1ln\'ed;}{\*\cs20\f0\i\fs20 Z\'e1hlav\'ed;}{\*\cs32\f0\ul\fs20\cf1 Odkaz;}} +BOOKMARKSTEXT +\pard\s10\plain\cs15\f0\fs20 \par +}""" + +# ─── Hlavní tělo skriptu ────────────────────────────────────────────────────── + +info = [] +for soubor in os.listdir(cesta): + if os.path.isfile(os.path.join(cesta, soubor)): + print(soubor) + if kontrola_struktury(soubor, conn): + info.append(vrat_info_o_souboru(soubor, conn)) + else: + prejmenuj_chybny_soubor(soubor, cesta) + +info = sorted(info, key=lambda x: (x[0], x[1])) +print(info) + +skupiny = {} +for row in info: + skupiny[row[0]] = [] +for row in info: + skupiny[row[0]].append(row) + +for key in skupiny.keys(): + print(f"\n{'='*60}") + print(f"RC: {key}, souborů: {len(skupiny[key])}") + + idpac = skupiny[key][0][1] + datumzapisu = datetime.datetime.now().date() + caszapisu = datetime.datetime.now().time() + + # ── PRE-CHECK: zkus zamknout dnešní dekurz PŘED zpracováním souborů ────── + print(f"\n>>> Kontrola zámku dekurzu pro IDPAC={idpac}...") + try: + existujici = zkus_zamknout_dnesni_dekurs(conn, idpac, datumzapisu) + except RuntimeError as e: + # Vlákno nepřišlo do timeoutu = záznam drží Medicus + print(f"\n!!! DEKURZ ZAMČEN – soubory skupiny RC={key} přeskočeny.") + print(" Spusťte skript znovu až bude záznam volný.") + continue + except fdb.DatabaseError as e: + chyba = str(e).lower() + if 'deadlock' in chyba or 'lock conflict' in chyba or 'update conflict' in chyba: + print(f"\n!!! DEKURZ ZAMČEN (DB konflikt) – soubory skupiny RC={key} přeskočeny.") + print(" Spusťte skript znovu až bude záznam volný.") + continue + raise # jiná DB chyba – propaguj dál + + cislo = 9 + poradi = 0 + bookmark_list = [] + filenameforbookmark_list = [] + bookmarks_body = '' + + # ── Krok 1: vložit každý soubor do ext DB + přesunout do zpracovaných ──── + for row in skupiny[key]: + fileid = funkce_ext.zapis_file_ext( + vstupconnection=conn, idpac=row[1], + cesta=cesta, souborname=row[6], prvnizavorka=row[4], + soubordate=row[2], souborfiledate=row[7], poznamka=row[5]) + print(f" → FILES.ID = {fileid} ({row[6]})") + + # Přesun souboru do zpracovaných + for attempt in range(3): + try: + dest = os.path.join(cestazpracovana, row[6]) + if not os.path.exists(dest): + shutil.move(os.path.join(cesta, row[6]), dest) + else: + ts = datetime.datetime.now().strftime("%Y-%m-%d %H-%M-%S") + shutil.move(os.path.join(cesta, row[6]), + os.path.join(cestazpracovana, row[6][:-4] + " " + ts + ".pdf")) + print(" Přesun OK!") + break + except Exception as e: + print(f" Attempt {attempt + 1} failed: {e}") + if attempt < 2: + print(" Retrying in 5 seconds...") + time.sleep(5) + else: + print(" Max retries reached. Command failed.") + + filenameforbookmark = row[2].strftime('%Y-%m-%d') + ' ' + row[4] + ': ' + row[5] + bookmark_list.append('"' + filenameforbookmark + '","Files:' + str(fileid) + '",' + str(cislo)) + filenameforbookmark_list.append(filenameforbookmark) + cislo += 7 + + bookmarks_body += (r'\pard\s10{\*\bkmkstart ' + str(poradi) + r'}' + r'\plain\cs32\f0\ul\fs20\cf1 ' + filenameforbookmark + + r'{\*\bkmkend ' + str(poradi) + r'}\par') + poradi += 1 + + # ── Krok 2: sestavit tělo nové sekce příloh ─────────────────────────────── + new_body = (r'\uc1\pard\s10\plain\cs20\f0\i\fs20 Vlo\'9een\'e9 p\'f8\'edlohy:\par' + '\n' + + bookmarks_body + '\n' + + r'\pard\s10\plain\cs15\f0\fs20 \par') + + # ── Krok 3: rozhodovací logika (3 případy) ──────────────────────────────── + cur = conn.cursor() + + if existujici: + dekurs_id, existing_rtf = existujici + + if ma_sekci_prilohy(existing_rtf): + # Případ 1: dnešní dekurz má sekci příloh → přidáme soubory dovnitř + print(f"\n>>> Sekce 'Vložené přílohy' nalezena v DEKURS ID={dekurs_id}") + print(">>> Přidávám soubory DO existující sekce...") + merged_rtf = pridat_do_sekce_prilohy(existing_rtf, bookmark_list, filenameforbookmark_list) + else: + # Případ 2: dnešní dekurz existuje, ale sekci příloh nemá → prepend + print(f"\n>>> DEKURS ID={dekurs_id} nemá sekci příloh → vkládám sekci na začátek...") + merged_rtf = merge_rtf_prepend(existing_rtf, bookmark_list, new_body, len(skupiny[key])) + + print("\n=== Výsledný RTF ===") + print(merged_rtf) + cur.execute("UPDATE DEKURS SET DEKURS = ? WHERE ID = ?", (merged_rtf, dekurs_id)) + conn.commit() + print(f"\n>>> UPDATE DEKURS ID={dekurs_id} – hotovo!") + + else: + # Případ 3: žádný dnešní dekurz → vytvoříme nový + print(f"\n>>> Žádný dekurs pro dnešek → vytvářím nový...") + bookmark_str = ';'.join(bookmark_list) + rtf = RTF_TEMPLATE.replace('BOOKMARKNAMES', bookmark_str) + rtf = rtf.replace('BOOKMARKSTEXT', new_body) + + print("\n=== Výsledný RTF ===") + print(rtf) + + dekursid = funkce.get_dekurs_id(conn) + cur.execute( + "INSERT INTO DEKURS (id, iduzi, idprac, idodd, idpac, datum, cas, dekurs)" + " VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + (dekursid, 6, 2, 2, idpac, datumzapisu, caszapisu, rtf) + ) + conn.commit() + print(f"\n>>> Nový DEKURS ID={dekursid}") + +print("\n=== HOTOVO ===") +conn.close() diff --git a/MedicusWithClaude/s03soubory_01_FINAL_notes.md b/MedicusWithClaude/s03soubory_01_FINAL_notes.md new file mode 100644 index 0000000..8f728f8 --- /dev/null +++ b/MedicusWithClaude/s03soubory_01_FINAL_notes.md @@ -0,0 +1,226 @@ +# s03soubory_01_FINAL.py – dokumentace + +**Finální verze importního skriptu pro vkládání PDF dokumentů do dekurzů Medicusu.** + +Datum finalizace: 2026-04-04 +Autor: Vladimír Buzalka + Claude (Anthropic) + +--- + +## Co skript dělá + +Zpracuje PDF soubory v určené složce (`cesta`) a pro každý soubor: +1. Ověří správnost názvu souboru (formát, RC, datum) +2. Zkontroluje, zda cílový dekurz není zamčený v Medicusu +3. Zapíše soubor do externí databáze souborů (tabulka FILES) +4. Přesune soubor do složky zpracovaných +5. Vloží odkaz (bookmark) do dekurzu pacienta jako RTF záznam + +--- + +## Adresáře + +| Proměnná | Cesta (testovací) | Popis | +|---|---|---| +| `cesta` | `u:\testimport` | Vstupní složka – sem patří soubory ke zpracování | +| `cestazpracovana` | `u:\testimportzpracovana` | Cílová složka – sem se přesouvají zpracované soubory | + +> V produkci tyto cesty nahradit skutečnými složkami (Nextcloud/Dropbox). + +--- + +## Formát názvu souboru + +Každý PDF soubor musí mít název ve tvaru: + +``` +RC YYYY-MM-DD Příjmení, Jméno [typ dokumentu] [poznámka].pdf +``` + +**Příklad:** +``` +7309208104 2020-10-16 Buzalka, Vladimír [LZ ortopedie] [VAS LS páteře, obstřik].pdf +``` + +| Část | Popis | +|---|---| +| `RC` | Rodné číslo pacienta (9 nebo 10 číslic) – musí existovat v tabulce KAR | +| `YYYY-MM-DD` | Datum dokumentu | +| `Příjmení, Jméno` | Jméno pacienta (jen pro čitelnost, nepoužívá se k vyhledání) | +| `[typ dokumentu]` | První závorka – druh nálezu (LZ ortopedie, EKG, Lab. nález…) | +| `[poznámka]` | Druhá závorka – krátký popis obsahu (může být prázdná `[]`) | + +**Chybný soubor** (špatný název, RC nenalezeno v DB) je přejmenován přidáním prefixu `♥`: +``` +♥chybny soubor.pdf +``` +Skript ho přeskočí a nechá v složce pro ruční opravu. + +--- + +## Databázové tabulky + +| Tabulka | DB | Popis | +|---|---|---| +| `KAR` | Medicus (`medicus.fdb`) | Kartotéka pacientů – lookup RC → IDPAC | +| `DEKURS` | Medicus (`medicus.fdb`) | Dekurzy – čtení a zápis RTF záznamu | +| `FILES` | Externí DB (`MEDICUS_FILES_YYYYMM.fdb`) | Binární uložení PDF souborů | + +--- + +## Klíčová novinka oproti s03soubory.py – ochrana před zamčeným dekurzem + +### Problém +Medicus drží **exkluzivní zámek** (Firebird row lock) na záznamu tabulky DEKURS po celou dobu, kdy má lékařka pacienta otevřeného. Kdyby skript provedl `UPDATE DEKURS` do zamčeného záznamu, přepsal by lékařčiny neuložené změny. + +### Řešení – detekce zámku pomocí vlákna s timeoutem + +Firebird neumí NOWAIT nastavit per-statement v SQL (syntaxe `FOR UPDATE WITH LOCK NOWAIT` není platná). Nastavení NOWAIT je vlastnost transakce, nikoliv dotazu. Knihovna `fdb` navíc toto nastavení spolehlivě nepodporuje. + +**Zvolené řešení:** spuštění pokusu o zámek ve vedlejším vlákně s timeoutem 2 sekundy. + +``` +hlavní vlákno vedlejší vlákno (_pokus_o_zamek) +───────────────── ───────────────────────────────── +t.start() ──────► fdb.connect() [nové spojení] +t.join(timeout=2s) SELECT ... FOR UPDATE WITH LOCK + ├── záznam volný → fetchone() → rollback() → konec + └── záznam zamčený → čeká (blokuje)... +───────────────── +po 2 sekundách: + t.is_alive()? + ANO → záznam zamčený → RuntimeError → přeskoč skupinu + NE → záznam volný → pokračuj se zápisem +``` + +### Důležitý detail – pořadí operací + +**Kontrola zámku probíhá PŘED zápisem do FILES a PŘED přesunem souboru.** +Kdyby se pořadí obrátilo, mohlo by dojít k situaci: +- soubor zapsán do FILES ✓ +- soubor přesunut do zpracovaných ✓ +- dekurz zamčen → UPDATE selže +- soubor je pryč ze vstupní složky, ale odkaz v dekurzu chybí + +Správné pořadí: +``` +1. Zkontroluj zámek dekurzu (NOWAIT) + └── zamčeno → přeskoč (soubory zůstanou v cesta) +2. Zapiš soubory do ext. DB (FILES) +3. Přesuň soubory do zpracovaných +4. Sestav RTF +5. UPDATE nebo INSERT do DEKURS +``` + +--- + +## Logika vkládání do dekurzu – 3 případy + +Po úspěšné kontrole zámku skript rozhodne, co s dekurzem udělat: + +``` +Existuje dnešní dekurz pro pacienta? + │ + ├── ANO → obsahuje sekci "Vložené přílohy"? + │ ├── ANO → Případ 1: přidá nové soubory DOVNITŘ sekce + │ └── NE → Případ 2: vloží celou sekci na ZAČÁTEK dekurzu (prepend) + │ + └── NE → Případ 3: vytvoří NOVÝ dekurz ze šablony RTF_TEMPLATE +``` + +### Případ 1 – `pridat_do_sekce_prilohy()` + +Dnešní dekurz **existuje a má** sekci „Vložené přílohy". + +- Spočítá kolik odkazů (Files:) už sekce obsahuje → nové indexy bookmarků navazují +- Nové `\pard` řádky vloží **před** uzavírací prázdný řádek sekce (`PRILOHY_CLOSING`) +- Nové bookmarky přidá na **konec** `{\info{\bookmarks ...}}` +- Provede `UPDATE DEKURS SET DEKURS = ? WHERE ID = ?` + +### Případ 2 – `merge_rtf_prepend()` + +Dnešní dekurz **existuje, ale nemá** sekci příloh (lékařka do něj napsala text). + +- Přečísluje existující bookmarky (posunutí o počet nových souborů) +- Novou sekci „Vložené přílohy" vloží **na začátek** těla RTF (před `\uc1\pard`) +- Nové bookmarky předřadí před existující v `{\info{\bookmarks ...}}` +- Lékařčin text zůstane zachován, jen se posune níž +- Provede `UPDATE DEKURS SET DEKURS = ? WHERE ID = ?` + +### Případ 3 – nový INSERT + +Pro dnešní datum **neexistuje žádný dekurz**. + +- Vyplní `RTF_TEMPLATE` (bookmarky + tělo sekce příloh) +- Provede `INSERT INTO DEKURS (id, iduzi, idprac, idodd, idpac, datum, cas, dekurs)` +- `iduzi=6` (Vladimír Buzalka), `idprac=2`, `idodd=2` + +--- + +## RTF formát dekurzu + +### Struktura bookmarku +Každý přiložený soubor je reprezentován jako: +1. **Bookmark entry** v `{\info{\bookmarks ...}}`: + ``` + "2020-10-16 LZ ortopedie: VAS LS páteře, obstřik","Files:21923",9 + ``` + - `"popis"` – zobrazený text odkazu + - `"Files:ID"` – odkaz na záznam v tabulce FILES (slouží Medicusu k načtení souboru) + - `9` – číslo fontu/stylu (od 9, každý další +7) + +2. **Vizuální řádek** v těle RTF: + ```rtf + \pard\s10{\*\bkmkstart 0}\plain\cs32\f0\ul\fs20\cf1 2020-10-16 LZ ortopedie: VAS LS páteře, obstřik{\*\bkmkend 0}\par + ``` + - `\bkmkstart N` / `\bkmkend N` – index bookmarku (0, 1, 2…) + - `\cs32\ul\cf1` – styl „Odkaz" (modrý podtržený text) + +### Konstanty pro detekci sekce příloh (win1250 RTF escape) +```python +PRILOHY_HEADER = r"Vlo\'9een\'e9 p\'f8\'edlohy:" # "Vložené přílohy:" +PRILOHY_CLOSING = r'\pard\s10\plain\cs15\f0\fs20 \par' # uzavírací prázdný řádek +``` + +--- + +## Funkce – přehled + +| Funkce | Popis | +|---|---| +| `restore_files_for_import(retezec)` | Debug utilita – vrátí soubory z Nextcloudu zpět do Dropboxu. Nepoužívá se v produkci. | +| `kontrola_rc(rc, connection)` | Ověří zda RC existuje v KAR, vrátí IDPAC nebo False. | +| `kontrola_struktury(souborname, connection)` | Ověří formát názvu souboru a existenci RC v DB. | +| `vrat_info_o_souboru(souborname, connection)` | Parsuje název souboru, dohledá IDPAC, vrátí tuple s metadaty. | +| `prejmenuj_chybny_soubor(souborname, cesta)` | Přidá prefix `♥` k chybnému souboru. | +| `_pokus_o_zamek(dekurs_id, vysledek)` | Interní – běží ve vlákně, zkouší zamknout dekurz. | +| `zkus_zamknout_dnesni_dekurs(conn, idpac, datum_vlozeni, timeout_sec=2)` | Zjistí zda dnešní dekurz existuje a není zamčený. Vyhodí RuntimeError pokud je zamčený. | +| `ma_sekci_prilohy(rtf)` | Vrátí True pokud RTF obsahuje sekci „Vložené přílohy". | +| `pridat_do_sekce_prilohy(rtf, bookmark_list, filenameforbookmark_list)` | Případ 1 – přidá soubory do existující sekce příloh. | +| `merge_rtf_prepend(existing_rtf, new_bkm_list, new_body_pards, n_new)` | Případ 2 – vloží sekci příloh na začátek existujícího dekurzu. | + +--- + +## Ošetření chyb + +| Situace | Chování | +|---|---| +| Soubor má chybný název | Přejmenován na `♥soubor.pdf`, přeskočen | +| RC nenalezeno v KAR | Přejmenován na `♥soubor.pdf`, přeskočen | +| Dekurz zamčený (timeout vlákna) | Skupina přeskočena, soubory zůstanou v `cesta` | +| DB konflikt při zamykání (-913 deadlock) | Skupina přeskočena, soubory zůstanou v `cesta` | +| Přesun souboru selže | 3 pokusy s 5s pauzou, poté varování | +| Jiná DB chyba | Výjimka se propaguje, skript havaruje | + +--- + +## Vývoj a testování + +| Verze | Soubor | Co přibilo | +|---|---|---| +| Prototyp | `test_import_FINAL.py` | Ruční zadání IDPAC a DATUM, ověření RTF logiky | +| v1 | `s03soubory.py` | Automatický parsing RC z názvu, dávkování po skupinách | +| **v1 FINAL** | `s03soubory_01_FINAL.py` | Ochrana před zamčeným dekurzem (threading + timeout) | + +### Jak byl objeven problém se zámky +Experimentem bylo ověřeno, že Medicus drží Firebird row lock na záznamu DEKURS po celou dobu, kdy má lékařka pacienta otevřeného (`SELECT FIRST 1 ... FOR UPDATE WITH LOCK` z Pythonu čekalo dokud lékařka neuložila). NOWAIT nelze nastavit přes SQL syntaxi ani spolehlivě přes fdb TPB bajty, proto bylo zvoleno řešení přes vlákno s timeoutem.