koleo-cli 0.2.137.16__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.

@@ -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,21 +1,24 @@
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
- def get_adequate_config_path():
10
+ if t.TYPE_CHECKING:
11
+ from yt_dlp.cookies import YoutubeDLCookieJar
12
+
13
+
14
+ def get_adequate_config_path() -> str:
11
15
  if platform == "darwin":
12
16
  # i dont fucking know nor want to
13
17
  return "~/Library/Preferences/koleo-cli/data.json"
14
- elif "win" in platform:
18
+ if "win" in platform:
15
19
  # same with this
16
20
  return "%USERPROFILE%\\AppData\\Local\\koleo-cli\\data.json"
17
- else:
18
- return "~/.config/koleo-cli.json"
21
+ return "~/.config/koleo-cli.json"
19
22
 
20
23
 
21
24
  DEFAULT_CONFIG_PATH = get_adequate_config_path()
@@ -24,23 +27,69 @@ DEFAULT_CONFIG_PATH = get_adequate_config_path()
24
27
  T = t.TypeVar("T")
25
28
 
26
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
+
27
67
  @dataclass
28
68
  class Storage:
29
- favourite_station: str | None = None
30
69
  cache: dict[str, tuple[int, t.Any]] = field(default_factory=dict)
70
+ favourite_station: str | None = None
31
71
  disable_cache: bool = False
32
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
33
78
 
34
79
  def __post_init__(self):
35
80
  self._path: str
36
81
  self._dirty = False
37
82
 
83
+ @property
84
+ def dirty(self) -> bool:
85
+ return self._dirty
86
+
38
87
  @classmethod
39
88
  def load(cls, *, path: str = DEFAULT_CONFIG_PATH) -> t.Self:
40
89
  expanded = ospath.expanduser(path)
41
90
  if ospath.exists(expanded):
42
- with open(expanded, "r") as f:
43
- data = load(f)
91
+ with open(expanded) as f:
92
+ data = {k: v for k, v in loads(f.read()).items() if k in cls.__dataclass_fields__}
44
93
  else:
45
94
  data = {}
46
95
  storage = cls(**data)
@@ -49,10 +98,10 @@ class Storage:
49
98
 
50
99
  def get_cache(self, id: str) -> t.Any | None:
51
100
  if self.disable_cache:
52
- return
101
+ return None
53
102
  cache_result = self.cache.get(id)
54
103
  if not cache_result:
55
- return
104
+ return None
56
105
  expiry, item = cache_result
57
106
  if expiry > time():
58
107
  return item
@@ -67,18 +116,26 @@ class Storage:
67
116
  self._dirty = True
68
117
  return item
69
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
+
70
126
  def save(self):
71
127
  dir = ospath.dirname(self._path)
72
128
  if dir:
73
129
  if not ospath.exists(dir):
74
130
  makedirs(dir)
75
- with open(self._path, "w+") as f:
76
- self.clean()
77
- dump(asdict(self), f, indent=True)
131
+ with open(self._path, "wb") as f:
132
+ self.clean_cache()
133
+ f.write(dumps(asdict(self)))
78
134
 
79
- def clean(self):
80
- now = time()
81
- copy = self.cache.copy()
82
- self.cache = {k: data for k, data in copy.items() if data[0] > now}
83
- if copy != self.cache:
84
- 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]
@@ -0,0 +1,103 @@
1
+ Metadata-Version: 2.4
2
+ Name: koleo-cli
3
+ Version: 0.2.137.18
4
+ Summary: Koleo CLI
5
+ Home-page: https://github.com/lzgirlcat/koleo-cli
6
+ Author: Zoey !
7
+ Maintainer-email: cb98uzhd@duck.com
8
+ License: GNU General Public License v3.0
9
+ Project-URL: Source (GitHub), https://github.com/lzgirlcat/koleo-cli
10
+ Project-URL: Issue Tracker, https://github.com/lzgirlcat/koleo-cli/issues
11
+ Keywords: koleo,timetable,trains,rail,poland
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Operating System :: OS Independent
14
+ Requires-Python: >=3.12
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: rich~=14.0.0
18
+ Requires-Dist: aiohttp~=3.12.13
19
+ Requires-Dist: orjson~=3.10.18
20
+ Dynamic: author
21
+ Dynamic: classifier
22
+ Dynamic: description
23
+ Dynamic: description-content-type
24
+ Dynamic: home-page
25
+ Dynamic: keywords
26
+ Dynamic: license
27
+ Dynamic: license-file
28
+ Dynamic: maintainer-email
29
+ Dynamic: project-url
30
+ Dynamic: requires-dist
31
+ Dynamic: requires-python
32
+ Dynamic: summary
33
+
34
+ # Koleo CLI
35
+ [![PyPI - Version](https://img.shields.io/pypi/v/koleo-cli.svg)](https://pypi.org/project/koleo-cli)
36
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/koleo-cli.svg)](https://pypi.org/project/koleo-cli)
37
+
38
+ ## Installation
39
+ **install via pip by running** `pip install koleo-cli`
40
+
41
+
42
+ ![gif showcasing the functionality](https://github.com/lzgirlcat/koleo-cli/blob/main/koleo-cli.gif?raw=true)
43
+
44
+ ## it currently allows you to:
45
+ - get departures/arrival list for a station
46
+ - get train info given its number and name(pull requests are welcome if you know how to get a train object by just the number)
47
+ - find a station or list all known stations
48
+ - find a connection from station a to b, with filtering by operators
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
52
+
53
+ ### coming soon™️:
54
+ - TUI ticket purchase interface
55
+ - ticket display
56
+ - your previous tickets + stats
57
+ - find empty compartments
58
+ additionally you can also use the KoleoAPI wrapper directly in your own projects, all returns are fully typed using `typing.TypedDict`
59
+
60
+ ## MY(possibly controversial) design choices:
61
+ - platforms and track numbers are shown using arabic numerals instead of roman
62
+ - you can change it by adding `use_roman_numerals: true` to your `koleo-cli.json` config file
63
+ - most api queries are cached for 24h
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
67
+ pull requests are welcome!!
68
+
69
+ ```
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} ...
72
+
73
+ Koleo CLI
74
+
75
+ options:
76
+ -h, --help show this help message and exit
77
+ -c, --config CONFIG Custom config path.
78
+ --nocolor Disable color output and formatting
79
+
80
+ actions:
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}
82
+ departures (d, dep, odjazdy, o)
83
+ Allows you to list station departures
84
+ arrivals (a, arr, przyjazdy, p)
85
+ Allows you to list station departures
86
+ all (w, wszystkie, all_trains, pociagi)
87
+ Allows you to list all station trains
88
+ trainroute (r, tr, t, poc, pociąg)
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)
95
+ Allows you to find stations by their name
96
+ connections (do, z, szukaj, path)
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!
103
+ ```