koleo-cli 0.2.137.17__py3-none-any.whl → 0.2.137.19__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.

@@ -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
+ )
@@ -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 json import dump, load
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 = load(f)
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, "w+") as f:
79
- self.clean()
80
- dump(asdict(self), f, indent=True)
131
+ with open(self._path, "wb") as f:
132
+ self.clean_cache()
133
+ f.write(dumps(asdict(self)))
81
134
 
82
- def clean(self):
83
- now = time()
84
- copy = self.cache.copy()
85
- self.cache = {k: data for k, data in copy.items() if data[0] > now}
86
- if copy != self.cache:
87
- self._dirty = True
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
- return datetime.now().replace(hour=0, minute=0) + timedelta(days=int(s[1:]))
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
- return datetime.combine(datetime.now(), datetime.strptime(s, "%H:%M").time())
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 arr_dep_to_dt(i: TimeDict | str):
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
- now = datetime.today()
33
- return datetime.combine(now, time(i["hour"], i["minute"], i["second"]))
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 | None) -> int | None | 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.17
3
+ Version: 0.2.137.19
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~=13.7
18
- Requires-Dist: requests~=2.32
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 config.json file
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 config.json file
57
- - the cli.py code is really dirty but printing formatted data is hard :<
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] {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} ...
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 CONFIG, --config CONFIG
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 show the train's route
80
- stations (s, find, f, stacje, ls)
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=zRwdn7wRjqzWyDIoQ8PDDd_K2GizFIto35uO-mGFwsQ,63
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.19.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
22
+ koleo_cli-0.2.137.19.dist-info/METADATA,sha256=Ewvfre33HQFcAPNrlK_KmD8teeBHXJf_-ZxT4DLX5iA,4731
23
+ koleo_cli-0.2.137.19.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
24
+ koleo_cli-0.2.137.19.dist-info/entry_points.txt,sha256=ouWbMR_XWpEwV7zfkFKeiFLe_IMP-47kTvVahgy4PRg,42
25
+ koleo_cli-0.2.137.19.dist-info/top_level.txt,sha256=AlWdXotkRYzHpFfOBYi6xOXl1H0zq4-tqtZ2XivoWB4,6
26
+ koleo_cli-0.2.137.19.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (78.1.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ koleo = koleo.args:main