Skip to content

Commit 733dab3

Browse files
authored
Merge pull request #71 from TaskarCenterAtUW/feature-dev-1379
Feature 1380
2 parents e5679c6 + 489c868 commit 733dab3

File tree

8 files changed

+272
-30
lines changed

8 files changed

+272
-30
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ Follow the steps to install the node packages required for both building and run
4848
# Installing requirements
4949
pip install -r requirements.txt
5050
```
51+
52+
NOTE: if you have problems building on a Mac, e.g. with uamqb, see here: https://github.com/Azure/azure-uamqp-python/issues/386
53+
5154
### How to Run the Server/APIs
5255
5356
1. The http server by default starts with `8000` port

requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
fastapi~=0.111.1
2-
pydantic==1.10.4
2+
pydantic==1.10.16
33
html_testRunner==1.2.1
44
uvicorn==0.20.0
55
python-ms-core==0.0.22
6-
gtfs-canonical-validator==0.0.5
6+
gtfs-canonical-validator==0.0.5

src/gtfs_flex_validation.py

Lines changed: 74 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,30 @@
77
from .config import Settings
88
from gtfs_canonical_validator import CanonicalValidator
99

10+
# in an effort to be more permissive of small errors, accept these which could conceivably be calculated/fixed/interpreted by common applications
11+
CHANGE_ERROR_TO_WARNING = [ "block_trips_with_overlapping_stop_times", "trip_distance_exceeds_shape_distance", "decreasing_or_equal_stop_time_distance", "decreasing_shape_distance",
12+
"empty_file", "equal_shape_distance_diff_coordinates", "fare_transfer_rule_duration_limit_type_without_duration_limit",
13+
"fare_transfer_rule_duration_limit_without_type", "fare_transfer_rule_invalid_transfer_count", "fare_transfer_rule_missing_transfer_count",
14+
"fare_transfer_rule_with_forbidden_transfer_count", "forbidden_shape_dist_traveled", "invalid_currency", "invalid_currency_amount",
15+
"invalid_url", "location_with_unexpected_stop_time", "missing_trip_edge", "new_line_in_value", "point_near_origin", "point_near_pole",
16+
"route_both_short_and_long_name_missing", "route_networks_specified_in_more_than_one_file", "start_and_end_range_equal", "start_and_end_range_out_of_order",
17+
"station_with_parent_station", "stop_time_timepoint_without_times", "stop_time_with_arrival_before_previous_departure_time",
18+
"stop_time_with_only_arrival_or_departure_time", "stop_without_location", "timeframe_only_start_or_end_time_specified", "timeframe_overlap",
19+
"timeframe_start_or_end_time_greater_than_twenty_four_hours", "u_r_i_syntax_error" ]
20+
21+
FLEX_FATAL_ERROR_CODES = [ "missing_required_element", "unsupported_feature_type", "unsupported_geo_json_type", "unsupported_geometry_type",
22+
"invalid_geometry", "forbidden_prior_day_booking_field_value", "forbidden_prior_notice_start_day", "forbidden_prior_notice_start_time",
23+
"forbidden_real_time_booking_field_value", "forbidden_same_day_booking_field_value", "invalid_prior_notice_duration_min",
24+
"missing_prior_day_booking_field_value", "missing_prior_notice_duration_min", "missing_prior_notice_start_time",
25+
"prior_notice_last_day_after_start_day"]
26+
27+
FLEX_FIELDS = {
28+
'stop_times.txt': [ "start_pickup_dropoff_window", "end_pickup_dropoff_window", "pickup_booking_rule_id", "drop_off_booking_rule_id",
29+
"mean_duration_factor", "mean_duration_offset", "safe_duration_factor", "safe_duration_offset"]
30+
}
31+
32+
FLEX_FILES = ["locations.geojson", "booking_rules.txt", "location_groups.txt", "location_group_stops.txt" ]
33+
1034
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
1135
# Path used for download file generation.
1236
DOWNLOAD_FILE_PATH = f'{Path.cwd()}/downloads'
@@ -47,30 +71,69 @@ def is_gtfs_flex_valid(self) -> tuple[Union[bool, Any], Union[str, Any]]:
4771
logger.info(f' Downloaded file path: {downloaded_file_path}')
4872
flex_validator = CanonicalValidator(zip_file=downloaded_file_path)
4973
result = flex_validator.validate()
74+
5075
is_valid = result.status
76+
if isinstance(result.error, list) and result.error is not None:
77+
for error in result.error[:]:
78+
# change some smaller errors to warnings instead to relax the strict validation MD gives us
79+
if error['code'] in CHANGE_ERROR_TO_WARNING:
80+
if(result.info is None): result.info = []
81+
82+
result.info.append(error)
83+
result.error.remove(error)
84+
continue
85+
86+
# these are error codes from MD that relate to pathways that are fatal
87+
if error['code'] in FLEX_FATAL_ERROR_CODES:
88+
is_valid = False
89+
continue
90+
91+
# some of the notices relate to pathways, but there's no way to tell except with this logic:
92+
for notice in error['sampleNotices']:
93+
# one of the fields in a given file is a pathway-spec field--if it's flagged, fail
94+
if "fieldName" in notice and "filename" in notice:
95+
if notice['filename'] in FLEX_FIELDS and \
96+
notice['fieldName'] in FLEX_FIELDS[notice['filename']]:
97+
is_valid = False
98+
continue
99+
100+
# one of the pathways spec'd files has an error--if so, fail
101+
if "filename" in notice:
102+
if(notice['filename'] in FLEX_FILES):
103+
is_valid = False
104+
continue
105+
106+
# similar to the above, but the field for the filename is parent/child
107+
if "childFilename" in notice:
108+
if(notice['childFilename'] in FLEX_FILES):
109+
is_valid = False
110+
continue
111+
112+
# if all errors have been downgraded to warnings, mark us as a success
113+
if len(result.error) == 0:
114+
is_valid = True
115+
116+
if result.error is not None:
117+
validation_message = str(result.error)
118+
logger.error(f' Error While Validating File: {str(result.error)}')
119+
120+
51121

52-
if result.error is not None:
53-
validation_message = str(result.error)
54-
logger.error(f' Error While Validating File: {str(result.error)}')
55122
GTFSFlexValidation.clean_up(downloaded_file_path)
56123
else:
57124
logger.error(f' Failed to validate because unknown file format')
58125

59126
return is_valid, validation_message
60127

61128
# Downloads the file to local folder of the server
62-
# file_upload_path is the fullUrl of where the
129+
# file_upload_path is the fullUrl of where the
63130
# file is uploaded.
64131
def download_single_file(self, file_upload_path=None) -> str:
65-
is_exists = os.path.exists(DOWNLOAD_FILE_PATH)
66-
if not is_exists:
67-
os.makedirs(DOWNLOAD_FILE_PATH)
68-
69-
unique_folder = self.prefix
132+
unique_folder = self.settings.get_unique_id()
70133
dl_folder_path = os.path.join(DOWNLOAD_FILE_PATH, unique_folder)
71134

72-
# Ensure the unique folder path is created
73-
os.makedirs(dl_folder_path, exist_ok=True)
135+
if not os.path.exists(dl_folder_path):
136+
os.makedirs(dl_folder_path)
74137

75138
file = self.storage_client.get_file_from_url(self.container_name, file_upload_path)
76139
try:
@@ -82,11 +145,9 @@ def download_single_file(self, file_upload_path=None) -> str:
82145
return f'{dl_folder_path}/{file_path}'
83146
else:
84147
logger.info(' File not found!')
85-
raise Exception('File not found!')
86148
except Exception as e:
87149
traceback.print_exc()
88150
logger.error(e)
89-
raise e
90151

91152
@staticmethod
92153
def clean_up(path):
146 KB
Binary file not shown.
9.02 KB
Binary file not shown.
147 KB
Binary file not shown.
146 KB
Binary file not shown.

0 commit comments

Comments
 (0)