diff --git a/README.md b/README.md
index 0bdb39509..aeada992a 100644
--- a/README.md
+++ b/README.md
@@ -95,6 +95,7 @@ Contains the JSON schemas used to validate the feeds in the integration tests.
| features | Array of Enums | Optional | An array of features which can be any of:
|
| status | Enum | Optional | Describes status of the feed. Should be one of: - `active`: The feed service is current.
- `deprecated`: Feed is explicitly deprecated and should not be used in public trip planners.
- `inactive`: Feed hasn't been recently updated and should be used at risk of providing outdated information.
- `development`: Feed is being used for development purposes and should not be used in public trip planners.
Feed is assumed to be `active` if status is not explicitly provided.| |
| is_official | Enum | Optional | Flag indicating if the source comes from the agency itself or not. - `True`: Feed comes from the agency as an authorized source.
- `False`: Feed is created by researchers or partners unaffiliated with the agency or municipality.
Feed's is_official flag is assumed to be `False` if it is not explicitly provided.|
+| is_producer_url_unstable | Enum | Optional | Indicates whether the feed's producer URL is stable and unchanging over time. - `False`: Producer URL is stable and unchanging over time.
- `True`: Producer URL is unstable and changes over time. This may be indicated from a timestamp in the URL, website documentation, or other information that indicates this URL changes more than twice a year.
- `null`: There is not enough information to describe the stability of the producer URL.
|
|redirect| Object | Optional | When a feed is deprecated by a provider and replaced with a new URL, redirect information is provided to point to the new feed.|
| - id | String | Optional | New feed ID that replaces the current feed that is out of date or no longer maintained by the provider. |
| - comment | String | Optional | comment to explain redirect if needed (e.g new aggregate feed) |
diff --git a/schemas/gtfs_realtime_source_schema.json b/schemas/gtfs_realtime_source_schema.json
index f56f893a1..bd913faf2 100644
--- a/schemas/gtfs_realtime_source_schema.json
+++ b/schemas/gtfs_realtime_source_schema.json
@@ -176,6 +176,11 @@
"type": "string",
"description": "True if a feed comes directly from the agency, False if a feed is created by researchers or partners unaffiliated with the agency or municipality.",
"enum": ["True", "False"]
+ },
+ "is_producer_url_unstable": {
+ "type": ["string", "null"],
+ "description": "True if the producer URL is known to be unstable (e.g. changes frequently), False otherwise.",
+ "enum": [null, "True", "False"]
}
},
"required": ["mdb_source_id", "data_type", "entity_type", "provider", "urls"]
diff --git a/scripts/identify_unstable_urls.py b/scripts/identify_unstable_urls.py
new file mode 100644
index 000000000..1a23bb8aa
--- /dev/null
+++ b/scripts/identify_unstable_urls.py
@@ -0,0 +1,471 @@
+# Find schedule and realtime feeds whose producer URL carries evidence of a date or a
+# version, and flag them with is_producer_url_unstable = "True".
+#
+# A producer URL containing a date or a rotating version number will stop resolving once
+# the producer publishes again, so the feed needs manual attention more than twice a year.
+# See the is_producer_url_unstable row of README.md for the field definition.
+#
+# Dry run by default. Nothing is written unless --apply is passed, and the field is only
+# ever set to "True" -- feeds without evidence are left untouched.
+#
+# python scripts/identify_unstable_urls.py --report unstable_urls.csv --weak
+# python scripts/identify_unstable_urls.py --apply
+#
+# This script is intentionally standalone (standard library only). tools.helpers pulls in
+# gtfs_kit, which needs GDAL, so scripts/ re-declares the handful of constants it needs
+# instead of importing the tools package. Same convention as scripts/create_urls_matrix.py.
+import argparse
+import csv
+import json
+import os
+import re
+from urllib.parse import unquote, urlsplit
+
+# OS constants
+ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+# tools.constants
+GTFS_SCHEDULE_CATALOG_PATH_FROM_ROOT = "catalogs/sources/gtfs/schedule"
+GTFS_REALTIME_CATALOG_PATH_FROM_ROOT = "catalogs/sources/gtfs/realtime"
+GTFS = "gtfs"
+GTFS_RT = "gtfs-rt"
+ALL = "all"
+MDB_SOURCE_ID = "mdb_source_id"
+DATA_TYPE = "data_type"
+PROVIDER = "provider"
+STATUS = "status"
+URLS = "urls"
+DIRECT_DOWNLOAD = "direct_download"
+IS_PRODUCER_URL_UNSTABLE = "is_producer_url_unstable"
+
+# Field constants
+TRUE = "True"
+DEPRECATED = "deprecated"
+
+# Report constants
+REPORT_COLUMNS = [
+ "mdb_source_id",
+ "data_type",
+ "provider",
+ "status",
+ "direct_download",
+ "rules",
+ "matched_text",
+]
+
+
+#########################
+# UNSTABLE PATTERNS
+#########################
+
+MONTHS_ABBREVIATED = "jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec"
+
+# Full month names only. Short and ambiguous forms (may, mai, mars, marco, marz, aout)
+# are left out because they collide with place and agency names.
+MONTHS_SPELLED_OUT = "|".join(
+ [
+ # English
+ "january|february|march|april|june|july|august|september|october|november|december",
+ # Spanish
+ "enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre",
+ # Portuguese
+ "janeiro|fevereiro|março|maio|junho|julho|setembro|outubro|novembro|dezembro",
+ # French
+ "janvier|fevrier|février|avril|juin|juillet|decembre|décembre",
+ # German
+ "januar|februar|märz|juni|juli|oktober|dezember",
+ # Italian
+ "gennaio|febbraio|aprile|maggio|giugno|luglio|settembre|ottobre|dicembre",
+ ]
+)
+
+SEASONS = "winter|spring|summer|fall|autumn"
+
+# Evidence that the producer URL encodes a point in time or a revision. Any match means
+# the URL is unstable. Each rule is named so the report can say which one fired.
+UNSTABLE_PATTERNS = [
+ # Republishing to a WordPress media library moves the file to a new year/month
+ # folder. The optional inner segments absorb the /sites// multisite id.
+ (
+ "wordpress_upload_folder",
+ re.compile(r"wp-content/uploads/(?:[^/]+/){0,2}(?:19|20)\d{2}/\d{1,2}/"),
+ ),
+ # YYYYMMDD, YYYY-MM-DD, YYYY_MM_DD, YYYY.MM.DD, optionally carrying an HHMM or
+ # HHMMSS time, as in mvv_ohneShape_20241004095702.zip.
+ (
+ "iso_date",
+ re.compile(
+ r"(?5} {row['rules']:<40} {row['matched_text']}")
+ print(f" {row['direct_download']}")
+
+ print()
+ print(f"Scanned {scanned} sources, skipped {skipped_deprecated} deprecated.")
+ print(f"Unstable producer URLs: {len(flagged)}")
+
+ counts = {}
+ for row in flagged:
+ for name in row["rules"].split("|"):
+ counts[name] = counts.get(name, 0) + 1
+ for name, count in sorted(counts.items(), key=lambda item: -item[1]):
+ print(f" {count:>4} {name}")
+
+ if already_set:
+ print()
+ print(f"Matched but left alone, the field is already set: {len(already_set)}")
+ for source, url, _ in already_set:
+ value = source.get(IS_PRODUCER_URL_UNSTABLE)
+ print(f" {source.get(MDB_SOURCE_ID):>5} {value:<6} {url}")
+
+ if args.weak:
+ print()
+ print(f"Weak signals, never flagged, review by hand: {len(weak)}")
+ for source, url, weak_signals in weak:
+ rules, matched = format_signals(weak_signals)
+ print(f" {source.get(MDB_SOURCE_ID):>5} {rules:<40} {matched}")
+ print(f" {url}")
+
+ if args.report:
+ write_report(args.report, flagged)
+ print()
+ print(f"Report written to {args.report}")
+
+ if args.apply:
+ print()
+ print(f"Wrote is_producer_url_unstable to {len(flagged)} files.")
+ else:
+ print()
+ print("Dry run, nothing written. Pass --apply to write.")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tests/test_identify_unstable_urls.py b/tests/test_identify_unstable_urls.py
new file mode 100644
index 000000000..274fc15bf
--- /dev/null
+++ b/tests/test_identify_unstable_urls.py
@@ -0,0 +1,202 @@
+import importlib.util
+import os
+from unittest import TestCase
+
+PROJECT_ROOT = os.path.dirname(os.path.dirname(__file__))
+SCRIPT_PATH = os.path.join(PROJECT_ROOT, "scripts", "identify_unstable_urls.py")
+
+# The script is standalone by design, so that it can run without the GDAL dependencies
+# that tools.helpers pulls in. Load it by path rather than turning scripts/ into a package.
+spec = importlib.util.spec_from_file_location("identify_unstable_urls", SCRIPT_PATH)
+identify_unstable_urls = importlib.util.module_from_spec(spec)
+spec.loader.exec_module(identify_unstable_urls)
+
+is_unstable = identify_unstable_urls.is_unstable
+
+# Producer URLs that carry a date or a version, paired with the rule that must fire.
+# Every URL here is taken from the catalog.
+UNSTABLE_URLS = [
+ (
+ "wordpress_upload_folder",
+ "https://moosejaw.ca/wp-content/uploads/2023/11/moosejaw-GTFS.zip",
+ ),
+ (
+ "wordpress_upload_folder",
+ "https://www.metrofor.ce.gov.br/wp-content/uploads/sites/32/2025/01/gtfs_metrofor.zip",
+ ),
+ ("iso_date", "https://www.dtpm.cl/descargas/gtfs/GTFS_20260704.zip"),
+ ("iso_date", "https://storage.googleapis.com/gtfs-estaticos/GTFS-2026-04-29.zip"),
+ (
+ "iso_date",
+ "https://zdzit.olsztyn.eu/wp-content/uploads/2025/05/GTFS_2025_05_26.zip",
+ ),
+ ("iso_date", "https://mts.pt/imt/MTS-20240129.zip"),
+ (
+ "iso_date",
+ "https://transport-data-gouv-fr-resource-history-prod.cellar-c2.services."
+ "clever-cloud.com/80742/80742.20241107.160916.535554.zip",
+ ),
+ # A date carrying a time. The trailing time must not defeat the rule.
+ (
+ "iso_date",
+ "https://www.mvv-muenchen.de/fileadmin/mediapool/02-Fahrplanauskunft/"
+ "03-Downloads/openData/mvv_ohneShape_20241004095702.zip",
+ ),
+ (
+ "year_month",
+ "https://dados.fortaleza.ce.gov.br/dataset/51afc610-d48b-4fa8-8dea-9c65747148c7/"
+ "resource/51e3d494-3b41-4328-a971-964e2cdd8a22/download/gtff-202311.zip",
+ ),
+ (
+ "locale_date",
+ "https://datos.jalisco.gob.mx/sites/default/files/"
+ "rutas_mitransporte_puertovallarta_03.12.2021.zip",
+ ),
+ (
+ "locale_date",
+ "https://www.circumetnea.it/download/"
+ "general-transit-feed-specification-fce-01-02-2025-28-02-2028/?wpdmdl=17798",
+ ),
+ (
+ "dated_version_tag",
+ "https://github.com/aruneko/DonanbusGTFS/archive/refs/tags/2020.0401.1.zip",
+ ),
+ (
+ "delimited_year",
+ "https://static.oebb.at/open-data/soll-fahrplan-gtfs/GTFS_OP_2024_obb.zip",
+ ),
+ ("delimited_year", "https://www.santaeulaliaferry.com/gtfs_feed_2025.zip"),
+ (
+ "delimited_year",
+ "https://data.opentransportdata.swiss/dataset/timetable-2026-gtfs2020/permalink",
+ ),
+ ("delimited_year", "https://maxtransit.org/GTFS/2026/google_transit_Working.zip"),
+ (
+ "labelled_year",
+ "https://opendata.hamilton.ca/GTFS-Static/2025Winter_GTFSStatic.zip",
+ ),
+ (
+ "labelled_year",
+ "http://go-rts.com/wp-content/uploads/2025/01/RTSGTFS_Spring2025.zip",
+ ),
+ (
+ "labelled_year",
+ "https://www.fredericksburgva.gov/DocumentCenter/View/31122/FXBGO-GTFS---CY2026",
+ ),
+ (
+ "labelled_year",
+ "https://www.fredericksburgva.gov/DocumentCenter/View/28958/FXBGO-GTFS-Q1-CY2025",
+ ),
+ (
+ "month_name",
+ "https://data.opencity.in/dataset/88e2d145-7ec6-4666-88dd-6cf18b18312e/resource/"
+ "1b0d18bb-b2fb-4a79-8ed0-1e071da5790c/download/"
+ "telangana_opendata_gtfs_tgsrtc_08_february_2026.zip",
+ ),
+ (
+ "month_name",
+ "https://dfef8f.p3cdn2.secureserver.net/wp-content/uploads/2023/08/"
+ "RMTD_GTFS_AUGUST_2023.zip",
+ ),
+ ("month_name", "https://www.dtpm.cl/descargas/gtfs/03%20GTFS_Final_03marzo.zip"),
+ (
+ "month_abbreviation_with_digits",
+ "http://datos.gob.cl/dataset/c77c9a50-6dd1-449d-b5ab-947ec0139b31/resource/"
+ "a4edcf07-0657-456d-bbbc-54b2aec1de8d/download/coquimbo10feb16.zip",
+ ),
+ (
+ "month_abbreviation_with_digits",
+ "http://datos.gob.cl/dataset/cef4c471-2837-412b-a78e-1d4c6a261bf9/resource/"
+ "7887a7e7-9af6-4fc8-b19f-8ff4ea474d1c/download/temuco24nov16.zip",
+ ),
+ # gateway.carris.pt really did move from v2.8 to v2.11,
+ (
+ "version_query_parameter",
+ "https://solweb.tper.it/web/tools/open-data/open-data-download.aspx?"
+ "source=solweb.tper.it&filename=gommagtfsbo&version=20260122&format=zip",
+ ),
+ (
+ "epoch_timestamp",
+ "https://www.wroclaw.pl/open-data/87b09b32-f076-4475-8ec9-6020ed1f9ac0/"
+ "1513602900.0_OtwartyWroclaw_rozklad_jazdy_GTFS.zip",
+ ),
+]
+
+# Producer URLs whose digits look like a date or a version but never change. Every URL
+# here is taken from the catalog, and each one stands for a whole cluster of feeds.
+STABLE_URLS = [
+ # Digits in the hostname, not the path.
+ "https://www3.septa.org/developer/google_bus.zip",
+ "http://apps2.saskatoon.ca/app/data/google_transit.zip",
+ # A raw IPv4 host reads as a version number.
+ "http://70.34.208.164/gtfs.zip",
+ "http://193.23.225.211:8002/export-gtfs-static",
+ # An explicit port reads as a year.
+ "https://cat.cadavl.com:4431/CAT/GTFS/GTFS_CAT.zip",
+ # The v2 is the GTFS-Flex spec version, not the feed's.
+ "https://data.trilliumtransit.com/gtfs/tracy-ca-us/tracy-ca-us--flex-v2.zip",
+ "https://data.trilliumtransit.com/gtfs/pueblo-co-us/pueblo-co-us--flex-v2.zip",
+ # Stable API version paths.
+ "https://api.transport.nsw.gov.au/v2/gtfs/alerts/all",
+ "https://transport.api.act.gov.au/gtfs/data/gtfs/v2/gtfs.zip",
+ "https://gitlab.com/api/v4/projects/vekejsn%2Fgtfs-generators/packages/generic/"
+ "nis-gtfs/latest/nis_gtfs.zip",
+ # A bare API version path segment.
+ "https://data.waltti.fi/tampere/api/gtfsrealtime/v1.0/feed/tripupdate",
+ "https://stibmivb.opendatasoft.com/api/datasets/1.0/gtfs-files-production/"
+ "alternative_exports/gtfszip/",
+ # The numeric TransitFeeds feed key.
+ "https://transitfeeds.com/p/rodoviaria-de-lisboa/998/latest/download",
+ # A permanent ArcGIS item id.
+ "https://www.arcgis.com/sharing/rest/content/items/"
+ "1a25440bf66f499bae2657ec7fb40144/data",
+ # A 40 hex character Mecatran credential.
+ "https://app.mecatran.com/utw/ws/gtfsfeed/static/lio"
+ "?apiKey=2b160d626f783808095373766f18714901325e45&type=gtfs_lio",
+ # A Google Drive file id.
+ "https://drive.usercontent.google.com/uc"
+ "?id=1l8BUIOaNZiu7hbO1UxMB1e9EaXm5s4Wa&export=download",
+ # Place and agency names that collide with month and season words.
+ "http://data.trilliumtransit.com/gtfs/winterpark-co-us/winterpark-co-us.zip",
+ "https://data.trilliumtransit.com/gtfs/cedarfalls-ia-us/cedarfalls-ia-us.zip",
+ "http://data.trilliumtransit.com/gtfs/centralmarylandrta-md-us/"
+ "centralmarylandrta-md-us.zip",
+ "https://s3.amazonaws.com/datatools-511ny/public/"
+ "Greater_Glens_Falls_Transit_System.zip",
+ # Stable numeric ids that contain a year-like run.
+ "https://addtransit.com/gtfsfile/42017/TheVictoriaClipper.zip",
+ "https://www.tib.org/documents/20124/478141/ctm-mallorca-es.zip",
+ # A plain producer URL with nothing rotating in it.
+ "http://www.wienerlinien.at/ogd_realtime/doku/ogd/gtfs/gtfs.zip",
+ # The MobilityData mirror template, which the script never scans but must not match.
+ "https://storage.googleapis.com/storage/v1/b/mdb-latest/o/"
+ "at-wien-wiener-lokalbahnen-wlb-gtfs-648.zip?alt=media",
+]
+
+
+class TestIsUnstable(TestCase):
+ def test_flags_dates_and_versions(self):
+ for expected_rule, url in UNSTABLE_URLS:
+ with self.subTest(url=url):
+ rules = [rule for rule, _ in is_unstable(url)]
+ self.assertTrue(rules, f"expected {url} to be flagged")
+ self.assertIn(expected_rule, rules)
+
+ def test_leaves_stable_urls_alone(self):
+ for url in STABLE_URLS:
+ with self.subTest(url=url):
+ self.assertEqual(is_unstable(url), [], f"expected {url} to be stable")
+
+
+class TestScannable(TestCase):
+ def test_drops_scheme_and_host(self):
+ under_test = identify_unstable_urls.scannable(
+ "https://www3.septa.org:8443/developer/google_bus.zip?a=b#c"
+ )
+ self.assertEqual(under_test, "/developer/google_bus.zip?a=b#c")
+
+ def test_decodes_percent_encoding(self):
+ under_test = identify_unstable_urls.scannable(
+ "https://www.dtpm.cl/descargas/gtfs/03%20GTFS_Final_03marzo.zip"
+ )
+ self.assertIn("03 GTFS_Final_03marzo.zip", under_test)
diff --git a/tools/operations.py b/tools/operations.py
index bae438dfa..b39e3ac00 100644
--- a/tools/operations.py
+++ b/tools/operations.py
@@ -50,6 +50,7 @@ def add_gtfs_realtime_source(
status=None,
features=None,
is_official=None,
+ is_producer_url_unstable=None,
):
"""
Add a new GTFS Realtime source to the Mobility Catalogs.
@@ -71,6 +72,7 @@ def add_gtfs_realtime_source(
status (str, optional): The status of the GTFS Realtime source. Defaults to None.
features (list, optional): A list of features of the GTFS Realtime source. Defaults to None.
is_official (str, optional): Flag indicating if the source comes from the agency itself or not. Defaults to None.
+ is_producer_url_unstable (str, optional): Indicates if the producer URL is unstable. Possible values: "True", "False". Defaults to None.
Returns:
GtfsRealtimeSourcesCatalog: The catalog with the newly added GTFS Realtime source.
@@ -89,7 +91,8 @@ def add_gtfs_realtime_source(
LICENSE: license_url,
STATUS: status,
FEATURES: features,
- IS_OFFICIAL: is_official
+ IS_OFFICIAL: is_official,
+ IS_PRODUCER_URL_UNSTABLE: is_producer_url_unstable,
}
catalog.add(**data)
return catalog
@@ -109,7 +112,8 @@ def update_gtfs_realtime_source(
note=None,
status=None,
features=None,
- is_official = None,
+ is_official=None,
+ is_producer_url_unstable=None,
):
"""
Update an existing GTFS Realtime source in the Mobility Catalogs.
@@ -132,6 +136,7 @@ def update_gtfs_realtime_source(
status (str, optional): The status of the GTFS Realtime source. Defaults to None.
features (list, optional): A list of features of the GTFS Realtime source. Defaults to None.
is_official (str, optional): Flag indicating if the source comes from the agency itself or not. Defaults to None.
+ is_producer_url_unstable (str, optional): Indicates if the producer URL is unstable. Possible values: "True", "False". Defaults to None.
Returns:
GtfsRealtimeSourcesCatalog: The catalog with the updated GTFS Realtime source.
@@ -152,6 +157,7 @@ def update_gtfs_realtime_source(
STATUS: status,
FEATURES: features,
IS_OFFICIAL: is_official,
+ IS_PRODUCER_URL_UNSTABLE: is_producer_url_unstable,
}
catalog.update(**data)
return catalog
diff --git a/tools/representations.py b/tools/representations.py
index 8b4e423e6..4e7208036 100644
--- a/tools/representations.py
+++ b/tools/representations.py
@@ -905,6 +905,7 @@ def __str__(self):
FEATURES: self.features,
STATUS: self.status,
IS_OFFICIAL: self.is_official,
+ IS_PRODUCER_URL_UNSTABLE: self.is_producer_url_unstable,
}
return json.dumps(self.schematize(**attributes), ensure_ascii=False)
@@ -1017,6 +1018,9 @@ def update(self, **kwargs):
is_official = kwargs.get(IS_OFFICIAL)
if is_official is not None:
self.is_official = is_official
+ is_producer_url_unstable = kwargs.get(IS_PRODUCER_URL_UNSTABLE)
+ if is_producer_url_unstable is not None:
+ self.is_producer_url_unstable = is_producer_url_unstable
return self
@classmethod
@@ -1076,6 +1080,7 @@ def schematize(cls, **kwargs):
LICENSE: kwargs.pop(LICENSE, None),
},
IS_OFFICIAL: kwargs.pop(IS_OFFICIAL, None),
+ IS_PRODUCER_URL_UNSTABLE: kwargs.pop(IS_PRODUCER_URL_UNSTABLE, None),
}
if schema[NAME] is None:
del schema[NAME]
@@ -1097,4 +1102,6 @@ def schematize(cls, **kwargs):
del schema[STATUS]
if schema[IS_OFFICIAL] is None:
del schema[IS_OFFICIAL]
+ if schema[IS_PRODUCER_URL_UNSTABLE] is None:
+ del schema[IS_PRODUCER_URL_UNSTABLE]
return schema
diff --git a/tools/tests/test_operations.py b/tools/tests/test_operations.py
index 631bb9220..8eb050e18 100644
--- a/tools/tests/test_operations.py
+++ b/tools/tests/test_operations.py
@@ -34,6 +34,7 @@ def test_add_gtfs_realtime_source(self, mock_catalog):
test_status = "active"
test_features = ["fares"]
test_is_official = "True"
+ test_is_producer_url_unstable = "True"
under_test = add_gtfs_realtime_source(
entity_type=test_entity_type,
provider=test_provider,
@@ -48,6 +49,7 @@ def test_add_gtfs_realtime_source(self, mock_catalog):
status=test_status,
features=test_features,
is_official=test_is_official,
+ is_producer_url_unstable=test_is_producer_url_unstable,
)
self.assertEqual(under_test, mock_catalog())
self.assertEqual(mock_catalog.call_count, 2)
@@ -69,6 +71,7 @@ def test_update_gtfs_realtime_source(self, mock_catalog):
test_status = "active"
test_features = ["flex-v2"]
test_is_official = "True"
+ test_is_producer_url_unstable = "False"
under_test = update_gtfs_realtime_source(
mdb_source_id=test_mdb_source_id,
entity_type=test_entity_type,
@@ -84,6 +87,7 @@ def test_update_gtfs_realtime_source(self, mock_catalog):
status=test_status,
features=test_features,
is_official=test_is_official,
+ is_producer_url_unstable=test_is_producer_url_unstable,
)
self.assertEqual(under_test, mock_catalog())
self.assertEqual(mock_catalog.call_count, 2)
diff --git a/tools/tests/test_representations.py b/tools/tests/test_representations.py
index 2c8739373..fe4fee563 100644
--- a/tools/tests/test_representations.py
+++ b/tools/tests/test_representations.py
@@ -701,6 +701,7 @@ def setUp(self):
self.test_features = [self.test_feature]
self.test_status = "some_status"
self.test_is_official = "some_is_official"
+ self.test_is_producer_url_unstable = "some_is_producer_url_unstable"
self.test_kwargs = {
MDB_SOURCE_ID: self.test_mdb_source_id,
DATA_TYPE: self.test_data_type,
@@ -718,6 +719,7 @@ def setUp(self):
FEATURES: self.test_features,
STATUS: self.test_status,
IS_OFFICIAL: self.test_is_official,
+ IS_PRODUCER_URL_UNSTABLE: self.test_is_producer_url_unstable,
}
self.test_schema = {
MDB_SOURCE_ID: self.test_mdb_source_id,
@@ -737,6 +739,7 @@ def setUp(self):
LICENSE: self.test_license_url,
},
IS_OFFICIAL: self.test_is_official,
+ IS_PRODUCER_URL_UNSTABLE: self.test_is_producer_url_unstable,
}
@patch("tools.representations.GtfsRealtimeSource.static_catalog")
@@ -876,6 +879,16 @@ def test_has_is_official(self, mock_static_catalog):
under_test = instance.has_is_official(is_official=test_another_is_official)
self.assertFalse(under_test)
+ @patch("tools.representations.GtfsRealtimeSource.static_catalog")
+ def test_has_is_producer_url_unstable(self, mock_static_catalog):
+ test_is_producer_url_unstable = self.test_is_producer_url_unstable
+ test_another_is_producer_url_unstable = "some_other_is_producer_url_unstable"
+ instance = GtfsRealtimeSource(filename=self.test_filename, **self.test_schema)
+ under_test = instance.has_is_producer_url_unstable(is_producer_url_unstable=test_is_producer_url_unstable)
+ self.assertTrue(under_test)
+ under_test = instance.has_is_producer_url_unstable(is_producer_url_unstable=test_another_is_producer_url_unstable)
+ self.assertFalse(under_test)
+
@patch("tools.representations.GtfsRealtimeSource.static_catalog")
def test_update(self, mock_static_catalog):
instance = GtfsRealtimeSource(filename=self.test_filename, **self.test_schema)