Add MedicusWithClaude project - DB exploration scripts and notes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
163
MedicusWithClaude/CLAUDE_NOTES.md
Normal file
163
MedicusWithClaude/CLAUDE_NOTES.md
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# MedicusWithClaude – poznámky pro Clauda
|
||||||
|
|
||||||
|
## Stav projektu
|
||||||
|
- Zahájeno: 2026-03-13
|
||||||
|
- Cíl: průzkum Firebird DB Medicus → analýzy a reporty
|
||||||
|
- Zatím: úspěšně čteme i zapisujeme do DB, rozumíme RTF formátu
|
||||||
|
|
||||||
|
## Bezpečnost
|
||||||
|
- Pracujeme na **místní kopii** – poškození DB nevadí, obnova = 5 minut
|
||||||
|
- Lze bez obav experimentovat, zapisovat testovací data atd.
|
||||||
|
- Testovací záznamy nemusíme mazat
|
||||||
|
|
||||||
|
## Připojení k DB
|
||||||
|
```python
|
||||||
|
import fdb
|
||||||
|
conn = fdb.connect(
|
||||||
|
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
|
||||||
|
user='SYSDBA',
|
||||||
|
password='masterkey',
|
||||||
|
charset='win1250'
|
||||||
|
)
|
||||||
|
```
|
||||||
|
- fdb verze 2.0.4, Python na PATH (prostý `python` příkaz)
|
||||||
|
|
||||||
|
## O Medicusu
|
||||||
|
- Lékařský software pro praktického lékaře
|
||||||
|
- Vyvíjen od ~roku 2000, organicky rostl přidáváním funkcí
|
||||||
|
- Firebird 2.5 – nikdy nepřejdou na nový (desetitisíce instalací, 993 tabulek)
|
||||||
|
- Žádné zabezpečení DB – vše čitelné přes SYSDBA/masterkey
|
||||||
|
- Charset: win1250
|
||||||
|
|
||||||
|
## DB fakta
|
||||||
|
- 993 tabulek celkem
|
||||||
|
- Charset: win1250
|
||||||
|
|
||||||
|
## Klíčové tabulky
|
||||||
|
|
||||||
|
### KAR – Kartotéka (základní kámen Medicusu)
|
||||||
|
- 6319 pacientů – všichni kdo kdy prošli ordinací
|
||||||
|
- Primární klíč: **IDPAC** (interní ID, používá se jako FK v ostatních tabulkách)
|
||||||
|
- Identifikace pacienta v ČR/pojišťovna: **RODCIS** (rodné číslo, bez lomítka, např. 7309208104)
|
||||||
|
- **PRIJMENI**, **JMENO**, TITUL, TITULZA, DATNAR, POHLAVI
|
||||||
|
- **POJ** – pojišťovna (3 znaky, např. 111 = VZP)
|
||||||
|
- Adresy: TRV* (trvalé), PRE* (přechodné)
|
||||||
|
- **VYRAZEN** – příznak vyřazeného pacienta
|
||||||
|
- HLAVDGN – hlavní diagnóza
|
||||||
|
|
||||||
|
### DEKURS – záznamy z návštěv
|
||||||
|
- 172 068 záznamů
|
||||||
|
- **IDPAC** → vazba na KAR
|
||||||
|
- **DATUM** (date), **CAS** (time) – kdy
|
||||||
|
- **DEKURS** – text záznamu (BLOB, RTF nebo holý text)
|
||||||
|
- **DGN1** – hlavní diagnóza (5 znaků, např. 'Z000 '), VDGN1-4 – vedlejší
|
||||||
|
- **IDPRAC** – který pracovník
|
||||||
|
- IDODD, IDUZI – oddělení/uživatel (u Buzalky = 2, 2, 6)
|
||||||
|
- IDSKUPINA, IDTYP – volitelné (NULL OK)
|
||||||
|
|
||||||
|
## Základní schéma
|
||||||
|
```
|
||||||
|
KAR (kartotéka) ←IDPAC→ DEKURS (záznamy z návštěv)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testovací pacient (uživatel)
|
||||||
|
- **IDPAC = 9742** – Buzalka Vladimír, RC 7309208104, nar. 20.9.1973
|
||||||
|
- IDODD=2, IDPRAC=2, IDUZI=6 (použít při INSERT do DEKURS)
|
||||||
|
|
||||||
|
## RTF formát v DEKURS
|
||||||
|
|
||||||
|
### Základní struktura
|
||||||
|
```
|
||||||
|
{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029
|
||||||
|
{\info{\bookmarks "popis","Typ:ID",číslo}} ← metadata Medicusu
|
||||||
|
{\fonttbl{\f0\fnil\fcharset238 Arial;}{\f2...Courier New;}{\f5...Symbol;}}
|
||||||
|
{\colortbl ;\red0\green0\blue255;\red255\green255\blue0;\red0\green128\blue0;\red192\green192\blue192;}
|
||||||
|
{\stylesheet{styly...}}
|
||||||
|
\uc1\pard\s10\plain... ← tělo záznamu
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Barvy (\cfX)
|
||||||
|
- `\cf1` = modrá (odkazy)
|
||||||
|
- `\cf2` = žlutá (highlight)
|
||||||
|
- `\cf3` = zelená
|
||||||
|
- `\cf4` = šedá (pozadí buněk tabulky `\clcbpat4`)
|
||||||
|
|
||||||
|
### Velikost písma (\fsXX = XX/2 bodů)
|
||||||
|
- `\fs18` = 9pt (tabulky)
|
||||||
|
- `\fs20` = 10pt (normální)
|
||||||
|
- `\fs24` = 12pt atd.
|
||||||
|
|
||||||
|
### Řezy a formátování
|
||||||
|
- `\b` = tučné, `\i` = kurzíva, `\ul` = podtržené
|
||||||
|
- `\ql` = vlevo, `\qr` = vpravo, `\qc` = střed
|
||||||
|
|
||||||
|
### Styly (stylesheet)
|
||||||
|
- `cs15/cs16` = Normální
|
||||||
|
- `cs20` = Vkládaný text
|
||||||
|
- `cs21` = Záhlaví (italic)
|
||||||
|
- `cs22/cs23` = Odkaz (underline, modrá)
|
||||||
|
- `s8/s10` = Vlevo (paragraph styl)
|
||||||
|
|
||||||
|
### Záložky (bookmarks v {\info})
|
||||||
|
- Klikatelné odkazy v Medicusu
|
||||||
|
- Formát: `"popis","Typ:ID",číslo`
|
||||||
|
- Typy: `Files:ID`, `MEDLAB:ID`, `Lab:ID`, `Ock:ID`, `VykA:ID`
|
||||||
|
|
||||||
|
### České znaky
|
||||||
|
- RTF hex escape v win1250: `\'e1`=á, `\'9e`=ž, `\'fd`=ý atd.
|
||||||
|
|
||||||
|
### Tabulky
|
||||||
|
- Plně funkční RTF tabulky (`\trowd`, `\cell`, `\cellx...`)
|
||||||
|
- Používají se např. pro laboratorní výsledky (viz ID=243082, 50KB)
|
||||||
|
|
||||||
|
### Holý text (starší záznamy)
|
||||||
|
- Některé starší záznamy jsou prostý text bez RTF obálky
|
||||||
|
- Nutno detekovat: začíná na `{\\rtf` = RTF, jinak plain text
|
||||||
|
|
||||||
|
### Příklad minimálního RTF záznamu
|
||||||
|
```
|
||||||
|
{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029
|
||||||
|
{\fonttbl{\f0\fnil\fcharset238 Arial;}}
|
||||||
|
{\colortbl ;\red0\green0\blue255;\red0\green128\blue0;\red0\green0\blue0;}
|
||||||
|
\pard\plain\f0\fs20 Text záznamu zde\par
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## INSERT do DEKURS – ověřený postup
|
||||||
|
```python
|
||||||
|
import fdb, datetime
|
||||||
|
|
||||||
|
conn = fdb.connect(...)
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
cur.execute('SELECT MAX(ID) FROM DEKURS')
|
||||||
|
next_id = cur.fetchone()[0] + 1
|
||||||
|
|
||||||
|
text = r'{\rtf1\ansi\ansicpg1250...\par' + '\n}' # raw string pro RTF!
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO DEKURS (ID, IDPAC, IDODD, IDPRAC, IDUZI, DATUM, CAS, DEKURS, DGN1)
|
||||||
|
VALUES (?, ?, 2, 2, 6, ?, ?, ?, ?)
|
||||||
|
""", (next_id, idpac, datum, cas, text, dgn1))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
```
|
||||||
|
- RTF string musí být **raw string** (r'...') kvůli zpětným lomítkům
|
||||||
|
- DGN1 = 5 znaků s mezerou: `'Z000 '`
|
||||||
|
- DATUM = datetime.date(...), CAS = datetime.time(...)
|
||||||
|
|
||||||
|
## Soubory v projektu
|
||||||
|
- `explore_db.py` – výpis všech tabulek a jejich sloupců
|
||||||
|
- `inspect_table.py` – detailní info o konkrétní tabulce (arg: název tabulky)
|
||||||
|
- `sample_dekurs.py` – ukázky nejstarších a nejnovějších záznamů
|
||||||
|
- `sample_dekurs2.py` – celý text jednoho záznamu
|
||||||
|
- `sample_rtf.py` – nejdelší záznamy pacienta (dle délky RTF)
|
||||||
|
- `analyze_rtf.py` – analýza RTF tagů v záznamu
|
||||||
|
- `insert_test.py` – testovací INSERT do DEKURS
|
||||||
|
- `CLAUDE_NOTES.md` – tento soubor
|
||||||
|
|
||||||
|
## Další postup (nápady)
|
||||||
|
- Napsat `rtf_to_text()` pro extrakci čistého textu z dekurzů
|
||||||
|
- Prozkoumat tabulky: LECH/LECD (léky?), POU (poukazy?), AMBULEKY (výkony?)
|
||||||
|
- První report – domluvit s uživatelem co chce vidět
|
||||||
34
MedicusWithClaude/analyze_rtf.py
Normal file
34
MedicusWithClaude/analyze_rtf.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import fdb, re
|
||||||
|
|
||||||
|
conn = fdb.connect(
|
||||||
|
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
|
||||||
|
user='SYSDBA', password='masterkey', charset='win1250'
|
||||||
|
)
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute('SELECT DEKURS FROM DEKURS WHERE ID=243082')
|
||||||
|
text = cur.fetchone()[0]
|
||||||
|
text = text.read() if hasattr(text, 'read') else text
|
||||||
|
text = text.decode('windows-1250', errors='replace') if isinstance(text, bytes) else text
|
||||||
|
|
||||||
|
# colortbl
|
||||||
|
ct = re.search(r'\\colortbl[^}]+\}', text)
|
||||||
|
if ct: print('COLORTBL:', ct.group(0))
|
||||||
|
|
||||||
|
# stylesheet
|
||||||
|
ss = re.search(r'\\stylesheet\{.+?\}(?=\n)', text, re.DOTALL)
|
||||||
|
if ss: print('\nSTYLESHEET:', ss.group(0)[:600])
|
||||||
|
|
||||||
|
# Najdi různé styly použité v textu
|
||||||
|
print('\n--- Použité RTF tagy (unikátní) ---')
|
||||||
|
tags = re.findall(r'\\[a-z]+\d*', text)
|
||||||
|
from collections import Counter
|
||||||
|
for tag, count in Counter(tags).most_common(40):
|
||||||
|
print(f" {tag:<20} {count}x")
|
||||||
|
|
||||||
|
# Ukázka tabulky
|
||||||
|
tbl_start = text.find(r'\trowd')
|
||||||
|
if tbl_start > 0:
|
||||||
|
print('\n--- Začátek tabulky ---')
|
||||||
|
print(text[tbl_start:tbl_start+300])
|
||||||
|
|
||||||
|
conn.close()
|
||||||
45
MedicusWithClaude/explore_db.py
Normal file
45
MedicusWithClaude/explore_db.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
"""
|
||||||
|
Průzkum Medicus databáze – výpis tabulek a jejich sloupců.
|
||||||
|
"""
|
||||||
|
import fdb
|
||||||
|
|
||||||
|
conn = fdb.connect(
|
||||||
|
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
|
||||||
|
user='SYSDBA',
|
||||||
|
password='masterkey',
|
||||||
|
charset='win1250'
|
||||||
|
)
|
||||||
|
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# Všechny uživatelské tabulky
|
||||||
|
cur.execute("""
|
||||||
|
SELECT rdb$relation_name
|
||||||
|
FROM rdb$relations
|
||||||
|
WHERE rdb$view_blr IS NULL
|
||||||
|
AND (rdb$system_flag IS NULL OR rdb$system_flag = 0)
|
||||||
|
ORDER BY rdb$relation_name
|
||||||
|
""")
|
||||||
|
tables = [row[0].strip() for row in cur.fetchall()]
|
||||||
|
|
||||||
|
print(f"Počet tabulek: {len(tables)}\n")
|
||||||
|
print("=== SEZNAM TABULEK ===")
|
||||||
|
for t in tables:
|
||||||
|
print(f" {t}")
|
||||||
|
|
||||||
|
print("\n=== SLOUPCE KAŽDÉ TABULKY ===")
|
||||||
|
for table in tables:
|
||||||
|
cur.execute("""
|
||||||
|
SELECT rf.rdb$field_name, f.rdb$field_type, f.rdb$field_length
|
||||||
|
FROM rdb$relation_fields rf
|
||||||
|
JOIN rdb$fields f ON rf.rdb$field_source = f.rdb$field_name
|
||||||
|
WHERE rf.rdb$relation_name = ?
|
||||||
|
ORDER BY rf.rdb$field_position
|
||||||
|
""", (table,))
|
||||||
|
cols = cur.fetchall()
|
||||||
|
col_names = [c[0].strip() for c in cols]
|
||||||
|
print(f"\n{table}")
|
||||||
|
print(" " + ", ".join(col_names))
|
||||||
|
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
23
MedicusWithClaude/insert_test.py
Normal file
23
MedicusWithClaude/insert_test.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import fdb
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
conn = fdb.connect(
|
||||||
|
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
|
||||||
|
user='SYSDBA', password='masterkey', charset='win1250'
|
||||||
|
)
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
cur.execute('SELECT MAX(ID) FROM DEKURS')
|
||||||
|
next_id = cur.fetchone()[0] + 1
|
||||||
|
print('Next ID:', next_id)
|
||||||
|
|
||||||
|
text = r'{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029{\fonttbl{\f0\fnil\fcharset238 Arial;}}{\colortbl ;\red0\green0\blue255;\red0\green128\blue0;\red0\green0\blue0;}' + '\n' + r'\pard\plain\f0\fs20 Test Claude AI - automaticky vlo\u382en\'fd z\'e1znam 13.03.2026\par' + '\n}'
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO DEKURS (ID, IDPAC, IDODD, IDPRAC, IDUZI, DATUM, CAS, DEKURS, DGN1)
|
||||||
|
VALUES (?, 9742, 2, 2, 6, ?, ?, ?, 'Z000 ')
|
||||||
|
""", (next_id, datetime.date(2026, 3, 13), datetime.time(12, 0, 0), text))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
print('Vloženo OK, ID:', next_id)
|
||||||
|
conn.close()
|
||||||
28
MedicusWithClaude/inspect_table.py
Normal file
28
MedicusWithClaude/inspect_table.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import fdb
|
||||||
|
import sys
|
||||||
|
|
||||||
|
table = sys.argv[1] if len(sys.argv) > 1 else 'DEKURS'
|
||||||
|
|
||||||
|
conn = fdb.connect(
|
||||||
|
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
|
||||||
|
user='SYSDBA', password='masterkey', charset='win1250'
|
||||||
|
)
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
SELECT rf.rdb$field_name, f.rdb$field_type, f.rdb$field_length
|
||||||
|
FROM rdb$relation_fields rf
|
||||||
|
JOIN rdb$fields f ON rf.rdb$field_source = f.rdb$field_name
|
||||||
|
WHERE rf.rdb$relation_name = ?
|
||||||
|
ORDER BY rf.rdb$field_position
|
||||||
|
""", (table,))
|
||||||
|
cols = cur.fetchall()
|
||||||
|
print(f"=== {table} – sloupce ===")
|
||||||
|
for c in cols:
|
||||||
|
print(f" {c[0].strip():<30} typ: {c[1]} délka: {c[2]}")
|
||||||
|
|
||||||
|
cur.execute(f'SELECT COUNT(*) FROM {table}')
|
||||||
|
print(f"\nPočet záznamů: {cur.fetchone()[0]}")
|
||||||
|
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
37
MedicusWithClaude/sample_dekurs.py
Normal file
37
MedicusWithClaude/sample_dekurs.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import fdb
|
||||||
|
|
||||||
|
conn = fdb.connect(
|
||||||
|
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
|
||||||
|
user='SYSDBA', password='masterkey', charset='win1250'
|
||||||
|
)
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
print("=== NEJSTARŠÍ záznamy ===")
|
||||||
|
cur.execute("""
|
||||||
|
SELECT FIRST 5 ID, DATUM, DEKURS
|
||||||
|
FROM DEKURS
|
||||||
|
WHERE DEKURS IS NOT NULL
|
||||||
|
ORDER BY DATUM ASC
|
||||||
|
""")
|
||||||
|
for row in cur.fetchall():
|
||||||
|
text = row[2].read() if hasattr(row[2], 'read') else row[2]
|
||||||
|
if isinstance(text, bytes):
|
||||||
|
text = text.decode('windows-1250', errors='replace')
|
||||||
|
print(f"\n--- ID={row[0]} DATUM={row[1]} ---")
|
||||||
|
print(repr(text[:300]))
|
||||||
|
|
||||||
|
print("\n\n=== NEJNOVĚJŠÍ záznamy ===")
|
||||||
|
cur.execute("""
|
||||||
|
SELECT FIRST 5 ID, DATUM, DEKURS
|
||||||
|
FROM DEKURS
|
||||||
|
WHERE DEKURS IS NOT NULL
|
||||||
|
ORDER BY DATUM DESC
|
||||||
|
""")
|
||||||
|
for row in cur.fetchall():
|
||||||
|
text = row[2].read() if hasattr(row[2], 'read') else row[2]
|
||||||
|
if isinstance(text, bytes):
|
||||||
|
text = text.decode('windows-1250', errors='replace')
|
||||||
|
print(f"\n--- ID={row[0]} DATUM={row[1]} ---")
|
||||||
|
print(repr(text[:300]))
|
||||||
|
|
||||||
|
conn.close()
|
||||||
25
MedicusWithClaude/sample_dekurs2.py
Normal file
25
MedicusWithClaude/sample_dekurs2.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import fdb
|
||||||
|
|
||||||
|
conn = fdb.connect(
|
||||||
|
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
|
||||||
|
user='SYSDBA', password='masterkey', charset='win1250'
|
||||||
|
)
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
SELECT FIRST 1 ID, DATUM, DEKURS
|
||||||
|
FROM DEKURS
|
||||||
|
WHERE DEKURS IS NOT NULL
|
||||||
|
ORDER BY DATUM DESC
|
||||||
|
""")
|
||||||
|
row = cur.fetchone()
|
||||||
|
text = row[2].read() if hasattr(row[2], 'read') else row[2]
|
||||||
|
if isinstance(text, bytes):
|
||||||
|
text = text.decode('windows-1250', errors='replace')
|
||||||
|
|
||||||
|
print(f"ID={row[0]} DATUM={row[1]}")
|
||||||
|
print(f"Délka: {len(text)} znaků")
|
||||||
|
print("\n--- CELÝ TEXT ---")
|
||||||
|
print(text)
|
||||||
|
|
||||||
|
conn.close()
|
||||||
35
MedicusWithClaude/sample_rtf.py
Normal file
35
MedicusWithClaude/sample_rtf.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import fdb
|
||||||
|
|
||||||
|
conn = fdb.connect(
|
||||||
|
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
|
||||||
|
user='SYSDBA', password='masterkey', charset='win1250'
|
||||||
|
)
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# Načti všechny záznamy pacienta a zjisti délku v Pythonu
|
||||||
|
cur.execute("""
|
||||||
|
SELECT ID, DATUM, DEKURS
|
||||||
|
FROM DEKURS
|
||||||
|
WHERE IDPAC = 9742 AND DEKURS IS NOT NULL
|
||||||
|
ORDER BY DATUM DESC
|
||||||
|
""")
|
||||||
|
rows = cur.fetchall()
|
||||||
|
|
||||||
|
def read_blob(val):
|
||||||
|
text = val.read() if hasattr(val, 'read') else val
|
||||||
|
if isinstance(text, bytes):
|
||||||
|
text = text.decode('windows-1250', errors='replace')
|
||||||
|
return text
|
||||||
|
|
||||||
|
# Seřaď podle délky
|
||||||
|
data = [(r[0], r[1], read_blob(r[2])) for r in rows]
|
||||||
|
data.sort(key=lambda x: len(x[2]), reverse=True)
|
||||||
|
|
||||||
|
print("Top 10 nejdelších záznamů:")
|
||||||
|
for d in data[:10]:
|
||||||
|
print(f" ID={d[0]} datum={d[1]} délka={len(d[2])}")
|
||||||
|
|
||||||
|
print("\n--- Nejdelší záznam (celý RTF) ---")
|
||||||
|
print(data[0][2])
|
||||||
|
|
||||||
|
conn.close()
|
||||||
Reference in New Issue
Block a user