Compare commits
19 Commits
a210f801d3
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c08ad8e35 | |||
| 30d64680be | |||
| 55e723788b | |||
| 2d2a60a845 | |||
| 186c98fd0d | |||
| d1bfe92e28 | |||
| 21c11d2336 | |||
| a95f8ae1f9 | |||
| 139e867a6d | |||
| 65c90750dc | |||
| 4c68c095db | |||
| eeea6740ac | |||
| c8e58a0246 | |||
| f159120175 | |||
| f8ada463a2 | |||
| ac16eedde9 | |||
| 8fce419afd | |||
| 7c185fec68 | |||
|
|
ea32ea0bc1 |
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Virtual environment
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# PyCharm / IDE
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
3
.idea/.gitignore
generated
vendored
3
.idea/.gitignore
generated
vendored
@@ -1,3 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
10
.idea/Medevio.iml
generated
10
.idea/Medevio.iml
generated
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module type="PYTHON_MODULE" version="4">
|
|
||||||
<component name="NewModuleRootManager">
|
|
||||||
<content url="file://$MODULE_DIR$">
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
|
||||||
</content>
|
|
||||||
<orderEntry type="jdk" jdkName="Python 3.12" jdkType="Python SDK" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
||||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
6
.idea/inspectionProfiles/profiles_settings.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<component name="InspectionProjectProfileManager">
|
|
||||||
<settings>
|
|
||||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
|
||||||
<version value="1.0" />
|
|
||||||
</settings>
|
|
||||||
</component>
|
|
||||||
7
.idea/misc.xml
generated
7
.idea/misc.xml
generated
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="Black">
|
|
||||||
<option name="sdkName" value="Python 3.12 (Medevio)" />
|
|
||||||
</component>
|
|
||||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12" project-jdk-type="Python SDK" />
|
|
||||||
</project>
|
|
||||||
8
.idea/modules.xml
generated
8
.idea/modules.xml
generated
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/Medevio.iml" filepath="$PROJECT_DIR$/.idea/Medevio.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
@@ -8,6 +8,18 @@ from datetime import datetime, timezone
|
|||||||
import time
|
import time
|
||||||
from dateutil import parser
|
from dateutil import parser
|
||||||
|
|
||||||
|
# Force UTF-8 output even under Windows Task Scheduler
|
||||||
|
import sys
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
sys.stderr.reconfigure(encoding='utf-8')
|
||||||
|
except AttributeError:
|
||||||
|
# Python < 3.7 fallback (not needed for you, but safe)
|
||||||
|
import io
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||||
|
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
# ================================
|
# ================================
|
||||||
# 🔧 CONFIGURATION
|
# 🔧 CONFIGURATION
|
||||||
# ================================
|
# ================================
|
||||||
@@ -16,7 +28,7 @@ CLINIC_SLUG = "mudr-buzalkova"
|
|||||||
BATCH_SIZE = 100
|
BATCH_SIZE = 100
|
||||||
|
|
||||||
DB_CONFIG = {
|
DB_CONFIG = {
|
||||||
"host": "127.0.0.1",
|
"host": "192.168.1.76",
|
||||||
"port": 3307,
|
"port": 3307,
|
||||||
"user": "root",
|
"user": "root",
|
||||||
"password": "Vlado9674+",
|
"password": "Vlado9674+",
|
||||||
|
|||||||
@@ -6,16 +6,44 @@ import requests
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from dateutil import parser
|
from dateutil import parser
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Force UTF-8 output even under Windows Task Scheduler
|
||||||
|
import sys
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
sys.stderr.reconfigure(encoding='utf-8')
|
||||||
|
except AttributeError:
|
||||||
|
# Python < 3.7 fallback (not needed for you, but safe)
|
||||||
|
import io
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||||
|
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
|
||||||
|
|
||||||
|
# ================================
|
||||||
|
# 🛡 SAFE PRINT FOR CP1250 / Emoji
|
||||||
|
# ================================
|
||||||
|
def safe_print(text: str):
|
||||||
|
enc = sys.stdout.encoding or ""
|
||||||
|
if not enc.lower().startswith("utf"):
|
||||||
|
# strip emoji + characters outside BMP
|
||||||
|
text = ''.join(ch for ch in text if ord(ch) < 65536)
|
||||||
|
try:
|
||||||
|
print(text)
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
# final fallback to ASCII only
|
||||||
|
text = ''.join(ch for ch in text if ord(ch) < 128)
|
||||||
|
print(text)
|
||||||
|
|
||||||
|
|
||||||
# ================================
|
# ================================
|
||||||
# 🔧 CONFIGURATION
|
# 🔧 CONFIGURATION
|
||||||
# ================================
|
# ================================
|
||||||
TOKEN_PATH = Path("token.txt")
|
TOKEN_PATH = Path("token.txt")
|
||||||
CLINIC_SLUG = "mudr-buzalkova"
|
CLINIC_SLUG = "mudr-buzalkova"
|
||||||
LIMIT = 300 # stáhneme posledních 300 ukončených požadavků
|
LIMIT = 300
|
||||||
|
|
||||||
DB_CONFIG = {
|
DB_CONFIG = {
|
||||||
"host": "127.0.0.1",
|
"host": "192.168.1.76",
|
||||||
"port": 3307,
|
"port": 3307,
|
||||||
"user": "root",
|
"user": "root",
|
||||||
"password": "Vlado9674+",
|
"password": "Vlado9674+",
|
||||||
@@ -24,7 +52,7 @@ DB_CONFIG = {
|
|||||||
"cursorclass": pymysql.cursors.DictCursor,
|
"cursorclass": pymysql.cursors.DictCursor,
|
||||||
}
|
}
|
||||||
|
|
||||||
# ⭐ Ověřený dotaz s lastMessage
|
# ⭐ GraphQL query
|
||||||
GRAPHQL_QUERY = r"""
|
GRAPHQL_QUERY = r"""
|
||||||
query ClinicRequestList2(
|
query ClinicRequestList2(
|
||||||
$clinicSlug: String!,
|
$clinicSlug: String!,
|
||||||
@@ -71,33 +99,31 @@ def read_token(path: Path) -> str:
|
|||||||
return tok.split(" ", 1)[1]
|
return tok.split(" ", 1)[1]
|
||||||
return tok
|
return tok
|
||||||
|
|
||||||
|
|
||||||
# ================================
|
# ================================
|
||||||
# DATETIME PARSER (UTC → MySQL)
|
# DATETIME PARSER
|
||||||
# ================================
|
# ================================
|
||||||
def to_mysql_dt(iso_str):
|
def to_mysql_dt(iso_str):
|
||||||
if not iso_str:
|
if not iso_str:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
dt = parser.isoparse(iso_str) # ISO8601 → aware datetime (UTC)
|
dt = parser.isoparse(iso_str)
|
||||||
dt = dt.astimezone() # převede na lokální čas (CET/CEST)
|
dt = dt.astimezone()
|
||||||
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
# ================================
|
# ================================
|
||||||
# UPSERT WITH MERGED UPDATED TIME
|
# UPSERT
|
||||||
# ================================
|
# ================================
|
||||||
def upsert(conn, r):
|
def upsert(conn, r):
|
||||||
p = r.get("extendedPatient") or {}
|
p = r.get("extendedPatient") or {}
|
||||||
|
|
||||||
# API pole
|
|
||||||
api_updated = to_mysql_dt(r.get("updatedAt"))
|
api_updated = to_mysql_dt(r.get("updatedAt"))
|
||||||
|
|
||||||
# poslední zpráva
|
|
||||||
last_msg = r.get("lastMessage") or {}
|
last_msg = r.get("lastMessage") or {}
|
||||||
msg_at = to_mysql_dt(last_msg.get("createdAt"))
|
msg_at = to_mysql_dt(last_msg.get("createdAt"))
|
||||||
|
|
||||||
# vybereme novější čas
|
|
||||||
def max_dt(a, b):
|
def max_dt(a, b):
|
||||||
if a and b:
|
if a and b:
|
||||||
return max(a, b)
|
return max(a, b)
|
||||||
@@ -137,6 +163,7 @@ def upsert(conn, r):
|
|||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
# ================================
|
# ================================
|
||||||
# FETCH LAST 300 DONE REQUESTS
|
# FETCH LAST 300 DONE REQUESTS
|
||||||
# ================================
|
# ================================
|
||||||
@@ -162,6 +189,7 @@ def fetch_done(headers):
|
|||||||
data = r.json()["data"]["requestsResponse"]
|
data = r.json()["data"]["requestsResponse"]
|
||||||
return data.get("patientRequests", [])
|
return data.get("patientRequests", [])
|
||||||
|
|
||||||
|
|
||||||
# ================================
|
# ================================
|
||||||
# MAIN
|
# MAIN
|
||||||
# ================================
|
# ================================
|
||||||
@@ -175,17 +203,18 @@ def main():
|
|||||||
|
|
||||||
conn = pymysql.connect(**DB_CONFIG)
|
conn = pymysql.connect(**DB_CONFIG)
|
||||||
|
|
||||||
print(f"\n=== Downloading last {LIMIT} DONE requests @ {datetime.now():%Y-%m-%d %H:%M:%S} ===")
|
safe_print(f"\n=== Downloading last {LIMIT} DONE requests @ {datetime.now():%Y-%m-%d %H:%M:%S} ===")
|
||||||
|
|
||||||
requests_list = fetch_done(headers)
|
requests_list = fetch_done(headers)
|
||||||
print(f"📌 Requests returned: {len(requests_list)}")
|
safe_print(f"📌 Requests returned: {len(requests_list)}")
|
||||||
|
|
||||||
for r in requests_list:
|
for r in requests_list:
|
||||||
upsert(conn, r)
|
upsert(conn, r)
|
||||||
|
|
||||||
conn.close()
|
conn.close()
|
||||||
print("\n✅ DONE - latest closed requests synced.\n")
|
safe_print("\n\u2705 DONE - latest closed requests synced.\n")
|
||||||
|
|
||||||
|
|
||||||
|
# ================================
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -12,6 +12,35 @@ import pymysql
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import time
|
import time
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Force UTF-8 output even under Windows Task Scheduler
|
||||||
|
import sys
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
sys.stderr.reconfigure(encoding='utf-8')
|
||||||
|
except AttributeError:
|
||||||
|
# Python < 3.7 fallback (not needed for you, but safe)
|
||||||
|
import io
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||||
|
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# 🛡 SAFE PRINT FOR CP1250 / EMOJI
|
||||||
|
# ==============================
|
||||||
|
def safe_print(text: str):
|
||||||
|
enc = sys.stdout.encoding or ""
|
||||||
|
if not enc.lower().startswith("utf"):
|
||||||
|
# strip emoji + anything above BMP
|
||||||
|
text = ''.join(ch for ch in text if ord(ch) < 65536)
|
||||||
|
try:
|
||||||
|
print(text)
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
# final ASCII fallback
|
||||||
|
text = ''.join(ch for ch in text if ord(ch) < 128)
|
||||||
|
print(text)
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
# ==============================
|
||||||
# 🔧 CONFIGURATION
|
# 🔧 CONFIGURATION
|
||||||
@@ -21,7 +50,7 @@ CLINIC_SLUG = "mudr-buzalkova"
|
|||||||
GRAPHQL_URL = "https://api.medevio.cz/graphql"
|
GRAPHQL_URL = "https://api.medevio.cz/graphql"
|
||||||
|
|
||||||
DB_CONFIG = {
|
DB_CONFIG = {
|
||||||
"host": "127.0.0.1",
|
"host": "192.168.1.76",
|
||||||
"port": 3307,
|
"port": 3307,
|
||||||
"user": "root",
|
"user": "root",
|
||||||
"password": "Vlado9674+",
|
"password": "Vlado9674+",
|
||||||
@@ -31,29 +60,30 @@ DB_CONFIG = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
from datetime import datetime
|
# ==============================
|
||||||
|
# 🕒 DATETIME FIXER
|
||||||
|
# ==============================
|
||||||
def fix_datetime(dt_str):
|
def fix_datetime(dt_str):
|
||||||
"""Convert ISO 8601 string with 'Z' or ms into MySQL DATETIME format."""
|
"""Convert ISO 8601 string with 'Z' or ms into MySQL DATETIME format."""
|
||||||
if not dt_str:
|
if not dt_str:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
# Remove trailing Z and parse flexible ISO format
|
|
||||||
return datetime.fromisoformat(dt_str.replace("Z", "").replace("+00:00", ""))
|
return datetime.fromisoformat(dt_str.replace("Z", "").replace("+00:00", ""))
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# ✅ Optional: limit which requests to process
|
|
||||||
CREATED_AFTER = "2025-01-01" # set "" to disable
|
# Optional filter
|
||||||
|
CREATED_AFTER = "2025-01-01"
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
# ==============================
|
||||||
# 🧮 HELPERS
|
# 🧮 HELPERS
|
||||||
# ==============================
|
# ==============================
|
||||||
def read_token(p: Path) -> str:
|
def read_token(p: Path) -> str:
|
||||||
"""Read Bearer token from file."""
|
|
||||||
tok = p.read_text(encoding="utf-8").strip()
|
tok = p.read_text(encoding="utf-8").strip()
|
||||||
if tok.startswith("Bearer "):
|
if tok.startswith("Bearer "):
|
||||||
tok = tok.split(" ", 1)[1]
|
return tok.split(" ", 1)[1]
|
||||||
return tok
|
return tok
|
||||||
|
|
||||||
|
|
||||||
@@ -101,7 +131,7 @@ def fetch_questionnaire(headers, request_id, clinic_slug):
|
|||||||
}
|
}
|
||||||
r = requests.post(GRAPHQL_URL, json=payload, headers=headers, timeout=40)
|
r = requests.post(GRAPHQL_URL, json=payload, headers=headers, timeout=40)
|
||||||
if r.status_code != 200:
|
if r.status_code != 200:
|
||||||
print(f"❌ HTTP {r.status_code} for {request_id}: {r.text}")
|
safe_print(f"❌ HTTP {r.status_code} for {request_id}: {r.text}")
|
||||||
return None
|
return None
|
||||||
return r.json().get("data", {}).get("request")
|
return r.json().get("data", {}).get("request")
|
||||||
|
|
||||||
@@ -134,7 +164,8 @@ VALUES (%s,%s,%s,%s,%s)
|
|||||||
req.get("userNote"),
|
req.get("userNote"),
|
||||||
json.dumps(ecrf_data, ensure_ascii=False),
|
json.dumps(ecrf_data, ensure_ascii=False),
|
||||||
))
|
))
|
||||||
print(f" 💾 Stored questionnaire for {patient.get('surname','')} {patient.get('name','')}")
|
|
||||||
|
safe_print(f" 💾 Stored questionnaire for {patient.get('surname','')} {patient.get('name','')}")
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
# ==============================
|
||||||
@@ -149,6 +180,8 @@ def main():
|
|||||||
}
|
}
|
||||||
|
|
||||||
conn = pymysql.connect(**DB_CONFIG)
|
conn = pymysql.connect(**DB_CONFIG)
|
||||||
|
|
||||||
|
# load list of requests
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
sql = """
|
sql = """
|
||||||
SELECT id, pacient_jmeno, pacient_prijmeni, createdAt, updatedAt, questionnaireprocessed
|
SELECT id, pacient_jmeno, pacient_prijmeni, createdAt, updatedAt, questionnaireprocessed
|
||||||
@@ -163,26 +196,30 @@ def main():
|
|||||||
|
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
|
|
||||||
print(f"📋 Found {len(rows)} requests needing questionnaire check.")
|
safe_print(f"📋 Found {len(rows)} requests needing questionnaire check.")
|
||||||
|
|
||||||
|
# process each one
|
||||||
for i, row in enumerate(rows, 1):
|
for i, row in enumerate(rows, 1):
|
||||||
req_id = row["id"]
|
req_id = row["id"]
|
||||||
print(f"\n[{i}/{len(rows)}] 🔍 Fetching questionnaire for {req_id} ...")
|
safe_print(f"\n[{i}/{len(rows)}] 🔍 Fetching questionnaire for {req_id} ...")
|
||||||
|
|
||||||
req = fetch_questionnaire(headers, req_id, CLINIC_SLUG)
|
req = fetch_questionnaire(headers, req_id, CLINIC_SLUG)
|
||||||
if not req:
|
if not req:
|
||||||
print(" ⚠️ No questionnaire data found.")
|
safe_print(" ⚠️ No questionnaire data found.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
insert_questionnaire(cur, req)
|
insert_questionnaire(cur, req)
|
||||||
cur.execute("UPDATE pozadavky SET questionnaireprocessed = NOW() WHERE id = %s", (req_id,))
|
cur.execute(
|
||||||
|
"UPDATE pozadavky SET questionnaireprocessed = NOW() WHERE id = %s",
|
||||||
|
(req_id,)
|
||||||
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
time.sleep(0.6) # polite pacing
|
time.sleep(0.6)
|
||||||
|
|
||||||
conn.close()
|
conn.close()
|
||||||
print("\n✅ Done! All questionnaires stored in MySQL table `medevio_questionnaires`.")
|
safe_print("\n✅ Done! All questionnaires stored in MySQL table `medevio_questionnaires`.")
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
# ==============================
|
||||||
|
|||||||
@@ -15,6 +15,34 @@ import pymysql
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import time
|
import time
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Force UTF-8 output even under Windows Task Scheduler
|
||||||
|
import sys
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
sys.stderr.reconfigure(encoding='utf-8')
|
||||||
|
except AttributeError:
|
||||||
|
# Python < 3.7 fallback (not needed for you, but safe)
|
||||||
|
import io
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||||
|
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# 🛡 SAFE PRINT FOR CP1250 / EMOJI
|
||||||
|
# ==============================
|
||||||
|
def safe_print(text: str):
|
||||||
|
enc = sys.stdout.encoding or ""
|
||||||
|
if not enc or not enc.lower().startswith("utf"):
|
||||||
|
# strip emoji + characters outside BMP for Task Scheduler (CP1250)
|
||||||
|
text = ''.join(ch for ch in text if ord(ch) < 65536)
|
||||||
|
try:
|
||||||
|
print(text)
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
# fallback pure ASCII
|
||||||
|
text = ''.join(ch for ch in text if ord(ch) < 128)
|
||||||
|
print(text)
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
# ==============================
|
||||||
# 🔧 CONFIGURATION
|
# 🔧 CONFIGURATION
|
||||||
@@ -22,7 +50,7 @@ import time
|
|||||||
TOKEN_PATH = Path("token.txt")
|
TOKEN_PATH = Path("token.txt")
|
||||||
|
|
||||||
DB_CONFIG = {
|
DB_CONFIG = {
|
||||||
"host": "127.0.0.1",
|
"host": "192.168.1.76",
|
||||||
"port": 3307,
|
"port": 3307,
|
||||||
"user": "root",
|
"user": "root",
|
||||||
"password": "Vlado9674+",
|
"password": "Vlado9674+",
|
||||||
@@ -94,7 +122,7 @@ def fetch_messages(headers, request_id):
|
|||||||
|
|
||||||
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30)
|
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30)
|
||||||
if r.status_code != 200:
|
if r.status_code != 200:
|
||||||
print("❌ HTTP", r.status_code, "for request", request_id)
|
safe_print(f"❌ HTTP {r.status_code} for request {request_id}")
|
||||||
return []
|
return []
|
||||||
return r.json().get("data", {}).get("messages", []) or []
|
return r.json().get("data", {}).get("messages", []) or []
|
||||||
|
|
||||||
@@ -158,7 +186,7 @@ def insert_download(cur, req_id, msg, existing_ids):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if attachment_id in existing_ids:
|
if attachment_id in existing_ids:
|
||||||
return # skip duplicates
|
return
|
||||||
|
|
||||||
url = mr.get("downloadUrl") or mr.get("url")
|
url = mr.get("downloadUrl") or mr.get("url")
|
||||||
if not url:
|
if not url:
|
||||||
@@ -169,7 +197,7 @@ def insert_download(cur, req_id, msg, existing_ids):
|
|||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
data = r.content
|
data = r.content
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("⚠️ Failed to download:", e)
|
safe_print(f"⚠️ Failed to download: {e}")
|
||||||
return
|
return
|
||||||
|
|
||||||
filename = url.split("/")[-1].split("?")[0]
|
filename = url.split("/")[-1].split("?")[0]
|
||||||
@@ -216,7 +244,7 @@ def main():
|
|||||||
cur.execute("SELECT attachment_id FROM medevio_downloads")
|
cur.execute("SELECT attachment_id FROM medevio_downloads")
|
||||||
existing_ids = {row["attachment_id"] for row in cur.fetchall()}
|
existing_ids = {row["attachment_id"] for row in cur.fetchall()}
|
||||||
|
|
||||||
print(f"📦 Already downloaded attachments: {len(existing_ids)}\n")
|
safe_print(f"📦 Already downloaded attachments: {len(existing_ids)}\n")
|
||||||
|
|
||||||
# ---- Select pozadavky needing message sync
|
# ---- Select pozadavky needing message sync
|
||||||
sql = """
|
sql = """
|
||||||
@@ -229,12 +257,12 @@ def main():
|
|||||||
cur.execute(sql)
|
cur.execute(sql)
|
||||||
requests_to_process = cur.fetchall()
|
requests_to_process = cur.fetchall()
|
||||||
|
|
||||||
print(f"📋 Found {len(requests_to_process)} pozadavků requiring message sync.\n")
|
safe_print(f"📋 Found {len(requests_to_process)} pozadavků requiring message sync.\n")
|
||||||
|
|
||||||
# ---- Process each pozadavek
|
# ---- Process each record
|
||||||
for idx, row in enumerate(requests_to_process, 1):
|
for idx, row in enumerate(requests_to_process, 1):
|
||||||
req_id = row["id"]
|
req_id = row["id"]
|
||||||
print(f"[{idx}/{len(requests_to_process)}] Processing {req_id} …")
|
safe_print(f"[{idx}/{len(requests_to_process)}] Processing {req_id} …")
|
||||||
|
|
||||||
messages = fetch_messages(headers, req_id)
|
messages = fetch_messages(headers, req_id)
|
||||||
|
|
||||||
@@ -248,11 +276,11 @@ def main():
|
|||||||
cur.execute("UPDATE pozadavky SET messagesProcessed = NOW() WHERE id = %s", (req_id,))
|
cur.execute("UPDATE pozadavky SET messagesProcessed = NOW() WHERE id = %s", (req_id,))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
print(f" ✅ {len(messages)} messages saved\n")
|
safe_print(f" ✅ {len(messages)} messages saved\n")
|
||||||
time.sleep(0.25)
|
time.sleep(0.25)
|
||||||
|
|
||||||
conn.close()
|
conn.close()
|
||||||
print("🎉 Done!")
|
safe_print("🎉 Done!")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
293
10ReadPozadavky/PRAVIDELNE_3_StahniKomunikaciDELTA.py
Normal file
293
10ReadPozadavky/PRAVIDELNE_3_StahniKomunikaciDELTA.py
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Delta sync Medevio communication.
|
||||||
|
Stáhne pouze zprávy změněné po messagesProcessed pro každý požadavek.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
import pymysql
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# UTF-8 SAFE OUTPUT
|
||||||
|
# ==============================
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
sys.stderr.reconfigure(encoding='utf-8')
|
||||||
|
except AttributeError:
|
||||||
|
import io
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||||
|
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def safe_print(text: str):
|
||||||
|
enc = sys.stdout.encoding or ""
|
||||||
|
if not enc.lower().startswith("utf"):
|
||||||
|
text = ''.join(ch for ch in text if ord(ch) < 65536)
|
||||||
|
try:
|
||||||
|
print(text)
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
text = ''.join(ch for ch in text if ord(ch) < 128)
|
||||||
|
print(text)
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# CONFIG
|
||||||
|
# ==============================
|
||||||
|
TOKEN_PATH = Path("token.txt")
|
||||||
|
|
||||||
|
DB_CONFIG = {
|
||||||
|
"host": "192.168.1.76",
|
||||||
|
"port": 3307,
|
||||||
|
"user": "root",
|
||||||
|
"password": "Vlado9674+",
|
||||||
|
"database": "medevio",
|
||||||
|
"charset": "utf8mb4",
|
||||||
|
"cursorclass": pymysql.cursors.DictCursor,
|
||||||
|
}
|
||||||
|
|
||||||
|
GRAPHQL_QUERY_MESSAGES = r"""
|
||||||
|
query UseMessages_ListMessages($requestId: String!, $updatedSince: DateTime) {
|
||||||
|
messages: listMessages(
|
||||||
|
patientRequestId: $requestId,
|
||||||
|
updatedSince: $updatedSince
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
readAt
|
||||||
|
text
|
||||||
|
type
|
||||||
|
sender {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
surname
|
||||||
|
clinicId
|
||||||
|
}
|
||||||
|
medicalRecord {
|
||||||
|
id
|
||||||
|
description
|
||||||
|
contentType
|
||||||
|
url
|
||||||
|
downloadUrl
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# HELPERS
|
||||||
|
# ==============================
|
||||||
|
def parse_dt(s):
|
||||||
|
if not s:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return datetime.fromisoformat(s.replace("Z", "+00:00"))
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def read_token(path: Path) -> str:
|
||||||
|
tok = path.read_text(encoding="utf-8").strip()
|
||||||
|
return tok.replace("Bearer ", "")
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# FETCH MESSAGES (DELTA)
|
||||||
|
# ==============================
|
||||||
|
def fetch_messages(headers, request_id, updated_since):
|
||||||
|
payload = {
|
||||||
|
"operationName": "UseMessages_ListMessages",
|
||||||
|
"query": GRAPHQL_QUERY_MESSAGES,
|
||||||
|
"variables": {
|
||||||
|
"requestId": request_id,
|
||||||
|
"updatedSince": updated_since,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
r = requests.post(
|
||||||
|
"https://api.medevio.cz/graphql",
|
||||||
|
json=payload,
|
||||||
|
headers=headers,
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
if r.status_code != 200:
|
||||||
|
safe_print(f"❌ HTTP {r.status_code} for request {request_id}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
j = r.json()
|
||||||
|
if "errors" in j:
|
||||||
|
safe_print(f"❌ GraphQL error for {request_id}: {j['errors']}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
return j.get("data", {}).get("messages", []) or []
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# INSERT MESSAGE
|
||||||
|
# ==============================
|
||||||
|
def insert_message(cur, req_id, msg):
|
||||||
|
sender = msg.get("sender") or {}
|
||||||
|
sender_name = " ".join(
|
||||||
|
x for x in [sender.get("name"), sender.get("surname")] if x
|
||||||
|
) or None
|
||||||
|
|
||||||
|
mr = msg.get("medicalRecord") or {}
|
||||||
|
|
||||||
|
sql = """
|
||||||
|
INSERT INTO medevio_conversation (
|
||||||
|
id, request_id,
|
||||||
|
sender_name, sender_id, sender_clinic_id,
|
||||||
|
text, created_at, read_at, updated_at,
|
||||||
|
attachment_url, attachment_description, attachment_content_type
|
||||||
|
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
sender_name = VALUES(sender_name),
|
||||||
|
sender_id = VALUES(sender_id),
|
||||||
|
sender_clinic_id = VALUES(sender_clinic_id),
|
||||||
|
text = VALUES(text),
|
||||||
|
created_at = VALUES(created_at),
|
||||||
|
read_at = VALUES(read_at),
|
||||||
|
updated_at = VALUES(updated_at),
|
||||||
|
attachment_url = VALUES(attachment_url),
|
||||||
|
attachment_description = VALUES(attachment_description),
|
||||||
|
attachment_content_type = VALUES(attachment_content_type)
|
||||||
|
"""
|
||||||
|
|
||||||
|
cur.execute(sql, (
|
||||||
|
msg.get("id"),
|
||||||
|
req_id,
|
||||||
|
sender_name,
|
||||||
|
sender.get("id"),
|
||||||
|
sender.get("clinicId"),
|
||||||
|
msg.get("text"),
|
||||||
|
parse_dt(msg.get("createdAt")),
|
||||||
|
parse_dt(msg.get("readAt")),
|
||||||
|
parse_dt(msg.get("updatedAt")),
|
||||||
|
mr.get("downloadUrl") or mr.get("url"),
|
||||||
|
mr.get("description"),
|
||||||
|
mr.get("contentType")
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# INSERT ATTACHMENT (DEDUP)
|
||||||
|
# ==============================
|
||||||
|
def insert_download(cur, req_id, msg, existing_ids):
|
||||||
|
mr = msg.get("medicalRecord") or {}
|
||||||
|
attachment_id = mr.get("id")
|
||||||
|
if not attachment_id or attachment_id in existing_ids:
|
||||||
|
return
|
||||||
|
|
||||||
|
url = mr.get("downloadUrl") or mr.get("url")
|
||||||
|
if not url:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = requests.get(url, timeout=30)
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.content
|
||||||
|
except Exception as e:
|
||||||
|
safe_print(f"⚠️ Attachment download failed: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
filename = url.split("/")[-1].split("?")[0]
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO medevio_downloads (
|
||||||
|
request_id, attachment_id, attachment_type,
|
||||||
|
filename, content_type, file_size, created_at, file_content
|
||||||
|
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
file_content = VALUES(file_content),
|
||||||
|
file_size = VALUES(file_size),
|
||||||
|
downloaded_at = NOW()
|
||||||
|
""", (
|
||||||
|
req_id,
|
||||||
|
attachment_id,
|
||||||
|
"MESSAGE_ATTACHMENT",
|
||||||
|
filename,
|
||||||
|
mr.get("contentType"),
|
||||||
|
len(data),
|
||||||
|
parse_dt(msg.get("createdAt")),
|
||||||
|
data
|
||||||
|
))
|
||||||
|
|
||||||
|
existing_ids.add(attachment_id)
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# MAIN
|
||||||
|
# ==============================
|
||||||
|
def main():
|
||||||
|
token = read_token(TOKEN_PATH)
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
conn = pymysql.connect(**DB_CONFIG)
|
||||||
|
|
||||||
|
# existing attachments
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("SELECT attachment_id FROM medevio_downloads")
|
||||||
|
existing_ids = {r["attachment_id"] for r in cur.fetchall()}
|
||||||
|
|
||||||
|
# select requests needing sync
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("""
|
||||||
|
SELECT id, messagesProcessed
|
||||||
|
FROM pozadavky
|
||||||
|
WHERE messagesProcessed IS NULL
|
||||||
|
OR messagesProcessed < updatedAt
|
||||||
|
""")
|
||||||
|
rows = cur.fetchall()
|
||||||
|
|
||||||
|
safe_print(f"📋 Found {len(rows)} requests for message delta-sync\n")
|
||||||
|
|
||||||
|
for i, row in enumerate(rows, 1):
|
||||||
|
req_id = row["id"]
|
||||||
|
updated_since = row["messagesProcessed"]
|
||||||
|
if updated_since:
|
||||||
|
updated_since = updated_since.replace(microsecond=0).isoformat() + "Z"
|
||||||
|
|
||||||
|
safe_print(f"[{i}/{len(rows)}] {req_id}")
|
||||||
|
|
||||||
|
messages = fetch_messages(headers, req_id, updated_since)
|
||||||
|
if not messages:
|
||||||
|
safe_print(" ⏭ No new messages")
|
||||||
|
else:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
for msg in messages:
|
||||||
|
insert_message(cur, req_id, msg)
|
||||||
|
insert_download(cur, req_id, msg, existing_ids)
|
||||||
|
conn.commit()
|
||||||
|
safe_print(f" ✅ {len(messages)} new/updated messages")
|
||||||
|
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"UPDATE pozadavky SET messagesProcessed = NOW() WHERE id = %s",
|
||||||
|
(req_id,)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
time.sleep(0.25)
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
safe_print("\n🎉 Delta message sync DONE")
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -14,6 +14,36 @@ import pymysql
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import time
|
import time
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Force UTF-8 output even under Windows Task Scheduler
|
||||||
|
import sys
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
sys.stderr.reconfigure(encoding='utf-8')
|
||||||
|
except AttributeError:
|
||||||
|
# Python < 3.7 fallback (not needed for you, but safe)
|
||||||
|
import io
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||||
|
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# 🛡 SAFE PRINT FOR CP1250 / EMOJI
|
||||||
|
# ==============================
|
||||||
|
def safe_print(text: str):
|
||||||
|
enc = sys.stdout.encoding or ""
|
||||||
|
if not enc or not enc.lower().startswith("utf"):
|
||||||
|
# strip emoji + characters outside BMP
|
||||||
|
text = ''.join(ch for ch in text if ord(ch) < 65536)
|
||||||
|
|
||||||
|
try:
|
||||||
|
print(text)
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
# ASCII fallback
|
||||||
|
text = ''.join(ch for ch in text if ord(ch) < 128)
|
||||||
|
print(text)
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
# ==============================
|
||||||
# 🔧 CONFIGURATION
|
# 🔧 CONFIGURATION
|
||||||
@@ -22,7 +52,7 @@ TOKEN_PATH = Path("token.txt")
|
|||||||
CLINIC_SLUG = "mudr-buzalkova"
|
CLINIC_SLUG = "mudr-buzalkova"
|
||||||
|
|
||||||
DB_CONFIG = {
|
DB_CONFIG = {
|
||||||
"host": "127.0.0.1",
|
"host": "192.168.1.76",
|
||||||
"port": 3307,
|
"port": 3307,
|
||||||
"user": "root",
|
"user": "root",
|
||||||
"password": "Vlado9674+",
|
"password": "Vlado9674+",
|
||||||
@@ -67,6 +97,7 @@ def read_token(p: Path) -> str:
|
|||||||
tok = p.read_text(encoding="utf-8").strip()
|
tok = p.read_text(encoding="utf-8").strip()
|
||||||
return tok.split(" ", 1)[1] if tok.startswith("Bearer ") else tok
|
return tok.split(" ", 1)[1] if tok.startswith("Bearer ") else tok
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
# ==============================
|
||||||
# 📡 FETCH ATTACHMENTS
|
# 📡 FETCH ATTACHMENTS
|
||||||
# ==============================
|
# ==============================
|
||||||
@@ -78,42 +109,40 @@ def fetch_attachments(headers, request_id):
|
|||||||
}
|
}
|
||||||
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30)
|
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30)
|
||||||
if r.status_code != 200:
|
if r.status_code != 200:
|
||||||
print(f"❌ HTTP {r.status_code} for request {request_id}")
|
safe_print(f"❌ HTTP {r.status_code} for request {request_id}")
|
||||||
return []
|
return []
|
||||||
return r.json().get("data", {}).get("patientRequestMedicalRecords", [])
|
return r.json().get("data", {}).get("patientRequestMedicalRecords", [])
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
# ==============================
|
||||||
# 💾 SAVE TO MYSQL (clean version)
|
# 💾 SAVE TO MYSQL
|
||||||
# ==============================
|
# ==============================
|
||||||
def insert_download(cur, req_id, a, m, created_date, existing_ids):
|
def insert_download(cur, req_id, a, m, created_date, existing_ids):
|
||||||
|
|
||||||
attachment_id = a.get("id")
|
attachment_id = a.get("id")
|
||||||
if attachment_id in existing_ids:
|
if attachment_id in existing_ids:
|
||||||
print(f" ⏭️ Already downloaded {attachment_id}")
|
safe_print(f" ⏭️ Already downloaded {attachment_id}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
url = m.get("downloadUrl")
|
url = m.get("downloadUrl")
|
||||||
if not url:
|
if not url:
|
||||||
print(" ⚠️ Missing download URL")
|
safe_print(" ⚠️ Missing download URL")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
filename = extract_filename_from_url(url)
|
filename = extract_filename_from_url(url)
|
||||||
|
|
||||||
# Download file
|
|
||||||
try:
|
try:
|
||||||
r = requests.get(url, timeout=30)
|
r = requests.get(url, timeout=30)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
content = r.content
|
content = r.content
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" ⚠️ Download failed {url}: {e}")
|
safe_print(f" ⚠️ Download failed {url}: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
file_size = len(content)
|
file_size = len(content)
|
||||||
attachment_type = a.get("attachmentType")
|
attachment_type = a.get("attachmentType")
|
||||||
content_type = m.get("contentType")
|
content_type = m.get("contentType")
|
||||||
|
|
||||||
# 🚨 CLEAN INSERT — no patient_jmeno/no patient_prijmeni
|
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
INSERT INTO medevio_downloads (
|
INSERT INTO medevio_downloads (
|
||||||
request_id, attachment_id, attachment_type,
|
request_id, attachment_id, attachment_type,
|
||||||
@@ -136,7 +165,7 @@ def insert_download(cur, req_id, a, m, created_date, existing_ids):
|
|||||||
))
|
))
|
||||||
|
|
||||||
existing_ids.add(attachment_id)
|
existing_ids.add(attachment_id)
|
||||||
print(f" 💾 Saved {filename} ({file_size/1024:.1f} kB)")
|
safe_print(f" 💾 Saved {filename} ({file_size/1024:.1f} kB)")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -152,11 +181,12 @@ def main():
|
|||||||
|
|
||||||
conn = pymysql.connect(**DB_CONFIG)
|
conn = pymysql.connect(**DB_CONFIG)
|
||||||
|
|
||||||
# Load existing IDs
|
# Load existing attachments
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute("SELECT attachment_id FROM medevio_downloads")
|
cur.execute("SELECT attachment_id FROM medevio_downloads")
|
||||||
existing_ids = {row["attachment_id"] for row in cur.fetchall()}
|
existing_ids = {row["attachment_id"] for row in cur.fetchall()}
|
||||||
print(f"✅ {len(existing_ids)} attachments already saved.")
|
|
||||||
|
safe_print(f"✅ {len(existing_ids)} attachments already saved.")
|
||||||
|
|
||||||
# Build query for pozadavky
|
# Build query for pozadavky
|
||||||
sql = """
|
sql = """
|
||||||
@@ -173,7 +203,7 @@ def main():
|
|||||||
cur.execute(sql, params)
|
cur.execute(sql, params)
|
||||||
req_rows = cur.fetchall()
|
req_rows = cur.fetchall()
|
||||||
|
|
||||||
print(f"📋 Found {len(req_rows)} pozadavky to process.")
|
safe_print(f"📋 Found {len(req_rows)} pozadavky to process.")
|
||||||
|
|
||||||
# Process each pozadavek
|
# Process each pozadavek
|
||||||
for i, row in enumerate(req_rows, 1):
|
for i, row in enumerate(req_rows, 1):
|
||||||
@@ -182,12 +212,12 @@ def main():
|
|||||||
jmeno = row.get("pacient_jmeno") or ""
|
jmeno = row.get("pacient_jmeno") or ""
|
||||||
created_date = row.get("createdAt") or datetime.now()
|
created_date = row.get("createdAt") or datetime.now()
|
||||||
|
|
||||||
print(f"\n[{i}/{len(req_rows)}] 🧾 {prijmeni}, {jmeno} ({req_id})")
|
safe_print(f"\n[{i}/{len(req_rows)}] 🧾 {prijmeni}, {jmeno} ({req_id})")
|
||||||
|
|
||||||
attachments = fetch_attachments(headers, req_id)
|
attachments = fetch_attachments(headers, req_id)
|
||||||
|
|
||||||
if not attachments:
|
if not attachments:
|
||||||
print(" ⚠️ No attachments found")
|
safe_print(" ⚠️ No attachments found")
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute("UPDATE pozadavky SET attachmentsProcessed = NOW() WHERE id = %s", (req_id,))
|
cur.execute("UPDATE pozadavky SET attachmentsProcessed = NOW() WHERE id = %s", (req_id,))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
@@ -199,17 +229,16 @@ def main():
|
|||||||
insert_download(cur, req_id, a, m, created_date, existing_ids)
|
insert_download(cur, req_id, a, m, created_date, existing_ids)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
# Mark processed
|
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute("UPDATE pozadavky SET attachmentsProcessed = NOW() WHERE id = %s", (req_id,))
|
cur.execute("UPDATE pozadavky SET attachmentsProcessed = NOW() WHERE id = %s", (req_id,))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
print(f" ✅ Done ({len(attachments)} attachments)")
|
safe_print(f" ✅ Done ({len(attachments)} attachments)")
|
||||||
|
|
||||||
time.sleep(0.3)
|
time.sleep(0.3)
|
||||||
|
|
||||||
conn.close()
|
conn.close()
|
||||||
print("\n🎯 All attachments processed.")
|
safe_print("\n🎯 All attachments processed.")
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
# ==============================
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -8,12 +8,40 @@ import re
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import time
|
import time
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Force UTF-8 output even under Windows Task Scheduler
|
||||||
|
import sys
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
sys.stderr.reconfigure(encoding='utf-8')
|
||||||
|
except AttributeError:
|
||||||
|
# Python < 3.7 fallback (not needed for you, but safe)
|
||||||
|
import io
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||||
|
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# 🛡 SAFE PRINT FOR CP1250 / EMOJI
|
||||||
|
# ==============================
|
||||||
|
def safe_print(text: str = ""):
|
||||||
|
enc = sys.stdout.encoding or ""
|
||||||
|
if not enc.lower().startswith("utf"):
|
||||||
|
# Strip emoji and characters outside BMP for Task Scheduler
|
||||||
|
text = ''.join(ch for ch in text if ord(ch) < 65536)
|
||||||
|
try:
|
||||||
|
print(text)
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
# ASCII fallback
|
||||||
|
text = ''.join(ch for ch in text if ord(ch) < 128)
|
||||||
|
print(text)
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
# ==============================
|
||||||
# ⚙️ CONFIGURATION
|
# ⚙️ CONFIGURATION
|
||||||
# ==============================
|
# ==============================
|
||||||
DB_CONFIG = {
|
DB_CONFIG = {
|
||||||
"host": "127.0.0.1",
|
"host": "192.168.1.76",
|
||||||
"port": 3307,
|
"port": 3307,
|
||||||
"user": "root",
|
"user": "root",
|
||||||
"password": "Vlado9674+",
|
"password": "Vlado9674+",
|
||||||
@@ -21,7 +49,7 @@ DB_CONFIG = {
|
|||||||
"charset": "utf8mb4",
|
"charset": "utf8mb4",
|
||||||
}
|
}
|
||||||
|
|
||||||
BASE_DIR = Path(r"z:\Dropbox\Ordinace\Dokumentace_ke_zpracování\MP")
|
BASE_DIR = Path(r"u:\Dropbox\Ordinace\Dokumentace_ke_zpracování\MP")
|
||||||
BASE_DIR.mkdir(parents=True, exist_ok=True)
|
BASE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
@@ -31,24 +59,15 @@ def sanitize_name(name: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def make_abbrev(title: str) -> str:
|
def make_abbrev(title: str) -> str:
|
||||||
"""
|
|
||||||
Create abbreviation from displayTitle:
|
|
||||||
- First letter of each word
|
|
||||||
- Keep digits together
|
|
||||||
- Uppercase
|
|
||||||
"""
|
|
||||||
if not title:
|
if not title:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
words = re.findall(r"[A-Za-zÁ-Žá-ž0-9]+", title)
|
words = re.findall(r"[A-Za-zÁ-Žá-ž0-9]+", title)
|
||||||
|
|
||||||
abbr = ""
|
abbr = ""
|
||||||
for w in words:
|
for w in words:
|
||||||
if w.isdigit():
|
if w.isdigit():
|
||||||
abbr += w
|
abbr += w
|
||||||
else:
|
else:
|
||||||
abbr += w[0]
|
abbr += w[0]
|
||||||
|
|
||||||
return abbr.upper()
|
return abbr.upper()
|
||||||
|
|
||||||
|
|
||||||
@@ -56,28 +75,20 @@ def make_abbrev(title: str) -> str:
|
|||||||
# 🧹 DELETE UNEXPECTED FILES
|
# 🧹 DELETE UNEXPECTED FILES
|
||||||
# ==============================
|
# ==============================
|
||||||
def clean_folder(folder: Path, valid_files: set):
|
def clean_folder(folder: Path, valid_files: set):
|
||||||
"""
|
|
||||||
Remove unexpected files.
|
|
||||||
RULE:
|
|
||||||
- Files starting with `▲` are ALWAYS kept.
|
|
||||||
"""
|
|
||||||
if not folder.exists():
|
if not folder.exists():
|
||||||
return
|
return
|
||||||
|
|
||||||
for f in folder.iterdir():
|
for f in folder.iterdir():
|
||||||
if f.is_file():
|
if f.is_file():
|
||||||
|
|
||||||
# zpracované soubory (▲filename.pdf) nikdy nemažeme
|
|
||||||
if f.name.startswith("▲"):
|
if f.name.startswith("▲"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
sanitized = sanitize_name(f.name)
|
sanitized = sanitize_name(f.name)
|
||||||
if sanitized not in valid_files:
|
if sanitized not in valid_files:
|
||||||
print(f"🗑️ Removing unexpected file: {f.name}")
|
safe_print(f"🗑️ Removing unexpected file: {f.name}")
|
||||||
try:
|
try:
|
||||||
f.unlink()
|
f.unlink()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ Could not delete {f}: {e}")
|
safe_print(f"⚠️ Could not delete {f}: {e}")
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
# ==============================
|
||||||
@@ -88,7 +99,7 @@ conn = pymysql.connect(**DB_CONFIG)
|
|||||||
cur_meta = conn.cursor(pymysql.cursors.DictCursor)
|
cur_meta = conn.cursor(pymysql.cursors.DictCursor)
|
||||||
cur_blob = conn.cursor()
|
cur_blob = conn.cursor()
|
||||||
|
|
||||||
print("🔍 Loading metadata from DB (FAST)…")
|
safe_print("🔍 Loading metadata from DB (FAST)…")
|
||||||
|
|
||||||
cur_meta.execute("""
|
cur_meta.execute("""
|
||||||
SELECT d.id AS download_id,
|
SELECT d.id AS download_id,
|
||||||
@@ -105,13 +116,25 @@ cur_meta.execute("""
|
|||||||
""")
|
""")
|
||||||
|
|
||||||
rows = cur_meta.fetchall()
|
rows = cur_meta.fetchall()
|
||||||
print(f"📋 Found {len(rows)} attachment records.\n")
|
safe_print(f"📋 Found {len(rows)} attachment records.\n")
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
# ==============================
|
||||||
# 🧠 MAIN LOOP
|
# 🧠 MAIN LOOP WITH PROGRESS
|
||||||
# ==============================
|
# ==============================
|
||||||
|
|
||||||
|
unique_request_ids = []
|
||||||
|
seen = set()
|
||||||
|
for r in rows:
|
||||||
|
req_id = r["request_id"]
|
||||||
|
if req_id not in seen:
|
||||||
|
unique_request_ids.append(req_id)
|
||||||
|
seen.add(req_id)
|
||||||
|
|
||||||
|
total_requests = len(unique_request_ids)
|
||||||
|
safe_print(f"🔄 Processing {total_requests} unique requests...\n")
|
||||||
|
|
||||||
processed_requests = set()
|
processed_requests = set()
|
||||||
|
current_index = 0
|
||||||
|
|
||||||
for r in rows:
|
for r in rows:
|
||||||
req_id = r["request_id"]
|
req_id = r["request_id"]
|
||||||
@@ -120,14 +143,19 @@ for r in rows:
|
|||||||
continue
|
continue
|
||||||
processed_requests.add(req_id)
|
processed_requests.add(req_id)
|
||||||
|
|
||||||
# ========== FETCH ALL VALID FILES FOR THIS REQUEST ==========
|
current_index += 1
|
||||||
|
percent = (current_index / total_requests) * 100
|
||||||
|
|
||||||
|
safe_print(f"\n[ {percent:5.1f}% ] Processing request {current_index} / {total_requests} → {req_id}")
|
||||||
|
|
||||||
|
# ========== FETCH VALID FILENAMES ==========
|
||||||
cur_meta.execute(
|
cur_meta.execute(
|
||||||
"SELECT filename FROM medevio_downloads WHERE request_id=%s",
|
"SELECT filename FROM medevio_downloads WHERE request_id=%s",
|
||||||
(req_id,)
|
(req_id,)
|
||||||
)
|
)
|
||||||
valid_files = {sanitize_name(row["filename"]) for row in cur_meta.fetchall()}
|
valid_files = {sanitize_name(row["filename"]) for row in cur_meta.fetchall()}
|
||||||
|
|
||||||
# ========== FOLDER NAME BASED ON UPDATEDAT ==========
|
# ========== BUILD FOLDER NAME ==========
|
||||||
updated_at = r["req_updated_at"] or datetime.now()
|
updated_at = r["req_updated_at"] or datetime.now()
|
||||||
date_str = updated_at.strftime("%Y-%m-%d")
|
date_str = updated_at.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
@@ -140,17 +168,14 @@ for r in rows:
|
|||||||
f"{date_str} {prijmeni}, {jmeno} [{abbr}] {req_id}"
|
f"{date_str} {prijmeni}, {jmeno} [{abbr}] {req_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# ========== DETECT EXISTING FOLDER (WITH OR WITHOUT ▲) ==========
|
# ========== DETECT EXISTING FOLDER ==========
|
||||||
existing_folder = None
|
existing_folder = None
|
||||||
folder_has_flag = False
|
|
||||||
|
|
||||||
for f in BASE_DIR.iterdir():
|
for f in BASE_DIR.iterdir():
|
||||||
if f.is_dir() and req_id in f.name:
|
if f.is_dir() and req_id in f.name:
|
||||||
existing_folder = f
|
existing_folder = f
|
||||||
folder_has_flag = ("▲" in f.name)
|
|
||||||
break
|
break
|
||||||
|
|
||||||
# pokud složka existuje → pracujeme v ní
|
|
||||||
main_folder = existing_folder if existing_folder else BASE_DIR / clean_folder_name
|
main_folder = existing_folder if existing_folder else BASE_DIR / clean_folder_name
|
||||||
|
|
||||||
# ========== MERGE DUPLICATES ==========
|
# ========== MERGE DUPLICATES ==========
|
||||||
@@ -160,14 +185,13 @@ for r in rows:
|
|||||||
]
|
]
|
||||||
|
|
||||||
for dup in possible_dups:
|
for dup in possible_dups:
|
||||||
print(f"♻️ Merging duplicate folder: {dup.name}")
|
safe_print(f"♻️ Merging duplicate folder: {dup.name}")
|
||||||
|
|
||||||
clean_folder(dup, valid_files)
|
clean_folder(dup, valid_files)
|
||||||
main_folder.mkdir(parents=True, exist_ok=True)
|
main_folder.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
for f in dup.iterdir():
|
for f in dup.iterdir():
|
||||||
if f.is_file():
|
if f.is_file():
|
||||||
# prostě přesuneme, ▲ případně zůstane v názvu
|
|
||||||
target = main_folder / f.name
|
target = main_folder / f.name
|
||||||
if not target.exists():
|
if not target.exists():
|
||||||
f.rename(target)
|
f.rename(target)
|
||||||
@@ -185,11 +209,9 @@ for r in rows:
|
|||||||
dest_plain = main_folder / filename
|
dest_plain = main_folder / filename
|
||||||
dest_marked = main_folder / ("▲" + filename)
|
dest_marked = main_folder / ("▲" + filename)
|
||||||
|
|
||||||
# soubor už existuje (buď filename, nebo ▲filename)
|
|
||||||
if dest_plain.exists() or dest_marked.exists():
|
if dest_plain.exists() or dest_marked.exists():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# stáhneme nový soubor → znamená že se má odstranit ▲ složky
|
|
||||||
added_new_file = True
|
added_new_file = True
|
||||||
|
|
||||||
cur_blob.execute(
|
cur_blob.execute(
|
||||||
@@ -208,31 +230,22 @@ for r in rows:
|
|||||||
with open(dest_plain, "wb") as f:
|
with open(dest_plain, "wb") as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
print(f"💾 Wrote: {dest_plain.relative_to(BASE_DIR)}")
|
safe_print(f"💾 Wrote: {dest_plain.relative_to(BASE_DIR)}")
|
||||||
|
|
||||||
# ==============================
|
# ========== REMOVE ▲ FLAG IF NEW FILES ADDED ==========
|
||||||
# 🔵 REMOVE FOLDER-LEVEL ▲ ONLY IF NEW FILE ADDED
|
if added_new_file and "▲" in main_folder.name:
|
||||||
# ==============================
|
new_name = main_folder.name.replace("▲", "").strip()
|
||||||
if added_new_file:
|
|
||||||
# složka se má přejmenovat bez ▲
|
|
||||||
if "▲" in main_folder.name:
|
|
||||||
new_name = main_folder.name.replace("▲", "")
|
|
||||||
new_name = new_name.strip() # pro jistotu
|
|
||||||
new_path = main_folder.parent / new_name
|
new_path = main_folder.parent / new_name
|
||||||
|
|
||||||
if new_path != main_folder:
|
if new_path != main_folder:
|
||||||
try:
|
try:
|
||||||
main_folder.rename(new_path)
|
main_folder.rename(new_path)
|
||||||
print(f"🔄 Folder flag ▲ removed → {new_name}")
|
safe_print(f"🔄 Folder flag ▲ removed → {new_name}")
|
||||||
main_folder = new_path
|
main_folder = new_path
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ Could not rename folder: {e}")
|
safe_print(f"⚠️ Could not rename folder: {e}")
|
||||||
else:
|
|
||||||
# žádné nové soubory → NIKDY nesahat na název složky
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
safe_print("\n🎯 Export complete.\n")
|
||||||
print("\n🎯 Export complete.\n")
|
|
||||||
|
|
||||||
cur_blob.close()
|
cur_blob.close()
|
||||||
cur_meta.close()
|
cur_meta.close()
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Load FunctionsLoader
|
||||||
|
FUNCTIONS_LOADER_PATH = Path(r"C:\Reporting\Functions\FunctionsLoader.py")
|
||||||
|
spec = importlib.util.spec_from_file_location("FunctionsLoader", FUNCTIONS_LOADER_PATH)
|
||||||
|
FunctionsLoader = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules["FunctionsLoader"] = FunctionsLoader
|
||||||
|
spec.loader.exec_module(FunctionsLoader)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Spustí všechny PRAVIDELNÉ skripty v daném pořadí:
|
Spustí všechny PRAVIDELNÉ skripty v daném pořadí:
|
||||||
|
|
||||||
@@ -12,6 +23,14 @@ Spustí všechny PRAVIDELNÉ skripty v daném pořadí:
|
|||||||
5) PRAVIDELNE_5_SaveToFileSystem incremental.py
|
5) PRAVIDELNE_5_SaveToFileSystem incremental.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import time, socket
|
||||||
|
for _ in range(30):
|
||||||
|
try:
|
||||||
|
socket.create_connection(("192.168.1.76", 3307), timeout=3).close()
|
||||||
|
break
|
||||||
|
except OSError:
|
||||||
|
time.sleep(10)
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|||||||
136
10ReadPozadavky/test.py
Normal file
136
10ReadPozadavky/test.py
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Orchestrator for all PRAVIDELNE scripts in exact order.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time, socket
|
||||||
|
for _ in range(30):
|
||||||
|
try:
|
||||||
|
socket.create_connection(("192.168.1.76", 3307), timeout=3).close()
|
||||||
|
break
|
||||||
|
except OSError:
|
||||||
|
time.sleep(10)
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# Import EXACT Functions.py from: C:\Reporting\Fio\Functions.py
|
||||||
|
# This bypasses all other Functions.py files in the system.
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
FUNCTIONS_FILE = Path(r"C:\Reporting\Fio\Functions.py")
|
||||||
|
|
||||||
|
spec = importlib.util.spec_from_file_location("Functions_FIO", FUNCTIONS_FILE)
|
||||||
|
Functions_FIO = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules["Functions_FIO"] = Functions_FIO
|
||||||
|
spec.loader.exec_module(Functions_FIO)
|
||||||
|
|
||||||
|
# correct WhatsApp function
|
||||||
|
SendWhatsAppMessage = Functions_FIO.SendWhatsAppMessage
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# General Orchestrator Settings
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
# folder where orchestrator + sub-scripts live
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
SCRIPTS_IN_ORDER = [
|
||||||
|
"PRAVIDELNE_0_READ_ALL_ACTIVE_POZADAVKY.py",
|
||||||
|
"PRAVIDELNE_1_ReadLast300DonePozadavku.py",
|
||||||
|
"PRAVIDELNE_2_ReadPoznamky.py",
|
||||||
|
"PRAVIDELNE_3_StahniKomunikaci.py",
|
||||||
|
"PRAVIDELNE_4_StahniPrilohyUlozDoMySQL.py",
|
||||||
|
"PRAVIDELNE_5_SaveToFileSystem incremental.py",
|
||||||
|
]
|
||||||
|
|
||||||
|
LOG_FILE = BASE_DIR / "PRAVIDELNE_log.txt"
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# Logging + WhatsApp wrappers
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
def log(msg: str):
|
||||||
|
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
line = f"[{ts}] {msg}"
|
||||||
|
print(line)
|
||||||
|
try:
|
||||||
|
with LOG_FILE.open("a", encoding="utf-8") as f:
|
||||||
|
f.write(line + "\n")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def whatsapp_notify(text: str):
|
||||||
|
"""WhatsApp message wrapper — never allowed to crash orchestrator"""
|
||||||
|
try:
|
||||||
|
SendWhatsAppMessage(text)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# Main orchestrator
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
def main():
|
||||||
|
log("=== START pravidelného běhu ===")
|
||||||
|
whatsapp_notify("🏁 *PRAVIDELNÉ skripty: START*")
|
||||||
|
|
||||||
|
for script_name in SCRIPTS_IN_ORDER:
|
||||||
|
script_path = BASE_DIR / script_name
|
||||||
|
|
||||||
|
if not script_path.exists():
|
||||||
|
err = f"❌ Skript nenalezen: {script_path}"
|
||||||
|
log(err)
|
||||||
|
whatsapp_notify(err)
|
||||||
|
continue
|
||||||
|
|
||||||
|
log(f"▶ Spouštím: {script_path.name}")
|
||||||
|
whatsapp_notify(f"▶ *Spouštím:* {script_path.name}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, str(script_path)],
|
||||||
|
cwd=str(BASE_DIR),
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
encoding="utf-8",
|
||||||
|
errors="ignore",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
err = f"💥 Chyba při spouštění {script_path.name}: {e}"
|
||||||
|
log(err)
|
||||||
|
whatsapp_notify(err)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# return code
|
||||||
|
rc_msg = f"↳ {script_path.name} return code: {result.returncode}"
|
||||||
|
log(rc_msg)
|
||||||
|
whatsapp_notify(rc_msg)
|
||||||
|
|
||||||
|
# stderr (warnings/errors)
|
||||||
|
if result.stderr:
|
||||||
|
err_msg = f"⚠ stderr v {script_path.name}:\n{result.stderr.strip()}"
|
||||||
|
log(err_msg)
|
||||||
|
whatsapp_notify(err_msg)
|
||||||
|
|
||||||
|
log("=== KONEC pravidelného běhu ===")
|
||||||
|
whatsapp_notify("✅ *PRAVIDELNÉ skripty: KONEC*\n")
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# Entry point
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -6,13 +6,40 @@ import requests
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from dateutil import parser
|
from dateutil import parser
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
|
||||||
# ================================
|
# ================================
|
||||||
# 🔧 CONFIGURATION
|
# UTF-8 SAFE OUTPUT (Windows friendly)
|
||||||
|
# ================================
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
sys.stderr.reconfigure(encoding='utf-8')
|
||||||
|
except AttributeError:
|
||||||
|
import io
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||||
|
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def safe_print(text: str):
|
||||||
|
enc = sys.stdout.encoding or ""
|
||||||
|
if not enc.lower().startswith("utf"):
|
||||||
|
text = ''.join(ch for ch in text if ord(ch) < 65536)
|
||||||
|
try:
|
||||||
|
print(text)
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
text = ''.join(ch for ch in text if ord(ch) < 128)
|
||||||
|
print(text)
|
||||||
|
|
||||||
|
|
||||||
|
# ================================
|
||||||
|
# 🔧 CONFIG
|
||||||
# ================================
|
# ================================
|
||||||
TOKEN_PATH = Path("token.txt")
|
TOKEN_PATH = Path("token.txt")
|
||||||
CLINIC_SLUG = "mudr-buzalkova"
|
CLINIC_SLUG = "mudr-buzalkova"
|
||||||
LIMIT = 300 # stáhneme posledních 300 ukončených požadavků
|
|
||||||
|
BATCH_SIZE = 500
|
||||||
|
STATES = ["ACTIVE", "DONE"] # explicitně – jinak API vrací jen ACTIVE
|
||||||
|
|
||||||
DB_CONFIG = {
|
DB_CONFIG = {
|
||||||
"host": "192.168.1.76",
|
"host": "192.168.1.76",
|
||||||
@@ -24,7 +51,6 @@ DB_CONFIG = {
|
|||||||
"cursorclass": pymysql.cursors.DictCursor,
|
"cursorclass": pymysql.cursors.DictCursor,
|
||||||
}
|
}
|
||||||
|
|
||||||
# ⭐ Ověřený dotaz s lastMessage
|
|
||||||
GRAPHQL_QUERY = r"""
|
GRAPHQL_QUERY = r"""
|
||||||
query ClinicRequestList2(
|
query ClinicRequestList2(
|
||||||
$clinicSlug: String!,
|
$clinicSlug: String!,
|
||||||
@@ -62,6 +88,7 @@ query ClinicRequestList2(
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
# ================================
|
# ================================
|
||||||
# TOKEN
|
# TOKEN
|
||||||
# ================================
|
# ================================
|
||||||
@@ -71,39 +98,32 @@ def read_token(path: Path) -> str:
|
|||||||
return tok.split(" ", 1)[1]
|
return tok.split(" ", 1)[1]
|
||||||
return tok
|
return tok
|
||||||
|
|
||||||
|
|
||||||
# ================================
|
# ================================
|
||||||
# DATETIME PARSER (UTC → MySQL)
|
# DATETIME PARSER
|
||||||
# ================================
|
# ================================
|
||||||
def to_mysql_dt(iso_str):
|
def to_mysql_dt(iso_str):
|
||||||
if not iso_str:
|
if not iso_str:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
dt = parser.isoparse(iso_str) # ISO8601 → aware datetime (UTC)
|
dt = parser.isoparse(iso_str)
|
||||||
dt = dt.astimezone() # převede na lokální čas (CET/CEST)
|
if dt.tzinfo is None:
|
||||||
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
dt = dt.replace(tzinfo=datetime.now().astimezone().tzinfo)
|
||||||
except:
|
return dt.astimezone().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
# ================================
|
# ================================
|
||||||
# UPSERT WITH MERGED UPDATED TIME
|
# UPSERT
|
||||||
# ================================
|
# ================================
|
||||||
def upsert(conn, r):
|
def upsert(conn, r):
|
||||||
p = r.get("extendedPatient") or {}
|
p = r.get("extendedPatient") or {}
|
||||||
|
|
||||||
# API pole
|
|
||||||
api_updated = to_mysql_dt(r.get("updatedAt"))
|
api_updated = to_mysql_dt(r.get("updatedAt"))
|
||||||
|
msg_updated = to_mysql_dt((r.get("lastMessage") or {}).get("createdAt"))
|
||||||
|
|
||||||
# poslední zpráva
|
final_updated = max(filter(None, [api_updated, msg_updated]), default=None)
|
||||||
last_msg = r.get("lastMessage") or {}
|
|
||||||
msg_at = to_mysql_dt(last_msg.get("createdAt"))
|
|
||||||
|
|
||||||
# vybereme novější čas
|
|
||||||
def max_dt(a, b):
|
|
||||||
if a and b:
|
|
||||||
return max(a, b)
|
|
||||||
return a or b
|
|
||||||
|
|
||||||
final_updated = max_dt(api_updated, msg_at)
|
|
||||||
|
|
||||||
sql = """
|
sql = """
|
||||||
INSERT INTO pozadavky (
|
INSERT INTO pozadavky (
|
||||||
@@ -134,33 +154,34 @@ def upsert(conn, r):
|
|||||||
|
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute(sql, vals)
|
cur.execute(sql, vals)
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
# ================================
|
# ================================
|
||||||
# FETCH LAST 300 DONE REQUESTS
|
# FETCH PAGE (per state)
|
||||||
# ================================
|
# ================================
|
||||||
def fetch_done(headers):
|
def fetch_state(headers, state, offset):
|
||||||
vars = {
|
variables = {
|
||||||
"clinicSlug": CLINIC_SLUG,
|
"clinicSlug": CLINIC_SLUG,
|
||||||
"queueId": None,
|
"queueId": None,
|
||||||
"queueAssignment": "ANY",
|
"queueAssignment": "ANY",
|
||||||
"pageInfo": {"first": LIMIT, "offset": 0},
|
"state": state,
|
||||||
|
"pageInfo": {"first": BATCH_SIZE, "offset": offset},
|
||||||
"locale": "cs",
|
"locale": "cs",
|
||||||
"state": "DONE",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"operationName": "ClinicRequestList2",
|
"operationName": "ClinicRequestList2",
|
||||||
"query": GRAPHQL_QUERY,
|
"query": GRAPHQL_QUERY,
|
||||||
"variables": vars,
|
"variables": variables,
|
||||||
}
|
}
|
||||||
|
|
||||||
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers)
|
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
data = r.json()["data"]["requestsResponse"]
|
data = r.json()["data"]["requestsResponse"]
|
||||||
return data.get("patientRequests", [])
|
return data.get("patientRequests", []), data.get("count", 0)
|
||||||
|
|
||||||
|
|
||||||
# ================================
|
# ================================
|
||||||
# MAIN
|
# MAIN
|
||||||
@@ -175,17 +196,44 @@ def main():
|
|||||||
|
|
||||||
conn = pymysql.connect(**DB_CONFIG)
|
conn = pymysql.connect(**DB_CONFIG)
|
||||||
|
|
||||||
print(f"\n=== Downloading last {LIMIT} DONE requests @ {datetime.now():%Y-%m-%d %H:%M:%S} ===")
|
safe_print(f"\n=== FULL Medevio READ-ALL sync @ {datetime.now():%Y-%m-%d %H:%M:%S} ===")
|
||||||
|
|
||||||
requests_list = fetch_done(headers)
|
grand_total = 0
|
||||||
print(f"📌 Requests returned: {len(requests_list)}")
|
|
||||||
|
|
||||||
for r in requests_list:
|
for state in STATES:
|
||||||
|
safe_print(f"\n🔁 STATE = {state}")
|
||||||
|
offset = 0
|
||||||
|
total = None
|
||||||
|
processed = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
batch, count = fetch_state(headers, state, offset)
|
||||||
|
|
||||||
|
if total is None:
|
||||||
|
total = count
|
||||||
|
safe_print(f"📡 {state}: celkem {total}")
|
||||||
|
|
||||||
|
if not batch:
|
||||||
|
break
|
||||||
|
|
||||||
|
for r in batch:
|
||||||
upsert(conn, r)
|
upsert(conn, r)
|
||||||
|
|
||||||
|
processed += len(batch)
|
||||||
|
safe_print(f" • {processed}/{total}")
|
||||||
|
|
||||||
|
offset += BATCH_SIZE
|
||||||
|
if offset >= count:
|
||||||
|
break
|
||||||
|
|
||||||
|
time.sleep(0.4)
|
||||||
|
|
||||||
|
grand_total += processed
|
||||||
|
|
||||||
conn.close()
|
conn.close()
|
||||||
print("\n✅ DONE - latest closed requests synced.\n")
|
safe_print(f"\n✅ HOTOVO – celkem zpracováno {grand_total} požadavků\n")
|
||||||
|
|
||||||
|
|
||||||
|
# ================================
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
279
12 Readallinbatches/PRAVIDELNE_3_StahniKomunikacifull.py
Normal file
279
12 Readallinbatches/PRAVIDELNE_3_StahniKomunikacifull.py
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Fetches messages from Medevio API.
|
||||||
|
|
||||||
|
Modes:
|
||||||
|
- Incremental (default): Only requests where messagesProcessed IS NULL or < updatedAt
|
||||||
|
- Full resync (--full): Fetches ALL messages for ALL pozadavky
|
||||||
|
"""
|
||||||
|
|
||||||
|
import zlib
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
import pymysql
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
import time
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# 🔧 CONFIGURATION
|
||||||
|
# ==============================
|
||||||
|
TOKEN_PATH = Path("../10ReadPozadavky/token.txt")
|
||||||
|
|
||||||
|
DB_CONFIG = {
|
||||||
|
"host": "192.168.1.76",
|
||||||
|
"port": 3307,
|
||||||
|
"user": "root",
|
||||||
|
"password": "Vlado9674+",
|
||||||
|
"database": "medevio",
|
||||||
|
"charset": "utf8mb4",
|
||||||
|
"cursorclass": pymysql.cursors.DictCursor,
|
||||||
|
}
|
||||||
|
|
||||||
|
GRAPHQL_QUERY_MESSAGES = r"""
|
||||||
|
query UseMessages_ListMessages($requestId: String!, $updatedSince: DateTime) {
|
||||||
|
messages: listMessages(patientRequestId: $requestId, updatedSince: $updatedSince) {
|
||||||
|
id
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
readAt
|
||||||
|
text
|
||||||
|
type
|
||||||
|
sender {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
surname
|
||||||
|
clinicId
|
||||||
|
}
|
||||||
|
medicalRecord {
|
||||||
|
id
|
||||||
|
description
|
||||||
|
contentType
|
||||||
|
url
|
||||||
|
downloadUrl
|
||||||
|
token
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# ⏱ DATETIME PARSER
|
||||||
|
# ==============================
|
||||||
|
def parse_dt(s):
|
||||||
|
if not s:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return datetime.fromisoformat(s.replace("Z", "+00:00"))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
return datetime.strptime(s[:19], "%Y-%m-%dT%H:%M:%S")
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# 🔐 TOKEN
|
||||||
|
# ==============================
|
||||||
|
def read_token(path: Path) -> str:
|
||||||
|
tok = path.read_text(encoding="utf-8").strip()
|
||||||
|
return tok.replace("Bearer ", "")
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# 📡 FETCH MESSAGES
|
||||||
|
# ==============================
|
||||||
|
def fetch_messages(headers, request_id):
|
||||||
|
payload = {
|
||||||
|
"operationName": "UseMessages_ListMessages",
|
||||||
|
"query": GRAPHQL_QUERY_MESSAGES,
|
||||||
|
"variables": {"requestId": request_id, "updatedSince": None},
|
||||||
|
}
|
||||||
|
|
||||||
|
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30)
|
||||||
|
if r.status_code != 200:
|
||||||
|
print("❌ HTTP", r.status_code, "for request", request_id)
|
||||||
|
return []
|
||||||
|
return r.json().get("data", {}).get("messages", []) or []
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# 💾 SAVE MESSAGE
|
||||||
|
# ==============================
|
||||||
|
def insert_message(cur, req_id, msg):
|
||||||
|
|
||||||
|
sender = msg.get("sender") or {}
|
||||||
|
sender_name = " ".join(
|
||||||
|
x for x in [sender.get("name"), sender.get("surname")] if x
|
||||||
|
) or None
|
||||||
|
|
||||||
|
sql = """
|
||||||
|
INSERT INTO medevio_conversation (
|
||||||
|
id, request_id,
|
||||||
|
sender_name, sender_id, sender_clinic_id,
|
||||||
|
text, created_at, read_at, updated_at,
|
||||||
|
attachment_url, attachment_description, attachment_content_type
|
||||||
|
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
sender_name = VALUES(sender_name),
|
||||||
|
sender_id = VALUES(sender_id),
|
||||||
|
sender_clinic_id = VALUES(sender_clinic_id),
|
||||||
|
text = VALUES(text),
|
||||||
|
created_at = VALUES(created_at),
|
||||||
|
read_at = VALUES(read_at),
|
||||||
|
updated_at = VALUES(updated_at),
|
||||||
|
attachment_url = VALUES(attachment_url),
|
||||||
|
attachment_description = VALUES(attachment_description),
|
||||||
|
attachment_content_type = VALUES(attachment_content_type)
|
||||||
|
"""
|
||||||
|
|
||||||
|
mr = msg.get("medicalRecord") or {}
|
||||||
|
|
||||||
|
cur.execute(sql, (
|
||||||
|
msg.get("id"),
|
||||||
|
req_id,
|
||||||
|
sender_name,
|
||||||
|
sender.get("id"),
|
||||||
|
sender.get("clinicId"),
|
||||||
|
msg.get("text"),
|
||||||
|
parse_dt(msg.get("createdAt")),
|
||||||
|
parse_dt(msg.get("readAt")),
|
||||||
|
parse_dt(msg.get("updatedAt")),
|
||||||
|
mr.get("downloadUrl") or mr.get("url"),
|
||||||
|
mr.get("description"),
|
||||||
|
mr.get("contentType")
|
||||||
|
))
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# 💾 DOWNLOAD MESSAGE ATTACHMENT
|
||||||
|
# ==============================
|
||||||
|
def insert_download(cur, req_id, msg, existing_ids):
|
||||||
|
|
||||||
|
mr = msg.get("medicalRecord") or {}
|
||||||
|
attachment_id = mr.get("id")
|
||||||
|
if not attachment_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
if attachment_id in existing_ids:
|
||||||
|
return # skip duplicates
|
||||||
|
|
||||||
|
url = mr.get("downloadUrl") or mr.get("url")
|
||||||
|
if not url:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = requests.get(url, timeout=30)
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.content
|
||||||
|
except Exception as e:
|
||||||
|
print("⚠️ Failed to download:", e)
|
||||||
|
return
|
||||||
|
|
||||||
|
filename = url.split("/")[-1].split("?")[0]
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO medevio_downloads (
|
||||||
|
request_id, attachment_id, attachment_type,
|
||||||
|
filename, content_type, file_size, created_at, file_content
|
||||||
|
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
file_content = VALUES(file_content),
|
||||||
|
file_size = VALUES(file_size),
|
||||||
|
downloaded_at = NOW()
|
||||||
|
""", (
|
||||||
|
req_id,
|
||||||
|
attachment_id,
|
||||||
|
"MESSAGE_ATTACHMENT",
|
||||||
|
filename,
|
||||||
|
mr.get("contentType"),
|
||||||
|
len(data),
|
||||||
|
parse_dt(msg.get("createdAt")),
|
||||||
|
data
|
||||||
|
))
|
||||||
|
|
||||||
|
existing_ids.add(attachment_id)
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# 🧠 MAIN
|
||||||
|
# ==============================
|
||||||
|
def main():
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--full", action="store_true", help="Load messages for ALL pozadavky")
|
||||||
|
# Force full mode ON
|
||||||
|
args = parser.parse_args(args=["--full"])
|
||||||
|
# args = parser.parse_args()
|
||||||
|
|
||||||
|
token = read_token(TOKEN_PATH)
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
conn = pymysql.connect(**DB_CONFIG)
|
||||||
|
|
||||||
|
# ---- Load existing attachments
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("SELECT attachment_id FROM medevio_downloads")
|
||||||
|
existing_ids = {row["attachment_id"] for row in cur.fetchall()}
|
||||||
|
|
||||||
|
print(f"📦 Already downloaded attachments: {len(existing_ids)}\n")
|
||||||
|
|
||||||
|
# ---- Select pozadavky to process
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
if args.full:
|
||||||
|
print("🔁 FULL REFRESH MODE: Fetching messages for ALL pozadavky!\n")
|
||||||
|
cur.execute("SELECT id FROM pozadavky")
|
||||||
|
else:
|
||||||
|
print("📥 Incremental mode: Only syncing updated pozadavky.\n")
|
||||||
|
cur.execute("""
|
||||||
|
SELECT id FROM pozadavky
|
||||||
|
WHERE messagesProcessed IS NULL
|
||||||
|
OR messagesProcessed < updatedAt
|
||||||
|
""")
|
||||||
|
requests_to_process = cur.fetchall()
|
||||||
|
|
||||||
|
# =================================
|
||||||
|
# ⏩ SKIP FIRST 3100 AS YESTERDAY
|
||||||
|
# =================================
|
||||||
|
|
||||||
|
SKIP = 3100
|
||||||
|
if len(requests_to_process) > SKIP:
|
||||||
|
print(f"⏩ Skipping first {SKIP} pozadavky (already processed yesterday).")
|
||||||
|
requests_to_process = requests_to_process[SKIP:]
|
||||||
|
else:
|
||||||
|
print("⚠️ Not enough pozadavky to skip!")
|
||||||
|
|
||||||
|
|
||||||
|
print(f"📋 Requests to process: {len(requests_to_process)}\n")
|
||||||
|
|
||||||
|
# ---- Process each request
|
||||||
|
for idx, row in enumerate(requests_to_process, 1):
|
||||||
|
req_id = row["id"]
|
||||||
|
print(f"[{idx}/{len(requests_to_process)}] Processing {req_id} …")
|
||||||
|
|
||||||
|
messages = fetch_messages(headers, req_id)
|
||||||
|
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
for msg in messages:
|
||||||
|
insert_message(cur, req_id, msg)
|
||||||
|
insert_download(cur, req_id, msg, existing_ids)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("UPDATE pozadavky SET messagesProcessed = NOW() WHERE id = %s", (req_id,))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
print(f" ✅ {len(messages)} messages saved\n")
|
||||||
|
time.sleep(0.25)
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
print("🎉 Done!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
1
12 Readallinbatches/medevio_storage.json
Normal file
1
12 Readallinbatches/medevio_storage.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"cookies": [{"name": "gateway-access-token", "value": "YwBgkf8McREDKs7vCZj0EZD2fJsuV8RyDPtYx7WiDoz0nFJ9kxId8kcNEPBLFSwM+Tiz80+SOdFwo+oj", "domain": "my.medevio.cz", "path": "/", "expires": 1763372319, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "aws-waf-token", "value": "b6a1d4eb-4350-40e5-8e52-1f5f9600fbb8:CgoAr9pC8c6zAAAA:OYwXLY5OyitSQPl5v2oIlS+hIxsrb5LxV4VjCyE2gJCFFE5PQu+0Zbxse2ZIofrNv5QKs0TYUDTmxPhZyTr9Qtjnq2gsVQxWHXzrbebv3Z7RbzB63u6Ymn3Fo8IbDev3CfCNcNuxCKltFEXLqSCjI2vqNY+7HZkgQBIqy2wMgzli3aSLq0w8lWYtZzyyot7q8RPXWMGTfaBUo2reY0SOSffm9rAivE9PszNfPid71CvNrGAAoxRbwb25eVujlyIcDVWe5vZ9Iw==", "domain": ".my.medevio.cz", "path": "/", "expires": 1761125920, "httpOnly": false, "secure": true, "sameSite": "Lax"}], "origins": [{"origin": "https://my.medevio.cz", "localStorage": [{"name": "awswaf_token_refresh_timestamp", "value": "1760780309860"}, {"name": "awswaf_session_storage", "value": "b6a1d4eb-4350-40e5-8e52-1f5f9600fbb8:CgoAr9pC8c+zAAAA:+vw//1NzmePjPpbGCJzUB+orCRivtJd098DbDX4AnABiGRw/+ql6ShqvFY4YdCY7w2tegb5mEPBdAmc4sNi22kNR9BuEoAgCUiMhkU1AZWfzM51zPfTh7SveCrREZ7xdvxcqKPMmfVLRYX5E4+UWh22z/LKQ7+d9VERp3J+wWCUW3dFFirkezy3N7b2FVjTlY/RxsZwhejQziTG/L3CkIFFP3mOReNgBvDpj7aKoM1knY4IL4TZ8E7zNv3nTsvzACLYvnUutVOUcofN1TfOzwZshSKsEXsMzrQn8PzLccX1jM5VSzce7gfEzl0zSPsT8NB3Sna+rhMIttDNYgvbW1HsfG2LIeKMR27Zf8hkslDRVVkcU/Kp2jLOEdhhrBKGjKY2o9/uX3NExdzh5MEKQSSRtmue01BpWYILPH23rMsz4YSmF+Ough5OeQoC95rkcYwVXMhwvUN9Zfp9UZ4xCNfFUex5dOrg9aJntYRnaceeocGUttNI5AdT0i3+osV6XHXzKxeqO8zLCS9BIsCzxaHfdqqem5DorMceuGKz+QqksatIQAA=="}, {"name": "Application.Intl.locale", "value": "cs"}, {"name": "Password.prefill", "value": "{\"username\":\"vladimir.buzalka@buzalka.cz\",\"type\":\"email\"}"}]}]}
|
||||||
1
12 Readallinbatches/token.txt
Normal file
1
12 Readallinbatches/token.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
nYvrvgflIKcDiQg8Hhpud+qG8iGZ8eH8su4nyT/Mgcm7XQp65ygY9s39+O01wIpk/7sKd6fBHkiKvsqH
|
||||||
@@ -7,6 +7,16 @@ from pathlib import Path
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
import time
|
import time
|
||||||
from dateutil import parser
|
from dateutil import parser
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Force UTF-8 output
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
sys.stderr.reconfigure(encoding='utf-8')
|
||||||
|
except AttributeError:
|
||||||
|
import io
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||||
|
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
|
||||||
|
|
||||||
# ================================
|
# ================================
|
||||||
# 🔧 CONFIGURATION
|
# 🔧 CONFIGURATION
|
||||||
@@ -16,8 +26,8 @@ CLINIC_SLUG = "mudr-buzalkova"
|
|||||||
BATCH_SIZE = 100
|
BATCH_SIZE = 100
|
||||||
|
|
||||||
DB_CONFIG = {
|
DB_CONFIG = {
|
||||||
"host": "192.168.1.76",
|
"host": "192.168.1.50",
|
||||||
"port": 3307,
|
"port": 3306,
|
||||||
"user": "root",
|
"user": "root",
|
||||||
"password": "Vlado9674+",
|
"password": "Vlado9674+",
|
||||||
"database": "medevio",
|
"database": "medevio",
|
||||||
@@ -63,33 +73,21 @@ query ClinicRequestList2(
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
# ================================
|
# ================================
|
||||||
# 🧿 SAFE DATETIME PARSER (ALWAYS UTC → LOCAL)
|
# 🧿 SAFE DATETIME PARSER (ALWAYS UTC → LOCAL)
|
||||||
# ================================
|
# ================================
|
||||||
def to_mysql_dt_utc(iso_str):
|
def to_mysql_dt_utc(iso_str):
|
||||||
"""
|
|
||||||
Parse Medevio timestamps safely.
|
|
||||||
Treat timestamps WITHOUT timezone as UTC.
|
|
||||||
Convert to local time before saving to MySQL.
|
|
||||||
"""
|
|
||||||
if not iso_str:
|
if not iso_str:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
dt = parser.isoparse(iso_str)
|
dt = parser.isoparse(iso_str)
|
||||||
|
|
||||||
# If tz is missing → assume UTC
|
|
||||||
if dt.tzinfo is None:
|
if dt.tzinfo is None:
|
||||||
dt = dt.replace(tzinfo=timezone.utc)
|
dt = dt.replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
# Convert to local timezone
|
|
||||||
dt_local = dt.astimezone()
|
dt_local = dt.astimezone()
|
||||||
|
|
||||||
return dt_local.strftime("%Y-%m-%d %H:%M:%S")
|
return dt_local.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
# ================================
|
# ================================
|
||||||
# 🔑 TOKEN
|
# 🔑 TOKEN
|
||||||
# ================================
|
# ================================
|
||||||
@@ -99,20 +97,15 @@ def read_token(path: Path) -> str:
|
|||||||
return tok.split(" ", 1)[1]
|
return tok.split(" ", 1)[1]
|
||||||
return tok
|
return tok
|
||||||
|
|
||||||
|
|
||||||
# ================================
|
# ================================
|
||||||
# 💾 UPSERT (včetně správného updatedAt)
|
# 💾 UPSERT
|
||||||
# ================================
|
# ================================
|
||||||
def upsert(conn, r):
|
def upsert(conn, r):
|
||||||
p = r.get("extendedPatient") or {}
|
p = r.get("extendedPatient") or {}
|
||||||
|
|
||||||
# raw timestamps z API – nyní přes nový parser
|
|
||||||
api_updated = to_mysql_dt_utc(r.get("updatedAt"))
|
api_updated = to_mysql_dt_utc(r.get("updatedAt"))
|
||||||
|
|
||||||
last_msg = r.get("lastMessage") or {}
|
last_msg = r.get("lastMessage") or {}
|
||||||
msg_updated = to_mysql_dt_utc(last_msg.get("createdAt"))
|
msg_updated = to_mysql_dt_utc(last_msg.get("createdAt"))
|
||||||
|
|
||||||
# nejnovější změna
|
|
||||||
def max_dt(a, b):
|
def max_dt(a, b):
|
||||||
if a and b:
|
if a and b:
|
||||||
return max(a, b)
|
return max(a, b)
|
||||||
@@ -151,7 +144,6 @@ def upsert(conn, r):
|
|||||||
cur.execute(sql, vals)
|
cur.execute(sql, vals)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
# ================================
|
# ================================
|
||||||
# 📡 FETCH ACTIVE PAGE
|
# 📡 FETCH ACTIVE PAGE
|
||||||
# ================================
|
# ================================
|
||||||
@@ -173,11 +165,9 @@ def fetch_active(headers, offset):
|
|||||||
|
|
||||||
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers)
|
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
data = r.json().get("data", {}).get("requestsResponse", {})
|
data = r.json().get("data", {}).get("requestsResponse", {})
|
||||||
return data.get("patientRequests", []), data.get("count", 0)
|
return data.get("patientRequests", []), data.get("count", 0)
|
||||||
|
|
||||||
|
|
||||||
# ================================
|
# ================================
|
||||||
# 🧠 MAIN
|
# 🧠 MAIN
|
||||||
# ================================
|
# ================================
|
||||||
@@ -190,7 +180,6 @@ def main():
|
|||||||
}
|
}
|
||||||
|
|
||||||
conn = pymysql.connect(**DB_CONFIG)
|
conn = pymysql.connect(**DB_CONFIG)
|
||||||
|
|
||||||
print(f"\n=== Sync ACTIVE požadavků @ {datetime.now():%Y-%m-%d %H:%M:%S} ===")
|
print(f"\n=== Sync ACTIVE požadavků @ {datetime.now():%Y-%m-%d %H:%M:%S} ===")
|
||||||
|
|
||||||
offset = 0
|
offset = 0
|
||||||
@@ -199,7 +188,6 @@ def main():
|
|||||||
|
|
||||||
while True:
|
while True:
|
||||||
batch, count = fetch_active(headers, offset)
|
batch, count = fetch_active(headers, offset)
|
||||||
|
|
||||||
if total_count is None:
|
if total_count is None:
|
||||||
total_count = count
|
total_count = count
|
||||||
print(f"📡 Celkem ACTIVE v Medevio: {count}")
|
print(f"📡 Celkem ACTIVE v Medevio: {count}")
|
||||||
@@ -222,7 +210,5 @@ def main():
|
|||||||
conn.close()
|
conn.close()
|
||||||
print("\n✅ ACTIVE sync hotovo!\n")
|
print("\n✅ ACTIVE sync hotovo!\n")
|
||||||
|
|
||||||
|
|
||||||
# ================================
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
@@ -5,18 +5,45 @@ import pymysql
|
|||||||
import requests
|
import requests
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from dateutil import parser
|
||||||
import time
|
import time
|
||||||
|
import sys
|
||||||
|
|
||||||
# ================================
|
# ================================
|
||||||
# 🔧 CONFIGURATION
|
# UTF-8 SAFE OUTPUT (Windows friendly)
|
||||||
|
# ================================
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
sys.stderr.reconfigure(encoding='utf-8')
|
||||||
|
except AttributeError:
|
||||||
|
import io
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||||
|
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def safe_print(text: str):
|
||||||
|
enc = sys.stdout.encoding or ""
|
||||||
|
if not enc.lower().startswith("utf"):
|
||||||
|
text = ''.join(ch for ch in text if ord(ch) < 65536)
|
||||||
|
try:
|
||||||
|
print(text)
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
text = ''.join(ch for ch in text if ord(ch) < 128)
|
||||||
|
print(text)
|
||||||
|
|
||||||
|
|
||||||
|
# ================================
|
||||||
|
# 🔧 CONFIG
|
||||||
# ================================
|
# ================================
|
||||||
TOKEN_PATH = Path("token.txt")
|
TOKEN_PATH = Path("token.txt")
|
||||||
CLINIC_SLUG = "mudr-buzalkova"
|
CLINIC_SLUG = "mudr-buzalkova"
|
||||||
BATCH_SIZE = 100
|
|
||||||
|
BATCH_SIZE = 500
|
||||||
|
STATES = ["ACTIVE", "DONE"] # explicitně – jinak API vrací jen ACTIVE
|
||||||
|
|
||||||
DB_CONFIG = {
|
DB_CONFIG = {
|
||||||
"host": "192.168.1.76",
|
"host": "192.168.1.50",
|
||||||
"port": 3307,
|
"port": 3306,
|
||||||
"user": "root",
|
"user": "root",
|
||||||
"password": "Vlado9674+",
|
"password": "Vlado9674+",
|
||||||
"database": "medevio",
|
"database": "medevio",
|
||||||
@@ -25,20 +52,20 @@ DB_CONFIG = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
GRAPHQL_QUERY = r"""
|
GRAPHQL_QUERY = r"""
|
||||||
query ClinicRequestGrid_ListPatientRequestsForClinic2(
|
query ClinicRequestList2(
|
||||||
$clinicSlug: String!,
|
$clinicSlug: String!,
|
||||||
$queueId: String,
|
$queueId: String,
|
||||||
$queueAssignment: QueueAssignmentFilter!,
|
$queueAssignment: QueueAssignmentFilter!,
|
||||||
|
$state: PatientRequestState,
|
||||||
$pageInfo: PageInfo!,
|
$pageInfo: PageInfo!,
|
||||||
$locale: Locale!,
|
$locale: Locale!
|
||||||
$state: PatientRequestState
|
|
||||||
) {
|
) {
|
||||||
requestsResponse: listPatientRequestsForClinic2(
|
requestsResponse: listPatientRequestsForClinic2(
|
||||||
clinicSlug: $clinicSlug,
|
clinicSlug: $clinicSlug,
|
||||||
queueId: $queueId,
|
queueId: $queueId,
|
||||||
queueAssignment: $queueAssignment,
|
queueAssignment: $queueAssignment,
|
||||||
pageInfo: $pageInfo,
|
state: $state,
|
||||||
state: $state
|
pageInfo: $pageInfo
|
||||||
) {
|
) {
|
||||||
count
|
count
|
||||||
patientRequests {
|
patientRequests {
|
||||||
@@ -53,40 +80,51 @@ query ClinicRequestGrid_ListPatientRequestsForClinic2(
|
|||||||
surname
|
surname
|
||||||
identificationNumber
|
identificationNumber
|
||||||
}
|
}
|
||||||
|
lastMessage {
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
# ================================
|
# ================================
|
||||||
# 🔑 TOKEN
|
# TOKEN
|
||||||
# ================================
|
# ================================
|
||||||
def read_token(path: Path) -> str:
|
def read_token(path: Path) -> str:
|
||||||
tok = path.read_text(encoding="utf-8").strip()
|
tok = path.read_text(encoding="utf-8").strip()
|
||||||
if tok.startswith("Bearer "):
|
if tok.startswith("Bearer "):
|
||||||
tok = tok.split(" ", 1)[1]
|
return tok.split(" ", 1)[1]
|
||||||
return tok
|
return tok
|
||||||
|
|
||||||
|
|
||||||
# ================================
|
# ================================
|
||||||
# 🕒 DATETIME FORMAT
|
# DATETIME PARSER
|
||||||
# ================================
|
# ================================
|
||||||
def to_mysql_dt(iso_str):
|
def to_mysql_dt(iso_str):
|
||||||
if not iso_str:
|
if not iso_str:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
dt = datetime.fromisoformat(iso_str.replace("Z", "+00:00"))
|
dt = parser.isoparse(iso_str)
|
||||||
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
if dt.tzinfo is None:
|
||||||
except:
|
dt = dt.replace(tzinfo=datetime.now().astimezone().tzinfo)
|
||||||
|
return dt.astimezone().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
# ================================
|
# ================================
|
||||||
# 💾 UPSERT
|
# UPSERT
|
||||||
# ================================
|
# ================================
|
||||||
def upsert(conn, r):
|
def upsert(conn, r):
|
||||||
p = r.get("extendedPatient") or {}
|
p = r.get("extendedPatient") or {}
|
||||||
|
|
||||||
|
api_updated = to_mysql_dt(r.get("updatedAt"))
|
||||||
|
msg_updated = to_mysql_dt((r.get("lastMessage") or {}).get("createdAt"))
|
||||||
|
|
||||||
|
final_updated = max(filter(None, [api_updated, msg_updated]), default=None)
|
||||||
|
|
||||||
sql = """
|
sql = """
|
||||||
INSERT INTO pozadavky (
|
INSERT INTO pozadavky (
|
||||||
id, displayTitle, createdAt, updatedAt, doneAt, removedAt,
|
id, displayTitle, createdAt, updatedAt, doneAt, removedAt,
|
||||||
@@ -106,7 +144,7 @@ def upsert(conn, r):
|
|||||||
r.get("id"),
|
r.get("id"),
|
||||||
r.get("displayTitle"),
|
r.get("displayTitle"),
|
||||||
to_mysql_dt(r.get("createdAt")),
|
to_mysql_dt(r.get("createdAt")),
|
||||||
to_mysql_dt(r.get("updatedAt")),
|
final_updated,
|
||||||
to_mysql_dt(r.get("doneAt")),
|
to_mysql_dt(r.get("doneAt")),
|
||||||
to_mysql_dt(r.get("removedAt")),
|
to_mysql_dt(r.get("removedAt")),
|
||||||
p.get("name"),
|
p.get("name"),
|
||||||
@@ -120,33 +158,33 @@ def upsert(conn, r):
|
|||||||
|
|
||||||
|
|
||||||
# ================================
|
# ================================
|
||||||
# 📡 FETCH ACTIVE PAGE
|
# FETCH PAGE (per state)
|
||||||
# ================================
|
# ================================
|
||||||
def fetch_active(headers, offset):
|
def fetch_state(headers, state, offset):
|
||||||
variables = {
|
variables = {
|
||||||
"clinicSlug": CLINIC_SLUG,
|
"clinicSlug": CLINIC_SLUG,
|
||||||
"queueId": None,
|
"queueId": None,
|
||||||
"queueAssignment": "ANY",
|
"queueAssignment": "ANY",
|
||||||
|
"state": state,
|
||||||
"pageInfo": {"first": BATCH_SIZE, "offset": offset},
|
"pageInfo": {"first": BATCH_SIZE, "offset": offset},
|
||||||
"locale": "cs",
|
"locale": "cs",
|
||||||
"state": "ACTIVE",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"operationName": "ClinicRequestGrid_ListPatientRequestsForClinic2",
|
"operationName": "ClinicRequestList2",
|
||||||
"query": GRAPHQL_QUERY,
|
"query": GRAPHQL_QUERY,
|
||||||
"variables": variables,
|
"variables": variables,
|
||||||
}
|
}
|
||||||
|
|
||||||
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers)
|
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
data = r.json().get("data", {}).get("requestsResponse", {})
|
|
||||||
|
|
||||||
|
data = r.json()["data"]["requestsResponse"]
|
||||||
return data.get("patientRequests", []), data.get("count", 0)
|
return data.get("patientRequests", []), data.get("count", 0)
|
||||||
|
|
||||||
|
|
||||||
# ================================
|
# ================================
|
||||||
# 🧠 MAIN
|
# MAIN
|
||||||
# ================================
|
# ================================
|
||||||
def main():
|
def main():
|
||||||
token = read_token(TOKEN_PATH)
|
token = read_token(TOKEN_PATH)
|
||||||
@@ -158,21 +196,22 @@ def main():
|
|||||||
|
|
||||||
conn = pymysql.connect(**DB_CONFIG)
|
conn = pymysql.connect(**DB_CONFIG)
|
||||||
|
|
||||||
print(f"\n=== Sync ACTIVE požadavků @ {datetime.now():%Y-%m-%d %H:%M:%S} ===")
|
safe_print(f"\n=== FULL Medevio READ-ALL sync @ {datetime.now():%Y-%m-%d %H:%M:%S} ===")
|
||||||
|
|
||||||
# -------------------------------
|
grand_total = 0
|
||||||
# 🚀 FETCH ALL ACTIVE REQUESTS
|
|
||||||
# -------------------------------
|
for state in STATES:
|
||||||
|
safe_print(f"\n🔁 STATE = {state}")
|
||||||
offset = 0
|
offset = 0
|
||||||
total_processed = 0
|
total = None
|
||||||
total_count = None
|
processed = 0
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
batch, count = fetch_active(headers, offset)
|
batch, count = fetch_state(headers, state, offset)
|
||||||
|
|
||||||
if total_count is None:
|
if total is None:
|
||||||
total_count = count
|
total = count
|
||||||
print(f"📡 Celkem ACTIVE v Medevio: {count}")
|
safe_print(f"📡 {state}: celkem {total}")
|
||||||
|
|
||||||
if not batch:
|
if not batch:
|
||||||
break
|
break
|
||||||
@@ -180,18 +219,21 @@ def main():
|
|||||||
for r in batch:
|
for r in batch:
|
||||||
upsert(conn, r)
|
upsert(conn, r)
|
||||||
|
|
||||||
total_processed += len(batch)
|
processed += len(batch)
|
||||||
print(f" • {total_processed}/{total_count} ACTIVE processed")
|
safe_print(f" • {processed}/{total}")
|
||||||
|
|
||||||
if offset + BATCH_SIZE >= count:
|
|
||||||
break
|
|
||||||
|
|
||||||
offset += BATCH_SIZE
|
offset += BATCH_SIZE
|
||||||
|
if offset >= count:
|
||||||
|
break
|
||||||
|
|
||||||
time.sleep(0.4)
|
time.sleep(0.4)
|
||||||
|
|
||||||
|
grand_total += processed
|
||||||
|
|
||||||
conn.close()
|
conn.close()
|
||||||
print("\n✅ ACTIVE sync hotovo!\n")
|
safe_print(f"\n✅ HOTOVO – celkem zpracováno {grand_total} požadavků\n")
|
||||||
|
|
||||||
|
|
||||||
|
# ================================
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
217
12 Tower1/20 ReadPoznamky.py
Normal file
217
12 Tower1/20 ReadPoznamky.py
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Download and store Medevio questionnaires (userNote + eCRF) for all patient requests.
|
||||||
|
Uses the verified working query "GetPatientRequest2".
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
import pymysql
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Force UTF-8 output even under Windows Task Scheduler
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
sys.stderr.reconfigure(encoding='utf-8')
|
||||||
|
except AttributeError:
|
||||||
|
import io
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||||
|
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# 🛡 SAFE PRINT FOR CP1250 / EMOJI
|
||||||
|
# ==============================
|
||||||
|
def safe_print(text: str):
|
||||||
|
enc = sys.stdout.encoding or ""
|
||||||
|
if not enc.lower().startswith("utf"):
|
||||||
|
text = ''.join(ch for ch in text if ord(ch) < 65536)
|
||||||
|
try:
|
||||||
|
print(text)
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
text = ''.join(ch for ch in text if ord(ch) < 128)
|
||||||
|
print(text)
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# 🔧 CONFIGURATION (UPDATED TO 192.168.1.50)
|
||||||
|
# ==============================
|
||||||
|
TOKEN_PATH = Path("token.txt")
|
||||||
|
CLINIC_SLUG = "mudr-buzalkova"
|
||||||
|
GRAPHQL_URL = "https://api.medevio.cz/graphql"
|
||||||
|
|
||||||
|
DB_CONFIG = {
|
||||||
|
"host": "192.168.1.50",
|
||||||
|
"port": 3306,
|
||||||
|
"user": "root",
|
||||||
|
"password": "Vlado9674+",
|
||||||
|
"database": "medevio",
|
||||||
|
"charset": "utf8mb4",
|
||||||
|
"cursorclass": pymysql.cursors.DictCursor,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# 🕒 DATETIME FIXER
|
||||||
|
# ==============================
|
||||||
|
def fix_datetime(dt_str):
|
||||||
|
if not dt_str:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return datetime.fromisoformat(dt_str.replace("Z", "").replace("+00:00", ""))
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# Optional filter
|
||||||
|
CREATED_AFTER = "2025-01-01"
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# 🧮 HELPERS
|
||||||
|
# ==============================
|
||||||
|
def read_token(p: Path) -> str:
|
||||||
|
tok = p.read_text(encoding="utf-8").strip()
|
||||||
|
if tok.startswith("Bearer "):
|
||||||
|
return tok.split(" ", 1)[1]
|
||||||
|
return tok
|
||||||
|
|
||||||
|
|
||||||
|
GRAPHQL_QUERY = r"""
|
||||||
|
query GetPatientRequest2($requestId: UUID!, $clinicSlug: String!, $locale: Locale!) {
|
||||||
|
request: getPatientRequest2(patientRequestId: $requestId, clinicSlug: $clinicSlug) {
|
||||||
|
id
|
||||||
|
displayTitle(locale: $locale)
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
userNote
|
||||||
|
eventType
|
||||||
|
extendedPatient(clinicSlug: $clinicSlug) {
|
||||||
|
name
|
||||||
|
surname
|
||||||
|
identificationNumber
|
||||||
|
}
|
||||||
|
ecrfFilledData(locale: $locale) {
|
||||||
|
name
|
||||||
|
groups {
|
||||||
|
label
|
||||||
|
fields {
|
||||||
|
name
|
||||||
|
label
|
||||||
|
type
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_questionnaire(headers, request_id, clinic_slug):
|
||||||
|
payload = {
|
||||||
|
"operationName": "GetPatientRequest2",
|
||||||
|
"query": GRAPHQL_QUERY,
|
||||||
|
"variables": {
|
||||||
|
"requestId": request_id,
|
||||||
|
"clinicSlug": clinic_slug,
|
||||||
|
"locale": "cs",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
r = requests.post(GRAPHQL_URL, json=payload, headers=headers, timeout=40)
|
||||||
|
if r.status_code != 200:
|
||||||
|
safe_print(f"❌ HTTP {r.status_code} for {request_id}: {r.text}")
|
||||||
|
return None
|
||||||
|
return r.json().get("data", {}).get("request")
|
||||||
|
|
||||||
|
|
||||||
|
def insert_questionnaire(cur, req):
|
||||||
|
if not req:
|
||||||
|
return
|
||||||
|
|
||||||
|
patient = req.get("extendedPatient") or {}
|
||||||
|
ecrf_data = req.get("ecrfFilledData")
|
||||||
|
created_at = fix_datetime(req.get("createdAt"))
|
||||||
|
updated_at = fix_datetime(req.get("updatedAt"))
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO medevio_questionnaires (
|
||||||
|
request_id, created_at, updated_at, user_note, ecrf_json
|
||||||
|
)
|
||||||
|
VALUES (%s,%s,%s,%s,%s)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
updated_at = VALUES(updated_at),
|
||||||
|
user_note = VALUES(user_note),
|
||||||
|
ecrf_json = VALUES(ecrf_json),
|
||||||
|
updated_local = NOW()
|
||||||
|
""", (
|
||||||
|
req.get("id"),
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
req.get("userNote"),
|
||||||
|
json.dumps(ecrf_data, ensure_ascii=False),
|
||||||
|
))
|
||||||
|
|
||||||
|
safe_print(f" 💾 Stored questionnaire for {patient.get('surname','')} {patient.get('name','')}")
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# 🧠 MAIN
|
||||||
|
# ==============================
|
||||||
|
def main():
|
||||||
|
token = read_token(TOKEN_PATH)
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
conn = pymysql.connect(**DB_CONFIG)
|
||||||
|
|
||||||
|
# load list of requests from the table we just filled
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
sql = """
|
||||||
|
SELECT id, pacient_jmeno, pacient_prijmeni, createdAt, updatedAt, questionnaireprocessed
|
||||||
|
FROM pozadavky
|
||||||
|
WHERE (questionnaireprocessed IS NULL OR questionnaireprocessed < updatedAt)
|
||||||
|
"""
|
||||||
|
if CREATED_AFTER:
|
||||||
|
sql += " AND createdAt >= %s"
|
||||||
|
cur.execute(sql, (CREATED_AFTER,))
|
||||||
|
else:
|
||||||
|
cur.execute(sql)
|
||||||
|
|
||||||
|
rows = cur.fetchall()
|
||||||
|
|
||||||
|
safe_print(f"📋 Found {len(rows)} requests needing questionnaire check.")
|
||||||
|
|
||||||
|
for i, row in enumerate(rows, 1):
|
||||||
|
req_id = row["id"]
|
||||||
|
safe_print(f"\n[{i}/{len(rows)}] 🔍 Fetching questionnaire for {req_id} ...")
|
||||||
|
|
||||||
|
req = fetch_questionnaire(headers, req_id, CLINIC_SLUG)
|
||||||
|
if not req:
|
||||||
|
safe_print(" ⚠️ No questionnaire data found.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
insert_questionnaire(cur, req)
|
||||||
|
cur.execute(
|
||||||
|
"UPDATE pozadavky SET questionnaireprocessed = NOW() WHERE id = %s",
|
||||||
|
(req_id,)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
time.sleep(0.6)
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
safe_print("\n✅ Done! All questionnaires stored in MySQL table `medevio_questionnaires`.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
147
12 Tower1/30 ReadConversation.py
Normal file
147
12 Tower1/30 ReadConversation.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
import pymysql
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# UTF-8 SAFE OUTPUT
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
sys.stderr.reconfigure(encoding='utf-8')
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# CONFIG (.50)
|
||||||
|
# ==============================
|
||||||
|
TOKEN_PATH = Path("token.txt")
|
||||||
|
|
||||||
|
DB_CONFIG = {
|
||||||
|
"host": "192.168.1.50",
|
||||||
|
"port": 3306,
|
||||||
|
"user": "root",
|
||||||
|
"password": "Vlado9674+",
|
||||||
|
"database": "medevio",
|
||||||
|
"charset": "utf8mb4",
|
||||||
|
"cursorclass": pymysql.cursors.DictCursor,
|
||||||
|
}
|
||||||
|
|
||||||
|
GRAPHQL_QUERY_MESSAGES = r"""
|
||||||
|
query UseMessages_ListMessages($requestId: String!, $updatedSince: DateTime) {
|
||||||
|
messages: listMessages(patientRequestId: $requestId, updatedSince: $updatedSince) {
|
||||||
|
id createdAt updatedAt readAt text type
|
||||||
|
sender { id name surname clinicId }
|
||||||
|
medicalRecord { id description contentType url downloadUrl createdAt updatedAt }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def parse_dt(s):
|
||||||
|
if not s: return None
|
||||||
|
try: return datetime.fromisoformat(s.replace("Z", "+00:00"))
|
||||||
|
except: return None
|
||||||
|
|
||||||
|
def read_token(path: Path) -> str:
|
||||||
|
return path.read_text(encoding="utf-8").strip().replace("Bearer ", "")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
token = read_token(TOKEN_PATH)
|
||||||
|
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||||
|
conn = pymysql.connect(**DB_CONFIG)
|
||||||
|
|
||||||
|
# 1. Seznam již stažených příloh (prevence duplicit)
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("SELECT attachment_id FROM medevio_downloads")
|
||||||
|
existing_ids = {r["attachment_id"] for r in cur.fetchall()}
|
||||||
|
|
||||||
|
# 2. Seznam požadavků k synchronizaci
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("""
|
||||||
|
SELECT id, messagesProcessed FROM pozadavky
|
||||||
|
WHERE messagesProcessed IS NULL OR messagesProcessed < updatedAt
|
||||||
|
""")
|
||||||
|
rows = cur.fetchall()
|
||||||
|
|
||||||
|
print(f"📋 Počet požadavků k synchronizaci zpráv: {len(rows)}")
|
||||||
|
|
||||||
|
for i, row in enumerate(rows, 1):
|
||||||
|
req_id = row["id"]
|
||||||
|
updated_since = row["messagesProcessed"]
|
||||||
|
if updated_since:
|
||||||
|
updated_since = updated_since.replace(microsecond=0).isoformat() + "Z"
|
||||||
|
|
||||||
|
print(f"[{i}/{len(rows)}] Synchronizuji: {req_id}")
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"operationName": "UseMessages_ListMessages",
|
||||||
|
"query": GRAPHQL_QUERY_MESSAGES,
|
||||||
|
"variables": {"requestId": req_id, "updatedSince": updated_since}
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30)
|
||||||
|
messages = r.json().get("data", {}).get("messages", []) or []
|
||||||
|
|
||||||
|
if messages:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
for msg in messages:
|
||||||
|
# Uložení zprávy
|
||||||
|
sender = msg.get("sender") or {}
|
||||||
|
sender_name = " ".join(filter(None, [sender.get("name"), sender.get("surname")]))
|
||||||
|
mr = msg.get("medicalRecord") or {}
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO medevio_conversation (
|
||||||
|
id, request_id, sender_name, sender_id, sender_clinic_id,
|
||||||
|
text, created_at, read_at, updated_at,
|
||||||
|
attachment_url, attachment_description, attachment_content_type
|
||||||
|
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
text = VALUES(text), updated_at = VALUES(updated_at), read_at = VALUES(read_at)
|
||||||
|
""", (
|
||||||
|
msg.get("id"), req_id, sender_name, sender.get("id"), sender.get("clinicId"),
|
||||||
|
msg.get("text"), parse_dt(msg.get("createdAt")), parse_dt(msg.get("readAt")),
|
||||||
|
parse_dt(msg.get("updatedAt")), mr.get("downloadUrl") or mr.get("url"),
|
||||||
|
mr.get("description"), mr.get("contentType")
|
||||||
|
))
|
||||||
|
|
||||||
|
# Uložení přílohy (pokud existuje a nemáme ji)
|
||||||
|
attachment_id = mr.get("id")
|
||||||
|
if attachment_id and attachment_id not in existing_ids:
|
||||||
|
url = mr.get("downloadUrl") or mr.get("url")
|
||||||
|
if url:
|
||||||
|
att_r = requests.get(url, timeout=30)
|
||||||
|
if att_r.status_code == 200:
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO medevio_downloads (
|
||||||
|
request_id, attachment_id, attachment_type,
|
||||||
|
filename, content_type, file_size, created_at, file_content
|
||||||
|
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s)
|
||||||
|
""", (
|
||||||
|
req_id, attachment_id, "MESSAGE_ATTACHMENT",
|
||||||
|
url.split("/")[-1].split("?")[0], mr.get("contentType"),
|
||||||
|
len(att_r.content), parse_dt(msg.get("createdAt")), att_r.content
|
||||||
|
))
|
||||||
|
existing_ids.add(attachment_id)
|
||||||
|
|
||||||
|
cur.execute("UPDATE pozadavky SET messagesProcessed = NOW() WHERE id = %s", (req_id,))
|
||||||
|
conn.commit()
|
||||||
|
else:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("UPDATE pozadavky SET messagesProcessed = NOW() WHERE id = %s", (req_id,))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
time.sleep(0.3)
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ Chyba u {req_id}: {e}")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
print("\n🎉 Delta sync zpráv a příloh DOKONČEN")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
177
12 Tower1/40 DownloadPrilohy.py
Normal file
177
12 Tower1/40 DownloadPrilohy.py
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Download all attachments for pozadavky where attachmentsProcessed IS NULL
|
||||||
|
Store them in MySQL table `medevio_downloads` on 192.168.1.50.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import zlib
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
import pymysql
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Force UTF-8 output
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
sys.stderr.reconfigure(encoding='utf-8')
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# 🛡 SAFE PRINT
|
||||||
|
# ==============================
|
||||||
|
def safe_print(text: str):
|
||||||
|
enc = sys.stdout.encoding or ""
|
||||||
|
if not enc or not enc.lower().startswith("utf"):
|
||||||
|
text = ''.join(ch for ch in text if ord(ch) < 65536)
|
||||||
|
try:
|
||||||
|
print(text)
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
text = ''.join(ch for ch in text if ord(ch) < 128)
|
||||||
|
print(text)
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# 🔧 CONFIGURATION (.50)
|
||||||
|
# ==============================
|
||||||
|
TOKEN_PATH = Path("token.txt")
|
||||||
|
CLINIC_SLUG = "mudr-buzalkova"
|
||||||
|
|
||||||
|
DB_CONFIG = {
|
||||||
|
"host": "192.168.1.50",
|
||||||
|
"port": 3306,
|
||||||
|
"user": "root",
|
||||||
|
"password": "Vlado9674+",
|
||||||
|
"database": "medevio",
|
||||||
|
"charset": "utf8mb4",
|
||||||
|
"cursorclass": pymysql.cursors.DictCursor,
|
||||||
|
}
|
||||||
|
|
||||||
|
CREATED_AFTER = "2024-12-01"
|
||||||
|
|
||||||
|
GRAPHQL_QUERY = r"""
|
||||||
|
query ClinicRequestDetail_GetPatientRequest2($requestId: UUID!) {
|
||||||
|
patientRequestMedicalRecords: listMedicalRecordsForPatientRequest(
|
||||||
|
attachmentTypes: [ECRF_FILL_ATTACHMENT, MESSAGE_ATTACHMENT, PATIENT_REQUEST_ATTACHMENT]
|
||||||
|
patientRequestId: $requestId
|
||||||
|
pageInfo: {first: 100, offset: 0}
|
||||||
|
) {
|
||||||
|
attachmentType
|
||||||
|
id
|
||||||
|
medicalRecord {
|
||||||
|
contentType
|
||||||
|
description
|
||||||
|
downloadUrl
|
||||||
|
id
|
||||||
|
url
|
||||||
|
visibleToPatient
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def extract_filename_from_url(url: str) -> str:
|
||||||
|
try:
|
||||||
|
return url.split("/")[-1].split("?")[0]
|
||||||
|
except:
|
||||||
|
return "unknown_filename"
|
||||||
|
|
||||||
|
|
||||||
|
def read_token(p: Path) -> str:
|
||||||
|
tok = p.read_text(encoding="utf-8").strip()
|
||||||
|
return tok.split(" ", 1)[1] if tok.startswith("Bearer ") else tok
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
token = read_token(TOKEN_PATH)
|
||||||
|
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||||
|
conn = pymysql.connect(**DB_CONFIG)
|
||||||
|
|
||||||
|
# 1. Načtení ID již stažených příloh
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("SELECT attachment_id FROM medevio_downloads")
|
||||||
|
existing_ids = {row["attachment_id"] for row in cur.fetchall()}
|
||||||
|
|
||||||
|
safe_print(f"✅ V databázi již máme {len(existing_ids)} příloh.")
|
||||||
|
|
||||||
|
# 2. Výběr požadavků ke zpracování
|
||||||
|
sql = "SELECT id, pacient_prijmeni, pacient_jmeno, createdAt FROM pozadavky WHERE attachmentsProcessed IS NULL"
|
||||||
|
params = []
|
||||||
|
if CREATED_AFTER:
|
||||||
|
sql += " AND createdAt >= %s"
|
||||||
|
params.append(CREATED_AFTER)
|
||||||
|
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(sql, params)
|
||||||
|
req_rows = cur.fetchall()
|
||||||
|
|
||||||
|
safe_print(f"📋 Počet požadavků ke stažení příloh: {len(req_rows)}")
|
||||||
|
|
||||||
|
for i, row in enumerate(req_rows, 1):
|
||||||
|
req_id = row["id"]
|
||||||
|
prijmeni = row.get("pacient_prijmeni") or "Neznamy"
|
||||||
|
created_date = row.get("createdAt") or datetime.now()
|
||||||
|
|
||||||
|
safe_print(f"\n[{i}/{len(req_rows)}] 🧾 {prijmeni} ({req_id})")
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"operationName": "ClinicRequestDetail_GetPatientRequest2",
|
||||||
|
"query": GRAPHQL_QUERY,
|
||||||
|
"variables": {"requestId": req_id},
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30)
|
||||||
|
attachments = r.json().get("data", {}).get("patientRequestMedicalRecords", [])
|
||||||
|
|
||||||
|
if attachments:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
for a in attachments:
|
||||||
|
m = a.get("medicalRecord") or {}
|
||||||
|
att_id = a.get("id")
|
||||||
|
|
||||||
|
if att_id in existing_ids:
|
||||||
|
continue
|
||||||
|
|
||||||
|
url = m.get("downloadUrl")
|
||||||
|
if url:
|
||||||
|
att_r = requests.get(url, timeout=30)
|
||||||
|
if att_r.status_code == 200:
|
||||||
|
content = att_r.content
|
||||||
|
filename = extract_filename_from_url(url)
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO medevio_downloads (
|
||||||
|
request_id, attachment_id, attachment_type,
|
||||||
|
filename, content_type, file_size,
|
||||||
|
created_at, file_content
|
||||||
|
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s)
|
||||||
|
""", (req_id, att_id, a.get("attachmentType"), filename,
|
||||||
|
m.get("contentType"), len(content), created_date, content))
|
||||||
|
existing_ids.add(att_id)
|
||||||
|
safe_print(f" 💾 Uloženo: {filename} ({len(content) / 1024:.1f} kB)")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
# Označíme jako zpracované i když nebyly nalezeny žádné přílohy
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("UPDATE pozadavky SET attachmentsProcessed = NOW() WHERE id = %s", (req_id,))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
time.sleep(0.3)
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ Chyba u {req_id}: {e}")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
safe_print("\n🎯 Všechny přílohy byly zpracovány.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
252
12 Tower1/50 SaveToFileSystem incremental.py
Normal file
252
12 Tower1/50 SaveToFileSystem incremental.py
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import pymysql
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Force UTF-8 output even under Windows Task Scheduler
|
||||||
|
import sys
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
sys.stderr.reconfigure(encoding='utf-8')
|
||||||
|
except AttributeError:
|
||||||
|
# Python < 3.7 fallback (not needed for you, but safe)
|
||||||
|
import io
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||||
|
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# 🛡 SAFE PRINT FOR CP1250 / EMOJI
|
||||||
|
# ==============================
|
||||||
|
def safe_print(text: str = ""):
|
||||||
|
enc = sys.stdout.encoding or ""
|
||||||
|
if not enc.lower().startswith("utf"):
|
||||||
|
# Strip emoji and characters outside BMP for Task Scheduler
|
||||||
|
text = ''.join(ch for ch in text if ord(ch) < 65536)
|
||||||
|
try:
|
||||||
|
print(text)
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
# ASCII fallback
|
||||||
|
text = ''.join(ch for ch in text if ord(ch) < 128)
|
||||||
|
print(text)
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# ⚙️ CONFIGURATION
|
||||||
|
# ==============================
|
||||||
|
DB_CONFIG = {
|
||||||
|
"host": "192.168.1.50",
|
||||||
|
"port": 3306,
|
||||||
|
"user": "root",
|
||||||
|
"password": "Vlado9674+",
|
||||||
|
"database": "medevio",
|
||||||
|
"charset": "utf8mb4",
|
||||||
|
}
|
||||||
|
|
||||||
|
BASE_DIR = Path(r"u:\Dropbox\Ordinace\Dokumentace_ke_zpracování\MP")
|
||||||
|
BASE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_name(name: str) -> str:
|
||||||
|
"""Replace invalid filename characters with underscore."""
|
||||||
|
return re.sub(r'[<>:"/\\|?*\x00-\x1F]', "_", name).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def make_abbrev(title: str) -> str:
|
||||||
|
if not title:
|
||||||
|
return ""
|
||||||
|
words = re.findall(r"[A-Za-zÁ-Žá-ž0-9]+", title)
|
||||||
|
abbr = ""
|
||||||
|
for w in words:
|
||||||
|
if w.isdigit():
|
||||||
|
abbr += w
|
||||||
|
else:
|
||||||
|
abbr += w[0]
|
||||||
|
return abbr.upper()
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# 🧹 DELETE UNEXPECTED FILES
|
||||||
|
# ==============================
|
||||||
|
def clean_folder(folder: Path, valid_files: set):
|
||||||
|
if not folder.exists():
|
||||||
|
return
|
||||||
|
|
||||||
|
for f in folder.iterdir():
|
||||||
|
if f.is_file():
|
||||||
|
if f.name.startswith("▲"):
|
||||||
|
continue
|
||||||
|
sanitized = sanitize_name(f.name)
|
||||||
|
if sanitized not in valid_files:
|
||||||
|
safe_print(f"🗑️ Removing unexpected file: {f.name}")
|
||||||
|
try:
|
||||||
|
f.unlink()
|
||||||
|
except Exception as e:
|
||||||
|
safe_print(f"⚠️ Could not delete {f}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# 📦 DB CONNECTION
|
||||||
|
# ==============================
|
||||||
|
conn = pymysql.connect(**DB_CONFIG)
|
||||||
|
|
||||||
|
cur_meta = conn.cursor(pymysql.cursors.DictCursor)
|
||||||
|
cur_blob = conn.cursor()
|
||||||
|
|
||||||
|
safe_print("🔍 Loading metadata from DB (FAST)…")
|
||||||
|
|
||||||
|
cur_meta.execute("""
|
||||||
|
SELECT d.id AS download_id,
|
||||||
|
d.request_id,
|
||||||
|
d.filename,
|
||||||
|
d.created_at,
|
||||||
|
p.updatedAt AS req_updated_at,
|
||||||
|
p.pacient_jmeno AS jmeno,
|
||||||
|
p.pacient_prijmeni AS prijmeni,
|
||||||
|
p.displayTitle
|
||||||
|
FROM medevio_downloads d
|
||||||
|
JOIN pozadavky p ON d.request_id = p.id
|
||||||
|
ORDER BY p.updatedAt DESC
|
||||||
|
""")
|
||||||
|
|
||||||
|
rows = cur_meta.fetchall()
|
||||||
|
safe_print(f"📋 Found {len(rows)} attachment records.\n")
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# 🧠 MAIN LOOP WITH PROGRESS
|
||||||
|
# ==============================
|
||||||
|
|
||||||
|
unique_request_ids = []
|
||||||
|
seen = set()
|
||||||
|
for r in rows:
|
||||||
|
req_id = r["request_id"]
|
||||||
|
if req_id not in seen:
|
||||||
|
unique_request_ids.append(req_id)
|
||||||
|
seen.add(req_id)
|
||||||
|
|
||||||
|
total_requests = len(unique_request_ids)
|
||||||
|
safe_print(f"🔄 Processing {total_requests} unique requests...\n")
|
||||||
|
|
||||||
|
processed_requests = set()
|
||||||
|
current_index = 0
|
||||||
|
|
||||||
|
for r in rows:
|
||||||
|
req_id = r["request_id"]
|
||||||
|
|
||||||
|
if req_id in processed_requests:
|
||||||
|
continue
|
||||||
|
processed_requests.add(req_id)
|
||||||
|
|
||||||
|
current_index += 1
|
||||||
|
percent = (current_index / total_requests) * 100
|
||||||
|
|
||||||
|
safe_print(f"\n[ {percent:5.1f}% ] Processing request {current_index} / {total_requests} → {req_id}")
|
||||||
|
|
||||||
|
# ========== FETCH VALID FILENAMES ==========
|
||||||
|
cur_meta.execute(
|
||||||
|
"SELECT filename FROM medevio_downloads WHERE request_id=%s",
|
||||||
|
(req_id,)
|
||||||
|
)
|
||||||
|
valid_files = {sanitize_name(row["filename"]) for row in cur_meta.fetchall()}
|
||||||
|
|
||||||
|
# ========== BUILD FOLDER NAME ==========
|
||||||
|
updated_at = r["req_updated_at"] or datetime.now()
|
||||||
|
date_str = updated_at.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
prijmeni = sanitize_name(r["prijmeni"] or "Unknown")
|
||||||
|
jmeno = sanitize_name(r["jmeno"] or "")
|
||||||
|
title = r.get("displayTitle") or ""
|
||||||
|
abbr = make_abbrev(title)
|
||||||
|
|
||||||
|
clean_folder_name = sanitize_name(
|
||||||
|
f"{date_str} {prijmeni}, {jmeno} [{abbr}] {req_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ========== DETECT EXISTING FOLDER ==========
|
||||||
|
existing_folder = None
|
||||||
|
|
||||||
|
for f in BASE_DIR.iterdir():
|
||||||
|
if f.is_dir() and req_id in f.name:
|
||||||
|
existing_folder = f
|
||||||
|
break
|
||||||
|
|
||||||
|
main_folder = existing_folder if existing_folder else BASE_DIR / clean_folder_name
|
||||||
|
|
||||||
|
# ========== MERGE DUPLICATES ==========
|
||||||
|
possible_dups = [
|
||||||
|
f for f in BASE_DIR.iterdir()
|
||||||
|
if f.is_dir() and req_id in f.name and f != main_folder
|
||||||
|
]
|
||||||
|
|
||||||
|
for dup in possible_dups:
|
||||||
|
safe_print(f"♻️ Merging duplicate folder: {dup.name}")
|
||||||
|
|
||||||
|
clean_folder(dup, valid_files)
|
||||||
|
main_folder.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
for f in dup.iterdir():
|
||||||
|
if f.is_file():
|
||||||
|
target = main_folder / f.name
|
||||||
|
if not target.exists():
|
||||||
|
f.rename(target)
|
||||||
|
|
||||||
|
shutil.rmtree(dup, ignore_errors=True)
|
||||||
|
|
||||||
|
# ========== CLEAN MAIN FOLDER ==========
|
||||||
|
clean_folder(main_folder, valid_files)
|
||||||
|
|
||||||
|
# ========== DOWNLOAD MISSING FILES ==========
|
||||||
|
added_new_file = False
|
||||||
|
main_folder.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
for filename in valid_files:
|
||||||
|
dest_plain = main_folder / filename
|
||||||
|
dest_marked = main_folder / ("▲" + filename)
|
||||||
|
|
||||||
|
if dest_plain.exists() or dest_marked.exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
added_new_file = True
|
||||||
|
|
||||||
|
cur_blob.execute(
|
||||||
|
"SELECT file_content FROM medevio_downloads "
|
||||||
|
"WHERE request_id=%s AND filename=%s",
|
||||||
|
(req_id, filename)
|
||||||
|
)
|
||||||
|
row = cur_blob.fetchone()
|
||||||
|
if not row:
|
||||||
|
continue
|
||||||
|
|
||||||
|
content = row[0]
|
||||||
|
if not content:
|
||||||
|
continue
|
||||||
|
|
||||||
|
with open(dest_plain, "wb") as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
safe_print(f"💾 Wrote: {dest_plain.relative_to(BASE_DIR)}")
|
||||||
|
|
||||||
|
# ========== REMOVE ▲ FLAG IF NEW FILES ADDED ==========
|
||||||
|
if added_new_file and "▲" in main_folder.name:
|
||||||
|
new_name = main_folder.name.replace("▲", "").strip()
|
||||||
|
new_path = main_folder.parent / new_name
|
||||||
|
|
||||||
|
if new_path != main_folder:
|
||||||
|
try:
|
||||||
|
main_folder.rename(new_path)
|
||||||
|
safe_print(f"🔄 Folder flag ▲ removed → {new_name}")
|
||||||
|
main_folder = new_path
|
||||||
|
except Exception as e:
|
||||||
|
safe_print(f"⚠️ Could not rename folder: {e}")
|
||||||
|
|
||||||
|
safe_print("\n🎯 Export complete.\n")
|
||||||
|
|
||||||
|
cur_blob.close()
|
||||||
|
cur_meta.close()
|
||||||
|
conn.close()
|
||||||
@@ -12,8 +12,8 @@ from datetime import datetime
|
|||||||
# ⚙️ CONFIGURATION
|
# ⚙️ CONFIGURATION
|
||||||
# ==============================
|
# ==============================
|
||||||
DB_CONFIG = {
|
DB_CONFIG = {
|
||||||
"host": "192.168.1.76",
|
"host": "192.168.1.50",
|
||||||
"port": 3307,
|
"port": 3306,
|
||||||
"user": "root",
|
"user": "root",
|
||||||
"password": "Vlado9674+",
|
"password": "Vlado9674+",
|
||||||
"database": "medevio",
|
"database": "medevio",
|
||||||
1
12 Tower1/token.txt
Normal file
1
12 Tower1/token.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
nYvrvgflIKcDiQg8Hhpud+qG8iGZ8eH8su4nyT/Mgcm7XQp65ygY9s39+O01wIpk/7sKd6fBHkiKvsqH
|
||||||
92
30 ManipulacePoznámek/100 JednoducheCteni1poznamky.py
Normal file
92
30 ManipulacePoznámek/100 JednoducheCteni1poznamky.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# UTF-8 safety
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# === CONFIG ===
|
||||||
|
|
||||||
|
TOKEN_PATH = Path("token.txt")
|
||||||
|
GRAPHQL_URL = "https://api.medevio.cz/graphql"
|
||||||
|
REQUEST_ID = "e17536c4-ed22-4242-ada5-d03713e0b7ac" # požadavek který sledujeme
|
||||||
|
|
||||||
|
|
||||||
|
def read_token(path: Path) -> str:
|
||||||
|
t = path.read_text().strip()
|
||||||
|
if t.startswith("Bearer "):
|
||||||
|
return t.split(" ", 1)[1]
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
|
# === QUERY ===
|
||||||
|
|
||||||
|
QUERY = r"""
|
||||||
|
query ClinicRequestNotes_Get($patientRequestId: String!) {
|
||||||
|
notes: getClinicPatientRequestNotes(requestId: $patientRequestId) {
|
||||||
|
id
|
||||||
|
content
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
createdBy {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
surname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def run_query(request_id, token):
|
||||||
|
payload = {
|
||||||
|
"operationName": "ClinicRequestNotes_Get",
|
||||||
|
"query": QUERY,
|
||||||
|
"variables": {"patientRequestId": request_id},
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
r = requests.post(GRAPHQL_URL, json=payload, headers=headers)
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
token = read_token(TOKEN_PATH)
|
||||||
|
|
||||||
|
print(f"🔍 Čtu interní klinické poznámky k požadavku {REQUEST_ID} ...\n")
|
||||||
|
|
||||||
|
data = run_query(REQUEST_ID, token)
|
||||||
|
|
||||||
|
notes = data.get("data", {}).get("notes", [])
|
||||||
|
if not notes:
|
||||||
|
print("📭 Žádné klinické poznámky nejsou uložené.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"📌 Nalezeno {len(notes)} poznámek:\n")
|
||||||
|
|
||||||
|
for n in notes:
|
||||||
|
print("──────────────────────────────")
|
||||||
|
print(f"🆔 ID: {n['id']}")
|
||||||
|
print(f"👤 Vytvořil: {n['createdBy']['surname']} {n['createdBy']['name']}")
|
||||||
|
print(f"📅 createdAt: {n['createdAt']}")
|
||||||
|
print(f"🕒 updatedAt: {n['updatedAt']}")
|
||||||
|
print("📝 Obsah:")
|
||||||
|
print(n['content'])
|
||||||
|
print("")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
121
30 ManipulacePoznámek/101 JednoducheDoplneniInterniPoznamky.py
Normal file
121
30 ManipulacePoznámek/101 JednoducheDoplneniInterniPoznamky.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# UTF-8 handling
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# === CONFIG ===
|
||||||
|
|
||||||
|
TOKEN_PATH = Path("token.txt")
|
||||||
|
GRAPHQL_URL = "https://api.medevio.cz/graphql"
|
||||||
|
|
||||||
|
REQUEST_ID = "e17536c4-ed22-4242-ada5-d03713e0b7ac" # požadavek
|
||||||
|
NOTE_PREPEND_TEXT = "🔥 NOVÝ TESTOVACÍ ŘÁDEK\n" # text, který se přidá NA ZAČÁTEK
|
||||||
|
|
||||||
|
|
||||||
|
# === Helpers ===
|
||||||
|
|
||||||
|
def read_token(p: Path) -> str:
|
||||||
|
t = p.read_text().strip()
|
||||||
|
if t.startswith("Bearer "):
|
||||||
|
return t.split(" ", 1)[1]
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
|
# === Queries ===
|
||||||
|
|
||||||
|
QUERY_GET_NOTES = r"""
|
||||||
|
query ClinicRequestNotes_Get($patientRequestId: String!) {
|
||||||
|
notes: getClinicPatientRequestNotes(requestId: $patientRequestId) {
|
||||||
|
id
|
||||||
|
content
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
createdBy {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
surname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
MUTATION_UPDATE_NOTE = r"""
|
||||||
|
mutation ClinicRequestNotes_Update($noteInput: UpdateClinicPatientRequestNoteInput!) {
|
||||||
|
updateClinicPatientRequestNote(noteInput: $noteInput) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# === Core functions ===
|
||||||
|
|
||||||
|
def gql(query, variables, token):
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
payload = {"query": query, "variables": variables}
|
||||||
|
|
||||||
|
r = requests.post(GRAPHQL_URL, json=payload, headers=headers)
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
|
def get_internal_note(request_id, token):
|
||||||
|
data = gql(QUERY_GET_NOTES, {"patientRequestId": request_id}, token)
|
||||||
|
notes = data.get("data", {}).get("notes", [])
|
||||||
|
return notes[0] if notes else None
|
||||||
|
|
||||||
|
|
||||||
|
def update_internal_note(note_id, new_content, token):
|
||||||
|
variables = {"noteInput": {"id": note_id, "content": new_content}}
|
||||||
|
return gql(MUTATION_UPDATE_NOTE, variables, token)
|
||||||
|
|
||||||
|
|
||||||
|
# === MAIN ===
|
||||||
|
|
||||||
|
def main():
|
||||||
|
token = read_token(TOKEN_PATH)
|
||||||
|
|
||||||
|
print(f"🔍 Načítám interní poznámku pro požadavek {REQUEST_ID}...\n")
|
||||||
|
|
||||||
|
note = get_internal_note(REQUEST_ID, token)
|
||||||
|
if not note:
|
||||||
|
print("❌ Nebyla nalezena žádná interní klinická poznámka!")
|
||||||
|
return
|
||||||
|
|
||||||
|
note_id = note["id"]
|
||||||
|
old_content = note["content"] or ""
|
||||||
|
|
||||||
|
print("📄 Původní obsah:")
|
||||||
|
print(old_content)
|
||||||
|
print("────────────────────────────\n")
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# PREPEND new text
|
||||||
|
# ===============================
|
||||||
|
new_content = NOTE_PREPEND_TEXT + old_content
|
||||||
|
|
||||||
|
print("📝 Nový obsah který odešlu:")
|
||||||
|
print(new_content)
|
||||||
|
print("────────────────────────────\n")
|
||||||
|
|
||||||
|
# UPDATE
|
||||||
|
result = update_internal_note(note_id, new_content, token)
|
||||||
|
|
||||||
|
print(f"✅ Hotovo! Poznámka {note_id} aktualizována.")
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import mysql.connector
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# UTF-8 handling
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# === KONFIGURACE ===
|
||||||
|
|
||||||
|
# --- Medevio API ---
|
||||||
|
TOKEN_PATH = Path("token.txt")
|
||||||
|
GRAPHQL_URL = "https://api.medevio.cz/graphql"
|
||||||
|
|
||||||
|
# --- ZPRACOVÁNÍ ---
|
||||||
|
# Zadejte počet požadavků ke zpracování.
|
||||||
|
# 0 znamená zpracovat VŠECHNY nesynchronizované požadavky.
|
||||||
|
PROCESS_LIMIT = 10 # <-- Používáme PROCESS_LIMIT
|
||||||
|
|
||||||
|
# --- MySQL DB ---
|
||||||
|
DB_CONFIG = {
|
||||||
|
"host": "192.168.1.76",
|
||||||
|
"port": 3307,
|
||||||
|
"user": "root",
|
||||||
|
"password": "Vlado9674+",
|
||||||
|
"database": "medevio",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# === Helpers ===
|
||||||
|
|
||||||
|
def read_token(p: Path) -> str:
|
||||||
|
"""Načte Bearer token z textového souboru."""
|
||||||
|
t = p.read_text().strip()
|
||||||
|
if t.startswith("Bearer "):
|
||||||
|
return t.split(" ", 1)[1]
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
|
# === DB Funkce ===
|
||||||
|
|
||||||
|
def get_requests_to_process_from_db(limit):
|
||||||
|
"""
|
||||||
|
Získá seznam požadavků (ID, Titul, Jméno, Příjmení) k synchronizaci z MySQL.
|
||||||
|
Použije LIMIT, pokud limit > 0.
|
||||||
|
"""
|
||||||
|
if limit == 0:
|
||||||
|
print("🔍 Připojuji se k MySQL a hledám **VŠECHNY** nesynchronizované požadavky...")
|
||||||
|
else:
|
||||||
|
print(f"🔍 Připojuji se k MySQL a hledám **{limit}** nesynchronizovaných požadavků...")
|
||||||
|
|
||||||
|
requests_list = []
|
||||||
|
conn = None
|
||||||
|
try:
|
||||||
|
conn = mysql.connector.connect(**DB_CONFIG)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Základní SQL dotaz
|
||||||
|
query = """
|
||||||
|
SELECT id, displayTitle, pacient_jmeno, pacient_prijmeni
|
||||||
|
FROM pozadavky
|
||||||
|
WHERE doneAt IS NULL
|
||||||
|
AND noteSyncedAt IS NULL
|
||||||
|
ORDER BY updatedAt DESC
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Podmíněné přidání LIMIT klauzule
|
||||||
|
if limit > 0:
|
||||||
|
query += f"LIMIT {limit};"
|
||||||
|
else:
|
||||||
|
query += ";"
|
||||||
|
|
||||||
|
cursor.execute(query)
|
||||||
|
results = cursor.fetchall()
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
request_id, display_title, jmeno, prijmeni = result
|
||||||
|
requests_list.append({
|
||||||
|
"id": request_id,
|
||||||
|
"displayTitle": display_title,
|
||||||
|
"jmeno": jmeno,
|
||||||
|
"prijmeni": prijmeni
|
||||||
|
})
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
if requests_list:
|
||||||
|
print(f"✅ Nalezeno {len(requests_list)} požadavků ke zpracování.")
|
||||||
|
else:
|
||||||
|
print("❌ Nebyl nalezen žádný nesynchronizovaný otevřený požadavek v DB.")
|
||||||
|
|
||||||
|
return requests_list
|
||||||
|
|
||||||
|
except mysql.connector.Error as err:
|
||||||
|
print(f"❌ Chyba při připojení/dotazu MySQL: {err}")
|
||||||
|
return []
|
||||||
|
finally:
|
||||||
|
if conn and conn.is_connected():
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def update_db_sync_time(request_id, conn):
|
||||||
|
"""Aktualizuje sloupec noteSyncedAt v tabulce pozadavky. Používá existující připojení."""
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
update_query = """
|
||||||
|
UPDATE pozadavky
|
||||||
|
SET noteSyncedAt = %s
|
||||||
|
WHERE id = %s;
|
||||||
|
"""
|
||||||
|
|
||||||
|
cursor.execute(update_query, (current_time, request_id))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
print(f" (DB: Čas synchronizace pro {request_id} uložen)")
|
||||||
|
|
||||||
|
|
||||||
|
# === GraphQL Operace (Beze Změny) ===
|
||||||
|
|
||||||
|
QUERY_GET_NOTE = r"""
|
||||||
|
query ClinicRequestNotes_Get($patientRequestId: String!) {
|
||||||
|
notes: getClinicPatientRequestNotes(requestId: $patientRequestId) {
|
||||||
|
id
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
MUTATION_UPDATE_NOTE = r"""
|
||||||
|
mutation ClinicRequestNotes_Update($noteInput: UpdateClinicPatientRequestNoteInput!) {
|
||||||
|
updateClinicPatientRequestNote(noteInput: $noteInput) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
MUTATION_CREATE_NOTE = r"""
|
||||||
|
mutation ClinicRequestNotes_Create($noteInput: CreateClinicPatientRequestNoteInput!) {
|
||||||
|
createClinicPatientRequestNote(noteInput: $noteInput) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def gql(query, variables, token):
|
||||||
|
"""Obecná funkce pro volání GraphQL endpointu."""
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
payload = {"query": query, "variables": variables}
|
||||||
|
|
||||||
|
r = requests.post(GRAPHQL_URL, json=payload, headers=headers)
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
|
def get_internal_note(request_id, token):
|
||||||
|
"""Získá jedinou interní poznámku (obsah a ID) pro daný požadavek."""
|
||||||
|
data = gql(QUERY_GET_NOTE, {"patientRequestId": request_id}, token)
|
||||||
|
notes = data.get("data", {}).get("notes", [])
|
||||||
|
return notes[0] if notes else None
|
||||||
|
|
||||||
|
|
||||||
|
def update_internal_note(note_id, new_content, token):
|
||||||
|
"""Aktualizuje obsah poznámky v Medeviu."""
|
||||||
|
variables = {"noteInput": {"id": note_id, "content": new_content}}
|
||||||
|
return gql(MUTATION_UPDATE_NOTE, variables, token)
|
||||||
|
|
||||||
|
|
||||||
|
def create_internal_note(request_id, content, token):
|
||||||
|
"""Vytvoří novou interní poznámku k požadavku v Medeviu."""
|
||||||
|
variables = {"noteInput": {"requestId": request_id, "content": content}}
|
||||||
|
return gql(MUTATION_CREATE_NOTE, variables, token)
|
||||||
|
|
||||||
|
|
||||||
|
# === MAIN ===
|
||||||
|
|
||||||
|
def main():
|
||||||
|
token = read_token(TOKEN_PATH)
|
||||||
|
|
||||||
|
# 1. Získat seznam ID požadavků ke zpracování (používáme PROCESS_LIMIT)
|
||||||
|
requests_to_process = get_requests_to_process_from_db(PROCESS_LIMIT)
|
||||||
|
|
||||||
|
if not requests_to_process:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Pro update DB time otevřeme připojení jednou a použijeme ho v cyklu
|
||||||
|
conn = mysql.connector.connect(**DB_CONFIG)
|
||||||
|
|
||||||
|
print("\n=============================================")
|
||||||
|
print(f"START ZPRACOVÁNÍ {len(requests_to_process)} POŽADAVKŮ")
|
||||||
|
print("=============================================\n")
|
||||||
|
|
||||||
|
for idx, request in enumerate(requests_to_process, 1):
|
||||||
|
request_id = request["id"]
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"[{idx}/{len(requests_to_process)}] Zpracovávám požadavek: {request['prijmeni']} {request['jmeno']} (ID: {request_id})")
|
||||||
|
|
||||||
|
# 2. Vytvořit text, který chceme přidat/vytvořit
|
||||||
|
prepend_text = f"ID: {request_id}\n"
|
||||||
|
|
||||||
|
# 3. Pokusit se získat existující interní poznámku z Medevia
|
||||||
|
note = get_internal_note(request_id, token)
|
||||||
|
|
||||||
|
medevio_update_success = False
|
||||||
|
|
||||||
|
if note:
|
||||||
|
# A) POZNÁMKA EXISTUJE -> AKTUALIZOVAT
|
||||||
|
note_id = note["id"]
|
||||||
|
old_content = note["content"] or ""
|
||||||
|
new_content = prepend_text + old_content
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Odeslání aktualizace
|
||||||
|
update_internal_note(note_id, new_content, token)
|
||||||
|
print(f" (Medevio: Poznámka {note_id} **aktualizována**.)")
|
||||||
|
medevio_update_success = True
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
print(f" ❌ Chyba při aktualizaci Medevio API: {e}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
# B) POZNÁMKA NEEXISTUJE -> VYTVOŘIT
|
||||||
|
new_content = prepend_text.strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Odeslání vytvoření
|
||||||
|
result = create_internal_note(request_id, new_content, token)
|
||||||
|
new_note_id = result.get("data", {}).get("createClinicPatientRequestNote", {}).get("id", "N/A")
|
||||||
|
print(f" (Medevio: Nová poznámka {new_note_id} **vytvořena**.)")
|
||||||
|
medevio_update_success = True
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
print(f" ❌ Chyba při vytváření Medevio API: {e}")
|
||||||
|
|
||||||
|
# 4. AKTUALIZACE ČASOVÉHO RAZÍTKA V DB
|
||||||
|
if medevio_update_success:
|
||||||
|
update_db_sync_time(request_id, conn)
|
||||||
|
|
||||||
|
print("---------------------------------------------")
|
||||||
|
|
||||||
|
# Uzavřeme připojení k DB po dokončení cyklu
|
||||||
|
if conn and conn.is_connected():
|
||||||
|
conn.close()
|
||||||
|
print("\n✅ Všechny požadavky zpracovány. Připojení k DB uzavřeno.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
1
30 ManipulacePoznámek/token.txt
Normal file
1
30 ManipulacePoznámek/token.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
nYvrvgflIKcDiQg8Hhpud+qG8iGZ8eH8su4nyT/Mgcm7XQp65ygY9s39+O01wIpk/7sKd6fBHkiKvsqH
|
||||||
@@ -38,7 +38,7 @@ DB_CONFIG = {
|
|||||||
"cursorclass": pymysql.cursors.DictCursor,
|
"cursorclass": pymysql.cursors.DictCursor,
|
||||||
}
|
}
|
||||||
|
|
||||||
EXPORT_DIR = Path(r"u:\Dropbox\Ordinace\Reporty")
|
EXPORT_DIR = Path(r"z:\Dropbox\Ordinace\Reporty")
|
||||||
EXPORT_DIR.mkdir(exist_ok=True, parents=True)
|
EXPORT_DIR.mkdir(exist_ok=True, parents=True)
|
||||||
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||||
xlsx_path = EXPORT_DIR / f"{timestamp} Agenda + Pozadavky (Merged).xlsx"
|
xlsx_path = EXPORT_DIR / f"{timestamp} Agenda + Pozadavky (Merged).xlsx"
|
||||||
1
40 agenda a požadavky/medevio_storage.json
Normal file
1
40 agenda a požadavky/medevio_storage.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"cookies": [{"name": "gateway-access-token", "value": "YwBgkf8McREDKs7vCZj0EZD2fJsuV8RyDPtYx7WiDoz0nFJ9kxId8kcNEPBLFSwM+Tiz80+SOdFwo+oj", "domain": "my.medevio.cz", "path": "/", "expires": 1763372319, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "aws-waf-token", "value": "b6a1d4eb-4350-40e5-8e52-1f5f9600fbb8:CgoAr9pC8c6zAAAA:OYwXLY5OyitSQPl5v2oIlS+hIxsrb5LxV4VjCyE2gJCFFE5PQu+0Zbxse2ZIofrNv5QKs0TYUDTmxPhZyTr9Qtjnq2gsVQxWHXzrbebv3Z7RbzB63u6Ymn3Fo8IbDev3CfCNcNuxCKltFEXLqSCjI2vqNY+7HZkgQBIqy2wMgzli3aSLq0w8lWYtZzyyot7q8RPXWMGTfaBUo2reY0SOSffm9rAivE9PszNfPid71CvNrGAAoxRbwb25eVujlyIcDVWe5vZ9Iw==", "domain": ".my.medevio.cz", "path": "/", "expires": 1761125920, "httpOnly": false, "secure": true, "sameSite": "Lax"}], "origins": [{"origin": "https://my.medevio.cz", "localStorage": [{"name": "awswaf_token_refresh_timestamp", "value": "1760780309860"}, {"name": "awswaf_session_storage", "value": "b6a1d4eb-4350-40e5-8e52-1f5f9600fbb8:CgoAr9pC8c+zAAAA:+vw//1NzmePjPpbGCJzUB+orCRivtJd098DbDX4AnABiGRw/+ql6ShqvFY4YdCY7w2tegb5mEPBdAmc4sNi22kNR9BuEoAgCUiMhkU1AZWfzM51zPfTh7SveCrREZ7xdvxcqKPMmfVLRYX5E4+UWh22z/LKQ7+d9VERp3J+wWCUW3dFFirkezy3N7b2FVjTlY/RxsZwhejQziTG/L3CkIFFP3mOReNgBvDpj7aKoM1knY4IL4TZ8E7zNv3nTsvzACLYvnUutVOUcofN1TfOzwZshSKsEXsMzrQn8PzLccX1jM5VSzce7gfEzl0zSPsT8NB3Sna+rhMIttDNYgvbW1HsfG2LIeKMR27Zf8hkslDRVVkcU/Kp2jLOEdhhrBKGjKY2o9/uX3NExdzh5MEKQSSRtmue01BpWYILPH23rMsz4YSmF+Ough5OeQoC95rkcYwVXMhwvUN9Zfp9UZ4xCNfFUex5dOrg9aJntYRnaceeocGUttNI5AdT0i3+osV6XHXzKxeqO8zLCS9BIsCzxaHfdqqem5DorMceuGKz+QqksatIQAA=="}, {"name": "Application.Intl.locale", "value": "cs"}, {"name": "Password.prefill", "value": "{\"username\":\"vladimir.buzalka@buzalka.cz\",\"type\":\"email\"}"}]}]}
|
||||||
46
50 Různé testy/10 RenameFoldersTriangles.py
Normal file
46
50 Různé testy/10 RenameFoldersTriangles.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Define the target directory
|
||||||
|
target_path = Path(r"U:\Dropbox\Ordinace\Dokumentace_ke_zpracování\MP")
|
||||||
|
|
||||||
|
|
||||||
|
def rename_folders():
|
||||||
|
# Ensure the path exists
|
||||||
|
if not target_path.exists():
|
||||||
|
print(f"Error: The path {target_path} does not exist.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Iterate through items in the directory
|
||||||
|
for folder in target_path.iterdir():
|
||||||
|
# Only process directories
|
||||||
|
if folder.is_dir():
|
||||||
|
original_name = folder.name
|
||||||
|
|
||||||
|
# Check if name starts with the triangle
|
||||||
|
if original_name.startswith("▲"):
|
||||||
|
# 1. Remove the triangle from the start
|
||||||
|
name_without_tri = original_name[1:]
|
||||||
|
|
||||||
|
# 2. Prepare the name to be at least 10 chars long
|
||||||
|
# (so the triangle can sit at index 10 / position 11)
|
||||||
|
clean_name = name_without_tri.ljust(10)
|
||||||
|
|
||||||
|
# 3. Construct new name: first 10 chars + triangle + the rest
|
||||||
|
new_name = clean_name[:10] + "▲" + clean_name[10:]
|
||||||
|
|
||||||
|
# Remove trailing spaces if the original name was short
|
||||||
|
# but you don't want extra spaces at the very end
|
||||||
|
new_name = new_name.rstrip()
|
||||||
|
|
||||||
|
new_folder_path = folder.parent / new_name
|
||||||
|
|
||||||
|
try:
|
||||||
|
print(f"Renaming: '{original_name}' -> '{new_name}'")
|
||||||
|
folder.rename(new_folder_path)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Could not rename {original_name}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
rename_folders()
|
||||||
@@ -96,7 +96,18 @@ def fetch_messages(headers, request_id):
|
|||||||
if r.status_code != 200:
|
if r.status_code != 200:
|
||||||
print("❌ HTTP", r.status_code, "for request", request_id)
|
print("❌ HTTP", r.status_code, "for request", request_id)
|
||||||
return []
|
return []
|
||||||
return r.json().get("data", {}).get("messages", []) or []
|
|
||||||
|
try:
|
||||||
|
data = r.json()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Failed to parse JSON for {request_id}: {e}")
|
||||||
|
print(" Response text:", r.text[:500])
|
||||||
|
return []
|
||||||
|
|
||||||
|
messages = data.get("data", {}).get("messages", []) or []
|
||||||
|
print(f" 🌐 API returned {len(messages)} messages for {request_id}")
|
||||||
|
return messages
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
# ==============================
|
||||||
@@ -218,18 +229,19 @@ def main():
|
|||||||
|
|
||||||
print(f"📦 Already downloaded attachments: {len(existing_ids)}\n")
|
print(f"📦 Already downloaded attachments: {len(existing_ids)}\n")
|
||||||
|
|
||||||
# ---- Select pozadavky needing message sync
|
# ---- Select 10 oldest pozadavky (regardless of messagesProcessed)
|
||||||
sql = """
|
sql = """
|
||||||
SELECT id
|
SELECT id
|
||||||
FROM pozadavky
|
FROM pozadavky
|
||||||
WHERE messagesProcessed IS NULL
|
ORDER BY updatedAt ASC
|
||||||
OR messagesProcessed < updatedAt
|
LIMIT 10
|
||||||
"""
|
"""
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute(sql)
|
cur.execute(sql)
|
||||||
requests_to_process = cur.fetchall()
|
requests_to_process = cur.fetchall()
|
||||||
|
|
||||||
print(f"📋 Found {len(requests_to_process)} pozadavků requiring message sync.\n")
|
print(f"📋 Will process {len(requests_to_process)} oldest pozadavků.\n")
|
||||||
|
|
||||||
|
|
||||||
# ---- Process each pozadavek
|
# ---- Process each pozadavek
|
||||||
for idx, row in enumerate(requests_to_process, 1):
|
for idx, row in enumerate(requests_to_process, 1):
|
||||||
122
Testy/01 Test.py
122
Testy/01 Test.py
@@ -1,122 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
Test: Read conversation messages for one request_id and save full JSON.
|
|
||||||
Uses same logic as the production messages downloader.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import requests
|
|
||||||
from pathlib import Path
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# ⚙️ CONFIGURATION
|
|
||||||
# ==============================
|
|
||||||
TOKEN_PATH = Path("token.txt") # same token file as your working script
|
|
||||||
REQUEST_ID = "092a0c63-28be-4c6b-ab3b-204e1e2641d4" # 🧾 replace as needed
|
|
||||||
OUTPUT_DIR = Path(r"u:\Dropbox\!!!Days\Downloads Z230") # where to save the JSON
|
|
||||||
|
|
||||||
GRAPHQL_QUERY = r"""
|
|
||||||
query UseMessages_ListMessages($requestId: String!, $updatedSince: DateTime) {
|
|
||||||
messages: listMessages(patientRequestId: $requestId, updatedSince: $updatedSince) {
|
|
||||||
id
|
|
||||||
createdAt
|
|
||||||
updatedAt
|
|
||||||
readAt
|
|
||||||
text
|
|
||||||
type
|
|
||||||
sender {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
surname
|
|
||||||
clinicId
|
|
||||||
}
|
|
||||||
medicalRecord {
|
|
||||||
id
|
|
||||||
description
|
|
||||||
contentType
|
|
||||||
url
|
|
||||||
downloadUrl
|
|
||||||
token
|
|
||||||
createdAt
|
|
||||||
updatedAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# 🔑 READ TOKEN
|
|
||||||
# ==============================
|
|
||||||
def read_token(p: Path) -> str:
|
|
||||||
tok = p.read_text(encoding="utf-8").strip()
|
|
||||||
if tok.startswith("Bearer "):
|
|
||||||
tok = tok.split(" ", 1)[1]
|
|
||||||
return tok
|
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# 🚀 FETCH FROM API
|
|
||||||
# ==============================
|
|
||||||
def fetch_messages(headers, request_id):
|
|
||||||
variables = {"requestId": request_id, "updatedSince": None}
|
|
||||||
payload = {
|
|
||||||
"operationName": "UseMessages_ListMessages",
|
|
||||||
"query": GRAPHQL_QUERY,
|
|
||||||
"variables": variables,
|
|
||||||
}
|
|
||||||
|
|
||||||
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30)
|
|
||||||
print("HTTP status:", r.status_code)
|
|
||||||
if r.status_code != 200:
|
|
||||||
print("❌ Response preview:", r.text[:500])
|
|
||||||
return None
|
|
||||||
return r.json()
|
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# 🧠 MAIN
|
|
||||||
# ==============================
|
|
||||||
def main():
|
|
||||||
token = read_token(TOKEN_PATH)
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {token}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Accept": "application/json",
|
|
||||||
}
|
|
||||||
|
|
||||||
data = fetch_messages(headers, REQUEST_ID)
|
|
||||||
if not data:
|
|
||||||
print("⚠️ No data returned.")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Save full JSON to file
|
|
||||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
output_path = OUTPUT_DIR / f"messages_{REQUEST_ID}.json"
|
|
||||||
output_path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
||||||
|
|
||||||
print(f"✅ JSON saved to {output_path}")
|
|
||||||
|
|
||||||
# Optional: print summary
|
|
||||||
messages = data.get("data", {}).get("messages", [])
|
|
||||||
print(f"💬 {len(messages)} messages found:")
|
|
||||||
print("─" * 100)
|
|
||||||
|
|
||||||
for msg in messages:
|
|
||||||
sender = msg.get("sender") or {}
|
|
||||||
sender_name = " ".join(x for x in [sender.get("name"), sender.get("surname")] if x).strip() or "(unknown)"
|
|
||||||
text = (msg.get("text") or "").strip().replace("\n", " ")
|
|
||||||
created = msg.get("createdAt", "")[:16].replace("T", " ")
|
|
||||||
print(f"[{created}] {sender_name}: {text}")
|
|
||||||
if msg.get("medicalRecord"):
|
|
||||||
mr = msg["medicalRecord"]
|
|
||||||
print(f" 📎 {mr.get('description') or '(no description)'} ({mr.get('contentType')})")
|
|
||||||
print(f" URL: {mr.get('downloadUrl') or mr.get('url')}")
|
|
||||||
print("─" * 100)
|
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
296
Testy/02 Test.py
296
Testy/02 Test.py
@@ -1,296 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
Read conversation messages for pozadavky where messagesProcessed IS NULL
|
|
||||||
(Optionally filtered by createdAt), insert them into `medevio_conversation`,
|
|
||||||
and if a message has an attachment (medicalRecord), download it and save into
|
|
||||||
`medevio_downloads` (same logic as your attachments script).
|
|
||||||
Finally, mark pozadavky.messagesProcessed = NOW().
|
|
||||||
"""
|
|
||||||
|
|
||||||
import zlib
|
|
||||||
import json
|
|
||||||
import requests
|
|
||||||
import pymysql
|
|
||||||
from pathlib import Path
|
|
||||||
from datetime import datetime
|
|
||||||
import time
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# 🔧 CONFIGURATION
|
|
||||||
# ==============================
|
|
||||||
TOKEN_PATH = Path("token.txt")
|
|
||||||
DB_CONFIG = {
|
|
||||||
"host": "192.168.1.76",
|
|
||||||
"port": 3307,
|
|
||||||
"user": "root",
|
|
||||||
"password": "Vlado9674+",
|
|
||||||
"database": "medevio",
|
|
||||||
"charset": "utf8mb4",
|
|
||||||
"cursorclass": pymysql.cursors.DictCursor,
|
|
||||||
}
|
|
||||||
|
|
||||||
# ✅ Optional: Only process requests created after this date ("" = no limit)
|
|
||||||
CREATED_AFTER = "2024-12-01"
|
|
||||||
|
|
||||||
GRAPHQL_QUERY_MESSAGES = r"""
|
|
||||||
query UseMessages_ListMessages($requestId: String!, $updatedSince: DateTime) {
|
|
||||||
messages: listMessages(patientRequestId: $requestId, updatedSince: $updatedSince) {
|
|
||||||
id
|
|
||||||
createdAt
|
|
||||||
updatedAt
|
|
||||||
readAt
|
|
||||||
text
|
|
||||||
type
|
|
||||||
sender {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
surname
|
|
||||||
clinicId
|
|
||||||
}
|
|
||||||
medicalRecord {
|
|
||||||
id
|
|
||||||
description
|
|
||||||
contentType
|
|
||||||
url
|
|
||||||
downloadUrl
|
|
||||||
token
|
|
||||||
createdAt
|
|
||||||
updatedAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# 🧮 HELPERS
|
|
||||||
# ==============================
|
|
||||||
def short_crc8(uuid_str: str) -> str:
|
|
||||||
return f"{zlib.crc32(uuid_str.encode('utf-8')) & 0xffffffff:08x}"
|
|
||||||
|
|
||||||
def extract_filename_from_url(url: str) -> str:
|
|
||||||
try:
|
|
||||||
return url.split("/")[-1].split("?")[0]
|
|
||||||
except Exception:
|
|
||||||
return "unknown_filename"
|
|
||||||
|
|
||||||
def read_token(p: Path) -> str:
|
|
||||||
tok = p.read_text(encoding="utf-8").strip()
|
|
||||||
if tok.startswith("Bearer "):
|
|
||||||
tok = tok.split(" ", 1)[1]
|
|
||||||
return tok
|
|
||||||
|
|
||||||
def parse_dt(s):
|
|
||||||
if not s:
|
|
||||||
return None
|
|
||||||
# handle both "YYYY-mm-ddTHH:MM:SS" and "YYYY-mm-dd HH:MM:SS"
|
|
||||||
s = s.replace("T", " ")
|
|
||||||
fmts = ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M")
|
|
||||||
for f in fmts:
|
|
||||||
try:
|
|
||||||
return datetime.strptime(s[:19], f)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# 📡 FETCH MESSAGES
|
|
||||||
# ==============================
|
|
||||||
def fetch_messages(headers, request_id):
|
|
||||||
variables = {"requestId": request_id, "updatedSince": None}
|
|
||||||
payload = {
|
|
||||||
"operationName": "UseMessages_ListMessages",
|
|
||||||
"query": GRAPHQL_QUERY_MESSAGES,
|
|
||||||
"variables": variables,
|
|
||||||
}
|
|
||||||
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30)
|
|
||||||
if r.status_code != 200:
|
|
||||||
print(f"❌ HTTP {r.status_code} for messages of request {request_id}")
|
|
||||||
return []
|
|
||||||
data = r.json().get("data", {}).get("messages", [])
|
|
||||||
return data or []
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# 💾 SAVE: conversation row
|
|
||||||
# ==============================
|
|
||||||
def insert_message(cur, req_id, msg):
|
|
||||||
sender = msg.get("sender") or {}
|
|
||||||
sender_name = " ".join(x for x in [sender.get("name"), sender.get("surname")] if x).strip() or None
|
|
||||||
sender_id = sender.get("id")
|
|
||||||
sender_clinic_id = sender.get("clinicId")
|
|
||||||
|
|
||||||
text = msg.get("text")
|
|
||||||
created_at = parse_dt(msg.get("createdAt"))
|
|
||||||
read_at = parse_dt(msg.get("readAt"))
|
|
||||||
updated_at = parse_dt(msg.get("updatedAt"))
|
|
||||||
|
|
||||||
mr = msg.get("medicalRecord") or {}
|
|
||||||
attachment_url = mr.get("downloadUrl") or mr.get("url")
|
|
||||||
attachment_description = mr.get("description")
|
|
||||||
attachment_content_type = mr.get("contentType")
|
|
||||||
|
|
||||||
sql = """
|
|
||||||
INSERT INTO medevio_conversation (
|
|
||||||
id, request_id, sender_name, sender_id, sender_clinic_id,
|
|
||||||
text, created_at, read_at, updated_at,
|
|
||||||
attachment_url, attachment_description, attachment_content_type
|
|
||||||
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
|
||||||
ON DUPLICATE KEY UPDATE
|
|
||||||
sender_name = VALUES(sender_name),
|
|
||||||
sender_id = VALUES(sender_id),
|
|
||||||
sender_clinic_id = VALUES(sender_clinic_id),
|
|
||||||
text = VALUES(text),
|
|
||||||
created_at = VALUES(created_at),
|
|
||||||
read_at = VALUES(read_at),
|
|
||||||
updated_at = VALUES(updated_at),
|
|
||||||
attachment_url = VALUES(attachment_url),
|
|
||||||
attachment_description = VALUES(attachment_description),
|
|
||||||
attachment_content_type = VALUES(attachment_content_type)
|
|
||||||
"""
|
|
||||||
cur.execute(sql, (
|
|
||||||
msg.get("id"),
|
|
||||||
req_id,
|
|
||||||
sender_name,
|
|
||||||
sender_id,
|
|
||||||
sender_clinic_id,
|
|
||||||
text,
|
|
||||||
created_at,
|
|
||||||
read_at,
|
|
||||||
updated_at,
|
|
||||||
attachment_url,
|
|
||||||
attachment_description,
|
|
||||||
attachment_content_type
|
|
||||||
))
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# 💾 SAVE: download attachment (from message)
|
|
||||||
# ==============================
|
|
||||||
def insert_download_from_message(cur, req_id, msg, existing_ids):
|
|
||||||
mr = msg.get("medicalRecord") or {}
|
|
||||||
attachment_id = mr.get("id")
|
|
||||||
if not attachment_id:
|
|
||||||
return False
|
|
||||||
if attachment_id in existing_ids:
|
|
||||||
print(f" ⏭️ Skipping already downloaded message-attachment {attachment_id}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
url = mr.get("downloadUrl") or mr.get("url")
|
|
||||||
if not url:
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
r = requests.get(url, timeout=30)
|
|
||||||
r.raise_for_status()
|
|
||||||
content = r.content
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ⚠️ Failed to download message attachment {attachment_id}: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
filename = extract_filename_from_url(url)
|
|
||||||
content_type = mr.get("contentType")
|
|
||||||
file_size = len(content)
|
|
||||||
created_date = parse_dt(msg.get("createdAt"))
|
|
||||||
|
|
||||||
# We don't have patient names on the message level here; keep NULLs.
|
|
||||||
cur.execute("""
|
|
||||||
INSERT INTO medevio_downloads (
|
|
||||||
request_id, attachment_id, attachment_type, filename,
|
|
||||||
content_type, file_size, pacient_jmeno, pacient_prijmeni,
|
|
||||||
created_at, file_content
|
|
||||||
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
|
||||||
ON DUPLICATE KEY UPDATE
|
|
||||||
file_content = VALUES(file_content),
|
|
||||||
file_size = VALUES(file_size),
|
|
||||||
downloaded_at = NOW()
|
|
||||||
""", (
|
|
||||||
req_id,
|
|
||||||
attachment_id,
|
|
||||||
"MESSAGE_ATTACHMENT",
|
|
||||||
filename,
|
|
||||||
content_type,
|
|
||||||
file_size,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
created_date,
|
|
||||||
content
|
|
||||||
))
|
|
||||||
existing_ids.add(attachment_id)
|
|
||||||
print(f" 💾 Saved msg attachment {filename} ({file_size/1024:.1f} kB)")
|
|
||||||
return True
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# 🧠 MAIN
|
|
||||||
# ==============================
|
|
||||||
def main():
|
|
||||||
token = read_token(TOKEN_PATH)
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {token}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Accept": "application/json",
|
|
||||||
}
|
|
||||||
|
|
||||||
conn = pymysql.connect(**DB_CONFIG)
|
|
||||||
|
|
||||||
# Load existing download IDs to skip duplicates (same logic as your script)
|
|
||||||
print("📦 Loading list of already downloaded attachments...")
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.execute("SELECT attachment_id FROM medevio_downloads")
|
|
||||||
existing_ids = {row["attachment_id"] for row in cur.fetchall()}
|
|
||||||
print(f"✅ Found {len(existing_ids)} attachments already saved.")
|
|
||||||
|
|
||||||
# Pull pozadavky where messagesProcessed IS NULL (optionally by createdAt)
|
|
||||||
sql = """
|
|
||||||
SELECT id, displayTitle, pacient_prijmeni, pacient_jmeno, createdAt
|
|
||||||
FROM pozadavky
|
|
||||||
WHERE messagesProcessed IS NULL
|
|
||||||
"""
|
|
||||||
params = []
|
|
||||||
if CREATED_AFTER:
|
|
||||||
sql += " AND createdAt >= %s"
|
|
||||||
params.append(CREATED_AFTER)
|
|
||||||
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.execute(sql, params)
|
|
||||||
rows = cur.fetchall()
|
|
||||||
|
|
||||||
print(f"📋 Found {len(rows)} pozadavky to process (messagesProcessed IS NULL"
|
|
||||||
+ (f", created >= {CREATED_AFTER}" if CREATED_AFTER else "") + ")")
|
|
||||||
|
|
||||||
for i, row in enumerate(rows, 1):
|
|
||||||
req_id = row["id"]
|
|
||||||
prijmeni = row.get("pacient_prijmeni") or "Neznamy"
|
|
||||||
jmeno = row.get("pacient_jmeno") or ""
|
|
||||||
print(f"\n[{i}/{len(rows)}] 💬 {prijmeni}, {jmeno} ({req_id})")
|
|
||||||
|
|
||||||
messages = fetch_messages(headers, req_id)
|
|
||||||
if not messages:
|
|
||||||
print(" ⚠️ No messages found")
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.execute("UPDATE pozadavky SET messagesProcessed = NOW() WHERE id = %s", (req_id,))
|
|
||||||
conn.commit()
|
|
||||||
continue
|
|
||||||
|
|
||||||
inserted = 0
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
for msg in messages:
|
|
||||||
insert_message(cur, req_id, msg)
|
|
||||||
# also pull any message attachments into downloads table
|
|
||||||
insert_download_from_message(cur, req_id, msg, existing_ids)
|
|
||||||
inserted += 1
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
# mark processed
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.execute("UPDATE pozadavky SET messagesProcessed = NOW() WHERE id = %s", (req_id,))
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
print(f" ✅ {inserted} messages processed for {prijmeni}, {jmeno}")
|
|
||||||
time.sleep(0.3) # polite API delay
|
|
||||||
|
|
||||||
conn.close()
|
|
||||||
print("\n✅ Done! All new conversations processed and pozadavky updated.")
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
105
Testy/03 Test.py
105
Testy/03 Test.py
@@ -1,105 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import os
|
|
||||||
import zlib
|
|
||||||
import pymysql
|
|
||||||
import re
|
|
||||||
from pathlib import Path
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# ⚙️ CONFIGURATION
|
|
||||||
# ==============================
|
|
||||||
DB_CONFIG = {
|
|
||||||
"host": "192.168.1.76",
|
|
||||||
"port": 3307,
|
|
||||||
"user": "root",
|
|
||||||
"password": "Vlado9674+",
|
|
||||||
"database": "medevio",
|
|
||||||
"charset": "utf8mb4",
|
|
||||||
}
|
|
||||||
|
|
||||||
BASE_DIR = Path(r"u:\Dropbox\Ordinace\Dokumentace_ke_zpracování\MP1")
|
|
||||||
BASE_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
|
|
||||||
def sanitize_name(name: str) -> str:
|
|
||||||
"""Replace invalid filename characters with underscore."""
|
|
||||||
return re.sub(r'[<>:"/\\|?*\x00-\x1F]', "_", name).strip()
|
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# 📦 STREAMING EXPORT WITH TRIANGLE CHECK
|
|
||||||
# ==============================
|
|
||||||
conn = pymysql.connect(**DB_CONFIG)
|
|
||||||
cur_meta = conn.cursor(pymysql.cursors.DictCursor)
|
|
||||||
cur_blob = conn.cursor()
|
|
||||||
|
|
||||||
cur_meta.execute("""
|
|
||||||
SELECT id, request_id, attachment_id, filename, pacient_jmeno,
|
|
||||||
pacient_prijmeni, created_at, downloaded_at
|
|
||||||
FROM medevio_downloads
|
|
||||||
WHERE file_content IS NOT NULL;
|
|
||||||
""")
|
|
||||||
|
|
||||||
rows = cur_meta.fetchall()
|
|
||||||
print(f"📋 Found {len(rows)} records to check/export")
|
|
||||||
|
|
||||||
skipped, exported = 0, 0
|
|
||||||
|
|
||||||
for r in rows:
|
|
||||||
try:
|
|
||||||
created = r["created_at"] or r["downloaded_at"] or datetime.now()
|
|
||||||
date_str = created.strftime("%Y-%m-%d")
|
|
||||||
|
|
||||||
prijmeni = sanitize_name(r["pacient_prijmeni"] or "Unknown")
|
|
||||||
jmeno = sanitize_name(r["pacient_jmeno"] or "")
|
|
||||||
|
|
||||||
# 🔥 NEW: use full request_id instead of CRC32
|
|
||||||
full_req_id = sanitize_name(r["request_id"])
|
|
||||||
|
|
||||||
# Base (non-triangle) and processed (triangle) folder variants
|
|
||||||
base_folder = f"{date_str} {prijmeni}, {jmeno} {full_req_id}"
|
|
||||||
tri_folder = f"{date_str}▲ {prijmeni}, {jmeno} {full_req_id}"
|
|
||||||
|
|
||||||
base_folder = sanitize_name(base_folder)
|
|
||||||
tri_folder = sanitize_name(tri_folder)
|
|
||||||
|
|
||||||
base_path = BASE_DIR / base_folder
|
|
||||||
tri_path = BASE_DIR / tri_folder
|
|
||||||
|
|
||||||
filename = sanitize_name(r["filename"] or f"unknown_{r['id']}.bin")
|
|
||||||
file_path_base = base_path / filename
|
|
||||||
file_path_tri = tri_path / filename
|
|
||||||
|
|
||||||
# 🟡 Skip if exists in either version
|
|
||||||
if file_path_base.exists() or file_path_tri.exists():
|
|
||||||
skipped += 1
|
|
||||||
found_in = "▲" if file_path_tri.exists() else ""
|
|
||||||
print(f"⏭️ Skipping existing{found_in}: {filename}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Make sure base folder exists before saving
|
|
||||||
base_path.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
# 2️⃣ Fetch blob
|
|
||||||
cur_blob.execute("SELECT file_content FROM medevio_downloads WHERE id = %s", (r["id"],))
|
|
||||||
blob = cur_blob.fetchone()[0]
|
|
||||||
|
|
||||||
if blob:
|
|
||||||
with open(file_path_base, "wb") as f:
|
|
||||||
f.write(blob)
|
|
||||||
exported += 1
|
|
||||||
print(f"✅ Saved: {file_path_base.relative_to(BASE_DIR)}")
|
|
||||||
else:
|
|
||||||
print(f"⚠️ No content for id={r['id']}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Error for id={r['id']}: {e}")
|
|
||||||
|
|
||||||
cur_blob.close()
|
|
||||||
cur_meta.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
print(f"\n🎯 Export complete — {exported} new files saved, {skipped} skipped.\n")
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import json
|
|
||||||
import requests
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# ============================
|
|
||||||
# CONFIG
|
|
||||||
# ============================
|
|
||||||
TOKEN_PATH = Path("token.txt")
|
|
||||||
|
|
||||||
# vlož libovolný existující request ID
|
|
||||||
REQUEST_ID = "3fc9b28c-ada2-4d21-ab2d-fe60ad29fd8f"
|
|
||||||
|
|
||||||
GRAPHQL_NOTES_QUERY = r"""
|
|
||||||
query ClinicRequestNotes_Get($patientRequestId: String!) {
|
|
||||||
notes: getClinicPatientRequestNotes(requestId: $patientRequestId) {
|
|
||||||
id
|
|
||||||
content
|
|
||||||
createdAt
|
|
||||||
updatedAt
|
|
||||||
createdBy {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
surname
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
# ============================
|
|
||||||
# TOKEN
|
|
||||||
# ============================
|
|
||||||
def read_token(p: Path) -> str:
|
|
||||||
tok = p.read_text(encoding="utf-8").strip()
|
|
||||||
if tok.startswith("Bearer "):
|
|
||||||
tok = tok.split(" ", 1)[1]
|
|
||||||
return tok
|
|
||||||
|
|
||||||
# ============================
|
|
||||||
# FETCH
|
|
||||||
# ============================
|
|
||||||
def fetch_notes(request_id, token):
|
|
||||||
url = "https://api.medevio.cz/graphql"
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {token}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Accept": "application/json",
|
|
||||||
}
|
|
||||||
|
|
||||||
variables = {"patientRequestId": request_id}
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"operationName": "ClinicRequestNotes_Get",
|
|
||||||
"query": GRAPHQL_NOTES_QUERY,
|
|
||||||
"variables": variables,
|
|
||||||
}
|
|
||||||
|
|
||||||
r = requests.post(url, json=payload, headers=headers)
|
|
||||||
r.raise_for_status()
|
|
||||||
return r.json()
|
|
||||||
|
|
||||||
# ============================
|
|
||||||
# MAIN
|
|
||||||
# ============================
|
|
||||||
def main():
|
|
||||||
token = read_token(TOKEN_PATH)
|
|
||||||
|
|
||||||
print(f"\n🔍 Fetching NOTES for request:\n ID = {REQUEST_ID}\n")
|
|
||||||
|
|
||||||
data = fetch_notes(REQUEST_ID, token)
|
|
||||||
|
|
||||||
print("📄 FULL RAW JSON:\n")
|
|
||||||
print(json.dumps(data, indent=2, ensure_ascii=False))
|
|
||||||
|
|
||||||
print("\n📝 Parsed notes:\n")
|
|
||||||
notes = data.get("data", {}).get("notes") or []
|
|
||||||
if not notes:
|
|
||||||
print(" (no notes found)")
|
|
||||||
return
|
|
||||||
|
|
||||||
for n in notes:
|
|
||||||
author = n.get("createdBy")
|
|
||||||
print(f"--- Note {n.get('id')} ---")
|
|
||||||
print(f"Created: {n.get('createdAt')}")
|
|
||||||
print(f"Updated: {n.get('updatedAt')}")
|
|
||||||
if author:
|
|
||||||
print(f"Author: {author.get('name')} {author.get('surname')}")
|
|
||||||
print("Content:")
|
|
||||||
print(n.get("content"))
|
|
||||||
print()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
216
Testy/04 Test.py
216
Testy/04 Test.py
@@ -1,216 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
Download all attachments for pozadavky where attachmentsProcessed IS NULL
|
|
||||||
and (optionally) createdAt is newer than a cutoff date.
|
|
||||||
Store them in MySQL table `medevio_downloads`, and update pozadavky.attachmentsProcessed.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import zlib
|
|
||||||
import json
|
|
||||||
import requests
|
|
||||||
import pymysql
|
|
||||||
from pathlib import Path
|
|
||||||
from datetime import datetime
|
|
||||||
import time
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# 🔧 CONFIGURATION
|
|
||||||
# ==============================
|
|
||||||
TOKEN_PATH = Path("token.txt")
|
|
||||||
CLINIC_SLUG = "mudr-buzalkova"
|
|
||||||
|
|
||||||
DB_CONFIG = {
|
|
||||||
"host": "192.168.1.76",
|
|
||||||
"port": 3307,
|
|
||||||
"user": "root",
|
|
||||||
"password": "Vlado9674+",
|
|
||||||
"database": "medevio",
|
|
||||||
"charset": "utf8mb4",
|
|
||||||
"cursorclass": pymysql.cursors.DictCursor,
|
|
||||||
}
|
|
||||||
|
|
||||||
CREATED_AFTER = "2024-12-01" # optional filter
|
|
||||||
|
|
||||||
GRAPHQL_QUERY = r"""
|
|
||||||
query ClinicRequestDetail_GetPatientRequest2($requestId: UUID!) {
|
|
||||||
patientRequestMedicalRecords: listMedicalRecordsForPatientRequest(
|
|
||||||
attachmentTypes: [ECRF_FILL_ATTACHMENT, MESSAGE_ATTACHMENT, PATIENT_REQUEST_ATTACHMENT]
|
|
||||||
patientRequestId: $requestId
|
|
||||||
pageInfo: {first: 100, offset: 0}
|
|
||||||
) {
|
|
||||||
attachmentType
|
|
||||||
id
|
|
||||||
medicalRecord {
|
|
||||||
contentType
|
|
||||||
description
|
|
||||||
downloadUrl
|
|
||||||
id
|
|
||||||
url
|
|
||||||
visibleToPatient
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# 🧮 HELPERS
|
|
||||||
# ==============================
|
|
||||||
def extract_filename_from_url(url: str) -> str:
|
|
||||||
try:
|
|
||||||
return url.split("/")[-1].split("?")[0]
|
|
||||||
except:
|
|
||||||
return "unknown_filename"
|
|
||||||
|
|
||||||
def read_token(p: Path) -> str:
|
|
||||||
tok = p.read_text(encoding="utf-8").strip()
|
|
||||||
return tok.split(" ", 1)[1] if tok.startswith("Bearer ") else tok
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# 📡 FETCH ATTACHMENTS
|
|
||||||
# ==============================
|
|
||||||
def fetch_attachments(headers, request_id):
|
|
||||||
payload = {
|
|
||||||
"operationName": "ClinicRequestDetail_GetPatientRequest2",
|
|
||||||
"query": GRAPHQL_QUERY,
|
|
||||||
"variables": {"requestId": request_id},
|
|
||||||
}
|
|
||||||
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30)
|
|
||||||
if r.status_code != 200:
|
|
||||||
print(f"❌ HTTP {r.status_code} for request {request_id}")
|
|
||||||
return []
|
|
||||||
return r.json().get("data", {}).get("patientRequestMedicalRecords", [])
|
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# 💾 SAVE TO MYSQL (clean version)
|
|
||||||
# ==============================
|
|
||||||
def insert_download(cur, req_id, a, m, created_date, existing_ids):
|
|
||||||
|
|
||||||
attachment_id = a.get("id")
|
|
||||||
if attachment_id in existing_ids:
|
|
||||||
print(f" ⏭️ Already downloaded {attachment_id}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
url = m.get("downloadUrl")
|
|
||||||
if not url:
|
|
||||||
print(" ⚠️ Missing download URL")
|
|
||||||
return False
|
|
||||||
|
|
||||||
filename = extract_filename_from_url(url)
|
|
||||||
|
|
||||||
# Download file
|
|
||||||
try:
|
|
||||||
r = requests.get(url, timeout=30)
|
|
||||||
r.raise_for_status()
|
|
||||||
content = r.content
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ⚠️ Download failed {url}: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
file_size = len(content)
|
|
||||||
attachment_type = a.get("attachmentType")
|
|
||||||
content_type = m.get("contentType")
|
|
||||||
|
|
||||||
# 🚨 CLEAN INSERT — no patient_jmeno/no patient_prijmeni
|
|
||||||
cur.execute("""
|
|
||||||
INSERT INTO medevio_downloads (
|
|
||||||
request_id, attachment_id, attachment_type,
|
|
||||||
filename, content_type, file_size,
|
|
||||||
created_at, file_content
|
|
||||||
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s)
|
|
||||||
ON DUPLICATE KEY UPDATE
|
|
||||||
file_content = VALUES(file_content),
|
|
||||||
file_size = VALUES(file_size),
|
|
||||||
downloaded_at = NOW()
|
|
||||||
""", (
|
|
||||||
req_id,
|
|
||||||
attachment_id,
|
|
||||||
attachment_type,
|
|
||||||
filename,
|
|
||||||
content_type,
|
|
||||||
file_size,
|
|
||||||
created_date,
|
|
||||||
content,
|
|
||||||
))
|
|
||||||
|
|
||||||
existing_ids.add(attachment_id)
|
|
||||||
print(f" 💾 Saved {filename} ({file_size/1024:.1f} kB)")
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# 🧠 MAIN
|
|
||||||
# ==============================
|
|
||||||
def main():
|
|
||||||
token = read_token(TOKEN_PATH)
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {token}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
}
|
|
||||||
|
|
||||||
conn = pymysql.connect(**DB_CONFIG)
|
|
||||||
|
|
||||||
# Load existing IDs
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.execute("SELECT attachment_id FROM medevio_downloads")
|
|
||||||
existing_ids = {row["attachment_id"] for row in cur.fetchall()}
|
|
||||||
print(f"✅ {len(existing_ids)} attachments already saved.")
|
|
||||||
|
|
||||||
# Build query for pozadavky
|
|
||||||
sql = """
|
|
||||||
SELECT id, pacient_prijmeni, pacient_jmeno, createdAt
|
|
||||||
FROM pozadavky
|
|
||||||
WHERE attachmentsProcessed IS NULL
|
|
||||||
"""
|
|
||||||
params = []
|
|
||||||
if CREATED_AFTER:
|
|
||||||
sql += " AND createdAt >= %s"
|
|
||||||
params.append(CREATED_AFTER)
|
|
||||||
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.execute(sql, params)
|
|
||||||
req_rows = cur.fetchall()
|
|
||||||
|
|
||||||
print(f"📋 Found {len(req_rows)} pozadavky to process.")
|
|
||||||
|
|
||||||
# Process each pozadavek
|
|
||||||
for i, row in enumerate(req_rows, 1):
|
|
||||||
req_id = row["id"]
|
|
||||||
prijmeni = row.get("pacient_prijmeni") or "Neznamy"
|
|
||||||
jmeno = row.get("pacient_jmeno") or ""
|
|
||||||
created_date = row.get("createdAt") or datetime.now()
|
|
||||||
|
|
||||||
print(f"\n[{i}/{len(req_rows)}] 🧾 {prijmeni}, {jmeno} ({req_id})")
|
|
||||||
|
|
||||||
attachments = fetch_attachments(headers, req_id)
|
|
||||||
|
|
||||||
if not attachments:
|
|
||||||
print(" ⚠️ No attachments found")
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.execute("UPDATE pozadavky SET attachmentsProcessed = NOW() WHERE id = %s", (req_id,))
|
|
||||||
conn.commit()
|
|
||||||
continue
|
|
||||||
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
for a in attachments:
|
|
||||||
m = a.get("medicalRecord") or {}
|
|
||||||
insert_download(cur, req_id, a, m, created_date, existing_ids)
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
# Mark processed
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.execute("UPDATE pozadavky SET attachmentsProcessed = NOW() WHERE id = %s", (req_id,))
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
print(f" ✅ Done ({len(attachments)} attachments)")
|
|
||||||
|
|
||||||
time.sleep(0.3)
|
|
||||||
|
|
||||||
conn.close()
|
|
||||||
print("\n🎯 All attachments processed.")
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import json
|
|
||||||
import requests
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
TOKEN_PATH = Path("token.txt")
|
|
||||||
CLINIC_SLUG = "mudr-buzalkova"
|
|
||||||
|
|
||||||
GRAPHQL_URL = "https://api.medevio.cz/graphql" # ← správná URL
|
|
||||||
|
|
||||||
# 👉 nastav offset zde
|
|
||||||
OFFSET = 0 # 0 = první pacient, 1 = druhý, 100 = 101. pacient
|
|
||||||
FIRST = 1 # načteme jen jednoho
|
|
||||||
|
|
||||||
QUERY = """
|
|
||||||
query PatientGridImpl_ListClinicPatients(
|
|
||||||
$clinicSlug: String!,
|
|
||||||
$filter: ListPatientFilter!,
|
|
||||||
$pageInfo: PageInfo!,
|
|
||||||
$sort: [ListPatientsSort!]
|
|
||||||
) {
|
|
||||||
patientsList: listPatients(
|
|
||||||
clinicSlug: $clinicSlug
|
|
||||||
filter: $filter
|
|
||||||
pageInfo: $pageInfo
|
|
||||||
sort: $sort
|
|
||||||
) {
|
|
||||||
count
|
|
||||||
patients {
|
|
||||||
id
|
|
||||||
identificationNumber
|
|
||||||
insuranceCompanyObject {
|
|
||||||
id
|
|
||||||
shortName
|
|
||||||
}
|
|
||||||
lastReservation
|
|
||||||
locale
|
|
||||||
name
|
|
||||||
nextReservation
|
|
||||||
key
|
|
||||||
phone
|
|
||||||
sex
|
|
||||||
status2
|
|
||||||
surname
|
|
||||||
type
|
|
||||||
kind
|
|
||||||
isInClinic
|
|
||||||
isUnknownPatient
|
|
||||||
user {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
surname
|
|
||||||
phone
|
|
||||||
registrationCompletedTime
|
|
||||||
}
|
|
||||||
owner {
|
|
||||||
name
|
|
||||||
surname
|
|
||||||
}
|
|
||||||
clinics {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
slug
|
|
||||||
}
|
|
||||||
tags(onlyImportant: false) {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
color
|
|
||||||
icon
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
variables = {
|
|
||||||
"clinicSlug": CLINIC_SLUG,
|
|
||||||
"filter": {},
|
|
||||||
"pageInfo": {
|
|
||||||
"first": FIRST,
|
|
||||||
"offset": OFFSET
|
|
||||||
},
|
|
||||||
"sort": [
|
|
||||||
{
|
|
||||||
"field": "ReverseFullName",
|
|
||||||
"sort": "ASC"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
token = Path("token.txt").read_text().strip()
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {token}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Accept": "application/json",
|
|
||||||
}
|
|
||||||
|
|
||||||
print("⏳ Fetching patient...")
|
|
||||||
response = requests.post(
|
|
||||||
GRAPHQL_URL,
|
|
||||||
json={"query": QUERY, "variables": variables},
|
|
||||||
headers=headers
|
|
||||||
)
|
|
||||||
|
|
||||||
response.raise_for_status()
|
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
print("\n📌 RAW JSON RESPONSE:\n")
|
|
||||||
print(json.dumps(data, indent=2, ensure_ascii=False))
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import json
|
|
||||||
import requests
|
|
||||||
import pymysql
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# 🔧 KONFIGURACE
|
|
||||||
# ==============================
|
|
||||||
TOKEN_PATH = Path("token.txt")
|
|
||||||
CLINIC_SLUG = "mudr-buzalkova"
|
|
||||||
GRAPHQL_URL = "https://api.medevio.cz/graphql"
|
|
||||||
|
|
||||||
DB_CONFIG = {
|
|
||||||
"host": "192.168.1.76",
|
|
||||||
"port": 3307,
|
|
||||||
"user": "root",
|
|
||||||
"password": "Vlado9674+",
|
|
||||||
"database": "medevio",
|
|
||||||
"charset": "utf8mb4",
|
|
||||||
"cursorclass": pymysql.cursors.DictCursor,
|
|
||||||
}
|
|
||||||
|
|
||||||
# počet pacientů na stránku
|
|
||||||
PAGE_SIZE = 100
|
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# 📌 GRAPHQL QUERY
|
|
||||||
# ==============================
|
|
||||||
QUERY = """
|
|
||||||
query PatientGridImpl_ListClinicPatients(
|
|
||||||
$clinicSlug: String!,
|
|
||||||
$filter: ListPatientFilter!,
|
|
||||||
$pageInfo: PageInfo!,
|
|
||||||
$sort: [ListPatientsSort!]
|
|
||||||
) {
|
|
||||||
patientsList: listPatients(
|
|
||||||
clinicSlug: $clinicSlug
|
|
||||||
filter: $filter
|
|
||||||
pageInfo: $pageInfo
|
|
||||||
sort: $sort
|
|
||||||
) {
|
|
||||||
count
|
|
||||||
patients {
|
|
||||||
id
|
|
||||||
identificationNumber
|
|
||||||
insuranceCompanyObject {
|
|
||||||
id
|
|
||||||
shortName
|
|
||||||
}
|
|
||||||
name
|
|
||||||
surname
|
|
||||||
phone
|
|
||||||
sex
|
|
||||||
status2
|
|
||||||
type
|
|
||||||
kind
|
|
||||||
isInClinic
|
|
||||||
isUnknownPatient
|
|
||||||
user {
|
|
||||||
registrationCompletedTime
|
|
||||||
}
|
|
||||||
clinics {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
slug
|
|
||||||
}
|
|
||||||
tags(onlyImportant: false) {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
color
|
|
||||||
icon
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
def normalize_dt(dt_str):
|
|
||||||
if not dt_str:
|
|
||||||
return None
|
|
||||||
# remove Z
|
|
||||||
dt_str = dt_str.replace("Z", "")
|
|
||||||
# replace T with space
|
|
||||||
dt_str = dt_str.replace("T", " ")
|
|
||||||
# remove fractional seconds
|
|
||||||
if "." in dt_str:
|
|
||||||
dt_str = dt_str.split(".")[0]
|
|
||||||
return dt_str # MySQL can accept "YYYY-MM-DD HH:MM:SS"
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# 💾 ULOŽENÍ PACIENTA
|
|
||||||
# ==============================
|
|
||||||
def save_patient_summary(cur, p):
|
|
||||||
sql = """
|
|
||||||
REPLACE INTO medevio_pacienti (
|
|
||||||
id, jmeno, prijmeni, rodne_cislo, telefon, pohlavi,
|
|
||||||
pojistovna_id, pojistovna_nazev,
|
|
||||||
status, typ, kind,
|
|
||||||
is_in_clinic, is_unknown,
|
|
||||||
registration_time,
|
|
||||||
tags_json, clinics_json,
|
|
||||||
last_update
|
|
||||||
)
|
|
||||||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,NOW())
|
|
||||||
"""
|
|
||||||
|
|
||||||
ins = p.get("insuranceCompanyObject") or {}
|
|
||||||
user = p.get("user") or {}
|
|
||||||
|
|
||||||
cur.execute(sql, (
|
|
||||||
p.get("id"),
|
|
||||||
p.get("name"),
|
|
||||||
p.get("surname"),
|
|
||||||
p.get("identificationNumber"),
|
|
||||||
p.get("phone"),
|
|
||||||
p.get("sex"),
|
|
||||||
ins.get("id"),
|
|
||||||
ins.get("shortName"),
|
|
||||||
p.get("status2"),
|
|
||||||
p.get("type"),
|
|
||||||
p.get("kind"),
|
|
||||||
1 if p.get("isInClinic") else 0,
|
|
||||||
1 if p.get("isUnknownPatient") else 0,
|
|
||||||
normalize_dt(user.get("registrationCompletedTime")),
|
|
||||||
json.dumps(p.get("tags"), ensure_ascii=False),
|
|
||||||
json.dumps(p.get("clinics"), ensure_ascii=False),
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# 🧠 MAIN
|
|
||||||
# ==============================
|
|
||||||
def main():
|
|
||||||
token = TOKEN_PATH.read_text().strip()
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {token}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Accept": "application/json",
|
|
||||||
}
|
|
||||||
|
|
||||||
conn = pymysql.connect(**DB_CONFIG)
|
|
||||||
cur = conn.cursor()
|
|
||||||
|
|
||||||
offset = 0
|
|
||||||
total = None
|
|
||||||
|
|
||||||
print("⏳ Starting patient sync...\n")
|
|
||||||
|
|
||||||
while True:
|
|
||||||
print(f"➡️ Fetching patients {offset}–{offset + PAGE_SIZE} ...")
|
|
||||||
|
|
||||||
variables = {
|
|
||||||
"clinicSlug": CLINIC_SLUG,
|
|
||||||
"filter": {},
|
|
||||||
"pageInfo": {"first": PAGE_SIZE, "offset": offset},
|
|
||||||
"sort": [{"field": "ReverseFullName", "sort": "ASC"}],
|
|
||||||
}
|
|
||||||
|
|
||||||
response = requests.post(
|
|
||||||
GRAPHQL_URL,
|
|
||||||
json={"query": QUERY, "variables": variables},
|
|
||||||
headers=headers,
|
|
||||||
timeout=30
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
block = data["data"]["patientsList"]
|
|
||||||
if total is None:
|
|
||||||
total = block["count"]
|
|
||||||
print(f"📌 Total patients: {total}\n")
|
|
||||||
|
|
||||||
patients = block["patients"]
|
|
||||||
|
|
||||||
if not patients:
|
|
||||||
print("✅ No more patients. Finished.")
|
|
||||||
break
|
|
||||||
|
|
||||||
# save each patient
|
|
||||||
for p in patients:
|
|
||||||
save_patient_summary(cur, p)
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
print(f" ✓ Saved {len(patients)} patients.")
|
|
||||||
|
|
||||||
offset += PAGE_SIZE
|
|
||||||
if offset >= total:
|
|
||||||
print("\n✅ All patients downloaded.")
|
|
||||||
break
|
|
||||||
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import json
|
|
||||||
import requests
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
TOKEN_PATH = Path("token.txt")
|
|
||||||
GRAPHQL_URL = "https://api.medevio.cz/graphql"
|
|
||||||
CLINIC_SLUG = "mudr-buzalkova"
|
|
||||||
|
|
||||||
PATIENT_ID = "5a0a9ff0-bbe8-4fc7-a27d-b7b475ee2189"
|
|
||||||
|
|
||||||
QUERY = """
|
|
||||||
query ClinicPatientDetailModal_GetData(
|
|
||||||
$clinicSlug: String!,
|
|
||||||
$patientId: String!,
|
|
||||||
$patientUuid: UUID!,
|
|
||||||
$challengesStatus: ECRFChallengeStatus!,
|
|
||||||
$locale: Locale!
|
|
||||||
) {
|
|
||||||
clinic: getClinic(clinicSlug: $clinicSlug) {
|
|
||||||
id
|
|
||||||
features
|
|
||||||
sslSUKLCertificateId
|
|
||||||
type
|
|
||||||
ais
|
|
||||||
slug
|
|
||||||
...ClinicWithTypeAndFeatures_Clinic
|
|
||||||
...PatientInfo_Clinic
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
patient: getPatientForClinic(clinicSlug: $clinicSlug, patientId: $patientId) {
|
|
||||||
...ClinicPatientDetailModal_Patient
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
challenges: listPatientChallenges2(
|
|
||||||
clinicSlug: $clinicSlug
|
|
||||||
patientId: $patientId
|
|
||||||
status: $challengesStatus
|
|
||||||
) {
|
|
||||||
...ChallengeTableList_EcrfChallenge
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
patientRequestsResponse: filterPatientRequestsForClinic(
|
|
||||||
clinicSlug: $clinicSlug
|
|
||||||
filter: {patientId: $patientUuid}
|
|
||||||
pageInfo: {first: 1, offset: 0}
|
|
||||||
) {
|
|
||||||
count
|
|
||||||
items { id __typename }
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
treatmentPlanPatients: listTreatmentPlanPatients(
|
|
||||||
clinicSlug: $clinicSlug
|
|
||||||
patientId: $patientUuid
|
|
||||||
) {
|
|
||||||
...ClinicPlanPatientList_PlanPatient
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
premiumPlans: listClinicPremiumPlans(clinicSlug: $clinicSlug) {
|
|
||||||
id
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
mergeSuggestions: findMergeSuggestions(
|
|
||||||
clinicSlug: $clinicSlug
|
|
||||||
input: {existingPatientId: $patientUuid}
|
|
||||||
) {
|
|
||||||
...MergeSuggestionAlert_MergeSuggestionResult
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
insuranceCards: getPatientDocuments(
|
|
||||||
patientId: $patientUuid
|
|
||||||
type: InsuranceCard
|
|
||||||
) {
|
|
||||||
...PatientInfo_InsuranceCard
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment ClinicWithTypeAndFeatures_Clinic on Clinic {
|
|
||||||
id
|
|
||||||
type
|
|
||||||
features
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment PatientInfo_Clinic on Clinic {
|
|
||||||
country
|
|
||||||
id
|
|
||||||
slug
|
|
||||||
ais
|
|
||||||
...ClinicWithTypeAndFeatures_Clinic
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment ClinicPatientDetailModal_Patient on ExtendedPatient {
|
|
||||||
id
|
|
||||||
isInClinic
|
|
||||||
kind
|
|
||||||
name
|
|
||||||
isUnknownPatient
|
|
||||||
sex
|
|
||||||
surname
|
|
||||||
identificationNumber
|
|
||||||
editableByDoctor
|
|
||||||
type
|
|
||||||
key
|
|
||||||
user { id name surname __typename }
|
|
||||||
...ClinicPatientDetail_Patient
|
|
||||||
...PatientInfo_AccountPatient
|
|
||||||
...ClinicPatientInfo_Patient
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment ClinicPatientDetail_Patient on ExtendedPatient {
|
|
||||||
name
|
|
||||||
surname
|
|
||||||
email
|
|
||||||
id
|
|
||||||
identificationNumber
|
|
||||||
isInClinic
|
|
||||||
key
|
|
||||||
phone
|
|
||||||
sex
|
|
||||||
type
|
|
||||||
dob
|
|
||||||
user { id __typename }
|
|
||||||
isUnknownPatient
|
|
||||||
hasMobileApp
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment PatientInfo_AccountPatient on ExtendedPatient {
|
|
||||||
id
|
|
||||||
createdAt
|
|
||||||
key
|
|
||||||
user {
|
|
||||||
registrationCompletedTime
|
|
||||||
deactivatedTime
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment ClinicPatientInfo_Patient on ExtendedPatient {
|
|
||||||
anamnesisShared
|
|
||||||
anamnesisStatusForClinic { updatedAt __typename }
|
|
||||||
clinics { id name slug __typename }
|
|
||||||
id
|
|
||||||
isInClinic
|
|
||||||
dob
|
|
||||||
city
|
|
||||||
familyMembers: family { __typename }
|
|
||||||
houseNumber
|
|
||||||
identificationNumber
|
|
||||||
insuranceCompanyObject { id code name shortName __typename }
|
|
||||||
kind
|
|
||||||
name
|
|
||||||
note
|
|
||||||
owner { name surname __typename }
|
|
||||||
key
|
|
||||||
status
|
|
||||||
street
|
|
||||||
surname
|
|
||||||
user { id email name phone surname __typename }
|
|
||||||
userRelationship
|
|
||||||
premiumPlanPatient { id __typename }
|
|
||||||
sex
|
|
||||||
tags(onlyImportant: false) { id name color icon __typename }
|
|
||||||
type
|
|
||||||
isUnknownPatient
|
|
||||||
hasMobileApp
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment ChallengeTableList_EcrfChallenge on ECRFChallenge {
|
|
||||||
id
|
|
||||||
createdAt
|
|
||||||
sentAt
|
|
||||||
issuedToPatient {
|
|
||||||
id
|
|
||||||
identificationNumber
|
|
||||||
name
|
|
||||||
surname
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
userECRF(locale: $locale) { id name __typename }
|
|
||||||
patientRequestId
|
|
||||||
status
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment MergeSuggestionAlert_MergeSuggestionResult on MergeSuggestionResult {
|
|
||||||
extendedPatient { id __typename }
|
|
||||||
matchResult
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment ClinicPlanPatientList_PlanPatient on TreatmentPlanPatient {
|
|
||||||
id
|
|
||||||
createdAt
|
|
||||||
listPatient { id identificationNumber name key status surname __typename }
|
|
||||||
treatmentPlan { id slug name __typename }
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment PatientInfo_InsuranceCard on PatientDocument {
|
|
||||||
id
|
|
||||||
contentType
|
|
||||||
url
|
|
||||||
downloadUrl
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
token = TOKEN_PATH.read_text().strip()
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {token}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Accept": "application/json",
|
|
||||||
}
|
|
||||||
|
|
||||||
variables = {
|
|
||||||
"clinicSlug": CLINIC_SLUG,
|
|
||||||
"patientId": PATIENT_ID,
|
|
||||||
"patientUuid": PATIENT_ID,
|
|
||||||
"challengesStatus": "SENT",
|
|
||||||
"locale": "cs",
|
|
||||||
}
|
|
||||||
|
|
||||||
print("⏳ Fetching patient detail…")
|
|
||||||
|
|
||||||
r = requests.post(
|
|
||||||
GRAPHQL_URL,
|
|
||||||
json={"query": QUERY, "variables": variables},
|
|
||||||
headers=headers,
|
|
||||||
timeout=30
|
|
||||||
)
|
|
||||||
|
|
||||||
r.raise_for_status()
|
|
||||||
data = r.json()
|
|
||||||
|
|
||||||
print("\n📌 RAW DETAIL JSON:\n")
|
|
||||||
print(json.dumps(data, indent=2, ensure_ascii=False))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,365 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import json
|
|
||||||
import requests
|
|
||||||
import pymysql
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# CONFIG
|
|
||||||
# ==============================
|
|
||||||
TOKEN_PATH = Path("token.txt")
|
|
||||||
GRAPHQL_URL = "https://api.medevio.cz/graphql"
|
|
||||||
CLINIC_SLUG = "mudr-buzalkova"
|
|
||||||
|
|
||||||
DB_CONFIG = {
|
|
||||||
"host": "192.168.1.76",
|
|
||||||
"port": 3307,
|
|
||||||
"user": "root",
|
|
||||||
"password": "Vlado9674+",
|
|
||||||
"database": "medevio",
|
|
||||||
"charset": "utf8mb4",
|
|
||||||
"cursorclass": pymysql.cursors.DictCursor,
|
|
||||||
}
|
|
||||||
|
|
||||||
PATIENT_ID = "5a0a9ff0-bbe8-4fc7-a27d-b7b475ee2189"
|
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# HELPERS
|
|
||||||
# ==============================
|
|
||||||
def normalize_dt(dt_str):
|
|
||||||
if not dt_str:
|
|
||||||
return None
|
|
||||||
dt_str = dt_str.replace("Z", "").replace("T", " ")
|
|
||||||
if "." in dt_str:
|
|
||||||
dt_str = dt_str.split(".")[0]
|
|
||||||
return dt_str
|
|
||||||
|
|
||||||
|
|
||||||
def save_patient_detail(cur, p):
|
|
||||||
user = p.get("user") or {}
|
|
||||||
ins = p.get("insuranceCompanyObject") or {}
|
|
||||||
tags = p.get("tags") or []
|
|
||||||
clinics = p.get("clinics") or []
|
|
||||||
|
|
||||||
sql = """
|
|
||||||
UPDATE medevio_pacienti SET
|
|
||||||
email = %s,
|
|
||||||
telefon = %s,
|
|
||||||
dob = %s,
|
|
||||||
street = %s,
|
|
||||||
house_number = %s,
|
|
||||||
city = %s,
|
|
||||||
|
|
||||||
user_id = %s,
|
|
||||||
user_email = %s,
|
|
||||||
user_name = %s,
|
|
||||||
user_surname = %s,
|
|
||||||
user_phone = %s,
|
|
||||||
user_reg_time = %s,
|
|
||||||
user_deactivated_time = %s,
|
|
||||||
|
|
||||||
created_at = %s,
|
|
||||||
note = %s,
|
|
||||||
has_mobile_app = %s,
|
|
||||||
user_relationship = %s,
|
|
||||||
|
|
||||||
pojistovna_code = %s,
|
|
||||||
tags_json = %s,
|
|
||||||
clinics_json = %s,
|
|
||||||
last_update = NOW()
|
|
||||||
|
|
||||||
WHERE id = %s
|
|
||||||
"""
|
|
||||||
|
|
||||||
cur.execute(sql, (
|
|
||||||
p.get("email"),
|
|
||||||
p.get("phone"),
|
|
||||||
p.get("dob"),
|
|
||||||
|
|
||||||
p.get("street"),
|
|
||||||
p.get("houseNumber"),
|
|
||||||
p.get("city"),
|
|
||||||
|
|
||||||
user.get("id"),
|
|
||||||
user.get("email"),
|
|
||||||
user.get("name"),
|
|
||||||
user.get("surname"),
|
|
||||||
user.get("phone"),
|
|
||||||
normalize_dt(user.get("registrationCompletedTime")),
|
|
||||||
normalize_dt(user.get("deactivatedTime")),
|
|
||||||
|
|
||||||
normalize_dt(p.get("createdAt")),
|
|
||||||
p.get("note"),
|
|
||||||
1 if p.get("hasMobileApp") else 0,
|
|
||||||
p.get("userRelationship"),
|
|
||||||
|
|
||||||
ins.get("code"),
|
|
||||||
json.dumps(tags, ensure_ascii=False),
|
|
||||||
json.dumps(clinics, ensure_ascii=False),
|
|
||||||
|
|
||||||
p.get("id")
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# FULL EXACT WORKING GRAPHQL QUERY
|
|
||||||
# ==============================
|
|
||||||
QUERY = """
|
|
||||||
query ClinicPatientDetailModal_GetData(
|
|
||||||
$clinicSlug: String!,
|
|
||||||
$patientId: String!,
|
|
||||||
$patientUuid: UUID!,
|
|
||||||
$challengesStatus: ECRFChallengeStatus!,
|
|
||||||
$locale: Locale!
|
|
||||||
) {
|
|
||||||
clinic: getClinic(clinicSlug: $clinicSlug) {
|
|
||||||
id
|
|
||||||
features
|
|
||||||
sslSUKLCertificateId
|
|
||||||
type
|
|
||||||
ais
|
|
||||||
slug
|
|
||||||
...ClinicWithTypeAndFeatures_Clinic
|
|
||||||
...PatientInfo_Clinic
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
patient: getPatientForClinic(clinicSlug: $clinicSlug, patientId: $patientId) {
|
|
||||||
...ClinicPatientDetailModal_Patient
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
challenges: listPatientChallenges2(
|
|
||||||
clinicSlug: $clinicSlug
|
|
||||||
patientId: $patientId
|
|
||||||
status: $challengesStatus
|
|
||||||
) {
|
|
||||||
...ChallengeTableList_EcrfChallenge
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
patientRequestsResponse: filterPatientRequestsForClinic(
|
|
||||||
clinicSlug: $clinicSlug
|
|
||||||
filter: {patientId: $patientUuid}
|
|
||||||
pageInfo: {first: 1, offset: 0}
|
|
||||||
) {
|
|
||||||
count
|
|
||||||
items { id __typename }
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
treatmentPlanPatients: listTreatmentPlanPatients(
|
|
||||||
clinicSlug: $clinicSlug
|
|
||||||
patientId: $patientUuid
|
|
||||||
) {
|
|
||||||
...ClinicPlanPatientList_PlanPatient
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
premiumPlans: listClinicPremiumPlans(clinicSlug: $clinicSlug) {
|
|
||||||
id
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
mergeSuggestions: findMergeSuggestions(
|
|
||||||
clinicSlug: $clinicSlug
|
|
||||||
input: {existingPatientId: $patientUuid}
|
|
||||||
) {
|
|
||||||
...MergeSuggestionAlert_MergeSuggestionResult
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
insuranceCards: getPatientDocuments(
|
|
||||||
patientId: $patientUuid
|
|
||||||
type: InsuranceCard
|
|
||||||
) {
|
|
||||||
...PatientInfo_InsuranceCard
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment ClinicWithTypeAndFeatures_Clinic on Clinic {
|
|
||||||
id
|
|
||||||
type
|
|
||||||
features
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment PatientInfo_Clinic on Clinic {
|
|
||||||
country
|
|
||||||
id
|
|
||||||
slug
|
|
||||||
ais
|
|
||||||
...ClinicWithTypeAndFeatures_Clinic
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment ClinicPatientDetailModal_Patient on ExtendedPatient {
|
|
||||||
id
|
|
||||||
isInClinic
|
|
||||||
kind
|
|
||||||
name
|
|
||||||
isUnknownPatient
|
|
||||||
sex
|
|
||||||
surname
|
|
||||||
identificationNumber
|
|
||||||
editableByDoctor
|
|
||||||
type
|
|
||||||
key
|
|
||||||
user { id name surname __typename }
|
|
||||||
...ClinicPatientDetail_Patient
|
|
||||||
...PatientInfo_AccountPatient
|
|
||||||
...ClinicPatientInfo_Patient
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment ClinicPatientDetail_Patient on ExtendedPatient {
|
|
||||||
name
|
|
||||||
surname
|
|
||||||
email
|
|
||||||
id
|
|
||||||
identificationNumber
|
|
||||||
isInClinic
|
|
||||||
key
|
|
||||||
phone
|
|
||||||
sex
|
|
||||||
type
|
|
||||||
dob
|
|
||||||
user { id __typename }
|
|
||||||
isUnknownPatient
|
|
||||||
hasMobileApp
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment PatientInfo_AccountPatient on ExtendedPatient {
|
|
||||||
id
|
|
||||||
createdAt
|
|
||||||
key
|
|
||||||
user {
|
|
||||||
registrationCompletedTime
|
|
||||||
deactivatedTime
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment ClinicPatientInfo_Patient on ExtendedPatient {
|
|
||||||
anamnesisShared
|
|
||||||
anamnesisStatusForClinic { updatedAt __typename }
|
|
||||||
clinics { id name slug __typename }
|
|
||||||
id
|
|
||||||
isInClinic
|
|
||||||
dob
|
|
||||||
city
|
|
||||||
familyMembers: family { __typename }
|
|
||||||
houseNumber
|
|
||||||
identificationNumber
|
|
||||||
insuranceCompanyObject { id code name shortName __typename }
|
|
||||||
kind
|
|
||||||
name
|
|
||||||
note
|
|
||||||
owner { name surname __typename }
|
|
||||||
key
|
|
||||||
status
|
|
||||||
street
|
|
||||||
surname
|
|
||||||
user { id email name phone surname __typename }
|
|
||||||
userRelationship
|
|
||||||
premiumPlanPatient { id __typename }
|
|
||||||
sex
|
|
||||||
tags(onlyImportant: false) { id name color icon __typename }
|
|
||||||
type
|
|
||||||
isUnknownPatient
|
|
||||||
hasMobileApp
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment ChallengeTableList_EcrfChallenge on ECRFChallenge {
|
|
||||||
id
|
|
||||||
createdAt
|
|
||||||
sentAt
|
|
||||||
issuedToPatient {
|
|
||||||
id
|
|
||||||
identificationNumber
|
|
||||||
name
|
|
||||||
surname
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
userECRF(locale: $locale) { id name __typename }
|
|
||||||
patientRequestId
|
|
||||||
status
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment MergeSuggestionAlert_MergeSuggestionResult on MergeSuggestionResult {
|
|
||||||
extendedPatient { id __typename }
|
|
||||||
matchResult
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment ClinicPlanPatientList_PlanPatient on TreatmentPlanPatient {
|
|
||||||
id
|
|
||||||
createdAt
|
|
||||||
listPatient { id identificationNumber name key status surname __typename }
|
|
||||||
treatmentPlan { id slug name __typename }
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment PatientInfo_InsuranceCard on PatientDocument {
|
|
||||||
id
|
|
||||||
contentType
|
|
||||||
url
|
|
||||||
downloadUrl
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# MAIN
|
|
||||||
# ==============================
|
|
||||||
def main():
|
|
||||||
token = TOKEN_PATH.read_text().strip()
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {token}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Accept": "application/json",
|
|
||||||
}
|
|
||||||
|
|
||||||
variables = {
|
|
||||||
"clinicSlug": CLINIC_SLUG,
|
|
||||||
"patientId": PATIENT_ID,
|
|
||||||
"patientUuid": PATIENT_ID,
|
|
||||||
"challengesStatus": "SENT",
|
|
||||||
"locale": "cs",
|
|
||||||
}
|
|
||||||
|
|
||||||
print(f"⏳ Fetching patient detail {PATIENT_ID}…")
|
|
||||||
|
|
||||||
r = requests.post(
|
|
||||||
GRAPHQL_URL,
|
|
||||||
json={"query": QUERY, "variables": variables},
|
|
||||||
headers=headers,
|
|
||||||
timeout=30
|
|
||||||
)
|
|
||||||
r.raise_for_status()
|
|
||||||
data = r.json()
|
|
||||||
|
|
||||||
patient = data["data"]["patient"]
|
|
||||||
if not patient:
|
|
||||||
print("❌ Patient not found in API response!")
|
|
||||||
return
|
|
||||||
|
|
||||||
print("📥 Patient detail downloaded.")
|
|
||||||
|
|
||||||
conn = pymysql.connect(**DB_CONFIG)
|
|
||||||
cur = conn.cursor()
|
|
||||||
|
|
||||||
save_patient_detail(cur, patient)
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
print("✅ Patient detail saved to DB.")
|
|
||||||
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import random
|
|
||||||
import requests
|
|
||||||
import pymysql
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
# Patients with details_updated_at older than this date will be refreshed
|
|
||||||
# UpdateOlderThan = datetime(2025, 2, 20) # example date
|
|
||||||
UpdateOlderThan = None #když chceš všechny aktualizovat
|
|
||||||
|
|
||||||
TOKEN_PATH = Path("token.txt")
|
|
||||||
GRAPHQL_URL = "https://api.medevio.cz/graphql"
|
|
||||||
CLINIC_SLUG = "mudr-buzalkova"
|
|
||||||
|
|
||||||
DB_CONFIG = {
|
|
||||||
"host": "192.168.1.76",
|
|
||||||
"port": 3307,
|
|
||||||
"user": "root",
|
|
||||||
"password": "Vlado9674+",
|
|
||||||
"database": "medevio",
|
|
||||||
"charset": "utf8mb4",
|
|
||||||
"cursorclass": pymysql.cursors.DictCursor,
|
|
||||||
}
|
|
||||||
|
|
||||||
def normalize_dt(dt_str):
|
|
||||||
if not dt_str:
|
|
||||||
return None
|
|
||||||
dt_str = dt_str.replace("Z", "").replace("T", " ")
|
|
||||||
if "." in dt_str:
|
|
||||||
dt_str = dt_str.split(".")[0]
|
|
||||||
return dt_str
|
|
||||||
|
|
||||||
def save_patient_detail(cur, p):
|
|
||||||
user = p.get("user") or {}
|
|
||||||
ins = p.get("insuranceCompanyObject") or {}
|
|
||||||
tags = p.get("tags") or []
|
|
||||||
clinics = p.get("clinics") or []
|
|
||||||
|
|
||||||
sql = """
|
|
||||||
UPDATE medevio_pacienti SET
|
|
||||||
email = %s,
|
|
||||||
telefon = %s,
|
|
||||||
dob = %s,
|
|
||||||
street = %s,
|
|
||||||
house_number = %s,
|
|
||||||
city = %s,
|
|
||||||
|
|
||||||
user_id = %s,
|
|
||||||
user_email = %s,
|
|
||||||
user_name = %s,
|
|
||||||
user_surname = %s,
|
|
||||||
user_phone = %s,
|
|
||||||
user_reg_time = %s,
|
|
||||||
user_deactivated_time = %s,
|
|
||||||
|
|
||||||
created_at = %s,
|
|
||||||
note = %s,
|
|
||||||
has_mobile_app = %s,
|
|
||||||
user_relationship = %s,
|
|
||||||
|
|
||||||
pojistovna_code = %s,
|
|
||||||
tags_json = %s,
|
|
||||||
clinics_json = %s,
|
|
||||||
last_update = NOW(),
|
|
||||||
details_updated_at = NOW()
|
|
||||||
|
|
||||||
WHERE id = %s
|
|
||||||
"""
|
|
||||||
|
|
||||||
cur.execute(sql, (
|
|
||||||
p.get("email"),
|
|
||||||
p.get("phone"),
|
|
||||||
p.get("dob"),
|
|
||||||
|
|
||||||
p.get("street"),
|
|
||||||
p.get("houseNumber"),
|
|
||||||
p.get("city"),
|
|
||||||
|
|
||||||
user.get("id"),
|
|
||||||
user.get("email"),
|
|
||||||
user.get("name"),
|
|
||||||
user.get("surname"),
|
|
||||||
user.get("phone"),
|
|
||||||
normalize_dt(user.get("registrationCompletedTime")),
|
|
||||||
normalize_dt(user.get("deactivatedTime")),
|
|
||||||
|
|
||||||
normalize_dt(p.get("createdAt")),
|
|
||||||
p.get("note"),
|
|
||||||
1 if p.get("hasMobileApp") else 0,
|
|
||||||
p.get("userRelationship"),
|
|
||||||
|
|
||||||
ins.get("code"),
|
|
||||||
json.dumps(tags, ensure_ascii=False),
|
|
||||||
json.dumps(clinics, ensure_ascii=False),
|
|
||||||
|
|
||||||
p.get("id")
|
|
||||||
))
|
|
||||||
|
|
||||||
# === PLACEHOLDER: Insert your full GraphQL query here ===
|
|
||||||
QUERY = Path("patient_detail.graphql").read_text(encoding="utf-8")
|
|
||||||
|
|
||||||
def main():
|
|
||||||
token = TOKEN_PATH.read_text().strip()
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {token}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Accept": "application/json",
|
|
||||||
}
|
|
||||||
|
|
||||||
conn = pymysql.connect(**DB_CONFIG)
|
|
||||||
cur = conn.cursor()
|
|
||||||
|
|
||||||
if UpdateOlderThan:
|
|
||||||
print(f"🔎 Updating patients with details_updated_at NULL or < {UpdateOlderThan}")
|
|
||||||
cur.execute("""
|
|
||||||
SELECT id, prijmeni, jmeno, details_updated_at
|
|
||||||
FROM medevio_pacienti
|
|
||||||
WHERE details_updated_at IS NULL OR details_updated_at < %s
|
|
||||||
ORDER BY prijmeni, jmeno
|
|
||||||
""", (UpdateOlderThan,))
|
|
||||||
else:
|
|
||||||
print("🔎 Updating only patients with details_updated_at NULL")
|
|
||||||
cur.execute("""
|
|
||||||
SELECT id, prijmeni, jmeno, details_updated_at
|
|
||||||
FROM medevio_pacienti
|
|
||||||
WHERE details_updated_at IS NULL
|
|
||||||
ORDER BY prijmeni, jmeno
|
|
||||||
""")
|
|
||||||
patients = cur.fetchall()
|
|
||||||
|
|
||||||
total = len(patients)
|
|
||||||
print(f"⏳ Starting full patient detail sync for {total} patients...")
|
|
||||||
|
|
||||||
for idx, row in enumerate(patients, start=1):
|
|
||||||
pid = row["id"]
|
|
||||||
name = f"{row.get('prijmeni','')}, {row.get('jmeno','')}"
|
|
||||||
print(f"[{idx}/{total}] Updating: {name} ({pid})")
|
|
||||||
|
|
||||||
variables = {
|
|
||||||
"clinicSlug": CLINIC_SLUG,
|
|
||||||
"patientId": pid,
|
|
||||||
"patientUuid": pid,
|
|
||||||
"challengesStatus": "SENT",
|
|
||||||
"locale": "cs",
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
r = requests.post(
|
|
||||||
GRAPHQL_URL,
|
|
||||||
json={"query": QUERY, "variables": variables},
|
|
||||||
headers=headers,
|
|
||||||
timeout=30
|
|
||||||
)
|
|
||||||
r.raise_for_status()
|
|
||||||
js = r.json()
|
|
||||||
|
|
||||||
p = js["data"]["patient"]
|
|
||||||
if p:
|
|
||||||
save_patient_detail(cur, p)
|
|
||||||
conn.commit()
|
|
||||||
print(" ✔ saved")
|
|
||||||
else:
|
|
||||||
print(" ⚠ no patient data returned")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ❌ ERROR: {e}")
|
|
||||||
time.sleep(2)
|
|
||||||
continue
|
|
||||||
|
|
||||||
time.sleep(random.uniform(0.5, 1.5))
|
|
||||||
|
|
||||||
conn.close()
|
|
||||||
print("✅ DONE – full detail sync completed.")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import json
|
|
||||||
import requests
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
TOKEN_PATH = Path("token.txt")
|
|
||||||
GRAPHQL_URL = "https://api.medevio.cz/graphql"
|
|
||||||
CLINIC_SLUG = "mudr-buzalkova"
|
|
||||||
|
|
||||||
QUERY = r"""
|
|
||||||
query ClinicLegacyRequestList_ListPatientRequestsForClinic(
|
|
||||||
$clinicSlug: String!,
|
|
||||||
$queueId: String,
|
|
||||||
$queueAssignment: QueueAssignmentFilter!,
|
|
||||||
$state: PatientRequestState,
|
|
||||||
$pageInfo: PageInfo!,
|
|
||||||
$locale: Locale!
|
|
||||||
) {
|
|
||||||
requests: listPatientRequestsForClinic(
|
|
||||||
clinicSlug: $clinicSlug
|
|
||||||
queueId: $queueId
|
|
||||||
queueAssignment: $queueAssignment
|
|
||||||
state: $state
|
|
||||||
pageInfo: $pageInfo
|
|
||||||
) {
|
|
||||||
id
|
|
||||||
displayTitle(locale: $locale)
|
|
||||||
createdAt
|
|
||||||
doneAt
|
|
||||||
removedAt
|
|
||||||
extendedPatient {
|
|
||||||
name
|
|
||||||
surname
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
def main():
|
|
||||||
token = TOKEN_PATH.read_text().strip()
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {token}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Accept": "application/json",
|
|
||||||
}
|
|
||||||
|
|
||||||
variables = {
|
|
||||||
"clinicSlug": CLINIC_SLUG,
|
|
||||||
"queueId": None,
|
|
||||||
"queueAssignment": "ANY",
|
|
||||||
"state": "ACTIVE",
|
|
||||||
"pageInfo": {"first": 200, "offset": 100},
|
|
||||||
"locale": "cs",
|
|
||||||
}
|
|
||||||
|
|
||||||
print("⏳ Testing ACTIVE request fetch (LEGACY API)…")
|
|
||||||
|
|
||||||
r = requests.post(
|
|
||||||
GRAPHQL_URL,
|
|
||||||
json={"query": QUERY, "variables": variables},
|
|
||||||
headers=headers,
|
|
||||||
timeout=30
|
|
||||||
)
|
|
||||||
r.raise_for_status()
|
|
||||||
|
|
||||||
js = r.json()
|
|
||||||
|
|
||||||
# extract list
|
|
||||||
requests_list = js.get("data", {}).get("requests", [])
|
|
||||||
|
|
||||||
print("\n📌 Number of ACTIVE requests returned:", len(requests_list))
|
|
||||||
|
|
||||||
print("\n📌 First 5 request IDs:")
|
|
||||||
for item in requests_list[:5]:
|
|
||||||
print(" •", item.get("id"))
|
|
||||||
|
|
||||||
# debug dump if needed
|
|
||||||
# print(json.dumps(js, indent=2, ensure_ascii=False))
|
|
||||||
|
|
||||||
print("\n✅ Test completed.\n")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import pymysql
|
|
||||||
import requests
|
|
||||||
from pathlib import Path
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
# ================================
|
|
||||||
# 🔧 CONFIGURATION
|
|
||||||
# ================================
|
|
||||||
TOKEN_PATH = Path("token.txt")
|
|
||||||
CLINIC_SLUG = "mudr-buzalkova"
|
|
||||||
|
|
||||||
LIMIT = 300 # download the latest 300 requests
|
|
||||||
|
|
||||||
DB_CONFIG = {
|
|
||||||
"host": "192.168.1.76",
|
|
||||||
"port": 3307,
|
|
||||||
"user": "root",
|
|
||||||
"password": "Vlado9674+",
|
|
||||||
"database": "medevio",
|
|
||||||
"charset": "utf8mb4",
|
|
||||||
"cursorclass": pymysql.cursors.DictCursor,
|
|
||||||
}
|
|
||||||
|
|
||||||
GRAPHQL_QUERY = r"""
|
|
||||||
query ClinicRequestGrid_ListPatientRequestsForClinic2(
|
|
||||||
$clinicSlug: String!,
|
|
||||||
$queueId: String,
|
|
||||||
$queueAssignment: QueueAssignmentFilter!,
|
|
||||||
$pageInfo: PageInfo!,
|
|
||||||
$locale: Locale!,
|
|
||||||
$state: PatientRequestState
|
|
||||||
) {
|
|
||||||
requestsResponse: listPatientRequestsForClinic2(
|
|
||||||
clinicSlug: $clinicSlug,
|
|
||||||
queueId: $queueId,
|
|
||||||
queueAssignment: $queueAssignment,
|
|
||||||
pageInfo: $pageInfo,
|
|
||||||
state: $state
|
|
||||||
) {
|
|
||||||
count
|
|
||||||
patientRequests {
|
|
||||||
id
|
|
||||||
displayTitle(locale: $locale)
|
|
||||||
createdAt
|
|
||||||
updatedAt
|
|
||||||
doneAt
|
|
||||||
removedAt
|
|
||||||
extendedPatient {
|
|
||||||
name
|
|
||||||
surname
|
|
||||||
identificationNumber
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
# ================================
|
|
||||||
# TOKEN
|
|
||||||
# ================================
|
|
||||||
def read_token(p: Path) -> str:
|
|
||||||
tok = p.read_text(encoding="utf-8").strip()
|
|
||||||
if tok.startswith("Bearer "):
|
|
||||||
return tok.split(" ", 1)[1]
|
|
||||||
return tok
|
|
||||||
|
|
||||||
# ================================
|
|
||||||
# DATE PARSER
|
|
||||||
# ================================
|
|
||||||
def to_mysql_dt(iso_str):
|
|
||||||
if not iso_str:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
dt = datetime.fromisoformat(iso_str.replace("Z", "+00:00"))
|
|
||||||
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# ================================
|
|
||||||
# UPSERT
|
|
||||||
# ================================
|
|
||||||
def upsert(conn, r):
|
|
||||||
p = (r.get("extendedPatient") or {})
|
|
||||||
sql = """
|
|
||||||
INSERT INTO pozadavky (
|
|
||||||
id, displayTitle, createdAt, updatedAt, doneAt, removedAt,
|
|
||||||
pacient_jmeno, pacient_prijmeni, pacient_rodnecislo
|
|
||||||
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
|
||||||
ON DUPLICATE KEY UPDATE
|
|
||||||
displayTitle=VALUES(displayTitle),
|
|
||||||
updatedAt=VALUES(updatedAt),
|
|
||||||
doneAt=VALUES(doneAt),
|
|
||||||
removedAt=VALUES(removedAt),
|
|
||||||
pacient_jmeno=VALUES(pacient_jmeno),
|
|
||||||
pacient_prijmeni=VALUES(pacient_prijmeni),
|
|
||||||
pacient_rodnecislo=VALUES(pacient_rodnecislo)
|
|
||||||
"""
|
|
||||||
vals = (
|
|
||||||
r.get("id"),
|
|
||||||
r.get("displayTitle"),
|
|
||||||
to_mysql_dt(r.get("createdAt")),
|
|
||||||
to_mysql_dt(r.get("updatedAt")),
|
|
||||||
to_mysql_dt(r.get("doneAt")),
|
|
||||||
to_mysql_dt(r.get("removedAt")),
|
|
||||||
p.get("name"),
|
|
||||||
p.get("surname"),
|
|
||||||
p.get("identificationNumber"),
|
|
||||||
)
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.execute(sql, vals)
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
# ================================
|
|
||||||
# FETCH LATEST 300 REQUESTS
|
|
||||||
# ================================
|
|
||||||
def fetch_latest_requests(headers):
|
|
||||||
vars = {
|
|
||||||
"clinicSlug": CLINIC_SLUG,
|
|
||||||
"queueId": None,
|
|
||||||
"queueAssignment": "ANY",
|
|
||||||
"pageInfo": {"first": LIMIT, "offset": 0},
|
|
||||||
"locale": "cs",
|
|
||||||
"state": "DONE" # ALL STATES (ACTIVE, DONE, REMOVED)
|
|
||||||
}
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"operationName": "ClinicRequestGrid_ListPatientRequestsForClinic2",
|
|
||||||
"query": GRAPHQL_QUERY,
|
|
||||||
"variables": vars,
|
|
||||||
}
|
|
||||||
|
|
||||||
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers)
|
|
||||||
r.raise_for_status()
|
|
||||||
|
|
||||||
data = r.json()["data"]["requestsResponse"]
|
|
||||||
return data.get("patientRequests", [])
|
|
||||||
|
|
||||||
# ================================
|
|
||||||
# MAIN
|
|
||||||
# ================================
|
|
||||||
def main():
|
|
||||||
token = read_token(TOKEN_PATH)
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {token}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Accept": "application/json",
|
|
||||||
}
|
|
||||||
|
|
||||||
conn = pymysql.connect(**DB_CONFIG)
|
|
||||||
|
|
||||||
print(f"\n=== Downloading last {LIMIT} requests @ {datetime.now():%Y-%m-%d %H:%M:%S} ===")
|
|
||||||
|
|
||||||
requests_list = fetch_latest_requests(headers)
|
|
||||||
|
|
||||||
print(f"📌 Requests returned: {len(requests_list)}")
|
|
||||||
|
|
||||||
for r in requests_list:
|
|
||||||
upsert(conn, r)
|
|
||||||
|
|
||||||
conn.close()
|
|
||||||
print("\n✅ Done. Latest requests synced.\n")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import pymysql
|
|
||||||
import openpyxl
|
|
||||||
from openpyxl.styles import PatternFill, Font
|
|
||||||
from openpyxl.utils import get_column_letter
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
timestamp = datetime.now().strftime("%Y-%m-%d %H-%M-%S")
|
|
||||||
OUTPUT_PATH = fr"U:\Dropbox\!!!Days\Downloads Z230\{timestamp} medevio_patients_report.xlsx"
|
|
||||||
|
|
||||||
# ============================
|
|
||||||
# CONFIGURATION
|
|
||||||
# ============================
|
|
||||||
DB_CONFIG = {
|
|
||||||
"host": "192.168.1.76",
|
|
||||||
"port": 3307,
|
|
||||||
"user": "root",
|
|
||||||
"password": "Vlado9674+",
|
|
||||||
"database": "medevio",
|
|
||||||
"charset": "utf8mb4",
|
|
||||||
"cursorclass": pymysql.cursors.DictCursor,
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================
|
|
||||||
# FUNCTIONS
|
|
||||||
# ============================
|
|
||||||
|
|
||||||
from openpyxl.styles import Border, Side
|
|
||||||
|
|
||||||
thin_border = Border(
|
|
||||||
left=Side(style='thin'),
|
|
||||||
right=Side(style='thin'),
|
|
||||||
top=Side(style='thin'),
|
|
||||||
bottom=Side(style='thin')
|
|
||||||
)
|
|
||||||
|
|
||||||
def apply_thin_borders(ws):
|
|
||||||
"""Apply thin borders to all cells in the worksheet."""
|
|
||||||
for row in ws.iter_rows():
|
|
||||||
for cell in row:
|
|
||||||
cell.border = thin_border
|
|
||||||
|
|
||||||
def autofit_columns(ws):
|
|
||||||
"""Auto-adjust column widths based on longest cell content."""
|
|
||||||
for col in ws.columns:
|
|
||||||
max_length = 0
|
|
||||||
col_letter = get_column_letter(col[0].column)
|
|
||||||
for cell in col:
|
|
||||||
try:
|
|
||||||
if cell.value:
|
|
||||||
max_length = max(max_length, len(str(cell.value)))
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
ws.column_dimensions[col_letter].width = max_length + 2
|
|
||||||
|
|
||||||
def apply_header_style(ws):
|
|
||||||
"""Make header BRIGHT YELLOW and bold."""
|
|
||||||
fill = PatternFill(start_color="FFFF00", end_color="FFFF00", fill_type="solid")
|
|
||||||
font = Font(bold=True)
|
|
||||||
|
|
||||||
for cell in ws[1]:
|
|
||||||
cell.fill = fill
|
|
||||||
cell.font = font
|
|
||||||
|
|
||||||
def create_compact_row(row):
|
|
||||||
"""Produce compact record with merged pojistovna, with user_relationship after prijmeni."""
|
|
||||||
|
|
||||||
# insurance merged
|
|
||||||
code = row.get("pojistovna_code") or ""
|
|
||||||
naz = row.get("pojistovna_nazev") or ""
|
|
||||||
|
|
||||||
if code and naz:
|
|
||||||
poj = f"{code} ({naz})"
|
|
||||||
elif code:
|
|
||||||
poj = code
|
|
||||||
elif naz:
|
|
||||||
poj = naz
|
|
||||||
else:
|
|
||||||
poj = ""
|
|
||||||
|
|
||||||
return {
|
|
||||||
"id": row["id"],
|
|
||||||
"jmeno": row["jmeno"],
|
|
||||||
"prijmeni": row["prijmeni"],
|
|
||||||
|
|
||||||
# 🔹 inserted here
|
|
||||||
"user_relationship": row.get("user_relationship"),
|
|
||||||
|
|
||||||
"rodne_cislo": row["rodne_cislo"],
|
|
||||||
"dob": row["dob"],
|
|
||||||
"telefon": row["telefon"],
|
|
||||||
"email": row["email"],
|
|
||||||
"pojistovna": poj,
|
|
||||||
"status": row["status"],
|
|
||||||
"has_mobile_app": row["has_mobile_app"],
|
|
||||||
"registration_time": row["registration_time"],
|
|
||||||
"last_update": row["last_update"],
|
|
||||||
}
|
|
||||||
|
|
||||||
def create_pozadavky_rows(rows):
|
|
||||||
"""Convert raw pozadavky SQL rows into rows for the Excel sheet."""
|
|
||||||
output = []
|
|
||||||
for r in rows:
|
|
||||||
output.append({
|
|
||||||
# 🔹 First the ID
|
|
||||||
"id": r["id"],
|
|
||||||
|
|
||||||
# 🔹 Your 3 patient columns immediately after ID
|
|
||||||
"pacient_jmeno": r["pacient_jmeno"],
|
|
||||||
"pacient_prijmeni": r["pacient_prijmeni"],
|
|
||||||
"pacient_rodnecislo": r["pacient_rodnecislo"],
|
|
||||||
|
|
||||||
# 🔹 Then all other fields in any order you prefer
|
|
||||||
"displayTitle": r["displayTitle"],
|
|
||||||
"createdAt": r["createdAt"],
|
|
||||||
"updatedAt": r["updatedAt"],
|
|
||||||
"doneAt": r["doneAt"],
|
|
||||||
"removedAt": r["removedAt"],
|
|
||||||
"attachmentsProcessed": r["attachmentsProcessed"],
|
|
||||||
"messagesProcessed": r["messagesProcessed"],
|
|
||||||
"communicationprocessed": r["communicationprocessed"],
|
|
||||||
"questionnaireprocessed": r["questionnaireprocessed"],
|
|
||||||
"lastSync": r["lastSync"],
|
|
||||||
})
|
|
||||||
return output
|
|
||||||
|
|
||||||
|
|
||||||
# ============================
|
|
||||||
# MAIN
|
|
||||||
# ============================
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print("📥 Connecting to MySQL...")
|
|
||||||
conn = pymysql.connect(**DB_CONFIG)
|
|
||||||
|
|
||||||
with conn:
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.execute("SELECT * FROM medevio_pacienti ORDER BY prijmeni, jmeno")
|
|
||||||
patients = cur.fetchall()
|
|
||||||
|
|
||||||
print(f"📊 Loaded {len(patients)} patients.")
|
|
||||||
|
|
||||||
# Load pozadavky
|
|
||||||
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.execute("SELECT * FROM pozadavky ORDER BY createdAt DESC")
|
|
||||||
pozadavky_rows = cur.fetchall()
|
|
||||||
|
|
||||||
print(f"📄 Loaded {len(pozadavky_rows)} pozadavky.")
|
|
||||||
|
|
||||||
wb = openpyxl.Workbook()
|
|
||||||
|
|
||||||
# ---------------------------------
|
|
||||||
# 1) FULL SHEET
|
|
||||||
# ---------------------------------
|
|
||||||
ws_full = wb.active
|
|
||||||
ws_full.title = "Patients FULL"
|
|
||||||
|
|
||||||
if patients:
|
|
||||||
headers = list(patients[0].keys())
|
|
||||||
ws_full.append(headers)
|
|
||||||
|
|
||||||
for row in patients:
|
|
||||||
ws_full.append([row.get(h) for h in headers])
|
|
||||||
|
|
||||||
apply_header_style(ws_full)
|
|
||||||
ws_full.freeze_panes = "A2"
|
|
||||||
ws_full.auto_filter.ref = ws_full.dimensions
|
|
||||||
autofit_columns(ws_full)
|
|
||||||
apply_thin_borders(ws_full)
|
|
||||||
|
|
||||||
# ---------------------------------
|
|
||||||
# 2) COMPACT SHEET
|
|
||||||
# ---------------------------------
|
|
||||||
ws_compact = wb.create_sheet("Patients COMPACT")
|
|
||||||
|
|
||||||
compact_rows = [create_compact_row(r) for r in patients]
|
|
||||||
compact_headers = list(compact_rows[0].keys())
|
|
||||||
|
|
||||||
ws_compact.append(compact_headers)
|
|
||||||
for row in compact_rows:
|
|
||||||
ws_compact.append([row.get(h) for h in compact_headers])
|
|
||||||
|
|
||||||
apply_header_style(ws_compact)
|
|
||||||
ws_compact.freeze_panes = "A2"
|
|
||||||
ws_compact.auto_filter.ref = ws_compact.dimensions
|
|
||||||
autofit_columns(ws_compact)
|
|
||||||
|
|
||||||
# >>> ADD THIS <<<
|
|
||||||
ur_col_index = compact_headers.index("user_relationship") + 1
|
|
||||||
col_letter = get_column_letter(ur_col_index)
|
|
||||||
ws_compact.column_dimensions[col_letter].width = 7.14
|
|
||||||
apply_thin_borders(ws_compact)
|
|
||||||
|
|
||||||
# ---------------------------------
|
|
||||||
# 3) POZADAVKY SHEET
|
|
||||||
# ---------------------------------
|
|
||||||
ws_p = wb.create_sheet("Pozadavky")
|
|
||||||
|
|
||||||
poz_list = create_pozadavky_rows(pozadavky_rows)
|
|
||||||
headers_p = list(poz_list[0].keys()) if poz_list else []
|
|
||||||
|
|
||||||
if headers_p:
|
|
||||||
ws_p.append(headers_p)
|
|
||||||
for row in poz_list:
|
|
||||||
ws_p.append([row.get(h) for h in headers_p])
|
|
||||||
|
|
||||||
apply_header_style(ws_p)
|
|
||||||
ws_p.freeze_panes = "A2"
|
|
||||||
ws_p.auto_filter.ref = ws_p.dimensions
|
|
||||||
autofit_columns(ws_p)
|
|
||||||
apply_thin_borders(ws_p)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------
|
|
||||||
# SAVE
|
|
||||||
# ---------------------------------
|
|
||||||
|
|
||||||
wb.save(OUTPUT_PATH)
|
|
||||||
print(f"✅ Excel report saved to:\n{OUTPUT_PATH}")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from pathlib import Path
|
|
||||||
import json
|
|
||||||
|
|
||||||
TOKEN_PATH = Path("token.txt")
|
|
||||||
CLINIC_SLUG = "mudr-buzalkova"
|
|
||||||
BATCH_SIZE = 100
|
|
||||||
|
|
||||||
# přesně tvůj původní dotaz, beze změn
|
|
||||||
# GRAPHQL_QUERY = r"""
|
|
||||||
# query ClinicRequestGrid_ListPatientRequestsForClinic2(
|
|
||||||
# $clinicSlug: String!,
|
|
||||||
# $queueId: String,
|
|
||||||
# $queueAssignment: QueueAssignmentFilter!,
|
|
||||||
# $pageInfo: PageInfo!,
|
|
||||||
# $locale: Locale!,
|
|
||||||
# $state: PatientRequestState
|
|
||||||
# ) {
|
|
||||||
# requestsResponse: listPatientRequestsForClinic2(
|
|
||||||
# clinicSlug: $clinicSlug,
|
|
||||||
# queueId: $queueId,
|
|
||||||
# queueAssignment: $queueAssignment,
|
|
||||||
# pageInfo: $pageInfo,
|
|
||||||
# state: $state
|
|
||||||
# ) {
|
|
||||||
# count
|
|
||||||
# patientRequests {
|
|
||||||
# id
|
|
||||||
# displayTitle(locale: $locale)
|
|
||||||
# createdAt
|
|
||||||
# updatedAt
|
|
||||||
# doneAt
|
|
||||||
# removedAt
|
|
||||||
# extendedPatient {
|
|
||||||
# name
|
|
||||||
# surname
|
|
||||||
# identificationNumber
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
# """
|
|
||||||
GRAPHQL_QUERY = r"""
|
|
||||||
query ClinicRequestGrid_ListPatientRequestsForClinic2(
|
|
||||||
$clinicSlug: String!,
|
|
||||||
$queueId: String,
|
|
||||||
$queueAssignment: QueueAssignmentFilter!,
|
|
||||||
$state: PatientRequestState,
|
|
||||||
$pageInfo: PageInfo!,
|
|
||||||
$locale: Locale!
|
|
||||||
) {
|
|
||||||
requestsResponse: listPatientRequestsForClinic2(
|
|
||||||
clinicSlug: $clinicSlug
|
|
||||||
queueId: $queueId
|
|
||||||
queueAssignment: $queueAssignment
|
|
||||||
state: $state
|
|
||||||
pageInfo: $pageInfo
|
|
||||||
) {
|
|
||||||
count
|
|
||||||
patientRequests {
|
|
||||||
id
|
|
||||||
displayTitle(locale: $locale)
|
|
||||||
|
|
||||||
### TIME FIELDS ADDED
|
|
||||||
createdAt
|
|
||||||
updatedAt
|
|
||||||
doneAt
|
|
||||||
removedAt
|
|
||||||
|
|
||||||
extendedPatient {
|
|
||||||
id
|
|
||||||
identificationNumber
|
|
||||||
name
|
|
||||||
surname
|
|
||||||
kind
|
|
||||||
key
|
|
||||||
type
|
|
||||||
user {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
surname
|
|
||||||
}
|
|
||||||
owner {
|
|
||||||
name
|
|
||||||
surname
|
|
||||||
}
|
|
||||||
dob
|
|
||||||
premiumPlanPatient {
|
|
||||||
id
|
|
||||||
premiumPlan {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
status2
|
|
||||||
tags(onlyImportant: true) {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
isUnknownPatient
|
|
||||||
}
|
|
||||||
|
|
||||||
invoice {
|
|
||||||
id
|
|
||||||
status
|
|
||||||
amount
|
|
||||||
currency
|
|
||||||
dueAmount
|
|
||||||
isOverdue
|
|
||||||
refundedAmount
|
|
||||||
settledAmount
|
|
||||||
}
|
|
||||||
|
|
||||||
lastMessage {
|
|
||||||
createdAt
|
|
||||||
id
|
|
||||||
readAt
|
|
||||||
sender {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
surname
|
|
||||||
clinicId
|
|
||||||
}
|
|
||||||
text
|
|
||||||
}
|
|
||||||
|
|
||||||
priority
|
|
||||||
|
|
||||||
queue {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
clinicPatientRequestQueueUsers {
|
|
||||||
accountable {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
surname
|
|
||||||
}
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reservations {
|
|
||||||
calendar {
|
|
||||||
id
|
|
||||||
internalName
|
|
||||||
name
|
|
||||||
}
|
|
||||||
id
|
|
||||||
canceledAt
|
|
||||||
done
|
|
||||||
start
|
|
||||||
}
|
|
||||||
|
|
||||||
tags(onlyImportant: true) {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
|
|
||||||
userECRF(locale: $locale) {
|
|
||||||
id
|
|
||||||
sid
|
|
||||||
icon {
|
|
||||||
color
|
|
||||||
id
|
|
||||||
urlSvg
|
|
||||||
}
|
|
||||||
ecrfSet {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
priceWhenCreated
|
|
||||||
currencyWhenCreated
|
|
||||||
createdByDoctor
|
|
||||||
eventType
|
|
||||||
clinicNotes {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
clinicMedicalRecord
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
def read_token(path: Path) -> str:
|
|
||||||
tok = path.read_text(encoding="utf-8").strip()
|
|
||||||
if tok.startswith("Bearer "):
|
|
||||||
tok = tok.split(" ", 1)[1]
|
|
||||||
return tok
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
token = read_token(TOKEN_PATH)
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {token}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Accept": "application/json",
|
|
||||||
}
|
|
||||||
|
|
||||||
variables = {
|
|
||||||
"clinicSlug": CLINIC_SLUG,
|
|
||||||
"queueId": None,
|
|
||||||
"queueAssignment": "ANY",
|
|
||||||
"pageInfo": {"first": BATCH_SIZE, "offset": 0},
|
|
||||||
"locale": "cs",
|
|
||||||
"state": "ACTIVE",
|
|
||||||
}
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"operationName": "ClinicRequestGrid_ListPatientRequestsForClinic2",
|
|
||||||
"query": GRAPHQL_QUERY,
|
|
||||||
"variables": variables,
|
|
||||||
}
|
|
||||||
|
|
||||||
print("\n===== ČISTÁ ODPOVĚĎ SERVERU =====\n")
|
|
||||||
|
|
||||||
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30)
|
|
||||||
|
|
||||||
print(f"HTTP {r.status_code}\n")
|
|
||||||
print(r.text) # <-- TISK NEUPRAVENÉHO JSONU
|
|
||||||
|
|
||||||
print("\n===== KONEC ČISTÉ ODPOVĚDI =====\n")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
136
Testy/15 test.py
136
Testy/15 test.py
@@ -1,136 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
TOKEN_PATH = Path("token.txt")
|
|
||||||
CLINIC_SLUG = "mudr-buzalkova"
|
|
||||||
BATCH_SIZE = 100
|
|
||||||
TARGET_ID = "cbf6000d-a6ca-4059-88b7-dfdc27220762" # ← sem tvoje ID
|
|
||||||
|
|
||||||
# ⭐ Updated GraphQL with lastMessage included
|
|
||||||
GRAPHQL_QUERY = r"""
|
|
||||||
query ClinicRequestGrid_ListPatientRequestsForClinic2(
|
|
||||||
$clinicSlug: String!,
|
|
||||||
$queueId: String,
|
|
||||||
$queueAssignment: QueueAssignmentFilter!,
|
|
||||||
$pageInfo: PageInfo!,
|
|
||||||
$locale: Locale!,
|
|
||||||
$state: PatientRequestState
|
|
||||||
) {
|
|
||||||
requestsResponse: listPatientRequestsForClinic2(
|
|
||||||
clinicSlug: $clinicSlug,
|
|
||||||
queueId: $queueId,
|
|
||||||
queueAssignment: $queueAssignment,
|
|
||||||
pageInfo: $pageInfo,
|
|
||||||
state: $state
|
|
||||||
) {
|
|
||||||
count
|
|
||||||
patientRequests {
|
|
||||||
id
|
|
||||||
displayTitle(locale: $locale)
|
|
||||||
createdAt
|
|
||||||
updatedAt
|
|
||||||
doneAt
|
|
||||||
removedAt
|
|
||||||
lastMessage {
|
|
||||||
id
|
|
||||||
createdAt
|
|
||||||
updatedAt
|
|
||||||
}
|
|
||||||
extendedPatient {
|
|
||||||
name
|
|
||||||
surname
|
|
||||||
identificationNumber
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
def read_token(path: Path) -> str:
|
|
||||||
tok = path.read_text(encoding="utf-8").strip()
|
|
||||||
if tok.startswith("Bearer "):
|
|
||||||
tok = tok.split(" ", 1)[1]
|
|
||||||
return tok
|
|
||||||
|
|
||||||
def fetch_active(headers, offset):
|
|
||||||
variables = {
|
|
||||||
"clinicSlug": CLINIC_SLUG,
|
|
||||||
"queueId": None,
|
|
||||||
"queueAssignment": "ANY",
|
|
||||||
"pageInfo": {"first": BATCH_SIZE, "offset": offset},
|
|
||||||
"locale": "cs",
|
|
||||||
"state": "ACTIVE",
|
|
||||||
}
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"operationName": "ClinicRequestGrid_ListPatientRequestsForClinic2",
|
|
||||||
"query": GRAPHQL_QUERY,
|
|
||||||
"variables": variables,
|
|
||||||
}
|
|
||||||
|
|
||||||
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30)
|
|
||||||
|
|
||||||
if r.status_code != 200:
|
|
||||||
print("HTTP status:", r.status_code)
|
|
||||||
print(r.text)
|
|
||||||
r.raise_for_status()
|
|
||||||
|
|
||||||
data = r.json().get("data", {}).get("requestsResponse", {})
|
|
||||||
return data.get("patientRequests", []), data.get("count", 0)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
token = read_token(TOKEN_PATH)
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {token}",
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
|
|
||||||
print(f"=== Hledám updatedAt a lastMessage pro pozadavek {TARGET_ID} ===\n")
|
|
||||||
|
|
||||||
offset = 0
|
|
||||||
total_count = None
|
|
||||||
found = False
|
|
||||||
|
|
||||||
while True:
|
|
||||||
batch, count = fetch_active(headers, offset)
|
|
||||||
|
|
||||||
if total_count is None:
|
|
||||||
total_count = count
|
|
||||||
|
|
||||||
if not batch:
|
|
||||||
break
|
|
||||||
|
|
||||||
for r in batch:
|
|
||||||
if r["id"] == TARGET_ID:
|
|
||||||
|
|
||||||
print("Nalezeno!\n")
|
|
||||||
print(f"id: {r['id']}")
|
|
||||||
print(f"updatedAt: {r['updatedAt']}")
|
|
||||||
|
|
||||||
lm = r.get("lastMessage") or {}
|
|
||||||
print(f"lastMessage.createdAt: {lm.get('createdAt')}")
|
|
||||||
print(f"lastMessage.updatedAt: {lm.get('updatedAt')}")
|
|
||||||
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if found:
|
|
||||||
break
|
|
||||||
|
|
||||||
if offset + BATCH_SIZE >= count:
|
|
||||||
break
|
|
||||||
|
|
||||||
offset += BATCH_SIZE
|
|
||||||
|
|
||||||
if not found:
|
|
||||||
print("❌ Požadavek nebyl nalezen mezi ACTIVE.")
|
|
||||||
|
|
||||||
print("\n=== HOTOVO ===")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import pymysql
|
|
||||||
from pathlib import Path
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# ⚙️ CONFIGURATION
|
|
||||||
# ==============================
|
|
||||||
DB_CONFIG = {
|
|
||||||
"host": "192.168.1.76",
|
|
||||||
"port": 3307,
|
|
||||||
"user": "root",
|
|
||||||
"password": "Vlado9674+",
|
|
||||||
"database": "medevio",
|
|
||||||
"charset": "utf8mb4",
|
|
||||||
}
|
|
||||||
|
|
||||||
OUTPUT = Path(r"d:\Dropbox\Ordinace\Dokumentace_ke_zpracování\mp1")
|
|
||||||
OUTPUT.mkdir(exist_ok=True)
|
|
||||||
|
|
||||||
|
|
||||||
def sanitize(name: str) -> str:
|
|
||||||
return re.sub(r'[<>:"/\\|?*\x00-\x1F]', "_", name).strip()
|
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# 📥 LOAD EVERYTHING IN ONE QUERY
|
|
||||||
# ==============================
|
|
||||||
def load_all_files():
|
|
||||||
conn = pymysql.connect(**DB_CONFIG)
|
|
||||||
cur = conn.cursor(pymysql.cursors.DictCursor)
|
|
||||||
|
|
||||||
print("📥 Loading ALL medevio_downloads including BLOBs… (can take a few seconds)")
|
|
||||||
|
|
||||||
cur.execute("""
|
|
||||||
SELECT
|
|
||||||
d.id AS download_id,
|
|
||||||
d.request_id,
|
|
||||||
d.attachment_id,
|
|
||||||
d.filename,
|
|
||||||
d.content_type,
|
|
||||||
d.file_size,
|
|
||||||
d.created_at,
|
|
||||||
p.pacient_jmeno,
|
|
||||||
p.pacient_prijmeni,
|
|
||||||
d.file_content
|
|
||||||
FROM medevio_downloads d
|
|
||||||
JOIN pozadavky p ON d.request_id = p.id
|
|
||||||
ORDER BY d.created_at
|
|
||||||
""")
|
|
||||||
|
|
||||||
rows = cur.fetchall()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
print(f"📦 Loaded {len(rows)} BLOB records.")
|
|
||||||
return rows
|
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# 💾 SAVE ALL TO FILESYSTEM
|
|
||||||
# ==============================
|
|
||||||
def save_all(rows):
|
|
||||||
saved = 0
|
|
||||||
|
|
||||||
for r in rows:
|
|
||||||
req_id = r["request_id"]
|
|
||||||
jmeno = sanitize(r["pacient_jmeno"] or "")
|
|
||||||
prijmeni = sanitize(r["pacient_prijmeni"] or "")
|
|
||||||
filename = sanitize(r["filename"] or f"{r['download_id']}.bin")
|
|
||||||
|
|
||||||
# Folder for each request
|
|
||||||
folder = OUTPUT / f"{prijmeni}, {jmeno} {req_id}"
|
|
||||||
folder.mkdir(exist_ok=True)
|
|
||||||
|
|
||||||
dest = folder / filename
|
|
||||||
|
|
||||||
# Skip existing
|
|
||||||
if dest.exists():
|
|
||||||
continue
|
|
||||||
|
|
||||||
data = r["file_content"]
|
|
||||||
if not data:
|
|
||||||
continue
|
|
||||||
|
|
||||||
with open(dest, "wb") as f:
|
|
||||||
f.write(data)
|
|
||||||
|
|
||||||
print(f"💾 Saved {dest}")
|
|
||||||
saved += 1
|
|
||||||
|
|
||||||
print(f"\n🎯 Done — {saved} files saved.")
|
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# MAIN
|
|
||||||
# ==============================
|
|
||||||
if __name__ == "__main__":
|
|
||||||
rows = load_all_files() # ONE query
|
|
||||||
save_all(rows) # then write to disk
|
|
||||||
173
Testy/19 Test.py
173
Testy/19 Test.py
@@ -1,173 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import pymysql
|
|
||||||
import re
|
|
||||||
from pathlib import Path
|
|
||||||
from datetime import datetime
|
|
||||||
import time
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# ⚙️ CONFIGURATION
|
|
||||||
# ==============================
|
|
||||||
DB_CONFIG = {
|
|
||||||
"host": "192.168.1.76",
|
|
||||||
"port": 3307,
|
|
||||||
"user": "root",
|
|
||||||
"password": "Vlado9674+",
|
|
||||||
"database": "medevio",
|
|
||||||
"charset": "utf8mb4",
|
|
||||||
}
|
|
||||||
|
|
||||||
BASE_DIR = Path(r"d:\Dropbox\Ordinace\Dokumentace_ke_zpracování\MP")
|
|
||||||
BASE_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
|
|
||||||
def sanitize_name(name: str) -> str:
|
|
||||||
"""Replace invalid filename characters with underscore."""
|
|
||||||
return re.sub(r'[<>:"/\\|?*\x00-\x1F]', "_", name).strip()
|
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# 🧹 DELETE UNEXPECTED FILES
|
|
||||||
# ==============================
|
|
||||||
def clean_folder(folder: Path, valid_files: set):
|
|
||||||
"""Remove all files in folder that are NOT present in valid_files."""
|
|
||||||
if not folder.exists():
|
|
||||||
return
|
|
||||||
|
|
||||||
for f in folder.iterdir():
|
|
||||||
if f.is_file():
|
|
||||||
if sanitize_name(f.name) not in valid_files:
|
|
||||||
print(f"🗑️ Removing unexpected file: {f.name}")
|
|
||||||
try:
|
|
||||||
f.unlink()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"⚠️ Could not delete {f}: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# 📦 DB CONNECTION
|
|
||||||
# ==============================
|
|
||||||
conn = pymysql.connect(**DB_CONFIG)
|
|
||||||
|
|
||||||
cur_meta = conn.cursor(pymysql.cursors.DictCursor)
|
|
||||||
cur_blob = conn.cursor()
|
|
||||||
|
|
||||||
print("🔍 Loading metadata from DB (FAST)…")
|
|
||||||
|
|
||||||
cur_meta.execute("""
|
|
||||||
SELECT d.id AS download_id,
|
|
||||||
d.request_id,
|
|
||||||
d.filename,
|
|
||||||
d.created_at,
|
|
||||||
p.updatedAt AS req_updated_at,
|
|
||||||
p.pacient_jmeno AS jmeno,
|
|
||||||
p.pacient_prijmeni AS prijmeni
|
|
||||||
FROM medevio_downloads d
|
|
||||||
JOIN pozadavky p ON d.request_id = p.id
|
|
||||||
ORDER BY p.updatedAt DESC
|
|
||||||
""")
|
|
||||||
|
|
||||||
rows = cur_meta.fetchall()
|
|
||||||
print(f"📋 Found {len(rows)} attachment records.\n")
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# 🧠 MAIN LOOP
|
|
||||||
# ==============================
|
|
||||||
processed_requests = set()
|
|
||||||
|
|
||||||
for r in rows:
|
|
||||||
req_id = r["request_id"]
|
|
||||||
|
|
||||||
if req_id in processed_requests:
|
|
||||||
continue
|
|
||||||
processed_requests.add(req_id)
|
|
||||||
|
|
||||||
# ========== FETCH ALL VALID FILES FOR THIS REQUEST ==========
|
|
||||||
cur_meta.execute(
|
|
||||||
"SELECT filename FROM medevio_downloads WHERE request_id=%s",
|
|
||||||
(req_id,)
|
|
||||||
)
|
|
||||||
valid_files = {sanitize_name(row["filename"]) for row in cur_meta.fetchall()}
|
|
||||||
|
|
||||||
# ========== FOLDER NAME BASED ON UPDATEDAT ==========
|
|
||||||
updated_at = r["req_updated_at"] or datetime.now()
|
|
||||||
date_str = updated_at.strftime("%Y-%m-%d")
|
|
||||||
|
|
||||||
prijmeni = sanitize_name(r["prijmeni"] or "Unknown")
|
|
||||||
jmeno = sanitize_name(r["jmeno"] or "")
|
|
||||||
|
|
||||||
folder_name = f"{date_str} {prijmeni}, {jmeno} {req_id}"
|
|
||||||
folder_name = sanitize_name(folder_name)
|
|
||||||
main_folder = BASE_DIR / folder_name
|
|
||||||
|
|
||||||
# ========== FIND OLD FOLDER (DUPLICATE) ==========
|
|
||||||
# Any folder that contains "_<req_id>" and is not main_folder is duplicate
|
|
||||||
possible_dups = [
|
|
||||||
f for f in BASE_DIR.iterdir()
|
|
||||||
if f.is_dir() and req_id in f.name and f != main_folder
|
|
||||||
]
|
|
||||||
|
|
||||||
# ========== MERGE DUPLICATES ==========
|
|
||||||
for dup in possible_dups:
|
|
||||||
print(f"♻️ Merging duplicate folder: {dup.name}")
|
|
||||||
|
|
||||||
# 1) Clean unexpected files in dup
|
|
||||||
clean_folder(dup, valid_files)
|
|
||||||
|
|
||||||
# 2) Move files from dup to main folder
|
|
||||||
main_folder.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
for f in dup.iterdir():
|
|
||||||
if f.is_file():
|
|
||||||
target = main_folder / f.name
|
|
||||||
if not target.exists():
|
|
||||||
f.rename(target)
|
|
||||||
|
|
||||||
# 3) Remove the duplicate folder
|
|
||||||
try:
|
|
||||||
shutil.rmtree(dup, ignore_errors=True)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"⚠️ Could not delete duplicate folder {dup}: {e}")
|
|
||||||
|
|
||||||
# ========== CLEAN MAIN FOLDER ==========
|
|
||||||
clean_folder(main_folder, valid_files)
|
|
||||||
|
|
||||||
# ========== DOWNLOAD MISSING FILES ==========
|
|
||||||
main_folder.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
for filename in valid_files:
|
|
||||||
dest = main_folder / filename
|
|
||||||
if dest.exists():
|
|
||||||
continue
|
|
||||||
|
|
||||||
# fetch blob only now
|
|
||||||
start = time.perf_counter()
|
|
||||||
cur_blob.execute(
|
|
||||||
"SELECT file_content FROM medevio_downloads "
|
|
||||||
"WHERE request_id=%s AND filename=%s",
|
|
||||||
(req_id, filename)
|
|
||||||
)
|
|
||||||
row = cur_blob.fetchone()
|
|
||||||
if not row:
|
|
||||||
continue
|
|
||||||
end = time.perf_counter()
|
|
||||||
print(f"⏱ Took {end - start:.4f} seconds")
|
|
||||||
|
|
||||||
content = row[0]
|
|
||||||
if not content:
|
|
||||||
continue
|
|
||||||
|
|
||||||
with open(dest, "wb") as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
print(f"💾 Wrote: {dest.relative_to(BASE_DIR)}")
|
|
||||||
|
|
||||||
print("\n🎯 Export complete.\n")
|
|
||||||
|
|
||||||
cur_blob.close()
|
|
||||||
cur_meta.close()
|
|
||||||
conn.close()
|
|
||||||
315
dddddd.py
Normal file
315
dddddd.py
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
FAST FILE HASH INDEXER – UNRAID (BLAKE3 ONLY, ALL SHARES)
|
||||||
|
- HARDCODED SINGLE SHARE MODE
|
||||||
|
- SQL OPTIMIZATION
|
||||||
|
- STRICT MODE (NO TOLERANCE) - Updates DB on any mismatch
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import pymysql
|
||||||
|
import socket
|
||||||
|
import platform
|
||||||
|
from blake3 import blake3
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# ENV / HOST
|
||||||
|
# ==============================
|
||||||
|
|
||||||
|
HOSTNAME = socket.gethostname()
|
||||||
|
OS_NAME = platform.system()
|
||||||
|
|
||||||
|
# ZDE JE TO NATVRDO PRO TESTOVÁNÍ:
|
||||||
|
# SCAN_ONLY_THIS = None #"#Fotky"
|
||||||
|
SCAN_ONLY_THIS = '#Library' # "#Fotky"
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# CONFIG
|
||||||
|
# ==============================
|
||||||
|
|
||||||
|
EXCLUDED_SHARES = {"domains", "appdata", "system", "isos"}
|
||||||
|
|
||||||
|
# --- File size limits (bytes) ---
|
||||||
|
FILE_MIN_SIZE = 0
|
||||||
|
FILE_MAX_SIZE = 1024 * 1024 * 1024 * 1024 # 50MB
|
||||||
|
|
||||||
|
DB_CONFIG = {
|
||||||
|
"host": "192.168.1.76",
|
||||||
|
"port": 3307,
|
||||||
|
"user": "root",
|
||||||
|
"password": "Vlado9674+",
|
||||||
|
"database": "torrents",
|
||||||
|
"charset": "utf8mb4",
|
||||||
|
"autocommit": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
CHUNK_SIZE = 4 * 1024 * 1024 # 4 MB
|
||||||
|
PRINT_SKIPPED = False
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# HASH
|
||||||
|
# ==============================
|
||||||
|
|
||||||
|
def compute_blake3(path: str) -> bytes:
|
||||||
|
h = blake3()
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
for chunk in iter(lambda: f.read(CHUNK_SIZE), b""):
|
||||||
|
h.update(chunk)
|
||||||
|
return h.digest()
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# SHARE / PATH HELPERS
|
||||||
|
# ==============================
|
||||||
|
|
||||||
|
def get_user_shares():
|
||||||
|
if SCAN_ONLY_THIS:
|
||||||
|
path = f"/mnt/user/{SCAN_ONLY_THIS}"
|
||||||
|
if os.path.isdir(path):
|
||||||
|
print(f"🎯 SINGLE SHARE MODE ACTIVE: Scanning only '{SCAN_ONLY_THIS}'")
|
||||||
|
return [SCAN_ONLY_THIS]
|
||||||
|
else:
|
||||||
|
print(f"⚠️ ERROR: Requested share '{SCAN_ONLY_THIS}' not found in /mnt/user!")
|
||||||
|
return []
|
||||||
|
|
||||||
|
shares = []
|
||||||
|
if not os.path.exists("/mnt/user"):
|
||||||
|
return []
|
||||||
|
|
||||||
|
for name in os.listdir("/mnt/user"):
|
||||||
|
if name.startswith("."):
|
||||||
|
continue
|
||||||
|
if name in EXCLUDED_SHARES:
|
||||||
|
continue
|
||||||
|
path = f"/mnt/user/{name}"
|
||||||
|
if os.path.isdir(path):
|
||||||
|
shares.append(name)
|
||||||
|
return sorted(shares)
|
||||||
|
|
||||||
|
|
||||||
|
def find_physical_roots(shares):
|
||||||
|
roots = []
|
||||||
|
if not os.path.exists("/mnt"):
|
||||||
|
return []
|
||||||
|
for disk in os.listdir("/mnt"):
|
||||||
|
if not disk.startswith("disk"):
|
||||||
|
continue
|
||||||
|
for share in shares:
|
||||||
|
path = f"/mnt/{disk}/{share}"
|
||||||
|
if os.path.isdir(path):
|
||||||
|
roots.append((share, path))
|
||||||
|
return sorted(roots)
|
||||||
|
|
||||||
|
|
||||||
|
def logical_path_from_disk_path(disk_path: str) -> str:
|
||||||
|
if not disk_path.startswith("/mnt/disk"):
|
||||||
|
raise ValueError(f"Unexpected disk path: {disk_path}")
|
||||||
|
parts = disk_path.split("/", 3)
|
||||||
|
return f"/mnt/user/{parts[3]}"
|
||||||
|
|
||||||
|
|
||||||
|
def size_allowed(size: int) -> bool:
|
||||||
|
if FILE_MIN_SIZE is not None and size < FILE_MIN_SIZE:
|
||||||
|
return False
|
||||||
|
if FILE_MAX_SIZE is not None and size > FILE_MAX_SIZE:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# MAIN
|
||||||
|
# ==============================
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("🚀 BLAKE3 indexer starting", flush=True)
|
||||||
|
print(f"🖥 Host: {HOSTNAME} | OS: {OS_NAME}", flush=True)
|
||||||
|
|
||||||
|
if FILE_MIN_SIZE or FILE_MAX_SIZE:
|
||||||
|
print(f"📏 File size limits: min={FILE_MIN_SIZE} max={FILE_MAX_SIZE}", flush=True)
|
||||||
|
|
||||||
|
shares = get_user_shares()
|
||||||
|
if not shares:
|
||||||
|
print("❌ No user shares to index!", flush=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
print("📦 User shares to index:", flush=True)
|
||||||
|
for s in shares:
|
||||||
|
print(f" - {s}", flush=True)
|
||||||
|
|
||||||
|
scan_roots = find_physical_roots(shares)
|
||||||
|
if not scan_roots:
|
||||||
|
print("❌ No physical disk roots found!", flush=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
print("📂 Physical scan roots:", flush=True)
|
||||||
|
for _, path in scan_roots:
|
||||||
|
print(f" - {path}", flush=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
db = pymysql.connect(**DB_CONFIG)
|
||||||
|
cur = db.cursor()
|
||||||
|
# === TOTO JE TEN PŘÍKAZ "NEPŘEMÝŠLEJ" ===
|
||||||
|
# Nastaví relaci na UTC. MySQL přestane posouvat časy o hodinu sem a tam.
|
||||||
|
# cur.execute("SET time_zone = '+00:00'")
|
||||||
|
# =========================================
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Database connection failed: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("📥 Loading already indexed files into memory...", flush=True)
|
||||||
|
|
||||||
|
# === OPTIMALIZACE SQL ===
|
||||||
|
if SCAN_ONLY_THIS:
|
||||||
|
search_pattern = f"/mnt/user/{SCAN_ONLY_THIS}%"
|
||||||
|
print(f"⚡ OPTIMIZATION: Fetching only DB records for '{search_pattern}'", flush=True)
|
||||||
|
cur.execute("""
|
||||||
|
SELECT full_path, file_size, UNIX_TIMESTAMP(mtime)
|
||||||
|
FROM file_md5_index
|
||||||
|
WHERE host_name = %s AND full_path LIKE %s
|
||||||
|
""", (HOSTNAME, search_pattern))
|
||||||
|
else:
|
||||||
|
cur.execute("""
|
||||||
|
SELECT full_path, file_size, UNIX_TIMESTAMP(mtime)
|
||||||
|
FROM file_md5_index
|
||||||
|
WHERE host_name = %s
|
||||||
|
""", (HOSTNAME,))
|
||||||
|
|
||||||
|
# Načteme do slovníku pro rychlé vyhledávání
|
||||||
|
# Formát: { "cesta": (velikost, mtime) }
|
||||||
|
indexed_map = {row[0]: (row[1], row[2]) for row in cur.fetchall()}
|
||||||
|
print(f"✅ Loaded {len(indexed_map):,} indexed entries", flush=True)
|
||||||
|
print("======================================", flush=True)
|
||||||
|
|
||||||
|
new_files = 0
|
||||||
|
skipped = 0
|
||||||
|
filtered = 0
|
||||||
|
seen_paths = set()
|
||||||
|
|
||||||
|
# --- SCAN ---
|
||||||
|
for share, scan_root in scan_roots:
|
||||||
|
for root, _, files in os.walk(scan_root):
|
||||||
|
for fname in files:
|
||||||
|
disk_path = os.path.join(root, fname)
|
||||||
|
|
||||||
|
try:
|
||||||
|
stat = os.stat(disk_path)
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
size = stat.st_size
|
||||||
|
if not size_allowed(size):
|
||||||
|
filtered += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
logical_path = logical_path_from_disk_path(disk_path)
|
||||||
|
|
||||||
|
if logical_path in seen_paths:
|
||||||
|
continue
|
||||||
|
seen_paths.add(logical_path)
|
||||||
|
|
||||||
|
mtime = int(stat.st_mtime)
|
||||||
|
|
||||||
|
# === PŘÍSNÁ KONTROLA (ŽÁDNÁ TOLERANCE) ===
|
||||||
|
# Pokud soubor v DB existuje a přesně sedí velikost i čas, přeskočíme ho.
|
||||||
|
# Vše ostatní (včetně posunu času o 1s) se považuje za změnu a aktualizuje se.
|
||||||
|
|
||||||
|
is_match = False
|
||||||
|
if logical_path in indexed_map:
|
||||||
|
db_size, db_mtime = indexed_map[logical_path]
|
||||||
|
if size == db_size and mtime == db_mtime:
|
||||||
|
is_match = True
|
||||||
|
|
||||||
|
if is_match:
|
||||||
|
skipped += 1
|
||||||
|
if PRINT_SKIPPED:
|
||||||
|
print(f"⏭ SKIP {logical_path}", flush=True)
|
||||||
|
continue
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
print("➕ NEW / UPDATED", flush=True)
|
||||||
|
print(f" File: {logical_path}", flush=True)
|
||||||
|
print(f" Size: {size:,} B", flush=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
b3 = compute_blake3(disk_path)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ BLAKE3 failed: {e}", flush=True)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Zde proběhne UPDATE mtime na hodnotu z disku
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO file_md5_index
|
||||||
|
(os_name, host_name, full_path, file_name, directory,
|
||||||
|
file_size, mtime, blake3)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, FROM_UNIXTIME(%s), %s)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
file_size = VALUES(file_size),
|
||||||
|
mtime = VALUES(mtime),
|
||||||
|
blake3 = VALUES(blake3),
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
""", (
|
||||||
|
OS_NAME,
|
||||||
|
HOSTNAME,
|
||||||
|
logical_path,
|
||||||
|
fname,
|
||||||
|
os.path.dirname(logical_path),
|
||||||
|
size,
|
||||||
|
mtime,
|
||||||
|
b3,
|
||||||
|
))
|
||||||
|
|
||||||
|
new_files += 1
|
||||||
|
print(f" B3 : {b3.hex()}", flush=True)
|
||||||
|
print("--------------------------------------", flush=True)
|
||||||
|
|
||||||
|
print("======================================", flush=True)
|
||||||
|
print(f"✅ New / updated : {new_files}", flush=True)
|
||||||
|
print(f"⏭ Skipped : {skipped}", flush=True)
|
||||||
|
print(f"🚫 Size filtered: {filtered}", flush=True)
|
||||||
|
print("🏁 Script finished", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# DB CLEANUP – REMOVE DELETED FILES
|
||||||
|
# ==============================
|
||||||
|
|
||||||
|
print("🧹 Checking for deleted files in DB...", flush=True)
|
||||||
|
|
||||||
|
db_paths = set(indexed_map.keys())
|
||||||
|
deleted_paths = db_paths - seen_paths
|
||||||
|
|
||||||
|
# Omezíme jen na aktuální share (pokud je aktivní)
|
||||||
|
if SCAN_ONLY_THIS:
|
||||||
|
prefix = f"/mnt/user/{SCAN_ONLY_THIS}/"
|
||||||
|
deleted_paths = {p for p in deleted_paths if p.startswith(prefix)}
|
||||||
|
|
||||||
|
if deleted_paths:
|
||||||
|
print(f"🗑 Removing {len(deleted_paths):,} deleted files from DB", flush=True)
|
||||||
|
|
||||||
|
BATCH_SIZE = 1000
|
||||||
|
deleted_paths = list(deleted_paths)
|
||||||
|
|
||||||
|
for i in range(0, len(deleted_paths), BATCH_SIZE):
|
||||||
|
batch = deleted_paths[i:i + BATCH_SIZE]
|
||||||
|
placeholders = ",".join(["%s"] * len(batch))
|
||||||
|
|
||||||
|
sql = f"""
|
||||||
|
DELETE FROM file_md5_index
|
||||||
|
WHERE host_name = %s
|
||||||
|
AND full_path IN ({placeholders})
|
||||||
|
"""
|
||||||
|
|
||||||
|
cur.execute(sql, (HOSTNAME, *batch))
|
||||||
|
|
||||||
|
print("✅ DB cleanup completed", flush=True)
|
||||||
|
else:
|
||||||
|
print("✅ No deleted files found in DB", flush=True)
|
||||||
|
|
||||||
|
cur.close()
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user