-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Add basic chart for vital signs observations #19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 2 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
1c6e032
doc: Add PRD for vital signs chart with openEHR transparency
claude 9a3a19a
feat: Implement vital signs chart with openEHR transparency
claude fce9ddf
fix: Update pulse archetype CKM link to non-deprecated version
claude 78af59d
fix: Correct encounter archetype CKM link
claude c994dc5
feat: Require encounter_id when recording vital signs
claude 8e5db61
feat: Add openEHR template management infrastructure
claude 6ab2b5c
feat: Use IDCR Vital Signs template from Ripple-openEHR
claude a46b042
docs: Update PRD and ADR with actual template implementation
claude 0f05221
build: specify pnpm as the package manager in package.json
platzhersh 0cbc32c
docs: add prisma migrate deploy command to API development instructions
platzhersh 703c8e0
fix: Correct template upload Content-Type and Accept headers
claude 82a1e93
feat: Improve template registration logging
claude 1d3ec29
feat: add basic logging configuration
platzhersh 7bf194d
fix: Update vital signs EHRBase composition structure, archetype vers…
platzhersh f5798c2
chore: Implement vital signs date validation and update EHRBase templ…
platzhersh 4340783
fix: Add checks for `reading.id` before fetching compositions or swit…
platzhersh 58792a6
chore: Add dedicated EHRbase client methods for formatted composition…
platzhersh 378e145
refactor: Standardize datetime usage to be timezone-aware and add a c…
platzhersh deb0158
fix: Robustify vital signs input handling and openEHR metadata panel …
platzhersh 2465817
feat: Add error handling and retry for encounter loading in Record Vi…
platzhersh File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,22 +1,232 @@ | ||
| from fastapi import APIRouter, HTTPException | ||
| """Router for vital signs observations with openEHR transparency.""" | ||
|
|
||
| from src.observations.schemas import VitalSignsCreate, VitalSignsResponse | ||
| from datetime import datetime | ||
| from typing import Literal | ||
|
|
||
| from fastapi import APIRouter, HTTPException, Query | ||
|
|
||
| from src.ehrbase.client import ehrbase_client | ||
| from src.observations.schemas import ( | ||
| RawCompositionResponse, | ||
| TemplateInfo, | ||
| TemplateListResponse, | ||
| VitalSignsCreate, | ||
| VitalSignsListResponse, | ||
| VitalSignsResponse, | ||
| ) | ||
| from src.observations.service import observation_service | ||
|
|
||
| router = APIRouter() | ||
|
|
||
|
|
||
| # ============================================================================ | ||
| # Vital Signs CRUD | ||
| # ============================================================================ | ||
|
|
||
|
|
||
| @router.post("/vital-signs", response_model=VitalSignsResponse, status_code=201) | ||
| async def record_vital_signs(data: VitalSignsCreate): | ||
| """Record vital signs for a patient.""" | ||
| async def record_vital_signs(data: VitalSignsCreate) -> VitalSignsResponse: | ||
| """Record vital signs for a patient. | ||
|
|
||
| Creates a composition in EHRBase with the vital signs data. | ||
| Returns the recorded data with openEHR metadata for transparency. | ||
| """ | ||
| try: | ||
| return await observation_service.record_vital_signs(data) | ||
| except ValueError as e: | ||
| raise HTTPException(status_code=404, detail=str(e)) from e | ||
|
|
||
|
|
||
| @router.get("/vital-signs/patient/{patient_id}") | ||
| async def get_patient_vital_signs(patient_id: str): | ||
| """Get all vital signs for a patient.""" | ||
| vital_signs = await observation_service.get_vital_signs_for_patient(patient_id) | ||
| return {"vital_signs": vital_signs} | ||
| @router.get("/vital-signs", response_model=VitalSignsListResponse) | ||
| async def list_vital_signs( | ||
| patient_id: str = Query(..., description="Patient ID (required)"), | ||
| from_date: datetime | None = Query(None, description="Start of date range"), | ||
| to_date: datetime | None = Query(None, description="End of date range"), | ||
| skip: int = Query(0, ge=0, description="Pagination offset"), | ||
| limit: int = Query(100, ge=1, le=1000, description="Page size"), | ||
| ) -> VitalSignsListResponse: | ||
| """List vital signs for a patient. | ||
|
|
||
| Returns paginated list of vital signs readings with openEHR metadata. | ||
| """ | ||
| return await observation_service.get_vital_signs_for_patient( | ||
| patient_id=patient_id, | ||
| from_date=from_date, | ||
| to_date=to_date, | ||
| skip=skip, | ||
| limit=limit, | ||
| ) | ||
|
|
||
|
|
||
| @router.get("/vital-signs/{composition_uid}", response_model=VitalSignsResponse) | ||
| async def get_vital_signs( | ||
| composition_uid: str, | ||
| patient_id: str = Query(..., description="Patient ID for EHR lookup"), | ||
| ) -> VitalSignsResponse: | ||
| """Get a single vital signs reading by composition UID.""" | ||
| result = await observation_service.get_vital_signs(composition_uid, patient_id) | ||
| if not result: | ||
| raise HTTPException(status_code=404, detail="Vital signs not found") | ||
| return result | ||
|
|
||
|
|
||
| @router.delete("/vital-signs/{composition_uid}", status_code=204) | ||
| async def delete_vital_signs( | ||
| composition_uid: str, | ||
| patient_id: str = Query(..., description="Patient ID for EHR lookup"), | ||
| ) -> None: | ||
| """Delete a vital signs composition.""" | ||
| success = await observation_service.delete_vital_signs(composition_uid, patient_id) | ||
| if not success: | ||
| raise HTTPException(status_code=404, detail="Vital signs not found or delete failed") | ||
|
|
||
|
|
||
| # ============================================================================ | ||
| # openEHR Transparency Endpoints | ||
| # ============================================================================ | ||
|
|
||
|
|
||
| @router.get("/openehr/templates", response_model=TemplateListResponse) | ||
| async def list_templates() -> TemplateListResponse: | ||
| """List all available operational templates in EHRBase.""" | ||
| try: | ||
| templates = await ehrbase_client.list_templates() | ||
| return TemplateListResponse( | ||
| templates=[ | ||
| TemplateInfo( | ||
| template_id=t.get("template_id", ""), | ||
| concept=t.get("concept"), | ||
| archetype_id=t.get("archetype_id"), | ||
| ) | ||
| for t in templates | ||
| ] | ||
| ) | ||
| except Exception as e: | ||
| raise HTTPException( | ||
| status_code=503, detail=f"Failed to fetch templates: {e}" | ||
| ) from e | ||
|
|
||
|
|
||
| @router.get("/openehr/templates/{template_id}") | ||
| async def get_template_info(template_id: str) -> dict: | ||
| """Get detailed information about a template. | ||
|
|
||
| Returns the template structure with example paths. | ||
| """ | ||
| try: | ||
| # Get template example to show structure | ||
| client = await ehrbase_client._get_client() | ||
| response = await client.get( | ||
| f"/openehr/v1/definition/template/adl1.4/{template_id}/example", | ||
| params={"format": "FLAT"}, | ||
| ) | ||
| if response.status_code == 200: | ||
| return { | ||
| "template_id": template_id, | ||
| "format": "FLAT", | ||
| "example": response.json(), | ||
| } | ||
| raise HTTPException(status_code=404, detail="Template not found") | ||
| except HTTPException: | ||
| raise | ||
| except Exception as e: | ||
| raise HTTPException( | ||
| status_code=503, detail=f"Failed to fetch template: {e}" | ||
| ) from e | ||
|
|
||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| @router.get("/openehr/compositions/{composition_uid}", response_model=RawCompositionResponse) | ||
| async def get_raw_composition( | ||
| composition_uid: str, | ||
| patient_id: str = Query(..., description="Patient ID for EHR lookup"), | ||
| format: Literal["FLAT", "STRUCTURED"] = Query("FLAT", description="Composition format"), | ||
| ) -> RawCompositionResponse: | ||
| """Get raw composition data for transparency. | ||
|
|
||
| Returns the full composition in the requested format (FLAT or STRUCTURED). | ||
| """ | ||
| result = await observation_service.get_raw_composition( | ||
| composition_uid, patient_id, format | ||
| ) | ||
| if not result: | ||
| raise HTTPException(status_code=404, detail="Composition not found") | ||
| return RawCompositionResponse(**result) | ||
|
|
||
|
|
||
| @router.get("/openehr/compositions/{composition_uid}/paths") | ||
| async def get_composition_paths( | ||
| composition_uid: str, | ||
| patient_id: str = Query(..., description="Patient ID for EHR lookup"), | ||
| ) -> dict: | ||
| """Get all paths in a composition for transparency. | ||
|
|
||
| Returns a flattened list of all paths and values in the composition. | ||
| """ | ||
| result = await observation_service.get_raw_composition( | ||
| composition_uid, patient_id, "FLAT" | ||
| ) | ||
| if not result: | ||
| raise HTTPException(status_code=404, detail="Composition not found") | ||
|
|
||
| composition = result.get("composition", {}) | ||
| paths = [] | ||
|
|
||
| for path, value in composition.items(): | ||
| paths.append( | ||
| { | ||
| "path": path, | ||
| "value": value, | ||
| "type": type(value).__name__, | ||
| } | ||
| ) | ||
|
|
||
| return { | ||
| "composition_uid": composition_uid, | ||
| "template_id": result.get("template_id"), | ||
| "paths": sorted(paths, key=lambda x: x["path"]), | ||
| } | ||
|
|
||
|
|
||
| @router.get("/openehr/archetypes/{archetype_id}") | ||
| async def get_archetype_info(archetype_id: str) -> dict: | ||
| """Get information about an archetype. | ||
|
|
||
| Provides archetype details and link to Clinical Knowledge Manager (CKM). | ||
| """ | ||
| # Parse archetype ID to construct CKM URL | ||
| # Format: openEHR-EHR-OBSERVATION.blood_pressure.v2 | ||
| parts = archetype_id.split(".") | ||
| if len(parts) >= 2: | ||
| concept = parts[-2] # e.g., "blood_pressure" | ||
| else: | ||
| concept = archetype_id | ||
|
|
||
| # Known archetype mappings | ||
| ckm_links = { | ||
| "openEHR-EHR-OBSERVATION.blood_pressure.v2": "https://ckm.openehr.org/ckm/archetypes/1013.1.3574", | ||
| "openEHR-EHR-OBSERVATION.pulse.v2": "https://ckm.openehr.org/ckm/archetypes/1013.1.170", | ||
| "openEHR-EHR-COMPOSITION.encounter.v1": "https://ckm.openehr.org/ckm/archetypes/1013.1.1366", | ||
| } | ||
|
|
||
| descriptions = { | ||
| "openEHR-EHR-OBSERVATION.blood_pressure.v2": ( | ||
| "The local systemic arterial blood pressure which is a surrogate " | ||
| "for arterial pressure in the systemic circulation." | ||
| ), | ||
| "openEHR-EHR-OBSERVATION.pulse.v2": ( | ||
| "The rate and associated attributes for a pulse or heart beat." | ||
| ), | ||
| "openEHR-EHR-COMPOSITION.encounter.v1": ( | ||
| "Interaction, contact or care event between a subject of care " | ||
| "and healthcare provider(s)." | ||
| ), | ||
| } | ||
|
|
||
| return { | ||
| "archetype_id": archetype_id, | ||
| "concept": concept, | ||
| "description": descriptions.get(archetype_id, "No description available"), | ||
| "ckm_url": ckm_links.get(archetype_id), | ||
| "reference_model": "EHR" if "EHR-" in archetype_id else "DEMOGRAPHIC", | ||
| "type": parts[0].split("-")[-1] if "-" in parts[0] else "UNKNOWN", | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.