Initial commit
This commit is contained in:
441
generate.py
Executable file
441
generate.py
Executable file
@@ -0,0 +1,441 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import coloredlogs
|
||||
import requests
|
||||
import os
|
||||
import json
|
||||
import zipfile
|
||||
import datetime
|
||||
import dateutil.parser
|
||||
import csv
|
||||
import shutil
|
||||
import glob
|
||||
import sys
|
||||
import shutil
|
||||
|
||||
if not sys.version_info >= (3, 8):
|
||||
print("Python >= 3.8 is required")
|
||||
exit(1)
|
||||
|
||||
URL_API_GTFS = "https://data.explore.star.fr/api/records/1.0/search?apikey=3324de385f6b7b5bdfa1727d3e1a9f4bed5dd1accb288db6df5e8487&dataset=tco-busmetro-horaires-gtfs-versions-td&q=&sort=-debutvalidite"
|
||||
URL_API_PICTO_BUS = "https://data.explore.star.fr/api/records/1.0/search/?apikey=3324de385f6b7b5bdfa1727d3e1a9f4bed5dd1accb288db6df5e8487&dataset=tco-bus-lignes-pictogrammes-dm&q=&rows=200&sort=idligne&refine.resolution=1%3A22"
|
||||
URL_API_PICTO_METRO = "https://data.explore.star.fr/api/records/1.0/search/?apikey=3324de385f6b7b5bdfa1727d3e1a9f4bed5dd1accb288db6df5e8487&dataset=tco-metro-lignes-pictogrammes-dm&q=&rows=200&sort=idligne&refine.resolution=1%3A22"
|
||||
URL_PICTO_PREFIX_BUS = (
|
||||
"https://data.explore.star.fr/explore/dataset/tco-bus-lignes-pictogrammes-dm/files/"
|
||||
)
|
||||
URL_PICTO_PREFIX_METRO = "https://data.explore.star.fr/explore/dataset/tco-metro-lignes-pictogrammes-dm/files/"
|
||||
|
||||
|
||||
class Station(object):
|
||||
"""
|
||||
Classe qui représente une station.
|
||||
Utilisé pour transformer un object en json
|
||||
"""
|
||||
|
||||
def __init__(self, sid, name=None, lon=None, lat=None):
|
||||
self.lat = lat
|
||||
self.lon = lon
|
||||
self.sid = sid
|
||||
self.name = name
|
||||
self.terminus = None
|
||||
self.limits = None
|
||||
self.lines = None
|
||||
self.parcours = None
|
||||
|
||||
def __str__(self):
|
||||
o = {}
|
||||
o["type"] = "Feature"
|
||||
o["geometry"] = {}
|
||||
o["geometry"]["type"] = "Point"
|
||||
o["geometry"]["coordinates"] = [self.lon, self.lat]
|
||||
o["properties"] = {}
|
||||
o["properties"]["id"] = self.sid
|
||||
o["properties"]["name"] = self.name
|
||||
o["properties"]["terminus"] = list(self.terminus)
|
||||
o["properties"]["limits"] = self.limits
|
||||
o["properties"]["lines"] = list(self.lines)
|
||||
o["properties"]["parcours"] = list(self.parcours)
|
||||
return json.dumps(o)
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
|
||||
def get_gtfs_dir(today):
|
||||
"""
|
||||
Récupère le repertoire GTFS le plus récent (en fonction de la publication)
|
||||
qui correspond à la date passé en paramètre.
|
||||
Si il n'y a pas de de données GTFS disponible retourne None.
|
||||
"""
|
||||
gtfs_path = "./GTFS/"
|
||||
available_dir = []
|
||||
for filename in os.listdir(gtfs_path):
|
||||
logger.debug("Found directory: " + filename)
|
||||
gtfs_dir = gtfs_path + filename
|
||||
if os.path.isdir(gtfs_dir):
|
||||
start = datetime.datetime.strptime(
|
||||
filename.split("_")[0], "%Y-%m-%d"
|
||||
).date()
|
||||
end = datetime.datetime.strptime(filename.split("_")[1], "%Y-%m-%d").date()
|
||||
if today >= start and today <= end:
|
||||
available_dir.append(gtfs_dir)
|
||||
available_dir_sorted = sorted(available_dir, key=lambda e: e.split("_")[2])
|
||||
if len(available_dir_sorted) > 0:
|
||||
gtfs_dir = available_dir_sorted[-1]
|
||||
logger.debug("Use directory: " + gtfs_dir)
|
||||
return gtfs_dir
|
||||
else:
|
||||
logger.debug("No GTFS data for date: " + str(today))
|
||||
return
|
||||
|
||||
|
||||
#### Generateur de données ###
|
||||
# Recodé en python depuis le générateur original en nodejs
|
||||
# Ajout des parcours en plus pour chaque station
|
||||
|
||||
|
||||
def get_serviceid_today(gtfs_dir, today):
|
||||
services = []
|
||||
csv_calendar = list(csv.DictReader(open(gtfs_dir + "/calendar.txt")))
|
||||
weekdays = list(csv_calendar[0].keys())[1:8]
|
||||
for i in csv_calendar:
|
||||
start_date = datetime.datetime.strptime(i["start_date"], "%Y%m%d").date()
|
||||
end_date = datetime.datetime.strptime(i["end_date"], "%Y%m%d").date()
|
||||
weekday = weekdays[today.weekday()]
|
||||
if i[weekday] == "1" and today >= start_date and today <= end_date:
|
||||
services.append(i["service_id"])
|
||||
|
||||
csv_calendar_dates = list(csv.DictReader(open(gtfs_dir + "/calendar_dates.txt")))
|
||||
for i in csv_calendar_dates:
|
||||
c_date = datetime.datetime.strptime(i["date"], "%Y%m%d").date()
|
||||
if today == c_date:
|
||||
services.append(i["service_id"])
|
||||
|
||||
return services
|
||||
|
||||
|
||||
def get_trips_from_services(gtfs_dir, services_today):
|
||||
trips = {}
|
||||
csv_trips = list(csv.DictReader(open(gtfs_dir + "/trips.txt")))
|
||||
for i in csv_trips:
|
||||
if i["service_id"] in services_today:
|
||||
info = {}
|
||||
info["route_id"] = i["route_id"]
|
||||
info["shape_id"] = i["shape_id"]
|
||||
info["sens"] = i["direction_id"]
|
||||
info["last_sequence"] = "1"
|
||||
trips[i["trip_id"]] = info
|
||||
|
||||
return trips
|
||||
|
||||
|
||||
def generate_stop_map(gtfs_dir):
|
||||
all_stops = {}
|
||||
|
||||
csv_stops = list(csv.DictReader(open(gtfs_dir + "/stops.txt")))
|
||||
for i in csv_stops:
|
||||
stop_detail = {}
|
||||
stop_detail["id"] = i["stop_id"]
|
||||
stop_detail["name"] = i["stop_name"]
|
||||
stop_detail["lat"] = i["stop_lat"]
|
||||
stop_detail["lon"] = i["stop_lon"]
|
||||
stop_detail["trips"] = {}
|
||||
all_stops[i["stop_id"]] = stop_detail
|
||||
|
||||
return all_stops
|
||||
|
||||
|
||||
def associate_trips(gtfs_dir, all_stops, trips):
|
||||
|
||||
csv_stop_times = list(csv.DictReader(open(gtfs_dir + "/stop_times.txt")))
|
||||
for i in csv_stop_times:
|
||||
if i["trip_id"] in trips:
|
||||
stop_info = {}
|
||||
stop_info["arrival_time"] = i["arrival_time"]
|
||||
stop_info["departure_time"] = i["departure_time"]
|
||||
stop_info["route_id"] = trips[i["trip_id"]]["route_id"]
|
||||
stop_info["shape_id"] = trips[i["trip_id"]]["shape_id"]
|
||||
stop_info["sens"] = trips[i["trip_id"]]["sens"]
|
||||
trips[i["trip_id"]]["last_sequence"] = str(
|
||||
max(int(trips[i["trip_id"]]["last_sequence"]), int(i["stop_sequence"]))
|
||||
)
|
||||
stop_info["stop_sequence"] = i["stop_sequence"]
|
||||
all_stops[i["stop_id"]]["trips"][i["trip_id"]] = stop_info
|
||||
|
||||
return all_stops, trips
|
||||
|
||||
|
||||
def filter_no_trips(all_stops):
|
||||
newDict = {}
|
||||
for (k, v) in all_stops.items():
|
||||
if len(all_stops[k]["trips"]) != 0:
|
||||
newDict[k] = v
|
||||
return newDict
|
||||
|
||||
|
||||
def finalize_stops(all_stops, trips):
|
||||
all_stops = filter_no_trips(all_stops)
|
||||
all_stations = []
|
||||
for stop_key, stop_value in all_stops.items():
|
||||
station = Station(stop_key)
|
||||
station.lat = stop_value["lat"]
|
||||
station.lon = stop_value["lon"]
|
||||
station.name = stop_value["name"]
|
||||
terminus_dict = {}
|
||||
station.terminus = set()
|
||||
first_trip = next(iter((stop_value["trips"].items())))
|
||||
first_arrival_time = first_trip[1]["arrival_time"]
|
||||
last_departure_time = first_trip[1]["departure_time"]
|
||||
station.lines = set()
|
||||
station.parcours = set()
|
||||
for (k, v) in stop_value["trips"].items():
|
||||
if v["stop_sequence"] == trips[k]["last_sequence"]:
|
||||
terminus_dict[k] = v
|
||||
arrival_time = v["arrival_time"]
|
||||
departure_time = v["departure_time"]
|
||||
first_arrival_time = min(first_arrival_time, arrival_time)
|
||||
last_departure_time = max(last_departure_time, departure_time)
|
||||
station.lines.add(v["route_id"] + "," + v["sens"])
|
||||
if len(v["shape_id"]):
|
||||
station.parcours.add(v["shape_id"])
|
||||
for (k, v) in terminus_dict.items():
|
||||
station.terminus.add(v["route_id"] + "," + v["sens"])
|
||||
station.limits = [first_arrival_time, last_departure_time]
|
||||
all_stations.append(station)
|
||||
return all_stations
|
||||
|
||||
|
||||
### Fin du générateur
|
||||
|
||||
|
||||
def stations():
|
||||
"""
|
||||
Génère toutes les données geojson possible depuis aujourd'hui
|
||||
jusqu'à la fin des données GTFS disponible.
|
||||
Si les données des stations ont déjà été generées et qu'elles proviennt de données
|
||||
GTFS qui ont la même date de publication on ne les génère pas à nouveau.
|
||||
"""
|
||||
|
||||
os.makedirs("./GTFS/", exist_ok=True)
|
||||
os.makedirs("./data/", exist_ok=True)
|
||||
today = datetime.datetime.now().date()
|
||||
while gtfs_dir := get_gtfs_dir(today):
|
||||
logger.debug("Generate data for " + str(today))
|
||||
publication_gtfs = gtfs_dir.split("_")[-1]
|
||||
publication_gtfs_date = datetime.datetime.fromtimestamp(
|
||||
int(publication_gtfs)
|
||||
).date()
|
||||
logger.debug("Use %s (%s)", gtfs_dir, str(publication_gtfs_date))
|
||||
filename = "./data/" + today.strftime("stations_%Y_%m_%d.geojson")
|
||||
if os.path.exists(filename):
|
||||
logger.debug(filename + " already exists")
|
||||
with open(filename + ".info", "r") as f:
|
||||
present_file_info = json.load(f)
|
||||
present_file_publication = present_file_info["publication"]
|
||||
logger.debug(
|
||||
"File is generated from data published on " + present_file_publication
|
||||
)
|
||||
present_file_publication_date = datetime.datetime.fromtimestamp(
|
||||
int(present_file_publication)
|
||||
).date()
|
||||
if present_file_publication_date == publication_gtfs_date:
|
||||
logger.debug("stations file is up to date")
|
||||
today += datetime.timedelta(days=1)
|
||||
continue
|
||||
services_today = get_serviceid_today(gtfs_dir, today)
|
||||
trips = get_trips_from_services(gtfs_dir, services_today)
|
||||
all_stops = generate_stop_map(gtfs_dir)
|
||||
all_stops, trips = associate_trips(gtfs_dir, all_stops, trips)
|
||||
all_stations = finalize_stops(all_stops, trips)
|
||||
with open(filename, "w") as f:
|
||||
f.write(str(all_stations))
|
||||
with open(filename + ".info", "w") as f:
|
||||
info = {"publication": publication_gtfs}
|
||||
json.dump(info, f)
|
||||
today += datetime.timedelta(days=1)
|
||||
# import ipdb; ipdb.set_trace()
|
||||
|
||||
|
||||
def download_file(url, output_filename):
|
||||
"""
|
||||
Helper: télécharge un fichier
|
||||
"""
|
||||
logger.info("Download: %s", url)
|
||||
with requests.get(url, stream=True) as r:
|
||||
r.raise_for_status()
|
||||
with open(output_filename, "wb") as f:
|
||||
for chunk in r.iter_content(chunk_size=8192):
|
||||
# If you have chunk encoded response uncomment if
|
||||
# and set chunk_size parameter to None.
|
||||
# if chunk:
|
||||
f.write(chunk)
|
||||
|
||||
|
||||
def picto():
|
||||
"""
|
||||
Télécharge tous les picto disponibles.
|
||||
Si ils sont déjà présent on ne fait rien.
|
||||
"""
|
||||
|
||||
for param in [
|
||||
(URL_API_PICTO_BUS, URL_PICTO_PREFIX_BUS),
|
||||
(URL_API_PICTO_METRO, URL_PICTO_PREFIX_METRO),
|
||||
]:
|
||||
r = requests.get(param[0])
|
||||
data = r.json()
|
||||
records = data["records"]
|
||||
logger.debug(json.dumps(records, indent=4))
|
||||
for entry in records:
|
||||
filename = "./images/picto_" + entry["fields"]["idligne"] + ".png"
|
||||
if not os.path.exists(filename):
|
||||
url = param[1] + entry["fields"]["image"]["id"] + "/download/"
|
||||
download_file(url, filename)
|
||||
assert os.path.exists(filename)
|
||||
|
||||
|
||||
def download():
|
||||
"""
|
||||
Télécharge les fichier zip des données GTFS puis les extrait.
|
||||
Si le fichier est déjà téléchargé on ne fait rien.
|
||||
"""
|
||||
|
||||
os.makedirs("./downloads/", exist_ok=True)
|
||||
os.makedirs("./GTFS/", exist_ok=True)
|
||||
r = requests.get(URL_API_GTFS)
|
||||
data = r.json()
|
||||
records = data["records"]
|
||||
logger.debug(json.dumps(records, indent=4))
|
||||
for entry in records:
|
||||
filename = "./downloads/" + entry["fields"]["fichier"]["filename"]
|
||||
filename_info = filename + ".json"
|
||||
with open(filename_info, "w") as f:
|
||||
f.write(json.dumps(entry, indent=4))
|
||||
size = entry["fields"]["tailleoctets"]
|
||||
if not os.path.exists(filename) or os.path.getsize(filename) != size:
|
||||
url = entry["fields"]["url"]
|
||||
output_filename = "./downloads/" + url.split("/")[-1]
|
||||
download_file(url, output_filename)
|
||||
assert os.path.exists(filename)
|
||||
assert os.path.getsize(filename) == size
|
||||
publication_timestamp = dateutil.parser.parse(
|
||||
entry["fields"]["publication"]
|
||||
).timestamp()
|
||||
path_extract = (
|
||||
"./GTFS/"
|
||||
+ entry["fields"]["debutvalidite"]
|
||||
+ "_"
|
||||
+ entry["fields"]["finvalidite"]
|
||||
+ "_"
|
||||
+ str(int(publication_timestamp))
|
||||
)
|
||||
if not os.path.exists(path_extract):
|
||||
os.mkdir(path_extract)
|
||||
if not os.listdir(path_extract):
|
||||
with zipfile.ZipFile(filename) as zf:
|
||||
zf.extractall(path_extract)
|
||||
|
||||
|
||||
def clean():
|
||||
"""
|
||||
Supprime toutes les données produites par ce programme.
|
||||
"""
|
||||
|
||||
shutil.rmtree("./downloads")
|
||||
shutil.rmtree("./GTFS")
|
||||
for picto in glob.glob("./images/picto_*"):
|
||||
os.remove(picto)
|
||||
for stations in glob.glob("./data/stations_*"):
|
||||
os.remove(stations)
|
||||
|
||||
|
||||
def prune():
|
||||
"""
|
||||
Supprime les fichier zip, les données GTFS et les stations si elles ne sont
|
||||
plus valide (avec un delta de deux jours).
|
||||
"""
|
||||
|
||||
today = (datetime.datetime.now() - datetime.timedelta(days=2)).date()
|
||||
for zipfile in glob.glob("./downloads/*.zip"):
|
||||
zipfileinfo = zipfile + ".json"
|
||||
with open(zipfileinfo, "r") as f:
|
||||
info = json.load(f)
|
||||
fin = dateutil.parser.parse(info["fields"]["finvalidite"]).date()
|
||||
if today > fin:
|
||||
os.remove(zipfile) if os.path.exists(zipfile) else None
|
||||
os.remove(zipfileinfo) if os.path.exists(zipfileinfo) else None
|
||||
logger.debug(zipfile + " removed")
|
||||
logger.debug(zipfileinfo + " removed")
|
||||
for gtfsdir in os.listdir("./GTFS/"):
|
||||
fin = dateutil.parser.parse(gtfsdir.split("_")[1]).date()
|
||||
if today > fin:
|
||||
dir_to_remove = "./GTFS/" + gtfsdir
|
||||
shutil.rmtree(dir_to_remove)
|
||||
logger.debug(dir_to_remove + " removed")
|
||||
for stationsfile in glob.glob("./data/stations_*"):
|
||||
filename = os.path.basename(stationsfile)
|
||||
datefromfile = "-".join(filename.split(".")[:1][0].split("_")[1:])
|
||||
date = dateutil.parser.parse(datefromfile).date()
|
||||
if today > date:
|
||||
os.remove(stationsfile) if os.path.exists(stationsfile) else None
|
||||
os.remove(stationsfile + ".info") if os.path.exists(
|
||||
stationsfile + ".info"
|
||||
) else None
|
||||
logger.debug(stationsfile + "removed")
|
||||
|
||||
|
||||
def auto():
|
||||
"""
|
||||
Execute toutes les tâches nécéssaire au fonctionnement du service.
|
||||
Commande à executer chaque jour avec tâche cron.
|
||||
"""
|
||||
|
||||
picto()
|
||||
download()
|
||||
stations()
|
||||
prune()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# fmt: off
|
||||
description_str = ('ouestmonbus.com GTFS downloader \n' +
|
||||
'and data generator\n')
|
||||
# fmt: on
|
||||
usage_str = "%(prog)s action"
|
||||
epilog_str = "v 0.1"
|
||||
frm = argparse.RawTextHelpFormatter
|
||||
parser = argparse.ArgumentParser(
|
||||
description=description_str,
|
||||
epilog=epilog_str,
|
||||
usage=usage_str,
|
||||
formatter_class=frm,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--log",
|
||||
help="log level",
|
||||
default="INFO",
|
||||
)
|
||||
cmd_parser = parser.add_subparsers(dest="command", help="command parser")
|
||||
subparser = cmd_parser.add_parser("download", help="download GTFS")
|
||||
subparser = cmd_parser.add_parser("stations", help="generate stations geojson")
|
||||
subparser = cmd_parser.add_parser("picto", help="download picto")
|
||||
subparser = cmd_parser.add_parser("clean", help="delete all")
|
||||
subparser = cmd_parser.add_parser("prune", help="delete expired data")
|
||||
subparser = cmd_parser.add_parser(
|
||||
"auto", help="execute all needed task to use with cron"
|
||||
)
|
||||
subparser.add_argument(
|
||||
"--days",
|
||||
type=int,
|
||||
default=1,
|
||||
help="nb days to generate",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
coloredlogs.install(level=args.log)
|
||||
logger = logging.getLogger(__name__)
|
||||
os.chdir(os.path.dirname(__file__))
|
||||
locals()[args.command]()
|
||||
Reference in New Issue
Block a user