koleo-cli 0.2.137.17__py3-none-any.whl → 0.2.137.18__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of koleo-cli might be problematic. Click here for more details.
- koleo/__init__.py +1 -1
- koleo/api/__init__.py +2 -0
- koleo/api/base.py +70 -0
- koleo/api/client.py +221 -0
- koleo/api/errors.py +46 -0
- koleo/api/logging.py +56 -0
- koleo/api/types.py +488 -0
- koleo/args.py +279 -0
- koleo/cli/__init__.py +9 -0
- koleo/cli/aliases.py +15 -0
- koleo/cli/base.py +103 -0
- koleo/cli/connections.py +142 -0
- koleo/cli/seats.py +103 -0
- koleo/cli/station_board.py +72 -0
- koleo/cli/stations.py +37 -0
- koleo/cli/train_info.py +142 -0
- koleo/cli/utils.py +27 -0
- koleo/storage.py +66 -12
- koleo/utils.py +95 -8
- {koleo_cli-0.2.137.17.dist-info → koleo_cli-0.2.137.18.dist-info}/METADATA +32 -13
- koleo_cli-0.2.137.18.dist-info/RECORD +26 -0
- {koleo_cli-0.2.137.17.dist-info → koleo_cli-0.2.137.18.dist-info}/WHEEL +1 -1
- koleo_cli-0.2.137.18.dist-info/entry_points.txt +2 -0
- koleo/api.py +0 -161
- koleo/cli.py +0 -608
- koleo/types.py +0 -237
- koleo_cli-0.2.137.17.dist-info/RECORD +0 -13
- koleo_cli-0.2.137.17.dist-info/entry_points.txt +0 -2
- {koleo_cli-0.2.137.17.dist-info → koleo_cli-0.2.137.18.dist-info}/licenses/LICENSE +0 -0
- {koleo_cli-0.2.137.17.dist-info → koleo_cli-0.2.137.18.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from asyncio import gather
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
|
|
4
|
+
from .base import BaseCli
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class StationBoard(BaseCli):
|
|
8
|
+
async def get_arrivals(self, station_id: int, date: datetime):
|
|
9
|
+
cache_id = f"arr-{station_id}-{date.strftime("%Y-%m-%d")}"
|
|
10
|
+
return self.storage.get_cache(cache_id) or self.storage.set_cache(
|
|
11
|
+
cache_id, await self.client.get_arrivals(station_id, date)
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
async def get_departures(self, station_id: int, date: datetime):
|
|
15
|
+
cache_id = f"dep-{station_id}-{date.strftime("%Y-%m-%d")}"
|
|
16
|
+
return self.storage.get_cache(cache_id) or self.storage.set_cache(
|
|
17
|
+
cache_id, await self.client.get_departures(station_id, date)
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
async def full_departures_view(self, station: str, date: datetime):
|
|
21
|
+
st = await self.get_station(station)
|
|
22
|
+
station_info = f"[bold blue][link=https://koleo.pl/dworzec-pkp/{st["name_slug"]}/odjazdy/{date.strftime("%Y-%m-%d")}]{st["name"]} at {date.strftime("%d-%m %H:%M")}[/bold blue] ID: {st["id"]}[/link]"
|
|
23
|
+
self.print(station_info)
|
|
24
|
+
trains = [
|
|
25
|
+
i
|
|
26
|
+
for i in await self.get_departures(st["id"], date)
|
|
27
|
+
if datetime.fromisoformat(i["departure"]).timestamp() > date.timestamp() # type: ignore
|
|
28
|
+
]
|
|
29
|
+
await self.trains_on_station_table(trains)
|
|
30
|
+
|
|
31
|
+
async def full_arrivals_view(self, station: str, date: datetime):
|
|
32
|
+
st = await self.get_station(station)
|
|
33
|
+
station_info = f"[bold blue][link=https://koleo.pl/dworzec-pkp/{st["name_slug"]}/przyjazdy/{date.strftime("%Y-%m-%d")}]{st["name"]} at {date.strftime("%d-%m %H:%M")}[/bold blue] ID: {st["id"]}[/link]"
|
|
34
|
+
self.print(station_info)
|
|
35
|
+
trains = [
|
|
36
|
+
i
|
|
37
|
+
for i in await self.get_arrivals(st["id"], date)
|
|
38
|
+
if datetime.fromisoformat(i["arrival"]).timestamp() > date.timestamp() # type: ignore
|
|
39
|
+
]
|
|
40
|
+
await self.trains_on_station_table(trains, type=2)
|
|
41
|
+
|
|
42
|
+
async def all_trains_view(self, station: str, date: datetime):
|
|
43
|
+
st = await self.get_station(station)
|
|
44
|
+
station_info = f"[bold blue][link=https://koleo.pl/dworzec-pkp/{st["name_slug"]}/odjazdy/{date.strftime("%Y-%m-%d")}]{st["name"]} at {date.strftime("%d-%m %H:%M")}[/bold blue] ID: {st["id"]}[/link]"
|
|
45
|
+
self.print(station_info)
|
|
46
|
+
departures, arrivals, brands = await gather(
|
|
47
|
+
self.get_departures(st["id"], date), self.get_arrivals(st["id"], date), self.get_brands()
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
trains = sorted(
|
|
51
|
+
[(i, 1) for i in departures] + [(i, 2) for i in arrivals],
|
|
52
|
+
key=lambda train: (
|
|
53
|
+
datetime.fromisoformat(train[0]["departure"]) + timedelta(microseconds=1) # type: ignore
|
|
54
|
+
if train[1] == 1
|
|
55
|
+
else (datetime.fromisoformat(train[0]["arrival"])) # type: ignore
|
|
56
|
+
).timestamp(),
|
|
57
|
+
)
|
|
58
|
+
trains = [
|
|
59
|
+
(i, type)
|
|
60
|
+
for i, type in trains
|
|
61
|
+
if datetime.fromisoformat(i["departure"] if type == 1 else i["arrival"]).timestamp() > date.timestamp() # type: ignore
|
|
62
|
+
]
|
|
63
|
+
for train, type in trains:
|
|
64
|
+
time = (
|
|
65
|
+
f"[bold green]{train['departure'][11:16]}[/bold green]" # type: ignore
|
|
66
|
+
if type == 1
|
|
67
|
+
else f"[bold yellow]{train['arrival'][11:16]}[/bold yellow]" # type: ignore
|
|
68
|
+
)
|
|
69
|
+
brand = next(iter(i for i in brands if i["id"] == train["brand_id"]), {}).get("logo_text")
|
|
70
|
+
self.print(
|
|
71
|
+
f"{time} [red]{brand}[/red] {train["train_full_name"]}[purple] {train["stations"][0]["name"]} {self.format_position(train["platform"], train["track"])}[/purple]"
|
|
72
|
+
)
|
koleo/cli/stations.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from .base import BaseCli
|
|
2
|
+
from .utils import COUNTRY_MAP
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Stations(BaseCli):
|
|
6
|
+
async def find_station_view(self, query: str | None, type: str | None, country: str | None):
|
|
7
|
+
if query:
|
|
8
|
+
stations = await self.client.find_station(query)
|
|
9
|
+
else:
|
|
10
|
+
stations = self.storage.get_cache("stations") or self.storage.set_cache(
|
|
11
|
+
"stations", await self.client.get_stations()
|
|
12
|
+
)
|
|
13
|
+
for st in stations:
|
|
14
|
+
result_info = ""
|
|
15
|
+
if "country" in st:
|
|
16
|
+
if country:
|
|
17
|
+
c_info = COUNTRY_MAP[st["country"]]
|
|
18
|
+
if not c_info[0] == country:
|
|
19
|
+
continue
|
|
20
|
+
else:
|
|
21
|
+
c_info = COUNTRY_MAP[st["country"]]
|
|
22
|
+
result_info += c_info[1] if self.storage.use_country_flags_emoji else c_info[0]
|
|
23
|
+
if type:
|
|
24
|
+
if not st["type"].startswith(type):
|
|
25
|
+
continue
|
|
26
|
+
else:
|
|
27
|
+
if st["type"] == "Quay":
|
|
28
|
+
result_info += "🚏" if self.storage.use_station_type_emoji else "BUS"
|
|
29
|
+
elif st["type"] == "TopographicalPlace":
|
|
30
|
+
result_info += "🏛️" if self.storage.use_station_type_emoji else "GROUP"
|
|
31
|
+
else:
|
|
32
|
+
result_info += "🚉" if self.storage.use_station_type_emoji else "RAIL"
|
|
33
|
+
if result_info:
|
|
34
|
+
result_info += " "
|
|
35
|
+
self.print(
|
|
36
|
+
f"[bold blue][link=https://koleo.pl/dworzec-pkp/{st["name_slug"]}]{result_info}{st["name"]}[/bold blue] ID: {st["id"]}[/link]"
|
|
37
|
+
)
|
koleo/cli/train_info.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from .base import BaseCli
|
|
2
|
+
from asyncio import gather
|
|
3
|
+
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
from koleo.api.types import TrainCalendar, TrainDetailResponse, TrainStop
|
|
6
|
+
from koleo.utils import koleo_time_to_dt
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TrainInfo(BaseCli):
|
|
10
|
+
async def get_train_calendars(self, brand: str, name: str) -> list[TrainCalendar]:
|
|
11
|
+
brand = brand.upper().strip()
|
|
12
|
+
name_parts = name.split(" ")
|
|
13
|
+
if len(name_parts) == 1 and name_parts[0].isnumeric():
|
|
14
|
+
number = int(name_parts[0])
|
|
15
|
+
train_name = ""
|
|
16
|
+
elif len(name) > 1:
|
|
17
|
+
number = int(name_parts.pop(0))
|
|
18
|
+
train_name = " ".join(name_parts)
|
|
19
|
+
else:
|
|
20
|
+
raise ValueError("Invalid train name!")
|
|
21
|
+
brands = await self.get_brands()
|
|
22
|
+
if brand not in [i["name"] for i in brands]:
|
|
23
|
+
res = {i["logo_text"]: i["name"] for i in brands}.get(brand)
|
|
24
|
+
if not res:
|
|
25
|
+
raise ValueError("Invalid brand name!")
|
|
26
|
+
brand = res
|
|
27
|
+
cache_id = f"tc-{brand}-{number}-{name}"
|
|
28
|
+
try:
|
|
29
|
+
train_calendars = self.storage.get_cache(cache_id) or self.storage.set_cache(
|
|
30
|
+
cache_id, await self.client.get_train_calendars(brand, number, train_name)
|
|
31
|
+
)
|
|
32
|
+
except self.client.errors.KoleoNotFound:
|
|
33
|
+
await self.error_and_exit(f"Train not found: [underline]nr={number}, name={train_name}[/underline]")
|
|
34
|
+
return train_calendars["train_calendars"]
|
|
35
|
+
|
|
36
|
+
async def train_calendar_view(self, brand: str, name: str):
|
|
37
|
+
train_calendars = await self.get_train_calendars(brand, name)
|
|
38
|
+
brands = await self.get_brands()
|
|
39
|
+
for calendar in train_calendars:
|
|
40
|
+
brand_obj = next(iter(i for i in brands if i["id"] == calendar["trainBrand"]), {})
|
|
41
|
+
link = f"https://koleo.pl/pociag/{brand_obj["name"]}/{name.replace(" ", "-")}"
|
|
42
|
+
brand = brand_obj.get("logo_text", "")
|
|
43
|
+
self.print(
|
|
44
|
+
f"[red][link={link}]{brand}[/red] [bold blue]{calendar['train_nr']}{" "+ v if (v:=calendar.get("train_name")) else ""}[/bold blue]:[/link]"
|
|
45
|
+
)
|
|
46
|
+
for k, v in sorted(calendar["date_train_map"].items(), key=lambda x: datetime.strptime(x[0], "%Y-%m-%d")):
|
|
47
|
+
self.print(f" [bold green]{k}[/bold green]: [purple]{v}[/purple]")
|
|
48
|
+
|
|
49
|
+
async def train_info_view(
|
|
50
|
+
self, brand: str, name: str, date: datetime, closest: bool, show_stations: tuple[str, str] | None = None
|
|
51
|
+
):
|
|
52
|
+
train_calendars = await self.get_train_calendars(brand, name)
|
|
53
|
+
if closest:
|
|
54
|
+
dates = sorted([datetime.strptime(i, "%Y-%m-%d") for i in train_calendars[0]["dates"]])
|
|
55
|
+
date = next(iter(i for i in dates if i > date)) or next(iter(i for i in reversed(dates) if i < date))
|
|
56
|
+
if not (train_id := train_calendars[0]["date_train_map"].get(date.strftime("%Y-%m-%d"))):
|
|
57
|
+
await self.error_and_exit(
|
|
58
|
+
f"This train doesn't run on the selected date: [underline]{date.strftime("%Y-%m-%d")}[/underline]"
|
|
59
|
+
)
|
|
60
|
+
await self.train_detail_view(train_id, date=date.strftime("%Y-%m-%d"), show_stations=show_stations)
|
|
61
|
+
|
|
62
|
+
async def train_detail_view(
|
|
63
|
+
self, train_id: int, date: str | None = None, show_stations: tuple[str, str] | None = None
|
|
64
|
+
):
|
|
65
|
+
train_details = await self.client.get_train(train_id)
|
|
66
|
+
|
|
67
|
+
if show_stations:
|
|
68
|
+
first_stop_slug, last_stop_slug = [
|
|
69
|
+
i["name_slug"] for i in await gather(*(self.get_station(i) for i in show_stations))
|
|
70
|
+
]
|
|
71
|
+
train_stops_slugs = [i["station_slug"] for i in train_details["stops"]]
|
|
72
|
+
if first_stop_slug not in train_stops_slugs:
|
|
73
|
+
await self.error_and_exit(
|
|
74
|
+
f"Train [underline]{train_details["train"]["train_name"]}[/underline] doesn't stop at [underline]{first_stop_slug}[/underline]"
|
|
75
|
+
)
|
|
76
|
+
elif last_stop_slug not in train_stops_slugs:
|
|
77
|
+
await self.error_and_exit(
|
|
78
|
+
f"Train [underline]{train_details["train"]["train_name"]}[/underline] doesn't stop at [underline]{last_stop_slug}[/underline]"
|
|
79
|
+
)
|
|
80
|
+
first_stop, first_stop_index = next(
|
|
81
|
+
iter((i, n) for n, i in enumerate(train_details["stops"]) if i["station_slug"] == first_stop_slug)
|
|
82
|
+
)
|
|
83
|
+
last_stop, last_stop_index = next(
|
|
84
|
+
iter((i, n + 1) for n, i in enumerate(train_details["stops"]) if i["station_slug"] == last_stop_slug)
|
|
85
|
+
)
|
|
86
|
+
if first_stop_index >= last_stop_index:
|
|
87
|
+
await self.error_and_exit("Station B has to be after station A (-s / --show_stations)")
|
|
88
|
+
else:
|
|
89
|
+
first_stop, last_stop = train_details["stops"][0], train_details["stops"][-1]
|
|
90
|
+
first_stop_index, last_stop_index = 0, len(train_details["stops"]) + 1
|
|
91
|
+
|
|
92
|
+
await self.show_train_header(train_details, first_stop, last_stop, date)
|
|
93
|
+
self.train_route_table(train_details["stops"][first_stop_index:last_stop_index])
|
|
94
|
+
|
|
95
|
+
async def show_train_header(
|
|
96
|
+
self, train_details: TrainDetailResponse, first_stop: TrainStop, last_stop: TrainStop, date: str | None = None
|
|
97
|
+
):
|
|
98
|
+
brands = await self.get_brands()
|
|
99
|
+
brand_obj = next(iter(i for i in brands if i["id"] == train_details["train"]["brand_id"]), {})
|
|
100
|
+
brand = brand_obj.get("logo_text", "")
|
|
101
|
+
|
|
102
|
+
link = (
|
|
103
|
+
f"https://koleo.pl/pociag/{brand_obj["name"]}/{train_details["train"]["train_full_name"].replace(" ", "-")}"
|
|
104
|
+
)
|
|
105
|
+
if date:
|
|
106
|
+
link += f"/{date}"
|
|
107
|
+
self.print(
|
|
108
|
+
f"[link={link}][red]{brand}[/red] [bold blue]{train_details["train"]["train_full_name"]}[/bold blue][/link]"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if train_details["train"]["run_desc"]:
|
|
112
|
+
self.print(f" {train_details["train"]["run_desc"]}")
|
|
113
|
+
|
|
114
|
+
route_start = koleo_time_to_dt(first_stop["departure"])
|
|
115
|
+
route_end = koleo_time_to_dt(last_stop["arrival"])
|
|
116
|
+
|
|
117
|
+
if route_start.day == route_end.day and (
|
|
118
|
+
route_end.hour < route_start.hour
|
|
119
|
+
or (route_end.hour == route_start.hour and route_end.minute < route_end.minute)
|
|
120
|
+
):
|
|
121
|
+
route_end += timedelta(days=1)
|
|
122
|
+
|
|
123
|
+
travel_time = route_end - route_start
|
|
124
|
+
speed = (last_stop["distance"] - first_stop["distance"]) / 1000 / travel_time.seconds * 3600
|
|
125
|
+
self.print(
|
|
126
|
+
f"[white] {travel_time.seconds//3600}h{(travel_time.seconds % 3600)/60:.0f}m {speed:^4.1f}km/h [/white]"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
vehicle_types: dict[str, str] = {
|
|
130
|
+
stop["station_display_name"]: stop["vehicle_type"]
|
|
131
|
+
for stop in train_details["stops"]
|
|
132
|
+
if stop["vehicle_type"]
|
|
133
|
+
and (koleo_time_to_dt(stop["departure"]) >= route_start and koleo_time_to_dt(stop["arrival"]) <= route_end)
|
|
134
|
+
}
|
|
135
|
+
if vehicle_types:
|
|
136
|
+
keys = list(vehicle_types.keys())
|
|
137
|
+
start = keys[0]
|
|
138
|
+
for i in range(1, len(keys)):
|
|
139
|
+
if vehicle_types[keys[i]] != vehicle_types[start]:
|
|
140
|
+
self.print(f" {start} - {keys[i]}: [bold green]{vehicle_types[start]}[/bold green]")
|
|
141
|
+
start = keys[i]
|
|
142
|
+
self.print(f" {start} - {keys[-1]}: [bold green]{vehicle_types[start]}[/bold green]")
|
koleo/cli/utils.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from koleo.api.types import Price
|
|
2
|
+
|
|
3
|
+
CLASS_COLOR_MAP = {
|
|
4
|
+
"Klasa 2": "bright_green",
|
|
5
|
+
"Economy": "bright_green",
|
|
6
|
+
"Economy Plus": "bright_green",
|
|
7
|
+
"Klasa 1": "bright_cyan",
|
|
8
|
+
"Business": "bright_cyan",
|
|
9
|
+
"Premium": "bright_cyan",
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
COUNTRY_MAP = {
|
|
14
|
+
"Słowacja": ("sk", "🇸🇰"),
|
|
15
|
+
"Ukraina": ("ua ", "🇺🇦"),
|
|
16
|
+
"Niemcy": ("de", "🇩🇪"),
|
|
17
|
+
"Czechy": ("cz", "🇨🇿"),
|
|
18
|
+
"Polska": ("pl", "🇵🇱"),
|
|
19
|
+
"Litwa": ("lt", "🇱🇹"),
|
|
20
|
+
"": ("pl", "🇵🇱"), # we have to assume...
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def format_price(price: str | Price):
|
|
25
|
+
if isinstance(price, dict):
|
|
26
|
+
price = price["value"]
|
|
27
|
+
return f"{float(price):.2f} zł"
|
koleo/storage.py
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import typing as t
|
|
2
2
|
from dataclasses import asdict, dataclass, field
|
|
3
|
-
from
|
|
3
|
+
from orjson import dumps, loads
|
|
4
4
|
from os import makedirs
|
|
5
5
|
from os import path as ospath
|
|
6
6
|
from sys import platform
|
|
7
7
|
from time import time
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
if t.TYPE_CHECKING:
|
|
11
|
+
from yt_dlp.cookies import YoutubeDLCookieJar
|
|
12
|
+
|
|
13
|
+
|
|
10
14
|
def get_adequate_config_path() -> str:
|
|
11
15
|
if platform == "darwin":
|
|
12
16
|
# i dont fucking know nor want to
|
|
@@ -23,12 +27,54 @@ DEFAULT_CONFIG_PATH = get_adequate_config_path()
|
|
|
23
27
|
T = t.TypeVar("T")
|
|
24
28
|
|
|
25
29
|
|
|
30
|
+
@dataclass
|
|
31
|
+
class Auth:
|
|
32
|
+
def __post_init__(self):
|
|
33
|
+
self._cache: dict | None = None
|
|
34
|
+
|
|
35
|
+
type: t.Literal["text", "command", "yt-dlp-browser"]
|
|
36
|
+
data: str
|
|
37
|
+
|
|
38
|
+
def get_auth(self) -> dict[str, str]:
|
|
39
|
+
if self._cache:
|
|
40
|
+
return self._cache
|
|
41
|
+
if self.type == "yt-dlp-browser":
|
|
42
|
+
try:
|
|
43
|
+
from yt_dlp.cookies import extract_cookies_from_browser
|
|
44
|
+
except ImportError as e:
|
|
45
|
+
raise ImportError(
|
|
46
|
+
"This feature requires the 'yt-dlp' package. " "Please install it with 'pip install yt-dlp'."
|
|
47
|
+
) from e
|
|
48
|
+
browser, _, profile = self.data.partition(",")
|
|
49
|
+
cookies: "YoutubeDLCookieJar" = extract_cookies_from_browser(browser, profile or None)
|
|
50
|
+
self._cache = {
|
|
51
|
+
cookie.name: cookie.value
|
|
52
|
+
for cookie in cookies
|
|
53
|
+
if (cookie.domain == "koleo.pl" or cookie.domain.endswith(".koleo.pl")) and cookie.value
|
|
54
|
+
}
|
|
55
|
+
elif self.type == "command":
|
|
56
|
+
from subprocess import run
|
|
57
|
+
|
|
58
|
+
process = run(self.data, capture_output=True)
|
|
59
|
+
self._cache = loads(process.stdout)
|
|
60
|
+
elif self.type == "str":
|
|
61
|
+
self._cache = t.cast(dict[str, str], loads(self.data))
|
|
62
|
+
else:
|
|
63
|
+
raise ValueError(f"invalid auth.type: {self.type}")
|
|
64
|
+
return self._cache # type: ignore
|
|
65
|
+
|
|
66
|
+
|
|
26
67
|
@dataclass
|
|
27
68
|
class Storage:
|
|
28
|
-
favourite_station: str | None = None
|
|
29
69
|
cache: dict[str, tuple[int, t.Any]] = field(default_factory=dict)
|
|
70
|
+
favourite_station: str | None = None
|
|
30
71
|
disable_cache: bool = False
|
|
31
72
|
use_roman_numerals: bool = False
|
|
73
|
+
aliases: dict[str, str] = field(default_factory=dict)
|
|
74
|
+
show_connection_id: bool = False
|
|
75
|
+
use_country_flags_emoji: bool = True
|
|
76
|
+
use_station_type_emoji: bool = True
|
|
77
|
+
auth: Auth | None = None
|
|
32
78
|
|
|
33
79
|
def __post_init__(self):
|
|
34
80
|
self._path: str
|
|
@@ -43,7 +89,7 @@ class Storage:
|
|
|
43
89
|
expanded = ospath.expanduser(path)
|
|
44
90
|
if ospath.exists(expanded):
|
|
45
91
|
with open(expanded) as f:
|
|
46
|
-
data =
|
|
92
|
+
data = {k: v for k, v in loads(f.read()).items() if k in cls.__dataclass_fields__}
|
|
47
93
|
else:
|
|
48
94
|
data = {}
|
|
49
95
|
storage = cls(**data)
|
|
@@ -70,18 +116,26 @@ class Storage:
|
|
|
70
116
|
self._dirty = True
|
|
71
117
|
return item
|
|
72
118
|
|
|
119
|
+
def clean_cache(self):
|
|
120
|
+
now = time()
|
|
121
|
+
copy = self.cache.copy()
|
|
122
|
+
self.cache = {k: data for k, data in copy.items() if data[0] > now}
|
|
123
|
+
if copy != self.cache:
|
|
124
|
+
self._dirty = True
|
|
125
|
+
|
|
73
126
|
def save(self):
|
|
74
127
|
dir = ospath.dirname(self._path)
|
|
75
128
|
if dir:
|
|
76
129
|
if not ospath.exists(dir):
|
|
77
130
|
makedirs(dir)
|
|
78
|
-
with open(self._path, "
|
|
79
|
-
self.
|
|
80
|
-
|
|
131
|
+
with open(self._path, "wb") as f:
|
|
132
|
+
self.clean_cache()
|
|
133
|
+
f.write(dumps(asdict(self)))
|
|
81
134
|
|
|
82
|
-
def
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
135
|
+
def add_alias(self, alias: str, station: str):
|
|
136
|
+
self.aliases[alias] = station
|
|
137
|
+
self._dirty = True
|
|
138
|
+
|
|
139
|
+
def remove_alias(self, alias: str):
|
|
140
|
+
self.aliases.pop(alias, None)
|
|
141
|
+
self._dirty = True
|
koleo/utils.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from argparse import Action
|
|
2
2
|
from datetime import datetime, time, timedelta
|
|
3
3
|
|
|
4
|
-
from .types import TimeDict
|
|
4
|
+
from .api.types import TimeDict, SeatsAvailabilityResponse, TrainComposition, CarriageType
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
def parse_datetime(s: str):
|
|
@@ -15,22 +15,47 @@ def parse_datetime(s: str):
|
|
|
15
15
|
return datetime.strptime(s, "%Y-%m-%d").replace(hour=0, minute=0)
|
|
16
16
|
except ValueError:
|
|
17
17
|
pass
|
|
18
|
-
if s[0]
|
|
19
|
-
|
|
18
|
+
if s[0] in "+-":
|
|
19
|
+
to_zero = []
|
|
20
|
+
num = int("".join([i for i in s if i.isnumeric()]))
|
|
21
|
+
if s[-1] == "h":
|
|
22
|
+
to_zero = ["minute"]
|
|
23
|
+
td = timedelta(hours=num)
|
|
24
|
+
elif s[-1] == "m":
|
|
25
|
+
td = timedelta(minutes=num)
|
|
26
|
+
else:
|
|
27
|
+
to_zero = ["hour", "minute"]
|
|
28
|
+
td = timedelta(days=num)
|
|
29
|
+
start = datetime.now()
|
|
30
|
+
if s[0] != s[1]:
|
|
31
|
+
start = start.replace(**{k: 0 for k in to_zero}) # type: ignore
|
|
32
|
+
if s[0] == "+":
|
|
33
|
+
return start + td
|
|
34
|
+
if s[0] == "-":
|
|
35
|
+
return start - td
|
|
36
|
+
if s[0] == "-":
|
|
37
|
+
return datetime.now().replace(hour=0, minute=0) - timedelta(days=int(s[1:]))
|
|
20
38
|
try:
|
|
21
39
|
now = datetime.now()
|
|
22
40
|
dt = datetime.strptime(s, "%d-%m %H:%M")
|
|
23
41
|
return dt.replace(year=now.year)
|
|
24
42
|
except ValueError:
|
|
25
43
|
pass
|
|
26
|
-
|
|
44
|
+
try:
|
|
45
|
+
now = datetime.now()
|
|
46
|
+
dt = datetime.strptime(s, "%H:%M %d-%m")
|
|
47
|
+
return dt.replace(year=now.year)
|
|
48
|
+
except ValueError:
|
|
49
|
+
pass
|
|
50
|
+
return datetime.combine(datetime.today(), datetime.strptime(s, "%H:%M").time())
|
|
27
51
|
|
|
28
52
|
|
|
29
|
-
def
|
|
53
|
+
def koleo_time_to_dt(i: TimeDict | str, *, base_date: datetime | None = None):
|
|
30
54
|
if isinstance(i, str):
|
|
31
55
|
return datetime.fromisoformat(i)
|
|
32
|
-
|
|
33
|
-
|
|
56
|
+
if not base_date:
|
|
57
|
+
base_date = datetime.today()
|
|
58
|
+
return datetime.combine(base_date, time(i["hour"], i["minute"], i["second"]))
|
|
34
59
|
|
|
35
60
|
|
|
36
61
|
TRANSLITERATIONS = {
|
|
@@ -69,7 +94,7 @@ NUMERAL_TO_ARABIC = {
|
|
|
69
94
|
}
|
|
70
95
|
|
|
71
96
|
|
|
72
|
-
def convert_platform_number(number: str
|
|
97
|
+
def convert_platform_number(number: str) -> int | None | str:
|
|
73
98
|
if number and number[-1] in "abcdefghi": # just to be safe...
|
|
74
99
|
arabic = NUMERAL_TO_ARABIC.get(number[:-1])
|
|
75
100
|
return f"{arabic}{number[-1]}" if arabic else None
|
|
@@ -79,3 +104,65 @@ def convert_platform_number(number: str | None) -> int | None | str:
|
|
|
79
104
|
class RemainderString(Action):
|
|
80
105
|
def __call__(self, _, namespace, values: list[str], __):
|
|
81
106
|
setattr(namespace, self.dest, " ".join(values) if values else self.default)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
BRAND_SEAT_TYPE_MAPPING = {
|
|
110
|
+
45: {30: "Klasa 2", 31: "Z rowerem"}, # kd premium
|
|
111
|
+
28: {4: "Klasa 1", 5: "Klasa 2"}, # ic
|
|
112
|
+
1: {4: "Klasa 1", 5: "Klasa 2"}, # tlk
|
|
113
|
+
29: {4: "Klasa 1", 5: "Klasa 2"}, # eip
|
|
114
|
+
2: {4: "Klasa 1", 5: "Klasa 2"}, # eic
|
|
115
|
+
43: { # łs
|
|
116
|
+
11: "Klasa 2",
|
|
117
|
+
},
|
|
118
|
+
47: { # leo
|
|
119
|
+
17: "Economy",
|
|
120
|
+
18: "Business",
|
|
121
|
+
19: "Premium",
|
|
122
|
+
20: "Economy Plus",
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def find_empty_compartments(seats: SeatsAvailabilityResponse, composition: TrainComposition) -> list[tuple[int, int]]:
|
|
128
|
+
num_taken_seats_in_group: dict[tuple[int, int], int] = {}
|
|
129
|
+
for seat in seats["seats"]:
|
|
130
|
+
key = (int(seat["carriage_nr"]), int(seat["seat_nr"][:-1]))
|
|
131
|
+
num_taken_seats_in_group.setdefault(key, 0)
|
|
132
|
+
if seat["state"] != "FREE":
|
|
133
|
+
num_taken_seats_in_group[key] += 1
|
|
134
|
+
return [k for k, v in num_taken_seats_in_group.items() if v == 0]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
SEAT_GROUPS = {
|
|
138
|
+
1: 1,
|
|
139
|
+
3: 1,
|
|
140
|
+
2: 2,
|
|
141
|
+
8: 2,
|
|
142
|
+
5: 3,
|
|
143
|
+
7: 3,
|
|
144
|
+
4: 4,
|
|
145
|
+
6: 4,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def get_double_key(seat: int) -> tuple[int, int]:
|
|
150
|
+
# x1, x3 -> x, 1
|
|
151
|
+
# x2, x8 -> x, 2
|
|
152
|
+
# x5, x7 -> x, 3
|
|
153
|
+
# x4, x6 -> x, 4
|
|
154
|
+
# x0, x9 nie istnieją!
|
|
155
|
+
seat_nr = str(seat)
|
|
156
|
+
return int(seat_nr[:-1]), SEAT_GROUPS[int(seat_nr[-1])]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def find_empty_doubles(
|
|
160
|
+
seats: SeatsAvailabilityResponse,
|
|
161
|
+
):
|
|
162
|
+
num_taken_seats_in_double: dict[tuple[int, int, int], int] = {}
|
|
163
|
+
for seat in seats["seats"]:
|
|
164
|
+
key = (int(seat["carriage_nr"]), *get_double_key(int(seat["seat_nr"])))
|
|
165
|
+
num_taken_seats_in_double.setdefault(key, 0)
|
|
166
|
+
if seat["state"] != "FREE":
|
|
167
|
+
num_taken_seats_in_double[key] += 1
|
|
168
|
+
return [k for k, v in num_taken_seats_in_double.items() if v == 0]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: koleo-cli
|
|
3
|
-
Version: 0.2.137.
|
|
3
|
+
Version: 0.2.137.18
|
|
4
4
|
Summary: Koleo CLI
|
|
5
5
|
Home-page: https://github.com/lzgirlcat/koleo-cli
|
|
6
6
|
Author: Zoey !
|
|
@@ -14,8 +14,9 @@ Classifier: Operating System :: OS Independent
|
|
|
14
14
|
Requires-Python: >=3.12
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
License-File: LICENSE
|
|
17
|
-
Requires-Dist: rich~=
|
|
18
|
-
Requires-Dist:
|
|
17
|
+
Requires-Dist: rich~=14.0.0
|
|
18
|
+
Requires-Dist: aiohttp~=3.12.13
|
|
19
|
+
Requires-Dist: orjson~=3.10.18
|
|
19
20
|
Dynamic: author
|
|
20
21
|
Dynamic: classifier
|
|
21
22
|
Dynamic: description
|
|
@@ -46,39 +47,57 @@ Dynamic: summary
|
|
|
46
47
|
- find a station or list all known stations
|
|
47
48
|
- find a connection from station a to b, with filtering by operators
|
|
48
49
|
- save a station as your favourite to quickly check it's departures
|
|
50
|
+
- add station aliases to query them more easily
|
|
51
|
+
- check seat allocation statistics
|
|
49
52
|
|
|
53
|
+
### coming soon™️:
|
|
54
|
+
- TUI ticket purchase interface
|
|
55
|
+
- ticket display
|
|
56
|
+
- your previous tickets + stats
|
|
57
|
+
- find empty compartments
|
|
50
58
|
additionally you can also use the KoleoAPI wrapper directly in your own projects, all returns are fully typed using `typing.TypedDict`
|
|
51
59
|
|
|
52
60
|
## MY(possibly controversial) design choices:
|
|
53
61
|
- platforms and track numbers are shown using arabic numerals instead of roman
|
|
54
|
-
- you can change it by adding `use_roman_numerals: true` to your
|
|
62
|
+
- you can change it by adding `use_roman_numerals: true` to your `koleo-cli.json` config file
|
|
55
63
|
- most api queries are cached for 24h
|
|
56
|
-
- you can change it by adding `disable_cache: true` to your
|
|
57
|
-
-
|
|
58
|
-
|
|
64
|
+
- you can change it by adding `disable_cache: true` to your `koleo-cli.json` config file
|
|
65
|
+
- stations/ls uses emojis by default
|
|
66
|
+
- you can disable them by adding `use_country_flags_emoji: false` and `use_country_flags_emoji: false` to your `koleo-cli.json` config file
|
|
59
67
|
pull requests are welcome!!
|
|
60
68
|
|
|
61
69
|
```
|
|
62
|
-
usage: koleo [-h] [-c CONFIG] [--nocolor]
|
|
70
|
+
usage: koleo [-h] [-c CONFIG] [--nocolor]
|
|
71
|
+
{departures,d,dep,odjazdy,o,arrivals,a,arr,przyjazdy,p,all,w,wszystkie,all_trains,pociagi,trainroute,r,tr,t,poc,pociąg,traincalendar,kursowanie,tc,k,traindetail,td,tid,id,idpoc,stations,s,find,f,stacje,ls,q,connections,do,z,szukaj,path,trainstats,ts,tp,miejsca,frekwencja,trainconnectionstats,tcs,aliases} ...
|
|
63
72
|
|
|
64
73
|
Koleo CLI
|
|
65
74
|
|
|
66
75
|
options:
|
|
67
76
|
-h, --help show this help message and exit
|
|
68
|
-
-c
|
|
69
|
-
Custom config path.
|
|
77
|
+
-c, --config CONFIG Custom config path.
|
|
70
78
|
--nocolor Disable color output and formatting
|
|
71
79
|
|
|
72
80
|
actions:
|
|
73
|
-
{departures,d,dep,odjazdy,o,arrivals,a,arr,przyjazdy,p,trainroute,r,tr,t,poc,pociąg,stations,s,find,f,stacje,ls,connections,do,z,szukaj,path}
|
|
81
|
+
{departures,d,dep,odjazdy,o,arrivals,a,arr,przyjazdy,p,all,w,wszystkie,all_trains,pociagi,trainroute,r,tr,t,poc,pociąg,traincalendar,kursowanie,tc,k,traindetail,td,tid,id,idpoc,stations,s,find,f,stacje,ls,q,connections,do,z,szukaj,path,trainstats,ts,tp,miejsca,frekwencja,trainconnectionstats,tcs,aliases}
|
|
74
82
|
departures (d, dep, odjazdy, o)
|
|
75
83
|
Allows you to list station departures
|
|
76
84
|
arrivals (a, arr, przyjazdy, p)
|
|
77
85
|
Allows you to list station departures
|
|
86
|
+
all (w, wszystkie, all_trains, pociagi)
|
|
87
|
+
Allows you to list all station trains
|
|
78
88
|
trainroute (r, tr, t, poc, pociąg)
|
|
79
|
-
Allows you to
|
|
80
|
-
|
|
89
|
+
Allows you to check the train's route
|
|
90
|
+
traincalendar (kursowanie, tc, k)
|
|
91
|
+
Allows you to check what days the train runs on
|
|
92
|
+
traindetail (td, tid, id, idpoc)
|
|
93
|
+
Allows you to show the train's route given it's koleo ID
|
|
94
|
+
stations (s, find, f, stacje, ls, q)
|
|
81
95
|
Allows you to find stations by their name
|
|
82
96
|
connections (do, z, szukaj, path)
|
|
83
97
|
Allows you to search for connections from a to b
|
|
98
|
+
trainstats (ts, tp, miejsca, frekwencja)
|
|
99
|
+
Allows you to check seat allocation info for a train.
|
|
100
|
+
trainconnectionstats (tcs)
|
|
101
|
+
Allows you to check the seat allocations on the train connection given it's koleo ID
|
|
102
|
+
aliases Save quick aliases for station names!
|
|
84
103
|
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
koleo/__init__.py,sha256=dp6PfGcuWP50QBWhjnT-zS03l3KzzYnMJnUeYMKBayc,51
|
|
2
|
+
koleo/__main__.py,sha256=wu5N2wk8mvBgyvr2ghmQf4prezAe0_i-p123VVreyYc,62
|
|
3
|
+
koleo/args.py,sha256=gncN8HA8a67U1Wpt3lc32bvPBydn0uCkSmiCZNVQRGk,10275
|
|
4
|
+
koleo/storage.py,sha256=pffcqpTakYp8o6dbVMkD5oF13R6L2eQR3ambU8qTRuM,4345
|
|
5
|
+
koleo/utils.py,sha256=OAjm90SN1BqpgNF8OTAaR9atyLNYIGy5owZ2pLP5OAM,4631
|
|
6
|
+
koleo/api/__init__.py,sha256=3TfO8eBJcDCDXvIFobgyvN3Sfa1kQ2U0lm7gxEAsaWQ,50
|
|
7
|
+
koleo/api/base.py,sha256=8wAasDQ1DBDI1PsPESmS3iUE5csIgaGT4DOX_A-hPwI,2070
|
|
8
|
+
koleo/api/client.py,sha256=Ru5M5ntp2vpQQI4VUaD1TCLIjNtJB4tkNCB4y766DUU,8457
|
|
9
|
+
koleo/api/errors.py,sha256=J9g75K9_yvnZ-7Hi521h0yGcw9NUNnTprQsIZ_mclO0,1324
|
|
10
|
+
koleo/api/logging.py,sha256=VhOFY6N_mofFfg9ZJ5ZnDVCzNuRilqLqrzZU9b-hpZY,1644
|
|
11
|
+
koleo/api/types.py,sha256=udCQeDjRvUqrUkuE_-Jidzsq_FOCjNyGd_bQWm3f9iA,10594
|
|
12
|
+
koleo/cli/__init__.py,sha256=50B_lMtZS27_FVrkIa2TOWMX7SozG1CPPMxbY1W9tqk,266
|
|
13
|
+
koleo/cli/aliases.py,sha256=is3NX5g0-ujJTmyjlFLoNnK_pwoYUVIojWNkgwXRBKk,581
|
|
14
|
+
koleo/cli/base.py,sha256=Rdv8FwenM0ymSHNrW9faGYI6mr65EL-n24bN5YXfLkg,4261
|
|
15
|
+
koleo/cli/connections.py,sha256=LJzoq3ADVUmBeqF4-h_me3pZbBHYg1sG3MaDYprwuLU,6873
|
|
16
|
+
koleo/cli/seats.py,sha256=hIALtopQPD-BcBGI_XpPo3ht6f_D5e16VpI7mFgN0Ao,5477
|
|
17
|
+
koleo/cli/station_board.py,sha256=lRGIeEKEOvUcctM6cwmqVE7Yk9m9R12gfFZahlWLQFc,3669
|
|
18
|
+
koleo/cli/stations.py,sha256=6L3PBWc6xssyR9eeLacuvGBJmaY7Ny3DalKy2Xq7zsA,1633
|
|
19
|
+
koleo/cli/train_info.py,sha256=XS_G4dsevHBPuGSpsbNTh_adSLyedame7WVe71oIe5M,7259
|
|
20
|
+
koleo/cli/utils.py,sha256=FzPGcJdwRwcz10mYiW63Y4zpjM9j6DzNH91UNR3MS8s,666
|
|
21
|
+
koleo_cli-0.2.137.18.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
22
|
+
koleo_cli-0.2.137.18.dist-info/METADATA,sha256=JLxhaXfSUCa7zhq12sGS8LLP94BvnW2Tc3DoFOk9NYU,4731
|
|
23
|
+
koleo_cli-0.2.137.18.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
24
|
+
koleo_cli-0.2.137.18.dist-info/entry_points.txt,sha256=ouWbMR_XWpEwV7zfkFKeiFLe_IMP-47kTvVahgy4PRg,42
|
|
25
|
+
koleo_cli-0.2.137.18.dist-info/top_level.txt,sha256=AlWdXotkRYzHpFfOBYi6xOXl1H0zq4-tqtZ2XivoWB4,6
|
|
26
|
+
koleo_cli-0.2.137.18.dist-info/RECORD,,
|