Compare commits

...

3 Commits

66 changed files with 2178 additions and 1537 deletions

1
.gitignore vendored
View File

@ -35,6 +35,7 @@ coverage
secrets.py secrets.py
settings_local.py settings_local.py
*.log *.log
*.txt
media/ media/
output/ output/
/static/ /static/

View File

@ -6,7 +6,7 @@ import sys
def main(): def main():
"""Run administrative tasks.""" """Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sncf.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "trainvel.settings")
try: try:
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
except ImportError as exc: except ImportError as exc:

View File

@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -1,566 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: 1.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-02-10 19:57+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Emmy D'Anello <ynerant@emy.lu>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: sncfgtfs/models.py:6
msgid "TGV"
msgstr "TGV"
#: sncfgtfs/models.py:7
msgid "TER"
msgstr "TER"
#: sncfgtfs/models.py:8
msgid "Intercités"
msgstr "Intercités"
#: sncfgtfs/models.py:9
msgid "Transilien"
msgstr "Transilien"
#: sncfgtfs/models.py:10
msgid "Eurostar"
msgstr "Eurostar"
#: sncfgtfs/models.py:11
msgid "Trenitalia"
msgstr "Trenitalia"
#: sncfgtfs/models.py:12
msgid "Renfe"
msgstr "Renfe"
#: sncfgtfs/models.py:13
msgid "ÖBB"
msgstr "ÖBB"
#: sncfgtfs/models.py:17
msgid "Stop/platform"
msgstr "Arrêt / quai"
#: sncfgtfs/models.py:18
msgid "Station"
msgstr "Gare"
#: sncfgtfs/models.py:19
msgid "Entrance/exit"
msgstr "Entrée / sortie"
#: sncfgtfs/models.py:20
msgid "Generic node"
msgstr "Nœud générique"
#: sncfgtfs/models.py:21
msgid "Boarding area"
msgstr "Zone d'embarquement"
#: sncfgtfs/models.py:25
msgid "No information"
msgstr "Pas d'information"
#: sncfgtfs/models.py:26
msgid "Possible"
msgstr "Possible"
#: sncfgtfs/models.py:27 sncfgtfs/models.py:57
msgid "Not possible"
msgstr "Impossible"
#: sncfgtfs/models.py:31
msgid "Regular"
msgstr "Régulier"
#: sncfgtfs/models.py:32
msgid "None"
msgstr "Aucun"
#: sncfgtfs/models.py:33
msgid "Must phone agency"
msgstr "Doit téléphoner à l'agence"
#: sncfgtfs/models.py:34
msgid "Must coordinate with driver"
msgstr "Doit se coordonner avec læ conducteurice"
#: sncfgtfs/models.py:38
msgid "Tram"
msgstr "Tram"
#: sncfgtfs/models.py:39
msgid "Metro"
msgstr "Métro"
#: sncfgtfs/models.py:40
msgid "Rail"
msgstr "Rail"
#: sncfgtfs/models.py:41
msgid "Bus"
msgstr "Bus"
#: sncfgtfs/models.py:42
msgid "Ferry"
msgstr "Ferry"
#: sncfgtfs/models.py:43
msgid "Cable car"
msgstr "Câble"
#: sncfgtfs/models.py:44
msgid "Gondola"
msgstr "Gondole"
#: sncfgtfs/models.py:45
msgid "Funicular"
msgstr "Funiculaire"
#: sncfgtfs/models.py:49
msgid "Outbound"
msgstr "Vers l'extérieur"
#: sncfgtfs/models.py:50
msgid "Inbound"
msgstr "Vers l'intérieur"
#: sncfgtfs/models.py:54
msgid "Recommended"
msgstr "Recommandé"
#: sncfgtfs/models.py:55
msgid "Timed"
msgstr "Correspondance programmée"
#: sncfgtfs/models.py:56
msgid "Minimum time"
msgstr "Temps de correspondance minimum requis"
#: sncfgtfs/models.py:61 sncfgtfs/models.py:67
msgid "Added"
msgstr "Ajouté"
#: sncfgtfs/models.py:62
msgid "Removed"
msgstr "Supprimé"
#: sncfgtfs/models.py:66 sncfgtfs/models.py:76
msgid "Scheduled"
msgstr "Planifié"
#: sncfgtfs/models.py:68 sncfgtfs/models.py:79
msgid "Unscheduled"
msgstr "Non planifié"
#: sncfgtfs/models.py:69
msgid "Canceled"
msgstr "Annulé"
#: sncfgtfs/models.py:70
msgid "Replacement"
msgstr "Remplacé"
#: sncfgtfs/models.py:71
msgid "Duplicated"
msgstr "Dupliqué"
#: sncfgtfs/models.py:72
msgid "Deleted"
msgstr "Supprimé"
#: sncfgtfs/models.py:77
msgid "Skipped"
msgstr "Sauté"
#: sncfgtfs/models.py:78
msgid "No data"
msgstr "Pas de données"
#: sncfgtfs/models.py:86
msgid "Agency ID"
msgstr "ID de l'agence"
#: sncfgtfs/models.py:91
msgid "Agency name"
msgstr "Nom de l'agence"
#: sncfgtfs/models.py:95
msgid "Agency URL"
msgstr "URL de l'agence"
#: sncfgtfs/models.py:100
msgid "Agency timezone"
msgstr "Fuseau horaire de l'agence"
#: sncfgtfs/models.py:105
msgid "Agency language"
msgstr "Langue de l'agence"
#: sncfgtfs/models.py:111
msgid "Agency phone"
msgstr "Téléphone de l'agence"
#: sncfgtfs/models.py:116
msgid "Agency email"
msgstr "Adresse email de l'agence"
#: sncfgtfs/models.py:124 sncfgtfs/models.py:242
msgid "Agency"
msgstr "Agence"
#: sncfgtfs/models.py:125
msgid "Agencies"
msgstr "Agences"
#: sncfgtfs/models.py:133 sncfgtfs/models.py:459
msgid "Stop ID"
msgstr "ID de l'arrêt"
#: sncfgtfs/models.py:138
msgid "Stop code"
msgstr "Code de l'arrêt"
#: sncfgtfs/models.py:144
msgid "Stop name"
msgstr "Nom de l'arrêt"
#: sncfgtfs/models.py:149
msgid "Stop description"
msgstr "Description de l'arrêt"
#: sncfgtfs/models.py:154
msgid "Stop longitude"
msgstr "Longitude de l'arrêt"
#: sncfgtfs/models.py:158
msgid "Stop latitude"
msgstr "Latitude de l'arrêt"
#: sncfgtfs/models.py:163
msgid "Zone ID"
msgstr "ID de la zone"
#: sncfgtfs/models.py:167
msgid "Stop URL"
msgstr "URL de l'arrêt"
#: sncfgtfs/models.py:172
msgid "Location type"
msgstr "Type de localisation"
#: sncfgtfs/models.py:181
msgid "Parent station"
msgstr "Gare parente"
#: sncfgtfs/models.py:189
msgid "Stop timezone"
msgstr "Fuseau horaire de l'arrêt"
#: sncfgtfs/models.py:195
msgid "Level ID"
msgstr "ID du niveau"
#: sncfgtfs/models.py:200
msgid "Wheelchair boarding"
msgstr "Embarquement en fauteuil roulant"
#: sncfgtfs/models.py:208
msgid "Platform code"
msgstr "Code du quai"
#: sncfgtfs/models.py:214 sncfgtfs/models.py:286 sncfgtfs/models.py:560
msgid "Transport type"
msgstr "Type de transport"
#: sncfgtfs/models.py:227
msgid "Stop"
msgstr "Arrêt"
#: sncfgtfs/models.py:228
msgid "Stops"
msgstr "Arrêts"
#: sncfgtfs/models.py:236 sncfgtfs/models.py:438 sncfgtfs/models.py:577
#: sncfgtfs/models.py:609
msgid "ID"
msgstr "Identifiant"
#: sncfgtfs/models.py:248
msgid "Route short name"
msgstr "Nom court de la ligne"
#: sncfgtfs/models.py:253
msgid "Route long name"
msgstr "Nom long de la ligne"
#: sncfgtfs/models.py:258
msgid "Route description"
msgstr "Description de la ligne"
#: sncfgtfs/models.py:263
msgid "Route type"
msgstr "Type de ligne"
#: sncfgtfs/models.py:268
msgid "Route URL"
msgstr "URL de la ligne"
#: sncfgtfs/models.py:274
msgid "Route color"
msgstr "Couleur de la ligne"
#: sncfgtfs/models.py:280
msgid "Route text color"
msgstr "Couleur du texte de la ligne"
#: sncfgtfs/models.py:294 sncfgtfs/models.py:309
msgid "Route"
msgstr "Ligne"
#: sncfgtfs/models.py:295
msgid "Routes"
msgstr "Lignes"
#: sncfgtfs/models.py:303
msgid "Trip ID"
msgstr "ID du trajet"
#: sncfgtfs/models.py:316 sncfgtfs/models.py:583
msgid "Service"
msgstr "Service"
#: sncfgtfs/models.py:322
msgid "Trip headsign"
msgstr "Destination du trajet"
#: sncfgtfs/models.py:328
msgid "Trip short name"
msgstr "Nom court du trajet"
#: sncfgtfs/models.py:333
msgid "Direction"
msgstr "Direction"
#: sncfgtfs/models.py:340
msgid "Block ID"
msgstr "ID du bloc"
#: sncfgtfs/models.py:346
msgid "Shape ID"
msgstr "ID de la forme"
#: sncfgtfs/models.py:351
msgid "Wheelchair accessible"
msgstr "Accessible en fauteuil roulant"
#: sncfgtfs/models.py:358
msgid "Bikes allowed"
msgstr "Vélos autorisés"
#: sncfgtfs/models.py:365
msgid "Last update"
msgstr "Dernière mise à jour"
#: sncfgtfs/models.py:430 sncfgtfs/models.py:444 sncfgtfs/models.py:681
msgid "Trip"
msgstr "Trajet"
#: sncfgtfs/models.py:431
msgid "Trips"
msgstr "Trajets"
#: sncfgtfs/models.py:449 sncfgtfs/models.py:731
msgid "Arrival time"
msgstr "Heure d'arrivée"
#: sncfgtfs/models.py:453 sncfgtfs/models.py:739
msgid "Departure time"
msgstr "Heure de départ"
#: sncfgtfs/models.py:464
msgid "Stop sequence"
msgstr "Séquence de l'arrêt"
#: sncfgtfs/models.py:469
msgid "Stop headsign"
msgstr "Destination de l'arrêt"
#: sncfgtfs/models.py:474
msgid "Pickup type"
msgstr "Type de prise en charge"
#: sncfgtfs/models.py:481
msgid "Drop off type"
msgstr "Type de dépose"
#: sncfgtfs/models.py:488
msgid "Timepoint"
msgstr "Ponctualité"
#: sncfgtfs/models.py:511 sncfgtfs/models.py:721
msgid "Stop time"
msgstr "Heure d'arrêt"
#: sncfgtfs/models.py:512
msgid "Stop times"
msgstr "Heures d'arrêt"
#: sncfgtfs/models.py:519
msgid "Service ID"
msgstr "ID du service"
#: sncfgtfs/models.py:523
msgid "Monday"
msgstr "Lundi"
#: sncfgtfs/models.py:527
msgid "Tuesday"
msgstr "Mardi"
#: sncfgtfs/models.py:531
msgid "Wednesday"
msgstr "Mercredi"
#: sncfgtfs/models.py:535
msgid "Thursday"
msgstr "Jeudi"
#: sncfgtfs/models.py:539
msgid "Friday"
msgstr "Vendredi"
#: sncfgtfs/models.py:543
msgid "Saturday"
msgstr "Samedi"
#: sncfgtfs/models.py:547
msgid "Sunday"
msgstr "Dimanche"
#: sncfgtfs/models.py:551 sncfgtfs/models.py:687
msgid "Start date"
msgstr "Date de début"
#: sncfgtfs/models.py:555
msgid "End date"
msgstr "Date de fin"
#: sncfgtfs/models.py:568
msgid "Calendar"
msgstr "Calendrier"
#: sncfgtfs/models.py:569
msgid "Calendars"
msgstr "Calendriers"
#: sncfgtfs/models.py:588
msgid "Date"
msgstr "Date"
#: sncfgtfs/models.py:592
msgid "Exception type"
msgstr "Type d'exception"
#: sncfgtfs/models.py:600
msgid "Calendar date"
msgstr "Date du calendrier"
#: sncfgtfs/models.py:601
msgid "Calendar dates"
msgstr "Dates du calendrier"
#: sncfgtfs/models.py:615
msgid "From stop"
msgstr "Depuis l'arrêt"
#: sncfgtfs/models.py:622
msgid "To stop"
msgstr "Jusqu'à l'arrêt"
#: sncfgtfs/models.py:627
msgid "Transfer type"
msgstr "Type de correspondance"
#: sncfgtfs/models.py:633
msgid "Minimum transfer time"
msgstr "Temps de correspondance minimum"
#: sncfgtfs/models.py:638
msgid "Transfer"
msgstr "Correspondance"
#: sncfgtfs/models.py:639
msgid "Transfers"
msgstr "Correspondances"
#: sncfgtfs/models.py:646
msgid "Feed publisher name"
msgstr "Nom de l'éditeur du flux"
#: sncfgtfs/models.py:650
msgid "Feed publisher URL"
msgstr "URL de l'éditeur du flux"
#: sncfgtfs/models.py:655
msgid "Feed language"
msgstr "Langue du flux"
#: sncfgtfs/models.py:659
msgid "Feed start date"
msgstr "Date de début du flux"
#: sncfgtfs/models.py:663
msgid "Feed end date"
msgstr "Date de fin du flux"
#: sncfgtfs/models.py:668
msgid "Feed version"
msgstr "Version du flux"
#: sncfgtfs/models.py:672
msgid "Feed info"
msgstr "Information du flux"
#: sncfgtfs/models.py:673
msgid "Feed infos"
msgstr "Informations du flux"
#: sncfgtfs/models.py:691
msgid "Start time"
msgstr "Heure de début"
#: sncfgtfs/models.py:695 sncfgtfs/models.py:743
msgid "Schedule relationship"
msgstr "Relation de la planification"
#: sncfgtfs/models.py:704 sncfgtfs/models.py:714
msgid "Trip update"
msgstr "Mise à jour du trajet"
#: sncfgtfs/models.py:705
msgid "Trip updates"
msgstr "Mises à jour des trajets"
#: sncfgtfs/models.py:727
msgid "Arrival delay"
msgstr "Retard à l'arrivée"
#: sncfgtfs/models.py:735
msgid "Departure delay"
msgstr "Retard au départ"
#: sncfgtfs/models.py:752
msgid "Stop time update"
msgstr "Mise à jour du temps d'arrêt"
#: sncfgtfs/models.py:753
msgid "Stop time updates"
msgstr "Mises à jour des temps d'arrêt"

View File

@ -1,272 +0,0 @@
from datetime import timedelta, datetime, date, time
from zoneinfo import ZoneInfo
import requests
from django.core.management import BaseCommand
from django.db.models import Q
from sncfgtfs.gtfs_realtime_pb2 import FeedMessage
from sncfgtfs.models import Agency, Calendar, CalendarDate, ExceptionType, LocationType, PickupType, \
Route, RouteType, Stop, StopScheduleRelationship, StopTime, StopTimeUpdate, \
Trip, TripUpdate, TripScheduleRelationship
class Command(BaseCommand):
help = "Update the SNCF GTFS Realtime database."
GTFS_RT_FEEDS = {
"TGV": "https://proxy.transport.data.gouv.fr/resource/sncf-tgv-gtfs-rt-trip-updates",
"IC": "https://proxy.transport.data.gouv.fr/resource/sncf-ic-gtfs-rt-trip-updates",
"TER": "https://proxy.transport.data.gouv.fr/resource/sncf-ter-gtfs-rt-trip-updates",
"TI": "https://thello.axelor.com/public/gtfs/GTFS-RT.bin",
}
def add_arguments(self, parser):
pass
def handle(self, *args, **options):
for feed_type, feed_url in self.GTFS_RT_FEEDS.items():
self.stdout.write(f"Updating {feed_type} feed...")
feed_message = FeedMessage()
feed_message.ParseFromString(requests.get(feed_url).content)
stop_times_updates = []
for entity in feed_message.entity:
if entity.HasField("trip_update"):
trip_update = entity.trip_update
trip_id = trip_update.trip.trip_id
if feed_type in ["TGV", "IC", "TER"]:
trip_id = trip_id.split(":", 1)[0]
start_date = date(year=int(trip_update.trip.start_date[:4]),
month=int(trip_update.trip.start_date[4:6]),
day=int(trip_update.trip.start_date[6:]))
start_dt = datetime.combine(start_date, time(0), tzinfo=ZoneInfo("Europe/Paris"))
if trip_update.trip.schedule_relationship == TripScheduleRelationship.ADDED:
# C'est un trajet nouveau. On crée le trajet associé.
self.create_trip(trip_update, trip_id, start_dt, feed_type)
if not Trip.objects.filter(id=trip_id).exists():
self.stdout.write(f"Trip {trip_id} does not exist in the GTFS feed.")
continue
# Création du TripUpdate
tu, _created = TripUpdate.objects.update_or_create(
trip_id=trip_id,
start_date=trip_update.trip.start_date,
start_time=trip_update.trip.start_time,
defaults=dict(
schedule_relationship=trip_update.trip.schedule_relationship,
)
)
for stop_sequence, stop_time_update in enumerate(trip_update.stop_time_update):
stop_id = stop_time_update.stop_id
if stop_id.startswith('StopArea:'):
# On est dans le cadre d'une gare. On cherche le quai associé.
if StopTime.objects.filter(trip_id=trip_id, stop__parent_station_id=stop_id).exists():
# U
stop = StopTime.objects.get(trip_id=trip_id, stop__parent_station_id=stop_id).stop
else:
stops = [s for s in Stop.objects.filter(parent_station_id=stop_id).all()
for s2 in StopTime.objects.filter(trip_id=trip_id).all()
if s.stop_type in s2.stop.stop_type
or s2.stop.stop_type in s.stop_type]
stop = stops[0] if stops else Stop.objects.get(id=stop_id)
st, _created = StopTime.objects.update_or_create(
id=f"{trip_id}-{stop.id}",
trip_id=trip_id,
stop_id=stop.id,
defaults={
"stop_sequence": stop_sequence,
"arrival_time": datetime.fromtimestamp(stop_time_update.arrival.time,
tz=ZoneInfo("Europe/Paris")) - start_dt,
"departure_time": datetime.fromtimestamp(stop_time_update.departure.time,
tz=ZoneInfo("Europe/Paris")) - start_dt,
"pickup_type": (PickupType.REGULAR if stop_time_update.departure.time
else PickupType.NONE),
"drop_off_type": (PickupType.REGULAR if stop_time_update.arrival.time
else PickupType.NONE),
}
)
elif stop_time_update.schedule_relationship == StopScheduleRelationship.SKIPPED:
st = StopTime.objects.get(Q(stop=stop_id) | Q(stop__parent_station_id=stop_id),
trip_id=trip_id)
if st.pickup_type != PickupType.NONE or st.drop_off_type != PickupType.NONE:
st.pickup_type = PickupType.NONE
st.drop_off_type = PickupType.NONE
st.save()
else:
qs = StopTime.objects.filter(Q(stop=stop_id) | Q(stop__parent_station_id=stop_id),
trip_id=trip_id)
if qs.count() == 1:
st = qs.first()
else:
st = qs.get(stop_sequence=stop_sequence)
if st.stop_sequence != stop_sequence:
st.stop_sequence = stop_sequence
st.save()
st_update = StopTimeUpdate(
trip_update=tu,
stop_time=st,
arrival_delay=timedelta(seconds=stop_time_update.arrival.delay),
arrival_time=datetime.fromtimestamp(stop_time_update.arrival.time,
tz=ZoneInfo("Europe/Paris")),
departure_delay=timedelta(seconds=stop_time_update.departure.delay),
departure_time=datetime.fromtimestamp(stop_time_update.departure.time,
tz=ZoneInfo("Europe/Paris")),
schedule_relationship=stop_time_update.schedule_relationship
or StopScheduleRelationship.SCHEDULED,
)
stop_times_updates.append(st_update)
else:
self.stdout.write(str(entity))
StopTimeUpdate.objects.bulk_create(stop_times_updates,
update_conflicts=True,
update_fields=['arrival_delay', 'arrival_time',
'departure_delay', 'departure_time'],
unique_fields=['trip_update', 'stop_time'])
def create_trip(self, trip_update, trip_id, start_dt, feed_type):
headsign = trip_id[5:-1]
trip_qs = Trip.objects.all()
trip_ids = trip_qs.values_list('id', flat=True)
first_stop_queryset = StopTime.objects.filter(
stop__parent_station_id=trip_update.stop_time_update[0].stop_id,
).values('trip_id')
last_stop_queryset = StopTime.objects.filter(
stop__parent_station_id=trip_update.stop_time_update[-1].stop_id,
).values('trip_id')
trip_ids = trip_ids.intersection(first_stop_queryset).intersection(last_stop_queryset)
# print(trip_id, trip_ids)
for stop_sequence, stop_time_update in enumerate(trip_update.stop_time_update):
stop_id = stop_time_update.stop_id
st_queryset = StopTime.objects.filter(stop__parent_station_id=stop_id)
if stop_sequence == 0:
st_queryset = st_queryset.filter(stop_sequence=0)
# print(stop_sequence, Stop.objects.get(id=stop_id).name, stop_time_update)
# print(trip_ids)
# print(st_queryset.values('trip_id').all())
trip_ids_restrict = trip_ids.intersection(st_queryset.values('trip_id'))
if trip_ids_restrict:
trip_ids = trip_ids_restrict
else:
stop = Stop.objects.get(id=stop_id)
self.stdout.write(self.style.WARNING(f"Warning: No trip is found passing by stop "
f"{stop.name} ({stop_id})"))
trip_ids = set(trip_ids)
route_ids = set(Trip.objects.filter(id__in=trip_ids).values_list('route_id', flat=True))
self.stdout.write(f"{len(route_ids)} routes found on trip for new train {headsign}")
if not route_ids:
origin_id = trip_update.stop_time_update[0].stop_id
origin = Stop.objects.get(id=origin_id)
destination_id = trip_update.stop_time_update[-1].stop_id
destination = Stop.objects.get(id=destination_id)
trip_name = f"{origin.name} - {destination.name}"
trip_reverse_name = f"{destination.name} - {origin.name}"
route_qs = Route.objects.filter(long_name=trip_name, transport_type=feed_type)
route_reverse_qs = Route.objects.filter(long_name=trip_reverse_name,
transport_type=feed_type)
if route_qs.exists():
route_ids = set(route_qs.values_list('id', flat=True))
elif route_reverse_qs.exists():
route_ids = set(route_reverse_qs.values_list('id', flat=True))
else:
self.stdout.write(f"Route not found for trip {trip_id} ({trip_name}). Creating new one")
route = Route.objects.create(
id=f"CREATED-{trip_name}",
agency=Agency.objects.filter(routes__transport_type=feed_type).first(),
transport_type=feed_type,
type=RouteType.RAIL,
short_name=trip_name,
long_name=trip_name,
)
route_ids = {route.id}
self.stdout.write(f"Route {route.id} created for trip {trip_id} ({trip_name})")
elif len(route_ids) > 1:
self.stdout.write(f"Multiple routes found for trip {trip_id}.")
self.stdout.write(", ".join(route_ids))
route_id = route_ids.pop()
Calendar.objects.update_or_create(
id=f"{feed_type}-new-{headsign}",
defaults={
"transport_type": feed_type,
"monday": False,
"tuesday": False,
"wednesday": False,
"thursday": False,
"friday": False,
"saturday": False,
"sunday": False,
"start_date": start_dt.date(),
"end_date": start_dt.date(),
}
)
CalendarDate.objects.update_or_create(
id=f"{feed_type}-{headsign}-{trip_update.trip.start_date}",
defaults={
"service_id": f"{feed_type}-new-{headsign}",
"date": trip_update.trip.start_date,
"exception_type": ExceptionType.ADDED,
}
)
Trip.objects.update_or_create(
id=trip_id,
defaults={
"route_id": route_id,
"service_id": f"{feed_type}-new-{headsign}",
"headsign": headsign,
"direction_id": trip_update.trip.direction_id,
}
)
sample_trip = Trip.objects.filter(id__in=trip_ids, route_id=route_id)
sample_trip = sample_trip.first() if sample_trip.exists() else None
for stop_sequence, stop_time_update in enumerate(trip_update.stop_time_update):
stop_id = stop_time_update.stop_id
stop = Stop.objects.get(id=stop_id)
if stop.location_type == LocationType.STATION:
if not StopTime.objects.filter(trip_id=trip_id).exists():
if sample_trip:
stop = StopTime.objects.get(trip_id=sample_trip.id,
stop__parent_station_id=stop_id).stop
elif StopTime.objects.filter(trip_id=trip_id, stop__parent_station_id=stop_id).exists():
stop = StopTime.objects.get(trip_id=trip_id, stop__parent_station_id=stop_id).stop
else:
stops = [s for s in Stop.objects.filter(parent_station_id=stop_id).all()
for s2 in StopTime.objects.filter(trip_id=trip_id).all()
if s.stop_type in s2.stop.stop_type
or s2.stop.stop_type in s.stop_type]
stop = stops[0] if stops else stop
stop_id = stop.id
arr_time = datetime.fromtimestamp(stop_time_update.arrival.time,
tz=ZoneInfo("Europe/Paris")) - start_dt
dep_time = datetime.fromtimestamp(stop_time_update.departure.time,
tz=ZoneInfo("Europe/Paris")) - start_dt
pickup_type = PickupType.REGULAR if stop_time_update.departure.time and stop_sequence > 0 \
else PickupType.NONE
drop_off_type = PickupType.REGULAR if stop_time_update.arrival.time \
and stop_sequence < len(trip_update.stop_time_update) - 1 else PickupType.NONE
StopTime.objects.update_or_create(
id=f"{trip_id}-{stop_id}",
trip_id=trip_id,
defaults={
"stop_id": stop_id,
"stop_sequence": stop_sequence,
"arrival_time": arr_time,
"departure_time": dep_time,
"pickup_type": pickup_type,
"drop_off_type": drop_off_type,
}
)

View File

@ -1,6 +0,0 @@
Django>=5.0,<6.0
django-cors-headers
django-filter
djangorestframework
protobuf
requests

View File

@ -1,11 +1,11 @@
{ {
"name": "sncf-station", "name": "trainvel-front",
"version": "0.1.0", "version": "0.1.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "sncf-station", "name": "trainvel-front",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@emotion/react": "^11.11.3", "@emotion/react": "^11.11.3",

View File

@ -1,5 +1,5 @@
{ {
"name": "sncf-station", "name": "trainvel-front",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -7,7 +7,7 @@
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta
name="description" name="description"
content="Écrans en gare affichant les horaires des trains des gares SNCF." content="Écrans en gare affichant les horaires des trains des gares."
/> />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@ -18,7 +18,7 @@ function App() {
element: <Home />, element: <Home />,
}, },
{ {
path: "/station/:stopId", path: "/station/:theme/:stopId",
element: <Station /> element: <Station />
} }
]) ])

View File

@ -40,23 +40,23 @@ function AutocompleteStop(params) {
} }
function getOptionGroup(option) { function getOptionGroup(option) {
switch (option.transport_type) { switch (option.gtfs_feed) {
case "TGV": case "FR-SNCF-TGV":
case "IC": case "FR-SNCF-IC":
case "TER": case "FR-SNCF-TER":
return "TGV/TER/Intercités" return "TGV/TER/Intercités"
case "TN": case "FR-IDF-TN":
return "Transilien" return "Transilien"
case "ES": case "FR-EUROSTAR":
return "Eurostar" return "Eurostar"
case "TI": case "IT-FRA-TI":
return "Trenitalia France" return "Trenitalia France"
case "RENFE": case "ES-RENFE":
return "RENFE" return "RENFE"
case "OBB": case "AT-OBB":
return "ÖBB" return "ÖBB"
default: default:
return option.transport_type return option.gtfs_feed
} }
} }

View File

@ -5,11 +5,11 @@ function Home() {
const navigate = useNavigate() const navigate = useNavigate()
function onStationSelected(event, stop) { function onStationSelected(event, stop) {
navigate(`/station/${stop.id}/`) navigate(`/station/sncf/${stop.id}/`)
} }
return <> return <>
<h1>Horaires SNCF</h1> <h1>Horaires des trains</h1>
<h2> <h2>
Choisissez une gare dont vous désirez connaître le tableau des prochains départs et arrivées : Choisissez une gare dont vous désirez connaître le tableau des prochains départs et arrivées :
</h2> </h2>

View File

@ -12,7 +12,7 @@ function DateTimeSelector({stop, date, time}) {
function onStationSelected(event, stop) { function onStationSelected(event, stop) {
if (stop !== null) if (stop !== null)
navigate(`/station/${stop.id}/`) navigate(`/station/sncf/${stop.id}/`)
} }
return <> return <>
@ -32,7 +32,7 @@ function DateTimeSelector({stop, date, time}) {
} }
function Station() { function Station() {
let {stopId} = useParams() let {stopId, theme} = useParams()
let [searchParams, _setSearchParams] = useSearchParams() let [searchParams, _setSearchParams] = useSearchParams()
const now = new Date() const now = new Date()
let dateNow = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}` let dateNow = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`

View File

@ -229,10 +229,10 @@ function TrainRow({train, tableType, date, time}) {
} }
function getTrainType(train, trip, route) { function getTrainType(train, trip, route) {
switch (route.transport_type) { switch (route.gtfs_feed) {
case "TGV": case "FR-SNCF-TGV":
case "TER": case "FR-SNCF-IC":
case "IC": case "FR-SNCF-TER":
let trainType = train.stop.split("StopPoint:OCE")[1].split("-")[0] let trainType = train.stop.split("StopPoint:OCE")[1].split("-")[0]
switch (trainType) { switch (trainType) {
case "Train TER": case "Train TER":
@ -244,20 +244,20 @@ function getTrainType(train, trip, route) {
default: default:
return trainType return trainType
} }
case "TN": case "FR-IDF-TN":
return route.short_name return route.short_name
case "ES": case "FR-EUROSTAR":
return "Eurostar" return "Eurostar"
case "TI": case "IT-FRA-TI":
return "Trenitalia" return "Trenitalia France"
case "RENFE": case "ES-RENFE":
return "RENFE" return "RENFE"
case "OBB": case "AT-OBB":
if (trip.short_name.startsWith("NJ")) if (trip.short_name?.startsWith("NJ"))
return "NJ" return "NJ"
return "ÖBB" return "ÖBB"
default: default:
return "" return trip.short_name?.split(" ")[0]
} }
} }
@ -280,6 +280,7 @@ function getTrainTypeDisplay(trainType) {
case "Eurostar": case "Eurostar":
return <img src="/eurostar_mini.svg" alt="Eurostar" width="80%" /> return <img src="/eurostar_mini.svg" alt="Eurostar" width="80%" />
case "Trenitalia": case "Trenitalia":
case "Trenitalia France":
return <img src="/trenitalia.svg" alt="Frecciarossa" width="80%" /> return <img src="/trenitalia.svg" alt="Frecciarossa" width="80%" />
case "RENFE": case "RENFE":
return <img src="/renfe.svg" alt="RENFE" width="80%" /> return <img src="/renfe.svg" alt="RENFE" width="80%" />

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -3,4 +3,4 @@ from django.apps import AppConfig
class ApiConfig(AppConfig): class ApiConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField" default_auto_field = "django.db.models.BigAutoField"
name = "sncf.api" name = "trainvel.api"

View File

@ -1,6 +1,6 @@
from rest_framework import serializers from rest_framework import serializers
from sncfgtfs.models import Agency, Stop, Route, Trip, StopTime, Calendar, CalendarDate, \ from trainvel.gtfs.models import Agency, Stop, Route, Trip, StopTime, Calendar, CalendarDate, \
Transfer, FeedInfo, TripUpdate, StopTimeUpdate Transfer, FeedInfo, TripUpdate, StopTimeUpdate

View File

@ -8,15 +8,14 @@ from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.filters import OrderingFilter, SearchFilter
from sncf.api.serializers import AgencySerializer, StopSerializer, RouteSerializer, TripSerializer, \ from trainvel.api.serializers import AgencySerializer, StopSerializer, RouteSerializer, TripSerializer, \
StopTimeSerializer, CalendarSerializer, CalendarDateSerializer, TransferSerializer, \ StopTimeSerializer, CalendarSerializer, CalendarDateSerializer, TransferSerializer, \
FeedInfoSerializer, TripUpdateSerializer, StopTimeUpdateSerializer FeedInfoSerializer, TripUpdateSerializer, StopTimeUpdateSerializer
from sncfgtfs.models import Agency, Stop, Route, Trip, StopTime, Calendar, CalendarDate, \ from trainvel.gtfs.models import Agency, Calendar, CalendarDate, FeedInfo, GTFSFeed, Route, Stop, StopTime, StopTimeUpdate, \
Transfer, FeedInfo, TripUpdate, StopTimeUpdate Transfer, Trip, TripUpdate
CACHE_CONTROL = cache_control(max_age=7200) CACHE_CONTROL = cache_control(max_age=7200)
LAST_MODIFIED = last_modified(lambda *args, **kwargs: datetime.fromisoformat( LAST_MODIFIED = last_modified(lambda *args, **kwargs: GTFSFeed.objects.order_by('-last_modified').first().last_modified)
FeedInfo.objects.get(publisher_name="SNCF_default").version))
LOOKUP_VALUE_REGEX = r"[\w.: |-]+" LOOKUP_VALUE_REGEX = r"[\w.: |-]+"

View File

@ -1,5 +1,5 @@
""" """
ASGI config for sncf project. ASGI config for trainvel project.
It exposes the ASGI callable as a module-level variable named ``application``. It exposes the ASGI callable as a module-level variable named ``application``.
@ -11,6 +11,6 @@ import os
from django.core.asgi import get_asgi_application from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sncf.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "trainvel.settings")
application = get_asgi_application() application = get_asgi_application()

View File

@ -1,26 +1,38 @@
from django.contrib import admin from django.contrib import admin
from django.forms import BaseInlineFormSet
from sncfgtfs.models import Agency, Stop, Route, Trip, StopTime, Calendar, CalendarDate, \ from trainvel.gtfs.models import Agency, Calendar, CalendarDate, FeedInfo, GTFSFeed, \
Transfer, FeedInfo, StopTimeUpdate, TripUpdate Route, Stop, StopTime, StopTimeUpdate, Transfer, Trip, TripUpdate
class LimitModelFormset(BaseInlineFormSet):
""" Base Inline formset to limit inline Model query results. """
def get_queryset(self):
return super(LimitModelFormset, self).get_queryset()[:50]
class CalendarDateInline(admin.TabularInline): class CalendarDateInline(admin.TabularInline):
model = CalendarDate model = CalendarDate
extra = 0 extra = 0
formset = LimitModelFormset
class TripInline(admin.TabularInline): class TripInline(admin.TabularInline):
model = Trip model = Trip
extra = 0 extra = 0
formset = LimitModelFormset
autocomplete_fields = ('route', 'service',) autocomplete_fields = ('route', 'service',)
show_change_link = True show_change_link = True
ordering = ('service',) ordering = ('service',)
readonly_fields = ('gtfs_feed',)
class StopTimeInline(admin.TabularInline): class StopTimeInline(admin.TabularInline):
model = StopTime model = StopTime
extra = 0 extra = 0
formset = LimitModelFormset
autocomplete_fields = ('stop',) autocomplete_fields = ('stop',)
readonly_fields = ('id',)
show_change_link = True show_change_link = True
ordering = ('stop_sequence',) ordering = ('stop_sequence',)
@ -28,47 +40,59 @@ class StopTimeInline(admin.TabularInline):
class TripUpdateInline(admin.StackedInline): class TripUpdateInline(admin.StackedInline):
model = TripUpdate model = TripUpdate
extra = 0 extra = 0
formset = LimitModelFormset
autocomplete_fields = ('trip',) autocomplete_fields = ('trip',)
class StopTimeUpdateInline(admin.StackedInline): class StopTimeUpdateInline(admin.StackedInline):
model = StopTimeUpdate model = StopTimeUpdate
extra = 0 extra = 0
formset = LimitModelFormset
autocomplete_fields = ('trip_update', 'stop_time',) autocomplete_fields = ('trip_update', 'stop_time',)
@admin.register(GTFSFeed)
class GTFSFeedAdmin(admin.ModelAdmin):
list_display = ('name', 'code', 'country', 'last_modified',)
list_filter = ('country', 'last_modified',)
search_fields = ('name', 'code',)
readonly_fields = ('code',)
@admin.register(Agency) @admin.register(Agency)
class AgencyAdmin(admin.ModelAdmin): class AgencyAdmin(admin.ModelAdmin):
list_display = ('name', 'id', 'url', 'timezone',) list_display = ('name', 'id', 'url', 'timezone', 'gtfs_feed',)
list_filter = ('gtfs_feed', 'timezone',)
search_fields = ('name',) search_fields = ('name',)
autocomplete_fields = ('gtfs_feed',)
@admin.register(Stop) @admin.register(Stop)
class StopAdmin(admin.ModelAdmin): class StopAdmin(admin.ModelAdmin):
list_display = ('name', 'id', 'lat', 'lon', 'location_type',) list_display = ('name', 'id', 'lat', 'lon', 'location_type',)
list_filter = ('location_type', 'transport_type',) list_filter = ('location_type', 'gtfs_feed',)
search_fields = ('name', 'id',) search_fields = ('name', 'id',)
ordering = ('name',) ordering = ('name',)
autocomplete_fields = ('parent_station',) autocomplete_fields = ('parent_station', 'gtfs_feed',)
@admin.register(Route) @admin.register(Route)
class RouteAdmin(admin.ModelAdmin): class RouteAdmin(admin.ModelAdmin):
list_display = ('short_name', 'long_name', 'id', 'type',) list_display = ('__str__', 'id', 'type', 'gtfs_feed',)
list_filter = ('transport_type', 'type', 'agency',) list_filter = ('gtfs_feed', 'type', 'agency',)
search_fields = ('long_name', 'short_name', 'id',) search_fields = ('long_name', 'short_name', 'id',)
ordering = ('long_name',) ordering = ('long_name',)
autocomplete_fields = ('agency',) autocomplete_fields = ('agency', 'gtfs_feed',)
inlines = (TripInline,) inlines = (TripInline,)
@admin.register(Trip) @admin.register(Trip)
class TripAdmin(admin.ModelAdmin): class TripAdmin(admin.ModelAdmin):
list_display = ('id', 'route', 'service', 'headsign', 'direction_id',) list_display = ('id', 'origin_destination', 'route', 'service', 'headsign', 'direction_id',)
list_filter = ('direction_id', 'route__transport_type',) list_filter = ('direction_id', 'route__gtfs_feed',)
search_fields = ('id', 'route__id', 'route__long_name', 'service__id', 'headsign',) search_fields = ('id', 'route__id', 'route__long_name', 'service__id', 'headsign',)
ordering = ('route', 'service',) ordering = ('route', 'service',)
autocomplete_fields = ('route', 'service',) autocomplete_fields = ('route', 'service', 'gtfs_feed',)
inlines = (StopTimeInline, TripUpdateInline,) inlines = (StopTimeInline, TripUpdateInline,)
@ -76,28 +100,30 @@ class TripAdmin(admin.ModelAdmin):
class StopTimeAdmin(admin.ModelAdmin): class StopTimeAdmin(admin.ModelAdmin):
list_display = ('trip', 'stop', 'arrival_time', 'departure_time', list_display = ('trip', 'stop', 'arrival_time', 'departure_time',
'stop_sequence', 'pickup_type', 'drop_off_type',) 'stop_sequence', 'pickup_type', 'drop_off_type',)
list_filter = ('pickup_type', 'drop_off_type', 'trip__route__transport_type',) list_filter = ('pickup_type', 'drop_off_type', 'trip__route__gtfs_feed',)
search_fields = ('trip__id', 'stop__name', 'arrival_time', 'departure_time',) search_fields = ('trip__id', 'stop__name', 'arrival_time', 'departure_time',)
ordering = ('trip', 'stop_sequence',) ordering = ('trip', 'stop_sequence',)
autocomplete_fields = ('trip', 'stop',) autocomplete_fields = ('trip', 'stop',)
readonly_fields = ('id',)
inlines = (StopTimeUpdateInline,) inlines = (StopTimeUpdateInline,)
@admin.register(Calendar) @admin.register(Calendar)
class CalendarAdmin(admin.ModelAdmin): class CalendarAdmin(admin.ModelAdmin):
list_display = ('id', 'transport_type', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', list_display = ('id', 'gtfs_feed', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday',
'saturday', 'sunday', 'start_date', 'end_date',) 'saturday', 'sunday', 'start_date', 'end_date',)
list_filter = ('transport_type', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday', list_filter = ('gtfs_feed', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday',
'start_date', 'end_date',) 'start_date', 'end_date',)
search_fields = ('id', 'start_date', 'end_date',) search_fields = ('id', 'start_date', 'end_date',)
ordering = ('transport_type', 'id',) autocomplete_fields = ('gtfs_feed',)
ordering = ('gtfs_feed', 'id',)
inlines = (CalendarDateInline, TripInline,) inlines = (CalendarDateInline, TripInline,)
@admin.register(CalendarDate) @admin.register(CalendarDate)
class CalendarDateAdmin(admin.ModelAdmin): class CalendarDateAdmin(admin.ModelAdmin):
list_display = ('id', 'service_id', 'date', 'exception_type',) list_display = ('id', 'service_id', 'date', 'exception_type',)
list_filter = ('exception_type', 'date', 'service__transport_type',) list_filter = ('exception_type', 'date', 'service__gtfs_feed',)
search_fields = ('id', 'date',) search_fields = ('id', 'date',)
ordering = ('date', 'service_id',) ordering = ('date', 'service_id',)
@ -116,6 +142,7 @@ class FeedInfoAdmin(admin.ModelAdmin):
'end_date', 'version',) 'end_date', 'version',)
search_fields = ('publisher_name', 'publisher_url', 'lang', 'start_date', search_fields = ('publisher_name', 'publisher_url', 'lang', 'start_date',
'end_date', 'version',) 'end_date', 'version',)
autocomplete_fields = ('gtfs_feed',)
ordering = ('publisher_name',) ordering = ('publisher_name',)
@ -123,7 +150,7 @@ class FeedInfoAdmin(admin.ModelAdmin):
class StopTimeUpdateAdmin(admin.ModelAdmin): class StopTimeUpdateAdmin(admin.ModelAdmin):
list_display = ('trip_update', 'stop_time', 'arrival_delay', 'arrival_time', list_display = ('trip_update', 'stop_time', 'arrival_delay', 'arrival_time',
'departure_delay', 'departure_time', 'schedule_relationship',) 'departure_delay', 'departure_time', 'schedule_relationship',)
list_filter = ('schedule_relationship',) list_filter = ('schedule_relationship', 'trip_update__trip__gtfs_feed',)
search_fields = ('trip_update__trip__id', 'stop_time__stop__name', 'arrival_time', 'departure_time',) search_fields = ('trip_update__trip__id', 'stop_time__stop__name', 'arrival_time', 'departure_time',)
ordering = ('trip_update', 'stop_time',) ordering = ('trip_update', 'stop_time',)
autocomplete_fields = ('trip_update', 'stop_time',) autocomplete_fields = ('trip_update', 'stop_time',)

View File

@ -1,6 +1,6 @@
from django.apps import AppConfig from django.apps import AppConfig
class SncfgtfsConfig(AppConfig): class TrainvelGTFSConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField" default_auto_field = "django.db.models.BigAutoField"
name = "sncfgtfs" name = "trainvel.gtfs"

View File

@ -0,0 +1,92 @@
[
{
"model": "gtfs.gtfsfeed",
"pk": "FR-SNCF-TGV",
"fields": {
"name": "SNCF - TGV",
"country": "FR",
"feed_url": "https://eu.ftp.opendatasoft.com/sncf/gtfs/export_gtfs_voyages.zip",
"rt_feed_url": "https://proxy.transport.data.gouv.fr/resource/sncf-tgv-gtfs-rt-trip-updates"
}
},
{
"model": "gtfs.gtfsfeed",
"pk": "FR-SNCF-IC",
"fields": {
"name": "SNCF - Intercités",
"country": "FR",
"feed_url": "https://eu.ftp.opendatasoft.com/sncf/gtfs/export-intercites-gtfs-last.zip",
"rt_feed_url": "https://proxy.transport.data.gouv.fr/resource/sncf-ic-gtfs-rt-trip-updates"
}
},
{
"model": "gtfs.gtfsfeed",
"pk": "FR-SNCF-TER",
"fields": {
"name": "SNCF - TER",
"country": "FR",
"feed_url": "https://eu.ftp.opendatasoft.com/sncf/gtfs/export-ter-gtfs-last.zip",
"rt_feed_url": "https://proxy.transport.data.gouv.fr/resource/sncf-ter-gtfs-rt-trip-updates"
}
},
{
"model": "gtfs.gtfsfeed",
"pk": "FR-IDF-TN",
"fields": {
"name": "SNCF - Transilien",
"country": "FR",
"feed_url": "https://eu.ftp.opendatasoft.com/sncf/gtfs/transilien-gtfs.zip",
"rt_feed_url": ""
}
},
{
"model": "gtfs.gtfsfeed",
"pk": "FR-EUROSTAR",
"fields": {
"name": "Eurostar",
"country": "FR",
"feed_url": "https://www.data.gouv.fr/fr/datasets/r/9089b550-696e-4ae0-87b5-40ea55a14292",
"rt_feed_url": ""
}
},
{
"model": "gtfs.gtfsfeed",
"pk": "IT-FRA-TI",
"fields": {
"name": "Trenitalia France",
"country": "FR",
"feed_url": "https://thello.axelor.com/public/gtfs/gtfs.zip",
"rt_feed_url": "https://thello.axelor.com/public/gtfs/GTFS-RT.bin"
}
},
{
"model": "gtfs.gtfsfeed",
"pk": "ES-RENFE",
"fields": {
"name": "Renfe",
"country": "ES",
"feed_url": "https://ssl.renfe.com/gtransit/Fichero_AV_LD/google_transit.zip",
"rt_feed_url": ""
}
},
{
"model": "gtfs.gtfsfeed",
"pk": "AT-ÖBB",
"fields": {
"name": "ÖBB",
"country": "AT",
"feed_url": "https://static.oebb.at/open-data/soll-fahrplan-gtfs/GTFS_OP_2024_obb.zip",
"rt_feed_url": ""
}
},
{
"model": "gtfs.gtfsfeed",
"pk": "CH-ALL",
"fields": {
"name": "Transports suisses",
"country": "CH",
"feed_url": "https://opentransportdata.swiss/fr/dataset/timetable-2024-gtfs2020/permalink",
"rt_feed_url": "https://api.opentransportdata.swiss/gtfsrt2020"
}
}
]

View File

@ -0,0 +1,821 @@
msgid ""
msgstr ""
"Project-Id-Version: 1.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-05-09 19:34+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Emmy D'Anello <ynerant@emy.lu>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: trainvel/gtfs/models.py:11
msgid "Albania"
msgstr "Albanie"
#: trainvel/gtfs/models.py:12
msgid "Andorra"
msgstr "Andorre"
#: trainvel/gtfs/models.py:13
msgid "Armenia"
msgstr "Arménie"
#: trainvel/gtfs/models.py:14
msgid "Austria"
msgstr "Autriche"
#: trainvel/gtfs/models.py:15
msgid "Azerbaijan"
msgstr "Azerbaijan"
#: trainvel/gtfs/models.py:16
msgid "Belgium"
msgstr "Belgique"
#: trainvel/gtfs/models.py:17
msgid "Bosnia and Herzegovina"
msgstr " Bosnie-Herzégovine"
#: trainvel/gtfs/models.py:18
msgid "Bulgaria"
msgstr "Bulgarie"
#: trainvel/gtfs/models.py:19
msgid "Croatia"
msgstr "Croatie"
#: trainvel/gtfs/models.py:20
msgid "Cyprus"
msgstr "Chypre"
#: trainvel/gtfs/models.py:21
msgid "Czech Republic"
msgstr "République Tchèque"
#: trainvel/gtfs/models.py:22
msgid "Denmark"
msgstr "Danemark"
#: trainvel/gtfs/models.py:23
msgid "Estonia"
msgstr "Estonie"
#: trainvel/gtfs/models.py:24
msgid "Finland"
msgstr "Finlande"
#: trainvel/gtfs/models.py:25
msgid "France"
msgstr "France"
#: trainvel/gtfs/models.py:26
msgid "Georgia"
msgstr "Géorgie"
#: trainvel/gtfs/models.py:27
msgid "Germany"
msgstr "Allemagne"
#: trainvel/gtfs/models.py:28
msgid "Greece"
msgstr "Grèce"
#: trainvel/gtfs/models.py:29
msgid "Hungary"
msgstr "Hongrie"
#: trainvel/gtfs/models.py:30
msgid "Iceland"
msgstr "Islande"
#: trainvel/gtfs/models.py:31
msgid "Ireland"
msgstr "Irlande"
#: trainvel/gtfs/models.py:32
msgid "Italy"
msgstr "Italie"
#: trainvel/gtfs/models.py:33
msgid "Latvia"
msgstr "Lettonie"
#: trainvel/gtfs/models.py:34
msgid "Liechtenstein"
msgstr "Liechtenstein"
#: trainvel/gtfs/models.py:35
msgid "Lithuania"
msgstr "Lituanie"
#: trainvel/gtfs/models.py:36
msgid "Luxembourg"
msgstr "Luxembourg"
#: trainvel/gtfs/models.py:37
msgid "Malta"
msgstr "Malte"
#: trainvel/gtfs/models.py:38
msgid "Moldova"
msgstr "Moldavie"
#: trainvel/gtfs/models.py:39
msgid "Monaco"
msgstr "Monaco"
#: trainvel/gtfs/models.py:40
msgid "Montenegro"
msgstr "Monténégro"
#: trainvel/gtfs/models.py:41
msgid "Netherlands"
msgstr "Pays-Bas"
#: trainvel/gtfs/models.py:42
msgid "North Macedonia"
msgstr "Macédoine du Nord"
#: trainvel/gtfs/models.py:43
msgid "Norway"
msgstr "Norvège"
#: trainvel/gtfs/models.py:44
msgid "Poland"
msgstr "Pologne"
#: trainvel/gtfs/models.py:45
msgid "Portugal"
msgstr "Portugal"
#: trainvel/gtfs/models.py:46
msgid "Romania"
msgstr "Roumanie"
#: trainvel/gtfs/models.py:47
msgid "San Marino"
msgstr "Saint-Marin"
#: trainvel/gtfs/models.py:48
msgid "Serbia"
msgstr "Serbie"
#: trainvel/gtfs/models.py:49
msgid "Slovakia"
msgstr "Slovaquie"
#: trainvel/gtfs/models.py:50
msgid "Slovenia"
msgstr "Slovénie"
#: trainvel/gtfs/models.py:51
msgid "Spain"
msgstr "Espagne"
#: trainvel/gtfs/models.py:52
msgid "Sweden"
msgstr "Suède"
#: trainvel/gtfs/models.py:53
msgid "Switzerland"
msgstr "Suisse"
#: trainvel/gtfs/models.py:54
msgid "Turkey"
msgstr "Turquie"
#: trainvel/gtfs/models.py:55
msgid "United Kingdom"
msgstr "Royaume-Uni"
#: trainvel/gtfs/models.py:56
msgid "Ukraine"
msgstr "Ukraine"
#: trainvel/gtfs/models.py:60
msgid "Stop/platform"
msgstr "Arrêt / quai"
#: trainvel/gtfs/models.py:61
msgid "Station"
msgstr "Gare"
#: trainvel/gtfs/models.py:62
msgid "Entrance/exit"
msgstr "Entrée / sortie"
#: trainvel/gtfs/models.py:63
msgid "Generic node"
msgstr "Nœud générique"
#: trainvel/gtfs/models.py:64
msgid "Boarding area"
msgstr "Zone d'embarquement"
#: trainvel/gtfs/models.py:68
msgid "No information"
msgstr "Pas d'information"
#: trainvel/gtfs/models.py:69
msgid "Possible"
msgstr "Possible"
#: trainvel/gtfs/models.py:70 trainvel/gtfs/models.py:100
msgid "Not possible"
msgstr "Impossible"
#: trainvel/gtfs/models.py:74
msgid "Regular"
msgstr "Régulier"
#: trainvel/gtfs/models.py:75
msgid "None"
msgstr "Aucun"
#: trainvel/gtfs/models.py:76
msgid "Must phone agency"
msgstr "Doit téléphoner à l'agence"
#: trainvel/gtfs/models.py:77
msgid "Must coordinate with driver"
msgstr "Doit se coordonner avec læ conducteurice"
#: trainvel/gtfs/models.py:81
msgid "Tram"
msgstr "Tram"
#: trainvel/gtfs/models.py:82
msgid "Metro"
msgstr "Métro"
#: trainvel/gtfs/models.py:83
msgid "Rail"
msgstr "Rail"
#: trainvel/gtfs/models.py:84
msgid "Bus"
msgstr "Bus"
#: trainvel/gtfs/models.py:85
msgid "Ferry"
msgstr "Ferry"
#: trainvel/gtfs/models.py:86
msgid "Cable car"
msgstr "Câble"
#: trainvel/gtfs/models.py:87
msgid "Gondola"
msgstr "Gondole"
#: trainvel/gtfs/models.py:88
msgid "Funicular"
msgstr "Funiculaire"
#: trainvel/gtfs/models.py:92
msgid "Outbound"
msgstr "Vers l'extérieur"
#: trainvel/gtfs/models.py:93
msgid "Inbound"
msgstr "Vers l'intérieur"
#: trainvel/gtfs/models.py:97
msgid "Recommended"
msgstr "Recommandé"
#: trainvel/gtfs/models.py:98
msgid "Timed"
msgstr "Correspondance programmée"
#: trainvel/gtfs/models.py:99
msgid "Minimum time"
msgstr "Temps de correspondance minimum requis"
#: trainvel/gtfs/models.py:104 trainvel/gtfs/models.py:110
msgid "Added"
msgstr "Ajouté"
#: trainvel/gtfs/models.py:105
msgid "Removed"
msgstr "Supprimé"
#: trainvel/gtfs/models.py:109 trainvel/gtfs/models.py:119
msgid "Scheduled"
msgstr "Planifié"
#: trainvel/gtfs/models.py:111 trainvel/gtfs/models.py:122
msgid "Unscheduled"
msgstr "Non planifié"
#: trainvel/gtfs/models.py:112
msgid "Canceled"
msgstr "Annulé"
#: trainvel/gtfs/models.py:113
msgid "Replacement"
msgstr "Remplacé"
#: trainvel/gtfs/models.py:114
msgid "Duplicated"
msgstr "Dupliqué"
#: trainvel/gtfs/models.py:115
msgid "Deleted"
msgstr "Supprimé"
#: trainvel/gtfs/models.py:120
msgid "Skipped"
msgstr "Sauté"
#: trainvel/gtfs/models.py:121
msgid "No data"
msgstr "Pas de données"
#: trainvel/gtfs/models.py:129
msgid "code"
msgstr "code"
#: trainvel/gtfs/models.py:130
msgid "Unique code of the feed."
msgstr "Code unique du flux."
#: trainvel/gtfs/models.py:135
msgid "name"
msgstr "nom"
#: trainvel/gtfs/models.py:137
msgid "Full name that describes the feed."
msgstr "Nom complet qui décrit le flux."
#: trainvel/gtfs/models.py:142
msgid "country"
msgstr "pays"
#: trainvel/gtfs/models.py:147
msgid "feed URL"
msgstr "URL du flux"
#: trainvel/gtfs/models.py:148
msgid ""
"URL to download the GTFS feed. Must point to a ZIP archive. See https://gtfs."
"org/schedule/ for more information."
msgstr ""
"URL où télécharger le flux GTFS. Doit pointer vers une archive ZIP. Voir "
"https://gtfs.org/fr/schedule/ pour plus d'informations."
#: trainvel/gtfs/models.py:153
msgid "realtime feed URL"
msgstr "URL du flux temps réel"
#: trainvel/gtfs/models.py:156
msgid ""
"URL to download the GTFS-Realtime feed, in the GTFS-RT format. See https://"
"gtfs.org/realtime/ for more information."
msgstr ""
"URL où télécharger le flux GTFS-Temps réel, au format GTFS-RT. Voir https://"
"gtfs.org/fr/realtime/ pour plus d'informations."
#: trainvel/gtfs/models.py:161
msgid "last modified date"
msgstr "Date de dernière modification"
#: trainvel/gtfs/models.py:168
msgid "ETag"
msgstr "ETag"
#: trainvel/gtfs/models.py:171
msgid ""
"If applicable, corresponds to the tag of the last downloaded file. If it is "
"not modified, the file is the same."
msgstr ""
"Si applicable, correspond au tag du dernier fichier téléchargé. S'il n'est "
"pas modifié, le fichier est considéré comme identique."
#: trainvel/gtfs/models.py:179 trainvel/gtfs/models.py:226
#: trainvel/gtfs/models.py:326 trainvel/gtfs/models.py:405
#: trainvel/gtfs/models.py:486 trainvel/gtfs/models.py:696
#: trainvel/gtfs/models.py:811
msgid "GTFS feed"
msgstr "flux GTFS"
#: trainvel/gtfs/models.py:180
msgid "GTFS feeds"
msgstr "flux GTFS"
#: trainvel/gtfs/models.py:189
msgid "Agency ID"
msgstr "ID de l'agence"
#: trainvel/gtfs/models.py:194
msgid "Agency name"
msgstr "Nom de l'agence"
#: trainvel/gtfs/models.py:198
msgid "Agency URL"
msgstr "URL de l'agence"
#: trainvel/gtfs/models.py:203
msgid "Agency timezone"
msgstr "Fuseau horaire de l'agence"
#: trainvel/gtfs/models.py:208
msgid "Agency language"
msgstr "Langue de l'agence"
#: trainvel/gtfs/models.py:214
msgid "Agency phone"
msgstr "Téléphone de l'agence"
#: trainvel/gtfs/models.py:219
msgid "Agency email"
msgstr "Adresse email de l'agence"
#: trainvel/gtfs/models.py:233 trainvel/gtfs/models.py:356
msgid "Agency"
msgstr "Agence"
#: trainvel/gtfs/models.py:234
msgid "Agencies"
msgstr "Agences"
#: trainvel/gtfs/models.py:243 trainvel/gtfs/models.py:593
msgid "Stop ID"
msgstr "ID de l'arrêt"
#: trainvel/gtfs/models.py:248
msgid "Stop code"
msgstr "Code de l'arrêt"
#: trainvel/gtfs/models.py:254
msgid "Stop name"
msgstr "Nom de l'arrêt"
#: trainvel/gtfs/models.py:259
msgid "Stop description"
msgstr "Description de l'arrêt"
#: trainvel/gtfs/models.py:264
msgid "Stop longitude"
msgstr "Longitude de l'arrêt"
#: trainvel/gtfs/models.py:268
msgid "Stop latitude"
msgstr "Latitude de l'arrêt"
#: trainvel/gtfs/models.py:273
msgid "Zone ID"
msgstr "ID de la zone"
#: trainvel/gtfs/models.py:278
msgid "Stop URL"
msgstr "URL de l'arrêt"
#: trainvel/gtfs/models.py:283
msgid "Location type"
msgstr "Type de localisation"
#: trainvel/gtfs/models.py:292
msgid "Parent station"
msgstr "Gare parente"
#: trainvel/gtfs/models.py:300
msgid "Stop timezone"
msgstr "Fuseau horaire de l'arrêt"
#: trainvel/gtfs/models.py:306
msgid "Level ID"
msgstr "ID du niveau"
#: trainvel/gtfs/models.py:311
msgid "Wheelchair boarding"
msgstr "Embarquement en fauteuil roulant"
#: trainvel/gtfs/models.py:319
msgid "Platform code"
msgstr "Code du quai"
#: trainvel/gtfs/models.py:338
msgid "Stop"
msgstr "Arrêt"
#: trainvel/gtfs/models.py:339
msgid "Stops"
msgstr "Arrêts"
#: trainvel/gtfs/models.py:350 trainvel/gtfs/models.py:572
#: trainvel/gtfs/models.py:713 trainvel/gtfs/models.py:746
msgid "ID"
msgstr "Identifiant"
#: trainvel/gtfs/models.py:365
msgid "Route short name"
msgstr "Nom court de la ligne"
#: trainvel/gtfs/models.py:370
msgid "Route long name"
msgstr "Nom long de la ligne"
#: trainvel/gtfs/models.py:376
msgid "Route description"
msgstr "Description de la ligne"
#: trainvel/gtfs/models.py:381
msgid "Route type"
msgstr "Type de ligne"
#: trainvel/gtfs/models.py:386
msgid "Route URL"
msgstr "URL de la ligne"
#: trainvel/gtfs/models.py:392
msgid "Route color"
msgstr "Couleur de la ligne"
#: trainvel/gtfs/models.py:398
msgid "Route text color"
msgstr "Couleur du texte de la ligne"
#: trainvel/gtfs/models.py:412 trainvel/gtfs/models.py:428
msgid "Route"
msgstr "Ligne"
#: trainvel/gtfs/models.py:413
msgid "Routes"
msgstr "Lignes"
#: trainvel/gtfs/models.py:422
msgid "Trip ID"
msgstr "ID du trajet"
#: trainvel/gtfs/models.py:435 trainvel/gtfs/models.py:719
msgid "Service"
msgstr "Service"
#: trainvel/gtfs/models.py:441
msgid "Trip headsign"
msgstr "Destination du trajet"
#: trainvel/gtfs/models.py:447
msgid "Trip short name"
msgstr "Nom court du trajet"
#: trainvel/gtfs/models.py:452
msgid "Direction"
msgstr "Direction"
#: trainvel/gtfs/models.py:459
msgid "Block ID"
msgstr "ID du bloc"
#: trainvel/gtfs/models.py:465
msgid "Shape ID"
msgstr "ID de la forme"
#: trainvel/gtfs/models.py:470
msgid "Wheelchair accessible"
msgstr "Accessible en fauteuil roulant"
#: trainvel/gtfs/models.py:477
msgid "Bikes allowed"
msgstr "Vélos autorisés"
#: trainvel/gtfs/models.py:500 trainvel/gtfs/models.py:509
#: trainvel/gtfs/models.py:552 trainvel/gtfs/models.py:554
msgid "Unknown"
msgstr "Inconnu"
#: trainvel/gtfs/models.py:557
msgid "Origin → Destination"
msgstr "Origine → Destination"
#: trainvel/gtfs/models.py:563 trainvel/gtfs/models.py:578
#: trainvel/gtfs/models.py:825
msgid "Trip"
msgstr "Trajet"
#: trainvel/gtfs/models.py:564
msgid "Trips"
msgstr "Trajets"
#: trainvel/gtfs/models.py:583 trainvel/gtfs/models.py:876
msgid "Arrival time"
msgstr "Heure d'arrivée"
#: trainvel/gtfs/models.py:587 trainvel/gtfs/models.py:884
msgid "Departure time"
msgstr "Heure de départ"
#: trainvel/gtfs/models.py:598
msgid "Stop sequence"
msgstr "Séquence de l'arrêt"
#: trainvel/gtfs/models.py:603
msgid "Stop headsign"
msgstr "Destination de l'arrêt"
#: trainvel/gtfs/models.py:608
msgid "Pickup type"
msgstr "Type de prise en charge"
#: trainvel/gtfs/models.py:615
msgid "Drop off type"
msgstr "Type de dépose"
#: trainvel/gtfs/models.py:622
msgid "Timepoint"
msgstr "Ponctualité"
#: trainvel/gtfs/models.py:645 trainvel/gtfs/models.py:866
msgid "Stop time"
msgstr "Heure d'arrêt"
#: trainvel/gtfs/models.py:646
msgid "Stop times"
msgstr "Heures d'arrêt"
#: trainvel/gtfs/models.py:654
msgid "Service ID"
msgstr "ID du service"
#: trainvel/gtfs/models.py:658
msgid "Monday"
msgstr "Lundi"
#: trainvel/gtfs/models.py:662
msgid "Tuesday"
msgstr "Mardi"
#: trainvel/gtfs/models.py:666
msgid "Wednesday"
msgstr "Mercredi"
#: trainvel/gtfs/models.py:670
msgid "Thursday"
msgstr "Jeudi"
#: trainvel/gtfs/models.py:674
msgid "Friday"
msgstr "Vendredi"
#: trainvel/gtfs/models.py:678
msgid "Saturday"
msgstr "Samedi"
#: trainvel/gtfs/models.py:682
msgid "Sunday"
msgstr "Dimanche"
#: trainvel/gtfs/models.py:686 trainvel/gtfs/models.py:831
msgid "Start date"
msgstr "Date de début"
#: trainvel/gtfs/models.py:690
msgid "End date"
msgstr "Date de fin"
#: trainvel/gtfs/models.py:703
msgid "Calendar"
msgstr "Calendrier"
#: trainvel/gtfs/models.py:704
msgid "Calendars"
msgstr "Calendriers"
#: trainvel/gtfs/models.py:724
msgid "Date"
msgstr "Date"
#: trainvel/gtfs/models.py:728
msgid "Exception type"
msgstr "Type d'exception"
#: trainvel/gtfs/models.py:736
msgid "Calendar date"
msgstr "Date du calendrier"
#: trainvel/gtfs/models.py:737
msgid "Calendar dates"
msgstr "Dates du calendrier"
#: trainvel/gtfs/models.py:752
msgid "From stop"
msgstr "Depuis l'arrêt"
#: trainvel/gtfs/models.py:759
msgid "To stop"
msgstr "Jusqu'à l'arrêt"
#: trainvel/gtfs/models.py:764
msgid "Transfer type"
msgstr "Type de correspondance"
#: trainvel/gtfs/models.py:770
msgid "Minimum transfer time"
msgstr "Temps de correspondance minimum"
#: trainvel/gtfs/models.py:775
msgid "Transfer"
msgstr "Correspondance"
#: trainvel/gtfs/models.py:776
msgid "Transfers"
msgstr "Correspondances"
#: trainvel/gtfs/models.py:783
msgid "Feed publisher name"
msgstr "Nom de l'éditeur du flux"
#: trainvel/gtfs/models.py:787
msgid "Feed publisher URL"
msgstr "URL de l'éditeur du flux"
#: trainvel/gtfs/models.py:792
msgid "Feed language"
msgstr "Langue du flux"
#: trainvel/gtfs/models.py:796
msgid "Feed start date"
msgstr "Date de début du flux"
#: trainvel/gtfs/models.py:800
msgid "Feed end date"
msgstr "Date de fin du flux"
#: trainvel/gtfs/models.py:805
msgid "Feed version"
msgstr "Version du flux"
#: trainvel/gtfs/models.py:815
msgid "Feed info"
msgstr "Information du flux"
#: trainvel/gtfs/models.py:816
msgid "Feed infos"
msgstr "Informations du flux"
#: trainvel/gtfs/models.py:835
msgid "Start time"
msgstr "Heure de début"
#: trainvel/gtfs/models.py:839 trainvel/gtfs/models.py:888
msgid "Schedule relationship"
msgstr "Relation de la planification"
#: trainvel/gtfs/models.py:848 trainvel/gtfs/models.py:859
msgid "Trip update"
msgstr "Mise à jour du trajet"
#: trainvel/gtfs/models.py:849
msgid "Trip updates"
msgstr "Mises à jour des trajets"
#: trainvel/gtfs/models.py:872
msgid "Arrival delay"
msgstr "Retard à l'arrivée"
#: trainvel/gtfs/models.py:880
msgid "Departure delay"
msgstr "Retard au départ"
#: trainvel/gtfs/models.py:897
msgid "Stop time update"
msgstr "Mise à jour du temps d'arrêt"
#: trainvel/gtfs/models.py:898
msgid "Stop time updates"
msgstr "Mises à jour des temps d'arrêt"
#~ msgid "TGV"
#~ msgstr "TGV"
#~ msgid "TER"
#~ msgstr "TER"
#~ msgid "Intercités"
#~ msgstr "Intercités"
#~ msgid "Transilien"
#~ msgstr "Transilien"
#~ msgid "Eurostar"
#~ msgstr "Eurostar"
#~ msgid "Trenitalia"
#~ msgstr "Trenitalia"
#~ msgid "Renfe"
#~ msgstr "Renfe"
#~ msgid "ÖBB"
#~ msgstr "ÖBB"
#~ msgid "Last update"
#~ msgstr "Dernière mise à jour"
#~ msgid "Transport type"
#~ msgstr "Type de transport"

View File

@ -2,63 +2,55 @@ import csv
from datetime import datetime, timedelta from datetime import datetime, timedelta
from io import BytesIO from io import BytesIO
from zipfile import ZipFile from zipfile import ZipFile
from zoneinfo import ZoneInfo
import requests import requests
from django.core.management import BaseCommand from django.core.management import BaseCommand
from sncfgtfs.models import Agency, Calendar, CalendarDate, FeedInfo, Route, Stop, StopTime, Transfer, Trip from trainvel.gtfs.models import Agency, Calendar, CalendarDate, FeedInfo, GTFSFeed, Route, Stop, StopTime, Transfer, Trip, \
PickupType
class Command(BaseCommand): class Command(BaseCommand):
help = "Update the SNCF GTFS database." help = "Update the Trainvel GTFS database."
GTFS_FEEDS = {
"TGV": "https://eu.ftp.opendatasoft.com/sncf/gtfs/export_gtfs_voyages.zip",
"IC": "https://eu.ftp.opendatasoft.com/sncf/gtfs/export-intercites-gtfs-last.zip",
"TER": "https://eu.ftp.opendatasoft.com/sncf/gtfs/export-ter-gtfs-last.zip",
"TN": "https://eu.ftp.opendatasoft.com/sncf/gtfs/transilien-gtfs.zip",
"ES": "https://www.data.gouv.fr/fr/datasets/r/9089b550-696e-4ae0-87b5-40ea55a14292",
"TI": "https://thello.axelor.com/public/gtfs/gtfs.zip",
"RENFE": "https://ssl.renfe.com/gtransit/Fichero_AV_LD/google_transit.zip",
"OBB": "https://static.oebb.at/open-data/soll-fahrplan-gtfs/GTFS_OP_2024_obb.zip",
}
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument('--debug', '-d', action='store_true', help="Activate debug mode")
parser.add_argument('--bulk_size', type=int, default=1000, help="Number of objects to create in bulk.") parser.add_argument('--bulk_size', type=int, default=1000, help="Number of objects to create in bulk.")
parser.add_argument('--dry-run', action='store_true', parser.add_argument('--dry-run', action='store_true',
help="Do not update the database, only print what would be done.") help="Do not update the database, only print what would be done.")
parser.add_argument('--force', '-f', action='store_true', help="Force the update of the database.") parser.add_argument('--force', '-f', action='store_true', help="Force the update of the database.")
def handle(self, *args, **options): def handle(self, debug: bool = False, bulk_size: int = 100, dry_run: bool = False, force: bool = False,
bulk_size = options['bulk_size'] verbosity: int = 1, *args, **options):
dry_run = options['dry_run']
force = options['force']
if dry_run: if dry_run:
self.stdout.write(self.style.WARNING("Dry run mode activated.")) self.stdout.write(self.style.WARNING("Dry run mode activated."))
if not FeedInfo.objects.exists():
last_update_date = "1970-01-01"
else:
last_update_date = FeedInfo.objects.get(publisher_name='SNCF_default').version
for url in self.GTFS_FEEDS.values():
resp = requests.head(url)
if "Last-Modified" not in resp.headers:
continue
last_modified = resp.headers["Last-Modified"]
last_modified = datetime.strptime(last_modified, "%a, %d %b %Y %H:%M:%S %Z")
if last_modified.date().isoformat() > last_update_date:
break
else:
if not force:
self.stdout.write(self.style.WARNING("Database already up-to-date."))
return
self.stdout.write("Updating database...") self.stdout.write("Updating database...")
for transport_type, feed_url in self.GTFS_FEEDS.items(): for gtfs_feed in GTFSFeed.objects.all():
self.stdout.write(f"Downloading {transport_type} GTFS feed...") gtfs_code = gtfs_feed.code
with ZipFile(BytesIO(requests.get(feed_url).content)) as zipfile:
if not force:
# Check if the source file was updated
resp = requests.head(gtfs_feed.feed_url, allow_redirects=True)
if 'ETag' in resp.headers and gtfs_feed.etag:
if resp.headers['ETag'] == gtfs_feed.etag:
if verbosity >= 1:
self.stdout.write(self.style.WARNING(f"Database is already up-to-date for {gtfs_feed}."))
continue
if 'Last-Modified' in resp.headers and gtfs_feed.last_modified:
last_modified = resp.headers['Last-Modified']
last_modified = datetime.strptime(last_modified, "%a, %d %b %Y %H:%M:%S %Z") \
.replace(tzinfo=ZoneInfo(last_modified.split(' ')[-1]))
if last_modified <= gtfs_feed.last_modified:
if verbosity >= 1:
self.stdout.write(self.style.WARNING(f"Database is already up-to-date for {gtfs_feed}."))
continue
self.stdout.write(f"Downloading GTFS feed for {gtfs_feed}...")
resp = requests.get(gtfs_feed.feed_url, allow_redirects=True, stream=True)
with ZipFile(BytesIO(resp.content)) as zipfile:
def read_file(filename): def read_file(filename):
lines = zipfile.read(filename).decode().replace('\ufeff', '').splitlines() lines = zipfile.read(filename).decode().replace('\ufeff', '').splitlines()
return [line.strip() for line in lines] return [line.strip() for line in lines]
@ -66,23 +58,25 @@ class Command(BaseCommand):
agencies = [] agencies = []
for agency_dict in csv.DictReader(read_file("agency.txt")): for agency_dict in csv.DictReader(read_file("agency.txt")):
agency_dict: dict agency_dict: dict
if transport_type == "ES" \ # if gtfs_code == "FR-EUROSTAR" \
and agency_dict['agency_id'] != 'ES' and agency_dict['agency_id'] != 'ER': # and agency_dict['agency_id'] != 'ES' and agency_dict['agency_id'] != 'ER':
continue # continue
agency = Agency( agency = Agency(
id=agency_dict['agency_id'], id=f"{gtfs_code}-{agency_dict['agency_id']}",
name=agency_dict['agency_name'], name=agency_dict['agency_name'],
url=agency_dict['agency_url'], url=agency_dict['agency_url'],
timezone=agency_dict['agency_timezone'], timezone=agency_dict['agency_timezone'],
lang=agency_dict.get('agency_lang', "fr"), lang=agency_dict.get('agency_lang', "fr"),
phone=agency_dict.get('agency_phone', ""), phone=agency_dict.get('agency_phone', ""),
email=agency_dict.get('agency_email', ""), email=agency_dict.get('agency_email', ""),
gtfs_feed=gtfs_feed,
) )
agencies.append(agency) agencies.append(agency)
if agencies and not dry_run: if agencies and not dry_run:
Agency.objects.bulk_create(agencies, Agency.objects.bulk_create(agencies,
update_conflicts=True, update_conflicts=True,
update_fields=['name', 'url', 'timezone', 'lang', 'phone', 'email'], update_fields=['name', 'url', 'timezone', 'lang', 'phone', 'email',
'gtfs_feed'],
unique_fields=['id']) unique_fields=['id'])
agencies.clear() agencies.clear()
@ -90,8 +84,10 @@ class Command(BaseCommand):
for stop_dict in csv.DictReader(read_file("stops.txt")): for stop_dict in csv.DictReader(read_file("stops.txt")):
stop_dict: dict stop_dict: dict
stop_id = stop_dict['stop_id'] stop_id = stop_dict['stop_id']
if transport_type in ["ES", "TI", "RENFE"]: stop_id = f"{gtfs_code}-{stop_id}"
stop_id = f"{transport_type}-{stop_id}"
parent_station_id = stop_dict.get('parent_station', None)
parent_station_id = f"{gtfs_code}-{parent_station_id}" if parent_station_id else None
stop = Stop( stop = Stop(
id=stop_id, id=stop_id,
@ -101,13 +97,13 @@ class Command(BaseCommand):
lon=stop_dict['stop_lon'], lon=stop_dict['stop_lon'],
zone_id=stop_dict.get('zone_id', ""), zone_id=stop_dict.get('zone_id', ""),
url=stop_dict.get('stop_url', ""), url=stop_dict.get('stop_url', ""),
location_type=stop_dict.get('location_type', 1) or 1, location_type=stop_dict.get('location_type', 0) or 0,
parent_station_id=stop_dict.get('parent_station', None) or None, parent_station_id=parent_station_id,
timezone=stop_dict.get('stop_timezone', ""), timezone=stop_dict.get('stop_timezone', ""),
wheelchair_boarding=stop_dict.get('wheelchair_boarding', 0), wheelchair_boarding=stop_dict.get('wheelchair_boarding', 0),
level_id=stop_dict.get('level_id', ""), level_id=stop_dict.get('level_id', ""),
platform_code=stop_dict.get('platform_code', ""), platform_code=stop_dict.get('platform_code', ""),
transport_type=transport_type, gtfs_feed=gtfs_feed,
) )
stops.append(stop) stops.append(stop)
@ -118,7 +114,7 @@ class Command(BaseCommand):
update_fields=['name', 'desc', 'lat', 'lon', 'zone_id', 'url', update_fields=['name', 'desc', 'lat', 'lon', 'zone_id', 'url',
'location_type', 'parent_station_id', 'timezone', 'location_type', 'parent_station_id', 'timezone',
'wheelchair_boarding', 'level_id', 'platform_code', 'wheelchair_boarding', 'level_id', 'platform_code',
'transport_type'], 'gtfs_feed'],
unique_fields=['id']) unique_fields=['id'])
stops.clear() stops.clear()
@ -126,11 +122,10 @@ class Command(BaseCommand):
for route_dict in csv.DictReader(read_file("routes.txt")): for route_dict in csv.DictReader(read_file("routes.txt")):
route_dict: dict route_dict: dict
route_id = route_dict['route_id'] route_id = route_dict['route_id']
if transport_type == "TI": route_id = f"{gtfs_code}-{route_id}"
route_id = f"{transport_type}-{route_id}"
route = Route( route = Route(
id=route_id, id=route_id,
agency_id=route_dict['agency_id'], agency_id=f"{gtfs_code}-{route_dict['agency_id']}",
short_name=route_dict['route_short_name'], short_name=route_dict['route_short_name'],
long_name=route_dict['route_long_name'], long_name=route_dict['route_long_name'],
desc=route_dict.get('route_desc', ""), desc=route_dict.get('route_desc', ""),
@ -138,7 +133,7 @@ class Command(BaseCommand):
url=route_dict.get('route_url', ""), url=route_dict.get('route_url', ""),
color=route_dict.get('route_color', ""), color=route_dict.get('route_color', ""),
text_color=route_dict.get('route_text_color', ""), text_color=route_dict.get('route_text_color', ""),
transport_type=transport_type, gtfs_feed=gtfs_feed,
) )
routes.append(route) routes.append(route)
@ -147,7 +142,7 @@ class Command(BaseCommand):
update_conflicts=True, update_conflicts=True,
update_fields=['agency_id', 'short_name', 'long_name', 'desc', update_fields=['agency_id', 'short_name', 'long_name', 'desc',
'type', 'url', 'color', 'text_color', 'type', 'url', 'color', 'text_color',
'transport_type'], 'gtfs_feed'],
unique_fields=['id']) unique_fields=['id'])
routes.clear() routes.clear()
if routes and not dry_run: if routes and not dry_run:
@ -155,17 +150,17 @@ class Command(BaseCommand):
update_conflicts=True, update_conflicts=True,
update_fields=['agency_id', 'short_name', 'long_name', 'desc', update_fields=['agency_id', 'short_name', 'long_name', 'desc',
'type', 'url', 'color', 'text_color', 'type', 'url', 'color', 'text_color',
'transport_type'], 'gtfs_feed'],
unique_fields=['id']) unique_fields=['id'])
routes.clear() routes.clear()
Calendar.objects.filter(transport_type=transport_type).delete() Calendar.objects.filter(gtfs_feed=gtfs_feed).delete()
calendars = {} calendars = {}
if "calendar.txt" in zipfile.namelist(): if "calendar.txt" in zipfile.namelist():
for calendar_dict in csv.DictReader(read_file("calendar.txt")): for calendar_dict in csv.DictReader(read_file("calendar.txt")):
calendar_dict: dict calendar_dict: dict
calendar = Calendar( calendar = Calendar(
id=f"{transport_type}-{calendar_dict['service_id']}", id=f"{gtfs_code}-{calendar_dict['service_id']}",
monday=calendar_dict['monday'], monday=calendar_dict['monday'],
tuesday=calendar_dict['tuesday'], tuesday=calendar_dict['tuesday'],
wednesday=calendar_dict['wednesday'], wednesday=calendar_dict['wednesday'],
@ -175,7 +170,7 @@ class Command(BaseCommand):
sunday=calendar_dict['sunday'], sunday=calendar_dict['sunday'],
start_date=calendar_dict['start_date'], start_date=calendar_dict['start_date'],
end_date=calendar_dict['end_date'], end_date=calendar_dict['end_date'],
transport_type=transport_type, gtfs_feed=gtfs_feed,
) )
calendars[calendar.id] = calendar calendars[calendar.id] = calendar
@ -184,14 +179,14 @@ class Command(BaseCommand):
update_conflicts=True, update_conflicts=True,
update_fields=['monday', 'tuesday', 'wednesday', 'thursday', update_fields=['monday', 'tuesday', 'wednesday', 'thursday',
'friday', 'saturday', 'sunday', 'start_date', 'friday', 'saturday', 'sunday', 'start_date',
'end_date', 'transport_type'], 'end_date', 'gtfs_feed'],
unique_fields=['id']) unique_fields=['id'])
calendars.clear() calendars.clear()
if calendars and not dry_run: if calendars and not dry_run:
Calendar.objects.bulk_create(calendars.values(), update_conflicts=True, Calendar.objects.bulk_create(calendars.values(), update_conflicts=True,
update_fields=['monday', 'tuesday', 'wednesday', 'thursday', update_fields=['monday', 'tuesday', 'wednesday', 'thursday',
'friday', 'saturday', 'sunday', 'start_date', 'friday', 'saturday', 'sunday', 'start_date',
'end_date', 'transport_type'], 'end_date', 'gtfs_feed'],
unique_fields=['id']) unique_fields=['id'])
calendars.clear() calendars.clear()
@ -199,8 +194,8 @@ class Command(BaseCommand):
for calendar_date_dict in csv.DictReader(read_file("calendar_dates.txt")): for calendar_date_dict in csv.DictReader(read_file("calendar_dates.txt")):
calendar_date_dict: dict calendar_date_dict: dict
calendar_date = CalendarDate( calendar_date = CalendarDate(
id=f"{transport_type}-{calendar_date_dict['service_id']}-{calendar_date_dict['date']}", id=f"{gtfs_code}-{calendar_date_dict['service_id']}-{calendar_date_dict['date']}",
service_id=f"{transport_type}-{calendar_date_dict['service_id']}", service_id=f"{gtfs_code}-{calendar_date_dict['service_id']}",
date=calendar_date_dict['date'], date=calendar_date_dict['date'],
exception_type=calendar_date_dict['exception_type'], exception_type=calendar_date_dict['exception_type'],
) )
@ -208,7 +203,7 @@ class Command(BaseCommand):
if calendar_date.service_id not in calendars: if calendar_date.service_id not in calendars:
calendar = Calendar( calendar = Calendar(
id=f"{transport_type}-{calendar_date_dict['service_id']}", id=f"{gtfs_code}-{calendar_date_dict['service_id']}",
monday=False, monday=False,
tuesday=False, tuesday=False,
wednesday=False, wednesday=False,
@ -218,11 +213,11 @@ class Command(BaseCommand):
sunday=False, sunday=False,
start_date=calendar_date_dict['date'], start_date=calendar_date_dict['date'],
end_date=calendar_date_dict['date'], end_date=calendar_date_dict['date'],
transport_type=transport_type, gtfs_feed=gtfs_feed,
) )
calendars[calendar.id] = calendar calendars[calendar.id] = calendar
else: else:
calendar = calendars[f"{transport_type}-{calendar_date_dict['service_id']}"] calendar = calendars[f"{gtfs_code}-{calendar_date_dict['service_id']}"]
if calendar.start_date > calendar_date.date: if calendar.start_date > calendar_date.date:
calendar.start_date = calendar_date.date calendar.start_date = calendar_date.date
if calendar.end_date < calendar_date.date: if calendar.end_date < calendar_date.date:
@ -232,7 +227,7 @@ class Command(BaseCommand):
Calendar.objects.bulk_create(calendars.values(), Calendar.objects.bulk_create(calendars.values(),
batch_size=bulk_size, batch_size=bulk_size,
update_conflicts=True, update_conflicts=True,
update_fields=['start_date', 'end_date'], update_fields=['start_date', 'end_date', 'gtfs_feed'],
unique_fields=['id']) unique_fields=['id'])
CalendarDate.objects.bulk_create(calendar_dates, CalendarDate.objects.bulk_create(calendar_dates,
batch_size=bulk_size, batch_size=bulk_size,
@ -247,22 +242,12 @@ class Command(BaseCommand):
trip_dict: dict trip_dict: dict
trip_id = trip_dict['trip_id'] trip_id = trip_dict['trip_id']
route_id = trip_dict['route_id'] route_id = trip_dict['route_id']
if transport_type in ["TGV", "IC", "TER"]: trip_id = f"{gtfs_code}-{trip_id}"
trip_id, last_update = trip_id.split(':', 1) route_id = f"{gtfs_code}-{route_id}"
last_update = datetime.fromisoformat(last_update)
elif transport_type in ["ES", "RENFE"]:
trip_id = f"{transport_type}-{trip_id}"
last_update = None
elif transport_type == "TI":
trip_id = f"{transport_type}-{trip_id}"
route_id = f"{transport_type}-{route_id}"
last_update = None
else:
last_update = None
trip = Trip( trip = Trip(
id=trip_id, id=trip_id,
route_id=route_id, route_id=route_id,
service_id=f"{transport_type}-{trip_dict['service_id']}", service_id=f"{gtfs_code}-{trip_dict['service_id']}",
headsign=trip_dict.get('trip_headsign', ""), headsign=trip_dict.get('trip_headsign', ""),
short_name=trip_dict.get('trip_short_name', ""), short_name=trip_dict.get('trip_short_name', ""),
direction_id=trip_dict.get('direction_id', None) or None, direction_id=trip_dict.get('direction_id', None) or None,
@ -270,7 +255,7 @@ class Command(BaseCommand):
shape_id=trip_dict.get('shape_id', ""), shape_id=trip_dict.get('shape_id', ""),
wheelchair_accessible=trip_dict.get('wheelchair_accessible', None), wheelchair_accessible=trip_dict.get('wheelchair_accessible', None),
bikes_allowed=trip_dict.get('bikes_allowed', None), bikes_allowed=trip_dict.get('bikes_allowed', None),
last_update=last_update, gtfs_feed=gtfs_feed,
) )
trips.append(trip) trips.append(trip)
@ -279,7 +264,7 @@ class Command(BaseCommand):
update_conflicts=True, update_conflicts=True,
update_fields=['route_id', 'service_id', 'headsign', 'short_name', update_fields=['route_id', 'service_id', 'headsign', 'short_name',
'direction_id', 'block_id', 'shape_id', 'direction_id', 'block_id', 'shape_id',
'wheelchair_accessible', 'bikes_allowed'], 'wheelchair_accessible', 'bikes_allowed', 'gtfs_feed'],
unique_fields=['id']) unique_fields=['id'])
trips.clear() trips.clear()
if trips and not dry_run: if trips and not dry_run:
@ -287,7 +272,7 @@ class Command(BaseCommand):
update_conflicts=True, update_conflicts=True,
update_fields=['route_id', 'service_id', 'headsign', 'short_name', update_fields=['route_id', 'service_id', 'headsign', 'short_name',
'direction_id', 'block_id', 'shape_id', 'direction_id', 'block_id', 'shape_id',
'wheelchair_accessible', 'bikes_allowed'], 'wheelchair_accessible', 'bikes_allowed', 'gtfs_feed'],
unique_fields=['id']) unique_fields=['id'])
trips.clear() trips.clear()
@ -296,14 +281,10 @@ class Command(BaseCommand):
stop_time_dict: dict stop_time_dict: dict
stop_id = stop_time_dict['stop_id'] stop_id = stop_time_dict['stop_id']
if transport_type in ["ES", "TI", "RENFE"]: stop_id = f"{gtfs_code}-{stop_id}"
stop_id = f"{transport_type}-{stop_id}"
trip_id = stop_time_dict['trip_id'] trip_id = stop_time_dict['trip_id']
if transport_type in ["TGV", "IC", "TER"]: trip_id = f"{gtfs_code}-{trip_id}"
trip_id = trip_id.split(':', 1)[0]
elif transport_type in ["ES", "TI", "RENFE"]:
trip_id = f"{transport_type}-{trip_id}"
arr_time = stop_time_dict['arrival_time'] arr_time = stop_time_dict['arrival_time']
arr_h, arr_m, arr_s = map(int, arr_time.split(':')) arr_h, arr_m, arr_s = map(int, arr_time.split(':'))
@ -314,19 +295,16 @@ class Command(BaseCommand):
pickup_type = stop_time_dict.get('pickup_type', 0) pickup_type = stop_time_dict.get('pickup_type', 0)
drop_off_type = stop_time_dict.get('drop_off_type', 0) drop_off_type = stop_time_dict.get('drop_off_type', 0)
if transport_type in ["ES", "RENFE", "OBB"]: if stop_time_dict['stop_sequence'] == "1":
if stop_time_dict['stop_sequence'] == "1": # First stop
drop_off_type = 1 drop_off_type = PickupType.NONE
elif arr_time == dep_time: elif arr_time == dep_time:
pickup_type = 1 # Last stop
elif transport_type == "TI": pickup_type = PickupType.NONE
if stop_time_dict['stop_sequence'] == "0":
drop_off_type = 1
elif arr_time == dep_time:
pickup_type = 1
st = StopTime( st = StopTime(
id=f"{trip_id}-{stop_id}-{stop_time_dict['departure_time']}", id=f"{gtfs_code}-{stop_time_dict['trip_id']}-{stop_time_dict['stop_id']}"
f"-{stop_time_dict['departure_time']}",
trip_id=trip_id, trip_id=trip_id,
arrival_time=timedelta(seconds=arr_time), arrival_time=timedelta(seconds=arr_time),
departure_time=timedelta(seconds=dep_time), departure_time=timedelta(seconds=dep_time),
@ -362,14 +340,13 @@ class Command(BaseCommand):
transfer_dict: dict transfer_dict: dict
from_stop_id = transfer_dict['from_stop_id'] from_stop_id = transfer_dict['from_stop_id']
to_stop_id = transfer_dict['to_stop_id'] to_stop_id = transfer_dict['to_stop_id']
if transport_type in ["ES", "RENFE", "OBB"]: from_stop_id = f"{gtfs_code}-{from_stop_id}"
from_stop_id = f"{transport_type}-{from_stop_id}" to_stop_id = f"{gtfs_code}-{to_stop_id}"
to_stop_id = f"{transport_type}-{to_stop_id}"
transfer = Transfer( transfer = Transfer(
id=f"{from_stop_id}-{to_stop_id}", id=f"{transfer_dict['from_stop_id']}-{transfer_dict['to_stop_id']}",
from_stop_id=transfer_dict['from_stop_id'], from_stop_id=from_stop_id,
to_stop_id=transfer_dict['to_stop_id'], to_stop_id=to_stop_id,
transfer_type=transfer_dict['transfer_type'], transfer_type=transfer_dict['transfer_type'],
min_transfer_time=transfer_dict['min_transfer_time'], min_transfer_time=transfer_dict['min_transfer_time'],
) )
@ -394,6 +371,7 @@ class Command(BaseCommand):
feed_info_dict: dict feed_info_dict: dict
FeedInfo.objects.update_or_create( FeedInfo.objects.update_or_create(
publisher_name=feed_info_dict['feed_publisher_name'], publisher_name=feed_info_dict['feed_publisher_name'],
gtfs_feed=gtfs_feed,
defaults=dict( defaults=dict(
publisher_url=feed_info_dict['feed_publisher_url'], publisher_url=feed_info_dict['feed_publisher_url'],
lang=feed_info_dict['feed_lang'], lang=feed_info_dict['feed_lang'],
@ -402,3 +380,12 @@ class Command(BaseCommand):
version=feed_info_dict.get('feed_version', 1), version=feed_info_dict.get('feed_version', 1),
) )
) )
if 'ETag' in resp.headers:
gtfs_feed.etag = resp.headers['ETag']
gtfs_feed.save()
if 'Last-Modified' in resp.headers:
last_modified = resp.headers['Last-Modified']
gtfs_feed.last_modified = datetime.strptime(last_modified, "%a, %d %b %Y %H:%M:%S %Z") \
.replace(tzinfo=ZoneInfo(last_modified.split(' ')[-1]))
gtfs_feed.save()

View File

@ -0,0 +1,198 @@
from datetime import timedelta, datetime, date, time
from zoneinfo import ZoneInfo
import requests
from django.core.management import BaseCommand
from trainvel.gtfs.gtfs_realtime_pb2 import FeedMessage, TripUpdate as GTFSTripUpdate
from trainvel.gtfs.models import Agency, Calendar, CalendarDate, ExceptionType, GTFSFeed, PickupType, \
Route, RouteType, Stop, StopScheduleRelationship, StopTime, StopTimeUpdate, \
Trip, TripUpdate, TripScheduleRelationship
class Command(BaseCommand):
help = "Update the Trainvel GTFS Realtime database."
def add_arguments(self, parser):
parser.add_argument('--debug', '-d', action='store_true', help="Activate debug mode")
def handle(self, debug: bool = False, verbosity: int = 1, *args, **options):
for gtfs_feed in GTFSFeed.objects.all():
if not gtfs_feed.rt_feed_url:
if verbosity >= 2:
self.stdout.write(self.style.WARNING(f"No GTFS-RT feed found for {gtfs_feed}."))
continue
self.stdout.write(f"Updating GTFS-RT feed for {gtfs_feed}")
gtfs_code = gtfs_feed.code
feed_message = FeedMessage()
feed_message.ParseFromString(requests.get(gtfs_feed.rt_feed_url, allow_redirects=True).content)
stop_times_updates = []
if debug:
with open(f'feed_message-{gtfs_code}.txt', 'w') as f:
f.write(str(feed_message))
for entity in feed_message.entity:
if entity.HasField("trip_update"):
trip_update = entity.trip_update
trip_id = trip_update.trip.trip_id
trip_id = f"{gtfs_code}-{trip_id}"
start_date = date(year=int(trip_update.trip.start_date[:4]),
month=int(trip_update.trip.start_date[4:6]),
day=int(trip_update.trip.start_date[6:]))
start_dt = datetime.combine(start_date, time(0), tzinfo=ZoneInfo("Europe/Paris"))
if trip_update.trip.schedule_relationship == TripScheduleRelationship.ADDED:
# C'est un trajet nouveau. On crée le trajet associé.
self.create_trip(trip_update, trip_id, start_dt, gtfs_feed)
if not Trip.objects.filter(id=trip_id).exists():
self.stdout.write(f"Trip {trip_id} does not exist in the GTFS feed.")
continue
# Création du TripUpdate
tu, _created = TripUpdate.objects.update_or_create(
trip_id=trip_id,
start_date=trip_update.trip.start_date,
start_time=trip_update.trip.start_time,
defaults=dict(
schedule_relationship=trip_update.trip.schedule_relationship,
)
)
for stop_sequence, stop_time_update in enumerate(trip_update.stop_time_update):
stop_id = stop_time_update.stop_id
stop_id = f"{gtfs_code}-{stop_id}"
if StopTime.objects.filter(trip_id=trip_id, stop=stop_id).exists():
st = StopTime.objects.filter(trip_id=trip_id, stop=stop_id)
if st.count() > 1:
st = st.get(stop_sequence=stop_sequence)
else:
st = st.first()
else:
# Stop is added
st = StopTime.objects.create(
id=f"{trip_id}-{stop_time_update.stop_id}",
trip_id=trip_id,
stop_id=stop_id,
defaults={
"stop_sequence": stop_sequence,
"arrival_time": datetime.fromtimestamp(stop_time_update.arrival.time,
tz=ZoneInfo("Europe/Paris")) - start_dt,
"departure_time": datetime.fromtimestamp(stop_time_update.departure.time,
tz=ZoneInfo("Europe/Paris")) - start_dt,
"pickup_type": (PickupType.REGULAR if stop_time_update.departure.time
else PickupType.NONE),
"drop_off_type": (PickupType.REGULAR if stop_time_update.arrival.time
else PickupType.NONE),
}
)
if stop_time_update.schedule_relationship == StopScheduleRelationship.SKIPPED:
if st.pickup_type != PickupType.NONE or st.drop_off_type != PickupType.NONE:
st.pickup_type = PickupType.NONE
st.drop_off_type = PickupType.NONE
st.save()
if st.stop_sequence != stop_sequence:
st.stop_sequence = stop_sequence
st.save()
st_update = StopTimeUpdate(
trip_update=tu,
stop_time=st,
arrival_delay=timedelta(seconds=stop_time_update.arrival.delay),
arrival_time=datetime.fromtimestamp(stop_time_update.arrival.time,
tz=ZoneInfo("Europe/Paris")),
departure_delay=timedelta(seconds=stop_time_update.departure.delay),
departure_time=datetime.fromtimestamp(stop_time_update.departure.time,
tz=ZoneInfo("Europe/Paris")),
schedule_relationship=stop_time_update.schedule_relationship
or StopScheduleRelationship.SCHEDULED,
)
stop_times_updates.append(st_update)
else:
self.stdout.write(str(entity))
StopTimeUpdate.objects.bulk_create(stop_times_updates,
update_conflicts=True,
update_fields=['arrival_delay', 'arrival_time',
'departure_delay', 'departure_time'],
unique_fields=['trip_update', 'stop_time'])
def create_trip(self, trip_update: GTFSTripUpdate, trip_id: str, start_dt: datetime, gtfs_feed: GTFSFeed) -> None:
headsign = trip_id[5:-1]
gtfs_code = gtfs_feed.code
route, _created = Route.objects.get_or_create(
id=f"{gtfs_code}-ADDED-{headsign}",
gtfs_feed=gtfs_feed,
type=RouteType.RAIL,
short_name="ADDED",
long_name="ADDED ROUTE",
)
Calendar.objects.update_or_create(
id=f"{gtfs_code}-ADDED-{headsign}",
defaults={
"gtfs_feed": gtfs_feed,
"monday": False,
"tuesday": False,
"wednesday": False,
"thursday": False,
"friday": False,
"saturday": False,
"sunday": False,
"start_date": start_dt.date(),
"end_date": start_dt.date(),
}
)
CalendarDate.objects.update_or_create(
id=f"{gtfs_code}-ADDED-{headsign}-{trip_update.trip.start_date}",
defaults={
"service_id": f"{gtfs_code}-ADDED-{headsign}",
"date": trip_update.trip.start_date,
"exception_type": ExceptionType.ADDED,
}
)
Trip.objects.update_or_create(
id=trip_id,
defaults={
"route_id": route.id,
"service_id": f"{gtfs_code}-ADDED-{headsign}",
"headsign": headsign,
"direction_id": trip_update.trip.direction_id,
"gtfs_feed": gtfs_feed,
}
)
for stop_sequence, stop_time_update in enumerate(trip_update.stop_time_update):
stop_id = stop_time_update.stop_id
stop_id = f"{gtfs_code}-{stop_id}"
arr_time = datetime.fromtimestamp(stop_time_update.arrival.time,
tz=ZoneInfo("Europe/Paris")) - start_dt
dep_time = datetime.fromtimestamp(stop_time_update.departure.time,
tz=ZoneInfo("Europe/Paris")) - start_dt
pickup_type = PickupType.REGULAR if stop_time_update.departure.time and stop_sequence > 0 \
else PickupType.NONE
drop_off_type = PickupType.REGULAR if stop_time_update.arrival.time \
and stop_sequence < len(trip_update.stop_time_update) - 1 else PickupType.NONE
StopTime.objects.update_or_create(
id=f"{trip_id}-{stop_time_update.stop_id}",
trip_id=trip_id,
defaults={
"stop_id": stop_id,
"stop_sequence": stop_sequence,
"arrival_time": arr_time,
"departure_time": dep_time,
"pickup_type": pickup_type,
"drop_off_type": drop_off_type,
}
)

View File

View File

@ -2,15 +2,58 @@ from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
class TransportType(models.TextChoices): class Country(models.TextChoices):
TGV = "TGV", _("TGV") """
TER = "TER", _("TER") Country list by ISO 3166-1 alpha-2 code.
INTERCITES = "IC", _("Intercités") Only countries that are member of the Council of Europe
TRANSILIEN = "TN", _("Transilien") are listed for now.
EUROSTAR = "ES", _("Eurostar") """
TRENITALIA = "TI", _("Trenitalia") ALBANIA = "AL", _("Albania")
RENFE = "RENFE", _("Renfe") ANDORRA = "AD", _("Andorra")
OBB = "OBB", _("ÖBB") ARMENIA = "AM", _("Armenia")
AUSTRIA = "AT", _("Austria")
AZERBAIJAN = "AZ", _("Azerbaijan")
BELGIUM = "BE", _("Belgium")
BOSNIA_AND_HERZEGOVINA = "BA", _("Bosnia and Herzegovina")
BULGARIA = "BG", _("Bulgaria")
CROATIA = "HR", _("Croatia")
CYPRUS = "CY", _("Cyprus")
CZECH_REPUBLIC = "CZ", _("Czech Republic")
DENMARK = "DK", _("Denmark")
ESTONIA = "EE", _("Estonia")
FINLAND = "FI", _("Finland")
FRANCE = "FR", _("France")
GEORGIA = "GE", _("Georgia")
GERMANY = "DE", _("Germany")
GREECE = "GR", _("Greece")
HUNGARY = "HU", _("Hungary")
ICELAND = "IS", _("Iceland")
IRELAND = "IE", _("Ireland")
ITALY = "IT", _("Italy")
LATVIA = "LV", _("Latvia")
LIECHTENSTEIN = "LI", _("Liechtenstein")
LITHUANIA = "LT", _("Lithuania")
LUXEMBOURG = "LU", _("Luxembourg")
MALTA = "MT", _("Malta")
MOLDOVA = "MD", _("Moldova")
MONACO = "MC", _("Monaco")
MONTENEGRO = "ME", _("Montenegro")
NETHERLANDS = "NL", _("Netherlands")
NORTH_MACEDONIA = "MK", _("North Macedonia")
NORWAY = "NO", _("Norway")
POLAND = "PL", _("Poland")
PORTUGAL = "PT", _("Portugal")
ROMANIA = "RO", _("Romania")
SAN_MARINO = "SM", _("San Marino")
SERBIA = "RS", _("Serbia")
SLOVAKIA = "SK", _("Slovakia")
SLOVENIA = "SI", _("Slovenia")
SPAIN = "ES", _("Spain")
SWEDEN = "SE", _("Sweden")
SWITZERLAND = "CH", _("Switzerland")
TURKEY = "TR", _("Turkey")
UNITED_KINGDOM = "GB", _("United Kingdom")
UKRAINE = "UA", _("Ukraine")
class LocationType(models.IntegerChoices): class LocationType(models.IntegerChoices):
@ -79,6 +122,66 @@ class StopScheduleRelationship(models.IntegerChoices):
UNSCHEDULED = 3, _("Unscheduled") UNSCHEDULED = 3, _("Unscheduled")
class GTFSFeed(models.Model):
code = models.CharField(
primary_key=True,
max_length=64,
verbose_name=_("code"),
help_text=_("Unique code of the feed.")
)
name = models.CharField(
max_length=255,
verbose_name=_("name"),
unique=True,
help_text=_("Full name that describes the feed."),
)
country = models.CharField(
max_length=2,
verbose_name=_("country"),
choices=Country,
)
feed_url = models.URLField(
verbose_name=_("feed URL"),
help_text=_("URL to download the GTFS feed. Must point to a ZIP archive. "
"See https://gtfs.org/schedule/ for more information."),
)
rt_feed_url = models.URLField(
verbose_name=_("realtime feed URL"),
blank=True,
default="",
help_text=_("URL to download the GTFS-Realtime feed, in the GTFS-RT format. "
"See https://gtfs.org/realtime/ for more information."),
)
last_modified = models.DateTimeField(
verbose_name=_("last modified date"),
null=True,
default=None,
)
etag = models.CharField(
max_length=255,
verbose_name=_("ETag"),
blank=True,
default="",
help_text=_("If applicable, corresponds to the tag of the last downloaded file. "
"If it is not modified, the file is the same."),
)
def __str__(self):
return f"{self.name} ({self.code})"
class Meta:
verbose_name = _("GTFS feed")
verbose_name_plural = _("GTFS feeds")
ordering = ('country', 'name',)
indexes = (models.Index(fields=['name']),)
class Agency(models.Model): class Agency(models.Model):
id = models.CharField( id = models.CharField(
max_length=255, max_length=255,
@ -117,6 +220,12 @@ class Agency(models.Model):
blank=True, blank=True,
) )
gtfs_feed = models.ForeignKey(
GTFSFeed,
on_delete=models.CASCADE,
verbose_name=_("GTFS feed"),
)
def __str__(self): def __str__(self):
return self.name return self.name
@ -124,6 +233,7 @@ class Agency(models.Model):
verbose_name = _("Agency") verbose_name = _("Agency")
verbose_name_plural = _("Agencies") verbose_name_plural = _("Agencies")
ordering = ("name",) ordering = ("name",)
indexes = (models.Index(fields=['name']), models.Index(fields=['gtfs_feed']),)
class Stop(models.Model): class Stop(models.Model):
@ -161,6 +271,7 @@ class Stop(models.Model):
zone_id = models.CharField( zone_id = models.CharField(
max_length=255, max_length=255,
verbose_name=_("Zone ID"), verbose_name=_("Zone ID"),
blank=True,
) )
url = models.URLField( url = models.URLField(
@ -209,10 +320,10 @@ class Stop(models.Model):
blank=True, blank=True,
) )
transport_type = models.CharField( gtfs_feed = models.ForeignKey(
max_length=255, GTFSFeed,
verbose_name=_("Transport type"), on_delete=models.CASCADE,
choices=TransportType, verbose_name=_("GTFS feed"),
) )
@property @property
@ -227,6 +338,9 @@ class Stop(models.Model):
verbose_name = _("Stop") verbose_name = _("Stop")
verbose_name_plural = _("Stops") verbose_name_plural = _("Stops")
ordering = ("id",) ordering = ("id",)
indexes = (models.Index(fields=['name']),
models.Index(fields=['code']),
models.Index(fields=['gtfs_feed']),)
class Route(models.Model): class Route(models.Model):
@ -241,6 +355,9 @@ class Route(models.Model):
on_delete=models.CASCADE, on_delete=models.CASCADE,
verbose_name=_("Agency"), verbose_name=_("Agency"),
related_name="routes", related_name="routes",
null=True,
blank=True,
default=None,
) )
short_name = models.CharField( short_name = models.CharField(
@ -251,6 +368,7 @@ class Route(models.Model):
long_name = models.CharField( long_name = models.CharField(
max_length=255, max_length=255,
verbose_name=_("Route long name"), verbose_name=_("Route long name"),
blank=True,
) )
desc = models.CharField( desc = models.CharField(
@ -281,19 +399,20 @@ class Route(models.Model):
blank=True, blank=True,
) )
transport_type = models.CharField( gtfs_feed = models.ForeignKey(
max_length=255, GTFSFeed,
verbose_name=_("Transport type"), on_delete=models.CASCADE,
choices=TransportType, verbose_name=_("GTFS feed"),
) )
def __str__(self): def __str__(self):
return f"{self.long_name}" return self.long_name or self.short_name
class Meta: class Meta:
verbose_name = _("Route") verbose_name = _("Route")
verbose_name_plural = _("Routes") verbose_name_plural = _("Routes")
ordering = ("id",) ordering = ("id",)
indexes = (models.Index(fields=['gtfs_feed']),)
class Trip(models.Model): class Trip(models.Model):
@ -361,21 +480,24 @@ class Trip(models.Model):
null=True, null=True,
) )
last_update = models.DateTimeField( gtfs_feed = models.ForeignKey(
verbose_name=_("Last update"), GTFSFeed,
null=True, on_delete=models.CASCADE,
verbose_name=_("GTFS feed"),
) )
@property @property
def origin(self): def origin(self) -> Stop | None:
return self.stop_times.order_by('stop_sequence').first().stop return self.stop_times.order_by('stop_sequence').first().stop if self.stop_times.exists() else None
@property @property
def destination(self): def destination(self) -> Stop | None:
return self.stop_times.order_by('-stop_sequence').first().stop return self.stop_times.order_by('-stop_sequence').first().stop if self.stop_times.exists() else None
@property @property
def departure_time(self): def departure_time(self):
if not self.stop_times.exists():
return _("Unknown")
dep_time = self.stop_times.order_by('stop_sequence').first().departure_time dep_time = self.stop_times.order_by('stop_sequence').first().departure_time
hours = int(dep_time.total_seconds() // 3600) hours = int(dep_time.total_seconds() // 3600)
minutes = int((dep_time.total_seconds() % 3600) // 60) minutes = int((dep_time.total_seconds() % 3600) // 60)
@ -383,6 +505,8 @@ class Trip(models.Model):
@property @property
def arrival_time(self): def arrival_time(self):
if not self.stop_times.exists():
return _("Unknown")
arr_time = self.stop_times.order_by('-stop_sequence').first().arrival_time arr_time = self.stop_times.order_by('-stop_sequence').first().arrival_time
hours = int(arr_time.total_seconds() // 3600) hours = int(arr_time.total_seconds() // 3600)
minutes = int((arr_time.total_seconds() % 3600) // 60) minutes = int((arr_time.total_seconds() % 3600) // 60)
@ -390,14 +514,14 @@ class Trip(models.Model):
@property @property
def train_type(self): def train_type(self):
if self.route.transport_type == TransportType.TRANSILIEN: if self.gtfs_feed.code == "FR-IDF-TN":
return self.route.short_name return self.route.short_name
else: else:
return self.origin.stop_type return self.origin.stop_type
@property @property
def train_number(self): def train_number(self):
if self.route.transport_type == TransportType.TRANSILIEN: if self.gtfs_feed.code == "FR-IDF-TN":
return self.short_name return self.short_name
else: else:
return self.headsign return self.headsign
@ -422,13 +546,23 @@ class Trip(models.Model):
return "404042" return "404042"
return "000000" return "000000"
@property
def origin_destination(self):
origin = self.origin
origin = origin.name if origin else _("Unknown")
destination = self.destination
destination = destination.name if destination else _("Unknown")
return f"{origin} {self.departure_time}{destination} {self.arrival_time}"
origin_destination.fget.short_description = _("Origin → Destination")
def __str__(self): def __str__(self):
return f"{self.origin.name} {self.departure_time}{self.destination.name} {self.arrival_time}" \ return self.origin_destination
f" - {self.service_id}"
class Meta: class Meta:
verbose_name = _("Trip") verbose_name = _("Trip")
verbose_name_plural = _("Trips") verbose_name_plural = _("Trips")
indexes = (models.Index(fields=['route']), models.Index(fields=['gtfs_feed']),)
class StopTime(models.Model): class StopTime(models.Model):
@ -510,6 +644,7 @@ class StopTime(models.Model):
class Meta: class Meta:
verbose_name = _("Stop time") verbose_name = _("Stop time")
verbose_name_plural = _("Stop times") verbose_name_plural = _("Stop times")
indexes = (models.Index(fields=['stop']), models.Index(fields=['trip']),)
class Calendar(models.Model): class Calendar(models.Model):
@ -555,10 +690,10 @@ class Calendar(models.Model):
verbose_name=_("End date"), verbose_name=_("End date"),
) )
transport_type = models.CharField( gtfs_feed = models.ForeignKey(
max_length=255, GTFSFeed,
verbose_name=_("Transport type"), on_delete=models.CASCADE,
choices=TransportType, verbose_name=_("GTFS feed"),
) )
def __str__(self): def __str__(self):
@ -568,6 +703,7 @@ class Calendar(models.Model):
verbose_name = _("Calendar") verbose_name = _("Calendar")
verbose_name_plural = _("Calendars") verbose_name_plural = _("Calendars")
ordering = ("id",) ordering = ("id",)
indexes = (models.Index(fields=['gtfs_feed']),)
class CalendarDate(models.Model): class CalendarDate(models.Model):
@ -600,6 +736,7 @@ class CalendarDate(models.Model):
verbose_name = _("Calendar date") verbose_name = _("Calendar date")
verbose_name_plural = _("Calendar dates") verbose_name_plural = _("Calendar dates")
ordering = ("id",) ordering = ("id",)
indexes = (models.Index(fields=['service']), models.Index(fields=['date']),)
class Transfer(models.Model): class Transfer(models.Model):
@ -668,10 +805,17 @@ class FeedInfo(models.Model):
verbose_name=_("Feed version"), verbose_name=_("Feed version"),
) )
gtfs_feed = models.ForeignKey(
GTFSFeed,
on_delete=models.CASCADE,
verbose_name=_("GTFS feed"),
)
class Meta: class Meta:
verbose_name = _("Feed info") verbose_name = _("Feed info")
verbose_name_plural = _("Feed infos") verbose_name_plural = _("Feed infos")
ordering = ("publisher_name",) ordering = ("publisher_name",)
indexes = (models.Index(fields=['gtfs_feed']),)
class TripUpdate(models.Model): class TripUpdate(models.Model):
@ -705,6 +849,7 @@ class TripUpdate(models.Model):
verbose_name_plural = _("Trip updates") verbose_name_plural = _("Trip updates")
ordering = ("start_date", "trip",) ordering = ("start_date", "trip",)
unique_together = ("trip", "start_date", "start_time",) unique_together = ("trip", "start_date", "start_time",)
indexes = (models.Index(fields=['trip']),)
class StopTimeUpdate(models.Model): class StopTimeUpdate(models.Model):
@ -753,3 +898,4 @@ class StopTimeUpdate(models.Model):
verbose_name_plural = _("Stop time updates") verbose_name_plural = _("Stop time updates")
ordering = ("trip_update", "stop_time",) ordering = ("trip_update", "stop_time",)
unique_together = ("trip_update", "stop_time",) unique_together = ("trip_update", "stop_time",)
indexes = (models.Index(fields=['trip_update']), models.Index(fields=['stop_time']),)

View File

@ -1,5 +1,5 @@
""" """
Django settings for sncf project. Django settings for trainvel project.
Generated by 'django-admin startproject' using Django 5.0.1. Generated by 'django-admin startproject' using Django 5.0.1.
@ -44,8 +44,8 @@ INSTALLED_APPS = [
"django_filters", "django_filters",
"rest_framework", "rest_framework",
"sncf.api", "trainvel.api",
"sncfgtfs", "trainvel.gtfs",
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@ -69,7 +69,7 @@ CORS_ALLOW_HEADERS = (
'Cache-Control', 'Cache-Control',
) )
ROOT_URLCONF = "sncf.urls" ROOT_URLCONF = "trainvel.urls"
TEMPLATES = [ TEMPLATES = [
{ {
@ -87,7 +87,7 @@ TEMPLATES = [
}, },
] ]
WSGI_APPLICATION = "sncf.wsgi.application" WSGI_APPLICATION = "trainvel.wsgi.application"
# Database # Database

View File

@ -11,8 +11,8 @@ CORS_ALLOWED_ORIGINS = [
DATABASES = { DATABASES = {
"default": { "default": {
"ENGINE": "django.db.backends.postgresql", "ENGINE": "django.db.backends.postgresql",
"NAME": "sncf", "NAME": "trainvel",
"USER": "sncf", "USER": "trainvel",
"PASSWORD": "CHANGE ME", "PASSWORD": "CHANGE ME",
"HOST": "localhost", "HOST": "localhost",
"PORT": "5432", "PORT": "5432",

View File

@ -1,5 +1,5 @@
""" """
URL configuration for sncf project. URL configuration for trainvel project.
The `urlpatterns` list routes URLs to views. For more information please see: The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.0/topics/http/urls/ https://docs.djangoproject.com/en/5.0/topics/http/urls/
@ -18,7 +18,7 @@ from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from rest_framework import routers from rest_framework import routers
from sncf.api.views import AgencyViewSet, StopViewSet, RouteViewSet, TripViewSet, StopTimeViewSet, \ from trainvel.api.views import AgencyViewSet, StopViewSet, RouteViewSet, TripViewSet, StopTimeViewSet, \
CalendarViewSet, CalendarDateViewSet, TransferViewSet, FeedInfoViewSet, NextDeparturesViewSet, NextArrivalsViewSet, \ CalendarViewSet, CalendarDateViewSet, TransferViewSet, FeedInfoViewSet, NextDeparturesViewSet, NextArrivalsViewSet, \
TripUpdateViewSet, StopTimeUpdateViewSet TripUpdateViewSet, StopTimeUpdateViewSet

View File

@ -1,5 +1,5 @@
""" """
WSGI config for sncf project. WSGI config for trainvel project.
It exposes the WSGI callable as a module-level variable named ``application``. It exposes the WSGI callable as a module-level variable named ``application``.
@ -11,6 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sncf.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "trainvel.settings")
application = get_wsgi_application() application = get_wsgi_application()