Skip to content

Commit 465bc5d

Browse files
authored
fix: process validation report fails when service date fields are empty (#978)
1 parent d074262 commit 465bc5d

File tree

5 files changed

+172
-15
lines changed

5 files changed

+172
-15
lines changed

functions-python/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,13 @@ or
156156
```
157157
scripts/api-tests.sh --folder functions-python
158158
```
159+
160+
To run the tests and generate the html coverage report:
161+
```
162+
scripts/api-tests.sh --folder functions-python --html_report
163+
```
164+
_The coverage reports are located in `{project}/scripts/coverage_reports` as individual folder per function._
165+
159166
This will
160167
- run the `function-python-setup.sh` script for the function (ie create the `shared` and `test_shared` folders with symlinks)
161168
- Create a python virtual environment in the function folder, e.g.: `functions-python/batch_datasets/venv`

functions-python/helpers/tests/test_transform.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from transform import to_boolean
1+
from transform import to_boolean, get_nested_value
22

33

44
def test_to_boolean():
@@ -19,3 +19,44 @@ def test_to_boolean():
1919
assert to_boolean(None) is False
2020
assert to_boolean([]) is False
2121
assert to_boolean({}) is False
22+
23+
24+
def test_get_nested_value():
25+
# Test case 1: Nested dictionary with string value
26+
assert get_nested_value({"a": {"b": {"c": "d"}}}, ["a", "b", "c"]) == "d"
27+
28+
# Test case 2: Nested dictionary with integer value
29+
assert get_nested_value({"a": {"b": {"c": 1}}}, ["a", "b", "c"]) == 1
30+
31+
# Test case 3: Nested dictionary with float value
32+
assert get_nested_value({"a": {"b": {"c": 1.5}}}, ["a", "b", "c"]) == 1.5
33+
34+
# Test case 4: Nested dictionary with boolean value
35+
assert get_nested_value({"a": {"b": {"c": True}}}, ["a", "b", "c"]) is True
36+
37+
# Test case 5: Nested dictionary with string value that needs trimming
38+
assert get_nested_value({"a": {"b": {"c": " d "}}}, ["a", "b", "c"]) == "d"
39+
40+
# Test case 6: Key not found in the dictionary
41+
assert get_nested_value({"a": {"b": {"c": "d"}}}, ["a", "b", "x"]) is None
42+
assert (
43+
get_nested_value({"a": {"b": {"c": "d"}}}, ["a", "b", "x"], "default")
44+
== "default"
45+
)
46+
assert get_nested_value({"a": {"b": {"c": "d"}}}, ["a", "b", "x"], []) == []
47+
48+
# Test case 7: Intermediate key not found in the dictionary
49+
assert get_nested_value({"a": {"b": {"c": "d"}}}, ["a", "x", "c"]) is None
50+
assert (
51+
get_nested_value({"a": {"b": {"c": "d"}}}, ["a", "x", "c"], "default")
52+
== "default"
53+
)
54+
assert get_nested_value({"a": {"b": {"c": "d"}}}, ["a", "x", "c"], []) == []
55+
56+
# Test case 8: Empty keys list
57+
assert get_nested_value({"a": {"b": {"c": "d"}}}, []) is None
58+
assert get_nested_value({"a": {"b": {"c": "d"}}}, [], {}) == {}
59+
60+
# Test case 9: Non-dictionary data
61+
assert get_nested_value("not a dict", ["a", "b", "c"]) is None
62+
assert get_nested_value("not a dict", ["a", "b", "c"], []) == []

functions-python/helpers/transform.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515
#
16+
from typing import List, Optional
1617

1718

1819
def to_boolean(value):
@@ -24,3 +25,31 @@ def to_boolean(value):
2425
if isinstance(value, str):
2526
return value.lower() in ["true", "1", "yes", "y"]
2627
return False
28+
29+
30+
def get_nested_value(
31+
data: dict, keys: List[str], default_value: Optional[any] = None
32+
) -> Optional[any]:
33+
"""
34+
Retrieve the value from a nested dictionary given a list of keys.
35+
36+
Args:
37+
data (dict): The dictionary to search.
38+
keys (List[str]): The list of keys representing the path to the field.
39+
default_value: The value to return if the field is not found.
40+
41+
Returns:
42+
Optional[any]: The value if found and valid, otherwise None. The str values are trimmed.
43+
"""
44+
if not keys:
45+
return default_value
46+
current_data = data
47+
for key in keys:
48+
if isinstance(current_data, dict) and key in current_data:
49+
current_data = current_data[key]
50+
else:
51+
return default_value
52+
if isinstance(current_data, str):
53+
result = current_data.strip()
54+
return result if result else default_value
55+
return current_data

functions-python/process_validation_report/src/main.py

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
Gtfsdataset,
2828
)
2929
from shared.helpers.logger import Logger
30+
from shared.helpers.transform import get_nested_value
3031

3132
logging.basicConfig(level=logging.INFO)
3233

@@ -149,20 +150,9 @@ def generate_report_entities(
149150
dataset = get_dataset(dataset_stable_id, session)
150151
dataset.validation_reports.append(validation_report_entity)
151152

152-
if (
153-
"summary" in json_report
154-
and "feedInfo" in json_report["summary"]
155-
and "feedServiceWindowStart" in json_report["summary"]["feedInfo"]
156-
and "feedServiceWindowEnd" in json_report["summary"]["feedInfo"]
157-
):
158-
dataset.service_date_range_start = json_report["summary"]["feedInfo"][
159-
"feedServiceWindowStart"
160-
]
161-
dataset.service_date_range_end = json_report["summary"]["feedInfo"][
162-
"feedServiceWindowEnd"
163-
]
164-
165-
for feature_name in json_report["summary"]["gtfsFeatures"]:
153+
populate_service_date(dataset, json_report)
154+
155+
for feature_name in get_nested_value(json_report, ["summary", "gtfsFeatures"], []):
166156
feature = get_feature(feature_name, session)
167157
feature.validations.append(validation_report_entity)
168158
entities.append(feature)
@@ -179,6 +169,23 @@ def generate_report_entities(
179169
return entities
180170

181171

172+
def populate_service_date(dataset, json_report):
173+
"""
174+
Populates the service date range of the dataset based on the JSON report.
175+
The service date range is extracted from the feedServiceWindowStart and feedServiceWindowEnd fields,
176+
if both are present and not empty.
177+
"""
178+
feed_service_window_start = get_nested_value(
179+
json_report, ["summary", "feedInfo", "feedServiceWindowStart"]
180+
)
181+
feed_service_window_end = get_nested_value(
182+
json_report, ["summary", "feedInfo", "feedServiceWindowEnd"]
183+
)
184+
if feed_service_window_start and feed_service_window_end:
185+
dataset.service_date_range_start = feed_service_window_start
186+
dataset.service_date_range_end = feed_service_window_end
187+
188+
182189
def create_validation_report_entities(feed_stable_id, dataset_stable_id, version):
183190
"""
184191
Creates and stores entities based on a validation report.

functions-python/process_validation_report/tests/test_validation_report.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
get_dataset,
2020
create_validation_report_entities,
2121
process_validation_report,
22+
populate_service_date,
2223
)
2324

2425
faker = Faker()
@@ -206,3 +207,75 @@ def test_process_validation_report_invalid_request(
206207
__, status = process_validation_report(request)
207208
self.assertEqual(status, 400)
208209
create_validation_report_entities_mock.assert_not_called()
210+
211+
def test_populate_service_date_valid_dates(self):
212+
"""Test populate_service_date function with valid date values."""
213+
dataset = Gtfsdataset(
214+
id=faker.word(), feed_id=faker.word(), stable_id=faker.word(), latest=True
215+
)
216+
json_report = {
217+
"summary": {
218+
"feedInfo": {
219+
"feedServiceWindowStart": "2024-01-01",
220+
"feedServiceWindowEnd": "2024-12-31",
221+
}
222+
}
223+
}
224+
225+
populate_service_date(dataset, json_report)
226+
227+
self.assertEqual(dataset.service_date_range_start, "2024-01-01")
228+
self.assertEqual(dataset.service_date_range_end, "2024-12-31")
229+
230+
def test_populate_service_date_valid_empty_dates(self):
231+
"""Test populate_service_date function."""
232+
dataset = Gtfsdataset(
233+
id=faker.word(), feed_id=faker.word(), stable_id=faker.word(), latest=True
234+
)
235+
json_report = {
236+
"summary": {
237+
"feedInfo": {
238+
"feedServiceWindowStart": "",
239+
"feedServiceWindowEnd": "2024-12-31",
240+
}
241+
}
242+
}
243+
populate_service_date(dataset, json_report)
244+
self.assertEqual(dataset.service_date_range_start, None)
245+
self.assertEqual(dataset.service_date_range_end, None)
246+
247+
json_report = {
248+
"summary": {
249+
"feedInfo": {
250+
"feedServiceWindowStart": "2024-12-31",
251+
"feedServiceWindowEnd": "",
252+
}
253+
}
254+
}
255+
populate_service_date(dataset, json_report)
256+
self.assertEqual(dataset.service_date_range_start, None)
257+
self.assertEqual(dataset.service_date_range_end, None)
258+
259+
json_report = {
260+
"summary": {
261+
"feedInfo": {
262+
"feedServiceWindowStart": "2024-12-31",
263+
"feedServiceWindowEnd": None,
264+
}
265+
}
266+
}
267+
populate_service_date(dataset, json_report)
268+
self.assertEqual(dataset.service_date_range_start, None)
269+
self.assertEqual(dataset.service_date_range_end, None)
270+
271+
json_report = {
272+
"summary": {
273+
"feedInfo": {
274+
"feedServiceWindowStart": None,
275+
"feedServiceWindowEnd": "2024-12-31",
276+
}
277+
}
278+
}
279+
populate_service_date(dataset, json_report)
280+
self.assertEqual(dataset.service_date_range_start, None)
281+
self.assertEqual(dataset.service_date_range_end, None)

0 commit comments

Comments
 (0)