rzd-api 1.0.0__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.
- mcp_server/__init__.py +0 -0
- mcp_server/server.py +204 -0
- rzd_api/__init__.py +6 -0
- rzd_api/api.py +95 -0
- rzd_api/client.py +122 -0
- rzd_api/config.py +12 -0
- rzd_api/query.py +86 -0
- rzd_api-1.0.0.dist-info/METADATA +391 -0
- rzd_api-1.0.0.dist-info/RECORD +12 -0
- rzd_api-1.0.0.dist-info/WHEEL +5 -0
- rzd_api-1.0.0.dist-info/entry_points.txt +2 -0
- rzd_api-1.0.0.dist-info/top_level.txt +2 -0
mcp_server/__init__.py
ADDED
|
File without changes
|
mcp_server/server.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Server for RZD (Russian Railways) API.
|
|
3
|
+
|
|
4
|
+
Exposes rzd_api as MCP tools so any MCP-compatible client (Claude, etc.)
|
|
5
|
+
can search train routes, carriages, and station codes.
|
|
6
|
+
|
|
7
|
+
Transport is configured via environment variables:
|
|
8
|
+
MCP_TRANSPORT — stdio (default) | sse | streamable-http
|
|
9
|
+
MCP_HOST — bind host for HTTP transports (default: 0.0.0.0)
|
|
10
|
+
MCP_PORT — bind port for HTTP transports (default: 8000)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
from rzd_api import Api
|
|
17
|
+
|
|
18
|
+
mcp = None
|
|
19
|
+
|
|
20
|
+
_api: Api | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_api() -> Api:
|
|
24
|
+
global _api
|
|
25
|
+
if _api is None:
|
|
26
|
+
_api = Api()
|
|
27
|
+
return _api
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def train_routes(
|
|
31
|
+
code0: str,
|
|
32
|
+
code1: str,
|
|
33
|
+
dt0: str,
|
|
34
|
+
dir: int = 0,
|
|
35
|
+
tfl: int = 3,
|
|
36
|
+
check_seats: int = 1,
|
|
37
|
+
md: int = 0,
|
|
38
|
+
) -> str:
|
|
39
|
+
"""Get one-way train routes with available seats and prices.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
code0: Origin station code (e.g. '2004000' for Saint Petersburg).
|
|
43
|
+
code1: Destination station code (e.g. '2000000' for Moscow).
|
|
44
|
+
dt0: Departure date in format dd.mm.yyyy.
|
|
45
|
+
dir: Direction — 0 one-way (default).
|
|
46
|
+
tfl: Train type: 1 = trains only, 2 = electric trains only, 3 = both (default).
|
|
47
|
+
check_seats: 1 = only trains with seats (default), 0 = all trains.
|
|
48
|
+
md: 0 = direct routes only (default), 1 = with transfers.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
JSON array of train objects with seats and pricing info.
|
|
52
|
+
"""
|
|
53
|
+
params = {
|
|
54
|
+
'code0': code0,
|
|
55
|
+
'code1': code1,
|
|
56
|
+
'dt0': dt0,
|
|
57
|
+
'dir': dir,
|
|
58
|
+
'tfl': tfl,
|
|
59
|
+
'checkSeats': check_seats,
|
|
60
|
+
'md': md,
|
|
61
|
+
}
|
|
62
|
+
return get_api().train_routes(params)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def train_routes_return(
|
|
66
|
+
code0: str,
|
|
67
|
+
code1: str,
|
|
68
|
+
dt0: str,
|
|
69
|
+
dt1: str,
|
|
70
|
+
tfl: int = 3,
|
|
71
|
+
check_seats: int = 1,
|
|
72
|
+
) -> str:
|
|
73
|
+
"""Get round-trip train routes (forward + back legs).
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
code0: Origin station code (e.g. '2004000' for Saint Petersburg).
|
|
77
|
+
code1: Destination station code (e.g. '2000000' for Moscow).
|
|
78
|
+
dt0: Departure date in format dd.mm.yyyy.
|
|
79
|
+
dt1: Return date in format dd.mm.yyyy.
|
|
80
|
+
tfl: Train type: 1 = trains only, 2 = electric trains only, 3 = both (default).
|
|
81
|
+
check_seats: 1 = only trains with seats (default), 0 = all trains.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
JSON object with 'forward' and 'back' arrays of train objects.
|
|
85
|
+
"""
|
|
86
|
+
params = {
|
|
87
|
+
'code0': code0,
|
|
88
|
+
'code1': code1,
|
|
89
|
+
'dt0': dt0,
|
|
90
|
+
'dt1': dt1,
|
|
91
|
+
'dir': 1,
|
|
92
|
+
'tfl': tfl,
|
|
93
|
+
'checkSeats': check_seats,
|
|
94
|
+
}
|
|
95
|
+
return get_api().train_routes_return(params)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def train_carriages(
|
|
99
|
+
code0: str,
|
|
100
|
+
code1: str,
|
|
101
|
+
dt0: str,
|
|
102
|
+
time0: str,
|
|
103
|
+
tnum0: str,
|
|
104
|
+
dir: int = 0,
|
|
105
|
+
) -> str:
|
|
106
|
+
"""Get detailed carriage and seat information for a specific train.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
code0: Origin station code.
|
|
110
|
+
code1: Destination station code.
|
|
111
|
+
dt0: Departure date in format dd.mm.yyyy.
|
|
112
|
+
time0: Departure time in format HH:MM.
|
|
113
|
+
tnum0: Train number (e.g. '054Г').
|
|
114
|
+
dir: Direction — 0 one-way (default).
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
JSON object with 'cars', 'functionBlocks', 'schemes', and 'companies'.
|
|
118
|
+
"""
|
|
119
|
+
params = {
|
|
120
|
+
'code0': code0,
|
|
121
|
+
'code1': code1,
|
|
122
|
+
'dt0': dt0,
|
|
123
|
+
'time0': time0,
|
|
124
|
+
'tnum0': tnum0,
|
|
125
|
+
'dir': dir,
|
|
126
|
+
}
|
|
127
|
+
return get_api().train_carriages(params)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def train_station_list(
|
|
131
|
+
train_number: str,
|
|
132
|
+
dep_date: str,
|
|
133
|
+
) -> str:
|
|
134
|
+
"""Get all stations on a train's route with arrival/departure times and distances.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
train_number: Train number (e.g. '054Г').
|
|
138
|
+
dep_date: Departure date in format dd.mm.yyyy.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
JSON object with 'train' info and 'routes' array of station objects.
|
|
142
|
+
"""
|
|
143
|
+
params = {
|
|
144
|
+
'trainNumber': train_number,
|
|
145
|
+
'depDate': dep_date,
|
|
146
|
+
}
|
|
147
|
+
return get_api().train_station_list(params)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def station_code(
|
|
151
|
+
station_name_part: str,
|
|
152
|
+
compact_mode: str = 'y',
|
|
153
|
+
) -> str:
|
|
154
|
+
"""Search for station codes by partial station name (in Russian).
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
station_name_part: Part of the station name (min 2 characters, e.g. 'ЧЕБ').
|
|
158
|
+
compact_mode: Response format, default 'y'.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
JSON array of objects with 'station' name and 'code' fields.
|
|
162
|
+
"""
|
|
163
|
+
params = {
|
|
164
|
+
'stationNamePart': station_name_part,
|
|
165
|
+
'compactMode': compact_mode,
|
|
166
|
+
}
|
|
167
|
+
return get_api().station_code(params)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def create_mcp_app():
|
|
171
|
+
try:
|
|
172
|
+
from mcp.server.fastmcp import FastMCP
|
|
173
|
+
except ImportError as exc:
|
|
174
|
+
raise RuntimeError(
|
|
175
|
+
"MCP support is not installed. Install it with: pip install 'rzd-api[mcp]'"
|
|
176
|
+
) from exc
|
|
177
|
+
|
|
178
|
+
app = FastMCP("RZD API")
|
|
179
|
+
app.tool()(train_routes)
|
|
180
|
+
app.tool()(train_routes_return)
|
|
181
|
+
app.tool()(train_carriages)
|
|
182
|
+
app.tool()(train_station_list)
|
|
183
|
+
app.tool()(station_code)
|
|
184
|
+
return app
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def main() -> None:
|
|
188
|
+
try:
|
|
189
|
+
app = create_mcp_app()
|
|
190
|
+
except RuntimeError as exc:
|
|
191
|
+
print(str(exc), file=sys.stderr)
|
|
192
|
+
raise SystemExit(1) from exc
|
|
193
|
+
|
|
194
|
+
transport = os.getenv('MCP_TRANSPORT', 'stdio')
|
|
195
|
+
if transport in ('sse', 'streamable-http'):
|
|
196
|
+
host = os.getenv('MCP_HOST', '0.0.0.0')
|
|
197
|
+
port = int(os.getenv('MCP_PORT', '8000'))
|
|
198
|
+
app.run(transport=transport, host=host, port=port)
|
|
199
|
+
else:
|
|
200
|
+
app.run(transport='stdio')
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
if __name__ == '__main__':
|
|
204
|
+
main()
|
rzd_api/__init__.py
ADDED
rzd_api/api.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from .config import Config
|
|
4
|
+
from .query import Query
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Api:
|
|
8
|
+
ROUTES_LAYER = 5827
|
|
9
|
+
CARRIAGES_LAYER = 5764
|
|
10
|
+
STATIONS_STRUCTURE_ID = 704
|
|
11
|
+
|
|
12
|
+
def __init__(self, config: Config = None):
|
|
13
|
+
if config is None:
|
|
14
|
+
config = Config()
|
|
15
|
+
|
|
16
|
+
self.lang = config.language
|
|
17
|
+
self.path = f'https://pass.rzd.ru/timetable/public/{self.lang}'
|
|
18
|
+
self.suggestion_path = 'https://pass.rzd.ru/suggester'
|
|
19
|
+
self.station_list_path = 'https://pass.rzd.ru/ticket/services/route/basicRoute'
|
|
20
|
+
self.query = Query(config)
|
|
21
|
+
|
|
22
|
+
def train_routes_data(self, params: dict) -> list[dict]:
|
|
23
|
+
"""Получает маршруты в одну точку как Python-объект."""
|
|
24
|
+
layer = {'layer_id': self.ROUTES_LAYER}
|
|
25
|
+
routes = self.query.get(self.path, {**layer, **params})
|
|
26
|
+
return routes['tp'][0]['list']
|
|
27
|
+
|
|
28
|
+
def train_routes(self, params: dict) -> str:
|
|
29
|
+
"""Получает маршруты в одну точку (one-way routes)."""
|
|
30
|
+
return json.dumps(self.train_routes_data(params), ensure_ascii=False)
|
|
31
|
+
|
|
32
|
+
def train_routes_return_data(self, params: dict) -> dict[str, list[dict]]:
|
|
33
|
+
"""Получает маршруты туда-обратно как Python-объект."""
|
|
34
|
+
layer = {'layer_id': self.ROUTES_LAYER}
|
|
35
|
+
routes = self.query.get(self.path, {**layer, **params})
|
|
36
|
+
return {
|
|
37
|
+
'forward': routes['tp'][0]['list'],
|
|
38
|
+
'back': routes['tp'][1]['list'],
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
def train_routes_return(self, params: dict) -> str:
|
|
42
|
+
"""Получает маршруты туда-обратно (round-trip routes)."""
|
|
43
|
+
return json.dumps(self.train_routes_return_data(params), ensure_ascii=False)
|
|
44
|
+
|
|
45
|
+
def train_carriages_data(self, params: dict) -> dict:
|
|
46
|
+
"""Получение списка вагонов как Python-объект."""
|
|
47
|
+
layer = {'layer_id': self.CARRIAGES_LAYER}
|
|
48
|
+
carriages = self.query.get(self.path, {**layer, **params})
|
|
49
|
+
lst = carriages.get('lst', [{}])
|
|
50
|
+
return {
|
|
51
|
+
'cars': lst[0].get('cars') if lst else None,
|
|
52
|
+
'functionBlocks': lst[0].get('functionBlocks') if lst else None,
|
|
53
|
+
'schemes': carriages.get('schemes'),
|
|
54
|
+
'companies': carriages.get('insuranceCompany'),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
def train_carriages(self, params: dict) -> str:
|
|
58
|
+
"""Получение списка вагонов (carriages/cars for a specific train)."""
|
|
59
|
+
return json.dumps(self.train_carriages_data(params), ensure_ascii=False)
|
|
60
|
+
|
|
61
|
+
def train_station_list_data(self, params: dict) -> dict:
|
|
62
|
+
"""Получение списка станций маршрута как Python-объект."""
|
|
63
|
+
layer = {'STRUCTURE_ID': self.STATIONS_STRUCTURE_ID}
|
|
64
|
+
stations = self.query.get(self.station_list_path, {**layer, **params})
|
|
65
|
+
return {
|
|
66
|
+
'train': stations['data']['trainInfo'],
|
|
67
|
+
'routes': stations['data']['routes'],
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
def train_station_list(self, params: dict) -> str:
|
|
71
|
+
"""Получение списка станций маршрута (all stations on a train's route)."""
|
|
72
|
+
return json.dumps(self.train_station_list_data(params), ensure_ascii=False)
|
|
73
|
+
|
|
74
|
+
def station_code_data(self, params: dict) -> list[dict]:
|
|
75
|
+
"""Получение кодов станций по части названия как Python-объект."""
|
|
76
|
+
query_params = {'lang': self.lang, **params}
|
|
77
|
+
routes = self.query.get(self.suggestion_path, query_params, method='GET')
|
|
78
|
+
|
|
79
|
+
stations = []
|
|
80
|
+
station_name_part = params.get('stationNamePart', '').upper()
|
|
81
|
+
|
|
82
|
+
if routes:
|
|
83
|
+
for station in routes:
|
|
84
|
+
name = station.get('n', '')
|
|
85
|
+
if station_name_part in name.upper():
|
|
86
|
+
stations.append({
|
|
87
|
+
'station': name,
|
|
88
|
+
'code': station.get('c'),
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
return stations
|
|
92
|
+
|
|
93
|
+
def station_code(self, params: dict) -> str:
|
|
94
|
+
"""Получение кодов станций по части названия (search stations by partial name)."""
|
|
95
|
+
return json.dumps(self.station_code_data(params), ensure_ascii=False)
|
rzd_api/client.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import date, datetime
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .api import Api
|
|
7
|
+
from .config import Config
|
|
8
|
+
from .query import RzdException
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RzdClient:
|
|
12
|
+
"""High-level client for using the package as a Python library."""
|
|
13
|
+
|
|
14
|
+
TRANSPORT_TYPES = {
|
|
15
|
+
'trains': 1,
|
|
16
|
+
'suburban': 2,
|
|
17
|
+
'all': 3,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
def __init__(self, config: Config | None = None, api: Api | None = None):
|
|
21
|
+
self.api = api or Api(config)
|
|
22
|
+
|
|
23
|
+
def search_tickets(
|
|
24
|
+
self,
|
|
25
|
+
from_station: str | int,
|
|
26
|
+
to_station: str | int,
|
|
27
|
+
departure_date: str | date | datetime,
|
|
28
|
+
return_date: str | date | datetime | None = None,
|
|
29
|
+
*,
|
|
30
|
+
only_with_seats: bool = True,
|
|
31
|
+
include_transfers: bool = False,
|
|
32
|
+
transport_type: str = 'all',
|
|
33
|
+
) -> list[dict[str, Any]] | dict[str, list[dict[str, Any]]]:
|
|
34
|
+
"""Search tickets by station names or station codes."""
|
|
35
|
+
params = {
|
|
36
|
+
'code0': self.resolve_station_code(from_station),
|
|
37
|
+
'code1': self.resolve_station_code(to_station),
|
|
38
|
+
'dt0': self._format_date(departure_date),
|
|
39
|
+
'dir': 1 if return_date is not None else 0,
|
|
40
|
+
'tfl': self._transport_type_code(transport_type),
|
|
41
|
+
'checkSeats': 1 if only_with_seats else 0,
|
|
42
|
+
'md': 1 if include_transfers else 0,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if return_date is not None:
|
|
46
|
+
params['dt1'] = self._format_date(return_date)
|
|
47
|
+
return self.api.train_routes_return_data(params)
|
|
48
|
+
|
|
49
|
+
return self.api.train_routes_data(params)
|
|
50
|
+
|
|
51
|
+
def get_carriages(
|
|
52
|
+
self,
|
|
53
|
+
from_station: str | int,
|
|
54
|
+
to_station: str | int,
|
|
55
|
+
departure_date: str | date | datetime,
|
|
56
|
+
departure_time: str,
|
|
57
|
+
train_number: str,
|
|
58
|
+
) -> dict[str, Any]:
|
|
59
|
+
"""Fetch carriage information for a train."""
|
|
60
|
+
params = {
|
|
61
|
+
'code0': self.resolve_station_code(from_station),
|
|
62
|
+
'code1': self.resolve_station_code(to_station),
|
|
63
|
+
'dt0': self._format_date(departure_date),
|
|
64
|
+
'time0': departure_time,
|
|
65
|
+
'tnum0': train_number,
|
|
66
|
+
'dir': 0,
|
|
67
|
+
}
|
|
68
|
+
return self.api.train_carriages_data(params)
|
|
69
|
+
|
|
70
|
+
def get_route_stations(
|
|
71
|
+
self,
|
|
72
|
+
train_number: str,
|
|
73
|
+
departure_date: str | date | datetime,
|
|
74
|
+
) -> dict[str, Any]:
|
|
75
|
+
"""Fetch all stations for a train route."""
|
|
76
|
+
return self.api.train_station_list_data({
|
|
77
|
+
'trainNumber': train_number,
|
|
78
|
+
'depDate': self._format_date(departure_date),
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
def find_stations(self, query: str, *, compact_mode: str = 'y') -> list[dict[str, str]]:
|
|
82
|
+
"""Find stations by a partial name."""
|
|
83
|
+
return self.api.station_code_data({
|
|
84
|
+
'stationNamePart': query,
|
|
85
|
+
'compactMode': compact_mode,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
def resolve_station_code(self, station: str | int) -> str:
|
|
89
|
+
"""Resolve either a station code or a station name to a code."""
|
|
90
|
+
value = str(station).strip()
|
|
91
|
+
if not value:
|
|
92
|
+
raise ValueError('Station name or code must not be empty.')
|
|
93
|
+
|
|
94
|
+
if value.isdigit():
|
|
95
|
+
return value
|
|
96
|
+
|
|
97
|
+
matches = self.find_stations(value)
|
|
98
|
+
if not matches:
|
|
99
|
+
raise RzdException(f'Station not found: {value}')
|
|
100
|
+
|
|
101
|
+
upper_value = value.upper()
|
|
102
|
+
exact_match = next((item for item in matches if item['station'].upper() == upper_value), None)
|
|
103
|
+
if exact_match:
|
|
104
|
+
return exact_match['code']
|
|
105
|
+
|
|
106
|
+
return matches[0]['code']
|
|
107
|
+
|
|
108
|
+
def _format_date(self, value: str | date | datetime) -> str:
|
|
109
|
+
if isinstance(value, datetime):
|
|
110
|
+
return value.strftime('%d.%m.%Y')
|
|
111
|
+
if isinstance(value, date):
|
|
112
|
+
return value.strftime('%d.%m.%Y')
|
|
113
|
+
return value
|
|
114
|
+
|
|
115
|
+
def _transport_type_code(self, transport_type: str) -> int:
|
|
116
|
+
try:
|
|
117
|
+
return self.TRANSPORT_TYPES[transport_type]
|
|
118
|
+
except KeyError as exc:
|
|
119
|
+
allowed = ', '.join(sorted(self.TRANSPORT_TYPES))
|
|
120
|
+
raise ValueError(
|
|
121
|
+
f'Unsupported transport_type: {transport_type}. Use one of: {allowed}.'
|
|
122
|
+
) from exc
|
rzd_api/config.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class Config:
|
|
7
|
+
language: str = 'ru'
|
|
8
|
+
timeout: float = 5.0
|
|
9
|
+
debug: bool = False
|
|
10
|
+
proxy: Optional[str] = None
|
|
11
|
+
user_agent: Optional[str] = None
|
|
12
|
+
referer: Optional[str] = None
|
rzd_api/query.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
import requests
|
|
4
|
+
import urllib3
|
|
5
|
+
|
|
6
|
+
from .config import Config
|
|
7
|
+
|
|
8
|
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RzdException(Exception):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Query:
|
|
18
|
+
def __init__(self, config: Config):
|
|
19
|
+
self.config = config
|
|
20
|
+
self.session = requests.Session()
|
|
21
|
+
self.session.verify = False
|
|
22
|
+
|
|
23
|
+
if config.debug:
|
|
24
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
25
|
+
from http.client import HTTPConnection
|
|
26
|
+
HTTPConnection.debuglevel = 1
|
|
27
|
+
|
|
28
|
+
headers = {'Accept': 'application/json'}
|
|
29
|
+
if config.user_agent:
|
|
30
|
+
headers['User-Agent'] = config.user_agent
|
|
31
|
+
if config.referer:
|
|
32
|
+
headers['Referer'] = config.referer
|
|
33
|
+
self.session.headers.update(headers)
|
|
34
|
+
|
|
35
|
+
if config.proxy:
|
|
36
|
+
self.session.proxies = {
|
|
37
|
+
'http': config.proxy,
|
|
38
|
+
'https': config.proxy,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
def get(self, path: str, params: dict, method: str = 'POST') -> dict | list:
|
|
42
|
+
return self._run(path, params, method)
|
|
43
|
+
|
|
44
|
+
def _run(self, path: str, params: dict, method: str) -> dict | list:
|
|
45
|
+
rid = None
|
|
46
|
+
content = None
|
|
47
|
+
|
|
48
|
+
for attempt in range(10):
|
|
49
|
+
request_params = dict(params)
|
|
50
|
+
if rid is not None:
|
|
51
|
+
request_params['rid'] = rid
|
|
52
|
+
|
|
53
|
+
logger.debug('[attempt %d] %s %s params=%s', attempt + 1, method, path, request_params)
|
|
54
|
+
|
|
55
|
+
if method == 'GET':
|
|
56
|
+
response = self.session.get(path, params=request_params, timeout=self.config.timeout)
|
|
57
|
+
else:
|
|
58
|
+
response = self.session.post(path, data=request_params, timeout=self.config.timeout)
|
|
59
|
+
|
|
60
|
+
response.raise_for_status()
|
|
61
|
+
content = response.json()
|
|
62
|
+
|
|
63
|
+
result = content.get('result', 'OK') if isinstance(content, dict) else 'OK'
|
|
64
|
+
logger.debug('[attempt %d] result=%s', attempt + 1, result)
|
|
65
|
+
|
|
66
|
+
if result in ('RID', 'REQUEST_ID'):
|
|
67
|
+
rid = self._get_rid(content)
|
|
68
|
+
time.sleep(1)
|
|
69
|
+
elif result == 'OK':
|
|
70
|
+
try:
|
|
71
|
+
msg = content['tp'][0]['msgList'][0]['message']
|
|
72
|
+
raise RzdException(msg)
|
|
73
|
+
except (KeyError, IndexError, TypeError):
|
|
74
|
+
pass
|
|
75
|
+
return content
|
|
76
|
+
else:
|
|
77
|
+
msg = content.get('message', 'Failed to get request data!') if isinstance(content, dict) else 'Failed to get request data!'
|
|
78
|
+
raise RzdException(msg)
|
|
79
|
+
|
|
80
|
+
return content
|
|
81
|
+
|
|
82
|
+
def _get_rid(self, content: dict) -> str:
|
|
83
|
+
for key in ('rid', 'RID'):
|
|
84
|
+
if key in content:
|
|
85
|
+
return str(content[key])
|
|
86
|
+
raise RzdException('Rid not found!')
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rzd-api
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python client for RZD (Russian Railways) pass.rzd.ru API
|
|
5
|
+
Author: drgod
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/drGOD/rzd-api
|
|
8
|
+
Project-URL: Repository, https://github.com/drGOD/rzd-api
|
|
9
|
+
Project-URL: Issues, https://github.com/drGOD/rzd-api/issues
|
|
10
|
+
Keywords: rzd,railway,trains,tickets,api,mcp
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
Requires-Dist: requests>=2.31.0
|
|
26
|
+
Requires-Dist: urllib3>=2.0.0
|
|
27
|
+
Provides-Extra: mcp
|
|
28
|
+
Requires-Dist: mcp>=1.0.0; extra == "mcp"
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
31
|
+
Requires-Dist: rzd-api[mcp]; extra == "dev"
|
|
32
|
+
|
|
33
|
+
# RZD API — Python
|
|
34
|
+
|
|
35
|
+
Python-клиент для API сайта [pass.rzd.ru](https://pass.rzd.ru) (РЖД).
|
|
36
|
+
Включает MCP-сервер для интеграции с Claude и другими MCP-совместимыми клиентами.
|
|
37
|
+
|
|
38
|
+
## Возможности
|
|
39
|
+
|
|
40
|
+
- Маршруты в одну сторону
|
|
41
|
+
- Маршруты туда-обратно
|
|
42
|
+
- Список вагонов (схема, цены, свободные места)
|
|
43
|
+
- Список станций по маршруту следования поезда
|
|
44
|
+
- Поиск кода станции по части названия
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Установка
|
|
49
|
+
|
|
50
|
+
**Из PyPI после публикации:**
|
|
51
|
+
```sh
|
|
52
|
+
pip install rzd-api
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Как библиотека без MCP-зависимостей:**
|
|
56
|
+
```sh
|
|
57
|
+
pip install rzd-api
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**С поддержкой MCP-сервера:**
|
|
61
|
+
```sh
|
|
62
|
+
pip install "rzd-api[mcp]"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Напрямую из GitHub:**
|
|
66
|
+
```sh
|
|
67
|
+
pip install "git+https://github.com/drGOD/rzd-api.git"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Напрямую из GitHub c MCP-сервером:**
|
|
71
|
+
```sh
|
|
72
|
+
pip install "rzd-api[mcp] @ git+https://github.com/drGOD/rzd-api.git"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Из исходников:**
|
|
76
|
+
```sh
|
|
77
|
+
pip install .
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Из исходников c MCP-сервером:**
|
|
81
|
+
```sh
|
|
82
|
+
pip install ".[mcp]"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Для разработки (с тестами):**
|
|
86
|
+
```sh
|
|
87
|
+
pip install -e ".[dev]"
|
|
88
|
+
# или
|
|
89
|
+
make install
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Проверка импорта после установки:**
|
|
93
|
+
```sh
|
|
94
|
+
python -c "from rzd_api import RzdClient; print(RzdClient.__name__)"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Проверка MCP-команды после установки extra:**
|
|
98
|
+
```sh
|
|
99
|
+
rzd-mcp-server
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Быстрый старт
|
|
105
|
+
|
|
106
|
+
### Как библиотека
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from datetime import date
|
|
110
|
+
from rzd_api import RzdClient
|
|
111
|
+
|
|
112
|
+
client = RzdClient()
|
|
113
|
+
|
|
114
|
+
# Можно передавать названия станций, клиент сам найдёт их коды
|
|
115
|
+
tickets = client.search_tickets(
|
|
116
|
+
from_station='Санкт-Петербург',
|
|
117
|
+
to_station='Москва',
|
|
118
|
+
departure_date=date(2026, 4, 1),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
for train in tickets[:3]:
|
|
122
|
+
print(train['number'], train['route0'], train['route1'], train['time0'], train['time1'])
|
|
123
|
+
|
|
124
|
+
# Туда-обратно
|
|
125
|
+
round_trip = client.search_tickets(
|
|
126
|
+
from_station='Санкт-Петербург',
|
|
127
|
+
to_station='Москва',
|
|
128
|
+
departure_date='01.04.2026',
|
|
129
|
+
return_date='05.04.2026',
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Поиск станций и получение кода
|
|
133
|
+
stations = client.find_stations('Чеб')
|
|
134
|
+
code = client.resolve_station_code('Москва')
|
|
135
|
+
|
|
136
|
+
# Детали по вагонам
|
|
137
|
+
cars = client.get_carriages(
|
|
138
|
+
from_station='Санкт-Петербург',
|
|
139
|
+
to_station='Москва',
|
|
140
|
+
departure_date='01.04.2026',
|
|
141
|
+
departure_time='22:30',
|
|
142
|
+
train_number='054А',
|
|
143
|
+
)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Низкоуровневый API
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from datetime import datetime, timedelta
|
|
150
|
+
from rzd_api import Api, Config
|
|
151
|
+
|
|
152
|
+
config = Config(
|
|
153
|
+
language='ru', # язык ответа: 'ru' или 'en'
|
|
154
|
+
timeout=10.0, # таймаут запроса в секундах
|
|
155
|
+
user_agent='Mozilla/5.0 ...',
|
|
156
|
+
referer='https://ticket.rzd.ru/',
|
|
157
|
+
# proxy='https://user:pass@host:port',
|
|
158
|
+
# debug=True, # включить HTTP-лог
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
api = Api(config) # config необязателен
|
|
162
|
+
|
|
163
|
+
tomorrow = (datetime.now() + timedelta(days=1)).strftime('%d.%m.%Y')
|
|
164
|
+
|
|
165
|
+
# Маршруты Санкт-Петербург → Москва
|
|
166
|
+
routes = api.train_routes({
|
|
167
|
+
'dir': 0, # 0 — в одну сторону
|
|
168
|
+
'tfl': 3, # 3 — поезда и электрички, 1 — только поезда, 2 — только электрички
|
|
169
|
+
'checkSeats': 1, # 1 — только с билетами, 0 — все поезда
|
|
170
|
+
'code0': '2004000', # код станции отправления
|
|
171
|
+
'code1': '2000000', # код станции прибытия
|
|
172
|
+
'dt0': tomorrow, # дата отправления dd.mm.yyyy
|
|
173
|
+
'md': 0, # 0 — без пересадок, 1 — с пересадками
|
|
174
|
+
})
|
|
175
|
+
print(routes) # JSON-строка
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## API
|
|
181
|
+
|
|
182
|
+
### `RzdClient(config=None, api=None)`
|
|
183
|
+
|
|
184
|
+
Высокоуровневый интерфейс для использования пакета как библиотеки.
|
|
185
|
+
Методы возвращают обычные Python-объекты (`list` / `dict`) и принимают станции как коды или названия.
|
|
186
|
+
|
|
187
|
+
| Метод | Описание |
|
|
188
|
+
|---|---|
|
|
189
|
+
| `search_tickets(from_station, to_station, departure_date, return_date=None, *, only_with_seats=True, include_transfers=False, transport_type='all')` | Удобный поиск билетов |
|
|
190
|
+
| `find_stations(query, compact_mode='y')` | Поиск станций по части названия |
|
|
191
|
+
| `resolve_station_code(station)` | Получить код станции по названию или вернуть переданный код |
|
|
192
|
+
| `get_carriages(from_station, to_station, departure_date, departure_time, train_number)` | Вагоны и свободные места |
|
|
193
|
+
| `get_route_stations(train_number, departure_date)` | Список станций маршрута |
|
|
194
|
+
|
|
195
|
+
#### `search_tickets`
|
|
196
|
+
|
|
197
|
+
| Параметр | Описание |
|
|
198
|
+
|---|---|
|
|
199
|
+
| `from_station` / `to_station` | Код станции (`2004000`) или название (`Санкт-Петербург`) |
|
|
200
|
+
| `departure_date` / `return_date` | `str`, `datetime.date` или `datetime.datetime` |
|
|
201
|
+
| `only_with_seats` | `True` — только поезда с билетами |
|
|
202
|
+
| `include_transfers` | `True` — искать варианты с пересадками |
|
|
203
|
+
| `transport_type` | `'trains'`, `'suburban'`, `'all'` |
|
|
204
|
+
|
|
205
|
+
### `Api(config=None)`
|
|
206
|
+
|
|
207
|
+
Низкоуровневый совместимый интерфейс. Методы ниже сохраняют текущее поведение и возвращают **JSON-строку**.
|
|
208
|
+
Если нужен Python-объект без `json.loads`, можно использовать парные методы с суффиксом `_data`.
|
|
209
|
+
|
|
210
|
+
| Метод | Описание |
|
|
211
|
+
|---|---|
|
|
212
|
+
| `train_routes(params)` | Маршруты в одну сторону |
|
|
213
|
+
| `train_routes_data(params)` | Маршруты в одну сторону как `list[dict]` |
|
|
214
|
+
| `train_routes_return(params)` | Маршруты туда-обратно |
|
|
215
|
+
| `train_routes_return_data(params)` | Маршруты туда-обратно как `dict` |
|
|
216
|
+
| `train_carriages(params)` | Вагоны и свободные места |
|
|
217
|
+
| `train_carriages_data(params)` | Вагоны и свободные места как `dict` |
|
|
218
|
+
| `train_station_list(params)` | Все станции на маршруте поезда |
|
|
219
|
+
| `train_station_list_data(params)` | Все станции на маршруте как `dict` |
|
|
220
|
+
| `station_code(params)` | Поиск кода станции по части названия |
|
|
221
|
+
| `station_code_data(params)` | Поиск кода станции как `list[dict]` |
|
|
222
|
+
|
|
223
|
+
#### `train_routes` / `train_routes_return`
|
|
224
|
+
|
|
225
|
+
| Параметр | Описание |
|
|
226
|
+
|---|---|
|
|
227
|
+
| `code0` | Код станции отправления |
|
|
228
|
+
| `code1` | Код станции прибытия |
|
|
229
|
+
| `dt0` | Дата отправления `dd.mm.yyyy` |
|
|
230
|
+
| `dt1` | Дата возврата `dd.mm.yyyy` (только для `train_routes_return`) |
|
|
231
|
+
| `dir` | `0` — в одну сторону, `1` — туда-обратно |
|
|
232
|
+
| `tfl` | `1` — поезда, `2` — электрички, `3` — всё |
|
|
233
|
+
| `checkSeats` | `1` — только с билетами, `0` — все |
|
|
234
|
+
| `md` | `0` — без пересадок, `1` — с пересадками |
|
|
235
|
+
|
|
236
|
+
#### `train_carriages`
|
|
237
|
+
|
|
238
|
+
| Параметр | Описание |
|
|
239
|
+
|---|---|
|
|
240
|
+
| `code0` / `code1` | Коды станций |
|
|
241
|
+
| `dt0` | Дата отправления `dd.mm.yyyy` |
|
|
242
|
+
| `time0` | Время отправления `HH:MM` |
|
|
243
|
+
| `tnum0` | Номер поезда (например `054Г`) |
|
|
244
|
+
|
|
245
|
+
#### `train_station_list`
|
|
246
|
+
|
|
247
|
+
| Параметр | Описание |
|
|
248
|
+
|---|---|
|
|
249
|
+
| `trainNumber` | Номер поезда (например `054Г`) |
|
|
250
|
+
| `depDate` | Дата отправления `dd.mm.yyyy` |
|
|
251
|
+
|
|
252
|
+
#### `station_code`
|
|
253
|
+
|
|
254
|
+
| Параметр | Описание |
|
|
255
|
+
|---|---|
|
|
256
|
+
| `stationNamePart` | Часть названия станции (мин. 2 символа, например `ЧЕБ`) |
|
|
257
|
+
| `compactMode` | Формат ответа, по умолчанию `y` |
|
|
258
|
+
|
|
259
|
+
### `Config`
|
|
260
|
+
|
|
261
|
+
| Поле | По умолчанию | Описание |
|
|
262
|
+
|---|---|---|
|
|
263
|
+
| `language` | `'ru'` | Язык ответа (`'ru'`, `'en'`) |
|
|
264
|
+
| `timeout` | `5.0` | Таймаут запроса (сек) |
|
|
265
|
+
| `debug` | `False` | HTTP-лог в stderr (уровень DEBUG) |
|
|
266
|
+
| `proxy` | `None` | URL прокси |
|
|
267
|
+
| `user_agent` | `None` | User-Agent |
|
|
268
|
+
| `referer` | `None` | Referer |
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## MCP-сервер
|
|
273
|
+
|
|
274
|
+
MCP-сервер позволяет использовать API РЖД напрямую из Claude Desktop или любого MCP-клиента.
|
|
275
|
+
|
|
276
|
+
Для локального запуска нужен extra `mcp`:
|
|
277
|
+
```sh
|
|
278
|
+
pip install "rzd-api[mcp]"
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Инструменты
|
|
282
|
+
|
|
283
|
+
| Инструмент | Описание |
|
|
284
|
+
|---|---|
|
|
285
|
+
| `train_routes` | Поиск поездов в одну сторону |
|
|
286
|
+
| `train_routes_return` | Поиск поездов туда-обратно |
|
|
287
|
+
| `train_carriages` | Информация о вагонах |
|
|
288
|
+
| `train_station_list` | Станции на маршруте |
|
|
289
|
+
| `station_code` | Поиск кода станции |
|
|
290
|
+
|
|
291
|
+
### Запуск локально (stdio)
|
|
292
|
+
|
|
293
|
+
```sh
|
|
294
|
+
rzd-mcp-server
|
|
295
|
+
# или
|
|
296
|
+
make run
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**Настройка в Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
300
|
+
|
|
301
|
+
```json
|
|
302
|
+
{
|
|
303
|
+
"mcpServers": {
|
|
304
|
+
"rzd": {
|
|
305
|
+
"command": "rzd-mcp-server"
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Запуск через Docker (HTTP)
|
|
312
|
+
|
|
313
|
+
```sh
|
|
314
|
+
docker compose up -d
|
|
315
|
+
# или
|
|
316
|
+
make docker-up
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Сервер запустится на `http://localhost:8000` (транспорт `streamable-http`).
|
|
320
|
+
|
|
321
|
+
**Настройка в Claude Desktop** для удалённого сервера:
|
|
322
|
+
|
|
323
|
+
```json
|
|
324
|
+
{
|
|
325
|
+
"mcpServers": {
|
|
326
|
+
"rzd": {
|
|
327
|
+
"type": "streamable-http",
|
|
328
|
+
"url": "http://localhost:8000/mcp"
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Переменные окружения MCP-сервера
|
|
335
|
+
|
|
336
|
+
| Переменная | По умолчанию | Описание |
|
|
337
|
+
|---|---|---|
|
|
338
|
+
| `MCP_TRANSPORT` | `stdio` | Транспорт: `stdio`, `sse`, `streamable-http` |
|
|
339
|
+
| `MCP_HOST` | `0.0.0.0` | Хост для HTTP-транспортов |
|
|
340
|
+
| `MCP_PORT` | `8000` | Порт для HTTP-транспортов |
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
## Docker
|
|
345
|
+
|
|
346
|
+
```sh
|
|
347
|
+
# Сборка образа
|
|
348
|
+
docker build -t rzd-api .
|
|
349
|
+
|
|
350
|
+
# Запуск
|
|
351
|
+
docker run -p 8000:8000 rzd-api
|
|
352
|
+
|
|
353
|
+
# С кастомным портом
|
|
354
|
+
docker run -p 9000:9000 -e MCP_PORT=9000 rzd-api
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## Тесты
|
|
360
|
+
|
|
361
|
+
```sh
|
|
362
|
+
pytest tests/ -v
|
|
363
|
+
# или
|
|
364
|
+
make test
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Тесты интеграционные — работают с живым API pass.rzd.ru.
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## Как работает протокол RZD
|
|
372
|
+
|
|
373
|
+
1. Первый запрос возвращает статус `RID` / `REQUEST_ID` + cookie
|
|
374
|
+
2. Повторный запрос с тем же `rid` и cookie возвращает статус `OK` с данными
|
|
375
|
+
3. Библиотека управляет сессией и повторными запросами автоматически (до 10 попыток, интервал 1 сек)
|
|
376
|
+
|
|
377
|
+
## Популярные коды станций
|
|
378
|
+
|
|
379
|
+
| Станция | Код |
|
|
380
|
+
|---|---|
|
|
381
|
+
| Санкт-Петербург Главный | `2004000` |
|
|
382
|
+
| Москва (Ленинградский вокзал) | `2000000` |
|
|
383
|
+
| Москва (Казанский вокзал) | `2000001` |
|
|
384
|
+
| Новосибирск Главный | `2060600` |
|
|
385
|
+
| Екатеринбург Пасс. | `2030000` |
|
|
386
|
+
|
|
387
|
+
Для поиска кода любой станции используйте метод `station_code`.
|
|
388
|
+
|
|
389
|
+
## Лицензия
|
|
390
|
+
|
|
391
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
mcp_server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
mcp_server/server.py,sha256=njffuItSxAkVx8o1a9iy_V7HA71XMZAjdE2MRX8Rpo4,5369
|
|
3
|
+
rzd_api/__init__.py,sha256=PiuGytV_vaPG9HDB5EuF3b-EQLd3cnDHIBORF6_yxJI,184
|
|
4
|
+
rzd_api/api.py,sha256=oZTxs8PpucROzcQP38ONrfUsgDCI2BhXIFk2M8sD9Jc,4233
|
|
5
|
+
rzd_api/client.py,sha256=TsrfuybBbCyCiAYl_tFKJpwDpJdkTsX4126jB7g5v5A,4155
|
|
6
|
+
rzd_api/config.py,sha256=4hKeH2W3QlK2gp4fqtZJNKBBgjYc4-p2NHT12ZsjkXE,266
|
|
7
|
+
rzd_api/query.py,sha256=UcwooXQRbNp7qyKrKtsXCHS0A9ppKZs9lMYbjpHmwpk,2795
|
|
8
|
+
rzd_api-1.0.0.dist-info/METADATA,sha256=iTre5wdUUZ2dYQTm820zDulpPSX_DY1bYRbs4M-SgJc,13339
|
|
9
|
+
rzd_api-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
10
|
+
rzd_api-1.0.0.dist-info/entry_points.txt,sha256=XnLatRdzqeiez4AHnpDyVc5SbxV3iAql7AsS-lbzWD8,58
|
|
11
|
+
rzd_api-1.0.0.dist-info/top_level.txt,sha256=NsYXt3dQpIaUpdga9uXm3JZUXkBtZWsXIzIB3Jm8K_k,19
|
|
12
|
+
rzd_api-1.0.0.dist-info/RECORD,,
|