|
1 | 1 | import re |
2 | | -from datetime import datetime |
| 2 | +from datetime import datetime, timedelta |
3 | 3 | from decimal import Decimal |
4 | 4 | from typing import Union |
5 | 5 |
|
@@ -104,49 +104,47 @@ def for_date_time(field_value: str, field_location: str): |
104 | 104 | containing a valid datetime. Note that partial dates are valid for FHIR, but are not allowed for this API. |
105 | 105 | Valid formats are any of the following: |
106 | 106 | * 'YYYY-MM-DD' - Full date only |
107 | | - * 'YYYY-MM-DDT00:00:00+00:00' - Full date, time without milliseconds, timezone |
108 | | - * 'YYYY-MM-DDT00:00:00.000+00:00' - Full date, time with milliseconds (any level of precision), timezone |
| 107 | + * 'YYYY-MM-DDThh:mm:ss' - Full date, time without milliseconds |
| 108 | + * 'YYYY-MM-DDThh:mm:ss.f' - Full date, time with milliseconds (any level of precision) |
| 109 | + * 'YYYY-MM-DDThh:mm:ss%z' - Full date, time without milliseconds, timezone |
| 110 | + * 'YYYY-MM-DDThh:mm:ss.f%z' - Full date, time with milliseconds (any level of precision), timezone |
109 | 111 | """ |
110 | 112 |
|
111 | 113 | if not isinstance(field_value, str): |
112 | 114 | raise TypeError(f"{field_location} must be a string") |
113 | 115 |
|
114 | 116 | error_message = ( |
115 | | - f"{field_location} must be a valid datetime in the format 'YYYY-MM-DDThh:mm:ss+zz:zz' (where time element " |
116 | | - + "is optional, timezone must be given if and only if time is given, and milliseconds can be optionally " |
117 | | - + "included after the seconds). Note that partial dates are not allowed for " |
118 | | - + f"{field_location} for this service." |
| 117 | + f"{field_location} must be a valid datetime in one of the following formats:\n" |
| 118 | + "- 'YYYY-MM-DD' — Full date only\n" |
| 119 | + "- 'YYYY-MM-DDThh:mm:ss' — Full date and time without milliseconds\n" |
| 120 | + "- 'YYYY-MM-DDThh:mm:ss.f' — Full date and time with milliseconds (any level of precision)\n" |
| 121 | + "- 'YYYY-MM-DDThh:mm:ss%z' — Full date and time with timezone (e.g. +00:00 or +01:00)\n" |
| 122 | + "- 'YYYY-MM-DDThh:mm:ss.f%z' — Full date and time with milliseconds and timezone\n\n" |
| 123 | + "Only '+00:00' and '+01:00' are accepted as valid timezone offsets.\n" |
| 124 | + f"Note that partial dates are not allowed for {field_location} in this service." |
119 | 125 | ) |
120 | 126 |
|
121 | | - # Full date only |
122 | | - if "T" not in field_value: |
123 | | - try: |
124 | | - datetime.strptime(field_value, "%Y-%m-%d") |
125 | | - except ValueError as error: |
126 | | - raise ValueError(error_message) from error |
| 127 | + allowed_suffixes = {"+00:00", "+01:00", "+0000", "+0100",} |
127 | 128 |
|
128 | | - else: |
| 129 | + # List of accepted strict formats |
| 130 | + formats = [ |
| 131 | + "%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M:%S.%f", |
| 132 | + "%Y-%m-%dT%H:%M:%S%z", "%Y-%m-%dT%H:%M:%S.%f%z", |
| 133 | + ] |
129 | 134 |
|
130 | | - # Using %z in datetime.strptime function is more permissive than FHIR, |
131 | | - # so check that timezone meets FHIR format requirements first |
132 | | - timezone_pattern = re.compile(r"(\+|-)\d{2}:\d{2}") |
133 | | - if not timezone_pattern.fullmatch(field_value[-6:]): |
134 | | - raise ValueError(error_message) |
135 | | - |
136 | | - # Full date, time without milliseconds, timezone |
137 | | - if "." not in field_value: |
138 | | - try: |
139 | | - datetime.strptime(field_value, "%Y-%m-%dT%H:%M:%S%z") |
140 | | - except ValueError as error: |
141 | | - raise ValueError(error_message) from error |
142 | | - |
143 | | - # Full date, time with milliseconds, timezone |
144 | | - else: |
145 | | - try: |
146 | | - datetime.strptime(field_value, "%Y-%m-%dT%H:%M:%S.%f%z") |
147 | | - except ValueError as error: |
148 | | - raise ValueError(error_message) from error |
149 | | - |
| 135 | + for fmt in formats: |
| 136 | + try: |
| 137 | + fhir_date = datetime.strptime(field_value, fmt) |
| 138 | + |
| 139 | + if fhir_date.tzinfo is not None: |
| 140 | + if not any(field_value.endswith(suffix) for suffix in allowed_suffixes): |
| 141 | + raise ValueError(error_message) |
| 142 | + return fhir_date.isoformat() |
| 143 | + except ValueError: |
| 144 | + continue |
| 145 | + |
| 146 | + raise ValueError(error_message) |
| 147 | + |
150 | 148 | @staticmethod |
151 | 149 | def for_snomed_code(field_value: str, field_location: str): |
152 | 150 | """ |
|
0 commit comments