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 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
@@ -0,0 +1,6 @@
1
+ from .api import Api
2
+ from .client import RzdClient
3
+ from .config import Config
4
+ from .query import Query, RzdException
5
+
6
+ __all__ = ['Api', 'RzdClient', 'Config', 'Query', 'RzdException']
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,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ rzd-mcp-server = mcp_server.server:main
@@ -0,0 +1,2 @@
1
+ mcp_server
2
+ rzd_api