Compare commits
4 commits
d583b7190f
...
eca5f2e439
| Author | SHA1 | Date | |
|---|---|---|---|
| eca5f2e439 | |||
| 823451f00a | |||
| cf968da148 | |||
| 553365c108 |
1 changed files with 85 additions and 10 deletions
|
|
@ -1,13 +1,15 @@
|
||||||
import csv
|
import csv
|
||||||
import json
|
import json
|
||||||
|
import sys
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from datetime import date, datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from typing import Annotated, Any
|
from typing import Annotated, Any
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import typer
|
import typer
|
||||||
|
from rich import print
|
||||||
|
|
||||||
BASE_URL = "https://www.nightjet.com"
|
BASE_URL = "https://www.nightjet.com"
|
||||||
BASE_DIR = "out"
|
BASE_DIR = "out"
|
||||||
|
|
@ -26,7 +28,7 @@ def dprint(txt) -> None:
|
||||||
print(f"{datetime.now()}: {txt}")
|
print(f"{datetime.now()}: {txt}")
|
||||||
|
|
||||||
|
|
||||||
def request_init_token(endpoint: str = "/nj-booking-ocp/init/start") -> str:
|
def request_start(endpoint: str = "/nj-booking-ocp/init/start") -> dict:
|
||||||
headers = {
|
headers = {
|
||||||
"Referer": "https://www.nightjet.com",
|
"Referer": "https://www.nightjet.com",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|
@ -35,8 +37,11 @@ def request_init_token(endpoint: str = "/nj-booking-ocp/init/start") -> str:
|
||||||
resp_json = requests.post(
|
resp_json = requests.post(
|
||||||
f"{BASE_URL}{endpoint}", data=json.dumps(body), headers=headers
|
f"{BASE_URL}{endpoint}", data=json.dumps(body), headers=headers
|
||||||
).json()
|
).json()
|
||||||
token = resp_json["token"]
|
return resp_json
|
||||||
|
|
||||||
|
|
||||||
|
def get_init_token(endpoint: str = "/nj-booking-ocp/init/start") -> str:
|
||||||
|
token = request_start(endpoint)["token"]
|
||||||
dprint(f"Received init token: {token}")
|
dprint(f"Received init token: {token}")
|
||||||
return token
|
return token
|
||||||
|
|
||||||
|
|
@ -266,21 +271,35 @@ def notify_user(previous: Price, new: Price, channel: str) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def query(start_station: int, end_station: int, travel_date: datetime, traveller_birthdate: datetime) -> list[Price]:
|
def query(
|
||||||
token = request_init_token()
|
start_station: int,
|
||||||
|
end_station: int,
|
||||||
|
travel_date: datetime,
|
||||||
|
traveller_birthdate: datetime,
|
||||||
|
) -> list[Price]:
|
||||||
|
token = get_init_token()
|
||||||
connections = request_connections(token, start_station, end_station, travel_date)
|
connections = request_connections(token, start_station, end_station, travel_date)
|
||||||
booking_requests = connection_data_to_booking_requests(connections, traveller_birthdate=traveller_birthdate)
|
booking_requests = connection_data_to_booking_requests(
|
||||||
|
connections, traveller_birthdate=traveller_birthdate
|
||||||
|
)
|
||||||
bookings = [request_bookings(token, req) for req in booking_requests]
|
bookings = [request_bookings(token, req) for req in booking_requests]
|
||||||
prices = extract_prices(bookings)
|
prices = extract_prices(bookings)
|
||||||
|
|
||||||
return prices
|
return prices
|
||||||
|
|
||||||
|
|
||||||
|
def query_max_booking_date(endpoint: str = "/nj-booking-ocp/init/start") -> date | None:
|
||||||
|
latest_date = request_start().get("maxBookableDate")
|
||||||
|
if not latest_date:
|
||||||
|
return None
|
||||||
|
return date.fromisoformat(latest_date)
|
||||||
|
|
||||||
|
|
||||||
## CLI
|
## CLI
|
||||||
app = typer.Typer()
|
app = typer.Typer()
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command("query")
|
||||||
def main(
|
def main(
|
||||||
travel_date: Annotated[
|
travel_date: Annotated[
|
||||||
str, typer.Argument(help="Travel day to search from. (YYYY-MM-DD)")
|
str, typer.Argument(help="Travel day to search from. (YYYY-MM-DD)")
|
||||||
|
|
@ -291,7 +310,10 @@ def main(
|
||||||
end_station: int = typer.Option(
|
end_station: int = typer.Option(
|
||||||
END_STATION, help="Destination station number. (default: Paris Est)"
|
END_STATION, help="Destination station number. (default: Paris Est)"
|
||||||
),
|
),
|
||||||
birthdate: str = typer.Option(TRAVELLER_BIRTHDATE.strftime("%Y-%m-%d"), help="Traveller birthdate, may be important for discounts. (YYYY-MM-DD)"),
|
birthdate: str = typer.Option(
|
||||||
|
TRAVELLER_BIRTHDATE.strftime("%Y-%m-%d"),
|
||||||
|
help="Traveller birthdate, may be important for discounts. (YYYY-MM-DD)",
|
||||||
|
),
|
||||||
notification_channel: str = typer.Option(
|
notification_channel: str = typer.Option(
|
||||||
NOTIFICATION_CHANNEL, help="ntfy channel to inform user on."
|
NOTIFICATION_CHANNEL, help="ntfy channel to inform user on."
|
||||||
),
|
),
|
||||||
|
|
@ -316,7 +338,23 @@ def main(
|
||||||
dump_price_snapshot: bool = typer.Option(
|
dump_price_snapshot: bool = typer.Option(
|
||||||
True, help="Dump _all_ queried prices into a timestamped csv file."
|
True, help="Dump _all_ queried prices into a timestamped csv file."
|
||||||
),
|
),
|
||||||
|
latest_bookable_date: bool = typer.Option(
|
||||||
|
False, help="Check for latest currently possible booking date only."
|
||||||
|
),
|
||||||
):
|
):
|
||||||
|
"""Check the (lowest) prices for a single connection.
|
||||||
|
|
||||||
|
Will run repeatedly or one-shot and query the prices for a specific connection.
|
||||||
|
Can be set to output all available prices or just output the lowest found.
|
||||||
|
Outputs will (curently) always be given as csv files.
|
||||||
|
"""
|
||||||
|
if latest_bookable_date:
|
||||||
|
latest_date = query_max_booking_date()
|
||||||
|
if not latest_date:
|
||||||
|
dprint("Could not determine max bookable date.")
|
||||||
|
dprint(f"Latest currently bookable date: {latest_date}")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
base_output_directory.mkdir(exist_ok=True, parents=True)
|
base_output_directory.mkdir(exist_ok=True, parents=True)
|
||||||
lowest_prices_path = base_output_directory.joinpath(lowest_prices_filename)
|
lowest_prices_path = base_output_directory.joinpath(lowest_prices_filename)
|
||||||
price_snapshot_path = base_output_directory.joinpath(price_snapshot_pattern)
|
price_snapshot_path = base_output_directory.joinpath(price_snapshot_pattern)
|
||||||
|
|
@ -330,7 +368,10 @@ def main(
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
prices = query(
|
prices = query(
|
||||||
start_station=start_station, end_station=end_station, travel_date=travel_date_obj, traveller_birthdate=birth_date_obj,
|
start_station=start_station,
|
||||||
|
end_station=end_station,
|
||||||
|
travel_date=travel_date_obj,
|
||||||
|
traveller_birthdate=birth_date_obj,
|
||||||
)
|
)
|
||||||
|
|
||||||
# create a snapshot of all current prices
|
# create a snapshot of all current prices
|
||||||
|
|
@ -362,7 +403,41 @@ def main(
|
||||||
if not monitor_mode:
|
if not monitor_mode:
|
||||||
break
|
break
|
||||||
dprint(
|
dprint(
|
||||||
f"Query complete. Monitoring mode active, sleeping for {monitor_frequency} seconds..."
|
f"Checked for connection prices. Monitoring mode active, sleeping for {monitor_frequency} seconds..."
|
||||||
|
)
|
||||||
|
sleep(monitor_frequency)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command("lastdate")
|
||||||
|
def latestbookable_command(
|
||||||
|
notification_channel: str = typer.Option(
|
||||||
|
NOTIFICATION_CHANNEL, help="ntfy channel to inform user on."
|
||||||
|
),
|
||||||
|
monitor_mode: bool = typer.Option(
|
||||||
|
True,
|
||||||
|
help="Run queries repeatedly over time. If False only runs a single query (oneshot mode).",
|
||||||
|
),
|
||||||
|
monitor_frequency: int = typer.Option(
|
||||||
|
MONITOR_FREQUENCY,
|
||||||
|
help="How often to run price queries if in monitoring mode, in seconds.",
|
||||||
|
),
|
||||||
|
):
|
||||||
|
"""Check for the currently latest possible booking date.
|
||||||
|
|
||||||
|
Will run repeatedly or one-shot and simply query for the date that is
|
||||||
|
currently the _last_ day which can be selected for any booking query.
|
||||||
|
"""
|
||||||
|
|
||||||
|
while True:
|
||||||
|
latest_date = query_max_booking_date()
|
||||||
|
if not latest_date:
|
||||||
|
dprint("Could not determine max bookable date.")
|
||||||
|
dprint(f"Latest currently bookable date: {latest_date}")
|
||||||
|
|
||||||
|
if not monitor_mode:
|
||||||
|
break
|
||||||
|
dprint(
|
||||||
|
f"Checked for latest bookable date. Monitoring mode active, sleeping for {monitor_frequency} seconds..."
|
||||||
)
|
)
|
||||||
sleep(monitor_frequency)
|
sleep(monitor_frequency)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue