From 5693cd1c1ecf17be334675ac679fddfb5f60d27e Mon Sep 17 00:00:00 2001
From: Darren <3921919+pendingintent@users.noreply.github.com>
Date: Fri, 6 Feb 2026 11:23:33 -0500
Subject: [PATCH 01/18] Ordered timing fields in logical way; added hover help
text
---
src/soa_builder/web/templates/timings.html | 59 +++++++++++-----------
1 file changed, 30 insertions(+), 29 deletions(-)
diff --git a/src/soa_builder/web/templates/timings.html b/src/soa_builder/web/templates/timings.html
index c4c015b..483d7ea 100644
--- a/src/soa_builder/web/templates/timings.html
+++ b/src/soa_builder/web/templates/timings.html
@@ -1,6 +1,6 @@
{% extends 'base.html' %}
{% block content %}
-
-
+
@@ -59,11 +59,11 @@
-
+
-
+
@@ -125,7 +125,7 @@
+ |
|
|
- |
+ |
+ |
|
|
{% endfor %}
+
+
{% endblock %}
diff --git a/tests/test_routers_timings.py b/tests/test_routers_timings.py
index a02bb49..8c9ec83 100644
--- a/tests/test_routers_timings.py
+++ b/tests/test_routers_timings.py
@@ -336,3 +336,183 @@ def test_timing_member_of_timeline():
assert resp.status_code == 201
data = resp.json()
assert data["member_of_timeline"] == timeline_uid
+
+
+def test_window_lower_rejects_non_iso8601():
+ """Test that window_lower rejects non-ISO 8601 values."""
+ r = client.post("/soa", json={"name": "Window Validate Lower"})
+ soa_id = r.json()["id"]
+
+ timing_data = {"name": "Bad Lower", "window_lower": "2 days"}
+ resp = client.post(f"/soa/{soa_id}/timings", json=timing_data)
+ assert resp.status_code == 422
+
+
+def test_window_upper_rejects_non_iso8601():
+ """Test that window_upper rejects non-ISO 8601 values."""
+ r = client.post("/soa", json={"name": "Window Validate Upper"})
+ soa_id = r.json()["id"]
+
+ timing_data = {"name": "Bad Upper", "window_upper": "+3"}
+ resp = client.post(f"/soa/{soa_id}/timings", json=timing_data)
+ assert resp.status_code == 422
+
+
+def test_window_accepts_valid_iso8601_durations():
+ """Test that various valid ISO 8601 durations are accepted."""
+ r = client.post("/soa", json={"name": "Window Valid ISO"})
+ soa_id = r.json()["id"]
+
+ valid_durations = [
+ ("P1D", "P1D"),
+ ("P2W", "P2W"),
+ ("PT8H", "PT8H"),
+ ("-P2D", "-P2D"),
+ ("P-2D", "P-2D"),
+ ("P1Y2M3D", "P1Y2M3D"),
+ ("PT1H30M", "PT1H30M"),
+ ]
+ for i, (lower, upper) in enumerate(valid_durations):
+ timing_data = {
+ "name": f"Valid Duration {i}",
+ "window_lower": lower,
+ "window_upper": upper,
+ "window_label": f"Window {i}",
+ }
+ resp = client.post(f"/soa/{soa_id}/timings", json=timing_data)
+ assert resp.status_code == 201, f"Failed for duration: {lower}/{upper}"
+ data = resp.json()
+ assert data["window_lower"] == lower
+ assert data["window_upper"] == upper
+
+
+def test_window_rejects_bare_p():
+ """Test that bare 'P' without any components is rejected."""
+ r = client.post("/soa", json={"name": "Window Bare P"})
+ soa_id = r.json()["id"]
+
+ timing_data = {"name": "Bare P", "window_lower": "P"}
+ resp = client.post(f"/soa/{soa_id}/timings", json=timing_data)
+ assert resp.status_code == 422
+
+
+def test_update_timing_rejects_invalid_window():
+ """Test that PATCH update also validates window fields."""
+ r = client.post("/soa", json={"name": "Update Window Validate"})
+ soa_id = r.json()["id"]
+
+ timing_resp = client.post(f"/soa/{soa_id}/timings", json={"name": "Good"})
+ timing_id = timing_resp.json()["id"]
+
+ resp = client.patch(
+ f"/soa/{soa_id}/timings/{timing_id}",
+ json={"name": "Good", "window_upper": "bad"},
+ )
+ assert resp.status_code == 422
+
+
+def test_ui_create_timing_rejects_invalid_window():
+ """Test that UI create form rejects non-ISO 8601 window values."""
+ r = client.post("/soa", json={"name": "UI Window Validate"})
+ soa_id = r.json()["id"]
+
+ form_data = {"name": "Bad Window", "window_lower": "not-iso"}
+ resp = client.post(
+ f"/ui/soa/{soa_id}/timings/create", data=form_data, follow_redirects=False
+ )
+ assert resp.status_code == 400
+
+
+def test_value_rejects_non_iso8601():
+ """Test that value rejects non-ISO 8601 values."""
+ r = client.post("/soa", json={"name": "Value Validate"})
+ soa_id = r.json()["id"]
+
+ timing_data = {"name": "Bad Value", "value": "5 days"}
+ resp = client.post(f"/soa/{soa_id}/timings", json=timing_data)
+ assert resp.status_code == 422
+
+
+def test_value_rejects_plain_number():
+ """Test that a plain number like '5' is rejected for value."""
+ r = client.post("/soa", json={"name": "Value Plain Number"})
+ soa_id = r.json()["id"]
+
+ timing_data = {"name": "Plain Num", "value": "5"}
+ resp = client.post(f"/soa/{soa_id}/timings", json=timing_data)
+ assert resp.status_code == 422
+
+
+def test_value_accepts_valid_iso8601_durations():
+ """Test that various valid ISO 8601 durations are accepted for value."""
+ r = client.post("/soa", json={"name": "Value Valid ISO"})
+ soa_id = r.json()["id"]
+
+ valid_values = ["P1D", "P2W", "PT8H", "-P2D", "P-2D", "P1Y2M3D", "PT1H30M"]
+ for i, val in enumerate(valid_values):
+ timing_data = {"name": f"Valid Value {i}", "value": val}
+ resp = client.post(f"/soa/{soa_id}/timings", json=timing_data)
+ assert resp.status_code == 201, f"Failed for value: {val}"
+ assert resp.json()["value"] == val
+
+
+def test_update_timing_rejects_invalid_value():
+ """Test that PATCH update also validates value field."""
+ r = client.post("/soa", json={"name": "Update Value Validate"})
+ soa_id = r.json()["id"]
+
+ timing_resp = client.post(
+ f"/soa/{soa_id}/timings", json={"name": "Good", "value": "P1D"}
+ )
+ timing_id = timing_resp.json()["id"]
+
+ resp = client.patch(
+ f"/soa/{soa_id}/timings/{timing_id}",
+ json={"name": "Good", "value": "not-a-duration"},
+ )
+ assert resp.status_code == 422
+
+
+def test_ui_create_timing_rejects_invalid_value():
+ """Test that UI create form rejects non-ISO 8601 value."""
+ r = client.post("/soa", json={"name": "UI Value Validate"})
+ soa_id = r.json()["id"]
+
+ form_data = {"name": "Bad Value", "value": "two weeks"}
+ resp = client.post(
+ f"/ui/soa/{soa_id}/timings/create", data=form_data, follow_redirects=False
+ )
+ assert resp.status_code == 400
+
+
+def test_window_all_or_none_accepts_all_three():
+ """Test that providing all three window fields is accepted."""
+ r = client.post("/soa", json={"name": "Window Complete"})
+ soa_id = r.json()["id"]
+
+ resp = client.post(
+ f"/soa/{soa_id}/timings",
+ json={
+ "name": "T1",
+ "window_lower": "-P1D",
+ "window_upper": "P2D",
+ "window_label": "Visit Window",
+ },
+ )
+ assert resp.status_code == 201
+
+
+def test_window_all_or_none_accepts_none():
+ """Test that providing no window fields is accepted."""
+ r = client.post("/soa", json={"name": "Window None"})
+ soa_id = r.json()["id"]
+
+ resp = client.post(
+ f"/soa/{soa_id}/timings",
+ json={"name": "No Window"},
+ )
+ assert resp.status_code == 201
+ data = resp.json()
+ assert data["window_lower"] is None
+ assert data["window_upper"] is None
+ assert data["window_label"] is None
diff --git a/tests/test_timings.py b/tests/test_timings.py
index 92f4b1e..415014c 100644
--- a/tests/test_timings.py
+++ b/tests/test_timings.py
@@ -52,14 +52,14 @@ def test_update_timing_mutable_fields_and_updated_fields():
"label": " Label X ",
"description": " Desc Y ",
"type": " relative ",
- "value": " 5 ",
+ "value": " P5D ",
"value_label": " days ",
"relative_to_from": " from ",
"relative_from_schedule_instance": " Arm A ",
"relative_to_schedule_instance": " Epoch 1 ",
"window_label": " Window ",
- "window_upper": " +2 ",
- "window_lower": " -1 ",
+ "window_upper": " P2D ",
+ "window_lower": " -P1D ",
}
r = client.patch(f"/soa/{soa_id}/timings/{tid}", json=payload)
assert r.status_code == 200, r.text
@@ -70,14 +70,14 @@ def test_update_timing_mutable_fields_and_updated_fields():
assert data["label"] == "Label X"
assert data["description"] == "Desc Y"
assert data["type"] == "relative"
- assert data["value"] == "5"
+ assert data["value"] == "P5D"
assert data["value_label"] == "days"
assert data["relative_to_from"] == "from"
assert data["relative_from_schedule_instance"] == "Arm A"
assert data["relative_to_schedule_instance"] == "Epoch 1"
assert data["window_label"] == "Window"
- assert data["window_upper"] == "+2"
- assert data["window_lower"] == "-1"
+ assert data["window_upper"] == "P2D"
+ assert data["window_lower"] == "-P1D"
# updated_fields must include changed keys
uf = set(data["updated_fields"])
From ee05f7ea42bd763a2272709f1b630d285f70af32 Mon Sep 17 00:00:00 2001
From: Darren <3921919+pendingintent@users.noreply.github.com>
Date: Sun, 8 Feb 2026 15:51:16 -0600
Subject: [PATCH 05/18] Removed numpy dependency
---
requirements.txt | 1 -
1 file changed, 1 deletion(-)
diff --git a/requirements.txt b/requirements.txt
index 7b7968f..9ced3b4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -23,7 +23,6 @@ MarkupSafe==3.0.3
mccabe==0.7.0
mypy_extensions==1.1.0
nodeenv==1.9.1
-numpy==1.26.4
openpyxl==3.1.5
packaging==25.0
pandas==2.3.3
From f52d47cdb28dfbccd02d01be2c4a4cfa37bfcb59 Mon Sep 17 00:00:00 2001
From: Darren <3921919+pendingintent@users.noreply.github.com>
Date: Sun, 8 Feb 2026 15:58:26 -0600
Subject: [PATCH 06/18] Ensure window values are all-or-nothing
---
src/soa_builder/web/schemas.py | 40 ++++++++++++++++-
tests/test_routers_timings.py | 80 ++++++++++++++++++++++++++++++++++
2 files changed, 119 insertions(+), 1 deletion(-)
diff --git a/src/soa_builder/web/schemas.py b/src/soa_builder/web/schemas.py
index b7892f5..0132106 100644
--- a/src/soa_builder/web/schemas.py
+++ b/src/soa_builder/web/schemas.py
@@ -1,7 +1,7 @@
import re
from typing import List, Optional
-from pydantic import BaseModel, field_validator
+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(
@@ -23,6 +23,30 @@ def _validate_iso8601_duration(v: Optional[str]) -> Optional[str]:
return v
+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):
name: Optional[str] = None
label: Optional[str] = None
@@ -67,6 +91,13 @@ class TimingCreate(BaseModel):
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
@@ -88,6 +119,13 @@ class TimingUpdate(BaseModel):
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
diff --git a/tests/test_routers_timings.py b/tests/test_routers_timings.py
index 8c9ec83..de52528 100644
--- a/tests/test_routers_timings.py
+++ b/tests/test_routers_timings.py
@@ -516,3 +516,83 @@ def test_window_all_or_none_accepts_none():
assert data["window_lower"] is None
assert data["window_upper"] is None
assert data["window_label"] is None
+
+
+def test_window_all_or_none_rejects_lower_only():
+ """Test that providing only window_lower is rejected."""
+ r = client.post("/soa", json={"name": "Window Lower Only"})
+ soa_id = r.json()["id"]
+
+ resp = client.post(
+ f"/soa/{soa_id}/timings",
+ json={"name": "T1", "window_lower": "-P1D"},
+ )
+ assert resp.status_code == 422
+
+
+def test_window_all_or_none_rejects_upper_only():
+ """Test that providing only window_upper is rejected."""
+ r = client.post("/soa", json={"name": "Window Upper Only"})
+ soa_id = r.json()["id"]
+
+ resp = client.post(
+ f"/soa/{soa_id}/timings",
+ json={"name": "T1", "window_upper": "P2D"},
+ )
+ assert resp.status_code == 422
+
+
+def test_window_all_or_none_rejects_label_only():
+ """Test that providing only window_label is rejected."""
+ r = client.post("/soa", json={"name": "Window Label Only"})
+ soa_id = r.json()["id"]
+
+ resp = client.post(
+ f"/soa/{soa_id}/timings",
+ json={"name": "T1", "window_label": "Visit Window"},
+ )
+ assert resp.status_code == 422
+
+
+def test_window_all_or_none_rejects_two_of_three():
+ """Test that providing two of three window fields is rejected."""
+ r = client.post("/soa", json={"name": "Window Two of Three"})
+ soa_id = r.json()["id"]
+
+ resp = client.post(
+ f"/soa/{soa_id}/timings",
+ json={"name": "T1", "window_lower": "-P1D", "window_upper": "P2D"},
+ )
+ assert resp.status_code == 422
+
+
+def test_window_all_or_none_update_rejects_partial():
+ """Test that PATCH update also enforces all-or-nothing window rule."""
+ r = client.post("/soa", json={"name": "Window Update Partial"})
+ soa_id = r.json()["id"]
+
+ timing_resp = client.post(f"/soa/{soa_id}/timings", json={"name": "T1"})
+ timing_id = timing_resp.json()["id"]
+
+ resp = client.patch(
+ f"/soa/{soa_id}/timings/{timing_id}",
+ json={"name": "T1", "window_lower": "-P1D"},
+ )
+ assert resp.status_code == 422
+
+
+def test_window_all_or_none_rejects_whitespace_only_label():
+ """Test that whitespace-only window_label counts as absent."""
+ r = client.post("/soa", json={"name": "Window Whitespace Label"})
+ soa_id = r.json()["id"]
+
+ resp = client.post(
+ f"/soa/{soa_id}/timings",
+ json={
+ "name": "T1",
+ "window_lower": "-P1D",
+ "window_upper": "P2D",
+ "window_label": " ",
+ },
+ )
+ assert resp.status_code == 422
From c50a8f181626e5dd0e1be485d9737db89c18f1da Mon Sep 17 00:00:00 2001
From: Darren <3921919+pendingintent@users.noreply.github.com>
Date: Sun, 8 Feb 2026 16:04:19 -0600
Subject: [PATCH 07/18] Updated requirements.txt
---
requirements.txt | 6 ------
1 file changed, 6 deletions(-)
diff --git a/requirements.txt b/requirements.txt
index 6d8c903..3e9ea6f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -7,12 +7,6 @@ et_xmlfile==2.0.0
fhir.resources==8.2.0
fhir_core==1.1.5
idna==3.11
-iniconfig==2.3.0
-Jinja2==3.1.6
-MarkupSafe==3.0.3
-mccabe==0.7.0
-mypy_extensions==1.1.0
-nodeenv==1.9.1
numpy==2.4.2
openpyxl==3.1.5
pandas==3.0.0
From 80cedf0c2c914dfd686b76aa417532f7f360ada1 Mon Sep 17 00:00:00 2001
From: Darren <3921919+pendingintent@users.noreply.github.com>
Date: Sun, 8 Feb 2026 16:05:01 -0600
Subject: [PATCH 08/18] Updated requirements.txt
---
requirements.txt | 1 -
1 file changed, 1 deletion(-)
diff --git a/requirements.txt b/requirements.txt
index 3e9ea6f..a796ccd 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -7,7 +7,6 @@ et_xmlfile==2.0.0
fhir.resources==8.2.0
fhir_core==1.1.5
idna==3.11
-numpy==2.4.2
openpyxl==3.1.5
pandas==3.0.0
pydantic==2.12.5
From aa577411c7fce134d13d86fb51ef61958899ef8e Mon Sep 17 00:00:00 2001
From: Darren <3921919+pendingintent@users.noreply.github.com>
Date: Sun, 8 Feb 2026 16:18:04 -0600
Subject: [PATCH 09/18] Updated to use python 3.13
---
.github/workflows/python-app.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml
index ff915c9..56cbebc 100644
--- a/.github/workflows/python-app.yml
+++ b/.github/workflows/python-app.yml
@@ -19,10 +19,10 @@ jobs:
steps:
- uses: actions/checkout@v4
- - name: Set up Python 3.10
+ - name: Set up Python 3.13
uses: actions/setup-python@v3
with:
- python-version: "3.10"
+ python-version: "3.13"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
From 10f0b8f438985dda3e53f6adb42c2a38c4b6bb95 Mon Sep 17 00:00:00 2001
From: Darren <3921919+pendingintent@users.noreply.github.com>
Date: Sun, 8 Feb 2026 16:21:17 -0600
Subject: [PATCH 10/18] Updated colspan from 12 to 16 to match the 16
columns
---
src/soa_builder/web/templates/timings.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/soa_builder/web/templates/timings.html b/src/soa_builder/web/templates/timings.html
index 6b909d4..2d23f9d 100644
--- a/src/soa_builder/web/templates/timings.html
+++ b/src/soa_builder/web/templates/timings.html
@@ -175,7 +175,7 @@ No timings yet.
+ | No timings yet. |
{% endfor %}
From 6125ae42ebeb5867b77500d8e0e929a67246e47c Mon Sep 17 00:00:00 2001
From: Darren <3921919+pendingintent@users.noreply.github.com>
Date: Sun, 8 Feb 2026 16:23:45 -0600
Subject: [PATCH 11/18] at least one time component when T is present
---
src/soa_builder/web/schemas.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/soa_builder/web/schemas.py b/src/soa_builder/web/schemas.py
index 0132106..8b837ec 100644
--- a/src/soa_builder/web/schemas.py
+++ b/src/soa_builder/web/schemas.py
@@ -5,7 +5,7 @@
# 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+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$"
+ r"^-?P-?(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$"
)
From 7b0e3aad36eeab1c02f7fc5b27956cd4caf396f2 Mon Sep 17 00:00:00 2001
From: Darren <3921919+pendingintent@users.noreply.github.com>
Date: Sun, 8 Feb 2026 16:26:37 -0600
Subject: [PATCH 12/18] Updated with latest versions
---
requirements.txt | 1 +
1 file changed, 1 insertion(+)
diff --git a/requirements.txt b/requirements.txt
index a796ccd..3e9ea6f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -7,6 +7,7 @@ et_xmlfile==2.0.0
fhir.resources==8.2.0
fhir_core==1.1.5
idna==3.11
+numpy==2.4.2
openpyxl==3.1.5
pandas==3.0.0
pydantic==2.12.5
From 3785d1d8a21226f806e58dfb723feb9eba0ad04c Mon Sep 17 00:00:00 2001
From: Darren <3921919+pendingintent@users.noreply.github.com>
Date: Sun, 8 Feb 2026 16:29:07 -0600
Subject: [PATCH 13/18] Updated
---
requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements.txt b/requirements.txt
index 3e9ea6f..beb534a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -22,4 +22,4 @@ typing-inspection==0.4.2
typing_extensions==4.15.0
urllib3==2.6.3
usdm==0.66.0
-yattag==1.16.1
+yattag==1.16.1
\ No newline at end of file
From 2becba46db46e7199312e9b3eefc05765cc211b0 Mon Sep 17 00:00:00 2001
From: Darren <3921919+pendingintent@users.noreply.github.com>
Date: Sun, 8 Feb 2026 16:32:11 -0600
Subject: [PATCH 14/18] Added fastapi
---
requirements.txt | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/requirements.txt b/requirements.txt
index beb534a..f662459 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,9 +1,12 @@
+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
docraptor==3.1.0
et_xmlfile==2.0.0
+fastapi==0.128.5
fhir.resources==8.2.0
fhir_core==1.1.5
idna==3.11
@@ -17,9 +20,10 @@ 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
-yattag==1.16.1
\ No newline at end of file
+yattag==1.16.1
From ffe6c061bfa111b512559344f56d9cd9be930c58 Mon Sep 17 00:00:00 2001
From: Darren <3921919+pendingintent@users.noreply.github.com>
Date: Sun, 8 Feb 2026 16:33:37 -0600
Subject: [PATCH 15/18] Added fastapi
---
requirements.txt | 3 +++
1 file changed, 3 insertions(+)
diff --git a/requirements.txt b/requirements.txt
index f662459..67107a9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -9,6 +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
numpy==2.4.2
openpyxl==3.1.5
From 02c22f4f0a1e267a6afb1126c53deef2b232c89c Mon Sep 17 00:00:00 2001
From: Darren <3921919+pendingintent@users.noreply.github.com>
Date: Sun, 8 Feb 2026 16:35:18 -0600
Subject: [PATCH 16/18] Added fastapi
---
requirements.txt | 2 ++
1 file changed, 2 insertions(+)
diff --git a/requirements.txt b/requirements.txt
index 67107a9..3e32a8e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,6 +5,7 @@ beautifulsoup4==4.14.3
certifi==2026.1.4
charset-normalizer==3.4.4
docraptor==3.1.0
+dotenv==0.9.9
et_xmlfile==2.0.0
fastapi==0.128.5
fhir.resources==8.2.0
@@ -19,6 +20,7 @@ pandas==3.0.0
pydantic==2.12.5
pydantic_core==2.41.5
python-dateutil==2.9.0.post0
+python-dotenv==1.2.1
PyYAML==6.0.3
requests==2.32.5
six==1.17.0
From 7b15cd1b30a371430b7c98df3c9718e0d6d5cd76 Mon Sep 17 00:00:00 2001
From: Darren <3921919+pendingintent@users.noreply.github.com>
Date: Sun, 8 Feb 2026 16:37:41 -0600
Subject: [PATCH 17/18] Added
---
requirements.txt | 1 +
1 file changed, 1 insertion(+)
diff --git a/requirements.txt b/requirements.txt
index 3e32a8e..b6c1705 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -21,6 +21,7 @@ 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
From 8da0a66b3c25800425a3824ca819a2fcc3372905 Mon Sep 17 00:00:00 2001
From: Darren <3921919+pendingintent@users.noreply.github.com>
Date: Sun, 8 Feb 2026 16:42:40 -0600
Subject: [PATCH 18/18] Added
---
requirements.txt | 3 +++
1 file changed, 3 insertions(+)
diff --git a/requirements.txt b/requirements.txt
index b6c1705..1f82b74 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,6 +4,7 @@ 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
@@ -14,6 +15,7 @@ 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
@@ -32,4 +34,5 @@ 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
|