Spaces:
Running
Running
File size: 5,520 Bytes
5cceba0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | """Resolve + fetch the real OHIP source files from ontario.ca.
The ministry embeds a date in each filename (e.g.
`moh-ohip-fee-schedule-master-text-2026-06-02.txt`) that changes on every
update, so we scrape the landing page to discover the *current* URLs rather
than hard-coding them. Downloads are content-hashed so the refresh job can
skip re-parsing/re-embedding when nothing changed.
Compliance note: these are PUBLIC reference documents (fee schedules), never
patient data. For a strictly air-gapped clinic, set OHIP_LOCAL_* paths and the
downloader is bypassed entirely.
"""
from __future__ import annotations
import hashlib
import logging
import re
from dataclasses import dataclass
from pathlib import Path
import httpx
from .config import settings
logger = logging.getLogger(__name__)
# Link to the fixed-width Physician Fee Schedule Master, "Text format".
_FSM_RE = re.compile(
r'href="([^"]*fee-schedule-master-text-[^"]*\.txt)"', re.IGNORECASE
)
# Link to the Physician Schedule of Benefits PDF (the big descriptive doc).
_SOB_RE = re.compile(
r'href="([^"]*moh-schedule-benefit-[^"]*\.pdf)"', re.IGNORECASE
)
# Trailing YYYY-MM-DD in the filename, used to pick the most recent file.
_DATE_RE = re.compile(r"(\d{4}-\d{2}-\d{2})")
def _latest(urls: list[str]) -> str | None:
"""Pick the URL whose embedded YYYY-MM-DD date is newest."""
if not urls:
return None
def key(u: str) -> str:
m = _DATE_RE.findall(u)
return m[-1] if m else "0000-00-00"
return max(urls, key=key)
@dataclass
class SourceFile:
path: Path
sha256: str
changed: bool
url: str | None = None
def _data_dir() -> Path:
d = Path(settings.ohip_data_dir)
d.mkdir(parents=True, exist_ok=True)
return d
def _sha256(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def _absolutize(url: str) -> str:
if url.startswith("http"):
return url
if url.startswith("/"):
return f"https://www.ontario.ca{url}"
return f"https://www.ontario.ca/{url}"
def _discover_urls() -> tuple[str | None, str | None]:
"""Scrape the landing page for the current FSM + SoB URLs."""
fsm_url, sob_urls = _discover_all()
return fsm_url, (sob_urls[0] if sob_urls else None)
def _discover_all() -> tuple[str | None, list[str]]:
"""Return the latest FSM URL and ALL SoB PDF URLs (newest first)."""
with httpx.Client(timeout=30.0, follow_redirects=True) as client:
html = client.get(settings.ohip_source_page).text
fsm_url = _latest([_absolutize(u) for u in _FSM_RE.findall(html)])
def date_key(u: str) -> str:
m = _DATE_RE.findall(u)
return m[-1] if m else "0000-00-00"
sob_urls = sorted(
{_absolutize(u) for u in _SOB_RE.findall(html)}, key=date_key, reverse=True
)
logger.info("Discovered FSM=%s and %d SoB PDF(s)", fsm_url, len(sob_urls))
return fsm_url, sob_urls
def _write_if_changed(name: str, data: bytes, url: str | None) -> SourceFile:
dest = _data_dir() / name
digest = _sha256(data)
hash_file = _data_dir() / f"{name}.sha256"
previous = hash_file.read_text().strip() if hash_file.exists() else None
changed = digest != previous
if changed:
dest.write_bytes(data)
hash_file.write_text(digest)
logger.info("Updated %s (%d bytes, sha256=%s…)", name, len(data), digest[:12])
else:
logger.info("%s unchanged (sha256=%s…)", name, digest[:12])
return SourceFile(path=dest, sha256=digest, changed=changed, url=url)
def _download(url: str) -> bytes:
with httpx.Client(timeout=120.0, follow_redirects=True) as client:
resp = client.get(url)
resp.raise_for_status()
return resp.content
def fetch_fsm() -> SourceFile:
"""Return the current FSM text file (downloaded or local override)."""
if settings.ohip_local_fsm_path:
data = Path(settings.ohip_local_fsm_path).read_bytes()
return _write_if_changed("fsm.txt", data, url=None)
if not settings.allow_network_download:
raise RuntimeError(
"Network download disabled and no OHIP_LOCAL_FSM_PATH provided."
)
fsm_url, _ = _discover_urls()
if not fsm_url:
raise RuntimeError("Could not locate the FSM text URL on the ministry page.")
return _write_if_changed("fsm.txt", _download(fsm_url), url=fsm_url)
def fetch_sob_pdfs() -> list[SourceFile]:
"""Return ALL published Schedule of Benefits PDFs, newest first.
Description coverage varies between editions (a code listed with an inline
description in one year may appear only in a fee matrix the next), so we
parse every edition and merge — newest wins, older fills gaps. Fees always
come from the current FSM, so an older description text is still accurate
for identification/embedding purposes.
"""
if settings.ohip_local_sob_pdf_path:
data = Path(settings.ohip_local_sob_pdf_path).read_bytes()
return [_write_if_changed("sob.pdf", data, url=None)]
if not settings.allow_network_download:
logger.warning("Skipping SoB PDFs: network disabled and no local path set.")
return []
_, sob_urls = _discover_all()
if not sob_urls:
logger.warning("Could not locate any SoB PDF URLs; descriptions will be sparse.")
return []
files: list[SourceFile] = []
for idx, url in enumerate(sob_urls):
files.append(_write_if_changed(f"sob_{idx}.pdf", _download(url), url=url))
return files
|