Files
medicus/MedicusWithClaude/s03soubory_01_FINAL_notes.md
2026-04-04 08:57:00 +02:00

227 lines
9.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.