diff --git a/.gitignore b/.gitignore index 6a4dae1..56b5200 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .DS_Store __pycache__ +.vscode \ No newline at end of file diff --git a/.style.yapf b/.style.yapf new file mode 100644 index 0000000..8e504e5 --- /dev/null +++ b/.style.yapf @@ -0,0 +1,2 @@ +[style] +based_on_style = google \ No newline at end of file diff --git a/README.md b/README.md index aeec274..9c9341b 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,13 @@ This tool is based on the fares-v2 [draft specification](https://docs.google.com The tool validates ONLY fares-v2 specific files and dependent files, and does NOT validate GTFS schedule data. -The tool does NOT read areas from stop_times.txt for performance reasons, but can using the -s option defined below. - ## Requirements python 3 ## Validate a fares dataset -`python3 validate.py PATH-TO-FOLDER-CONTAINING-FARES-V2-DATASET [-s, --read-stop-times] [-o, --output-file FILE-TO-EXPORT-VALIDATION-REPORT-TO]` +`python3 validate.py PATH-TO-FOLDER-CONTAINING-FARES-V2-DATASET [-o, --output-file FILE-TO-EXPORT-VALIDATION-REPORT-TO]` For example: diff --git a/fares_validator/__main__.py b/fares_validator/__main__.py index 409a56f..e1e91e4 100644 --- a/fares_validator/__main__.py +++ b/fares_validator/__main__.py @@ -6,9 +6,18 @@ def main(): parser = argparse.ArgumentParser(description='Validate GTFS fares-v2 data.') - parser.add_argument("-s", "--read-stop-times", help="Scan stop_times for area_ids", action='store_true') - parser.add_argument("-o", "--output-file", type=str, help="Export the errors and warnings to a file") - parser.add_argument("input_gtfs_folder", type=str, help="Path to unzipped folder containing the Fares-v2 GTFS") + parser.add_argument("-s", + "--read-stop-times", + help="Scan stop_times for area_ids", + action='store_true') + parser.add_argument("-o", + "--output-file", + type=str, + help="Export the errors and warnings to a file") + parser.add_argument( + "input_gtfs_folder", + type=str, + help="Path to unzipped folder containing the Fares-v2 GTFS") args = parser.parse_args() @@ -25,7 +34,9 @@ def main(): f = open(args.output_file, 'w') f.write(output) except Exception: - raise Exception('Writing to output file failed. Please ensure the output file path is valid.') + raise Exception( + 'Writing to output file failed. Please ensure the output file path is valid.' + ) else: print(output) diff --git a/fares_validator/diagnostics.py b/fares_validator/diagnostics.py index 38b5ce8..ecd6753 100644 --- a/fares_validator/diagnostics.py +++ b/fares_validator/diagnostics.py @@ -13,6 +13,7 @@ def format(code, line_context='', path='', extra_info=''): class Diagnostics: + def __init__(self): self.errors = [] self.warnings = [] diff --git a/fares_validator/errors.py b/fares_validator/errors.py index f74406d..18ef14e 100644 --- a/fares_validator/errors.py +++ b/fares_validator/errors.py @@ -1,9 +1,6 @@ # generic errors (see utils.py) AMOUNT_WITH_MIN_OR_MAX_AMOUNT = 'An amount is defined alongside at least one of min_ or max_amount.' AMOUNT_WITHOUT_CURRENCY = 'An amount field is defined without a currency to accompany it.' -CONFLICTING_FARE_CONTAINER_ON_FARE_PRODUCT = 'A fare_container referenced conflicts with the fare_container on the fare_product.' -CONFLICTING_RIDER_CATEGORY_ON_FARE_CONTAINER = 'A rider_category referenced conflicts with the rider_category on the fare_container.' -CONFLICTING_RIDER_CATEGORY_ON_FARE_PRODUCT = 'A rider_category referenced conflicts with the rider_category on the fare_product.' CURRENCY_WITHOUT_AMOUNT = 'A currency is defined without an amount field to accompany it.' FOREIGN_ID_INVALID = 'An id defined in a dependent table is referenced, but does not exist in that table.' INVALID_AMOUNT_FORMAT = 'An amount field is defined, but is not an integer or float.' @@ -16,16 +13,13 @@ UNRECOGNIZED_CURRENCY_CODE = 'A currency code is unrecognized.' # areas.txt -DUPLICATE_AREAS_TXT_ENTRY = 'There are two entries in areas.txt with the same area_id and greater_area_id.' +DUPLICATE_AREAS_TXT_ENTRY = 'There are two entries in areas.txt with the same area_id.' EMPTY_AREA_ID_AREAS = 'An entry in areas.txt has empty area_id.' -GREATER_AREA_ID_LOOP = 'Some area_ids have themselves as greater_area_ids.' -UNDEFINED_GREATER_AREA_ID = 'A greater_area_id is not defined as an area_id in areas.txt.' # stop_areas.txt +DUPLICATE_STOP_AREAS_TXT_ENTRY = 'There are two entries in stop_areas.txt with the same area_id and stop_id.' EMPTY_AREA_ID_STOP_AREAS = 'An entry in stop_areas.txt has empty area_id.' EMPTY_STOP_ID_STOP_AREAS = 'An entry in stop_areas.txt has empty stop_id.' -INVALID_AREA_ID = 'An entry in stop_areas.txt references a non-existent area_id.' -INVALID_STOP_ID = 'An entry in stop_areas.txt references a non-existent stop_id.' # calendar.txt, calendar_dates.txt DUPLICATE_SERVICE_ID = 'A service_id is defined twice in calendar.txt.' @@ -73,36 +67,23 @@ TIMEFRAME_TYPE_WITHOUT_TIMEFRAME = 'A timeframe_type in fare_products.txt is defined without a timeframe_id.' # fare_leg_rules.txt -AMOUNT_WITH_FARE_PRODUCT = 'An entry in fare_leg_rules.txt has both a fare_product and an amount field defined.' -AREA_WITHOUT_IS_SYMMETRICAL = 'A from_ and/or to_area in fare_leg_rules.txt is defined without is_symmetrical.' -CONTAINS_AREA_WITHOUT_FROM_TO_AREA = 'A contains_area in fare_leg_rules.txt is defined without a from and to area.' DISTANCE_TYPE_WITHOUT_DISTANCE = 'A distance_type in fare_leg_rules.txt is defined without a min_ or max_distance.' DISTANCE_WITHOUT_DISTANCE_TYPE = 'A min_ or max_distance in fare_leg_rules.txt is defined without a distance_type.' FARE_LEG_NAME_WITH_FARE_PRODUCT = 'An entry in fare_leg_rules.txt has both a fare_product and a fare_leg_name field defined.' INVALID_DISTANCE_TYPE = 'A distance_type in fare_leg_rules.txt has an invalid value.' -INVALID_IS_SYMMETRICAL_LEG_RULES = 'An is_symmetrical in fare_leg_rules.txt is not one of the accepted values.' INVALID_MAX_DISTANCE = 'A max_distance in fare_leg_rules.txt is not a float.' INVALID_MIN_DISTANCE = 'A min_distance in fare_leg_rules.txt is not a float.' -IS_SYMMETRICAL_WITHOUT_FROM_TO_AREA = 'An is_symmetrical in fare_leg_rules.txt is defined without a from_ and/or to_area.' NEGATIVE_MAX_DISTANCE = 'A max_distance in fare_leg_rules.txt is negative.' NEGATIVE_MIN_DISTANCE = 'A min_distance in fare_leg_rules.txt is negative.' # fare_transfer_rules.txt -AMOUNT_WITHOUT_FARE_TRANSFER_TYPE = 'An entry in fare_transfer_rules.txt has an amount field defined without fare_transfer_type.' DURATION_LIMIT_WITHOUT_LIMIT_TYPE = 'An entry in fare_transfer_rules.txt has duration_limit without duration_limit_type.' DURATION_LIMIT_TYPE_WITHOUT_DURATION = 'An entry in fare_transfer_rules.txt has duration_limit_type without duration_limit.' -FARE_TRANSFER_TYPE_WITHOUT_AMOUNT = 'An entry in fare_transfer_rules.txt has fare_transfer_type defined without an amount field.' INVALID_DURATION_LIMIT = 'An entry in fare_transfer_rules.txt has duration_limit with invalid value.' INVALID_DURATION_LIMIT_TYPE = 'An entry in fare_transfer_rules.txt has duration_limit_type with invalid value.' INVALID_FARE_TRANSFER_TYPE = 'An entry in fare_transfer_rules.txt has fare_transfer_type with invalid value.' INVALID_FROM_LEG_GROUP = 'A from_leg_group_id in fare_transfer_rules.txt is not defined in fare_leg_rules.txt.' -INVALID_IS_SYMMETRICAL_TRANSFER_RULES = 'An is_symmetrical in fare_transfer_rules.txt is not one of the accepted values.' -INVALID_SPANNING_LIMIT = 'An entry in fare_transfer_rules.txt has spanning_limit with incorrect type or invalid integer value.' +INVALID_TRANSFER_COUNT = 'An entry in fare_transfer_rules.txt has transfer_count with incorrect type or invalid integer value.' INVALID_TO_LEG_GROUP = 'A to_leg_group_id in fare_transfer_rules.txt is not defined in fare_leg_rules.txt.' -INVALID_TRANSFER_SEQUENCE = 'A transfer_sequence in fare_transfer_rules.txt has incorrect type or invalid integer value.' -IS_SYMMETRICAL_WITHOUT_FROM_TO_LEG_GROUP = 'An is_symmetrical in fare_transfer_rules.txt is defined without a from_ and/or to_leg_group_id.' -LEG_GROUP_WITHOUT_IS_SYMMETRICAL = 'A from_ and/or to_leg_group_id in fare_transfer_rules.txt is defined without is_symmetrical.' -SPANNING_LIMIT_WITH_BAD_LEGS = 'An entry in fare_transfer_rules.txt has spanning_limit with different from and to leg group ids.' -SPANNING_LIMIT_WITH_TRANSFER_ID = 'An entry in fare_transfer_rules.txt has spanning_limit with transfer_id defined.' -TRANSFER_ID_WITHOUT_TRANSFER_SEQUENCE = 'A transfer_id in fare_transfer_rules.txt is defined without a transfer_sequence.' -TRANSFER_SEQUENCE_WITHOUT_TRANSFER_ID = 'A transfer_sequence in fare_transfer_rules.txt is defined without a transfer_id.' +NONEXISTENT_FILTER_FARE_PRODUCT_ID = 'A filter_fare_product referenced is not defined in fare_products.txt.' +TRANSFER_COUNT_WITH_BAD_LEGS = 'An entry in fare_transfer_rules.txt has transfer_count with different from and to leg group ids.' diff --git a/fares_validator/fare_leg_rule_checkers.py b/fares_validator/fare_leg_rule_checkers.py index b84d0ac..e1c7693 100644 --- a/fares_validator/fare_leg_rule_checkers.py +++ b/fares_validator/fare_leg_rule_checkers.py @@ -3,18 +3,6 @@ def check_areas(line, areas, unused_areas): - if line.is_symmetrical and line.is_symmetrical not in {'0', '1'}: - line.add_error(INVALID_IS_SYMMETRICAL_LEG_RULES) - - if line.contains_area_id and (not line.from_area_id and not line.to_area_id): - line.add_error(CONTAINS_AREA_WITHOUT_FROM_TO_AREA) - - if (line.from_area_id or line.to_area_id) and not line.is_symmetrical: - line.add_error(AREA_WITHOUT_IS_SYMMETRICAL) - - if (not line.from_area_id and not line.to_area_id) and line.is_symmetrical: - line.add_error(IS_SYMMETRICAL_WITHOUT_FROM_TO_AREA) - if line.from_area_id and line.from_area_id in unused_areas: unused_areas.remove(line.from_area_id) if line.to_area_id and line.to_area_id in unused_areas: @@ -22,7 +10,6 @@ def check_areas(line, areas, unused_areas): utils.check_linked_id(line, 'from_area_id', areas) utils.check_linked_id(line, 'to_area_id', areas) - utils.check_linked_id(line, 'contains_area_id', areas) def check_distances(line): diff --git a/fares_validator/fare_product_checkers.py b/fares_validator/fare_product_checkers.py index 6bfdb95..8ff1d5a 100644 --- a/fares_validator/fare_product_checkers.py +++ b/fares_validator/fare_product_checkers.py @@ -3,13 +3,17 @@ class LinkedEntities: + def __init__(self): self.rider_category_ids = set() self.fare_container_ids = set() -def check_linked_fp_entities(line, rider_categories, rider_category_by_fare_container, linked_entities_by_fare_product): - linked_entities = linked_entities_by_fare_product.setdefault(line.fare_product_id, LinkedEntities()) +def check_linked_fp_entities(line, rider_categories, + rider_category_by_fare_container, + linked_entities_by_fare_product): + linked_entities = linked_entities_by_fare_product.setdefault( + line.fare_product_id, LinkedEntities()) if line.rider_category_id: linked_entities.rider_category_ids.add(line.rider_category_id) @@ -23,8 +27,10 @@ def check_linked_fp_entities(line, rider_categories, rider_category_by_fare_cont if line.fare_container_id not in rider_category_by_fare_container: line.add_error(NONEXISTENT_FARE_CONTAINER_ID) - fare_container_rider_cat = rider_category_by_fare_container.get(line.fare_container_id) - if line.rider_category_id and fare_container_rider_cat and (line.rider_category_id != fare_container_rider_cat): + fare_container_rider_cat = rider_category_by_fare_container.get( + line.fare_container_id) + if line.rider_category_id and fare_container_rider_cat and ( + line.rider_category_id != fare_container_rider_cat): line.add_error(CONFLICTING_RIDER_CATEGORY_ON_FARE_CONTAINER) else: linked_entities.fare_container_ids.add('') @@ -46,7 +52,9 @@ def check_durations_and_offsets(line): if line.duration_start and line.duration_start not in {'0', '1'}: line.add_error(INVALID_DURATION_START) - if line.duration_unit and line.duration_unit not in {'0', '1', '2', '3', '4', '5', '6'}: + if line.duration_unit and line.duration_unit not in { + '0', '1', '2', '3', '4', '5', '6' + }: line.add_error(INVALID_DURATION_UNIT) if line.duration_type and line.duration_type not in {'1', '2'}: @@ -74,7 +82,9 @@ def check_durations_and_offsets(line): if line.duration_unit: line.add_error(DURATION_UNIT_WITHOUT_AMOUNT) - if line.offset_unit and line.offset_unit not in {'0', '1', '2', '3', '4', '5', '6'}: + if line.offset_unit and line.offset_unit not in { + '0', '1', '2', '3', '4', '5', '6' + }: line.add_error(INVALID_OFFSET_UNIT) if line.offset_amount: diff --git a/fares_validator/fare_transfer_rule_checkers.py b/fares_validator/fare_transfer_rule_checkers.py index ee3efa9..b12f45d 100644 --- a/fares_validator/fare_transfer_rule_checkers.py +++ b/fares_validator/fare_transfer_rule_checkers.py @@ -2,12 +2,6 @@ def check_leg_groups(line, leg_group_ids, unused_leg_groups): - if line.is_symmetrical and line.is_symmetrical not in {'0', '1'}: - line.add_error(INVALID_IS_SYMMETRICAL_TRANSFER_RULES) - if (line.from_leg_group_id or line.to_leg_group_id) and not line.is_symmetrical: - line.add_error(LEG_GROUP_WITHOUT_IS_SYMMETRICAL) - if (not line.from_leg_group_id and not line.to_leg_group_id) and line.is_symmetrical: - line.add_error(IS_SYMMETRICAL_WITHOUT_FROM_TO_LEG_GROUP) if line.from_leg_group_id and not line.from_leg_group_id in leg_group_ids: line.add_error(INVALID_FROM_LEG_GROUP) if line.to_leg_group_id and not line.to_leg_group_id in leg_group_ids: @@ -19,36 +13,22 @@ def check_leg_groups(line, leg_group_ids, unused_leg_groups): unused_leg_groups.remove(line.to_leg_group_id) -def check_spans_and_transfer_ids(line): - if line.spanning_limit: +def check_transfer_count(line): + if line.transfer_count: if line.from_leg_group_id != line.to_leg_group_id: - line.add_error(SPANNING_LIMIT_WITH_BAD_LEGS) - if line.transfer_id: - line.add_error(SPANNING_LIMIT_WITH_TRANSFER_ID) + line.add_error(TRANSFER_COUNT_WITH_BAD_LEGS) try: - limit = int(line.spanning_limit) - if limit <= 1: - line.add_error(INVALID_SPANNING_LIMIT) + limit = int(line.transfer_count) + if limit < 1 and limit != -1: + line.add_error(INVALID_TRANSFER_COUNT) except ValueError: - line.add_error(INVALID_SPANNING_LIMIT) - - if line.transfer_id: - if not line.transfer_sequence: - line.add_error(TRANSFER_ID_WITHOUT_TRANSFER_SEQUENCE) - - if line.transfer_sequence: - if not line.transfer_id: - line.add_error(TRANSFER_SEQUENCE_WITHOUT_TRANSFER_ID) - try: - seq = int(line.transfer_sequence) - if seq < 1: - line.add_error(INVALID_TRANSFER_SEQUENCE) - except ValueError: - line.add_error(INVALID_TRANSFER_SEQUENCE) + line.add_error(INVALID_TRANSFER_COUNT) def check_durations(line): - if line.duration_limit_type and line.duration_limit_type not in {'0', '1', '2', '3'}: + if line.duration_limit_type and line.duration_limit_type not in { + '0', '1', '2', '3' + }: line.add_error(INVALID_DURATION_LIMIT_TYPE) if line.duration_limit: diff --git a/fares_validator/loader.py b/fares_validator/loader.py index 079d38c..8ff957f 100644 --- a/fares_validator/loader.py +++ b/fares_validator/loader.py @@ -23,31 +23,28 @@ def run_validator(gtfs_root_dir, should_read_stop_times): gtfs.networks = read_gtfs_entities.networks(gtfs_root_dir, results) - read_gtfs_entities.read_areas_in_stop_files(gtfs_root_dir, gtfs.areas, results, should_read_stop_times) - gtfs.service_ids = read_gtfs_entities.service_ids(gtfs_root_dir, results) gtfs.timeframe_ids = read_fares_entities.timeframes(gtfs_root_dir, results) unused_timeframes = gtfs.timeframe_ids.copy() - gtfs.rider_category_ids = read_fares_entities.rider_categories(gtfs_root_dir, results) + gtfs.rider_category_ids = read_fares_entities.rider_categories( + gtfs_root_dir, results) - gtfs.rider_category_by_fare_container = read_fares_entities.fare_containers(gtfs_root_dir, - gtfs.rider_category_ids, - results) + gtfs.rider_category_by_fare_container = read_fares_entities.fare_containers( + gtfs_root_dir, gtfs.rider_category_ids, results) - gtfs.linked_entities_by_fare_product = read_fares_entities.fare_products(gtfs_root_dir, - gtfs, - unused_timeframes, - results) + gtfs.linked_entities_by_fare_product = read_fares_entities.fare_products( + gtfs_root_dir, gtfs, unused_timeframes, results) - gtfs.leg_group_ids = read_fares_entities.fare_leg_rules(gtfs_root_dir, gtfs, - unused_timeframes, results) + gtfs.leg_group_ids = read_fares_entities.fare_leg_rules( + gtfs_root_dir, gtfs, unused_timeframes, results) read_fares_entities.fare_transfer_rules(gtfs_root_dir, gtfs, results) if len(unused_timeframes): warning_info = 'Unused timeframes: ' + str(unused_timeframes) - results.add_warning(diagnostics.format(warn.UNUSED_TIMEFRAME_IDS, '', '', warning_info)) + results.add_warning( + diagnostics.format(warn.UNUSED_TIMEFRAME_IDS, '', '', warning_info)) return results diff --git a/fares_validator/read_fares_entities.py b/fares_validator/read_fares_entities.py index 26206f3..957fea1 100644 --- a/fares_validator/read_fares_entities.py +++ b/fares_validator/read_fares_entities.py @@ -6,19 +6,27 @@ from .errors import * from .fare_leg_rule_checkers import check_areas, check_distances from .fare_product_checkers import check_linked_fp_entities, check_bundle, check_durations_and_offsets -from .fare_transfer_rule_checkers import check_leg_groups, check_spans_and_transfer_ids, check_durations -from .utils import check_fare_amount, read_csv_file, check_linked_id, check_amts, check_linked_flr_ftr_entities, check_area_cycles +from .fare_transfer_rule_checkers import check_leg_groups, check_transfer_count, check_durations +from .utils import check_fare_amount, read_csv_file, check_linked_id, check_amts, check_linked_flr_ftr_entities from .warnings import * def areas(gtfs_root_dir, messages): + areas = set() for line in read_csv_file(gtfs_root_dir, schema.AREAS, messages): if not line.area_id: line.add_error(EMPTY_AREA_ID_AREAS) continue + if line.area_id in areas: + line.add_error(DUPLICATE_AREAS_TXT_ENTRY) + if line.area_id: + areas.add(line.area_id) + + return areas def stop_areas(gtfs_root_dir, messages, areas, stops): + stop_areas = set() for line in read_csv_file(gtfs_root_dir, schema.STOP_AREAS, messages): if not line.area_id: line.add_error(EMPTY_AREA_ID_STOP_AREAS) @@ -26,16 +34,16 @@ def stop_areas(gtfs_root_dir, messages, areas, stops): if not line.stop_id: line.add_error(EMPTY_STOP_ID_STOP_AREAS) continue - if line.area_id not in areas: - line.add_error(INVALID_AREA_ID) - if line.stop_id not in stops: - line.add_error(INVALID_STOP_ID) + check_linked_id(line, 'area_id', areas) + check_linked_id(line, 'stop_id', stops) + if (line.area_id, line.stop_id) in stop_areas: + line.add_error(DUPLICATE_STOP_AREAS_TXT_ENTRY) + stop_areas.add((line.area_id, line.stop_id)) def timeframes(gtfs_root_dir, messages): timeframes = set() - for line in read_csv_file(gtfs_root_dir, schema.TIMEFRAMES, - messages): + for line in read_csv_file(gtfs_root_dir, schema.TIMEFRAMES, messages): if not line.timeframe_id: line.add_error(EMPTY_TIMEFRAME_ID) continue @@ -73,8 +81,7 @@ def timeframes(gtfs_root_dir, messages): def rider_categories(gtfs_root_dir, messages): rider_categories = set() - for line in read_csv_file(gtfs_root_dir, - schema.RIDER_CATEGORIES, messages): + for line in read_csv_file(gtfs_root_dir, schema.RIDER_CATEGORIES, messages): min_age_int = 0 if not line.rider_category_id: line.add_error(EMPTY_RIDER_CATEGORY_ID) @@ -110,8 +117,7 @@ def rider_categories(gtfs_root_dir, messages): def fare_containers(gtfs_root_dir, rider_categories, messages): rider_category_by_fare_container = {} - for line in read_csv_file(gtfs_root_dir, - schema.FARE_CONTAINERS, messages): + for line in read_csv_file(gtfs_root_dir, schema.FARE_CONTAINERS, messages): if not line.fare_container_id: line.add_error(EMPTY_FARE_CONTAINER_ID) continue @@ -121,7 +127,9 @@ def fare_containers(gtfs_root_dir, rider_categories, messages): continue amount_exists = check_fare_amount(line, 'amount', 'currency') - min_purchase_exists = check_fare_amount(line, 'minimum_initial_purchase', 'currency') + min_purchase_exists = check_fare_amount(line, + 'minimum_initial_purchase', + 'currency') if (not amount_exists and not min_purchase_exists) and line.currency: line.add_error(CURRENCY_WITHOUT_AMOUNT) @@ -133,7 +141,8 @@ def fare_containers(gtfs_root_dir, rider_categories, messages): if line.rider_category_id not in rider_categories: line.add_error(NONEXISTENT_RIDER_CATEGORY_ID) - rider_category_by_fare_container[line.fare_container_id] = line.rider_category_id + rider_category_by_fare_container[ + line.fare_container_id] = line.rider_category_id return rider_category_by_fare_container @@ -151,20 +160,24 @@ def fare_products(gtfs_root_dir, gtfs, unused_timeframes, messages): line.add_error(EMPTY_FARE_PRODUCT_NAME) continue - check_linked_fp_entities(line, gtfs.rider_category_ids, gtfs.rider_category_by_fare_container, + check_linked_fp_entities(line, gtfs.rider_category_ids, + gtfs.rider_category_by_fare_container, linked_entities_by_fare_product) min_amt_exists = check_fare_amount(line, 'min_amount', 'currency') max_amt_exists = check_fare_amount(line, 'max_amount', 'currency') amt_exists = check_fare_amount(line, 'amount', 'currency') - if (not min_amt_exists and not max_amt_exists and not amt_exists) and line.currency: + if (not min_amt_exists and not max_amt_exists and + not amt_exists) and line.currency: line.add_error(CURRENCY_WITHOUT_AMOUNT) - check_amts(fare_products_path, line, min_amt_exists, max_amt_exists, amt_exists) + check_amts(fare_products_path, line, min_amt_exists, max_amt_exists, + amt_exists) check_bundle(line) check_linked_id(line, 'service_id', gtfs.service_ids) - timeframe_exists = check_linked_id(line, 'timeframe_id', gtfs.timeframe_ids) + timeframe_exists = check_linked_id(line, 'timeframe_id', + gtfs.timeframe_ids) if line.timeframe_id in unused_timeframes: unused_timeframes.remove(line.timeframe_id) @@ -211,26 +224,21 @@ def fare_leg_rules(gtfs_root_dir, gtfs, unused_timeframes, messages): check_distances(line) - min_amt_exists = check_fare_amount(line, 'min_amount', 'currency') - max_amt_exists = check_fare_amount(line, 'max_amount', 'currency') - amt_exists = check_fare_amount(line, 'amount', 'currency') - if (not min_amt_exists and not max_amt_exists and not amt_exists) and line.currency: - line.add_error(CURRENCY_WITHOUT_AMOUNT) - check_amts(fare_leg_rules_path, line, min_amt_exists, max_amt_exists, amt_exists) - if (min_amt_exists or max_amt_exists or amt_exists) and line.fare_product_id: - line.add_error(AMOUNT_WITH_FARE_PRODUCT) - if line.fare_leg_name and line.fare_product_id: line.add_error(FARE_LEG_NAME_WITH_FARE_PRODUCT) - check_linked_flr_ftr_entities(line, gtfs.rider_category_ids, gtfs.rider_category_by_fare_container, - gtfs.linked_entities_by_fare_product) + if line.fare_product_id and line.fare_product_id not in gtfs.linked_entities_by_fare_product: + line.add_error(NONEXISTENT_FARE_PRODUCT_ID) if len(unused_areas): - messages.add_warning(diagnostics.format(UNUSED_AREA_IDS, '', '', f'Unused areas: {unused_areas}')) + messages.add_warning( + diagnostics.format(UNUSED_AREA_IDS, '', '', + f'Unused areas: {unused_areas}')) if len(unused_networks): - messages.add_warning(diagnostics.format(UNUSED_NETWORK_IDS, '', '', f'Unused networks: {unused_networks}')) + messages.add_warning( + diagnostics.format(UNUSED_NETWORK_IDS, '', '', + f'Unused networks: {unused_networks}')) return leg_group_ids @@ -242,29 +250,23 @@ def fare_transfer_rules(gtfs_root_dir, gtfs, messages): if not fare_transfer_rules_path.exists(): messages.add_warning(diagnostics.format(NO_FARE_TRANSFER_RULES, '')) - for line in read_csv_file(gtfs_root_dir, schema.FARE_TRANSFER_RULES, messages): + for line in read_csv_file(gtfs_root_dir, schema.FARE_TRANSFER_RULES, + messages): check_leg_groups(line, gtfs.leg_group_ids, unused_leg_groups) - check_spans_and_transfer_ids(line) + check_transfer_count(line) check_durations(line) - min_amt_exists = check_fare_amount(line, 'min_amount', 'currency', ) - max_amt_exists = check_fare_amount(line, 'max_amount', 'currency') - amt_exists = check_fare_amount(line, 'amount', 'currency') - if (not min_amt_exists and not max_amt_exists and not amt_exists) and line.currency: - line.add_error(CURRENCY_WITHOUT_AMOUNT) - - check_amts(fare_transfer_rules_path, line, min_amt_exists, max_amt_exists, amt_exists) - - if (min_amt_exists or max_amt_exists or amt_exists) and not line.fare_transfer_type: - line.add_error(AMOUNT_WITHOUT_FARE_TRANSFER_TYPE) - if (not min_amt_exists and not max_amt_exists and not amt_exists) and line.fare_transfer_type: - line.add_error(FARE_TRANSFER_TYPE_WITHOUT_AMOUNT) - if line.fare_transfer_type and (line.fare_transfer_type not in {'0', '1', '2', '3'}): + if line.fare_transfer_type and (line.fare_transfer_type + not in {'0', '1', '2'}): line.add_error(INVALID_FARE_TRANSFER_TYPE) - check_linked_flr_ftr_entities(line, gtfs.rider_category_ids, - gtfs.rider_category_by_fare_container, gtfs.linked_entities_by_fare_product) + if line.fare_product_id and line.fare_product_id not in gtfs.linked_entities_by_fare_product: + line.add_error(NONEXISTENT_FARE_PRODUCT_ID) + + if line.filter_fare_product_id and line.filter_fare_product_id not in gtfs.linked_entities_by_fare_product: + line.add_error(NONEXISTENT_FILTER_FARE_PRODUCT_ID) if len(unused_leg_groups): - messages.add_warning(diagnostics.format(UNUSED_LEG_GROUPS, '', '', - f'Unused leg groups: {unused_leg_groups}')) + messages.add_warning( + diagnostics.format(UNUSED_LEG_GROUPS, '', '', + f'Unused leg groups: {unused_leg_groups}')) diff --git a/fares_validator/read_gtfs_entities.py b/fares_validator/read_gtfs_entities.py index 8be966a..596d4a3 100644 --- a/fares_validator/read_gtfs_entities.py +++ b/fares_validator/read_gtfs_entities.py @@ -26,9 +26,11 @@ def stops(gtfs_root_dir, messages): return stop_ids + def service_ids(gtfs_root_dir, messages): service_ids = set() - if not (gtfs_root_dir / 'calendar.txt').exists() and not (gtfs_root_dir / 'calendar_dates.txt').exists(): + if not (gtfs_root_dir / 'calendar.txt').exists() and not ( + gtfs_root_dir / 'calendar_dates.txt').exists(): messages.add_warning(diagnostics.format(NO_SERVICE_IDS, '')) return service_ids @@ -38,11 +40,13 @@ def service_ids(gtfs_root_dir, messages): continue if line.service_id in service_ids: - line.add_error(DUPLICATE_SERVICE_ID, f'service_id: {line.service_id}') + line.add_error(DUPLICATE_SERVICE_ID, + f'service_id: {line.service_id}') service_ids.add(line.service_id) - for line in utils.read_csv_file(gtfs_root_dir, schema.CALENDAR_DATES, messages): + for line in utils.read_csv_file(gtfs_root_dir, schema.CALENDAR_DATES, + messages): if not line.service_id: line.add_error(EMPTY_SERVICE_ID_CALENDAR_DATES) continue diff --git a/fares_validator/schema.py b/fares_validator/schema.py index e605823..4b4f261 100644 --- a/fares_validator/schema.py +++ b/fares_validator/schema.py @@ -7,9 +7,9 @@ message_if_missing=warnings.NO_AREAS) STOPS = Schema('stops.txt', - set(), {'stop_id'}, - message_if_missing=warnings.NO_STOPS, - ) + set(), {'stop_id'}, + message_if_missing=warnings.NO_STOPS, + suppress_undefined_field_warning=True) STOP_AREAS = Schema('stop_areas.txt', required_fields={'area_id', 'stop_id'}, @@ -63,17 +63,16 @@ required_fields={'fare_product_id'}, defined_fields={ 'leg_group_id', 'fare_leg_name', 'network_id', - 'from_area_id', 'contains_area_id', 'to_area_id', - 'is_symmetrical', 'from_timeframe_id', + 'from_area_id', 'to_area_id', 'from_timeframe_id', 'to_timeframe_id', 'min_distance', 'max_distance', 'distance_type', 'service_id', 'fare_product_id' }) FARE_TRANSFER_RULES = Schema('fare_transfer_rules.txt', - required_fields=set(), + required_fields={'fare_transfer_type'}, defined_fields={ 'from_leg_group_id', 'to_leg_group_id', 'transfer_count', 'duration_limit', 'duration_limit_type', 'fare_transfer_type', - 'fare_product_id' + 'fare_product_id', 'filter_fare_product_id' }) diff --git a/fares_validator/test_area_cycles.py b/fares_validator/test_area_cycles.py deleted file mode 100644 index 90aebf3..0000000 --- a/fares_validator/test_area_cycles.py +++ /dev/null @@ -1,42 +0,0 @@ -from fares_validator.utils import check_area_cycles -from fares_validator import diagnostics, errors - -def test_area_cycles(): - messages = diagnostics.Diagnostics() - - min_cycle = { - '1': ['1'] - } - check_area_cycles(min_cycle, messages) - assert errors.GREATER_AREA_ID_LOOP in messages.errors[0] - assert len(messages.errors) == 1 - - not_a_cycle = { - '2': ['1'], - '3': ['4'], - '4': ['5'], - '1': [], - '5': ['2'] - } - check_area_cycles(not_a_cycle, messages) - assert len(messages.errors) == 1 - - more_complex_not_a_cycle = { - '1': ['2', '3', '4'], - '2': [], - '3': ['2'], - '4': ['3'] - } - check_area_cycles(more_complex_not_a_cycle, messages) - assert len(messages.errors) == 1 - - more_complex_cycle = { - '1': ['2', '3'], - '2': ['4'], - '3': ['2'], - '4': ['3'] - } - check_area_cycles(more_complex_cycle, messages) - assert errors.GREATER_AREA_ID_LOOP in messages.errors[1] - assert len(messages.errors) == 2 - diff --git a/fares_validator/tests/test_data/bad_fare_leg_rules/fare_leg_rules.txt b/fares_validator/tests/test_data/bad_fare_leg_rules/fare_leg_rules.txt index f02792b..a18c1f3 100644 --- a/fares_validator/tests/test_data/bad_fare_leg_rules/fare_leg_rules.txt +++ b/fares_validator/tests/test_data/bad_fare_leg_rules/fare_leg_rules.txt @@ -1,8 +1,4 @@ leg_group_id,from_area_id,contains_area_id,to_area_id,is_symmetrical,network_id,from_timeframe_id,to_timeframe_id,service_id,min_distance,max_distance,distance_type,amount,min_amount,max_amount,currency,fare_product_id,fare_leg_name,rider_category_id,fare_container_id -1,1,1,1,,,,,,,,,,,,,,,, -1,,1,,,,,,,,,,,,,,,,, -1,,,,1,,,,,,,,,,,,,,, -1,1,1,1,invalid_is_symmetrical,,,,,,,,,,,,,,, 1,1,1,invalid_area,1,,,,,,,,,,,,,,, 1,1,1,1,1,invalid_network,,,,,,,,,,,,,, 1,1,1,1,1,,invalid_timeframe,,,,,,,,,,,,, @@ -11,13 +7,5 @@ leg_group_id,from_area_id,contains_area_id,to_area_id,is_symmetrical,network_id, 1,1,1,1,1,,,,,bad_distance,bad_distance,,,,,,,,, 1,1,1,1,1,,,,,-2,-1,invalid_distance_type,,,,,,,, 1,1,1,1,1,,,,,,,1,,,,,,,, -1,1,1,1,1,,,,,,,,,,,USD,,,, -1,1,1,1,1,,,,,,,,3.00,,,USD,1,,, -1,1,1,1,1,,,,,,,,3.00,2.00,,USD,,,, 1,1,1,1,1,,,,,,,,,,,,1,invalid_fare_leg_name,, 1,1,1,1,1,,,,,,,,,,,,invalid_fare_product_id,,, -1,1,1,1,1,,,,,,,,,,,,1,,invalid_rider_category_id, -1,1,1,1,1,,,,,,,,,,,,1,,,invalid_fare_container_id -1,1,1,1,1,,,,,,,,,,,,2,,2, -1,1,1,1,1,,,,,,,,,,,,2,,,2 -1,1,1,1,1,,,,,,,,,,,,1,,1,2 diff --git a/fares_validator/tests/test_data/bad_fare_transfer_rules/fare_leg_rules.txt b/fares_validator/tests/test_data/bad_fare_transfer_rules/fare_leg_rules.txt index f858603..0d9dbcf 100644 --- a/fares_validator/tests/test_data/bad_fare_transfer_rules/fare_leg_rules.txt +++ b/fares_validator/tests/test_data/bad_fare_transfer_rules/fare_leg_rules.txt @@ -1,2 +1,2 @@ -leg_group_id -1 \ No newline at end of file +leg_group_id,fare_product_id +1,1 \ No newline at end of file diff --git a/fares_validator/tests/test_data/bad_fare_transfer_rules/fare_transfer_rules.txt b/fares_validator/tests/test_data/bad_fare_transfer_rules/fare_transfer_rules.txt index b4a30b5..bf82142 100644 --- a/fares_validator/tests/test_data/bad_fare_transfer_rules/fare_transfer_rules.txt +++ b/fares_validator/tests/test_data/bad_fare_transfer_rules/fare_transfer_rules.txt @@ -1,30 +1,12 @@ -from_leg_group_id,to_leg_group_id,is_symmetrical,fare_product_id,rider_category_id,fare_container_id,spanning_limit,transfer_id,transfer_sequence,duration_limit,duration_limit_type,amount,min_amount,max_amount,currency,fare_transfer_type -,,1,,,,,,,,,,,,, -1,1,,,,,,,,,,,,,, -1,1,invalid_is_symmetrical,,,,,,,,,,,,, -1,invalid_leg_group_id,1,,,,,,,,,,,,, -invalid_leg_group_id,1,1,,,,,,,,,,,,, -1,,1,,,,2,,,,,,,,, -1,1,1,,,,invalid_spanning_limit,,,,,,,,, -1,1,1,,,,1,,,,,,,,, -1,1,1,,,,2,1,1,,,,,,, -1,1,1,,,,,1,,,,,,,, -1,1,1,,,,,,1,,,,,,, -1,1,1,,,,,1,invalid_fare_transfer_sequence,,,,,,, -1,1,1,,,,,1,-1,,,,,,, -1,1,1,,,,,,,1,invalid_duration_limit_type,,,,, -1,1,1,,,,,,,1,,,,,, -1,1,1,,,,,,,invalid_duration_limit,1,,,,, -1,1,1,,,,,,,,1,,,,, -1,1,1,,,,,,,,,,,,USD, -1,1,1,,,,,,,,,3.00,,,,0 -1,1,1,,,,,,,,,3.00,,,USD, +from_leg_group_id,to_leg_group_id,is_symmetrical,fare_product_id,rider_category_id,fare_container_id,transfer_count,transfer_id,transfer_sequence,duration_limit,duration_limit_type,amount,min_amount,max_amount,currency,fare_transfer_type +1,invalid_leg_group_id,1,,,,,,,,,,,,,1 +invalid_leg_group_id,1,1,,,,,,,,,,,,,1 +1,,1,,,,2,,,,,,,,,1 +1,1,1,,,,invalid_transfer_count,,,,,,,,,1 +1,1,1,,,,0,,,,,,,,,1 +1,1,1,,,,,,,1,invalid_duration_limit_type,,,,,1 +1,1,1,,,,,,,1,,,,,,1 +1,1,1,,,,,,,invalid_duration_limit,1,,,,,1 +1,1,1,,,,,,,,1,,,,,1 1,1,1,,,,,,,,,3.00,,,USD,invalid_fare_transfer_type -1,1,1,,,,,,,,,3.00,,,invalid_currency_code,0 -1,1,1,,,,,,,,,,,,,0 -1,1,1,invalid_fare_product_id,,,,,,,,,,,, -1,1,1,,invalid_rider_category_id,,,,,,,,,,, -1,1,1,,,invalid_fare_container_id,,,,,,,,,, -1,1,1,2,2,,,,,,,,,,, -1,1,1,2,,2,,,,,,,,,, -1,1,1,1,1,2,,,,,,,,,, \ No newline at end of file +1,1,1,invalid_fare_product_id,,,,,,,,,,,, \ No newline at end of file diff --git a/fares_validator/tests/test_data/bad_gtfs_simple/areas.txt b/fares_validator/tests/test_data/bad_gtfs_simple/areas.txt index 138a523..6b5ac22 100644 --- a/fares_validator/tests/test_data/bad_gtfs_simple/areas.txt +++ b/fares_validator/tests/test_data/bad_gtfs_simple/areas.txt @@ -1,7 +1,7 @@ -area_id,greater_area_id +area_id,area_name , -area_1,area_2 -area_2,area_3 -area_3,area_1 +area_1,a +area_2,b +area_3,c area_4, -area_3,area_1 \ No newline at end of file +area_3,e \ No newline at end of file diff --git a/fares_validator/tests/test_data/bad_gtfs_simple/stop_areas.txt b/fares_validator/tests/test_data/bad_gtfs_simple/stop_areas.txt new file mode 100644 index 0000000..66d8397 --- /dev/null +++ b/fares_validator/tests/test_data/bad_gtfs_simple/stop_areas.txt @@ -0,0 +1,6 @@ +area_id,stop_id +,1 +1, +area_1,garbage_stop +1,1 +area_1,garbage_stop \ No newline at end of file diff --git a/fares_validator/tests/test_data/required_fields_test/fare_leg_rules.txt b/fares_validator/tests/test_data/required_fields_test/fare_leg_rules.txt new file mode 100644 index 0000000..7b4d9ef --- /dev/null +++ b/fares_validator/tests/test_data/required_fields_test/fare_leg_rules.txt @@ -0,0 +1,2 @@ +leg_group_id,fare_product_ids +1,1 \ No newline at end of file diff --git a/fares_validator/tests/test_data/required_fields_test/fare_transfer_rules.txt b/fares_validator/tests/test_data/required_fields_test/fare_transfer_rules.txt new file mode 100644 index 0000000..741464c --- /dev/null +++ b/fares_validator/tests/test_data/required_fields_test/fare_transfer_rules.txt @@ -0,0 +1,2 @@ +fare_transfer_types +2 \ No newline at end of file diff --git a/fares_validator/tests/test_data/required_fields_test/stop_areas.txt b/fares_validator/tests/test_data/required_fields_test/stop_areas.txt new file mode 100644 index 0000000..b9443e2 --- /dev/null +++ b/fares_validator/tests/test_data/required_fields_test/stop_areas.txt @@ -0,0 +1,3 @@ +area_ids,stop_ids +1,a +2,b \ No newline at end of file diff --git a/fares_validator/tests/test_data/warnings_test_gtfs/fare_leg_rules.txt b/fares_validator/tests/test_data/warnings_test_gtfs/fare_leg_rules.txt index 1b3bc1d..bfeacc6 100644 --- a/fares_validator/tests/test_data/warnings_test_gtfs/fare_leg_rules.txt +++ b/fares_validator/tests/test_data/warnings_test_gtfs/fare_leg_rules.txt @@ -1,3 +1,3 @@ -leg_group_id,from_area_id,network_id -1,1, -2,,2 \ No newline at end of file +leg_group_id,from_area_id,network_id,fare_product_id +1,1,,1 +2,,2,1 \ No newline at end of file diff --git a/fares_validator/tests/test_data/warnings_test_gtfs/stop_areas.txt b/fares_validator/tests/test_data/warnings_test_gtfs/stop_areas.txt new file mode 100644 index 0000000..69a3eef --- /dev/null +++ b/fares_validator/tests/test_data/warnings_test_gtfs/stop_areas.txt @@ -0,0 +1,2 @@ +area_id,stop_id +1,1 \ No newline at end of file diff --git a/fares_validator/tests/test_data/warnings_test_gtfs/stops.txt b/fares_validator/tests/test_data/warnings_test_gtfs/stops.txt index b60a166..b331124 100644 --- a/fares_validator/tests/test_data/warnings_test_gtfs/stops.txt +++ b/fares_validator/tests/test_data/warnings_test_gtfs/stops.txt @@ -1,3 +1,3 @@ -stop_id,area_id -1,1 -2,1 \ No newline at end of file +stop_id +1 +2 \ No newline at end of file diff --git a/fares_validator/tests/test_errors.py b/fares_validator/tests/test_errors.py index a7ebb5a..75e8f1f 100644 --- a/fares_validator/tests/test_errors.py +++ b/fares_validator/tests/test_errors.py @@ -2,181 +2,206 @@ from fares_validator import errors from pathlib import Path -test_data_dir = Path(__file__).parent / 'test_data' +test_data_dir = Path(__file__).parent / 'test_data' + def test_errors_simple_files(): results = run_validator(test_data_dir / 'bad_gtfs_simple', True) - + error_iter = results.errors.__iter__() + # Areas errors - assert errors.EMPTY_AREA_ID in results.errors[0] - assert errors.DUPLICATE_AREAS_TXT_ENTRY in results.errors[1] - assert errors.GREATER_AREA_ID_LOOP in results.errors[2] + assert errors.EMPTY_AREA_ID_AREAS in error_iter.__next__() + assert errors.DUPLICATE_AREAS_TXT_ENTRY in error_iter.__next__() + + # Stop_areas errors + assert errors.EMPTY_AREA_ID_STOP_AREAS in error_iter.__next__() + assert errors.EMPTY_STOP_ID_STOP_AREAS in error_iter.__next__() + assert errors.FOREIGN_ID_INVALID in error_iter.__next__() + assert errors.FOREIGN_ID_INVALID in error_iter.__next__() + assert errors.DUPLICATE_STOP_AREAS_TXT_ENTRY in error_iter.__next__() # Calendar errors - assert errors.EMPTY_SERVICE_ID_CALENDAR in results.errors[3] - assert errors.DUPLICATE_SERVICE_ID in results.errors[4] - + assert errors.EMPTY_SERVICE_ID_CALENDAR in error_iter.__next__() + assert errors.DUPLICATE_SERVICE_ID in error_iter.__next__() + # Calendar dates errors - assert errors.EMPTY_SERVICE_ID_CALENDAR_DATES in results.errors[5] + assert errors.EMPTY_SERVICE_ID_CALENDAR_DATES in error_iter.__next__() # Timeframes errors - assert errors.INVALID_TIME_FORMAT in results.errors[6] - assert errors.INVALID_TIME_FORMAT in results.errors[7] - assert errors.EMPTY_START_TIME in results.errors[8] - assert errors.EMPTY_END_TIME in results.errors[9] - assert errors.EMPTY_TIMEFRAME_ID in results.errors[10] + assert errors.INVALID_TIME_FORMAT in error_iter.__next__() + assert errors.INVALID_TIME_FORMAT in error_iter.__next__() + assert errors.EMPTY_START_TIME in error_iter.__next__() + assert errors.EMPTY_END_TIME in error_iter.__next__() + assert errors.EMPTY_TIMEFRAME_ID in error_iter.__next__() # Rider categories errors - assert errors.EMPTY_RIDER_CATEGORY_ID in results.errors[11] - assert errors.NEGATIVE_MIN_AGE in results.errors[12] - assert errors.NEGATIVE_MAX_AGE in results.errors[13] - assert errors.NON_INT_MIN_AGE in results.errors[14] - assert errors.NON_INT_MAX_AGE in results.errors[15] + assert errors.EMPTY_RIDER_CATEGORY_ID in error_iter.__next__() + assert errors.NEGATIVE_MIN_AGE in error_iter.__next__() + assert errors.NEGATIVE_MAX_AGE in error_iter.__next__() + assert errors.NON_INT_MIN_AGE in error_iter.__next__() + assert errors.NON_INT_MAX_AGE in error_iter.__next__() # Fare containers errors - assert errors.EMPTY_FARE_CONTAINER_ID in results.errors[16] - assert errors.EMPTY_FARE_CONTAINER_NAME in results.errors[17] - assert errors.NONEXISTENT_RIDER_CATEGORY_ID in results.errors[18] - assert errors.AMOUNT_WITHOUT_CURRENCY in results.errors[19] - assert errors.INVALID_AMOUNT_FORMAT in results.errors[20] - assert errors.AMOUNT_WITHOUT_CURRENCY in results.errors[21] - assert errors.INVALID_AMOUNT_FORMAT in results.errors[22] - assert errors.CURRENCY_WITHOUT_AMOUNT in results.errors[23] - assert errors.DUPLICATE_FARE_CONTAINER_ID in results.errors[24] - - assert len(results.errors) == 25 + assert errors.EMPTY_FARE_CONTAINER_ID in error_iter.__next__() + assert errors.EMPTY_FARE_CONTAINER_NAME in error_iter.__next__() + assert errors.NONEXISTENT_RIDER_CATEGORY_ID in error_iter.__next__() + assert errors.AMOUNT_WITHOUT_CURRENCY in error_iter.__next__() + assert errors.INVALID_AMOUNT_FORMAT in error_iter.__next__() + assert errors.AMOUNT_WITHOUT_CURRENCY in error_iter.__next__() + assert errors.INVALID_AMOUNT_FORMAT in error_iter.__next__() + assert errors.CURRENCY_WITHOUT_AMOUNT in error_iter.__next__() + assert errors.DUPLICATE_FARE_CONTAINER_ID in error_iter.__next__() + + try: + should_not_exist = error_iter.__next__() + assert not should_not_exist + except StopIteration: + assert True + def test_errors_fare_products(): results = run_validator(test_data_dir / 'bad_fare_products', False) + error_iter = results.errors.__iter__() + + assert errors.EMPTY_FARE_PRODUCT_ID in error_iter.__next__() + assert errors.EMPTY_FARE_PRODUCT_NAME in error_iter.__next__() + assert errors.MISSING_MIN_OR_MAX_AMOUNT in error_iter.__next__() + assert errors.AMOUNT_WITH_MIN_OR_MAX_AMOUNT in error_iter.__next__() + assert errors.AMOUNT_WITHOUT_CURRENCY in error_iter.__next__() + assert errors.AMOUNT_WITHOUT_CURRENCY in error_iter.__next__() + assert errors.AMOUNT_WITHOUT_CURRENCY in error_iter.__next__( + ) # this also is for line 7 of fare products + assert errors.NO_AMOUNT_DEFINED in error_iter.__next__() + assert errors.FOREIGN_ID_INVALID in error_iter.__next__() + assert errors.INVALID_TIMEFRAME_TYPE in error_iter.__next__() + assert errors.INVALID_TIMEFRAME_TYPE in error_iter.__next__() + assert errors.FOREIGN_ID_INVALID in error_iter.__next__() + assert errors.TIMEFRAME_TYPE_WITHOUT_TIMEFRAME in error_iter.__next__() + + try: + should_not_exist = error_iter.__next__() + assert not should_not_exist + except StopIteration: + assert True - assert errors.EMPTY_FARE_PRODUCT_ID in results.errors[0] - assert errors.EMPTY_FARE_PRODUCT_NAME in results.errors[1] - assert errors.MISSING_MIN_OR_MAX_AMOUNT in results.errors[2] - assert errors.AMOUNT_WITH_MIN_OR_MAX_AMOUNT in results.errors[3] - assert errors.AMOUNT_WITHOUT_CURRENCY in results.errors[4] - assert errors.AMOUNT_WITHOUT_CURRENCY in results.errors[5] - assert errors.AMOUNT_WITHOUT_CURRENCY in results.errors[6] # this also is for line 7 of fare products - assert errors.NO_AMOUNT_DEFINED in results.errors[7] - assert errors.FOREIGN_ID_INVALID in results.errors[8] - assert errors.INVALID_TIMEFRAME_TYPE in results.errors[9] - assert errors.INVALID_TIMEFRAME_TYPE in results.errors[10] - assert errors.FOREIGN_ID_INVALID in results.errors[11] - assert errors.TIMEFRAME_TYPE_WITHOUT_TIMEFRAME in results.errors[12] - - assert len(results.errors) == 13 def test_errors_fare_leg_rules(): results = run_validator(test_data_dir / 'bad_fare_leg_rules', False) + error_iter = results.errors.__iter__() # check areas - assert errors.AREA_WITHOUT_IS_SYMMETRICAL in results.errors[0] - assert errors.CONTAINS_AREA_WITHOUT_FROM_TO_AREA in results.errors[1] - assert errors.IS_SYMMETRICAL_WITHOUT_FROM_TO_AREA in results.errors[2] - assert errors.INVALID_IS_SYMMETRICAL_LEG_RULES in results.errors[3] - assert errors.FOREIGN_ID_INVALID in results.errors[4] + assert errors.FOREIGN_ID_INVALID in error_iter.__next__() # check networks - assert errors.FOREIGN_ID_INVALID in results.errors[5] + assert errors.FOREIGN_ID_INVALID in error_iter.__next__() # check timeframes - assert errors.FOREIGN_ID_INVALID in results.errors[6] - assert errors.FOREIGN_ID_INVALID in results.errors[7] + assert errors.FOREIGN_ID_INVALID in error_iter.__next__() + assert errors.FOREIGN_ID_INVALID in error_iter.__next__() # check service_id - assert errors.FOREIGN_ID_INVALID in results.errors[8] + assert errors.FOREIGN_ID_INVALID in error_iter.__next__() # check distances - assert errors.INVALID_MIN_DISTANCE in results.errors[9] - assert errors.INVALID_MAX_DISTANCE in results.errors[10] - assert errors.DISTANCE_WITHOUT_DISTANCE_TYPE in results.errors[11] - assert errors.INVALID_DISTANCE_TYPE in results.errors[12] - assert errors.NEGATIVE_MIN_DISTANCE in results.errors[13] - assert errors.NEGATIVE_MAX_DISTANCE in results.errors[14] - assert errors.DISTANCE_TYPE_WITHOUT_DISTANCE in results.errors[15] + assert errors.INVALID_MIN_DISTANCE in error_iter.__next__() + assert errors.INVALID_MAX_DISTANCE in error_iter.__next__() + assert errors.DISTANCE_WITHOUT_DISTANCE_TYPE in error_iter.__next__() + assert errors.INVALID_DISTANCE_TYPE in error_iter.__next__() + assert errors.NEGATIVE_MIN_DISTANCE in error_iter.__next__() + assert errors.NEGATIVE_MAX_DISTANCE in error_iter.__next__() + assert errors.DISTANCE_TYPE_WITHOUT_DISTANCE in error_iter.__next__() # check amounts/fare_product/fare_leg_name - assert errors.CURRENCY_WITHOUT_AMOUNT in results.errors[16] - assert errors.AMOUNT_WITH_FARE_PRODUCT in results.errors[17] - assert errors.AMOUNT_WITH_MIN_OR_MAX_AMOUNT in results.errors[18] - assert errors.MISSING_MIN_OR_MAX_AMOUNT in results.errors[19] - assert errors.FARE_LEG_NAME_WITH_FARE_PRODUCT in results.errors[20] + assert errors.FARE_LEG_NAME_WITH_FARE_PRODUCT in error_iter.__next__() # check linked entities - assert errors.NONEXISTENT_FARE_PRODUCT_ID in results.errors[21] - assert errors.NONEXISTENT_RIDER_CATEGORY_ID in results.errors[22] - assert errors.NONEXISTENT_FARE_CONTAINER_ID in results.errors[23] - assert errors.CONFLICTING_RIDER_CATEGORY_ON_FARE_PRODUCT in results.errors[24] - assert errors.CONFLICTING_FARE_CONTAINER_ON_FARE_PRODUCT in results.errors[25] - assert errors.CONFLICTING_RIDER_CATEGORY_ON_FARE_CONTAINER in results.errors[26] + assert errors.NONEXISTENT_FARE_PRODUCT_ID in error_iter.__next__() + + try: + should_not_exist = error_iter.__next__() + assert not should_not_exist + except StopIteration: + assert True - assert len(results.errors) == 27 def test_errors_fare_transfer_rules(): results = run_validator(test_data_dir / 'bad_fare_transfer_rules', False) + error_iter = results.errors.__iter__() # check leg groups - assert errors.IS_SYMMETRICAL_WITHOUT_FROM_TO_LEG_GROUP in results.errors[0] - assert errors.LEG_GROUP_WITHOUT_IS_SYMMETRICAL in results.errors[1] - assert errors.INVALID_IS_SYMMETRICAL_TRANSFER_RULES in results.errors[2] - assert errors.INVALID_TO_LEG_GROUP in results.errors[3] - assert errors.INVALID_FROM_LEG_GROUP in results.errors[4] + assert errors.INVALID_TO_LEG_GROUP in error_iter.__next__() + assert errors.INVALID_FROM_LEG_GROUP in error_iter.__next__() # check transfer_id and spans - assert errors.SPANNING_LIMIT_WITH_BAD_LEGS in results.errors[5] - assert errors.INVALID_SPANNING_LIMIT in results.errors[6] - assert errors.INVALID_SPANNING_LIMIT in results.errors[7] - assert errors.SPANNING_LIMIT_WITH_TRANSFER_ID in results.errors[8] - assert errors.TRANSFER_ID_WITHOUT_TRANSFER_SEQUENCE in results.errors[9] - assert errors.TRANSFER_SEQUENCE_WITHOUT_TRANSFER_ID in results.errors[10] - assert errors.INVALID_TRANSFER_SEQUENCE in results.errors[11] - assert errors.INVALID_TRANSFER_SEQUENCE in results.errors[12] + assert errors.TRANSFER_COUNT_WITH_BAD_LEGS in error_iter.__next__() + assert errors.INVALID_TRANSFER_COUNT in error_iter.__next__() + assert errors.INVALID_TRANSFER_COUNT in error_iter.__next__() # check durations - assert errors.INVALID_DURATION_LIMIT_TYPE in results.errors[13] - assert errors.DURATION_LIMIT_WITHOUT_LIMIT_TYPE in results.errors[14] - assert errors.INVALID_DURATION_LIMIT in results.errors[15] - assert errors.DURATION_LIMIT_TYPE_WITHOUT_DURATION in results.errors[16] + assert errors.INVALID_DURATION_LIMIT_TYPE in error_iter.__next__() + assert errors.DURATION_LIMIT_WITHOUT_LIMIT_TYPE in error_iter.__next__() + assert errors.INVALID_DURATION_LIMIT in error_iter.__next__() + assert errors.DURATION_LIMIT_TYPE_WITHOUT_DURATION in error_iter.__next__() # check amounts - assert errors.CURRENCY_WITHOUT_AMOUNT in results.errors[17] - assert errors.AMOUNT_WITHOUT_CURRENCY in results.errors[18] - assert errors.AMOUNT_WITHOUT_FARE_TRANSFER_TYPE in results.errors[19] - assert errors.INVALID_FARE_TRANSFER_TYPE in results.errors[20] - assert errors.UNRECOGNIZED_CURRENCY_CODE in results.errors[21] - assert errors.FARE_TRANSFER_TYPE_WITHOUT_AMOUNT in results.errors[22] + assert errors.INVALID_FARE_TRANSFER_TYPE in error_iter.__next__() # check linked entities - assert errors.NONEXISTENT_FARE_PRODUCT_ID in results.errors[23] - assert errors.NONEXISTENT_RIDER_CATEGORY_ID in results.errors[24] - assert errors.NONEXISTENT_FARE_CONTAINER_ID in results.errors[25] - assert errors.CONFLICTING_RIDER_CATEGORY_ON_FARE_PRODUCT in results.errors[26] - assert errors.CONFLICTING_FARE_CONTAINER_ON_FARE_PRODUCT in results.errors[27] - assert errors.CONFLICTING_RIDER_CATEGORY_ON_FARE_CONTAINER in results.errors[28] + assert errors.NONEXISTENT_FARE_PRODUCT_ID in error_iter.__next__() + + try: + should_not_exist = error_iter.__next__() + assert not should_not_exist + except StopIteration: + assert True - assert len(results.errors) == 29 def test_required_fields(): results = run_validator(test_data_dir / 'required_fields_test', False) + error_iter = results.errors.__iter__() + + area_error = error_iter.__next__() + assert errors.REQUIRED_FIELD_MISSING in area_error + assert 'areas.txt' in area_error + + stop_area_error = error_iter.__next__() + assert errors.REQUIRED_FIELD_MISSING in stop_area_error + assert 'stop_areas.txt' in stop_area_error + + calendar_error = error_iter.__next__() + assert errors.REQUIRED_FIELD_MISSING in calendar_error + assert 'calendar.txt' in calendar_error + + calendar_dates_error = error_iter.__next__() + assert errors.REQUIRED_FIELD_MISSING in calendar_dates_error + assert 'calendar_dates.txt' in calendar_dates_error - assert errors.REQUIRED_FIELD_MISSING in results.errors[0] - assert 'areas.txt' in results.errors[0] - - assert errors.REQUIRED_FIELD_MISSING in results.errors[1] - assert 'calendar.txt' in results.errors[1] + timeframes_error = error_iter.__next__() + assert errors.REQUIRED_FIELD_MISSING in timeframes_error + assert 'timeframes.txt' in timeframes_error - assert errors.REQUIRED_FIELD_MISSING in results.errors[2] - assert 'calendar_dates.txt' in results.errors[2] + rider_categories_error = error_iter.__next__() + assert errors.REQUIRED_FIELD_MISSING in rider_categories_error + assert 'rider_categories.txt' in rider_categories_error - assert errors.REQUIRED_FIELD_MISSING in results.errors[3] - assert 'timeframes.txt' in results.errors[3] + fare_containers_error = error_iter.__next__() + assert errors.REQUIRED_FIELD_MISSING in fare_containers_error + assert 'fare_containers.txt' in fare_containers_error - assert errors.REQUIRED_FIELD_MISSING in results.errors[4] - assert 'rider_categories.txt' in results.errors[4] + fare_products_error = error_iter.__next__() + assert errors.REQUIRED_FIELD_MISSING in fare_products_error + assert 'fare_products.txt' in fare_products_error - assert errors.REQUIRED_FIELD_MISSING in results.errors[5] - assert 'fare_containers.txt' in results.errors[5] + fare_leg_rules_error = error_iter.__next__() + assert errors.REQUIRED_FIELD_MISSING in fare_leg_rules_error + assert 'fare_leg_rules.txt' in fare_leg_rules_error - assert errors.REQUIRED_FIELD_MISSING in results.errors[6] - assert 'fare_products.txt' in results.errors[6] + fare_transfer_rules_error = error_iter.__next__() + assert errors.REQUIRED_FIELD_MISSING in fare_transfer_rules_error + assert 'fare_transfer_rules.txt' in fare_transfer_rules_error - assert len(results.errors) == 7 + try: + should_not_exist = error_iter.__next__() + assert not should_not_exist + except StopIteration: + assert True diff --git a/fares_validator/tests/test_warnings.py b/fares_validator/tests/test_warnings.py index 902ae90..955b926 100644 --- a/fares_validator/tests/test_warnings.py +++ b/fares_validator/tests/test_warnings.py @@ -4,44 +4,54 @@ test_data_dir = Path(__file__).parent / 'test_data' + def test_warnings(): results = run_validator(test_data_dir / 'warnings_test_gtfs', True) - - # Stops / stop times warnings - assert warnings.UNUSED_AREAS_IN_STOPS in results.warnings[0] + warning_iter = results.warnings.__iter__() # Rider categories warnings - assert warnings.MAX_AGE_LESS_THAN_MIN_AGE in results.warnings[1] - assert warnings.VERY_LARGE_MIN_AGE in results.warnings[2] - assert warnings.VERY_LARGE_MAX_AGE in results.warnings[3] + assert warnings.MAX_AGE_LESS_THAN_MIN_AGE in warning_iter.__next__() + assert warnings.VERY_LARGE_MIN_AGE in warning_iter.__next__() + assert warnings.VERY_LARGE_MAX_AGE in warning_iter.__next__() # Fare products warnings - assert warnings.OFFSET_AMOUNT_WITHOUT_OFFSET_UNIT in results.warnings[4] + assert warnings.OFFSET_AMOUNT_WITHOUT_OFFSET_UNIT in warning_iter.__next__() # Fare leg rule warnings - assert warnings.UNUSED_AREA_IDS in results.warnings[5] - assert warnings.UNUSED_NETWORK_IDS in results.warnings[6] + assert warnings.UNUSED_AREA_IDS in warning_iter.__next__() + assert warnings.UNUSED_NETWORK_IDS in warning_iter.__next__() # Fare transfer rule warnings - assert warnings.UNUSED_LEG_GROUPS in results.warnings[7] + assert warnings.UNUSED_LEG_GROUPS in warning_iter.__next__() # generic warnings - assert warnings.UNUSED_TIMEFRAME_IDS in results.warnings[8] + assert warnings.UNUSED_TIMEFRAME_IDS in warning_iter.__next__() + + try: + should_not_exist = warning_iter.__next__() + assert not should_not_exist + except StopIteration: + assert True - assert len(results.warnings) == 9 def test_warnings_nonexistent_files(): results = run_validator(test_data_dir / 'no_files', True) - - assert warnings.NO_AREAS in results.warnings[0] - assert warnings.NO_ROUTES in results.warnings[1] - assert warnings.NO_STOPS in results.warnings[2] - assert warnings.NO_SERVICE_IDS in results.warnings[3] - assert warnings.NO_TIMEFRAMES in results.warnings[4] - assert warnings.NO_RIDER_CATEGORIES in results.warnings[5] - assert warnings.NO_FARE_CONTAINERS in results.warnings[6] - assert warnings.NO_FARE_PRODUCTS in results.warnings[7] - assert warnings.NO_FARE_LEG_RULES in results.warnings[8] - assert warnings.NO_FARE_TRANSFER_RULES in results.warnings[9] - - assert len(results.warnings) == 10 + warning_iter = results.warnings.__iter__() + + assert warnings.NO_AREAS in warning_iter.__next__() + assert warnings.NO_STOPS in warning_iter.__next__() + assert warnings.NO_STOP_AREAS in warning_iter.__next__() + assert warnings.NO_ROUTES in warning_iter.__next__() + assert warnings.NO_SERVICE_IDS in warning_iter.__next__() + assert warnings.NO_TIMEFRAMES in warning_iter.__next__() + assert warnings.NO_RIDER_CATEGORIES in warning_iter.__next__() + assert warnings.NO_FARE_CONTAINERS in warning_iter.__next__() + assert warnings.NO_FARE_PRODUCTS in warning_iter.__next__() + assert warnings.NO_FARE_LEG_RULES in warning_iter.__next__() + assert warnings.NO_FARE_TRANSFER_RULES in warning_iter.__next__() + + try: + should_not_exist = warning_iter.__next__() + assert not should_not_exist + except StopIteration: + assert True diff --git a/fares_validator/utils.py b/fares_validator/utils.py index fda9501..1753e20 100644 --- a/fares_validator/utils.py +++ b/fares_validator/utils.py @@ -11,7 +11,11 @@ class Schema: FAKE_FIELDS = {'line_num_error_msg'} - def __init__(self, basename, required_fields, defined_fields, *, + def __init__(self, + basename, + required_fields, + defined_fields, + *, message_if_missing=None, suppress_undefined_field_warning=False): self.basename = basename @@ -26,6 +30,7 @@ def has_field(self, field_name): class Entity: + def __init__(self, schema, messages, original_dict): self._schema = schema self._messages = messages @@ -38,10 +43,14 @@ def __getattr__(self, item): raise TypeError(f'Reference to undefined field {item} in code!') def add_error(self, code, extra_info=''): - self._messages.add_error(diagnostics.format(code, self.line_num_error_msg, self._schema.basename, extra_info)) + self._messages.add_error( + diagnostics.format(code, self.line_num_error_msg, + self._schema.basename, extra_info)) def add_warning(self, code, extra_info=''): - self._messages.add_warning(diagnostics.format(code, self.line_num_error_msg, self._schema.basename, extra_info)) + self._messages.add_warning( + diagnostics.format(code, self.line_num_error_msg, + self._schema.basename, extra_info)) def read_csv_file(gtfs_root_dir, schema, messages): @@ -58,7 +67,9 @@ def read_csv_file(gtfs_root_dir, schema, messages): for required_field in schema.required_fields: if required_field not in reader.fieldnames: messages.add_error( - diagnostics.format(REQUIRED_FIELD_MISSING, '', schema.basename, f'field: {required_field}')) + diagnostics.format(REQUIRED_FIELD_MISSING, '', + schema.basename, + f'field: {required_field}')) return [] if schema.defined_fields and not schema.suppress_undefined_field_warning: @@ -67,8 +78,9 @@ def read_csv_file(gtfs_root_dir, schema, messages): if field not in schema.defined_fields: unexpected_fields.append(field) if len(unexpected_fields): - messages.add_warning(diagnostics.format(UNEXPECTED_FIELDS, '', schema.basename, - f'\nColumn(s): {unexpected_fields}')) + messages.add_warning( + diagnostics.format(UNEXPECTED_FIELDS, '', schema.basename, + f'\nColumn(s): {unexpected_fields}')) for line in reader: line['line_num_error_msg'] = f'\nLine: {reader.line_num}' @@ -104,99 +116,27 @@ def check_amts(path, line, min_amt_exists, max_amt_exists, amt_exists): filename = Path(path).name if (min_amt_exists or max_amt_exists) and amt_exists: line.add_error(AMOUNT_WITH_MIN_OR_MAX_AMOUNT) - if (min_amt_exists and not max_amt_exists) or (max_amt_exists and not min_amt_exists): + if (min_amt_exists and not max_amt_exists) or (max_amt_exists and + not min_amt_exists): line.add_error(MISSING_MIN_OR_MAX_AMOUNT) - if (not amt_exists and not min_amt_exists and not max_amt_exists) and filename == 'fare_products.txt': + if (not amt_exists and not min_amt_exists and + not max_amt_exists) and filename == 'fare_products.txt': line.add_error(NO_AMOUNT_DEFINED) -def read_areas_of_file(path, areas, unused_areas): - with open(path, 'r', encoding='utf-8-sig') as csvfile: - reader = csv.DictReader(csvfile, skipinitialspace=True) - - # Avoid parsing huge file if areas are not in use - if 'area_id' not in reader.fieldnames: - return - - for line in reader: - area_id = line.get('area_id') - - if not area_id: - continue - - if area_id in unused_areas: - unused_areas.remove(area_id) - - if area_id not in areas: - areas.add(area_id) - - def check_linked_id(line, fieldname, defined_ids): if not getattr(line, fieldname): return False if getattr(line, fieldname) not in defined_ids: - line.add_error(FOREIGN_ID_INVALID, extra_info=f'{fieldname}: {getattr(line, fieldname)}') + line.add_error(FOREIGN_ID_INVALID, + extra_info=f'{fieldname}: {getattr(line, fieldname)}') return True -def check_linked_flr_ftr_entities(line, rider_categories, rider_category_by_fare_container, +def check_linked_flr_ftr_entities(line, rider_categories, + rider_category_by_fare_container, linked_entities_by_fare_product): if line.fare_product_id and line.fare_product_id not in linked_entities_by_fare_product: line.add_error(NONEXISTENT_FARE_PRODUCT_ID) - if line.rider_category_id and line.rider_category_id not in rider_categories: - line.add_error(NONEXISTENT_RIDER_CATEGORY_ID) - if line.fare_container_id and line.fare_container_id not in rider_category_by_fare_container: - line.add_error(NONEXISTENT_FARE_CONTAINER_ID) - - if line.fare_product_id: - if line.rider_category_id: - fp_rider_cats = linked_entities_by_fare_product[line.fare_product_id].rider_category_ids - if len(fp_rider_cats) and (line.rider_category_id not in fp_rider_cats) and ('' not in fp_rider_cats): - line.add_error(CONFLICTING_RIDER_CATEGORY_ON_FARE_PRODUCT) - if line.fare_container_id: - fp_fare_containers = linked_entities_by_fare_product[line.fare_product_id].fare_container_ids - if len(fp_fare_containers) and (line.fare_container_id not in fp_fare_containers) and ('' not in fp_fare_containers): - line.add_error(CONFLICTING_FARE_CONTAINER_ON_FARE_PRODUCT) - if line.rider_category_id and line.fare_container_id: - fc_rider_cat = rider_category_by_fare_container[line.fare_container_id] - if fc_rider_cat and (fc_rider_cat != line.rider_category_id): - line.add_error(CONFLICTING_RIDER_CATEGORY_ON_FARE_CONTAINER) - - -# This uses an adapted version of Kahn's algorithm -# https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm -def check_area_cycles(greater_area_ids_by_area_id, messages): - non_parent_areas = deque(greater_area_ids_by_area_id.keys()) - in_degree_by_area_id = Counter() - - for area_id, greater_areas in greater_area_ids_by_area_id.items(): - if not greater_areas: - continue - for greater_area_id in greater_areas: - if greater_area_id not in greater_area_ids_by_area_id: - messages.add_error(diagnostics.format(UNDEFINED_GREATER_AREA_ID, '', '', - f'greater_area_id: {greater_area_id}')) - return - in_degree_by_area_id[greater_area_id] += 1 - if greater_area_id in non_parent_areas: - non_parent_areas.remove(greater_area_id) - - sorted_area_ids = [] - while len(non_parent_areas) > 0: - area_id = non_parent_areas.popleft() - sorted_area_ids.append(area_id) - for greater_area_id in greater_area_ids_by_area_id[area_id]: - in_degree_by_area_id[greater_area_id] -= 1 - if in_degree_by_area_id[greater_area_id] == 0: - non_parent_areas.append(greater_area_id) - - nonzero_in_degree_area_ids = [] - for area_id in in_degree_by_area_id: - if in_degree_by_area_id[area_id] > 0: - nonzero_in_degree_area_ids.append(area_id) - - if len(nonzero_in_degree_area_ids) > 0: - messages.add_error(diagnostics.format(GREATER_AREA_ID_LOOP, '', '', - f'area_ids: {str(nonzero_in_degree_area_ids)}')) diff --git a/setup.py b/setup.py index 508772d..e4035a0 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,13 @@ from setuptools import setup -setup(name='gtfs-fares-v2-validator', - version='0.1.0', - description='Validate transit feeds for conformance to the GTFS Fares v2 specification', - url='https://github.com/TransitApp/gtfs-fares-v2-validator', - author='Jeremy Steele', - packages=['fares_validator'], - classifiers=[ - 'License :: OSI Approved :: MIT License' - ], - zip_safe=False, - install_requires=[]) +setup( + name='gtfs-fares-v2-validator', + version='0.1.0', + description= + 'Validate transit feeds for conformance to the GTFS Fares v2 specification', + url='https://github.com/TransitApp/gtfs-fares-v2-validator', + author='Jeremy Steele', + packages=['fares_validator'], + classifiers=['License :: OSI Approved :: MIT License'], + zip_safe=False, + install_requires=[])