| """ |
| profiles.py — Multi-Patient Care Profile Database |
| Stores individual care profiles (Self, Parent, Child, etc.) |
| Each profile holds legal/clinical baseline data for pre-visit paperwork. |
| """ |
| import json |
| import uuid |
| from copy import deepcopy |
| from pathlib import Path |
| from typing import Optional |
|
|
| import config as cfg |
|
|
| PROFILES_JSON = cfg.DATA_DIR / "profiles.json" |
|
|
| |
| _US_STATE_NAMES: dict[str, str] = { |
| "AL": "Alabama", "AK": "Alaska", "AZ": "Arizona", "AR": "Arkansas", |
| "CA": "California", "CO": "Colorado", "CT": "Connecticut", "DE": "Delaware", |
| "DC": "District of Columbia", "FL": "Florida", "GA": "Georgia", "HI": "Hawaii", |
| "ID": "Idaho", "IL": "Illinois", "IN": "Indiana", "IA": "Iowa", |
| "KS": "Kansas", "KY": "Kentucky", "LA": "Louisiana", "ME": "Maine", |
| "MD": "Maryland", "MA": "Massachusetts", "MI": "Michigan", "MN": "Minnesota", |
| "MS": "Mississippi", "MO": "Missouri", "MT": "Montana", "NE": "Nebraska", |
| "NV": "Nevada", "NH": "New Hampshire", "NJ": "New Jersey", "NM": "New Mexico", |
| "NY": "New York", "NC": "North Carolina", "ND": "North Dakota", "OH": "Ohio", |
| "OK": "Oklahoma", "OR": "Oregon", "PA": "Pennsylvania", "RI": "Rhode Island", |
| "SC": "South Carolina", "SD": "South Dakota", "TN": "Tennessee", "TX": "Texas", |
| "UT": "Utah", "VT": "Vermont", "VA": "Virginia", "WA": "Washington", |
| "WV": "West Virginia", "WI": "Wisconsin", "WY": "Wyoming", |
| } |
| _US_NAME_TO_ABBR = {name.lower(): abbr for abbr, name in _US_STATE_NAMES.items()} |
|
|
| US_STATE_CHOICES: list[tuple[str, str]] = [ |
| (f"{abbr} — {name}", abbr) |
| for abbr, name in sorted(_US_STATE_NAMES.items(), key=lambda item: item[1]) |
| ] |
|
|
|
|
| def normalize_us_state(state: str) -> Optional[str]: |
| """Return a 2-letter US state code, or None if empty/unrecognized.""" |
| s = (state or "").strip() |
| if not s: |
| return None |
| if len(s) == 2: |
| abbr = s.upper() |
| return abbr if abbr in _US_STATE_NAMES else None |
| return _US_NAME_TO_ABBR.get(s.lower()) |
|
|
| PROFILE_TEMPLATE = { |
| "id": "", |
| "label": "", |
| "full_name": "", |
| "dob": "", |
| "street_address": "", |
| "address_line1": "", |
| "address_line2": "", |
| "address_city": "", |
| "address_state": "", |
| "address_zip": "", |
| "phone_number": "", |
| "email": "", |
| "insurance_provider": "", |
| "policy_id": "", |
| "group_id": "", |
| "pcp_name": "", |
| "pcp_clinic": "", |
| "pcp_address": "", |
| "pcp_address_line1": "", |
| "pcp_address_line2": "", |
| "pcp_address_city": "", |
| "pcp_address_state": "", |
| "pcp_address_zip": "", |
| "pcp_phone": "", |
| |
| |
| |
| "care_mode": "Managing someone else's care", |
| "caregiver_name": "", |
| "caregiver_phone": "", |
| "caregiver_email": "", |
| |
| |
| "emergency_contacts": [], |
| |
| |
| |
| |
| "emergency_contact_name": "", |
| "emergency_contact_phone": "", |
| |
| "tracked_symptoms": [], |
| } |
|
|
|
|
| def get_tracked_symptoms(profile_id: str) -> list[str]: |
| """Return cleaned tracked symptom labels for a profile.""" |
| if not profile_id: |
| return [] |
| p = get_profile(profile_id) or {} |
| raw = p.get("tracked_symptoms") or [] |
| if not isinstance(raw, list): |
| return [] |
| return [str(s).strip() for s in raw if str(s).strip()] |
|
|
|
|
| def _load_all() -> list[dict]: |
| """Load all profiles from disk.""" |
| if PROFILES_JSON.exists(): |
| try: |
| with open(PROFILES_JSON, "r", encoding="utf-8") as f: |
| return json.load(f) |
| except Exception: |
| pass |
| return [] |
|
|
|
|
| def _save_all(profiles: list[dict]): |
| """Persist all profiles to disk.""" |
| cfg.DATA_DIR.mkdir(parents=True, exist_ok=True) |
| with open(PROFILES_JSON, "w", encoding="utf-8") as f: |
| json.dump(profiles, f, indent=2) |
|
|
|
|
| |
|
|
| def create_profile(label: str = "", template: Optional[dict] = None) -> dict: |
| """Create a new profile. Returns the created profile dict.""" |
| profiles = _load_all() |
| profile = deepcopy(PROFILE_TEMPLATE) |
| profile["id"] = str(uuid.uuid4()) |
| profile["label"] = label |
| if template: |
| |
| tid = template.pop("id", None) |
| profile.update(template) |
| if tid: |
| profile["id"] = tid |
| profiles.append(profile) |
| _save_all(profiles) |
| return profile |
|
|
|
|
| def get_profiles() -> list[dict]: |
| """Return all profiles.""" |
| return _load_all() |
|
|
|
|
| def get_profile(profile_id: str) -> Optional[dict]: |
| """Return a single profile by id, or None.""" |
| for p in _load_all(): |
| if p["id"] == profile_id: |
| return p |
| return None |
|
|
|
|
| def update_profile(profile_id: str, updates: dict) -> Optional[dict]: |
| """Update fields on an existing profile. Returns updated profile or None.""" |
| profiles = _load_all() |
| for i, p in enumerate(profiles): |
| if p["id"] == profile_id: |
| |
| updates.pop("id", None) |
| profiles[i].update(updates) |
| _save_all(profiles) |
| return profiles[i] |
| return None |
|
|
|
|
| def delete_profile(profile_id: str) -> bool: |
| """Delete a profile by id. Returns True if deleted.""" |
| profiles = _load_all() |
| before = len(profiles) |
| profiles = [p for p in profiles if p["id"] != profile_id] |
| if len(profiles) < before: |
| _save_all(profiles) |
| return True |
| return False |
|
|
|
|
| def delete_profile_data(profile_id: str): |
| """Remove all tracker data scoped to profile_id.""" |
| import med_tracker |
| import appointment_tracker |
| import food_tracker |
|
|
| med_tracker.delete_for_profile(profile_id) |
| appointment_tracker.delete_for_profile(profile_id) |
| food_tracker.delete_for_profile(profile_id) |
|
|
| try: |
| import rag_engine |
| rag_engine.delete_for_profile(profile_id) |
| except Exception: |
| pass |
|
|
|
|
| def profile_display_name(profile: dict) -> str: |
| """Human-readable name for dropdowns and session banner.""" |
| full = (profile.get("full_name") or "").strip() |
| if full: |
| return full |
| return (profile.get("label") or "").strip() or "Unnamed" |
|
|
|
|
| def get_profile_choices() -> list[tuple[str, str]]: |
| """Return (display name, id) pairs for dropdown population.""" |
| return [(profile_display_name(p), p["id"]) for p in _load_all()] |
|
|
|
|
| def compose_address( |
| line1: str = "", |
| line2: str = "", |
| city: str = "", |
| state: str = "", |
| zip_code: str = "", |
| legacy: str = "", |
| ) -> str: |
| """Build a multi-line mailing address from structured parts.""" |
| line1 = (line1 or "").strip() |
| line2 = (line2 or "").strip() |
| city = (city or "").strip() |
| state = (state or "").strip() |
| zip_code = (zip_code or "").strip() |
| if not any((line1, line2, city, state, zip_code)) and legacy: |
| return legacy.strip() |
| lines = [] |
| if line1: |
| lines.append(line1) |
| if line2: |
| lines.append(line2) |
| tail = [] |
| if state: |
| tail.append(state.upper() if len(state) == 2 else state) |
| if zip_code: |
| tail.append(zip_code) |
| if city and tail: |
| lines.append(f"{city}, {' '.join(tail)}") |
| elif city: |
| lines.append(city) |
| elif tail: |
| lines.append(" ".join(tail)) |
| return "\n".join(lines) |
|
|
|
|
| def address_parts(profile: dict, *, pcp: bool = False) -> tuple[str, str, str, str, str]: |
| """Return (line1, line2, city, state, zip) with legacy single-field fallback.""" |
| if pcp: |
| line1 = profile.get("pcp_address_line1", "") |
| line2 = profile.get("pcp_address_line2", "") |
| city = profile.get("pcp_address_city", "") |
| state = profile.get("pcp_address_state", "") |
| zip_code = profile.get("pcp_address_zip", "") |
| legacy = profile.get("pcp_address", "") |
| else: |
| line1 = profile.get("address_line1", "") |
| line2 = profile.get("address_line2", "") |
| city = profile.get("address_city", "") |
| state = profile.get("address_state", "") |
| zip_code = profile.get("address_zip", "") |
| legacy = profile.get("street_address", "") |
| line1, line2, city, state, zip_code = map( |
| lambda v: (v or "").strip(), (line1, line2, city, state, zip_code) |
| ) |
| if not any((line1, line2, city, state, zip_code)) and legacy: |
| return legacy.strip(), "", "", "", "" |
| return line1, line2, city, state, zip_code |
|
|