This commit is contained in:
2025-12-02 06:24:27 +01:00
parent f8ada463a2
commit f159120175
10 changed files with 359 additions and 78 deletions

2
.idea/Medevio.iml generated
View File

@@ -4,7 +4,7 @@
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.12 (Medevio)" jdkType="Python SDK" />
<orderEntry type="jdk" jdkName="Python 3.13 (Medevio)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

2
.idea/misc.xml generated
View File

@@ -3,5 +3,5 @@
<component name="Black">
<option name="sdkName" value="Python 3.12 (Medevio)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (Medevio)" project-jdk-type="Python SDK" />
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 (Medevio)" project-jdk-type="Python SDK" />
</project>

View File

@@ -8,6 +8,18 @@ from datetime import datetime, timezone
import time
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
# ================================

View File

@@ -6,13 +6,41 @@ import requests
from pathlib import Path
from datetime import datetime
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
# ================================
TOKEN_PATH = Path("token.txt")
CLINIC_SLUG = "mudr-buzalkova"
LIMIT = 300 # stáhneme posledních 300 ukončených požadavků
LIMIT = 300
DB_CONFIG = {
"host": "192.168.1.76",
@@ -24,7 +52,7 @@ DB_CONFIG = {
"cursorclass": pymysql.cursors.DictCursor,
}
# ⭐ Ověřený dotaz s lastMessage
# ⭐ GraphQL query
GRAPHQL_QUERY = r"""
query ClinicRequestList2(
$clinicSlug: String!,
@@ -71,33 +99,31 @@ def read_token(path: Path) -> str:
return tok.split(" ", 1)[1]
return tok
# ================================
# DATETIME PARSER (UTC → MySQL)
# DATETIME PARSER
# ================================
def to_mysql_dt(iso_str):
if not iso_str:
return None
try:
dt = parser.isoparse(iso_str) # ISO8601 → aware datetime (UTC)
dt = dt.astimezone() # převede na lokální čas (CET/CEST)
dt = parser.isoparse(iso_str)
dt = dt.astimezone()
return dt.strftime("%Y-%m-%d %H:%M:%S")
except:
return None
# ================================
# UPSERT WITH MERGED UPDATED TIME
# UPSERT
# ================================
def upsert(conn, r):
p = r.get("extendedPatient") or {}
# API pole
api_updated = to_mysql_dt(r.get("updatedAt"))
# poslední zpráva
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)
@@ -137,6 +163,7 @@ def upsert(conn, r):
conn.commit()
# ================================
# FETCH LAST 300 DONE REQUESTS
# ================================
@@ -162,6 +189,7 @@ def fetch_done(headers):
data = r.json()["data"]["requestsResponse"]
return data.get("patientRequests", [])
# ================================
# MAIN
# ================================
@@ -175,17 +203,18 @@ def main():
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)
print(f"📌 Requests returned: {len(requests_list)}")
safe_print(f"📌 Requests returned: {len(requests_list)}")
for r in requests_list:
upsert(conn, r)
conn.close()
print("\n DONE - latest closed requests synced.\n")
safe_print("\n\u2705 DONE - latest closed requests synced.\n")
# ================================
if __name__ == "__main__":
main()

View File

@@ -12,6 +12,35 @@ import pymysql
from datetime import datetime
from pathlib import Path
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
@@ -31,29 +60,30 @@ DB_CONFIG = {
}
from datetime import datetime
# ==============================
# 🕒 DATETIME FIXER
# ==============================
def fix_datetime(dt_str):
"""Convert ISO 8601 string with 'Z' or ms into MySQL DATETIME format."""
if not dt_str:
return None
try:
# Remove trailing Z and parse flexible ISO format
return datetime.fromisoformat(dt_str.replace("Z", "").replace("+00:00", ""))
except Exception:
return None
# ✅ Optional: limit which requests to process
CREATED_AFTER = "2025-01-01" # set "" to disable
# Optional filter
CREATED_AFTER = "2025-01-01"
# ==============================
# 🧮 HELPERS
# ==============================
def read_token(p: Path) -> str:
"""Read Bearer token from file."""
tok = p.read_text(encoding="utf-8").strip()
if tok.startswith("Bearer "):
tok = tok.split(" ", 1)[1]
return tok.split(" ", 1)[1]
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)
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 r.json().get("data", {}).get("request")
@@ -118,23 +148,24 @@ def insert_questionnaire(cur, req):
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)
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),
))
print(f" 💾 Stored questionnaire for {patient.get('surname','')} {patient.get('name','')}")
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','')}")
# ==============================
@@ -149,6 +180,8 @@ def main():
}
conn = pymysql.connect(**DB_CONFIG)
# load list of requests
with conn.cursor() as cur:
sql = """
SELECT id, pacient_jmeno, pacient_prijmeni, createdAt, updatedAt, questionnaireprocessed
@@ -163,26 +196,30 @@ def main():
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):
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)
if not req:
print(" ⚠️ No questionnaire data found.")
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,))
cur.execute(
"UPDATE pozadavky SET questionnaireprocessed = NOW() WHERE id = %s",
(req_id,)
)
conn.commit()
time.sleep(0.6) # polite pacing
time.sleep(0.6)
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`.")
# ==============================

View File

@@ -15,6 +15,34 @@ import pymysql
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 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
@@ -94,7 +122,7 @@ def fetch_messages(headers, request_id):
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)
safe_print(f"❌ HTTP {r.status_code} for request {request_id}")
return []
return r.json().get("data", {}).get("messages", []) or []
@@ -158,7 +186,7 @@ def insert_download(cur, req_id, msg, existing_ids):
return
if attachment_id in existing_ids:
return # skip duplicates
return
url = mr.get("downloadUrl") or mr.get("url")
if not url:
@@ -169,7 +197,7 @@ def insert_download(cur, req_id, msg, existing_ids):
r.raise_for_status()
data = r.content
except Exception as e:
print("⚠️ Failed to download:", e)
safe_print(f"⚠️ Failed to download: {e}")
return
filename = url.split("/")[-1].split("?")[0]
@@ -216,7 +244,7 @@ def main():
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")
safe_print(f"📦 Already downloaded attachments: {len(existing_ids)}\n")
# ---- Select pozadavky needing message sync
sql = """
@@ -229,12 +257,12 @@ def main():
cur.execute(sql)
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):
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)
@@ -248,11 +276,11 @@ def main():
cur.execute("UPDATE pozadavky SET messagesProcessed = NOW() WHERE id = %s", (req_id,))
conn.commit()
print(f"{len(messages)} messages saved\n")
safe_print(f"{len(messages)} messages saved\n")
time.sleep(0.25)
conn.close()
print("🎉 Done!")
safe_print("🎉 Done!")
if __name__ == "__main__":

View File

@@ -14,6 +14,36 @@ import pymysql
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 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
@@ -67,6 +97,7 @@ 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
# ==============================
@@ -78,42 +109,40 @@ def fetch_attachments(headers, 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}")
safe_print(f"❌ HTTP {r.status_code} for request {request_id}")
return []
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):
attachment_id = a.get("id")
if attachment_id in existing_ids:
print(f" ⏭️ Already downloaded {attachment_id}")
safe_print(f" ⏭️ Already downloaded {attachment_id}")
return False
url = m.get("downloadUrl")
if not url:
print(" ⚠️ Missing download URL")
safe_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}")
safe_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,
@@ -136,7 +165,7 @@ def insert_download(cur, req_id, a, m, created_date, existing_ids):
))
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
@@ -152,11 +181,12 @@ def main():
conn = pymysql.connect(**DB_CONFIG)
# Load existing IDs
# 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"{len(existing_ids)} attachments already saved.")
safe_print(f"{len(existing_ids)} attachments already saved.")
# Build query for pozadavky
sql = """
@@ -173,7 +203,7 @@ def main():
cur.execute(sql, params)
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
for i, row in enumerate(req_rows, 1):
@@ -182,12 +212,12 @@ def main():
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})")
safe_print(f"\n[{i}/{len(req_rows)}] 🧾 {prijmeni}, {jmeno} ({req_id})")
attachments = fetch_attachments(headers, req_id)
if not attachments:
print(" ⚠️ No attachments found")
safe_print(" ⚠️ No attachments found")
with conn.cursor() as cur:
cur.execute("UPDATE pozadavky SET attachmentsProcessed = NOW() WHERE id = %s", (req_id,))
conn.commit()
@@ -199,17 +229,16 @@ def main():
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)")
safe_print(f" ✅ Done ({len(attachments)} attachments)")
time.sleep(0.3)
conn.close()
print("\n🎯 All attachments processed.")
safe_print("\n🎯 All attachments processed.")
# ==============================
if __name__ == "__main__":

View File

@@ -10,6 +10,17 @@ 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
# ==============================
@@ -92,17 +103,16 @@ 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
WHERE d.downloaded_at IS NULL
ORDER BY p.updatedAt DESC
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()

View File

@@ -15,7 +15,7 @@ Spustí všechny PRAVIDELNÉ skripty v daném pořadí:
import time, socket
for _ in range(30):
try:
socket.create_connection(("127.0.0.1", 3307), timeout=3).close()
socket.create_connection(("192.168.1.76", 3307), timeout=3).close()
break
except OSError:
time.sleep(10)

136
10ReadPozadavky/test.py Normal file
View 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()