koleo-cli 0.2.137__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 +4 -0
- koleo/__main__.py +5 -0
- koleo/api.py +150 -0
- koleo/cli.py +231 -0
- koleo/storage.py +71 -0
- koleo/types.py +177 -0
- koleo/utils.py +41 -0
- koleo_cli-0.2.137.dist-info/METADATA +18 -0
- koleo_cli-0.2.137.dist-info/RECORD +12 -0
- koleo_cli-0.2.137.dist-info/WHEEL +5 -0
- koleo_cli-0.2.137.dist-info/entry_points.txt +2 -0
- koleo_cli-0.2.137.dist-info/top_level.txt +1 -0
koleo/__init__.py
ADDED
koleo/__main__.py
ADDED
koleo/api.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
from requests import Session, PreparedRequest, Response
|
|
5
|
+
|
|
6
|
+
from koleo.types import *
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class KoleoAPIException(Exception):
|
|
10
|
+
status: int
|
|
11
|
+
request: PreparedRequest
|
|
12
|
+
response: Response
|
|
13
|
+
|
|
14
|
+
def __init__(self, response: Response, *args: object) -> None:
|
|
15
|
+
super().__init__(*args)
|
|
16
|
+
self.status = response.status_code
|
|
17
|
+
self.request = response.request
|
|
18
|
+
self.response = response
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def from_response(cls, response: Response) -> "KoleoAPIException":
|
|
22
|
+
if response.status_code == 404:
|
|
23
|
+
return KoleoNotFound(response)
|
|
24
|
+
elif response.status_code == 401:
|
|
25
|
+
return KoleoUnauthorized(response)
|
|
26
|
+
elif response.status_code == 403:
|
|
27
|
+
return KoleoForbidden(response)
|
|
28
|
+
elif response.status_code == 429:
|
|
29
|
+
return KoleoRatelimited(response)
|
|
30
|
+
else:
|
|
31
|
+
return KoleoAPIException(response)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class KoleoNotFound(KoleoAPIException):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class KoleoForbidden(KoleoAPIException):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class KoleoUnauthorized(KoleoAPIException):
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class KoleoRatelimited(KoleoAPIException):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class KoleoAPI:
|
|
51
|
+
def __init__(self) -> None:
|
|
52
|
+
self.session = Session()
|
|
53
|
+
self.base_url = "https://koleo.pl"
|
|
54
|
+
self.version = 2
|
|
55
|
+
self.base_headers = {
|
|
56
|
+
"x-koleo-version": str(self.version),
|
|
57
|
+
"User-Agent": "Koleo-CLI(https://pypi.org/project/koleo-cli)",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
def _get(self, path, *args, **kwargs) -> Response:
|
|
61
|
+
headers = {**self.base_headers, **kwargs.get("headers", {})}
|
|
62
|
+
r = self.session.get(self.base_url + path, *args, headers=headers, **kwargs)
|
|
63
|
+
if not r.ok:
|
|
64
|
+
raise KoleoAPIException.from_response(r)
|
|
65
|
+
return r
|
|
66
|
+
|
|
67
|
+
def _get_json(self, path, *args, **kwargs) -> t.Any:
|
|
68
|
+
r = self._get(path, *args, **kwargs)
|
|
69
|
+
return r.json()
|
|
70
|
+
|
|
71
|
+
def _get_bytes(self, path, *args, **kwargs) -> bytes:
|
|
72
|
+
r = self._get(path, *args, **kwargs)
|
|
73
|
+
return r.content
|
|
74
|
+
|
|
75
|
+
def find_station(self, query: str, language: str = "pl") -> list[ExtendedStationInfo]:
|
|
76
|
+
# https://koleo.pl/ls?q=tere&language=pl
|
|
77
|
+
return self._get_json("/ls", query={"q": query, "language": language})
|
|
78
|
+
|
|
79
|
+
def get_station_by_slug(self, slug: str) -> ExtendedBaseStationInfo:
|
|
80
|
+
# https://koleo.pl/api/v2/main/stations/by_slug/inowroclaw
|
|
81
|
+
return self._get_json(
|
|
82
|
+
f"/api/v2/main/stations/by_slug/{slug}",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def get_station_info_by_slug(self, slug: str) -> StationDetails:
|
|
86
|
+
# https://koleo.pl/api/v2/main/station_info/inowroclaw
|
|
87
|
+
return self._get_json(
|
|
88
|
+
f"/api/v2/main/station_info/{slug}",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def get_departures(self, station_id: int, date: datetime) -> list[TrainOnStationInfo]:
|
|
92
|
+
# https://koleo.pl/api/v2/main/timetables/18705/2024-03-25/departures
|
|
93
|
+
return self._get_json(
|
|
94
|
+
f"/api/v2/main/timetables/{station_id}/{date.strftime("%Y-%m-%d")}/departures",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def get_arrivals(self, station_id: int, date: datetime) -> list[TrainOnStationInfo]:
|
|
98
|
+
# https://koleo.pl/api/v2/main/timetables/18705/2024-03-25/arrivals
|
|
99
|
+
return self._get_json(
|
|
100
|
+
f"/api/v2/main/timetables/{station_id}/{date.strftime("%Y-%m-%d")}/arrivals",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def get_train_calendars(self, brand_name: str, number: int, name: str | None = None) -> TrainCalendarResponse:
|
|
104
|
+
# https://koleo.pl/pl/train_calendars?brand=REG&nr=10417
|
|
105
|
+
# https://koleo.pl/pl/train_calendars?brand=IC&nr=1106&name=ESPERANTO ; WHY!!!! WHY!!!!!!1
|
|
106
|
+
params = {"brand": brand_name, "nr": number}
|
|
107
|
+
if name:
|
|
108
|
+
params["name"] = name.upper() # WHY!!!!!!!!!
|
|
109
|
+
return self._get_json("/api/v2/main/train_calendars", params=params)
|
|
110
|
+
|
|
111
|
+
def get_train(self, id: int) -> TrainDetailResponse:
|
|
112
|
+
# https://koleo.pl/pl/trains/142821312
|
|
113
|
+
return self._get_json(f"/api/v2/main/trains/{id}")
|
|
114
|
+
|
|
115
|
+
def get_connections(
|
|
116
|
+
self,
|
|
117
|
+
start: str,
|
|
118
|
+
end: str,
|
|
119
|
+
brand_ids: list[int],
|
|
120
|
+
date: datetime,
|
|
121
|
+
direct: bool = False,
|
|
122
|
+
purchasable: bool = False
|
|
123
|
+
) -> ...:
|
|
124
|
+
params = {
|
|
125
|
+
"query[date]": date.strftime("%d-%m-%Y %H:%M:%S"),
|
|
126
|
+
"query[start_station]": start,
|
|
127
|
+
"query[end_station]": end,
|
|
128
|
+
"query[only_purchasable]": purchasable,
|
|
129
|
+
"query[direct]": direct,
|
|
130
|
+
"query[brand_ids]": brand_ids
|
|
131
|
+
}
|
|
132
|
+
return self._get_json("/api/v2/main/connections", params=params)
|
|
133
|
+
|
|
134
|
+
def get_brands(self) -> list[ApiBrand]:
|
|
135
|
+
# https://koleo.pl/api/v2/main/brands
|
|
136
|
+
return self._get_json(
|
|
137
|
+
"/api/v2/main/brands",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def get_carriers(self) -> list[Carrier]:
|
|
141
|
+
# https://koleo.pl/api/v2/main/carriers
|
|
142
|
+
return self._get_json(
|
|
143
|
+
"/api/v2/main/carriers",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def get_discounts(self) -> list[DiscountInfo]:
|
|
147
|
+
# https://koleo.pl/api/v2/main/discounts
|
|
148
|
+
return self._get_json(
|
|
149
|
+
"/api/v2/main/discounts",
|
|
150
|
+
)
|
koleo/cli.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from argparse import ArgumentParser
|
|
3
|
+
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.traceback import install
|
|
6
|
+
|
|
7
|
+
from .api import KoleoAPI
|
|
8
|
+
from .types import TrainOnStationInfo, TrainDetailResponse
|
|
9
|
+
from .utils import name_to_slug, parse_datetime, time_dict_to_dt
|
|
10
|
+
from .storage import Storage, DEFAULT_CONFIG_PATH
|
|
11
|
+
|
|
12
|
+
install(show_locals=True)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CLI:
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
no_color: bool = False,
|
|
19
|
+
client: KoleoAPI | None = None,
|
|
20
|
+
storage: Storage | None = None,
|
|
21
|
+
) -> None:
|
|
22
|
+
self._client = client
|
|
23
|
+
self._storage = storage
|
|
24
|
+
self.console = Console(color_system="standard", no_color=no_color)
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def client(self) -> KoleoAPI:
|
|
28
|
+
if not self._client:
|
|
29
|
+
raise ValueError("Client not set!")
|
|
30
|
+
return self._client
|
|
31
|
+
|
|
32
|
+
@client.setter
|
|
33
|
+
def client(self, client: KoleoAPI):
|
|
34
|
+
self._client = client
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def storage(self) -> Storage:
|
|
38
|
+
if not self._storage:
|
|
39
|
+
raise ValueError("Storage not set!")
|
|
40
|
+
return self._storage
|
|
41
|
+
|
|
42
|
+
@storage.setter
|
|
43
|
+
def storage(self, storage: Storage):
|
|
44
|
+
self._storage = storage
|
|
45
|
+
|
|
46
|
+
def list_stations(self, name: str): ...
|
|
47
|
+
|
|
48
|
+
def get_departures(self, station_id: int, date: datetime):
|
|
49
|
+
trains = self.client.get_departures(station_id, date)
|
|
50
|
+
trains = [
|
|
51
|
+
i
|
|
52
|
+
for i in trains
|
|
53
|
+
if datetime.fromisoformat(i["departure"]).timestamp() > date.timestamp() # type: ignore
|
|
54
|
+
]
|
|
55
|
+
table = self.trains_on_station_table(trains)
|
|
56
|
+
self.console.print(table)
|
|
57
|
+
return table
|
|
58
|
+
|
|
59
|
+
def get_arrivals(self, station_id: int, date: datetime):
|
|
60
|
+
trains = self.client.get_arrivals(station_id, date)
|
|
61
|
+
trains = [
|
|
62
|
+
i
|
|
63
|
+
for i in trains
|
|
64
|
+
if datetime.fromisoformat(i["arrival"]).timestamp() > date.timestamp() # type: ignore
|
|
65
|
+
]
|
|
66
|
+
table = self.trains_on_station_table(trains, type=2)
|
|
67
|
+
self.console.print(table)
|
|
68
|
+
return table
|
|
69
|
+
|
|
70
|
+
def full_departures(self, station: str, date: datetime):
|
|
71
|
+
slug = name_to_slug(station)
|
|
72
|
+
st = self.storage.get_cache(f"st-{slug}") or self.storage.set_cache(
|
|
73
|
+
f"st-{slug}", self.client.get_station_by_slug(slug)
|
|
74
|
+
)
|
|
75
|
+
station_info = f"[bold blue]{st["name"]}[/bold blue] ID: {st["id"]}"
|
|
76
|
+
self.console.print(station_info)
|
|
77
|
+
self.get_departures(st["id"], date)
|
|
78
|
+
|
|
79
|
+
def full_arrivals(self, station: str, date: datetime):
|
|
80
|
+
slug = name_to_slug(station)
|
|
81
|
+
st = self.storage.get_cache(f"st-{slug}") or self.storage.set_cache(
|
|
82
|
+
f"st-{slug}", self.client.get_station_by_slug(slug)
|
|
83
|
+
)
|
|
84
|
+
station_info = f"[bold blue]{st["name"]}[/bold blue] ID: {st["id"]}"
|
|
85
|
+
self.console.print(station_info)
|
|
86
|
+
self.get_arrivals(st["id"], date)
|
|
87
|
+
|
|
88
|
+
def train_info(self, brand: str, name: str, date: datetime):
|
|
89
|
+
brand = brand.upper().strip()
|
|
90
|
+
name = name.upper().strip()
|
|
91
|
+
cache_id = f"tc-{brand}-{name}"
|
|
92
|
+
if name.isnumeric():
|
|
93
|
+
number = int(name)
|
|
94
|
+
name = ""
|
|
95
|
+
elif len(parts := name.split(" ")) == 2 or len(parts := name.split("-")) == 2:
|
|
96
|
+
number, name = parts
|
|
97
|
+
number = int(number)
|
|
98
|
+
else:
|
|
99
|
+
raise ValueError("Invalid train name!")
|
|
100
|
+
train_calendars = self.storage.get_cache(cache_id) or self.storage.set_cache(
|
|
101
|
+
cache_id, self.client.get_train_calendars(brand, number, name)
|
|
102
|
+
)
|
|
103
|
+
brands = self.storage.get_cache("brands") or self.storage.set_cache("brands", self.client.get_brands())
|
|
104
|
+
train_id = train_calendars["train_calendars"][0]["date_train_map"][date.strftime("%Y-%m-%d")]
|
|
105
|
+
train_details = self.client.get_train(train_id)
|
|
106
|
+
brand = next(iter(i for i in brands if i["id"] == train_details["train"]["brand_id"]), {}).get("name", "")
|
|
107
|
+
data = f"{brand} {train_details["train"]["train_full_name"]}\n"
|
|
108
|
+
vehicle_types: dict[str, str] = {
|
|
109
|
+
stop["station_display_name"]: stop["vehicle_type"]
|
|
110
|
+
for stop in train_details["stops"]
|
|
111
|
+
if stop["vehicle_type"]
|
|
112
|
+
}
|
|
113
|
+
if vehicle_types:
|
|
114
|
+
keys = list(vehicle_types.keys())
|
|
115
|
+
start = keys[0]
|
|
116
|
+
for i in range(1, len(keys)):
|
|
117
|
+
if vehicle_types[keys[i]] != vehicle_types[start]:
|
|
118
|
+
data += f"[bold green] {start} - {keys[i]}:[/bold green] {vehicle_types[start]}\n"
|
|
119
|
+
start = keys[i]
|
|
120
|
+
data += f"[bold green] {start} - {keys[-1]}:[/bold green] {vehicle_types[start]}"
|
|
121
|
+
self.console.print(data)
|
|
122
|
+
self.console.print(self.train_route_table(train_details))
|
|
123
|
+
|
|
124
|
+
def route(self, start: str, end: str, date: datetime, direct: bool = False, purchasable: bool = False):
|
|
125
|
+
slug = name_to_slug(station)
|
|
126
|
+
st = self.storage.get_cache(f"st-{slug}") or self.storage.set_cache(
|
|
127
|
+
f"st-{slug}", self.client.get_station_by_slug(slug)
|
|
128
|
+
)
|
|
129
|
+
station_info = f"[bold blue]{st["name"]}[/bold blue] ID: {st["id"]}"
|
|
130
|
+
self.console.print(station_info)
|
|
131
|
+
self.get_arrivals(st["id"], date)
|
|
132
|
+
|
|
133
|
+
def trains_on_station_table(self, trains: list[TrainOnStationInfo], type: int = 1):
|
|
134
|
+
parts = []
|
|
135
|
+
brands = self.storage.get_cache("brands") or self.storage.set_cache("brands", self.client.get_brands())
|
|
136
|
+
for train in trains:
|
|
137
|
+
time = train["departure"] if type == 1 else train["arrival"]
|
|
138
|
+
assert time
|
|
139
|
+
brand = next(iter(i for i in brands if i["id"] == train["brand_id"]), {}).get("name")
|
|
140
|
+
parts.append(
|
|
141
|
+
f"[bold green]{time[11:16]}[/bold green] {brand} {train["train_full_name"]}[purple] {train["stations"][0]["name"]} [/purple]"
|
|
142
|
+
)
|
|
143
|
+
return "\n".join(parts)
|
|
144
|
+
|
|
145
|
+
def train_route_table(self, train: TrainDetailResponse):
|
|
146
|
+
parts = []
|
|
147
|
+
for stop in train["stops"]:
|
|
148
|
+
arr = time_dict_to_dt(stop["arrival"])
|
|
149
|
+
dep = time_dict_to_dt(stop["departure"])
|
|
150
|
+
parts.append(
|
|
151
|
+
f"[white underline]{stop["distance"] / 1000:0.4}km[/white underline] [bold green]{arr.strftime("%H:%M")}[/bold green] - [bold red]{dep.strftime("%H:%M")}[/bold red] [purple] {stop["station_display_name"]} {stop["platform"]} [/purple]"
|
|
152
|
+
)
|
|
153
|
+
return "\n".join(parts)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def main():
|
|
157
|
+
cli = CLI()
|
|
158
|
+
|
|
159
|
+
parser = ArgumentParser("koleo", description="Koleo CLI")
|
|
160
|
+
parser.add_argument("-c", "--config", help="Custom config path.", default=DEFAULT_CONFIG_PATH)
|
|
161
|
+
parser.add_argument("--nocolor", help="Disable color output", action="store_true", default=False)
|
|
162
|
+
subparsers = parser.add_subparsers(title="actions", required=False)
|
|
163
|
+
|
|
164
|
+
departures = subparsers.add_parser("departures", aliases=["d", "dep"], help="Allows you to list station departures")
|
|
165
|
+
departures.add_argument(
|
|
166
|
+
"station",
|
|
167
|
+
help="The station name",
|
|
168
|
+
default=None,
|
|
169
|
+
nargs="?",
|
|
170
|
+
)
|
|
171
|
+
departures.add_argument(
|
|
172
|
+
"-d",
|
|
173
|
+
"--date",
|
|
174
|
+
help="the departure date",
|
|
175
|
+
type=lambda s: parse_datetime(s),
|
|
176
|
+
default=datetime.now(),
|
|
177
|
+
)
|
|
178
|
+
departures.add_argument("-s", "--save", help="save the station as your default one", action="store_true")
|
|
179
|
+
departures.set_defaults(func=cli.full_departures, pass_=["station", "date"])
|
|
180
|
+
|
|
181
|
+
arrivals = subparsers.add_parser("arrivals", aliases=["a", "arr"], help="Allows you to list station departures")
|
|
182
|
+
arrivals.add_argument(
|
|
183
|
+
"station",
|
|
184
|
+
help="The station name",
|
|
185
|
+
default=None,
|
|
186
|
+
nargs="?",
|
|
187
|
+
)
|
|
188
|
+
arrivals.add_argument(
|
|
189
|
+
"-d",
|
|
190
|
+
"--date",
|
|
191
|
+
help="the arrival date",
|
|
192
|
+
type=lambda s: parse_datetime(s),
|
|
193
|
+
default=datetime.now(),
|
|
194
|
+
)
|
|
195
|
+
arrivals.add_argument("-s", "--save", help="save the station as your default one", action="store_true")
|
|
196
|
+
arrivals.set_defaults(func=cli.full_arrivals, pass_=["station", "date"])
|
|
197
|
+
|
|
198
|
+
train_route = subparsers.add_parser(
|
|
199
|
+
"trainroute",
|
|
200
|
+
aliases=["r", "tr", "t"],
|
|
201
|
+
help="Allows you to show the train's route",
|
|
202
|
+
)
|
|
203
|
+
train_route.add_argument("brand", help="The brand name", type=str)
|
|
204
|
+
train_route.add_argument("name", help="The train name", type=str)
|
|
205
|
+
train_route.add_argument(
|
|
206
|
+
"-d",
|
|
207
|
+
"--date",
|
|
208
|
+
help="the date",
|
|
209
|
+
type=lambda s: parse_datetime(s),
|
|
210
|
+
default=datetime.now(),
|
|
211
|
+
)
|
|
212
|
+
train_route.set_defaults(func=cli.train_info, pass_=["brand", "name", "date"])
|
|
213
|
+
|
|
214
|
+
args = parser.parse_args()
|
|
215
|
+
|
|
216
|
+
storage = Storage.load(path=args.config)
|
|
217
|
+
client = KoleoAPI()
|
|
218
|
+
cli.client, cli.storage = client, storage
|
|
219
|
+
cli.console.no_color = args.nocolor
|
|
220
|
+
if hasattr(args, "station") and args.station is None:
|
|
221
|
+
args.station = storage.favourite_station
|
|
222
|
+
elif hasattr(args, "station") and args.save:
|
|
223
|
+
storage.favourite_station = args.station
|
|
224
|
+
storage.save()
|
|
225
|
+
if not hasattr(args, "func"):
|
|
226
|
+
if storage.favourite_station:
|
|
227
|
+
cli.full_departures(storage.favourite_station, datetime.now())
|
|
228
|
+
else:
|
|
229
|
+
raise ValueError("favourite station not set!")
|
|
230
|
+
else:
|
|
231
|
+
args.func(**{k: v for k, v in args.__dict__.items() if k in getattr(args, "pass_", [])})
|
koleo/storage.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
from time import time
|
|
3
|
+
from dataclasses import dataclass, asdict, field
|
|
4
|
+
from sys import platform
|
|
5
|
+
from json import dump, load
|
|
6
|
+
from os import makedirs, path as ospath
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_adequate_config_path():
|
|
10
|
+
if platform == "darwin":
|
|
11
|
+
# i dont fucking know nor want to
|
|
12
|
+
return "~/Library/Preferences/koleo-cli/data.json"
|
|
13
|
+
elif "win" in platform:
|
|
14
|
+
# same with this
|
|
15
|
+
return "%USERPROFILE%\\AppData\\Local\\koleo-cli\\data.json"
|
|
16
|
+
else:
|
|
17
|
+
return "~/.config/koleo-cli.json"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
DEFAULT_CONFIG_PATH = get_adequate_config_path()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
T = t.TypeVar("T")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class Storage:
|
|
28
|
+
favourite_station: str | None = None
|
|
29
|
+
last_searched_connections: list[tuple[str, str]] = field(default_factory=list)
|
|
30
|
+
last_searched_stations: list[str] = field(default_factory=list)
|
|
31
|
+
cache: dict[str, tuple[int, t.Any]] = field(default_factory=dict)
|
|
32
|
+
|
|
33
|
+
def __post_init__(self):
|
|
34
|
+
self._path: str
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def load(cls, *, path: str = DEFAULT_CONFIG_PATH) -> t.Self:
|
|
38
|
+
expanded = ospath.expanduser(path)
|
|
39
|
+
if ospath.exists(expanded):
|
|
40
|
+
with open(expanded, "r") as f:
|
|
41
|
+
data = load(f)
|
|
42
|
+
else:
|
|
43
|
+
data = {}
|
|
44
|
+
storage = cls(**data)
|
|
45
|
+
storage._path = path
|
|
46
|
+
return storage
|
|
47
|
+
|
|
48
|
+
def get_cache(self, id: str) -> t.Any | None:
|
|
49
|
+
cache_result = self.cache.get(id)
|
|
50
|
+
if not cache_result:
|
|
51
|
+
return
|
|
52
|
+
expiry, item = cache_result
|
|
53
|
+
if expiry > time():
|
|
54
|
+
return item
|
|
55
|
+
else:
|
|
56
|
+
self.cache.pop(id)
|
|
57
|
+
self.save()
|
|
58
|
+
|
|
59
|
+
def set_cache(self, id: str, item: T, ttl: int = 86400) -> T:
|
|
60
|
+
self.cache[id] = (int(time() + ttl), item)
|
|
61
|
+
self.save()
|
|
62
|
+
return item
|
|
63
|
+
|
|
64
|
+
def save(self):
|
|
65
|
+
expanded = ospath.expanduser(self._path)
|
|
66
|
+
dir = ospath.dirname(expanded)
|
|
67
|
+
if dir:
|
|
68
|
+
if not ospath.exists(dir):
|
|
69
|
+
makedirs(dir)
|
|
70
|
+
with open(expanded, "w+") as f:
|
|
71
|
+
dump(asdict(self), f)
|
koleo/types.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BaseStationInfo(t.TypedDict):
|
|
5
|
+
id: int
|
|
6
|
+
name: str
|
|
7
|
+
name_slug: str
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# i want to kms
|
|
11
|
+
class ExtendedBaseStationInfo(BaseStationInfo):
|
|
12
|
+
hits: int
|
|
13
|
+
version: str # "A", "B"
|
|
14
|
+
is_group: bool
|
|
15
|
+
region: str
|
|
16
|
+
country: str
|
|
17
|
+
latitude: float
|
|
18
|
+
longitued: float
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ExtendedStationInfo(BaseStationInfo):
|
|
22
|
+
ibnr: int
|
|
23
|
+
localised_name: str
|
|
24
|
+
on_demand: bool
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class StationBannerClockHandsPositon(t.TypedDict):
|
|
28
|
+
x: float
|
|
29
|
+
y: float
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class StationBannerClockHands(t.TypedDict):
|
|
33
|
+
url: str | None
|
|
34
|
+
position: StationBannerClockHandsPositon
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class StationBannerClock(t.TypedDict):
|
|
38
|
+
visible: bool | None
|
|
39
|
+
hands: StationBannerClockHands
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class StationBanner(t.TypedDict):
|
|
43
|
+
url: str
|
|
44
|
+
clock: StationBannerClock
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Address(t.TypedDict):
|
|
48
|
+
full: str
|
|
49
|
+
zip: str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Feature(t.TypedDict):
|
|
53
|
+
id: str
|
|
54
|
+
name: str
|
|
55
|
+
available: bool
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class StationOpeningHours(t.TypedDict):
|
|
59
|
+
day: int # 0-6
|
|
60
|
+
open: str # 00:00
|
|
61
|
+
close: str # 24:00
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class StationDetails(t.TypedDict):
|
|
65
|
+
banner: StationBanner
|
|
66
|
+
address: Address
|
|
67
|
+
lat: float
|
|
68
|
+
lon: float
|
|
69
|
+
opening_hours: list[StationOpeningHours]
|
|
70
|
+
features: list[Feature]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class StationLocalizedToTrain(BaseStationInfo):
|
|
74
|
+
train_id: int
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ApiBrand(t.TypedDict):
|
|
78
|
+
id: int
|
|
79
|
+
name: str # EIC, IC, IR, REG, ...
|
|
80
|
+
display_name: str
|
|
81
|
+
logo_text: str
|
|
82
|
+
color: str # hex
|
|
83
|
+
carrier_id: int
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class Carrier(t.TypedDict):
|
|
87
|
+
id: int
|
|
88
|
+
name: str # Koleje Dolnośląskie, POLREGIO
|
|
89
|
+
short_name: str # KD, PR
|
|
90
|
+
slug: str
|
|
91
|
+
legal_name: str # PKP Szybka Kolej Miejska w Trójmieście Sp.z o.o.
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class DiscountInfo(t.TypedDict):
|
|
95
|
+
id: int
|
|
96
|
+
passenger_percentage: int
|
|
97
|
+
display_passenger_percentage: float
|
|
98
|
+
flyer_second_class_percentage: int
|
|
99
|
+
flyer_first_class_percentage: int
|
|
100
|
+
express_second_class_percentage: int
|
|
101
|
+
express_first_class_percentage: int
|
|
102
|
+
dependent_on_ids: list[int]
|
|
103
|
+
name: str
|
|
104
|
+
rank: int
|
|
105
|
+
season_passenger_percentage: int
|
|
106
|
+
displayable: bool
|
|
107
|
+
is_company: bool
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class TrainOnStationInfo(t.TypedDict):
|
|
111
|
+
arrival: str | None # first station
|
|
112
|
+
departure: str | None # last station
|
|
113
|
+
stations: list[StationLocalizedToTrain]
|
|
114
|
+
train_full_name: str
|
|
115
|
+
brand_id: int
|
|
116
|
+
platform: str # could be empty; some countries dont use arabic numerals or even tracks
|
|
117
|
+
track: str # could be empty; some countries dont use arabic numerals or even tracks
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class TrainCalendar(t.TypedDict):
|
|
121
|
+
id: int
|
|
122
|
+
train_nr: int
|
|
123
|
+
train_name: str
|
|
124
|
+
trainBrand: int
|
|
125
|
+
dates: list[str] # Y-M-D
|
|
126
|
+
train_ids: list[int]
|
|
127
|
+
date_train_map: dict[str, int]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class TrainCalendarResponse(t.TypedDict):
|
|
131
|
+
train_calendars: list[TrainCalendar]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class TimeDict(t.TypedDict):
|
|
135
|
+
hour: int
|
|
136
|
+
minute: int
|
|
137
|
+
second: int
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
TrainAttribute = t.Tuple[int, str, str, str, bool, str]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class TrainDetail(t.TypedDict):
|
|
144
|
+
id: int
|
|
145
|
+
train_nr: int
|
|
146
|
+
name: str | None
|
|
147
|
+
train_full_name: str
|
|
148
|
+
run_desc: str # "09.09-15.09 - w pt - nd; 16.09-29.09, 14.10-03.11 - codziennie; 30.09-06.10 - w pn; 07.10-13.10 - we wt - nd; 04.11-10.11 - w pn - sb"
|
|
149
|
+
carrier_id: int
|
|
150
|
+
brand_id: int
|
|
151
|
+
train_name: int # wtf
|
|
152
|
+
duration_offset: int # wtf?
|
|
153
|
+
db_train_nr: int # lol
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class TrainStop(t.TypedDict):
|
|
157
|
+
id: int
|
|
158
|
+
station_id: int
|
|
159
|
+
station_name: str
|
|
160
|
+
station_slug: str
|
|
161
|
+
train_id: int
|
|
162
|
+
arrival: TimeDict
|
|
163
|
+
departure: TimeDict
|
|
164
|
+
position: int # the stop nr
|
|
165
|
+
train_nr: int | None
|
|
166
|
+
brand_id: int
|
|
167
|
+
distance: int # meters
|
|
168
|
+
entry_only: bool
|
|
169
|
+
exit_only: bool
|
|
170
|
+
station_display_name: str
|
|
171
|
+
platform: str
|
|
172
|
+
vehicle_type: str | None # ED161
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class TrainDetailResponse(t.TypedDict):
|
|
176
|
+
train: TrainDetail
|
|
177
|
+
stops: list[TrainStop]
|
koleo/utils.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from datetime import datetime, time
|
|
2
|
+
|
|
3
|
+
from .types import TimeDict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def parse_datetime(s: str):
|
|
7
|
+
now = datetime.today()
|
|
8
|
+
try:
|
|
9
|
+
dt = datetime.strptime(s, "%d-%m")
|
|
10
|
+
return dt.replace(year=now.year, hour=0, minute=0)
|
|
11
|
+
except ValueError:
|
|
12
|
+
pass
|
|
13
|
+
try:
|
|
14
|
+
return datetime.strptime(s, "%Y-%m-%d").replace(hour=0, minute=0)
|
|
15
|
+
except ValueError:
|
|
16
|
+
pass
|
|
17
|
+
return datetime.combine(now, datetime.strptime(s, "%H:%M").time())
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def time_dict_to_dt(s: TimeDict):
|
|
21
|
+
now = datetime.today()
|
|
22
|
+
return datetime.combine(now, time(s["hour"], s["minute"], s["second"]))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
TRANSLITERATIONS = {
|
|
26
|
+
"ł": "l",
|
|
27
|
+
"ń": "n",
|
|
28
|
+
"ą": "a",
|
|
29
|
+
"ę": "e",
|
|
30
|
+
"ś": "s",
|
|
31
|
+
"ć": "c",
|
|
32
|
+
"ó": "o",
|
|
33
|
+
"ź": "z",
|
|
34
|
+
"ż": "z",
|
|
35
|
+
" ": "-",
|
|
36
|
+
"_": "-",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def name_to_slug(name: str) -> str:
|
|
41
|
+
return "".join([TRANSLITERATIONS.get(char, char) for char in name.lower()])
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: koleo-cli
|
|
3
|
+
Version: 0.2.137
|
|
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
|
+
Requires-Dist: rich==13.7.1
|
|
17
|
+
Requires-Dist: requests==2.31.0
|
|
18
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
koleo/__init__.py,sha256=ChPpKyc4vrcgWj-Uk_ZlDw32s_7iFpE3f1FTAd6zxPY,51
|
|
2
|
+
koleo/__main__.py,sha256=wu5N2wk8mvBgyvr2ghmQf4prezAe0_i-p123VVreyYc,62
|
|
3
|
+
koleo/api.py,sha256=8aYn4SsavjOx9cPHLgSlwVmbZaZWJNYpIL3lZPuvdTk,5070
|
|
4
|
+
koleo/cli.py,sha256=fd1O_gO5EF-7BrrLe79j7fn7SEKr5yLgWu33UIjLghM,9230
|
|
5
|
+
koleo/storage.py,sha256=uCh6edwizAuw1z_Ti5AXvDan2pJAJBSobCVmYw096F8,2015
|
|
6
|
+
koleo/types.py,sha256=8mAsRdNh3jMJwAV5KnHVvDQAekhs0N70EVem4p_w18o,3760
|
|
7
|
+
koleo/utils.py,sha256=MYfeQkk9spT2lmlFVsHlpzgnAf0hqN-gKEbgYASqW6U,904
|
|
8
|
+
koleo_cli-0.2.137.dist-info/METADATA,sha256=-PLG8A9e3iD-az9tyrdJEitqThaZrBkOhiWHWuMSPXk,624
|
|
9
|
+
koleo_cli-0.2.137.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
|
|
10
|
+
koleo_cli-0.2.137.dist-info/entry_points.txt,sha256=LtCidkVDq8Zd7-fxpRbys1Xa9LTHMZwXVbdcQEscdes,41
|
|
11
|
+
koleo_cli-0.2.137.dist-info/top_level.txt,sha256=AlWdXotkRYzHpFfOBYi6xOXl1H0zq4-tqtZ2XivoWB4,6
|
|
12
|
+
koleo_cli-0.2.137.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
koleo
|