-
Notifications
You must be signed in to change notification settings - Fork 1
New features #89
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
New features #89
Changes from all commits
5693cd1
a84ff8d
18e6102
ef1a7ca
ee05f7e
564fab7
f52d47c
1511c1e
c50a8f1
80cedf0
aa57741
10f0b8f
6125ae4
7b0e3aa
3785d1d
2becba4
ffe6c06
02c22f4
7b15cd1
8da0a66
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,25 +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 | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,6 +3,8 @@ | |||||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||||||
| from typing import Optional | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| from pydantic import ValidationError | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| from fastapi import APIRouter, HTTPException, Request, Form | ||||||||||||||||||||||||||||
| from fastapi.responses import JSONResponse, HTMLResponse, RedirectResponse | ||||||||||||||||||||||||||||
| from fastapi.templating import Jinja2Templates | ||||||||||||||||||||||||||||
|
|
@@ -291,21 +293,25 @@ def ui_create_timing( | |||||||||||||||||||||||||||
| conn_c2.close() | ||||||||||||||||||||||||||||
| except Exception: | ||||||||||||||||||||||||||||
| rtf_code_uid = None | ||||||||||||||||||||||||||||
| payload = TimingCreate( | ||||||||||||||||||||||||||||
| name=name, | ||||||||||||||||||||||||||||
| label=label, | ||||||||||||||||||||||||||||
| description=description, | ||||||||||||||||||||||||||||
| type=code_uid, | ||||||||||||||||||||||||||||
| value=value, | ||||||||||||||||||||||||||||
| value_label=value_label, | ||||||||||||||||||||||||||||
| relative_to_from=rtf_code_uid, | ||||||||||||||||||||||||||||
| relative_from_schedule_instance=relative_from_schedule_instance, | ||||||||||||||||||||||||||||
| relative_to_schedule_instance=relative_to_schedule_instance, | ||||||||||||||||||||||||||||
| window_label=window_label, | ||||||||||||||||||||||||||||
| window_upper=window_upper, | ||||||||||||||||||||||||||||
| window_lower=window_lower, | ||||||||||||||||||||||||||||
| member_of_timeline=member_of_timeline, | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||
| payload = TimingCreate( | ||||||||||||||||||||||||||||
| name=name, | ||||||||||||||||||||||||||||
| label=label, | ||||||||||||||||||||||||||||
| description=description, | ||||||||||||||||||||||||||||
| type=code_uid, | ||||||||||||||||||||||||||||
| value=value, | ||||||||||||||||||||||||||||
| value_label=value_label, | ||||||||||||||||||||||||||||
| relative_to_from=rtf_code_uid, | ||||||||||||||||||||||||||||
| relative_from_schedule_instance=relative_from_schedule_instance, | ||||||||||||||||||||||||||||
| relative_to_schedule_instance=relative_to_schedule_instance, | ||||||||||||||||||||||||||||
| window_label=window_label, | ||||||||||||||||||||||||||||
| window_upper=window_upper, | ||||||||||||||||||||||||||||
| window_lower=window_lower, | ||||||||||||||||||||||||||||
| member_of_timeline=member_of_timeline, | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| except ValidationError as exc: | ||||||||||||||||||||||||||||
| msgs = "; ".join(e["msg"] for e in exc.errors()) | ||||||||||||||||||||||||||||
| raise HTTPException(400, f"Validation error: {msgs}") | ||||||||||||||||||||||||||||
| create_timing(soa_id, payload) | ||||||||||||||||||||||||||||
| return RedirectResponse(url=f"/ui/soa/{int(soa_id)}/timings", status_code=303) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
@@ -623,21 +629,25 @@ def ui_update_timing( | |||||||||||||||||||||||||||
| # On any error, fall back to previous behavior (leave fields unchanged) | ||||||||||||||||||||||||||||
| mapped_type = None if type_submission_value not in ("",) else "" | ||||||||||||||||||||||||||||
| mapped_rtf = None if relative_to_from_submission_value not in ("",) else "" | ||||||||||||||||||||||||||||
| payload = TimingUpdate( | ||||||||||||||||||||||||||||
| name=name, | ||||||||||||||||||||||||||||
| label=label, | ||||||||||||||||||||||||||||
| description=description, | ||||||||||||||||||||||||||||
| type=mapped_type, | ||||||||||||||||||||||||||||
| value=value, | ||||||||||||||||||||||||||||
| value_label=value_label, | ||||||||||||||||||||||||||||
| relative_to_from=mapped_rtf, | ||||||||||||||||||||||||||||
| relative_from_schedule_instance=relative_from_schedule_instance, | ||||||||||||||||||||||||||||
| relative_to_schedule_instance=relative_to_schedule_instance, | ||||||||||||||||||||||||||||
| window_label=window_label, | ||||||||||||||||||||||||||||
| window_upper=window_upper, | ||||||||||||||||||||||||||||
| window_lower=window_lower, | ||||||||||||||||||||||||||||
| member_of_timeline=member_of_timeline, | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||
| payload = TimingUpdate( | ||||||||||||||||||||||||||||
| name=name, | ||||||||||||||||||||||||||||
| label=label, | ||||||||||||||||||||||||||||
| description=description, | ||||||||||||||||||||||||||||
| type=mapped_type, | ||||||||||||||||||||||||||||
| value=value, | ||||||||||||||||||||||||||||
| value_label=value_label, | ||||||||||||||||||||||||||||
| relative_to_from=mapped_rtf, | ||||||||||||||||||||||||||||
| relative_from_schedule_instance=relative_from_schedule_instance, | ||||||||||||||||||||||||||||
| relative_to_schedule_instance=relative_to_schedule_instance, | ||||||||||||||||||||||||||||
| window_label=window_label, | ||||||||||||||||||||||||||||
| window_upper=window_upper, | ||||||||||||||||||||||||||||
| window_lower=window_lower, | ||||||||||||||||||||||||||||
| member_of_timeline=member_of_timeline, | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| except ValidationError as exc: | ||||||||||||||||||||||||||||
| msgs = "; ".join(e["msg"] for e in exc.errors()) | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
| msgs = "; ".join(e["msg"] for e in exc.errors()) | |
| # Include field locations in the validation error so clients can | |
| # associate messages with specific inputs. | |
| detailed_parts = [] | |
| for err in exc.errors(): | |
| loc = err.get("loc") or () | |
| msg = err.get("msg", "") | |
| loc_str = ".".join(str(part) for part in loc) if loc else "" | |
| if loc_str: | |
| detailed_parts.append(f"{loc_str}: {msg}") | |
| else: | |
| detailed_parts.append(msg) | |
| msgs = "; ".join(detailed_parts) |
| 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)?)?$" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
| r"^-?P-?(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$" | |
| r"^(?:P|-P|P-)(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$" |
Copilot
AI
Feb 8, 2026
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 regex allows a double-negative form like -P-2D (leading - plus P-), which is neither standard ISO 8601 nor a clear USDM convention. Consider explicitly rejecting -P-... inputs (e.g., a simple pre-check) or refactoring the pattern into mutually exclusive alternatives so only -P... or P-... (but not both) can match.
Copilot
AI
Feb 8, 2026
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.
Returning None for empty/whitespace strings collapses “explicitly cleared” and “not provided” into the same value. This is likely to break clearing existing values on update (e.g., UI text inputs typically submit "" when cleared), because downstream update logic often treats None as “leave unchanged”. Consider preserving the ability to clear fields by (a) using Pydantic’s model_fields_set to distinguish missing vs provided fields in update handling, and/or (b) normalizing empty strings consistently but still honoring explicit field presence in the update merge logic.
| 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 | |
| # Preserve distinction between "not provided" (None) and "explicitly cleared" (empty string) | |
| return None | |
| v_stripped = v.strip() | |
| # If the value is empty or only whitespace, treat it as an explicit empty string rather than None | |
| if not v_stripped: | |
| return "" | |
| m = _ISO8601_DURATION_RE.match(v_stripped) | |
| if not m or not any(m.group(i) is not None for i in range(1, 8)): | |
| raise ValueError( | |
| f"'{v_stripped}' is not a valid ISO 8601 duration (e.g. P1D, P2W, PT8H, -P2D)" | |
| ) | |
| # Return the normalized duration string (whitespace removed) | |
| return v_stripped |
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 concatenates only
msgstrings, which can be ambiguous when multiple fields fail validation (it omits which field each message applies to). Consider including the Pydantic errorloc(field path) alongside the message (e.g.,value: ...; window_upper: ...) so users can identify what to fix.