aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/gtfs_perstop_report/.gitignore13
-rw-r--r--src/gtfs_perstop_report/pyproject.toml27
-rw-r--r--src/gtfs_perstop_report/rolling_dates_example.json8
-rw-r--r--src/gtfs_perstop_report/src/__init__.py0
-rw-r--r--src/gtfs_perstop_report/src/common.py66
-rw-r--r--src/gtfs_perstop_report/src/download.py146
-rw-r--r--src/gtfs_perstop_report/src/logger.py58
-rw-r--r--src/gtfs_perstop_report/src/proto/__init__.py1
-rw-r--r--src/gtfs_perstop_report/src/proto/stop_schedule_pb2.py32
-rw-r--r--src/gtfs_perstop_report/src/proto/stop_schedule_pb2.pyi126
-rw-r--r--src/gtfs_perstop_report/src/providers.py137
-rw-r--r--src/gtfs_perstop_report/src/report_writer.py145
-rw-r--r--src/gtfs_perstop_report/src/rolling_dates.py168
-rw-r--r--src/gtfs_perstop_report/src/routes.py47
-rw-r--r--src/gtfs_perstop_report/src/services.py120
-rw-r--r--src/gtfs_perstop_report/src/shapes.py103
-rw-r--r--src/gtfs_perstop_report/src/stop_schedule_pb2.py40
-rw-r--r--src/gtfs_perstop_report/src/stop_schedule_pb2.pyi88
-rw-r--r--src/gtfs_perstop_report/src/stop_times.py151
-rw-r--r--src/gtfs_perstop_report/src/stops.py98
-rw-r--r--src/gtfs_perstop_report/src/street_name.py52
-rw-r--r--src/gtfs_perstop_report/src/trips.py155
-rw-r--r--src/gtfs_perstop_report/stop_report.py677
-rw-r--r--src/gtfs_perstop_report/uv.lock253
-rw-r--r--src/stop_downloader/README.md113
-rw-r--r--src/stop_downloader/vigo/download-stops.py260
-rw-r--r--src/stop_downloader/vigo/overrides/amenities.yaml23
-rw-r--r--src/stop_downloader/vigo/overrides/example-new-stops.yaml31
-rw-r--r--src/stop_downloader/vigo/overrides/fix-gregorio-espino.yaml20
-rw-r--r--src/stop_downloader/vigo/overrides/hide-virtual-stops.yaml17
-rw-r--r--src/stop_downloader/vigo/overrides/improve-coordinates-misc.yaml40
31 files changed, 0 insertions, 3215 deletions
diff --git a/src/gtfs_perstop_report/.gitignore b/src/gtfs_perstop_report/.gitignore
deleted file mode 100644
index 2be2c5f..0000000
--- a/src/gtfs_perstop_report/.gitignore
+++ /dev/null
@@ -1,13 +0,0 @@
-feed/
-output/
-
-# Python-generated files
-__pycache__/
-*.py[oc]
-build/
-dist/
-wheels/
-*.egg-info
-
-# Virtual environments
-.venv
diff --git a/src/gtfs_perstop_report/pyproject.toml b/src/gtfs_perstop_report/pyproject.toml
deleted file mode 100644
index 97d24a3..0000000
--- a/src/gtfs_perstop_report/pyproject.toml
+++ /dev/null
@@ -1,27 +0,0 @@
-[project]
-name = "gtfs-vigo"
-version = "0.1.0"
-description = "Add your description here"
-readme = "README.md"
-requires-python = ">=3.13"
-dependencies = [
- "colorama>=0.4.6",
- "jinja2>=3.1.6",
- "protobuf>=5.29.1",
- "pyproj>=3.7.2",
- "pytest>=8.4.1",
- "requests>=2.32.3",
-]
-
-[tool.ruff]
-line-length = 88
-target-version = "py313"
-
-[tool.ruff.lint]
-select = ["E", "F", "I", "W"]
-ignore = []
-
-[tool.ruff.format]
-quote-style = "double"
-indent-style = "space"
-
diff --git a/src/gtfs_perstop_report/rolling_dates_example.json b/src/gtfs_perstop_report/rolling_dates_example.json
deleted file mode 100644
index 66525c3..0000000
--- a/src/gtfs_perstop_report/rolling_dates_example.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "2025-12-18": "2025-12-11",
- "2025-12-19": "2025-12-12",
- "2025-12-20": "2025-12-13",
- "2025-12-21": "2025-12-14",
- "2025-12-22": "2025-12-15",
- "2025-12-23": "2025-12-16"
-}
diff --git a/src/gtfs_perstop_report/src/__init__.py b/src/gtfs_perstop_report/src/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/src/gtfs_perstop_report/src/__init__.py
+++ /dev/null
diff --git a/src/gtfs_perstop_report/src/common.py b/src/gtfs_perstop_report/src/common.py
deleted file mode 100644
index c2df785..0000000
--- a/src/gtfs_perstop_report/src/common.py
+++ /dev/null
@@ -1,66 +0,0 @@
-"""
-Common utilities for GTFS report generation.
-"""
-
-import csv
-import os
-from datetime import datetime, timedelta
-from typing import List
-
-
-def get_all_feed_dates(feed_dir: str) -> List[str]:
- """
- Returns all dates the feed is valid for, using calendar.txt if present, else calendar_dates.txt.
- """
- calendar_path = os.path.join(feed_dir, "calendar.txt")
- calendar_dates_path = os.path.join(feed_dir, "calendar_dates.txt")
-
- # Try calendar.txt first
- if os.path.exists(calendar_path):
- with open(calendar_path, encoding="utf-8") as f:
- reader = csv.DictReader(f)
- rows = list(reader)
- if rows: # Check if there's actual data
- start_dates: List[str] = []
- end_dates: List[str] = []
- for row in rows:
- if row.get("start_date") and row.get("end_date"):
- start_dates.append(row["start_date"])
- end_dates.append(row["end_date"])
- if start_dates and end_dates:
- min_date = min(start_dates)
- max_date = max(end_dates)
- # Convert YYYYMMDD to YYYY-MM-DD
- start = datetime.strptime(min_date, "%Y%m%d")
- end = datetime.strptime(max_date, "%Y%m%d")
- result: List[str] = []
- while start <= end:
- result.append(start.strftime("%Y-%m-%d"))
- start += timedelta(days=1)
- if len(result) > 0:
- return result
-
- # Fallback: use calendar_dates.txt
- if os.path.exists(calendar_dates_path):
- with open(calendar_dates_path, encoding="utf-8") as f:
- reader = csv.DictReader(f)
- dates: set[str] = set()
- for row in reader:
- if row.get("exception_type") == "1" and row.get("date"):
- # Convert YYYYMMDD to YYYY-MM-DD
- d = row["date"]
- dates.add(f"{d[:4]}-{d[4:6]}-{d[6:]}")
- if len(dates) > 0:
- return sorted(dates)
-
- today = datetime.today()
- return [(today + timedelta(days=i)).strftime("%Y-%m-%d") for i in range(8)]
-
-
-def time_to_seconds(time_str: str) -> int:
- """Convert HH:MM:SS to seconds since midnight."""
- parts = time_str.split(":")
- if len(parts) != 3:
- return 0
- hours, minutes, seconds = map(int, parts)
- return hours * 3600 + minutes * 60 + seconds
diff --git a/src/gtfs_perstop_report/src/download.py b/src/gtfs_perstop_report/src/download.py
deleted file mode 100644
index c924b6b..0000000
--- a/src/gtfs_perstop_report/src/download.py
+++ /dev/null
@@ -1,146 +0,0 @@
-import os
-import tempfile
-import zipfile
-import requests
-import json
-from typing import Optional, Tuple
-
-from src.logger import get_logger
-
-logger = get_logger("download")
-
-
-def _get_metadata_path(output_dir: str) -> str:
- """Get the path to the metadata file for storing ETag and Last-Modified info."""
- return os.path.join(output_dir, ".gtfsmetadata")
-
-
-def _load_metadata(output_dir: str) -> Optional[dict]:
- """Load existing metadata from the output directory."""
- metadata_path = _get_metadata_path(output_dir)
- if os.path.exists(metadata_path):
- try:
- with open(metadata_path, "r", encoding="utf-8") as f:
- return json.load(f)
- except (json.JSONDecodeError, IOError) as e:
- logger.warning(f"Failed to load metadata from {metadata_path}: {e}")
- return None
-
-
-def _save_metadata(
- output_dir: str, etag: Optional[str], last_modified: Optional[str]
-) -> None:
- """Save ETag and Last-Modified metadata to the output directory."""
- metadata_path = _get_metadata_path(output_dir)
- metadata = {"etag": etag, "last_modified": last_modified}
-
- # Ensure output directory exists
- os.makedirs(output_dir, exist_ok=True)
-
- try:
- with open(metadata_path, "w", encoding="utf-8") as f:
- json.dump(metadata, f, indent=2)
- except IOError as e:
- logger.warning(f"Failed to save metadata to {metadata_path}: {e}")
-
-
-def _check_if_modified(
- feed_url: str, output_dir: str
-) -> Tuple[bool, Optional[str], Optional[str]]:
- """
- Check if the feed has been modified using conditional headers.
- Returns (is_modified, etag, last_modified)
- """
- metadata = _load_metadata(output_dir)
- if not metadata:
- return True, None, None
-
- headers = {}
- if metadata.get("etag"):
- headers["If-None-Match"] = metadata["etag"]
- if metadata.get("last_modified"):
- headers["If-Modified-Since"] = metadata["last_modified"]
-
- if not headers:
- return True, None, None
-
- try:
- response = requests.head(feed_url, headers=headers)
-
- if response.status_code == 304:
- logger.info(
- "Feed has not been modified (304 Not Modified), skipping download"
- )
- return False, metadata.get("etag"), metadata.get("last_modified")
- elif response.status_code == 200:
- etag = response.headers.get("ETag")
- last_modified = response.headers.get("Last-Modified")
- return True, etag, last_modified
- else:
- logger.warning(
- f"Unexpected response status {response.status_code} when checking for modifications, proceeding with download"
- )
- return True, None, None
- except requests.RequestException as e:
- logger.warning(
- f"Failed to check if feed has been modified: {e}, proceeding with download"
- )
- return True, None, None
-
-
-def download_feed_from_url(
- feed_url: str, output_dir: str = None, force_download: bool = False
-) -> Optional[str]:
- """
- Download GTFS feed from URL.
-
- Args:
- feed_url: URL to download the GTFS feed from
- output_dir: Directory where reports will be written (used for metadata storage)
- force_download: If True, skip conditional download checks
-
- Returns:
- Path to the directory containing the extracted GTFS files, or None if download was skipped
- """
-
- # Check if we need to download the feed
- if not force_download and output_dir:
- is_modified, cached_etag, cached_last_modified = _check_if_modified(
- feed_url, output_dir
- )
- if not is_modified:
- logger.info("Feed has not been modified, skipping download")
- return None
-
- # Create a directory in the system temporary directory
- temp_dir = tempfile.mkdtemp(prefix="gtfs_feed_")
-
- # Create a temporary zip file in the temporary directory
- zip_filename = os.path.join(temp_dir, "gtfs_vigo.zip")
-
- headers = {}
- response = requests.get(feed_url, headers=headers)
-
- if response.status_code != 200:
- raise Exception(f"Failed to download GTFS data: {response.status_code}")
-
- with open(zip_filename, "wb") as file:
- file.write(response.content)
-
- # Extract and save metadata if output_dir is provided
- if output_dir:
- etag = response.headers.get("ETag")
- last_modified = response.headers.get("Last-Modified")
- if etag or last_modified:
- _save_metadata(output_dir, etag, last_modified)
-
- # Extract the zip file
- with zipfile.ZipFile(zip_filename, "r") as zip_ref:
- zip_ref.extractall(temp_dir)
-
- # Clean up the downloaded zip file
- os.remove(zip_filename)
-
- logger.info(f"GTFS feed downloaded from {feed_url} and extracted to {temp_dir}")
-
- return temp_dir
diff --git a/src/gtfs_perstop_report/src/logger.py b/src/gtfs_perstop_report/src/logger.py
deleted file mode 100644
index 6c56787..0000000
--- a/src/gtfs_perstop_report/src/logger.py
+++ /dev/null
@@ -1,58 +0,0 @@
-"""
-Logging configuration for the GTFS application.
-"""
-
-import logging
-from colorama import init, Fore, Style
-
-# Initialize Colorama (required on Windows)
-init(autoreset=True)
-
-
-class ColorFormatter(logging.Formatter):
- def format(self, record: logging.LogRecord):
- # Base format
- log_format = "%(asctime)s | %(levelname)-8s | %(name)s | %(funcName)s:%(lineno)d | %(message)s"
-
- # Apply colors based on log level
- if record.levelno == logging.DEBUG:
- prefix = Style.DIM + Fore.WHITE # "Dark grey"
- elif record.levelno == logging.INFO:
- prefix = Fore.CYAN
- elif record.levelno == logging.WARNING:
- prefix = Fore.YELLOW
- elif record.levelno == logging.ERROR:
- prefix = Fore.RED
- elif record.levelno == logging.CRITICAL:
- prefix = Style.BRIGHT + Fore.RED
- else:
- prefix = ""
-
- # Add color to the entire line
- formatter = logging.Formatter(
- prefix + log_format + Style.RESET_ALL, "%Y-%m-%d %H:%M:%S"
- )
- return formatter.format(record)
-
-
-def get_logger(name: str) -> logging.Logger:
- """
- Create and return a logger with the given name.
-
- Args:
- name (str): The name of the logger.
-
- Returns:
- logging.Logger: Configured logger instance.
- """
- logger = logging.getLogger(name)
- logger.setLevel(logging.INFO)
-
- # Only add handler if it doesn't already have one
- if not logger.handlers:
- console_handler = logging.StreamHandler()
- console_handler.setLevel(logging.DEBUG)
- console_handler.setFormatter(ColorFormatter())
- logger.addHandler(console_handler)
-
- return logger
diff --git a/src/gtfs_perstop_report/src/proto/__init__.py b/src/gtfs_perstop_report/src/proto/__init__.py
deleted file mode 100644
index b775c17..0000000
--- a/src/gtfs_perstop_report/src/proto/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-# Protobuf generated files
diff --git a/src/gtfs_perstop_report/src/proto/stop_schedule_pb2.py b/src/gtfs_perstop_report/src/proto/stop_schedule_pb2.py
deleted file mode 100644
index c7279c5..0000000
--- a/src/gtfs_perstop_report/src/proto/stop_schedule_pb2.py
+++ /dev/null
@@ -1,32 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by the protocol buffer compiler. DO NOT EDIT!
-# source: stop_schedule.proto
-"""Generated protocol buffer code."""
-
-from google.protobuf.internal import builder as _builder
-from google.protobuf import descriptor as _descriptor
-from google.protobuf import descriptor_pool as _descriptor_pool
-from google.protobuf import symbol_database as _symbol_database
-# @@protoc_insertion_point(imports)
-
-_sym_db = _symbol_database.Default()
-
-
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(
- b'\n\x13stop_schedule.proto\x12\x05proto"!\n\tEpsg25829\x12\t\n\x01x\x18\x01 \x01(\x01\x12\t\n\x01y\x18\x02 \x01(\x01"\x83\x04\n\x0cStopArrivals\x12\x0f\n\x07stop_id\x18\x01 \x01(\t\x12"\n\x08location\x18\x03 \x01(\x0b\x32\x10.proto.Epsg25829\x12\x36\n\x08\x61rrivals\x18\x05 \x03(\x0b\x32$.proto.StopArrivals.ScheduledArrival\x1a\x85\x03\n\x10ScheduledArrival\x12\x12\n\nservice_id\x18\x01 \x01(\t\x12\x0f\n\x07trip_id\x18\x02 \x01(\t\x12\x0c\n\x04line\x18\x03 \x01(\t\x12\r\n\x05route\x18\x04 \x01(\t\x12\x10\n\x08shape_id\x18\x05 \x01(\t\x12\x1b\n\x13shape_dist_traveled\x18\x06 \x01(\x01\x12\x15\n\rstop_sequence\x18\x0b \x01(\r\x12\x14\n\x0cnext_streets\x18\x0c \x03(\t\x12\x15\n\rstarting_code\x18\x15 \x01(\t\x12\x15\n\rstarting_name\x18\x16 \x01(\t\x12\x15\n\rstarting_time\x18\x17 \x01(\t\x12\x14\n\x0c\x63\x61lling_time\x18! \x01(\t\x12\x13\n\x0b\x63\x61lling_ssm\x18" \x01(\r\x12\x15\n\rterminus_code\x18) \x01(\t\x12\x15\n\rterminus_name\x18* \x01(\t\x12\x15\n\rterminus_time\x18+ \x01(\t\x12\x1e\n\x16previous_trip_shape_id\x18\x33 \x01(\t";\n\x05Shape\x12\x10\n\x08shape_id\x18\x01 \x01(\t\x12 \n\x06points\x18\x03 \x03(\x0b\x32\x10.proto.Epsg25829B$\xaa\x02!Costasdev.Busurbano.Backend.Typesb\x06proto3'
-)
-
-_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
-_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "stop_schedule_pb2", globals())
-if _descriptor._USE_C_DESCRIPTORS == False:
- DESCRIPTOR._options = None
- DESCRIPTOR._serialized_options = b"\252\002!Costasdev.Busurbano.Backend.Types"
- _EPSG25829._serialized_start = 30
- _EPSG25829._serialized_end = 63
- _STOPARRIVALS._serialized_start = 66
- _STOPARRIVALS._serialized_end = 581
- _STOPARRIVALS_SCHEDULEDARRIVAL._serialized_start = 192
- _STOPARRIVALS_SCHEDULEDARRIVAL._serialized_end = 581
- _SHAPE._serialized_start = 583
- _SHAPE._serialized_end = 642
-# @@protoc_insertion_point(module_scope)
diff --git a/src/gtfs_perstop_report/src/proto/stop_schedule_pb2.pyi b/src/gtfs_perstop_report/src/proto/stop_schedule_pb2.pyi
deleted file mode 100644
index fc55f4e..0000000
--- a/src/gtfs_perstop_report/src/proto/stop_schedule_pb2.pyi
+++ /dev/null
@@ -1,126 +0,0 @@
-from google.protobuf.internal import containers as _containers
-from google.protobuf import descriptor as _descriptor
-from google.protobuf import message as _message
-from typing import (
- ClassVar as _ClassVar,
- Iterable as _Iterable,
- Mapping as _Mapping,
- Optional as _Optional,
- Union as _Union,
-)
-
-DESCRIPTOR: _descriptor.FileDescriptor
-
-class Epsg25829(_message.Message):
- __slots__ = ["x", "y"]
- X_FIELD_NUMBER: _ClassVar[int]
- Y_FIELD_NUMBER: _ClassVar[int]
- x: float
- y: float
- def __init__(
- self, x: _Optional[float] = ..., y: _Optional[float] = ...
- ) -> None: ...
-
-class Shape(_message.Message):
- __slots__ = ["points", "shape_id"]
- POINTS_FIELD_NUMBER: _ClassVar[int]
- SHAPE_ID_FIELD_NUMBER: _ClassVar[int]
- points: _containers.RepeatedCompositeFieldContainer[Epsg25829]
- shape_id: str
- def __init__(
- self,
- shape_id: _Optional[str] = ...,
- points: _Optional[_Iterable[_Union[Epsg25829, _Mapping]]] = ...,
- ) -> None: ...
-
-class StopArrivals(_message.Message):
- __slots__ = ["arrivals", "location", "stop_id"]
- class ScheduledArrival(_message.Message):
- __slots__ = [
- "calling_ssm",
- "calling_time",
- "line",
- "next_streets",
- "previous_trip_shape_id",
- "route",
- "service_id",
- "shape_dist_traveled",
- "shape_id",
- "starting_code",
- "starting_name",
- "starting_time",
- "stop_sequence",
- "terminus_code",
- "terminus_name",
- "terminus_time",
- "trip_id",
- ]
- CALLING_SSM_FIELD_NUMBER: _ClassVar[int]
- CALLING_TIME_FIELD_NUMBER: _ClassVar[int]
- LINE_FIELD_NUMBER: _ClassVar[int]
- NEXT_STREETS_FIELD_NUMBER: _ClassVar[int]
- PREVIOUS_TRIP_SHAPE_ID_FIELD_NUMBER: _ClassVar[int]
- ROUTE_FIELD_NUMBER: _ClassVar[int]
- SERVICE_ID_FIELD_NUMBER: _ClassVar[int]
- SHAPE_DIST_TRAVELED_FIELD_NUMBER: _ClassVar[int]
- SHAPE_ID_FIELD_NUMBER: _ClassVar[int]
- STARTING_CODE_FIELD_NUMBER: _ClassVar[int]
- STARTING_NAME_FIELD_NUMBER: _ClassVar[int]
- STARTING_TIME_FIELD_NUMBER: _ClassVar[int]
- STOP_SEQUENCE_FIELD_NUMBER: _ClassVar[int]
- TERMINUS_CODE_FIELD_NUMBER: _ClassVar[int]
- TERMINUS_NAME_FIELD_NUMBER: _ClassVar[int]
- TERMINUS_TIME_FIELD_NUMBER: _ClassVar[int]
- TRIP_ID_FIELD_NUMBER: _ClassVar[int]
- calling_ssm: int
- calling_time: str
- line: str
- next_streets: _containers.RepeatedScalarFieldContainer[str]
- previous_trip_shape_id: str
- route: str
- service_id: str
- shape_dist_traveled: float
- shape_id: str
- starting_code: str
- starting_name: str
- starting_time: str
- stop_sequence: int
- terminus_code: str
- terminus_name: str
- terminus_time: str
- trip_id: str
- def __init__(
- self,
- service_id: _Optional[str] = ...,
- trip_id: _Optional[str] = ...,
- line: _Optional[str] = ...,
- route: _Optional[str] = ...,
- shape_id: _Optional[str] = ...,
- shape_dist_traveled: _Optional[float] = ...,
- stop_sequence: _Optional[int] = ...,
- next_streets: _Optional[_Iterable[str]] = ...,
- starting_code: _Optional[str] = ...,
- starting_name: _Optional[str] = ...,
- starting_time: _Optional[str] = ...,
- calling_time: _Optional[str] = ...,
- calling_ssm: _Optional[int] = ...,
- terminus_code: _Optional[str] = ...,
- terminus_name: _Optional[str] = ...,
- terminus_time: _Optional[str] = ...,
- previous_trip_shape_id: _Optional[str] = ...,
- ) -> None: ...
-
- ARRIVALS_FIELD_NUMBER: _ClassVar[int]
- LOCATION_FIELD_NUMBER: _ClassVar[int]
- STOP_ID_FIELD_NUMBER: _ClassVar[int]
- arrivals: _containers.RepeatedCompositeFieldContainer[StopArrivals.ScheduledArrival]
- location: Epsg25829
- stop_id: str
- def __init__(
- self,
- stop_id: _Optional[str] = ...,
- location: _Optional[_Union[Epsg25829, _Mapping]] = ...,
- arrivals: _Optional[
- _Iterable[_Union[StopArrivals.ScheduledArrival, _Mapping]]
- ] = ...,
- ) -> None: ...
diff --git a/src/gtfs_perstop_report/src/providers.py b/src/gtfs_perstop_report/src/providers.py
deleted file mode 100644
index fa04261..0000000
--- a/src/gtfs_perstop_report/src/providers.py
+++ /dev/null
@@ -1,137 +0,0 @@
-"""
-Provider-specific configuration for different GTFS feed formats.
-"""
-
-from typing import Protocol, Optional
-from src.street_name import get_street_name
-
-
-class FeedProvider(Protocol):
- """Protocol defining provider-specific behavior for GTFS feeds."""
-
- @staticmethod
- def format_service_id(service_id: str) -> str:
- """Format service_id for output."""
- ...
-
- @staticmethod
- def format_trip_id(trip_id: str) -> str:
- """Format trip_id for output."""
- ...
-
- @staticmethod
- def format_route(route: str, terminus_name: str) -> str:
- """Format route/headsign, potentially using terminus name as fallback."""
- ...
-
- @staticmethod
- def extract_street_name(stop_name: str) -> str:
- """Extract street name from stop name, or return full name."""
- ...
-
-
-class VitrasaProvider:
- """Provider configuration for Vitrasa (Vigo bus system)."""
-
- @staticmethod
- def format_service_id(service_id: str) -> str:
- """Extract middle part from underscore-separated service_id."""
- parts = service_id.split("_")
- return parts[1] if len(parts) >= 2 else service_id
-
- @staticmethod
- def format_trip_id(trip_id: str) -> str:
- """Extract middle parts from underscore-separated trip_id."""
- parts = trip_id.split("_")
- return "_".join(parts[1:3]) if len(parts) >= 3 else trip_id
-
- @staticmethod
- def format_route(route: str, terminus_name: str) -> str:
- """Return route as-is for Vitrasa."""
- return route
-
- @staticmethod
- def extract_street_name(stop_name: str) -> str:
- """Extract street name from stop name using standard logic."""
- return get_street_name(stop_name) or ""
-
-
-class RenfeProvider:
- """Provider configuration for Renfe (Spanish rail system)."""
-
- @staticmethod
- def format_service_id(service_id: str) -> str:
- """Use full service_id for Renfe (no underscores)."""
- return service_id
-
- @staticmethod
- def format_trip_id(trip_id: str) -> str:
- """Use full trip_id for Renfe (no underscores)."""
- return trip_id
-
- @staticmethod
- def format_route(route: str, terminus_name: str) -> str:
- """Use terminus name as route if route is empty."""
- val = route if route else terminus_name
- return val.replace("NY", "Ñ").replace("ny", "ñ")
-
- @staticmethod
- def extract_street_name(stop_name: str) -> str:
- """Preserve full stop name for train stations."""
- return stop_name.replace("NY", "Ñ").replace("ny", "ñ")
-
-
-class DefaultProvider:
- """Default provider configuration for generic GTFS feeds."""
-
- @staticmethod
- def format_service_id(service_id: str) -> str:
- """Try to extract from underscores, fallback to full ID."""
- parts = service_id.split("_")
- return parts[1] if len(parts) >= 2 else service_id
-
- @staticmethod
- def format_trip_id(trip_id: str) -> str:
- """Try to extract from underscores, fallback to full ID."""
- parts = trip_id.split("_")
- return "_".join(parts[1:3]) if len(parts) >= 3 else trip_id
-
- @staticmethod
- def format_route(route: str, terminus_name: str) -> str:
- """Use terminus name as route if route is empty."""
- return route if route else terminus_name
-
- @staticmethod
- def extract_street_name(stop_name: str) -> str:
- """Extract street name from stop name using standard logic."""
- return get_street_name(stop_name) or ""
-
-
-# Provider registry
-PROVIDERS = {
- "vitrasa": VitrasaProvider,
- "renfe": RenfeProvider,
- "default": DefaultProvider,
-}
-
-
-def get_provider(provider_name: str) -> type[FeedProvider]:
- """
- Get provider configuration by name.
-
- Args:
- provider_name: Name of the provider (case-insensitive)
-
- Returns:
- Provider class with configuration methods
-
- Raises:
- ValueError: If provider name is not recognized
- """
- provider_name_lower = provider_name.lower()
- if provider_name_lower not in PROVIDERS:
- raise ValueError(
- f"Unknown provider: {provider_name}. "
- f"Available providers: {', '.join(PROVIDERS.keys())}"
- )
- return PROVIDERS[provider_name_lower]
diff --git a/src/gtfs_perstop_report/src/report_writer.py b/src/gtfs_perstop_report/src/report_writer.py
deleted file mode 100644
index f6d8763..0000000
--- a/src/gtfs_perstop_report/src/report_writer.py
+++ /dev/null
@@ -1,145 +0,0 @@
-"""
-Report writers for various output formats (HTML, JSON).
-Centralizes all write operations for different report types.
-"""
-
-import json
-import os
-from typing import Any, Dict, List
-
-from src.logger import get_logger
-from src.proto.stop_schedule_pb2 import Epsg25829, StopArrivals
-
-
-def write_stop_protobuf(
- output_dir: str,
- date: str,
- stop_code: str,
- arrivals: List[Dict[str, Any]],
- stop_x: float,
- stop_y: float,
-) -> None:
- """
- Write stop arrivals data to a Protobuf file.
-
- Args:
- output_dir: Base output directory
- date: Date string for the data
- stop_code: Stop code identifier
- stop_arrivals_proto: Serialized Protobuf data
- """
- logger = get_logger("report_writer")
-
- item = StopArrivals(
- stop_id=stop_code,
- location=Epsg25829(x=stop_x, y=stop_y),
- arrivals=[
- StopArrivals.ScheduledArrival(
- service_id=arrival["service_id"],
- trip_id=arrival["trip_id"],
- line=arrival["line"],
- route=arrival["route"],
- shape_id=arrival["shape_id"],
- shape_dist_traveled=arrival["shape_dist_traveled"],
- stop_sequence=arrival["stop_sequence"],
- next_streets=arrival["next_streets"],
- starting_code=arrival["starting_code"],
- starting_name=arrival["starting_name"],
- starting_time=arrival["starting_time"],
- calling_time=arrival["calling_time"],
- calling_ssm=arrival["calling_ssm"],
- terminus_code=arrival["terminus_code"],
- terminus_name=arrival["terminus_name"],
- terminus_time=arrival["terminus_time"],
- previous_trip_shape_id=arrival.get("previous_trip_shape_id", ""),
- )
- for arrival in arrivals
- ],
- )
-
- try:
- # Create the stops directory for this date
- date_dir = os.path.join(output_dir, date)
- os.makedirs(date_dir, exist_ok=True)
-
- # Create the Protobuf file
- file_path = os.path.join(date_dir, f"{stop_code}.pb")
-
- with open(file_path, "wb") as f:
- f.write(item.SerializeToString())
-
- logger.debug(f"Stop Protobuf written to: {file_path}")
- except Exception as e:
- logger.error(
- f"Error writing stop Protobuf to {output_dir}/stops/{date}/{stop_code}.pb: {e}"
- )
- raise
-
-
-def write_stop_json(
- output_dir: str, date: str, stop_code: str, arrivals: List[Dict[str, Any]]
-) -> None:
- """
- Write stop arrivals data to a JSON file.
-
- Args:
- output_dir: Base output directory
- date: Date string for the data
- stop_code: Stop code identifier
- arrivals: List of arrival dictionaries
- pretty: Whether to format JSON with indentation
- """
- logger = get_logger("report_writer")
-
- try:
- # Create the stops directory for this date
- date_dir = os.path.join(output_dir, date)
- os.makedirs(date_dir, exist_ok=True)
-
- # Create the JSON file
- file_path = os.path.join(date_dir, f"{stop_code}.json")
-
- with open(file_path, "w", encoding="utf-8") as f:
- json.dump(arrivals, f, ensure_ascii=False)
-
- logger.debug(f"Stop JSON written to: {file_path}")
- except Exception as e:
- logger.error(
- f"Error writing stop JSON to {output_dir}/stops/{date}/{stop_code}.json: {e}"
- )
- raise
-
-
-def write_index_json(
- output_dir: str,
- data: Dict[str, Any],
- filename: str = "index.json",
- pretty: bool = False,
-) -> None:
- """
- Write index data to a JSON file.
-
- Args:
- output_dir: Directory where the JSON file should be written
- data: Dictionary containing the index data
- filename: Name of the JSON file (default: "index.json")
- pretty: Whether to format JSON with indentation
- """
- logger = get_logger("report_writer")
-
- try:
- # Create the output directory if it doesn't exist
- os.makedirs(output_dir, exist_ok=True)
-
- # Write the index.json file
- index_filepath = os.path.join(output_dir, filename)
- with open(index_filepath, "w", encoding="utf-8") as f:
- if pretty:
- json.dump(data, f, ensure_ascii=False, indent=2)
- else:
- json.dump(data, f, ensure_ascii=False, separators=(",", ":"))
-
- logger.info(f"Index JSON written to: {index_filepath}")
- except Exception as e:
- logger.error(f"Error writing index JSON to {output_dir}/{filename}: {e}")
- raise
diff --git a/src/gtfs_perstop_report/src/rolling_dates.py b/src/gtfs_perstop_report/src/rolling_dates.py
deleted file mode 100644
index 3c6b166..0000000
--- a/src/gtfs_perstop_report/src/rolling_dates.py
+++ /dev/null
@@ -1,168 +0,0 @@
-"""
-
-Rolling dates module.
-
-Handles mapping of future dates not in a GTFS feed to equivalent dates that exist in the feed.
-This allows extending feed coverage by reusing data from past dates.
-"""
-import json
-import os
-from typing import Optional, Dict, Tuple
-from datetime import datetime
-from src.logger import get_logger
-
-logger = get_logger("rolling_dates")
-
-
-class RollingDateConfig:
- """
- Manages rolling date mappings from a configuration file.
-
- The configuration file should be a JSON file with the following format:
- {
- "2025-09-30": "2025-09-24",
- "2025-10-01": "2025-09-25"
- }
-
- Where keys are the target dates (not in feed) and values are the source dates (in feed).
- """
-
- def __init__(self, config_path: Optional[str] = None):
- """
- Initialize the rolling date configuration.
-
- Args:
- config_path: Path to the JSON configuration file. If None, no mapping is active.
- """
- self.mappings: Dict[str, str] = {}
- self.config_path = config_path
-
- if config_path:
- self._load_config(config_path)
-
- def _load_config(self, config_path: str):
- """
- Load rolling date mappings from a JSON file.
-
- Args:
- config_path: Path to the JSON configuration file.
-
- Raises:
- FileNotFoundError: If the config file doesn't exist.
- json.JSONDecodeError: If the config file is not valid JSON.
- ValueError: If the config file has invalid date formats.
- """
- if not os.path.exists(config_path):
- raise FileNotFoundError(f"Rolling dates config file not found: {config_path}")
-
- try:
- with open(config_path, 'r', encoding='utf-8') as f:
- data = json.load(f)
-
- # Validate that data is a dictionary
- if not isinstance(data, dict):
- raise ValueError("Rolling dates config must be a JSON object (dictionary)")
-
- # Validate date formats
- for target_date, source_date in data.items():
- self._validate_date_format(target_date, "target")
- self._validate_date_format(source_date, "source")
-
- self.mappings = data
- logger.info(f"Loaded {len(self.mappings)} rolling date mappings from {config_path}")
-
- except json.JSONDecodeError as e:
- logger.error(f"Failed to parse rolling dates config: {e}")
- raise
- except Exception as e:
- logger.error(f"Error loading rolling dates config: {e}")
- raise
-
- def _validate_date_format(self, date_str: str, date_type: str):
- """
- Validate that a date string is in YYYY-MM-DD format.
-
- Args:
- date_str: The date string to validate.
- date_type: Type of date (for error messages).
-
- Raises:
- ValueError: If the date format is invalid.
- """
- try:
- datetime.strptime(date_str, '%Y-%m-%d')
- except ValueError:
- raise ValueError(
- f"Invalid {date_type} date format '{date_str}'. "
- f"Expected YYYY-MM-DD format."
- )
-
- def get_source_date(self, target_date: str) -> Optional[str]:
- """
- Get the source date for a given target date.
-
- Args:
- target_date: The date to look up (YYYY-MM-DD format).
-
- Returns:
- The source date if a mapping exists, None otherwise.
- """
- return self.mappings.get(target_date)
-
- def is_rolling_date(self, date: str) -> bool:
- """
- Check if a date is a rolling date (has a mapping).
-
- Args:
- date: The date to check (YYYY-MM-DD format).
-
- Returns:
- True if the date has a rolling mapping, False otherwise.
- """
- return date in self.mappings
-
- def get_mapping_info(self, target_date: str) -> Optional[Tuple[str, str]]:
- """
- Get complete mapping information for a target date.
-
- Args:
- target_date: The date to look up (YYYY-MM-DD format).
-
- Returns:
- Tuple of (source_date, target_date) if mapping exists, None otherwise.
- """
- source_date = self.get_source_date(target_date)
- if source_date:
- return (source_date, target_date)
- return None
-
- def has_mappings(self) -> bool:
- """
- Check if any rolling date mappings are configured.
-
- Returns:
- True if at least one mapping exists, False otherwise.
- """
- return len(self.mappings) > 0
-
- def get_all_mappings(self) -> Dict[str, str]:
- """
- Get all configured rolling date mappings.
-
- Returns:
- Dictionary of target_date -> source_date mappings.
- """
- return self.mappings.copy()
-
-
-def create_rolling_date_config(config_path: Optional[str] = None) -> RollingDateConfig:
- """
- Factory function to create a RollingDateConfig instance.
-
- Args:
- config_path: Path to the JSON configuration file. If None, returns an empty config.
-
- Returns:
- RollingDateConfig instance.
- """
- return RollingDateConfig(config_path)
diff --git a/src/gtfs_perstop_report/src/routes.py b/src/gtfs_perstop_report/src/routes.py
deleted file mode 100644
index 06cf0e5..0000000
--- a/src/gtfs_perstop_report/src/routes.py
+++ /dev/null
@@ -1,47 +0,0 @@
-"""
-Module for loading and querying GTFS routes data.
-"""
-
-import os
-import csv
-from src.logger import get_logger
-
-logger = get_logger("routes")
-
-
-def load_routes(feed_dir: str) -> dict[str, dict[str, str]]:
- """
- Load routes data from the GTFS feed.
-
- Returns:
- dict[str, dict[str, str]]: A dictionary where keys are route IDs and values are dictionaries
- containing route_short_name and route_color.
- """
- routes: dict[str, dict[str, str]] = {}
- routes_file_path = os.path.join(feed_dir, "routes.txt")
-
- try:
- with open(routes_file_path, "r", encoding="utf-8") as routes_file:
- reader = csv.DictReader(routes_file)
- header = reader.fieldnames or []
- if "route_color" not in header:
- logger.warning(
- "Column 'route_color' not found in routes.txt. Defaulting to black (#000000)."
- )
-
- for row in reader:
- route_id = row["route_id"]
- if "route_color" in row and row["route_color"]:
- route_color = row["route_color"]
- else:
- route_color = "000000"
- routes[route_id] = {
- "route_short_name": row["route_short_name"],
- "route_color": route_color,
- }
- except FileNotFoundError:
- raise FileNotFoundError(f"Routes file not found at {routes_file_path}")
- except KeyError as e:
- raise KeyError(f"Missing required column in routes file: {e}")
-
- return routes
diff --git a/src/gtfs_perstop_report/src/services.py b/src/gtfs_perstop_report/src/services.py
deleted file mode 100644
index d456e43..0000000
--- a/src/gtfs_perstop_report/src/services.py
+++ /dev/null
@@ -1,120 +0,0 @@
-import os
-import datetime
-from src.logger import get_logger
-
-logger = get_logger("services")
-
-
-def get_active_services(feed_dir: str, date: str) -> list[str]:
- """
- Get active services for a given date based on the 'calendar.txt' and 'calendar_dates.txt' files.
-
- Args:
- date (str): Date in 'YYYY-MM-DD' format.
-
- Returns:
- list[str]: List of active service IDs for the given date.
-
- Raises:
- ValueError: If the date format is incorrect.
- """
- search_date = date.replace("-", "").replace(":", "").replace("/", "")
- weekday = datetime.datetime.strptime(date, "%Y-%m-%d").weekday()
- active_services: list[str] = []
-
- try:
- with open(
- os.path.join(feed_dir, "calendar.txt"), "r", encoding="utf-8"
- ) as calendar_file:
- lines = calendar_file.readlines()
- if len(lines) > 1:
- # First parse the header, get each column's index
- header = lines[0].strip().split(",")
- try:
- service_id_index = header.index("service_id")
- monday_index = header.index("monday")
- tuesday_index = header.index("tuesday")
- wednesday_index = header.index("wednesday")
- thursday_index = header.index("thursday")
- friday_index = header.index("friday")
- saturday_index = header.index("saturday")
- sunday_index = header.index("sunday")
- start_date_index = header.index("start_date")
- end_date_index = header.index("end_date")
- except ValueError as e:
- logger.error(f"Required column not found in header: {e}")
- return active_services
- # Now read the rest of the file, find all services where the day of the week matches
- weekday_columns = {
- 0: monday_index,
- 1: tuesday_index,
- 2: wednesday_index,
- 3: thursday_index,
- 4: friday_index,
- 5: saturday_index,
- 6: sunday_index,
- }
-
- for idx, line in enumerate(lines[1:], 1):
- parts = line.strip().split(",")
- if len(parts) < len(header):
- logger.warning(
- f"Skipping malformed line in calendar.txt line {idx + 1}: {line.strip()}"
- )
- continue
-
- service_id = parts[service_id_index]
- day_value = parts[weekday_columns[weekday]]
- start_date = parts[start_date_index]
- end_date = parts[end_date_index]
-
- # Check if day of week is active AND date is within the service range
- if day_value == "1" and start_date <= search_date <= end_date:
- active_services.append(service_id)
- except FileNotFoundError:
- logger.warning("calendar.txt file not found.")
-
- try:
- with open(
- os.path.join(feed_dir, "calendar_dates.txt"), "r", encoding="utf-8"
- ) as calendar_dates_file:
- lines = calendar_dates_file.readlines()
- if len(lines) <= 1:
- logger.warning(
- "calendar_dates.txt file is empty or has only header line, not processing."
- )
- return active_services
-
- header = lines[0].strip().split(",")
- try:
- service_id_index = header.index("service_id")
- date_index = header.index("date")
- exception_type_index = header.index("exception_type")
- except ValueError as e:
- logger.error(f"Required column not found in header: {e}")
- return active_services
-
- # Now read the rest of the file, find all services where 'date' matches the search_date
- # Start from 1 to skip header
- for idx, line in enumerate(lines[1:], 1):
- parts = line.strip().split(",")
- if len(parts) < len(header):
- logger.warning(
- f"Skipping malformed line in calendar_dates.txt line {idx + 1}: {line.strip()}"
- )
- continue
-
- service_id = parts[service_id_index]
- date_value = parts[date_index]
- exception_type = parts[exception_type_index]
-
- if date_value == search_date and exception_type == "1":
- active_services.append(service_id)
-
- if date_value == search_date and exception_type == "2":
- if service_id in active_services:
- active_services.remove(service_id)
- except FileNotFoundError:
- logger.warning("calendar_dates.txt file not found.")
-
- return active_services
diff --git a/src/gtfs_perstop_report/src/shapes.py b/src/gtfs_perstop_report/src/shapes.py
deleted file mode 100644
index a308999..0000000
--- a/src/gtfs_perstop_report/src/shapes.py
+++ /dev/null
@@ -1,103 +0,0 @@
-import csv
-from dataclasses import dataclass
-import os
-from typing import Dict, Optional
-
-from pyproj import Transformer
-
-from src.logger import get_logger
-
-
-logger = get_logger("shapes")
-
-
-@dataclass
-class Shape:
- shape_id: str
- shape_pt_lat: Optional[float]
- shape_pt_lon: Optional[float]
- shape_pt_position: Optional[int]
- shape_dist_traveled: Optional[float]
-
- shape_pt_25829_x: Optional[float] = None
- shape_pt_25829_y: Optional[float] = None
-
-
-def process_shapes(feed_dir: str, out_dir: str) -> None:
- file_path = os.path.join(feed_dir, "shapes.txt")
- shapes: Dict[str, list[Shape]] = {}
-
- transformer = Transformer.from_crs(4326, 25829, always_xy=True)
-
- try:
- with open(file_path, "r", encoding="utf-8", newline="") as f:
- reader = csv.DictReader(f, quotechar='"', delimiter=",")
- for row_num, row in enumerate(reader, start=2):
- try:
- shape = Shape(
- shape_id=row["shape_id"],
- shape_pt_lat=float(row["shape_pt_lat"])
- if row.get("shape_pt_lat")
- else None,
- shape_pt_lon=float(row["shape_pt_lon"])
- if row.get("shape_pt_lon")
- else None,
- shape_pt_position=int(row["shape_pt_position"])
- if row.get("shape_pt_position")
- else None,
- shape_dist_traveled=float(row["shape_dist_traveled"])
- if row.get("shape_dist_traveled")
- else None,
- )
-
- if (
- shape.shape_pt_lat is not None
- and shape.shape_pt_lon is not None
- ):
- shape_pt_25829_x, shape_pt_25829_y = transformer.transform(
- shape.shape_pt_lon, shape.shape_pt_lat
- )
- shape.shape_pt_25829_x = shape_pt_25829_x
- shape.shape_pt_25829_y = shape_pt_25829_y
-
- if shape.shape_id not in shapes:
- shapes[shape.shape_id] = []
- shapes[shape.shape_id].append(shape)
- except Exception as e:
- logger.warning(
- f"Error parsing stops.txt line {row_num}: {e} - line data: {row}"
- )
- except FileNotFoundError:
- logger.error(f"File not found: {file_path}")
- except Exception as e:
- logger.error(f"Error reading stops.txt: {e}")
-
- # Write shapes to Protobuf files
- from src.proto.stop_schedule_pb2 import Epsg25829, Shape as PbShape
-
- for shape_id, shape_points in shapes.items():
- points = sorted(
- shape_points,
- key=lambda sp: sp.shape_pt_position
- if sp.shape_pt_position is not None
- else 0,
- )
-
- pb_shape = PbShape(
- shape_id=shape_id,
- points=[
- Epsg25829(x=pt.shape_pt_25829_x, y=pt.shape_pt_25829_y)
- for pt in points
- if pt.shape_pt_25829_x is not None and pt.shape_pt_25829_y is not None
- ],
- )
-
- shape_file_path = os.path.join(out_dir, "shapes", f"{shape_id}.pb")
- os.makedirs(os.path.dirname(shape_file_path), exist_ok=True)
-
- try:
- with open(shape_file_path, "wb") as f:
- f.write(pb_shape.SerializeToString())
- logger.debug(f"Shape Protobuf written to: {shape_file_path}")
- except Exception as e:
- logger.error(f"Error writing shape Protobuf to {shape_file_path}: {e}")
diff --git a/src/gtfs_perstop_report/src/stop_schedule_pb2.py b/src/gtfs_perstop_report/src/stop_schedule_pb2.py
deleted file mode 100644
index 76a1da4..0000000
--- a/src/gtfs_perstop_report/src/stop_schedule_pb2.py
+++ /dev/null
@@ -1,40 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by the protocol buffer compiler. DO NOT EDIT!
-# NO CHECKED-IN PROTOBUF GENCODE
-# source: stop_schedule.proto
-# Protobuf Python Version: 6.33.0
-"""Generated protocol buffer code."""
-
-from google.protobuf import descriptor as _descriptor
-from google.protobuf import descriptor_pool as _descriptor_pool
-from google.protobuf import runtime_version as _runtime_version
-from google.protobuf import symbol_database as _symbol_database
-from google.protobuf.internal import builder as _builder
-
-_runtime_version.ValidateProtobufRuntimeVersion(
- _runtime_version.Domain.PUBLIC, 6, 33, 0, "", "stop_schedule.proto"
-)
-# @@protoc_insertion_point(imports)
-
-_sym_db = _symbol_database.Default()
-
-
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(
- b'\n\x13stop_schedule.proto\x12\x05proto"!\n\tEpsg25829\x12\t\n\x01x\x18\x01 \x01(\x01\x12\t\n\x01y\x18\x02 \x01(\x01"\xe3\x03\n\x0cStopArrivals\x12\x0f\n\x07stop_id\x18\x01 \x01(\t\x12"\n\x08location\x18\x03 \x01(\x0b\x32\x10.proto.Epsg25829\x12\x36\n\x08\x61rrivals\x18\x05 \x03(\x0b\x32$.proto.StopArrivals.ScheduledArrival\x1a\xe5\x02\n\x10ScheduledArrival\x12\x12\n\nservice_id\x18\x01 \x01(\t\x12\x0f\n\x07trip_id\x18\x02 \x01(\t\x12\x0c\n\x04line\x18\x03 \x01(\t\x12\r\n\x05route\x18\x04 \x01(\t\x12\x10\n\x08shape_id\x18\x05 \x01(\t\x12\x1b\n\x13shape_dist_traveled\x18\x06 \x01(\x01\x12\x15\n\rstop_sequence\x18\x0b \x01(\r\x12\x14\n\x0cnext_streets\x18\x0c \x03(\t\x12\x15\n\rstarting_code\x18\x15 \x01(\t\x12\x15\n\rstarting_name\x18\x16 \x01(\t\x12\x15\n\rstarting_time\x18\x17 \x01(\t\x12\x14\n\x0c\x63\x61lling_time\x18! \x01(\t\x12\x13\n\x0b\x63\x61lling_ssm\x18" \x01(\r\x12\x15\n\rterminus_code\x18) \x01(\t\x12\x15\n\rterminus_name\x18* \x01(\t\x12\x15\n\rterminus_time\x18+ \x01(\tB$\xaa\x02!Costasdev.Busurbano.Backend.Typesb\x06proto3'
-)
-
-_globals = globals()
-_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
-_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "stop_schedule_pb2", _globals)
-if not _descriptor._USE_C_DESCRIPTORS:
- _globals["DESCRIPTOR"]._loaded_options = None
- _globals[
- "DESCRIPTOR"
- ]._serialized_options = b"\252\002!Costasdev.Busurbano.Backend.Types"
- _globals["_EPSG25829"]._serialized_start = 30
- _globals["_EPSG25829"]._serialized_end = 63
- _globals["_STOPARRIVALS"]._serialized_start = 66
- _globals["_STOPARRIVALS"]._serialized_end = 549
- _globals["_STOPARRIVALS_SCHEDULEDARRIVAL"]._serialized_start = 192
- _globals["_STOPARRIVALS_SCHEDULEDARRIVAL"]._serialized_end = 549
-# @@protoc_insertion_point(module_scope)
diff --git a/src/gtfs_perstop_report/src/stop_schedule_pb2.pyi b/src/gtfs_perstop_report/src/stop_schedule_pb2.pyi
deleted file mode 100644
index c8d7f36..0000000
--- a/src/gtfs_perstop_report/src/stop_schedule_pb2.pyi
+++ /dev/null
@@ -1,88 +0,0 @@
-from google.protobuf.internal import containers as _containers
-from google.protobuf import descriptor as _descriptor
-from google.protobuf import message as _message
-from collections.abc import Iterable as _Iterable, Mapping as _Mapping
-from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
-
-DESCRIPTOR: _descriptor.FileDescriptor
-
-class Epsg25829(_message.Message):
- __slots__ = ()
- X_FIELD_NUMBER: _ClassVar[int]
- Y_FIELD_NUMBER: _ClassVar[int]
- x: float
- y: float
- def __init__(
- self, x: _Optional[float] = ..., y: _Optional[float] = ...
- ) -> None: ...
-
-class StopArrivals(_message.Message):
- __slots__ = ()
- class ScheduledArrival(_message.Message):
- __slots__ = ()
- SERVICE_ID_FIELD_NUMBER: _ClassVar[int]
- TRIP_ID_FIELD_NUMBER: _ClassVar[int]
- LINE_FIELD_NUMBER: _ClassVar[int]
- ROUTE_FIELD_NUMBER: _ClassVar[int]
- SHAPE_ID_FIELD_NUMBER: _ClassVar[int]
- SHAPE_DIST_TRAVELED_FIELD_NUMBER: _ClassVar[int]
- STOP_SEQUENCE_FIELD_NUMBER: _ClassVar[int]
- NEXT_STREETS_FIELD_NUMBER: _ClassVar[int]
- STARTING_CODE_FIELD_NUMBER: _ClassVar[int]
- STARTING_NAME_FIELD_NUMBER: _ClassVar[int]
- STARTING_TIME_FIELD_NUMBER: _ClassVar[int]
- CALLING_TIME_FIELD_NUMBER: _ClassVar[int]
- CALLING_SSM_FIELD_NUMBER: _ClassVar[int]
- TERMINUS_CODE_FIELD_NUMBER: _ClassVar[int]
- TERMINUS_NAME_FIELD_NUMBER: _ClassVar[int]
- TERMINUS_TIME_FIELD_NUMBER: _ClassVar[int]
- service_id: str
- trip_id: str
- line: str
- route: str
- shape_id: str
- shape_dist_traveled: float
- stop_sequence: int
- next_streets: _containers.RepeatedScalarFieldContainer[str]
- starting_code: str
- starting_name: str
- starting_time: str
- calling_time: str
- calling_ssm: int
- terminus_code: str
- terminus_name: str
- terminus_time: str
- def __init__(
- self,
- service_id: _Optional[str] = ...,
- trip_id: _Optional[str] = ...,
- line: _Optional[str] = ...,
- route: _Optional[str] = ...,
- shape_id: _Optional[str] = ...,
- shape_dist_traveled: _Optional[float] = ...,
- stop_sequence: _Optional[int] = ...,
- next_streets: _Optional[_Iterable[str]] = ...,
- starting_code: _Optional[str] = ...,
- starting_name: _Optional[str] = ...,
- starting_time: _Optional[str] = ...,
- calling_time: _Optional[str] = ...,
- calling_ssm: _Optional[int] = ...,
- terminus_code: _Optional[str] = ...,
- terminus_name: _Optional[str] = ...,
- terminus_time: _Optional[str] = ...,
- ) -> None: ...
-
- STOP_ID_FIELD_NUMBER: _ClassVar[int]
- LOCATION_FIELD_NUMBER: _ClassVar[int]
- ARRIVALS_FIELD_NUMBER: _ClassVar[int]
- stop_id: str
- location: Epsg25829
- arrivals: _containers.RepeatedCompositeFieldContainer[StopArrivals.ScheduledArrival]
- def __init__(
- self,
- stop_id: _Optional[str] = ...,
- location: _Optional[_Union[Epsg25829, _Mapping]] = ...,
- arrivals: _Optional[
- _Iterable[_Union[StopArrivals.ScheduledArrival, _Mapping]]
- ] = ...,
- ) -> None: ...
diff --git a/src/gtfs_perstop_report/src/stop_times.py b/src/gtfs_perstop_report/src/stop_times.py
deleted file mode 100644
index c48f505..0000000
--- a/src/gtfs_perstop_report/src/stop_times.py
+++ /dev/null
@@ -1,151 +0,0 @@
-"""
-Functions for handling GTFS stop_times data.
-"""
-
-import csv
-import os
-from src.logger import get_logger
-
-logger = get_logger("stop_times")
-
-
-STOP_TIMES_BY_FEED: dict[str, dict[str, list["StopTime"]]] = {}
-STOP_TIMES_BY_REQUEST: dict[
- tuple[str, frozenset[str]], dict[str, list["StopTime"]]
-] = {}
-
-
-class StopTime:
- """
- Class representing a stop time entry in the GTFS data.
- """
-
- def __init__(
- self,
- trip_id: str,
- arrival_time: str,
- departure_time: str,
- stop_id: str,
- stop_sequence: int,
- shape_dist_traveled: float | None,
- ):
- self.trip_id = trip_id
- self.arrival_time = arrival_time
- self.departure_time = departure_time
- self.stop_id = stop_id
- self.stop_sequence = stop_sequence
- self.shape_dist_traveled = shape_dist_traveled
- self.day_change = False # New attribute to indicate day change
-
- def __str__(self):
- return f"StopTime({self.trip_id=}, {self.arrival_time=}, {self.departure_time=}, {self.stop_id=}, {self.stop_sequence=})"
-
-
-def _load_stop_times_for_feed(feed_dir: str) -> dict[str, list[StopTime]]:
- """Load and cache all stop_times for a feed directory."""
- if feed_dir in STOP_TIMES_BY_FEED:
- return STOP_TIMES_BY_FEED[feed_dir]
-
- stops: dict[str, list[StopTime]] = {}
-
- try:
- with open(
- os.path.join(feed_dir, "stop_times.txt"), "r", encoding="utf-8", newline=""
- ) as stop_times_file:
- reader = csv.DictReader(stop_times_file)
- if reader.fieldnames is None:
- logger.error("stop_times.txt missing header row.")
- STOP_TIMES_BY_FEED[feed_dir] = {}
- return STOP_TIMES_BY_FEED[feed_dir]
-
- required_columns = [
- "trip_id",
- "arrival_time",
- "departure_time",
- "stop_id",
- "stop_sequence",
- ]
- missing_columns = [
- col for col in required_columns if col not in reader.fieldnames
- ]
- if missing_columns:
- logger.error(f"Required columns not found in header: {missing_columns}")
- STOP_TIMES_BY_FEED[feed_dir] = {}
- return STOP_TIMES_BY_FEED[feed_dir]
-
- has_shape_dist = "shape_dist_traveled" in reader.fieldnames
- if not has_shape_dist:
- logger.warning(
- "Column 'shape_dist_traveled' not found in stop_times.txt. Distances will be set to None."
- )
-
- for row in reader:
- trip_id = row["trip_id"]
- if trip_id not in stops:
- stops[trip_id] = []
-
- dist = None
- if has_shape_dist and row["shape_dist_traveled"]:
- try:
- dist = float(row["shape_dist_traveled"])
- except ValueError:
- pass
-
- try:
- stops[trip_id].append(
- StopTime(
- trip_id=trip_id,
- arrival_time=row["arrival_time"],
- departure_time=row["departure_time"],
- stop_id=row["stop_id"],
- stop_sequence=int(row["stop_sequence"]),
- shape_dist_traveled=dist,
- )
- )
- except ValueError as e:
- logger.warning(
- f"Error parsing stop_sequence for trip {trip_id}: {e}"
- )
-
- for trip_stop_times in stops.values():
- trip_stop_times.sort(key=lambda st: st.stop_sequence)
-
- except FileNotFoundError:
- logger.warning("stop_times.txt file not found.")
- stops = {}
-
- STOP_TIMES_BY_FEED[feed_dir] = stops
- return stops
-
-
-def get_stops_for_trips(
- feed_dir: str, trip_ids: list[str]
-) -> dict[str, list[StopTime]]:
- """
- Get stops for a list of trip IDs based on the cached 'stop_times.txt' data.
- """
- if not trip_ids:
- return {}
-
- request_key = (feed_dir, frozenset(trip_ids))
- cached_subset = STOP_TIMES_BY_REQUEST.get(request_key)
- if cached_subset is not None:
- return cached_subset
-
- feed_cache = _load_stop_times_for_feed(feed_dir)
- if not feed_cache:
- STOP_TIMES_BY_REQUEST[request_key] = {}
- return {}
-
- result: dict[str, list[StopTime]] = {}
- seen: set[str] = set()
- for trip_id in trip_ids:
- if trip_id in seen:
- continue
- seen.add(trip_id)
- trip_stop_times = feed_cache.get(trip_id)
- if trip_stop_times:
- result[trip_id] = trip_stop_times
-
- STOP_TIMES_BY_REQUEST[request_key] = result
- return result
diff --git a/src/gtfs_perstop_report/src/stops.py b/src/gtfs_perstop_report/src/stops.py
deleted file mode 100644
index fb95cf2..0000000
--- a/src/gtfs_perstop_report/src/stops.py
+++ /dev/null
@@ -1,98 +0,0 @@
-import csv
-import os
-from dataclasses import dataclass
-from typing import Dict, Optional
-
-from pyproj import Transformer
-
-from src.logger import get_logger
-
-logger = get_logger("stops")
-
-
-@dataclass
-class Stop:
- stop_id: str
- stop_code: Optional[str]
- stop_name: Optional[str]
- stop_lat: Optional[float]
- stop_lon: Optional[float]
-
- stop_25829_x: Optional[float] = None
- stop_25829_y: Optional[float] = None
-
-
-CACHED_STOPS: dict[str, dict[str, Stop]] = {}
-CACHED_BY_CODE: dict[str, dict[str, Stop]] = {}
-
-
-def get_all_stops_by_code(feed_dir: str) -> Dict[str, Stop]:
- if feed_dir in CACHED_BY_CODE:
- return CACHED_BY_CODE[feed_dir]
-
- transformer = Transformer.from_crs(4326, 25829, always_xy=True)
-
- stops_by_code: Dict[str, Stop] = {}
- all_stops = get_all_stops(feed_dir)
-
- for stop in all_stops.values():
- stop_25829_x, stop_25829_y = transformer.transform(stop.stop_lon, stop.stop_lat)
- stop.stop_25829_x = stop_25829_x
- stop.stop_25829_y = stop_25829_y
-
- if stop.stop_code:
- stops_by_code[get_numeric_code(stop.stop_code)] = stop
- else:
- stops_by_code[stop.stop_id] = stop
-
- CACHED_BY_CODE[feed_dir] = stops_by_code
-
- return stops_by_code
-
-
-def get_all_stops(feed_dir: str) -> Dict[str, Stop]:
- if feed_dir in CACHED_STOPS:
- return CACHED_STOPS[feed_dir]
-
- stops: Dict[str, Stop] = {}
- file_path = os.path.join(feed_dir, "stops.txt")
-
- try:
- with open(file_path, "r", encoding="utf-8", newline="") as f:
- reader = csv.DictReader(f, quotechar='"', delimiter=",")
- for row_num, row in enumerate(reader, start=2):
- try:
- stop = Stop(
- stop_id=row["stop_id"],
- stop_code=row.get("stop_code"),
- stop_name=row["stop_name"].strip()
- if row.get("stop_name", "").strip()
- else row.get("stop_desc"),
- stop_lat=float(row["stop_lat"])
- if row.get("stop_lat")
- else None,
- stop_lon=float(row["stop_lon"])
- if row.get("stop_lon")
- else None,
- )
- stops[stop.stop_id] = stop
- except Exception as e:
- logger.warning(
- f"Error parsing stops.txt line {row_num}: {e} - line data: {row}"
- )
-
- except FileNotFoundError:
- logger.error(f"File not found: {file_path}")
- except Exception as e:
- logger.error(f"Error reading stops.txt: {e}")
-
- CACHED_STOPS[feed_dir] = stops
-
- return stops
-
-
-def get_numeric_code(stop_code: str | None) -> str:
- if not stop_code:
- return ""
- numeric_code = "".join(c for c in stop_code if c.isdigit())
- return str(int(numeric_code)) if numeric_code else ""
diff --git a/src/gtfs_perstop_report/src/street_name.py b/src/gtfs_perstop_report/src/street_name.py
deleted file mode 100644
index e744ea3..0000000
--- a/src/gtfs_perstop_report/src/street_name.py
+++ /dev/null
@@ -1,52 +0,0 @@
-import re
-
-
-re_remove_quotation_marks = re.compile(r'[""”]', re.IGNORECASE)
-re_anything_before_stopcharacters_with_parentheses = re.compile(
- r"^(.*?)(?:,|\s\s|\s-\s| \d| S\/N|\s\()", re.IGNORECASE
-)
-
-
-NAME_REPLACEMENTS = {
- "Rúa da Salguera Entrada": "Rúa da Salgueira",
- "Rúa da Salgueira Entrada": "Rúa da Salgueira",
- "Estrada de Miraflores": "Estrada Miraflores",
- "FORA DE SERVIZO.G.B.": "",
- "Praza de Fernando O Católico": "",
- "Rúa da Travesía de Vigo": "Travesía de Vigo",
- "Rúa de ": " ",
- "Rúa do ": " ",
- "Rúa da ": " ",
- "Rúa das ": " ",
- "Avda. de ": " ",
- "Avda. do ": " ",
- "Avda. da ": " ",
- "Avda. das ": " ",
- "Riós": "Ríos",
-}
-
-
-def get_street_name(original_name: str) -> str:
- original_name = re.sub(re_remove_quotation_marks, "", original_name).strip()
- match = re.match(re_anything_before_stopcharacters_with_parentheses, original_name)
- if match:
- street_name = match.group(1)
- else:
- street_name = original_name
-
- for old_name, new_name in NAME_REPLACEMENTS.items():
- if old_name.lower() in street_name.lower():
- street_name = street_name.replace(old_name, new_name)
- return street_name.strip()
-
- return street_name
-
-
-def normalise_stop_name(original_name: str | None) -> str:
- if original_name is None:
- return ""
- stop_name = re.sub(re_remove_quotation_marks, "", original_name).strip()
-
- stop_name = stop_name.replace(" ", ", ")
-
- return stop_name
diff --git a/src/gtfs_perstop_report/src/trips.py b/src/gtfs_perstop_report/src/trips.py
deleted file mode 100644
index 0de632a..0000000
--- a/src/gtfs_perstop_report/src/trips.py
+++ /dev/null
@@ -1,155 +0,0 @@
-"""
-Functions for handling GTFS trip data.
-"""
-
-import os
-from src.logger import get_logger
-
-logger = get_logger("trips")
-
-
-class TripLine:
- """
- Class representing a trip line in the GTFS data.
- """
-
- def __init__(
- self,
- route_id: str,
- service_id: str,
- trip_id: str,
- headsign: str,
- direction_id: int,
- shape_id: str | None = None,
- block_id: str | None = None,
- ):
- self.route_id = route_id
- self.service_id = service_id
- self.trip_id = trip_id
- self.headsign = headsign
- self.direction_id = direction_id
- self.shape_id = shape_id
- self.block_id = block_id
- self.route_short_name = ""
- self.route_color = ""
-
- def __str__(self):
- return f"TripLine({self.route_id=}, {self.service_id=}, {self.trip_id=}, {self.headsign=}, {self.direction_id=}, {self.shape_id=}, {self.block_id=})"
-
-
-TRIPS_BY_SERVICE_ID: dict[str, dict[str, list[TripLine]]] = {}
-
-
-def get_trips_for_services(
- feed_dir: str, service_ids: list[str]
-) -> dict[str, list[TripLine]]:
- """
- Get trips for a list of service IDs based on the 'trips.txt' file.
- Uses caching to avoid reading and parsing the file multiple times.
-
- Args:
- feed_dir (str): Directory containing the GTFS feed files.
- service_ids (list[str]): List of service IDs to find trips for.
-
- Returns:
- dict[str, list[TripLine]]: Dictionary mapping service IDs to lists of trip objects.
- """
- # Check if we already have cached data for this feed directory
- if feed_dir in TRIPS_BY_SERVICE_ID:
- logger.debug(f"Using cached trips data for {feed_dir}")
- # Return only the trips for the requested service IDs
- return {
- service_id: TRIPS_BY_SERVICE_ID[feed_dir].get(service_id, [])
- for service_id in service_ids
- }
-
- trips: dict[str, list[TripLine]] = {}
-
- try:
- with open(
- os.path.join(feed_dir, "trips.txt"), "r", encoding="utf-8"
- ) as trips_file:
- lines = trips_file.readlines()
- if len(lines) <= 1:
- logger.warning(
- "trips.txt file is empty or has only header line, not processing."
- )
- return trips
-
- header = lines[0].strip().split(",")
- try:
- service_id_index = header.index("service_id")
- trip_id_index = header.index("trip_id")
- route_id_index = header.index("route_id")
- headsign_index = header.index("trip_headsign")
- direction_id_index = header.index("direction_id")
- except ValueError as e:
- logger.error(f"Required column not found in header: {e}")
- return trips
-
- # Check if shape_id column exists
- shape_id_index = None
- if "shape_id" in header:
- shape_id_index = header.index("shape_id")
- else:
- logger.warning("shape_id column not found in trips.txt")
-
- # Check if block_id column exists
- block_id_index = None
- if "block_id" in header:
- block_id_index = header.index("block_id")
- else:
- logger.info("block_id column not found in trips.txt")
-
- # Initialize cache for this feed directory
- TRIPS_BY_SERVICE_ID[feed_dir] = {}
-
- for line in lines[1:]:
- parts = line.strip().split(",")
- if len(parts) < len(header):
- logger.warning(
- f"Skipping malformed line in trips.txt: {line.strip()}"
- )
- continue
-
- service_id = parts[service_id_index]
- trip_id = parts[trip_id_index]
-
- # Cache all trips, not just the ones requested
- if service_id not in TRIPS_BY_SERVICE_ID[feed_dir]:
- TRIPS_BY_SERVICE_ID[feed_dir][service_id] = []
-
- # Get shape_id if available
- shape_id = None
- if shape_id_index is not None and shape_id_index < len(parts):
- shape_id = parts[shape_id_index] if parts[shape_id_index] else None
-
- # Get block_id if available
- block_id = None
- if block_id_index is not None and block_id_index < len(parts):
- block_id = parts[block_id_index] if parts[block_id_index] else None
-
- trip_line = TripLine(
- route_id=parts[route_id_index],
- service_id=service_id,
- trip_id=trip_id,
- headsign=parts[headsign_index],
- direction_id=int(
- parts[direction_id_index] if parts[direction_id_index] else -1
- ),
- shape_id=shape_id,
- block_id=block_id,
- )
-
- TRIPS_BY_SERVICE_ID[feed_dir][service_id].append(trip_line)
-
- # Also build the result for the requested service IDs
- if service_id in service_ids:
- if service_id not in trips:
- trips[service_id] = []
- trips[service_id].append(trip_line)
-
- except FileNotFoundError:
- logger.warning("trips.txt file not found.")
-
- return trips
diff --git a/src/gtfs_perstop_report/stop_report.py b/src/gtfs_perstop_report/stop_report.py
deleted file mode 100644
index ef40417..0000000
--- a/src/gtfs_perstop_report/stop_report.py
+++ /dev/null
@@ -1,677 +0,0 @@
-import argparse
-import os
-import shutil
-import sys
-import time
-import traceback
-from typing import Any, Dict, List, Optional, Tuple
-
-from src.shapes import process_shapes
-from src.common import get_all_feed_dates
-from src.download import download_feed_from_url
-from src.logger import get_logger
-from src.report_writer import write_stop_json, write_stop_protobuf
-from src.routes import load_routes
-from src.services import get_active_services
-from src.rolling_dates import create_rolling_date_config
-from src.stop_times import get_stops_for_trips, StopTime
-from src.stops import get_all_stops, get_all_stops_by_code, get_numeric_code
-from src.street_name import normalise_stop_name
-from src.trips import get_trips_for_services, TripLine
-from src.providers import get_provider
-
-logger = get_logger("stop_report")
-
-
-def parse_args():
- parser = argparse.ArgumentParser(
- description="Generate stop-based JSON reports for a date or date range."
- )
- parser.add_argument(
- "--output-dir",
- type=str,
- default="./output/",
- help="Directory to write reports to (default: ./output/)",
- )
- parser.add_argument("--feed-dir", type=str, help="Path to the feed directory")
- parser.add_argument(
- "--feed-url",
- type=str,
- help="URL to download the GTFS feed from (if not using local feed directory)",
- )
- parser.add_argument(
- "--force-download",
- action="store_true",
- help="Force download even if the feed hasn't been modified (only applies when using --feed-url)",
- )
- parser.add_argument(
- "--provider",
- type=str,
- default="default",
- help="Feed provider type (vitrasa, renfe, default). Default: default",
- )
- parser.add_argument('--rolling-dates', type=str,
- help="Path to rolling dates configuration file (JSON)")
- args = parser.parse_args()
-
- if args.feed_dir and args.feed_url:
- parser.error("Specify either --feed-dir or --feed-url, not both.")
- if not args.feed_dir and not args.feed_url:
- parser.error(
- "You must specify either a path to the existing feed (unzipped) or a URL to download the GTFS feed from."
- )
- if args.feed_dir and not os.path.exists(args.feed_dir):
- parser.error(f"Feed directory does not exist: {args.feed_dir}")
- return args
-
-
-def time_to_seconds(time_str: str) -> int:
- """
- Convert HH:MM:SS to seconds since midnight.
- Handles GTFS times that can exceed 24 hours (e.g., 25:30:00 for 1:30 AM next day).
- """
- if not time_str:
- return 0
-
- parts = time_str.split(":")
- if len(parts) != 3:
- return 0
-
- try:
- hours, minutes, seconds = map(int, parts)
- return hours * 3600 + minutes * 60 + seconds
- except ValueError:
- return 0
-
-
-def normalize_gtfs_time(time_str: str) -> str:
- """
- Normalize GTFS time format to standard HH:MM:SS (0-23 hours).
- Converts times like 25:30:00 to 01:30:00.
-
- Args:
- time_str: Time in HH:MM:SS format, possibly with hours >= 24
-
- Returns:
- Normalized time string in HH:MM:SS format
- """
- if not time_str:
- return time_str
-
- parts = time_str.split(":")
- if len(parts) != 3:
- return time_str
-
- try:
- hours, minutes, seconds = map(int, parts)
- normalized_hours = hours % 24
- return f"{normalized_hours:02d}:{minutes:02d}:{seconds:02d}"
- except ValueError:
- return time_str
-
-
-def format_gtfs_time(time_str: str) -> str:
- """
- Format GTFS time to HH:MM:SS, preserving hours >= 24.
- """
- if not time_str:
- return time_str
-
- parts = time_str.split(":")
- if len(parts) != 3:
- return time_str
-
- try:
- hours, minutes, seconds = map(int, parts)
- return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
- except ValueError:
- return time_str
-
-
-def is_next_day_service(time_str: str) -> bool:
- """
- Check if a GTFS time represents a service on the next day (hours >= 24).
-
- Args:
- time_str: Time in HH:MM:SS format
-
- Returns:
- True if the time is >= 24:00:00, False otherwise
- """
- if not time_str:
- return False
-
- parts = time_str.split(":")
- if len(parts) != 3:
- return False
-
- try:
- hours = int(parts[0])
- return hours >= 24
- except ValueError:
- return False
-
-
-def parse_trip_id_components(trip_id: str) -> Optional[Tuple[str, str, int]]:
- """
- Parse a trip ID in format XXXYYY-Z or XXXYYY_Z where:
- - XXX = line number (e.g., 003)
- - YYY = shift/internal ID (e.g., 001)
- - Z = trip number (e.g., 12)
-
- Supported formats:
- 1. ..._XXXYYY_Z (e.g. "C1 01SNA00_001001_18")
- 2. ..._XXXYYY-Z (e.g. "VIGO_20241122_003001-12")
-
- Returns tuple of (line, shift_id, trip_number) or None if parsing fails.
- """
- try:
- parts = trip_id.split("_")
- if len(parts) < 2:
- return None
-
- # Try format 1: ..._XXXYYY_Z
- # Check if second to last part is 6 digits (XXXYYY) and last part is numeric
- if len(parts) >= 2:
- shift_part = parts[-2]
- trip_num_str = parts[-1]
- if len(shift_part) == 6 and shift_part.isdigit() and trip_num_str.isdigit():
- line = shift_part[:3]
- shift_id = shift_part[3:6]
- trip_number = int(trip_num_str)
- return (line, shift_id, trip_number)
-
- # Try format 2: ..._XXXYYY-Z
- # The trip ID is the last part in format XXXYYY-Z
- trip_part = parts[-1]
-
- if "-" in trip_part:
- shift_part, trip_num_str = trip_part.split("-", 1)
-
- # shift_part should be 6 digits: XXXYYY
- if len(shift_part) == 6 and shift_part.isdigit():
- line = shift_part[:3] # First 3 digits
- shift_id = shift_part[3:6] # Next 3 digits
- trip_number = int(trip_num_str)
- return (line, shift_id, trip_number)
-
- return None
- except (ValueError, IndexError):
- return None
-
-
-def build_trip_previous_shape_map(
- trips: Dict[str, List[TripLine]],
- stops_for_all_trips: Dict[str, List[StopTime]],
-) -> Dict[str, Optional[str]]:
- """
- Build a mapping from trip_id to previous_trip_shape_id.
-
- Links trips based on trip ID structure (XXXYYY-Z) where trips with the same
- XXX (line) and YYY (shift) and sequential Z (trip numbers) are connected
- if the terminus of trip N matches the start of trip N+1.
-
- Args:
- trips: Dictionary of service_id -> list of trips
- stops_for_all_trips: Dictionary of trip_id -> list of stop times
-
- Returns:
- Dictionary mapping trip_id to previous_trip_shape_id (or None)
- """
- trip_previous_shape: Dict[str, Optional[str]] = {}
-
- # Collect all trips across all services
- all_trips_list: List[TripLine] = []
- for trip_list in trips.values():
- all_trips_list.extend(trip_list)
-
- # Group trips by shift ID (line + shift combination)
- trips_by_shift: Dict[str, List[Tuple[TripLine, int, str, str]]] = {}
-
- for trip in all_trips_list:
- parsed = parse_trip_id_components(trip.trip_id)
- if not parsed:
- continue
-
- line, shift_id, trip_number = parsed
- shift_key = f"{line}{shift_id}"
-
- trip_stops = stops_for_all_trips.get(trip.trip_id)
- if not trip_stops or len(trip_stops) < 2:
- continue
-
- first_stop = trip_stops[0]
- last_stop = trip_stops[-1]
-
- if shift_key not in trips_by_shift:
- trips_by_shift[shift_key] = []
-
- trips_by_shift[shift_key].append(
- (trip, trip_number, first_stop.stop_id, last_stop.stop_id)
- )
- # For each shift, sort trips by trip number and link consecutive trips
- for shift_key, shift_trips in trips_by_shift.items():
- # Sort by trip number
- shift_trips.sort(key=lambda x: x[1])
- # Link consecutive trips if their stops match
- for i in range(1, len(shift_trips)):
- current_trip, current_num, current_start_stop, _ = shift_trips[i]
- prev_trip, prev_num, _, prev_end_stop = shift_trips[i - 1]
-
- # Check if trips are consecutive (trip numbers differ by 1),
- # if previous trip's terminus matches current trip's start,
- # and if both trips have valid shape IDs
- if (
- current_num == prev_num + 1
- and prev_end_stop == current_start_stop
- and prev_trip.shape_id
- and current_trip.shape_id
- ):
- trip_previous_shape[current_trip.trip_id] = prev_trip.shape_id
-
- return trip_previous_shape
-
-
-def get_stop_arrivals(
- feed_dir: str, date: str, provider, rolling_config=None
-) -> Dict[str, List[Dict[str, Any]]]:
- """
- Process trips for the given date and organize stop arrivals.
- Also includes night services from the previous day (times >= 24:00:00).
-
- Args:
- feed_dir: Path to the GTFS feed directory
- date: Date in YYYY-MM-DD format
- provider: Provider class with feed-specific formatting methods
- rolling_config: Optional RollingDateConfig for date mapping
-
- Returns:
- Dictionary mapping stop_code to lists of arrival information.
- """
- from datetime import datetime, timedelta
-
- stops = get_all_stops(feed_dir)
- logger.info(f"Found {len(stops)} stops in the feed.")
-
- effective_date = date
- if rolling_config and rolling_config.is_rolling_date(date):
- effective_date = rolling_config.get_source_date(date)
- logger.info(f"Using source date {effective_date} for rolling date {date}")
-
- active_services = get_active_services(feed_dir, effective_date)
- if not active_services:
- logger.info(f"No active services found for the given date {effective_date}.")
-
- logger.info(f"Found {len(active_services)} active services for date {effective_date}.")
-
- # Also get services from the previous day to include night services (times >= 24:00)
- prev_date = (datetime.strptime(effective_date, "%Y-%m-%d") - timedelta(days=1)).strftime(
- "%Y-%m-%d"
- )
- prev_services = get_active_services(feed_dir, prev_date)
- logger.info(
- f"Found {len(prev_services)} active services for previous date {prev_date} (for night services)."
- )
-
- all_services = list(set(active_services + prev_services))
-
- if not all_services:
- logger.info("No active services found for current or previous date.")
- return {}
-
- trips = get_trips_for_services(feed_dir, all_services)
- total_trip_count = sum(len(trip_list) for trip_list in trips.values())
- logger.info(f"Found {total_trip_count} trips for active services.")
-
- # Get all trip IDs
- all_trip_ids = [trip.trip_id for trip_list in trips.values() for trip in trip_list]
-
- # Get stops for all trips
- stops_for_all_trips = get_stops_for_trips(feed_dir, all_trip_ids)
- logger.info(f"Precomputed stops for {len(stops_for_all_trips)} trips.")
-
- # Build mapping from trip_id to previous trip's shape_id
- trip_previous_shape_map = build_trip_previous_shape_map(trips, stops_for_all_trips)
- logger.info(
- f"Built previous trip shape mapping for {len(trip_previous_shape_map)} trips."
- )
-
- # Load routes information
- routes = load_routes(feed_dir)
- logger.info(f"Loaded {len(routes)} routes from feed.")
-
- # Create a reverse lookup from stop_id to stop_code (or stop_id as fallback)
- stop_id_to_code = {}
- for stop_id, stop in stops.items():
- if stop.stop_code:
- stop_id_to_code[stop_id] = get_numeric_code(stop.stop_code)
- else:
- # Fallback to stop_id if stop_code is not available (e.g., train stations)
- stop_id_to_code[stop_id] = stop_id
-
- # Organize data by stop_code
- stop_arrivals = {}
-
- active_services_set = set(active_services)
- prev_services_set = set(prev_services)
-
- for service_id, trip_list in trips.items():
- is_active = service_id in active_services_set
- is_prev = service_id in prev_services_set
-
- if not is_active and not is_prev:
- continue
-
- for trip in trip_list:
- # Get route information once per trip
- route_info = routes.get(trip.route_id, {})
- route_short_name = route_info.get("route_short_name", "")
- trip_headsign = getattr(trip, "headsign", "") or ""
- trip_id = trip.trip_id
-
- # Get stop times for this trip
- trip_stops = stops_for_all_trips.get(trip.trip_id, [])
- if not trip_stops:
- continue
-
- # Pair stop_times with stop metadata once to avoid repeated lookups
- trip_stop_pairs = []
- stop_names = []
- for stop_time in trip_stops:
- stop = stops.get(stop_time.stop_id)
- trip_stop_pairs.append((stop_time, stop))
- stop_names.append(stop.stop_name if stop else "Unknown Stop")
-
- # Memoize street names per stop name for this trip and build segments
- street_cache: dict[str, str] = {}
- segment_names: list[str] = []
- stop_to_segment_idx: list[int] = []
- previous_street: str | None = None
- for name in stop_names:
- street = street_cache.get(name)
- if street is None:
- street = provider.extract_street_name(name)
- street_cache[name] = street
- if street != previous_street:
- segment_names.append(street)
- previous_street = street
- stop_to_segment_idx.append(len(segment_names) - 1)
-
- # Precompute future street transitions per segment
- future_suffix_by_segment: list[tuple[str, ...]] = [()] * len(segment_names)
- future_tuple: tuple[str, ...] = ()
- for idx in range(len(segment_names) - 1, -1, -1):
- future_suffix_by_segment[idx] = future_tuple
- current_street = segment_names[idx]
- future_tuple = (
- (current_street,) + future_tuple
- if current_street is not None
- else future_tuple
- )
-
- segment_future_lists: dict[int, list[str]] = {}
-
- first_stop_time, first_stop = trip_stop_pairs[0]
- last_stop_time, last_stop = trip_stop_pairs[-1]
-
- starting_stop_name = first_stop.stop_name if first_stop else "Unknown Stop"
- terminus_stop_name = last_stop.stop_name if last_stop else "Unknown Stop"
-
- # Get stop codes with fallback to stop_id if stop_code is empty
- if first_stop:
- starting_code = get_numeric_code(first_stop.stop_code)
- if not starting_code:
- starting_code = first_stop_time.stop_id
- else:
- starting_code = ""
-
- if last_stop:
- terminus_code = get_numeric_code(last_stop.stop_code)
- if not terminus_code:
- terminus_code = last_stop_time.stop_id
- else:
- terminus_code = ""
-
- starting_name = normalise_stop_name(starting_stop_name)
- terminus_name = normalise_stop_name(terminus_stop_name)
- starting_time = first_stop_time.departure_time
- terminus_time = last_stop_time.arrival_time
-
- # Determine processing passes for this trip
- passes = []
- if is_active:
- passes.append("current")
- if is_prev:
- passes.append("previous")
-
- for mode in passes:
- is_current_mode = mode == "current"
-
- for i, (stop_time, _) in enumerate(trip_stop_pairs):
- # Skip the last stop of the trip (terminus) to avoid duplication
- if i == len(trip_stop_pairs) - 1:
- continue
-
- stop_code = stop_id_to_code.get(stop_time.stop_id)
-
- if not stop_code:
- continue # Skip stops without a code
-
- dep_time = stop_time.departure_time
-
- if not is_current_mode:
- # Previous day service: only include if calling_time >= 24:00:00 (night services rolling to this day)
- if not is_next_day_service(dep_time):
- continue
-
- # Normalize times for display on current day (e.g. 25:30 -> 01:30)
- final_starting_time = normalize_gtfs_time(starting_time)
- final_calling_time = normalize_gtfs_time(dep_time)
- final_terminus_time = normalize_gtfs_time(terminus_time)
- # SSM should be small (early morning)
- final_calling_ssm = time_to_seconds(final_calling_time)
- else:
- # Current day service: include ALL times
- # Keep times as is (e.g. 25:30 stays 25:30)
- final_starting_time = format_gtfs_time(starting_time)
- final_calling_time = format_gtfs_time(dep_time)
- final_terminus_time = format_gtfs_time(terminus_time)
- # SSM should be large if > 24:00
- final_calling_ssm = time_to_seconds(dep_time)
-
- if stop_code not in stop_arrivals:
- stop_arrivals[stop_code] = []
-
- if segment_names:
- segment_idx = stop_to_segment_idx[i]
- if segment_idx not in segment_future_lists:
- segment_future_lists[segment_idx] = list(
- future_suffix_by_segment[segment_idx]
- )
- next_streets = segment_future_lists[segment_idx].copy()
- else:
- next_streets = []
-
- # Format IDs and route using provider-specific logic
- service_id_fmt = provider.format_service_id(service_id)
- trip_id_fmt = provider.format_trip_id(trip_id)
- route_fmt = provider.format_route(trip_headsign, terminus_name)
-
- # Get previous trip shape_id if available
- previous_trip_shape_id = trip_previous_shape_map.get(trip_id, "")
-
- stop_arrivals[stop_code].append(
- {
- "service_id": service_id_fmt,
- "trip_id": trip_id_fmt,
- "line": route_short_name,
- "route": route_fmt,
- "shape_id": getattr(trip, "shape_id", ""),
- "stop_sequence": stop_time.stop_sequence,
- "shape_dist_traveled": getattr(
- stop_time, "shape_dist_traveled", 0
- ),
- "next_streets": [s for s in next_streets if s != ""],
- "starting_code": starting_code,
- "starting_name": starting_name,
- "starting_time": final_starting_time,
- "calling_time": final_calling_time,
- "calling_ssm": final_calling_ssm,
- "terminus_code": terminus_code,
- "terminus_name": terminus_name,
- "terminus_time": final_terminus_time,
- "previous_trip_shape_id": previous_trip_shape_id,
- }
- )
-
- # Sort each stop's arrivals by arrival time
- for stop_code in stop_arrivals:
- # Filter out entries with None arrival_seconds
- stop_arrivals[stop_code] = [
- item for item in stop_arrivals[stop_code] if item["calling_ssm"] is not None
- ]
- stop_arrivals[stop_code].sort(key=lambda x: x["calling_ssm"])
-
- return stop_arrivals
-
-
-def process_date(
- feed_dir: str, date: str, output_dir: str, provider, rolling_config=None
-) -> tuple[str, Dict[str, int]]:
- """
- Process a single date and write its stop JSON files.
- Returns summary data for index generation.
- """
- logger = get_logger(f"stop_report_{date}")
- try:
- logger.info(f"Starting stop report generation for date {date}")
-
- stops_by_code = get_all_stops_by_code(feed_dir)
-
- # Get all stop arrivals for the current date
- stop_arrivals = get_stop_arrivals(feed_dir, date, provider, rolling_config)
-
- if not stop_arrivals:
- logger.warning(f"No stop arrivals found for date {date}")
- return date, {}
-
- logger.info(
- f"Writing stop reports for {len(stop_arrivals)} stops for date {date}"
- )
-
- # Write individual stop JSON files
- writing_start_time = time.perf_counter()
- for stop_code, arrivals in stop_arrivals.items():
- write_stop_json(output_dir, date, stop_code, arrivals)
- writing_end_time = time.perf_counter()
- writing_elapsed = writing_end_time - writing_start_time
-
- logger.info(
- f"Finished writing stop JSON reports for date {date} in {writing_elapsed:.2f}s"
- )
-
- # Write individual stop JSON files
- writing_start_time = time.perf_counter()
- for stop_code, arrivals in stop_arrivals.items():
- stop_by_code = stops_by_code.get(stop_code)
-
- if stop_by_code is not None:
- write_stop_protobuf(
- output_dir,
- date,
- stop_code,
- arrivals,
- stop_by_code.stop_25829_x or 0.0,
- stop_by_code.stop_25829_y or 0.0,
- )
-
- writing_end_time = time.perf_counter()
- writing_elapsed = writing_end_time - writing_start_time
-
- logger.info(
- f"Finished writing stop protobuf reports for date {date} in {writing_elapsed:.2f}s"
- )
-
- logger.info(f"Processed {len(stop_arrivals)} stops for date {date}")
-
- stop_summary = {
- stop_code: len(arrivals) for stop_code, arrivals in stop_arrivals.items()
- }
- return date, stop_summary
- except Exception as e:
- logger.error(f"Error processing date {date}: {e}")
- raise
-
-
-def main():
- args = parse_args()
- output_dir = args.output_dir
- feed_url = args.feed_url
-
- # Get provider configuration
- try:
- provider = get_provider(args.provider)
- logger.info(f"Using provider: {args.provider}")
- except ValueError as e:
- logger.error(str(e))
- sys.exit(1)
-
- if not feed_url:
- feed_dir = args.feed_dir
- else:
- logger.info(f"Downloading GTFS feed from {feed_url}...")
- feed_dir = download_feed_from_url(feed_url, output_dir, args.force_download)
- if feed_dir is None:
- logger.info("Download was skipped (feed not modified). Exiting.")
- return
-
- all_dates = get_all_feed_dates(feed_dir)
- if not all_dates:
- logger.error("No valid dates found in feed.")
- return
- date_list = all_dates
-
- # Handle rolling dates
- rolling_config = create_rolling_date_config(args.rolling_dates)
- if rolling_config.has_mappings():
- for target_date in rolling_config.get_all_mappings().keys():
- if target_date not in date_list:
- date_list.append(target_date)
- # Sort dates to ensure they are processed in order
- date_list.sort()
-
- # Ensure date_list is not empty before processing
- if not date_list:
- logger.error("No valid dates to process.")
- return
-
- logger.info(f"Processing {len(date_list)} dates")
-
- # Dictionary to store summary data for index files
- all_stops_summary = {}
-
- for date in date_list:
- _, stop_summary = process_date(feed_dir, date, output_dir, provider, rolling_config)
- all_stops_summary[date] = stop_summary
-
- logger.info("Finished processing all dates. Beginning with shape transformation.")
-
- # Process shapes, converting each coordinate to EPSG:25829 and saving as Protobuf
- process_shapes(feed_dir, output_dir)
-
- logger.info("Finished processing shapes.")
-
- if feed_url:
- if os.path.exists(feed_dir):
- shutil.rmtree(feed_dir)
- logger.info(f"Removed temporary feed directory: {feed_dir}")
-
-
-if __name__ == "__main__":
- try:
- main()
- except Exception as e:
- logger = get_logger("stop_report")
- logger.critical(f"An unexpected error occurred: {e}", exc_info=True)
- traceback.print_exc()
- sys.exit(1)
diff --git a/src/gtfs_perstop_report/uv.lock b/src/gtfs_perstop_report/uv.lock
deleted file mode 100644
index bf7b7bd..0000000
--- a/src/gtfs_perstop_report/uv.lock
+++ /dev/null
@@ -1,253 +0,0 @@
-version = 1
-revision = 3
-requires-python = ">=3.13"
-
-[[package]]
-name = "certifi"
-version = "2025.4.26"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" },
-]
-
-[[package]]
-name = "charset-normalizer"
-version = "3.4.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" },
- { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" },
- { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" },
- { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" },
- { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" },
- { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" },
- { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" },
- { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" },
- { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" },
- { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" },
- { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" },
- { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" },
- { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" },
- { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
-]
-
-[[package]]
-name = "colorama"
-version = "0.4.6"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
-]
-
-[[package]]
-name = "gtfs-vigo"
-version = "0.1.0"
-source = { virtual = "." }
-dependencies = [
- { name = "colorama" },
- { name = "jinja2" },
- { name = "protobuf" },
- { name = "pyproj" },
- { name = "pytest" },
- { name = "requests" },
-]
-
-[package.metadata]
-requires-dist = [
- { name = "colorama", specifier = ">=0.4.6" },
- { name = "jinja2", specifier = ">=3.1.6" },
- { name = "protobuf", specifier = ">=5.29.1" },
- { name = "pyproj", specifier = ">=3.7.2" },
- { name = "pytest", specifier = ">=8.4.1" },
- { name = "requests", specifier = ">=2.32.3" },
-]
-
-[[package]]
-name = "idna"
-version = "3.10"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
-]
-
-[[package]]
-name = "iniconfig"
-version = "2.1.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
-]
-
-[[package]]
-name = "jinja2"
-version = "3.1.6"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "markupsafe" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
-]
-
-[[package]]
-name = "markupsafe"
-version = "3.0.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" },
- { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" },
- { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" },
- { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" },
- { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" },
- { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" },
- { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" },
- { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" },
- { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" },
- { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" },
- { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" },
- { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" },
- { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" },
- { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" },
- { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" },
- { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" },
- { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" },
- { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" },
- { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" },
- { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
-]
-
-[[package]]
-name = "packaging"
-version = "25.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
-]
-
-[[package]]
-name = "pluggy"
-version = "1.6.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
-]
-
-[[package]]
-name = "protobuf"
-version = "6.33.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/0a/03/a1440979a3f74f16cab3b75b0da1a1a7f922d56a8ddea96092391998edc0/protobuf-6.33.1.tar.gz", hash = "sha256:97f65757e8d09870de6fd973aeddb92f85435607235d20b2dfed93405d00c85b", size = 443432, upload-time = "2025-11-13T16:44:18.895Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/06/f1/446a9bbd2c60772ca36556bac8bfde40eceb28d9cc7838755bc41e001d8f/protobuf-6.33.1-cp310-abi3-win32.whl", hash = "sha256:f8d3fdbc966aaab1d05046d0240dd94d40f2a8c62856d41eaa141ff64a79de6b", size = 425593, upload-time = "2025-11-13T16:44:06.275Z" },
- { url = "https://files.pythonhosted.org/packages/a6/79/8780a378c650e3df849b73de8b13cf5412f521ca2ff9b78a45c247029440/protobuf-6.33.1-cp310-abi3-win_amd64.whl", hash = "sha256:923aa6d27a92bf44394f6abf7ea0500f38769d4b07f4be41cb52bd8b1123b9ed", size = 436883, upload-time = "2025-11-13T16:44:09.222Z" },
- { url = "https://files.pythonhosted.org/packages/cd/93/26213ff72b103ae55bb0d73e7fb91ea570ef407c3ab4fd2f1f27cac16044/protobuf-6.33.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:fe34575f2bdde76ac429ec7b570235bf0c788883e70aee90068e9981806f2490", size = 427522, upload-time = "2025-11-13T16:44:10.475Z" },
- { url = "https://files.pythonhosted.org/packages/c2/32/df4a35247923393aa6b887c3b3244a8c941c32a25681775f96e2b418f90e/protobuf-6.33.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:f8adba2e44cde2d7618996b3fc02341f03f5bc3f2748be72dc7b063319276178", size = 324445, upload-time = "2025-11-13T16:44:11.869Z" },
- { url = "https://files.pythonhosted.org/packages/8e/d0/d796e419e2ec93d2f3fa44888861c3f88f722cde02b7c3488fcc6a166820/protobuf-6.33.1-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:0f4cf01222c0d959c2b399142deb526de420be8236f22c71356e2a544e153c53", size = 339161, upload-time = "2025-11-13T16:44:12.778Z" },
- { url = "https://files.pythonhosted.org/packages/1d/2a/3c5f05a4af06649547027d288747f68525755de692a26a7720dced3652c0/protobuf-6.33.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:8fd7d5e0eb08cd5b87fd3df49bc193f5cfd778701f47e11d127d0afc6c39f1d1", size = 323171, upload-time = "2025-11-13T16:44:14.035Z" },
- { url = "https://files.pythonhosted.org/packages/08/b4/46310463b4f6ceef310f8348786f3cff181cea671578e3d9743ba61a459e/protobuf-6.33.1-py3-none-any.whl", hash = "sha256:d595a9fd694fdeb061a62fbe10eb039cc1e444df81ec9bb70c7fc59ebcb1eafa", size = 170477, upload-time = "2025-11-13T16:44:17.633Z" },
-]
-
-[[package]]
-name = "pygments"
-version = "2.19.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
-]
-
-[[package]]
-name = "pyproj"
-version = "3.7.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "certifi" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/04/90/67bd7260b4ea9b8b20b4f58afef6c223ecb3abf368eb4ec5bc2cdef81b49/pyproj-3.7.2.tar.gz", hash = "sha256:39a0cf1ecc7e282d1d30f36594ebd55c9fae1fda8a2622cee5d100430628f88c", size = 226279, upload-time = "2025-08-14T12:05:42.18Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/be/14/faf1b90d267cea68d7e70662e7f88cefdb1bc890bd596c74b959e0517a72/pyproj-3.7.2-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:19466e529b1b15eeefdf8ff26b06fa745856c044f2f77bf0edbae94078c1dfa1", size = 6214580, upload-time = "2025-08-14T12:04:28.804Z" },
- { url = "https://files.pythonhosted.org/packages/35/48/da9a45b184d375f62667f62eba0ca68569b0bd980a0bb7ffcc1d50440520/pyproj-3.7.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:c79b9b84c4a626c5dc324c0d666be0bfcebd99f7538d66e8898c2444221b3da7", size = 4615388, upload-time = "2025-08-14T12:04:30.553Z" },
- { url = "https://files.pythonhosted.org/packages/5e/e7/d2b459a4a64bca328b712c1b544e109df88e5c800f7c143cfbc404d39bfb/pyproj-3.7.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ceecf374cacca317bc09e165db38ac548ee3cad07c3609442bd70311c59c21aa", size = 9628455, upload-time = "2025-08-14T12:04:32.435Z" },
- { url = "https://files.pythonhosted.org/packages/f8/85/c2b1706e51942de19076eff082f8495e57d5151364e78b5bef4af4a1d94a/pyproj-3.7.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5141a538ffdbe4bfd157421828bb2e07123a90a7a2d6f30fa1462abcfb5ce681", size = 9514269, upload-time = "2025-08-14T12:04:34.599Z" },
- { url = "https://files.pythonhosted.org/packages/34/38/07a9b89ae7467872f9a476883a5bad9e4f4d1219d31060f0f2b282276cbe/pyproj-3.7.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f000841e98ea99acbb7b8ca168d67773b0191de95187228a16110245c5d954d5", size = 10808437, upload-time = "2025-08-14T12:04:36.485Z" },
- { url = "https://files.pythonhosted.org/packages/12/56/fda1daeabbd39dec5b07f67233d09f31facb762587b498e6fc4572be9837/pyproj-3.7.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8115faf2597f281a42ab608ceac346b4eb1383d3b45ab474fd37341c4bf82a67", size = 10745540, upload-time = "2025-08-14T12:04:38.568Z" },
- { url = "https://files.pythonhosted.org/packages/0d/90/c793182cbba65a39a11db2ac6b479fe76c59e6509ae75e5744c344a0da9d/pyproj-3.7.2-cp313-cp313-win32.whl", hash = "sha256:f18c0579dd6be00b970cb1a6719197fceecc407515bab37da0066f0184aafdf3", size = 5896506, upload-time = "2025-08-14T12:04:41.059Z" },
- { url = "https://files.pythonhosted.org/packages/be/0f/747974129cf0d800906f81cd25efd098c96509026e454d4b66868779ab04/pyproj-3.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:bb41c29d5f60854b1075853fe80c58950b398d4ebb404eb532536ac8d2834ed7", size = 6310195, upload-time = "2025-08-14T12:04:42.974Z" },
- { url = "https://files.pythonhosted.org/packages/82/64/fc7598a53172c4931ec6edf5228280663063150625d3f6423b4c20f9daff/pyproj-3.7.2-cp313-cp313-win_arm64.whl", hash = "sha256:2b617d573be4118c11cd96b8891a0b7f65778fa7733ed8ecdb297a447d439100", size = 6230748, upload-time = "2025-08-14T12:04:44.491Z" },
- { url = "https://files.pythonhosted.org/packages/aa/f0/611dd5cddb0d277f94b7af12981f56e1441bf8d22695065d4f0df5218498/pyproj-3.7.2-cp313-cp313t-macosx_13_0_x86_64.whl", hash = "sha256:d27b48f0e81beeaa2b4d60c516c3a1cfbb0c7ff6ef71256d8e9c07792f735279", size = 6241729, upload-time = "2025-08-14T12:04:46.274Z" },
- { url = "https://files.pythonhosted.org/packages/15/93/40bd4a6c523ff9965e480870611aed7eda5aa2c6128c6537345a2b77b542/pyproj-3.7.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:55a3610d75023c7b1c6e583e48ef8f62918e85a2ae81300569d9f104d6684bb6", size = 4652497, upload-time = "2025-08-14T12:04:48.203Z" },
- { url = "https://files.pythonhosted.org/packages/1b/ae/7150ead53c117880b35e0d37960d3138fe640a235feb9605cb9386f50bb0/pyproj-3.7.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:8d7349182fa622696787cc9e195508d2a41a64765da9b8a6bee846702b9e6220", size = 9942610, upload-time = "2025-08-14T12:04:49.652Z" },
- { url = "https://files.pythonhosted.org/packages/d8/17/7a4a7eafecf2b46ab64e5c08176c20ceb5844b503eaa551bf12ccac77322/pyproj-3.7.2-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:d230b186eb876ed4f29a7c5ee310144c3a0e44e89e55f65fb3607e13f6db337c", size = 9692390, upload-time = "2025-08-14T12:04:51.731Z" },
- { url = "https://files.pythonhosted.org/packages/c3/55/ae18f040f6410f0ea547a21ada7ef3e26e6c82befa125b303b02759c0e9d/pyproj-3.7.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:237499c7862c578d0369e2b8ac56eec550e391a025ff70e2af8417139dabb41c", size = 11047596, upload-time = "2025-08-14T12:04:53.748Z" },
- { url = "https://files.pythonhosted.org/packages/e6/2e/d3fff4d2909473f26ae799f9dda04caa322c417a51ff3b25763f7d03b233/pyproj-3.7.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8c225f5978abd506fd9a78eaaf794435e823c9156091cabaab5374efb29d7f69", size = 10896975, upload-time = "2025-08-14T12:04:55.875Z" },
- { url = "https://files.pythonhosted.org/packages/f2/bc/8fc7d3963d87057b7b51ebe68c1e7c51c23129eee5072ba6b86558544a46/pyproj-3.7.2-cp313-cp313t-win32.whl", hash = "sha256:2da731876d27639ff9d2d81c151f6ab90a1546455fabd93368e753047be344a2", size = 5953057, upload-time = "2025-08-14T12:04:58.466Z" },
- { url = "https://files.pythonhosted.org/packages/cc/27/ea9809966cc47d2d51e6d5ae631ea895f7c7c7b9b3c29718f900a8f7d197/pyproj-3.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:f54d91ae18dd23b6c0ab48126d446820e725419da10617d86a1b69ada6d881d3", size = 6375414, upload-time = "2025-08-14T12:04:59.861Z" },
- { url = "https://files.pythonhosted.org/packages/5b/f8/1ef0129fba9a555c658e22af68989f35e7ba7b9136f25758809efec0cd6e/pyproj-3.7.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fc52ba896cfc3214dc9f9ca3c0677a623e8fdd096b257c14a31e719d21ff3fdd", size = 6262501, upload-time = "2025-08-14T12:05:01.39Z" },
- { url = "https://files.pythonhosted.org/packages/42/17/c2b050d3f5b71b6edd0d96ae16c990fdc42a5f1366464a5c2772146de33a/pyproj-3.7.2-cp314-cp314-macosx_13_0_x86_64.whl", hash = "sha256:2aaa328605ace41db050d06bac1adc11f01b71fe95c18661497763116c3a0f02", size = 6214541, upload-time = "2025-08-14T12:05:03.166Z" },
- { url = "https://files.pythonhosted.org/packages/03/68/68ada9c8aea96ded09a66cfd9bf87aa6db8c2edebe93f5bf9b66b0143fbc/pyproj-3.7.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:35dccbce8201313c596a970fde90e33605248b66272595c061b511c8100ccc08", size = 4617456, upload-time = "2025-08-14T12:05:04.563Z" },
- { url = "https://files.pythonhosted.org/packages/81/e4/4c50ceca7d0e937977866b02cb64e6ccf4df979a5871e521f9e255df6073/pyproj-3.7.2-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:25b0b7cb0042444c29a164b993c45c1b8013d6c48baa61dc1160d834a277e83b", size = 9615590, upload-time = "2025-08-14T12:05:06.094Z" },
- { url = "https://files.pythonhosted.org/packages/05/1e/ada6fb15a1d75b5bd9b554355a69a798c55a7dcc93b8d41596265c1772e3/pyproj-3.7.2-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:85def3a6388e9ba51f964619aa002a9d2098e77c6454ff47773bb68871024281", size = 9474960, upload-time = "2025-08-14T12:05:07.973Z" },
- { url = "https://files.pythonhosted.org/packages/51/07/9d48ad0a8db36e16f842f2c8a694c1d9d7dcf9137264846bef77585a71f3/pyproj-3.7.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b1bccefec3875ab81eabf49059e2b2ea77362c178b66fd3528c3e4df242f1516", size = 10799478, upload-time = "2025-08-14T12:05:14.102Z" },
- { url = "https://files.pythonhosted.org/packages/85/cf/2f812b529079f72f51ff2d6456b7fef06c01735e5cfd62d54ffb2b548028/pyproj-3.7.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d5371ca114d6990b675247355a801925814eca53e6c4b2f1b5c0a956336ee36e", size = 10710030, upload-time = "2025-08-14T12:05:16.317Z" },
- { url = "https://files.pythonhosted.org/packages/99/9b/4626a19e1f03eba4c0e77b91a6cf0f73aa9cb5d51a22ee385c22812bcc2c/pyproj-3.7.2-cp314-cp314-win32.whl", hash = "sha256:77f066626030f41be543274f5ac79f2a511fe89860ecd0914f22131b40a0ec25", size = 5991181, upload-time = "2025-08-14T12:05:19.492Z" },
- { url = "https://files.pythonhosted.org/packages/04/b2/5a6610554306a83a563080c2cf2c57565563eadd280e15388efa00fb5b33/pyproj-3.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:5a964da1696b8522806f4276ab04ccfff8f9eb95133a92a25900697609d40112", size = 6434721, upload-time = "2025-08-14T12:05:21.022Z" },
- { url = "https://files.pythonhosted.org/packages/ae/ce/6c910ea2e1c74ef673c5d48c482564b8a7824a44c4e35cca2e765b68cfcc/pyproj-3.7.2-cp314-cp314-win_arm64.whl", hash = "sha256:e258ab4dbd3cf627809067c0ba8f9884ea76c8e5999d039fb37a1619c6c3e1f6", size = 6363821, upload-time = "2025-08-14T12:05:22.627Z" },
- { url = "https://files.pythonhosted.org/packages/e4/e4/5532f6f7491812ba782a2177fe9de73fd8e2912b59f46a1d056b84b9b8f2/pyproj-3.7.2-cp314-cp314t-macosx_13_0_x86_64.whl", hash = "sha256:bbbac2f930c6d266f70ec75df35ef851d96fdb3701c674f42fd23a9314573b37", size = 6241773, upload-time = "2025-08-14T12:05:24.577Z" },
- { url = "https://files.pythonhosted.org/packages/20/1f/0938c3f2bbbef1789132d1726d9b0e662f10cfc22522743937f421ad664e/pyproj-3.7.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:b7544e0a3d6339dc9151e9c8f3ea62a936ab7cc446a806ec448bbe86aebb979b", size = 4652537, upload-time = "2025-08-14T12:05:26.391Z" },
- { url = "https://files.pythonhosted.org/packages/c7/a8/488b1ed47d25972f33874f91f09ca8f2227902f05f63a2b80dc73e7b1c97/pyproj-3.7.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:f7f5133dca4c703e8acadf6f30bc567d39a42c6af321e7f81975c2518f3ed357", size = 9940864, upload-time = "2025-08-14T12:05:27.985Z" },
- { url = "https://files.pythonhosted.org/packages/c7/cc/7f4c895d0cb98e47b6a85a6d79eaca03eb266129eed2f845125c09cf31ff/pyproj-3.7.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:5aff3343038d7426aa5076f07feb88065f50e0502d1b0d7c22ddfdd2c75a3f81", size = 9688868, upload-time = "2025-08-14T12:05:30.425Z" },
- { url = "https://files.pythonhosted.org/packages/b2/b7/c7e306b8bb0f071d9825b753ee4920f066c40fbfcce9372c4f3cfb2fc4ed/pyproj-3.7.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b0552178c61f2ac1c820d087e8ba6e62b29442debddbb09d51c4bf8acc84d888", size = 11045910, upload-time = "2025-08-14T12:05:32.507Z" },
- { url = "https://files.pythonhosted.org/packages/42/fb/538a4d2df695980e2dde5c04d965fbdd1fe8c20a3194dc4aaa3952a4d1be/pyproj-3.7.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:47d87db2d2c436c5fd0409b34d70bb6cdb875cca2ebe7a9d1c442367b0ab8d59", size = 10895724, upload-time = "2025-08-14T12:05:35.465Z" },
- { url = "https://files.pythonhosted.org/packages/e8/8b/a3f0618b03957de9db5489a04558a8826f43906628bb0b766033aa3b5548/pyproj-3.7.2-cp314-cp314t-win32.whl", hash = "sha256:c9b6f1d8ad3e80a0ee0903a778b6ece7dca1d1d40f6d114ae01bc8ddbad971aa", size = 6056848, upload-time = "2025-08-14T12:05:37.553Z" },
- { url = "https://files.pythonhosted.org/packages/bc/56/413240dd5149dd3291eda55aa55a659da4431244a2fd1319d0ae89407cfb/pyproj-3.7.2-cp314-cp314t-win_amd64.whl", hash = "sha256:1914e29e27933ba6f9822663ee0600f169014a2859f851c054c88cf5ea8a333c", size = 6517676, upload-time = "2025-08-14T12:05:39.126Z" },
- { url = "https://files.pythonhosted.org/packages/15/73/a7141a1a0559bf1a7aa42a11c879ceb19f02f5c6c371c6d57fd86cefd4d1/pyproj-3.7.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d9d25bae416a24397e0d85739f84d323b55f6511e45a522dd7d7eae70d10c7e4", size = 6391844, upload-time = "2025-08-14T12:05:40.745Z" },
-]
-
-[[package]]
-name = "pytest"
-version = "8.4.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
- { name = "iniconfig" },
- { name = "packaging" },
- { name = "pluggy" },
- { name = "pygments" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" },
-]
-
-[[package]]
-name = "requests"
-version = "2.32.3"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "certifi" },
- { name = "charset-normalizer" },
- { name = "idna" },
- { name = "urllib3" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" },
-]
-
-[[package]]
-name = "urllib3"
-version = "2.4.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" },
-]
diff --git a/src/stop_downloader/README.md b/src/stop_downloader/README.md
deleted file mode 100644
index 2cf8151..0000000
--- a/src/stop_downloader/README.md
+++ /dev/null
@@ -1,113 +0,0 @@
-# Bus Stop Overrides and Manual Stops
-
-This directory contains YAML files for overriding properties of existing bus stops and manually adding new stops.
-
-## Overrides Format
-
-Overrides modify or extend properties of existing stops from the transit API.
-
-```yaml
-stopId: # Numeric ID of the stop to override
- name: # Override the name (string)
- alternateNames: # Additional names for the stop (map)
- key: # e.g. name used in metro maps
- location: # Override location coordinates (map)
- latitude: # New latitude value (float)
- longitude: # New longitude value (float)
- hide: # Hide the stop from the map and list (boolean)
- amenities: # List of amenities available at this stop (list)
- - shelter
- - display
- cancelled: # Mark stop as cancelled/out of service (boolean)
- title: # Alert title shown to users (string)
- message: # Alert message shown to users (string)
- alternateCodes: # Alternative stop codes (list of strings)
- - "ALT-123"
-```
-
-## Adding New Stops
-
-New stops that don't exist in the transit API can be added directly in override files using the `new: true` parameter. The `new` parameter is automatically removed after the stop is added to the list.
-
-```yaml
-stopId: # Numeric ID for the new stop (must not conflict with existing stops)
- new: true # Mark this as a new stop (required, will be removed after processing)
- name: # Name of the stop (string)
- location: # Location coordinates (required for new stops)
- latitude: # Latitude coordinate (float)
- longitude: # Longitude coordinate (float)
- lines: # List of lines serving this stop (list of strings)
- - "1"
- - "2"
- amenities: # Optional: List of amenities (list)
- - shelter
- title: # Optional: Alert title (string)
- message: # Optional: Alert message (string)
- cancelled: # Optional: Mark as cancelled (boolean)
- alternateCodes: # Optional: Alternative stop codes (list)
-```
-
-## Field Descriptions
-
-- **stopId** (integer): Unique identifier of the bus stop.
-- **new** (boolean): Set to `true` to add a new stop that doesn't exist in the API. This parameter is removed after processing.
-- **name** (string): Override or set the stop name.
-- **alternateNames** (object): Other names used in different contexts.
- - **key** (string): Name used in a specific context, such as `metro`.
-- **location** (object):
- - **latitude** (float): Override/set latitude coordinate.
- - **longitude** (float): Override/set longitude coordinate.
-- **lines** (array of strings): List of line numbers serving this stop (required for new stops).
-- **hide** (boolean): Set to `true` to exclude the stop from maps and listings.
-- **cancelled** (boolean): Set to `true` to mark the stop as cancelled or out of service.
-- **title** (string): Alert title displayed to users (e.g., "Stop Temporarily Closed").
-- **message** (string): Detailed message about the stop status or alert.
-- **alternateCodes** (array of strings): Alternative stop codes or identifiers.
-- **amenities** (array of strings): Amenities available at this stop, such as `shelter` or `display`.
-
-## Examples
-
-### Override Example
-
-```yaml
-12345:
- name: "Central Station"
- alternateNames:
- metro: "Main Hub"
- location:
- latitude: 40.712776
- longitude: -74.005974
- hide: false
- amenities:
- - shelter
- - display
- title: "Stop Relocated"
- message: "This stop has been temporarily moved 50 meters north."
-```
-
-### New Stop Example
-
-```yaml
-99999:
- new: true
- name: "New Development Stop"
- location:
- latitude: 42.229188
- longitude: -8.722469
- lines:
- - "5"
- - "12"
- amenities:
- - shelter
-```
-
-### Cancelled Stop Example
-
-```yaml
-54321:
- cancelled: true
- title: "Stop Out of Service"
- message: "This stop is temporarily closed for construction. Use stop 54322 (100m south) instead."
- alternateCodes:
- - "54322"
-```
diff --git a/src/stop_downloader/vigo/download-stops.py b/src/stop_downloader/vigo/download-stops.py
deleted file mode 100644
index fa08019..0000000
--- a/src/stop_downloader/vigo/download-stops.py
+++ /dev/null
@@ -1,260 +0,0 @@
-# /// script
-# requires-python = ">=3.12"
-# dependencies = [
-# "PyYAML>=6.0.2", # For YAML support
-# ]
-# ///
-import csv
-import json
-import os
-import sys
-import urllib.request
-import yaml # Add YAML support for overrides
-
-OVERRIDES_DIR = "overrides"
-OUTPUT_FILE = "../../frontend/public/stops/vigo.json"
-
-SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
-
-
-def load_stop_overrides(file_path):
- """Load stop overrides from a YAML file"""
- if not os.path.exists(file_path):
- print(f"Warning: Overrides file {file_path} not found")
- return {}
-
- try:
- with open(file_path, "r", encoding="utf-8") as f:
- overrides = yaml.safe_load(f)
- print(f"Loaded {len(overrides) if overrides else 0} stop overrides")
- return overrides or {}
- except Exception as e:
- print(f"Error loading overrides: {e}", file=sys.stderr)
- return {}
-
-
-def apply_overrides(stops, overrides):
- """Apply overrides to the stop data and add new stops"""
- # Track existing stop IDs
- existing_stop_ids = {stop.get("stopId") for stop in stops}
-
- # Apply overrides to existing stops
- for stop in stops:
- stop_id = stop.get("stopId")
- if stop_id in overrides:
- override = overrides[stop_id]
-
- # Override name if provided
- if "name" in override:
- stop["name"] = override["name"]
-
- # Apply or add alternate names
- if "alternateNames" in override:
- for key, value in override["alternateNames"].items():
- stop["name"][key] = value
-
- # Apply location override
- if "location" in override:
- if "latitude" in override["location"]:
- stop["latitude"] = override["location"]["latitude"]
- if "longitude" in override["location"]:
- stop["longitude"] = override["location"]["longitude"]
-
- # Add amenities
- if "amenities" in override:
- stop["amenities"] = override["amenities"]
-
- # Mark stop as hidden if needed
- if "hide" in override:
- stop["hide"] = override["hide"]
-
- # Mark stop as cancelled
- if "cancelled" in override:
- stop["cancelled"] = override["cancelled"]
-
- if "alert" in override:
- stop["alert"] = override["alert"]
-
- if "title" in override:
- stop["title"] = override["title"]
-
- if "message" in override:
- stop["message"] = override["message"]
-
- # Add new stops (those with "new: true" parameter)
- new_stops_added = 0
- for stop_id, override in overrides.items():
- # Check if this is a new stop
- if override.get("new") and stop_id not in existing_stop_ids:
- # Ensure stop_id is an integer for consistency
- stop_id_int = int(stop_id) if isinstance(stop_id, str) else stop_id
-
- # Create the new stop
- new_stop = {
- "stopId": stop_id_int,
- "name": override.get("name", f"Stop {stop_id_int}"),
- "latitude": override.get("location", {}).get("latitude"),
- "longitude": override.get("location", {}).get("longitude"),
- "lines": override.get("lines", []),
- }
-
- # Add optional fields (excluding the 'new' parameter)
- if "alternateNames" in override:
- for key, value in override["alternateNames"].items():
- new_stop["name"][key] = value
- if "amenities" in override:
- new_stop["amenities"] = override["amenities"]
- if "cancelled" in override:
- new_stop["cancelled"] = override["cancelled"]
- if "title" in override:
- new_stop["title"] = override["title"]
- if "message" in override:
- new_stop["message"] = override["message"]
- if "alternateCodes" in override:
- new_stop["alternateCodes"] = override["alternateCodes"]
-
- stops.append(new_stop)
- new_stops_added += 1
-
- if new_stops_added > 0:
- print(f"Added {new_stops_added} new stops from overrides")
-
- return stops
-
-
-def download_stops_vitrasa() -> list[dict]:
- url = "https://datos.vigo.org/vci_api_app/api2.jsp?tipo=TRANSPORTE_PARADAS"
- req = urllib.request.Request(url)
-
- try:
- with urllib.request.urlopen(req) as response:
- # Read the response and decode from ISO-8859-1 to UTF-8
- content = response.read().decode("iso-8859-1")
- data = json.loads(content)
-
- print(f"Downloaded {len(data)} stops")
-
- # Process the data
- processed_stops = []
- for stop in data:
- name = stop.get("nombre", "").strip()
- # Fix double space equals comma-space: "Castrelos 202" -> "Castrelos, 202"; and remove quotes
- name = name.replace(" ", ", ").replace('"', "").replace("'", "")
-
- processed_stop = {
- "stopId": "vitrasa:" + str(stop.get("id")),
- "name": name,
- "latitude": stop.get("lat"),
- "longitude": stop.get("lon"),
- "lines": [line.strip() for line in stop.get("lineas", "").split(",")]
- if stop.get("lineas")
- else [],
- }
- processed_stops.append(processed_stop)
-
- return processed_stops
- except Exception as e:
- print(f"Error processing vigo stops data: {e}", file=sys.stderr)
- return []
-
-
-def download_stops_renfe() -> list[dict]:
- url = "https://data.renfe.com/dataset/1146f3f1-e06d-477c-8f74-84f8d0668cf9/resource/b22cd560-3a2b-45dd-a25d-2406941f6fcc/download/listado_completo_av_ld_md.csv"
- req = urllib.request.Request(url)
-
- # CÓDIGO;DESCRIPCION;LATITUD;LONGITUD;DIRECCIÓN;C.P.;POBLACION;PROVINCIA;PAIS
-
- try:
- with urllib.request.urlopen(req) as response:
- content = response.read()
- data = csv.DictReader(
- content.decode("utf-8").splitlines(),
- delimiter=";",
- fieldnames=[
- "CODE",
- "NAME",
- "LAT",
- "LNG",
- "ADDRESS",
- "ZIP",
- "CITY",
- "PROVINCE",
- "COUNTRY",
- ],
- )
-
- stops = [row for row in data]
-
- print(f"Downloaded {len(stops)} stops")
-
- # Process the data
- processed_stops = []
- for stop in stops:
- if stop.get("PROVINCE") != "Pontevedra":
- continue
-
- name = stop.get("NAME", "").strip()
-
- processed_stop = {
- "stopId": "renfe:" + str(stop.get("CODE", 0)),
- "name": name,
- "latitude": float(stop.get("LAT", 0).replace(",", ".")),
- "longitude": float(stop.get("LNG", 0).replace(",", ".")),
- "lines": [],
- }
- processed_stops.append(processed_stop)
-
- print(f"Processed {len(processed_stops)} Renfe stops in Pontevedra")
- return processed_stops
- except Exception as e:
- print(f"Error processing Pontevedra stops data: {e}", file=sys.stderr)
- return []
-
-
-def main():
- print("Fetching stop list data...")
-
- vigo_stops = download_stops_vitrasa()
- renfe_stops = download_stops_renfe()
-
- all_stops = vigo_stops + (renfe_stops if renfe_stops else [])
-
- try:
- # Load and apply overrides
- overrides_dir = os.path.join(SCRIPT_DIR, OVERRIDES_DIR)
- # For each YML/YAML file in the overrides directory, load and apply the overrides
- for filename in os.listdir(overrides_dir):
- if not filename.endswith(".yml") and not filename.endswith(".yaml"):
- continue
-
- print(f"Loading overrides from {filename}")
- overrides_file = os.path.join(overrides_dir, filename)
- overrides = load_stop_overrides(overrides_file)
- all_stops = apply_overrides(all_stops, overrides)
-
- # Filter out hidden stops
- visible_stops = [stop for stop in all_stops if not stop.get("hide")]
- print(f"Removed {len(all_stops) - len(visible_stops)} hidden stops")
-
- # Sort stops by ID ascending
- visible_stops.sort(key=lambda x: x["stopId"])
-
- output_file = os.path.join(SCRIPT_DIR, OUTPUT_FILE)
-
- with open(output_file, "w", encoding="utf-8") as f:
- json.dump(visible_stops, f, ensure_ascii=False, indent=2)
-
- print(f"Saved processed stops data to {output_file}")
- return 0
-
- except Exception as e:
- print(f"Error processing stops data: {e}", file=sys.stderr)
- # Print full exception traceback
- import traceback
-
- traceback.print_exc()
- return 1
-
-
-if __name__ == "__main__":
- sys.exit(main())
diff --git a/src/stop_downloader/vigo/overrides/amenities.yaml b/src/stop_downloader/vigo/overrides/amenities.yaml
deleted file mode 100644
index 014b235..0000000
--- a/src/stop_downloader/vigo/overrides/amenities.yaml
+++ /dev/null
@@ -1,23 +0,0 @@
-vitrasa:5520: # García Barbón, 7
- amenities:
- - shelter
- - display
-vitrasa:5530: # García Barbón, 18
- amenities:
- - shelter
- - display
-vitrasa:6620: #Policarpo Sanz, 40
- amenities:
- - shelter
- - display
-vitrasa:14264: # Urzáiz - Príncipe
- amenities:
- - shelter
- - display
-vitrasa:20193: # Policarpo Sanz, 25
- amenities:
- - shelter
- - display
-vitrasa:20198: # Policarpo Sanz, 26
- amenities:
- - shelter
diff --git a/src/stop_downloader/vigo/overrides/example-new-stops.yaml b/src/stop_downloader/vigo/overrides/example-new-stops.yaml
deleted file mode 100644
index 6937471..0000000
--- a/src/stop_downloader/vigo/overrides/example-new-stops.yaml
+++ /dev/null
@@ -1,31 +0,0 @@
-# Example: Adding a new stop using the 'new' parameter
-# New stops are added directly in override files with new: true
-
-# Example 1: New stop with basic information (commented out to avoid affecting production)
-# 99001:
-# new: true
-# name: "New Development Stop"
-# location:
-# latitude: 42.229188
-# longitude: -8.722469
-# lines:
-# - "5"
-# - "12"
-# amenities:
-# - shelter
-
-# Example 2: New stop with alert information
-# 99002:
-# new: true
-# name: "Temporary Event Stop"
-# location:
-# latitude: 42.230000
-# longitude: -8.723000
-# lines:
-# - "EVENT"
-# title: "Special Event Stop"
-# message: "This stop is active during special events only."
-
-# Note: The 'new: true' parameter tells the system to create a new stop.
-# This parameter is automatically removed after the stop is added to the dataset.
-# Choose stop IDs in the 90000+ range to avoid conflicts with existing stops.
diff --git a/src/stop_downloader/vigo/overrides/fix-gregorio-espino.yaml b/src/stop_downloader/vigo/overrides/fix-gregorio-espino.yaml
deleted file mode 100644
index 849eea1..0000000
--- a/src/stop_downloader/vigo/overrides/fix-gregorio-espino.yaml
+++ /dev/null
@@ -1,20 +0,0 @@
-# Fix the position of the stops in Gregorio Espino, which are "opposite" to the actual location of the bus stops.
-vitrasa:5720: # Gregorio Espino, 33
- location:
- latitude: 42.23004933454558
- longitude: -8.706947409683313
-
-vitrasa:5710: # Gregorio Espino, 22
- location:
- latitude: 42.23003666347398
- longitude: -8.707266671978003
-
-vitrasa:5730: # Gregorio Espino, 44
- location:
- latitude: 42.227850036119314
- longitude: -8.708105429626789
-
-vitrasa:5740: # Gregorio Espino, 57
- location:
- latitude: 42.22783722597372
- longitude: -8.707849091551859
diff --git a/src/stop_downloader/vigo/overrides/hide-virtual-stops.yaml b/src/stop_downloader/vigo/overrides/hide-virtual-stops.yaml
deleted file mode 100644
index 5978a35..0000000
--- a/src/stop_downloader/vigo/overrides/hide-virtual-stops.yaml
+++ /dev/null
@@ -1,17 +0,0 @@
-# The Vitrasa network has several virtual stops created for internal purposes, like
-# end of certain lines with a "nice" name.
-
-vitrasa:20223: # Castrelos (Pavillón) - Final U1
- hide: true
-vitrasa:20146: # García Barbón 7 - final líneas A y 18A
- hide: true
-vitrasa:20220: # (Samil) COIA-SAMIL - Final L15A
- hide: true
-vitrasa:20001: # (Samil) Samil por Beiramar - Final L15B
- hide: true
-vitrasa:20002: # (Samil) Samil por Torrecedeira - Final L15C
- hide: true
-vitrasa:20144: # (Samil) Samil por Coia - Final C3D+C3i
- hide: true
-vitrasa:20145: # (Samil) Samil por Bouzas - Final C3D+C3i
- hide: true
diff --git a/src/stop_downloader/vigo/overrides/improve-coordinates-misc.yaml b/src/stop_downloader/vigo/overrides/improve-coordinates-misc.yaml
deleted file mode 100644
index a96c84b..0000000
--- a/src/stop_downloader/vigo/overrides/improve-coordinates-misc.yaml
+++ /dev/null
@@ -1,40 +0,0 @@
-# Improves coordinates for some locations in the dataset to be more accurate, and avoid clustering
-vitrasa:6620: # Policarpo Sanz, 40
- location:
- latitude: 42.23757846151978
- longitude: -8.721031378896738
-
-vitrasa:20193: # Policarpo Sanz, 25
- location:
- latitude: 42.23767601188501
- longitude: -8.721582630122455
-
-vitrasa:3130: #Avda. de Cesáreo Vázquez 169
- location:
- latitude: 42.191024803868736
- longitude: -8.799397387002196
-
-vitrasa:3090: # Avda. de Cesáreo Vázquez 182
- location:
- latitude: 42.191019711713736
- longitude: -8.799628565094565
-
-vitrasa:14294: # Avda. de Ricardo Mella 406
- location:
- latitude: 42.190684424876565
- longitude: -8.799308812770041
-
-vitrasa:3120: # Cesáreo Vázquez 141
- location:
- latitude: 42.187488521491225
- longitude: -8.801226626055183
-
-vitrasa:3080: # Cesáreo Vázquez 136
- location:
- latitude: 42.1873653089623
- longitude: -8.800886236766305
-
-renfe:22308: # Vigo Guixar
- location:
- latitude: 42.2394426820947
- longitude: -8.712087821668435