-
Notifications
You must be signed in to change notification settings - Fork 1
Fixing muddle #90
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
Fixing muddle #90
Changes from all commits
5693cd1
a84ff8d
18e6102
ef1a7ca
ee05f7e
564fab7
f52d47c
1511c1e
c50a8f1
80cedf0
aa57741
10f0b8f
6125ae4
7b0e3aa
3785d1d
2becba4
ffe6c06
02c22f4
7b15cd1
8da0a66
6af934e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,24 +1,38 @@ | ||
| annotated-doc==0.0.4 | ||
| annotated-types==0.7.0 | ||
| anyio==4.12.1 | ||
| beautifulsoup4==4.14.3 | ||
| certifi==2026.1.4 | ||
| charset-normalizer==3.4.4 | ||
| click==8.3.0 | ||
| docraptor==3.1.0 | ||
| dotenv==0.9.9 | ||
| et_xmlfile==2.0.0 | ||
| fastapi==0.128.5 | ||
| fhir.resources==8.2.0 | ||
| fhir_core==1.1.5 | ||
| h11==0.16.0 | ||
| httpcore==1.0.9 | ||
| httpx==0.28.1 | ||
| idna==3.11 | ||
| Jinja2==3.1.6 | ||
| numpy==2.4.2 | ||
| openpyxl==3.1.5 | ||
| pandas==3.0.0 | ||
| pydantic==2.12.5 | ||
| pydantic_core==2.41.5 | ||
| python-dateutil==2.9.0.post0 | ||
| python-dotenv==1.2.1 | ||
| python-multipart==0.0.22 | ||
| PyYAML==6.0.3 | ||
| requests==2.32.5 | ||
| six==1.17.0 | ||
| soupsieve==2.8.3 | ||
| starlette==0.52.1 | ||
| stringcase==1.2.0 | ||
| typing-inspection==0.4.2 | ||
| typing_extensions==4.15.0 | ||
| urllib3==2.6.3 | ||
| usdm==0.66.0 | ||
| uvicorn==0.38.0 | ||
| yattag==1.16.1 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,50 @@ | ||
| import re | ||
| from typing import List, Optional | ||
|
|
||
| from pydantic import BaseModel | ||
| from pydantic import BaseModel, field_validator, model_validator | ||
|
|
||
| # ISO 8601 duration pattern supporting both standard (-P2D) and USDM (P-2D) conventions | ||
| _ISO8601_DURATION_RE = re.compile( | ||
| r"^-?P-?(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$" | ||
| ) | ||
|
|
||
|
|
||
| def _validate_iso8601_duration(v: Optional[str]) -> Optional[str]: | ||
| if v is None: | ||
| return v | ||
| v = v.strip() | ||
| if not v: | ||
| return None | ||
| m = _ISO8601_DURATION_RE.match(v) | ||
| if not m or not any(m.group(i) is not None for i in range(1, 8)): | ||
| raise ValueError( | ||
| f"'{v}' is not a valid ISO 8601 duration (e.g. P1D, P2W, PT8H, -P2D)" | ||
| ) | ||
| return v | ||
|
Comment on lines
+12
to
+23
|
||
|
|
||
|
|
||
| def _validate_window_all_or_none( | ||
| window_lower: Optional[str], | ||
| window_upper: Optional[str], | ||
| window_label: Optional[str], | ||
| ) -> None: | ||
| """Enforce that window_lower, window_upper, and window_label are all provided or all absent.""" | ||
|
|
||
| def _present(v: Optional[str]) -> bool: | ||
| return v is not None and v.strip() != "" | ||
|
|
||
| provided = [_present(window_lower), _present(window_upper), _present(window_label)] | ||
| if any(provided) and not all(provided): | ||
| missing = [] | ||
| if not _present(window_lower): | ||
| missing.append("window_lower") | ||
| if not _present(window_upper): | ||
| missing.append("window_upper") | ||
| if not _present(window_label): | ||
| missing.append("window_label") | ||
| raise ValueError( | ||
| f"Window fields are all-or-nothing: missing {', '.join(missing)}" | ||
| ) | ||
|
|
||
|
|
||
| class InstanceUpdate(BaseModel): | ||
|
|
@@ -42,6 +86,18 @@ class TimingCreate(BaseModel): | |
| window_lower: Optional[str] = None | ||
| member_of_timeline: Optional[str] = None | ||
|
|
||
| @field_validator("value", "window_lower", "window_upper", mode="before") | ||
| @classmethod | ||
| def check_iso8601_duration(cls, v: Optional[str]) -> Optional[str]: | ||
| return _validate_iso8601_duration(v) | ||
|
|
||
| @model_validator(mode="after") | ||
| def check_window_all_or_none(self) -> "TimingCreate": | ||
| _validate_window_all_or_none( | ||
| self.window_lower, self.window_upper, self.window_label | ||
| ) | ||
| return self | ||
|
|
||
|
|
||
| class TimingUpdate(BaseModel): | ||
| name: str | ||
|
|
@@ -58,6 +114,18 @@ class TimingUpdate(BaseModel): | |
| window_lower: Optional[str] = None | ||
| member_of_timeline: Optional[str] = None | ||
|
|
||
| @field_validator("value", "window_lower", "window_upper", mode="before") | ||
| @classmethod | ||
| def check_iso8601_duration(cls, v: Optional[str]) -> Optional[str]: | ||
| return _validate_iso8601_duration(v) | ||
|
|
||
| @model_validator(mode="after") | ||
| def check_window_all_or_none(self) -> "TimingUpdate": | ||
| _validate_window_all_or_none( | ||
| self.window_lower, self.window_upper, self.window_label | ||
| ) | ||
| return self | ||
|
|
||
|
|
||
| class ScheduleTimelineCreate(BaseModel): | ||
| name: str | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The UI error message drops field context (
loc), so users won’t know which input is invalid when multiple validations fail. Consider includinglocalongsidemsg(e.g.,window_upper: <msg>) when buildingmsgsfromexc.errors().