py-nymta 0.1.0__tar.gz
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.
- py_nymta-0.1.0/LICENSE +21 -0
- py_nymta-0.1.0/MANIFEST.in +3 -0
- py_nymta-0.1.0/PKG-INFO +216 -0
- py_nymta-0.1.0/README.md +194 -0
- py_nymta-0.1.0/py_nymta.egg-info/PKG-INFO +216 -0
- py_nymta-0.1.0/py_nymta.egg-info/SOURCES.txt +14 -0
- py_nymta-0.1.0/py_nymta.egg-info/dependency_links.txt +1 -0
- py_nymta-0.1.0/py_nymta.egg-info/requires.txt +2 -0
- py_nymta-0.1.0/py_nymta.egg-info/top_level.txt +1 -0
- py_nymta-0.1.0/pymta/__init__.py +156 -0
- py_nymta-0.1.0/pymta/constants.py +53 -0
- py_nymta-0.1.0/pymta/models.py +25 -0
- py_nymta-0.1.0/pymta/py.typed +0 -0
- py_nymta-0.1.0/pyproject.toml +35 -0
- py_nymta-0.1.0/setup.cfg +4 -0
- py_nymta-0.1.0/tests/test_subway_feed.py +167 -0
py_nymta-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 py-nymta Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
py_nymta-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: py-nymta
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python library for accessing MTA (Metropolitan Transportation Authority) real-time transit data for NYC
|
|
5
|
+
Author: py-nymta Contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/OnFreund/py-nymta
|
|
8
|
+
Project-URL: Issues, https://github.com/OnFreund/py-nymta/issues
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: gtfs-realtime-bindings>=1.0.0
|
|
20
|
+
Requires-Dist: requests>=2.31.0
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# py-nymta
|
|
24
|
+
|
|
25
|
+
Python library for accessing MTA (Metropolitan Transportation Authority) real-time transit data for NYC.
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
- Simple, clean API for accessing MTA subway real-time arrival data
|
|
30
|
+
- Support for all MTA subway lines
|
|
31
|
+
- Compatible with protobuf 6.x
|
|
32
|
+
- Type hints for better IDE support
|
|
33
|
+
- Extensible design for future bus API support
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install py-nymta
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
### Basic Example
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from pymta import SubwayFeed
|
|
47
|
+
|
|
48
|
+
# Create a feed for the N/Q/R/W lines
|
|
49
|
+
feed = SubwayFeed(feed_id="N")
|
|
50
|
+
|
|
51
|
+
# Get the next 3 arrivals for the Q line at station B08S (southbound)
|
|
52
|
+
arrivals = feed.get_arrivals(route_id="Q", stop_id="B08S")
|
|
53
|
+
|
|
54
|
+
for arrival in arrivals:
|
|
55
|
+
print(f"Route {arrival.route_id} to {arrival.destination}")
|
|
56
|
+
print(f" Arrives at: {arrival.arrival_time}")
|
|
57
|
+
print(f" Stop ID: {arrival.stop_id}")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Finding the Feed ID for a Route
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from pymta import SubwayFeed
|
|
64
|
+
|
|
65
|
+
# Get the feed ID for a specific route
|
|
66
|
+
feed_id = SubwayFeed.get_feed_id_for_route("Q")
|
|
67
|
+
print(f"The Q line is in feed: {feed_id}") # Output: N
|
|
68
|
+
|
|
69
|
+
# Create a feed using the discovered feed_id
|
|
70
|
+
feed = SubwayFeed(feed_id=feed_id)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Custom Timeout and Max Arrivals
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from pymta import SubwayFeed
|
|
77
|
+
|
|
78
|
+
# Create a feed with custom timeout
|
|
79
|
+
feed = SubwayFeed(feed_id="1", timeout=60)
|
|
80
|
+
|
|
81
|
+
# Get up to 5 arrivals instead of the default 3
|
|
82
|
+
arrivals = feed.get_arrivals(
|
|
83
|
+
route_id="1",
|
|
84
|
+
stop_id="127N", # Times Square - 42 St (northbound)
|
|
85
|
+
max_arrivals=5
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Error Handling
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from pymta import SubwayFeed, MTAFeedError
|
|
93
|
+
|
|
94
|
+
feed = SubwayFeed(feed_id="A")
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
arrivals = feed.get_arrivals(route_id="A", stop_id="A42N")
|
|
98
|
+
except MTAFeedError as e:
|
|
99
|
+
print(f"Error fetching arrivals: {e}")
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Station IDs and Directions
|
|
103
|
+
|
|
104
|
+
MTA station IDs include a direction suffix:
|
|
105
|
+
- `N` suffix: Northbound/Uptown direction
|
|
106
|
+
- `S` suffix: Southbound/Downtown direction
|
|
107
|
+
|
|
108
|
+
For example:
|
|
109
|
+
- `127N`: Times Square - 42 St (northbound)
|
|
110
|
+
- `127S`: Times Square - 42 St (southbound)
|
|
111
|
+
- `B08N`: DeKalb Av (northbound)
|
|
112
|
+
- `B08S`: DeKalb Av (southbound)
|
|
113
|
+
|
|
114
|
+
**Note**: These are MTA designations and don't always correspond to geographic north/south.
|
|
115
|
+
|
|
116
|
+
## Feed IDs
|
|
117
|
+
|
|
118
|
+
The MTA groups subway lines into feeds:
|
|
119
|
+
|
|
120
|
+
| Feed ID | Lines |
|
|
121
|
+
|---------|-------|
|
|
122
|
+
| `1` | 1, 2, 3, 4, 5, 6, GS |
|
|
123
|
+
| `A` | A, C, E, H, FS |
|
|
124
|
+
| `N` | N, Q, R, W |
|
|
125
|
+
| `B` | B, D, F, M |
|
|
126
|
+
| `L` | L |
|
|
127
|
+
| `SI` | SIR (Staten Island Railway) |
|
|
128
|
+
| `G` | G |
|
|
129
|
+
| `J` | J, Z |
|
|
130
|
+
| `7` | 7, 7X |
|
|
131
|
+
|
|
132
|
+
## API Reference
|
|
133
|
+
|
|
134
|
+
### `SubwayFeed`
|
|
135
|
+
|
|
136
|
+
Main class for accessing subway GTFS-RT feeds.
|
|
137
|
+
|
|
138
|
+
#### `__init__(feed_id: str, timeout: int = 30)`
|
|
139
|
+
|
|
140
|
+
Initialize the subway feed.
|
|
141
|
+
|
|
142
|
+
**Parameters:**
|
|
143
|
+
- `feed_id`: The feed ID (e.g., '1', 'A', 'N', 'B', 'L', 'SI', 'G', 'J', '7')
|
|
144
|
+
- `timeout`: Request timeout in seconds (default: 30)
|
|
145
|
+
|
|
146
|
+
**Raises:**
|
|
147
|
+
- `ValueError`: If feed_id is not valid
|
|
148
|
+
|
|
149
|
+
#### `get_arrivals(route_id: str, stop_id: str, max_arrivals: int = 3) -> list[Arrival]`
|
|
150
|
+
|
|
151
|
+
Get upcoming train arrivals for a specific route and stop.
|
|
152
|
+
|
|
153
|
+
**Parameters:**
|
|
154
|
+
- `route_id`: The route/line ID (e.g., '1', 'A', 'Q')
|
|
155
|
+
- `stop_id`: The stop ID including direction (e.g., '127N', 'B08S')
|
|
156
|
+
- `max_arrivals`: Maximum number of arrivals to return (default: 3)
|
|
157
|
+
|
|
158
|
+
**Returns:**
|
|
159
|
+
- List of `Arrival` objects sorted by arrival time
|
|
160
|
+
|
|
161
|
+
**Raises:**
|
|
162
|
+
- `MTAFeedError`: If feed cannot be fetched or parsed
|
|
163
|
+
|
|
164
|
+
#### `get_feed_id_for_route(route_id: str) -> str` (static method)
|
|
165
|
+
|
|
166
|
+
Get the feed ID for a given route.
|
|
167
|
+
|
|
168
|
+
**Parameters:**
|
|
169
|
+
- `route_id`: The route/line ID (e.g., '1', 'A', 'Q')
|
|
170
|
+
|
|
171
|
+
**Returns:**
|
|
172
|
+
- The feed ID for the route
|
|
173
|
+
|
|
174
|
+
**Raises:**
|
|
175
|
+
- `ValueError`: If route_id is not valid
|
|
176
|
+
|
|
177
|
+
### `Arrival`
|
|
178
|
+
|
|
179
|
+
Dataclass representing a single train arrival.
|
|
180
|
+
|
|
181
|
+
**Attributes:**
|
|
182
|
+
- `arrival_time` (datetime): The datetime when the train will arrive (UTC)
|
|
183
|
+
- `route_id` (str): The route/line ID (e.g., '1', 'A', 'Q')
|
|
184
|
+
- `stop_id` (str): The stop ID including direction (e.g., '127N', 'B08S')
|
|
185
|
+
- `destination` (str): The trip headsign/destination
|
|
186
|
+
|
|
187
|
+
### Exceptions
|
|
188
|
+
|
|
189
|
+
- `MTAError`: Base exception for the library
|
|
190
|
+
- `MTAFeedError`: Raised when feed cannot be fetched or parsed
|
|
191
|
+
|
|
192
|
+
## Development
|
|
193
|
+
|
|
194
|
+
### Setup
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
git clone https://github.com/OnFreund/py-nymta.git
|
|
198
|
+
cd py-nymta
|
|
199
|
+
pip install -e .
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Running Tests
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
pytest
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## License
|
|
209
|
+
|
|
210
|
+
MIT License - see LICENSE file for details.
|
|
211
|
+
|
|
212
|
+
## Credits
|
|
213
|
+
|
|
214
|
+
This library uses the official GTFS-RT protocol buffers from Google's [gtfs-realtime-bindings](https://github.com/MobilityData/gtfs-realtime-bindings) package.
|
|
215
|
+
|
|
216
|
+
MTA data is provided by the [Metropolitan Transportation Authority](https://www.mta.info/).
|
py_nymta-0.1.0/README.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# py-nymta
|
|
2
|
+
|
|
3
|
+
Python library for accessing MTA (Metropolitan Transportation Authority) real-time transit data for NYC.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Simple, clean API for accessing MTA subway real-time arrival data
|
|
8
|
+
- Support for all MTA subway lines
|
|
9
|
+
- Compatible with protobuf 6.x
|
|
10
|
+
- Type hints for better IDE support
|
|
11
|
+
- Extensible design for future bus API support
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install py-nymta
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### Basic Example
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from pymta import SubwayFeed
|
|
25
|
+
|
|
26
|
+
# Create a feed for the N/Q/R/W lines
|
|
27
|
+
feed = SubwayFeed(feed_id="N")
|
|
28
|
+
|
|
29
|
+
# Get the next 3 arrivals for the Q line at station B08S (southbound)
|
|
30
|
+
arrivals = feed.get_arrivals(route_id="Q", stop_id="B08S")
|
|
31
|
+
|
|
32
|
+
for arrival in arrivals:
|
|
33
|
+
print(f"Route {arrival.route_id} to {arrival.destination}")
|
|
34
|
+
print(f" Arrives at: {arrival.arrival_time}")
|
|
35
|
+
print(f" Stop ID: {arrival.stop_id}")
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Finding the Feed ID for a Route
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from pymta import SubwayFeed
|
|
42
|
+
|
|
43
|
+
# Get the feed ID for a specific route
|
|
44
|
+
feed_id = SubwayFeed.get_feed_id_for_route("Q")
|
|
45
|
+
print(f"The Q line is in feed: {feed_id}") # Output: N
|
|
46
|
+
|
|
47
|
+
# Create a feed using the discovered feed_id
|
|
48
|
+
feed = SubwayFeed(feed_id=feed_id)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Custom Timeout and Max Arrivals
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from pymta import SubwayFeed
|
|
55
|
+
|
|
56
|
+
# Create a feed with custom timeout
|
|
57
|
+
feed = SubwayFeed(feed_id="1", timeout=60)
|
|
58
|
+
|
|
59
|
+
# Get up to 5 arrivals instead of the default 3
|
|
60
|
+
arrivals = feed.get_arrivals(
|
|
61
|
+
route_id="1",
|
|
62
|
+
stop_id="127N", # Times Square - 42 St (northbound)
|
|
63
|
+
max_arrivals=5
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Error Handling
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from pymta import SubwayFeed, MTAFeedError
|
|
71
|
+
|
|
72
|
+
feed = SubwayFeed(feed_id="A")
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
arrivals = feed.get_arrivals(route_id="A", stop_id="A42N")
|
|
76
|
+
except MTAFeedError as e:
|
|
77
|
+
print(f"Error fetching arrivals: {e}")
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Station IDs and Directions
|
|
81
|
+
|
|
82
|
+
MTA station IDs include a direction suffix:
|
|
83
|
+
- `N` suffix: Northbound/Uptown direction
|
|
84
|
+
- `S` suffix: Southbound/Downtown direction
|
|
85
|
+
|
|
86
|
+
For example:
|
|
87
|
+
- `127N`: Times Square - 42 St (northbound)
|
|
88
|
+
- `127S`: Times Square - 42 St (southbound)
|
|
89
|
+
- `B08N`: DeKalb Av (northbound)
|
|
90
|
+
- `B08S`: DeKalb Av (southbound)
|
|
91
|
+
|
|
92
|
+
**Note**: These are MTA designations and don't always correspond to geographic north/south.
|
|
93
|
+
|
|
94
|
+
## Feed IDs
|
|
95
|
+
|
|
96
|
+
The MTA groups subway lines into feeds:
|
|
97
|
+
|
|
98
|
+
| Feed ID | Lines |
|
|
99
|
+
|---------|-------|
|
|
100
|
+
| `1` | 1, 2, 3, 4, 5, 6, GS |
|
|
101
|
+
| `A` | A, C, E, H, FS |
|
|
102
|
+
| `N` | N, Q, R, W |
|
|
103
|
+
| `B` | B, D, F, M |
|
|
104
|
+
| `L` | L |
|
|
105
|
+
| `SI` | SIR (Staten Island Railway) |
|
|
106
|
+
| `G` | G |
|
|
107
|
+
| `J` | J, Z |
|
|
108
|
+
| `7` | 7, 7X |
|
|
109
|
+
|
|
110
|
+
## API Reference
|
|
111
|
+
|
|
112
|
+
### `SubwayFeed`
|
|
113
|
+
|
|
114
|
+
Main class for accessing subway GTFS-RT feeds.
|
|
115
|
+
|
|
116
|
+
#### `__init__(feed_id: str, timeout: int = 30)`
|
|
117
|
+
|
|
118
|
+
Initialize the subway feed.
|
|
119
|
+
|
|
120
|
+
**Parameters:**
|
|
121
|
+
- `feed_id`: The feed ID (e.g., '1', 'A', 'N', 'B', 'L', 'SI', 'G', 'J', '7')
|
|
122
|
+
- `timeout`: Request timeout in seconds (default: 30)
|
|
123
|
+
|
|
124
|
+
**Raises:**
|
|
125
|
+
- `ValueError`: If feed_id is not valid
|
|
126
|
+
|
|
127
|
+
#### `get_arrivals(route_id: str, stop_id: str, max_arrivals: int = 3) -> list[Arrival]`
|
|
128
|
+
|
|
129
|
+
Get upcoming train arrivals for a specific route and stop.
|
|
130
|
+
|
|
131
|
+
**Parameters:**
|
|
132
|
+
- `route_id`: The route/line ID (e.g., '1', 'A', 'Q')
|
|
133
|
+
- `stop_id`: The stop ID including direction (e.g., '127N', 'B08S')
|
|
134
|
+
- `max_arrivals`: Maximum number of arrivals to return (default: 3)
|
|
135
|
+
|
|
136
|
+
**Returns:**
|
|
137
|
+
- List of `Arrival` objects sorted by arrival time
|
|
138
|
+
|
|
139
|
+
**Raises:**
|
|
140
|
+
- `MTAFeedError`: If feed cannot be fetched or parsed
|
|
141
|
+
|
|
142
|
+
#### `get_feed_id_for_route(route_id: str) -> str` (static method)
|
|
143
|
+
|
|
144
|
+
Get the feed ID for a given route.
|
|
145
|
+
|
|
146
|
+
**Parameters:**
|
|
147
|
+
- `route_id`: The route/line ID (e.g., '1', 'A', 'Q')
|
|
148
|
+
|
|
149
|
+
**Returns:**
|
|
150
|
+
- The feed ID for the route
|
|
151
|
+
|
|
152
|
+
**Raises:**
|
|
153
|
+
- `ValueError`: If route_id is not valid
|
|
154
|
+
|
|
155
|
+
### `Arrival`
|
|
156
|
+
|
|
157
|
+
Dataclass representing a single train arrival.
|
|
158
|
+
|
|
159
|
+
**Attributes:**
|
|
160
|
+
- `arrival_time` (datetime): The datetime when the train will arrive (UTC)
|
|
161
|
+
- `route_id` (str): The route/line ID (e.g., '1', 'A', 'Q')
|
|
162
|
+
- `stop_id` (str): The stop ID including direction (e.g., '127N', 'B08S')
|
|
163
|
+
- `destination` (str): The trip headsign/destination
|
|
164
|
+
|
|
165
|
+
### Exceptions
|
|
166
|
+
|
|
167
|
+
- `MTAError`: Base exception for the library
|
|
168
|
+
- `MTAFeedError`: Raised when feed cannot be fetched or parsed
|
|
169
|
+
|
|
170
|
+
## Development
|
|
171
|
+
|
|
172
|
+
### Setup
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
git clone https://github.com/OnFreund/py-nymta.git
|
|
176
|
+
cd py-nymta
|
|
177
|
+
pip install -e .
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Running Tests
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
pytest
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
MIT License - see LICENSE file for details.
|
|
189
|
+
|
|
190
|
+
## Credits
|
|
191
|
+
|
|
192
|
+
This library uses the official GTFS-RT protocol buffers from Google's [gtfs-realtime-bindings](https://github.com/MobilityData/gtfs-realtime-bindings) package.
|
|
193
|
+
|
|
194
|
+
MTA data is provided by the [Metropolitan Transportation Authority](https://www.mta.info/).
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: py-nymta
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python library for accessing MTA (Metropolitan Transportation Authority) real-time transit data for NYC
|
|
5
|
+
Author: py-nymta Contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/OnFreund/py-nymta
|
|
8
|
+
Project-URL: Issues, https://github.com/OnFreund/py-nymta/issues
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: gtfs-realtime-bindings>=1.0.0
|
|
20
|
+
Requires-Dist: requests>=2.31.0
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# py-nymta
|
|
24
|
+
|
|
25
|
+
Python library for accessing MTA (Metropolitan Transportation Authority) real-time transit data for NYC.
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
- Simple, clean API for accessing MTA subway real-time arrival data
|
|
30
|
+
- Support for all MTA subway lines
|
|
31
|
+
- Compatible with protobuf 6.x
|
|
32
|
+
- Type hints for better IDE support
|
|
33
|
+
- Extensible design for future bus API support
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install py-nymta
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
### Basic Example
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from pymta import SubwayFeed
|
|
47
|
+
|
|
48
|
+
# Create a feed for the N/Q/R/W lines
|
|
49
|
+
feed = SubwayFeed(feed_id="N")
|
|
50
|
+
|
|
51
|
+
# Get the next 3 arrivals for the Q line at station B08S (southbound)
|
|
52
|
+
arrivals = feed.get_arrivals(route_id="Q", stop_id="B08S")
|
|
53
|
+
|
|
54
|
+
for arrival in arrivals:
|
|
55
|
+
print(f"Route {arrival.route_id} to {arrival.destination}")
|
|
56
|
+
print(f" Arrives at: {arrival.arrival_time}")
|
|
57
|
+
print(f" Stop ID: {arrival.stop_id}")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Finding the Feed ID for a Route
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from pymta import SubwayFeed
|
|
64
|
+
|
|
65
|
+
# Get the feed ID for a specific route
|
|
66
|
+
feed_id = SubwayFeed.get_feed_id_for_route("Q")
|
|
67
|
+
print(f"The Q line is in feed: {feed_id}") # Output: N
|
|
68
|
+
|
|
69
|
+
# Create a feed using the discovered feed_id
|
|
70
|
+
feed = SubwayFeed(feed_id=feed_id)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Custom Timeout and Max Arrivals
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from pymta import SubwayFeed
|
|
77
|
+
|
|
78
|
+
# Create a feed with custom timeout
|
|
79
|
+
feed = SubwayFeed(feed_id="1", timeout=60)
|
|
80
|
+
|
|
81
|
+
# Get up to 5 arrivals instead of the default 3
|
|
82
|
+
arrivals = feed.get_arrivals(
|
|
83
|
+
route_id="1",
|
|
84
|
+
stop_id="127N", # Times Square - 42 St (northbound)
|
|
85
|
+
max_arrivals=5
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Error Handling
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from pymta import SubwayFeed, MTAFeedError
|
|
93
|
+
|
|
94
|
+
feed = SubwayFeed(feed_id="A")
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
arrivals = feed.get_arrivals(route_id="A", stop_id="A42N")
|
|
98
|
+
except MTAFeedError as e:
|
|
99
|
+
print(f"Error fetching arrivals: {e}")
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Station IDs and Directions
|
|
103
|
+
|
|
104
|
+
MTA station IDs include a direction suffix:
|
|
105
|
+
- `N` suffix: Northbound/Uptown direction
|
|
106
|
+
- `S` suffix: Southbound/Downtown direction
|
|
107
|
+
|
|
108
|
+
For example:
|
|
109
|
+
- `127N`: Times Square - 42 St (northbound)
|
|
110
|
+
- `127S`: Times Square - 42 St (southbound)
|
|
111
|
+
- `B08N`: DeKalb Av (northbound)
|
|
112
|
+
- `B08S`: DeKalb Av (southbound)
|
|
113
|
+
|
|
114
|
+
**Note**: These are MTA designations and don't always correspond to geographic north/south.
|
|
115
|
+
|
|
116
|
+
## Feed IDs
|
|
117
|
+
|
|
118
|
+
The MTA groups subway lines into feeds:
|
|
119
|
+
|
|
120
|
+
| Feed ID | Lines |
|
|
121
|
+
|---------|-------|
|
|
122
|
+
| `1` | 1, 2, 3, 4, 5, 6, GS |
|
|
123
|
+
| `A` | A, C, E, H, FS |
|
|
124
|
+
| `N` | N, Q, R, W |
|
|
125
|
+
| `B` | B, D, F, M |
|
|
126
|
+
| `L` | L |
|
|
127
|
+
| `SI` | SIR (Staten Island Railway) |
|
|
128
|
+
| `G` | G |
|
|
129
|
+
| `J` | J, Z |
|
|
130
|
+
| `7` | 7, 7X |
|
|
131
|
+
|
|
132
|
+
## API Reference
|
|
133
|
+
|
|
134
|
+
### `SubwayFeed`
|
|
135
|
+
|
|
136
|
+
Main class for accessing subway GTFS-RT feeds.
|
|
137
|
+
|
|
138
|
+
#### `__init__(feed_id: str, timeout: int = 30)`
|
|
139
|
+
|
|
140
|
+
Initialize the subway feed.
|
|
141
|
+
|
|
142
|
+
**Parameters:**
|
|
143
|
+
- `feed_id`: The feed ID (e.g., '1', 'A', 'N', 'B', 'L', 'SI', 'G', 'J', '7')
|
|
144
|
+
- `timeout`: Request timeout in seconds (default: 30)
|
|
145
|
+
|
|
146
|
+
**Raises:**
|
|
147
|
+
- `ValueError`: If feed_id is not valid
|
|
148
|
+
|
|
149
|
+
#### `get_arrivals(route_id: str, stop_id: str, max_arrivals: int = 3) -> list[Arrival]`
|
|
150
|
+
|
|
151
|
+
Get upcoming train arrivals for a specific route and stop.
|
|
152
|
+
|
|
153
|
+
**Parameters:**
|
|
154
|
+
- `route_id`: The route/line ID (e.g., '1', 'A', 'Q')
|
|
155
|
+
- `stop_id`: The stop ID including direction (e.g., '127N', 'B08S')
|
|
156
|
+
- `max_arrivals`: Maximum number of arrivals to return (default: 3)
|
|
157
|
+
|
|
158
|
+
**Returns:**
|
|
159
|
+
- List of `Arrival` objects sorted by arrival time
|
|
160
|
+
|
|
161
|
+
**Raises:**
|
|
162
|
+
- `MTAFeedError`: If feed cannot be fetched or parsed
|
|
163
|
+
|
|
164
|
+
#### `get_feed_id_for_route(route_id: str) -> str` (static method)
|
|
165
|
+
|
|
166
|
+
Get the feed ID for a given route.
|
|
167
|
+
|
|
168
|
+
**Parameters:**
|
|
169
|
+
- `route_id`: The route/line ID (e.g., '1', 'A', 'Q')
|
|
170
|
+
|
|
171
|
+
**Returns:**
|
|
172
|
+
- The feed ID for the route
|
|
173
|
+
|
|
174
|
+
**Raises:**
|
|
175
|
+
- `ValueError`: If route_id is not valid
|
|
176
|
+
|
|
177
|
+
### `Arrival`
|
|
178
|
+
|
|
179
|
+
Dataclass representing a single train arrival.
|
|
180
|
+
|
|
181
|
+
**Attributes:**
|
|
182
|
+
- `arrival_time` (datetime): The datetime when the train will arrive (UTC)
|
|
183
|
+
- `route_id` (str): The route/line ID (e.g., '1', 'A', 'Q')
|
|
184
|
+
- `stop_id` (str): The stop ID including direction (e.g., '127N', 'B08S')
|
|
185
|
+
- `destination` (str): The trip headsign/destination
|
|
186
|
+
|
|
187
|
+
### Exceptions
|
|
188
|
+
|
|
189
|
+
- `MTAError`: Base exception for the library
|
|
190
|
+
- `MTAFeedError`: Raised when feed cannot be fetched or parsed
|
|
191
|
+
|
|
192
|
+
## Development
|
|
193
|
+
|
|
194
|
+
### Setup
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
git clone https://github.com/OnFreund/py-nymta.git
|
|
198
|
+
cd py-nymta
|
|
199
|
+
pip install -e .
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Running Tests
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
pytest
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## License
|
|
209
|
+
|
|
210
|
+
MIT License - see LICENSE file for details.
|
|
211
|
+
|
|
212
|
+
## Credits
|
|
213
|
+
|
|
214
|
+
This library uses the official GTFS-RT protocol buffers from Google's [gtfs-realtime-bindings](https://github.com/MobilityData/gtfs-realtime-bindings) package.
|
|
215
|
+
|
|
216
|
+
MTA data is provided by the [Metropolitan Transportation Authority](https://www.mta.info/).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
py_nymta.egg-info/PKG-INFO
|
|
6
|
+
py_nymta.egg-info/SOURCES.txt
|
|
7
|
+
py_nymta.egg-info/dependency_links.txt
|
|
8
|
+
py_nymta.egg-info/requires.txt
|
|
9
|
+
py_nymta.egg-info/top_level.txt
|
|
10
|
+
pymta/__init__.py
|
|
11
|
+
pymta/constants.py
|
|
12
|
+
pymta/models.py
|
|
13
|
+
pymta/py.typed
|
|
14
|
+
tests/test_subway_feed.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pymta
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""py-nymta library for accessing NYC transit real-time data."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
|
|
5
|
+
from google.protobuf.message import DecodeError
|
|
6
|
+
from google.transit import gtfs_realtime_pb2
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
from .constants import FEED_URLS, LINE_TO_FEED
|
|
10
|
+
from .models import Arrival
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.0"
|
|
13
|
+
__all__ = ["SubwayFeed", "Arrival", "MTAError", "MTAFeedError"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MTAError(Exception):
|
|
17
|
+
"""Base exception for py-nymta library."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MTAFeedError(MTAError):
|
|
21
|
+
"""Exception raised when feed cannot be fetched or parsed."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SubwayFeed:
|
|
25
|
+
"""Interface for MTA subway real-time feeds."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, feed_id: str, timeout: int = 30) -> None:
|
|
28
|
+
"""Initialize the subway feed.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
feed_id: The feed ID (e.g., '1', 'A', 'N', 'B', 'L', 'SI', 'G', 'J', '7').
|
|
32
|
+
timeout: Request timeout in seconds (default: 30).
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
ValueError: If feed_id is not valid.
|
|
36
|
+
"""
|
|
37
|
+
if feed_id not in FEED_URLS:
|
|
38
|
+
raise ValueError(
|
|
39
|
+
f"Invalid feed_id '{feed_id}'. "
|
|
40
|
+
f"Must be one of: {', '.join(FEED_URLS.keys())}"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
self.feed_id = feed_id
|
|
44
|
+
self.feed_url = FEED_URLS[feed_id]
|
|
45
|
+
self.timeout = timeout
|
|
46
|
+
|
|
47
|
+
def get_arrivals(
|
|
48
|
+
self,
|
|
49
|
+
route_id: str,
|
|
50
|
+
stop_id: str,
|
|
51
|
+
max_arrivals: int = 3,
|
|
52
|
+
) -> list[Arrival]:
|
|
53
|
+
"""Get upcoming train arrivals for a specific route and stop.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
route_id: The route/line ID (e.g., '1', 'A', 'Q').
|
|
57
|
+
stop_id: The stop ID including direction (e.g., '127N', 'B08S').
|
|
58
|
+
max_arrivals: Maximum number of arrivals to return (default: 3).
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
List of Arrival objects sorted by arrival time.
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
MTAFeedError: If feed cannot be fetched or parsed.
|
|
65
|
+
"""
|
|
66
|
+
# Fetch the GTFS-RT feed
|
|
67
|
+
try:
|
|
68
|
+
response = requests.get(self.feed_url, timeout=self.timeout)
|
|
69
|
+
response.raise_for_status()
|
|
70
|
+
except requests.exceptions.RequestException as err:
|
|
71
|
+
raise MTAFeedError(f"Error fetching GTFS-RT feed: {err}") from err
|
|
72
|
+
|
|
73
|
+
# Parse the protobuf
|
|
74
|
+
feed = gtfs_realtime_pb2.FeedMessage()
|
|
75
|
+
try:
|
|
76
|
+
feed.ParseFromString(response.content)
|
|
77
|
+
except DecodeError as err:
|
|
78
|
+
raise MTAFeedError(f"Error parsing GTFS-RT feed: {err}") from err
|
|
79
|
+
|
|
80
|
+
arrivals: list[Arrival] = []
|
|
81
|
+
now = datetime.now(timezone.utc)
|
|
82
|
+
|
|
83
|
+
# Get base station ID (without direction suffix) for flexible matching
|
|
84
|
+
base_station_id = stop_id.rstrip("NS")
|
|
85
|
+
direction_suffix = stop_id[-1] if stop_id and stop_id[-1] in ("N", "S") else ""
|
|
86
|
+
|
|
87
|
+
# Process each entity in the feed
|
|
88
|
+
for entity in feed.entity:
|
|
89
|
+
if not entity.HasField("trip_update"):
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
trip_update = entity.trip_update
|
|
93
|
+
trip = trip_update.trip
|
|
94
|
+
|
|
95
|
+
# Filter by route/line
|
|
96
|
+
if trip.route_id != route_id:
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
# Process stop time updates
|
|
100
|
+
for stop_time_update in trip_update.stop_time_update:
|
|
101
|
+
current_stop_id = stop_time_update.stop_id
|
|
102
|
+
|
|
103
|
+
# Match on base station ID and direction suffix
|
|
104
|
+
if (
|
|
105
|
+
current_stop_id
|
|
106
|
+
and current_stop_id.startswith(base_station_id)
|
|
107
|
+
and current_stop_id.endswith(direction_suffix)
|
|
108
|
+
and stop_time_update.HasField("arrival")
|
|
109
|
+
):
|
|
110
|
+
# Get the arrival time
|
|
111
|
+
arrival_timestamp = stop_time_update.arrival.time
|
|
112
|
+
arrival_time = datetime.fromtimestamp(
|
|
113
|
+
arrival_timestamp, tz=timezone.utc
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Only include future arrivals
|
|
117
|
+
if arrival_time > now:
|
|
118
|
+
# Get trip headsign if available
|
|
119
|
+
destination = "Unknown"
|
|
120
|
+
if trip.HasField("trip_headsign"):
|
|
121
|
+
destination = trip.trip_headsign
|
|
122
|
+
elif stop_time_update.HasField("stop_headsign"):
|
|
123
|
+
destination = stop_time_update.stop_headsign
|
|
124
|
+
|
|
125
|
+
arrivals.append(
|
|
126
|
+
Arrival(
|
|
127
|
+
arrival_time=arrival_time,
|
|
128
|
+
route_id=trip.route_id,
|
|
129
|
+
stop_id=current_stop_id,
|
|
130
|
+
destination=destination,
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Sort by arrival time and limit to max_arrivals
|
|
135
|
+
arrivals.sort()
|
|
136
|
+
return arrivals[:max_arrivals]
|
|
137
|
+
|
|
138
|
+
@staticmethod
|
|
139
|
+
def get_feed_id_for_route(route_id: str) -> str:
|
|
140
|
+
"""Get the feed ID for a given route.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
route_id: The route/line ID (e.g., '1', 'A', 'Q').
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
The feed ID for the route.
|
|
147
|
+
|
|
148
|
+
Raises:
|
|
149
|
+
ValueError: If route_id is not valid.
|
|
150
|
+
"""
|
|
151
|
+
if route_id not in LINE_TO_FEED:
|
|
152
|
+
raise ValueError(
|
|
153
|
+
f"Invalid route_id '{route_id}'. "
|
|
154
|
+
f"Must be one of: {', '.join(LINE_TO_FEED.keys())}"
|
|
155
|
+
)
|
|
156
|
+
return LINE_TO_FEED[route_id]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Constants for MTA GTFS-RT library."""
|
|
2
|
+
|
|
3
|
+
# MTA GTFS-RT feed URLs
|
|
4
|
+
FEED_URLS = {
|
|
5
|
+
"1": "https://api-endpoint.mta.info/Dataservice/mtagtfsfeeds/nyct%2Fgtfs",
|
|
6
|
+
"A": "https://api-endpoint.mta.info/Dataservice/mtagtfsfeeds/nyct%2Fgtfs-ace",
|
|
7
|
+
"N": "https://api-endpoint.mta.info/Dataservice/mtagtfsfeeds/nyct%2Fgtfs-nqrw",
|
|
8
|
+
"B": "https://api-endpoint.mta.info/Dataservice/mtagtfsfeeds/nyct%2Fgtfs-bdfm",
|
|
9
|
+
"L": "https://api-endpoint.mta.info/Dataservice/mtagtfsfeeds/nyct%2Fgtfs-l",
|
|
10
|
+
"SI": "https://api-endpoint.mta.info/Dataservice/mtagtfsfeeds/nyct%2Fgtfs-si",
|
|
11
|
+
"G": "https://api-endpoint.mta.info/Dataservice/mtagtfsfeeds/nyct%2Fgtfs-g",
|
|
12
|
+
"J": "https://api-endpoint.mta.info/Dataservice/mtagtfsfeeds/nyct%2Fgtfs-jz",
|
|
13
|
+
"7": "https://api-endpoint.mta.info/Dataservice/mtagtfsfeeds/nyct%2Fgtfs-7",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
# Mapping of subway lines to feed IDs
|
|
17
|
+
LINE_TO_FEED = {
|
|
18
|
+
# Feed 1: 1, 2, 3, 4, 5, 6, GS (Grand Central Shuttle)
|
|
19
|
+
"1": "1",
|
|
20
|
+
"2": "1",
|
|
21
|
+
"3": "1",
|
|
22
|
+
"4": "1",
|
|
23
|
+
"5": "1",
|
|
24
|
+
"6": "1",
|
|
25
|
+
"GS": "1",
|
|
26
|
+
# Feed A: A, C, E, H (Rockaway Shuttle), FS (Franklin Av Shuttle)
|
|
27
|
+
"A": "A",
|
|
28
|
+
"C": "A",
|
|
29
|
+
"E": "A",
|
|
30
|
+
"H": "A",
|
|
31
|
+
"FS": "A",
|
|
32
|
+
# Feed N: N, Q, R, W
|
|
33
|
+
"N": "N",
|
|
34
|
+
"Q": "N",
|
|
35
|
+
"R": "N",
|
|
36
|
+
"W": "N",
|
|
37
|
+
# Feed B: B, D, F, M
|
|
38
|
+
"B": "B",
|
|
39
|
+
"D": "B",
|
|
40
|
+
"F": "B",
|
|
41
|
+
"M": "B",
|
|
42
|
+
# Feed L: L
|
|
43
|
+
"L": "L",
|
|
44
|
+
# Feed SI: SIR (Staten Island Railway)
|
|
45
|
+
"SI": "SI",
|
|
46
|
+
# Feed G: G
|
|
47
|
+
"G": "G",
|
|
48
|
+
# Feed J: J, Z
|
|
49
|
+
"J": "J",
|
|
50
|
+
"Z": "J",
|
|
51
|
+
# Feed 7: 7, 7X (7 Express)
|
|
52
|
+
"7": "7",
|
|
53
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Data models for MTA GTFS-RT library."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Arrival:
|
|
9
|
+
"""Represents a single train arrival."""
|
|
10
|
+
|
|
11
|
+
arrival_time: datetime
|
|
12
|
+
"""The datetime when the train will arrive."""
|
|
13
|
+
|
|
14
|
+
route_id: str
|
|
15
|
+
"""The route/line ID (e.g., '1', 'A', 'Q')."""
|
|
16
|
+
|
|
17
|
+
stop_id: str
|
|
18
|
+
"""The stop ID including direction (e.g., '127N', 'B08S')."""
|
|
19
|
+
|
|
20
|
+
destination: str
|
|
21
|
+
"""The trip headsign/destination (e.g., 'Van Cortlandt Park - 242 St')."""
|
|
22
|
+
|
|
23
|
+
def __lt__(self, other: "Arrival") -> bool:
|
|
24
|
+
"""Allow sorting by arrival time."""
|
|
25
|
+
return self.arrival_time < other.arrival_time
|
|
File without changes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "py-nymta"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python library for accessing MTA (Metropolitan Transportation Authority) real-time transit data for NYC"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{name = "py-nymta Contributors"}
|
|
12
|
+
]
|
|
13
|
+
license = {text = "MIT"}
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
]
|
|
23
|
+
requires-python = ">=3.11"
|
|
24
|
+
dependencies = [
|
|
25
|
+
"gtfs-realtime-bindings>=1.0.0",
|
|
26
|
+
"requests>=2.31.0",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/OnFreund/py-nymta"
|
|
31
|
+
Issues = "https://github.com/OnFreund/py-nymta/issues"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
include = ["pymta*"]
|
|
35
|
+
exclude = ["tests*"]
|
py_nymta-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Tests for SubwayFeed class."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from unittest.mock import Mock, patch
|
|
5
|
+
|
|
6
|
+
from google.transit import gtfs_realtime_pb2
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from pymta import Arrival, MTAFeedError, SubwayFeed
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_init_valid_feed_id():
|
|
13
|
+
"""Test initialization with valid feed ID."""
|
|
14
|
+
feed = SubwayFeed(feed_id="N")
|
|
15
|
+
assert feed.feed_id == "N"
|
|
16
|
+
assert feed.timeout == 30
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_init_invalid_feed_id():
|
|
20
|
+
"""Test initialization with invalid feed ID."""
|
|
21
|
+
with pytest.raises(ValueError, match="Invalid feed_id"):
|
|
22
|
+
SubwayFeed(feed_id="INVALID")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_get_feed_id_for_route():
|
|
26
|
+
"""Test getting feed ID for a route."""
|
|
27
|
+
assert SubwayFeed.get_feed_id_for_route("Q") == "N"
|
|
28
|
+
assert SubwayFeed.get_feed_id_for_route("1") == "1"
|
|
29
|
+
assert SubwayFeed.get_feed_id_for_route("F") == "B"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_get_feed_id_for_invalid_route():
|
|
33
|
+
"""Test getting feed ID for invalid route."""
|
|
34
|
+
with pytest.raises(ValueError, match="Invalid route_id"):
|
|
35
|
+
SubwayFeed.get_feed_id_for_route("INVALID")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@patch("pymta.requests.get")
|
|
39
|
+
def test_get_arrivals_success(mock_get):
|
|
40
|
+
"""Test getting arrivals successfully."""
|
|
41
|
+
# Create a GTFS-RT FeedMessage
|
|
42
|
+
feed_message = gtfs_realtime_pb2.FeedMessage()
|
|
43
|
+
|
|
44
|
+
# Create a trip update entity
|
|
45
|
+
entity = feed_message.entity.add()
|
|
46
|
+
entity.id = "trip1"
|
|
47
|
+
|
|
48
|
+
trip_update = entity.trip_update
|
|
49
|
+
trip_update.trip.route_id = "Q"
|
|
50
|
+
trip_update.trip.trip_headsign = "Coney Island - Stillwell Av"
|
|
51
|
+
|
|
52
|
+
# Add stop time update for the future
|
|
53
|
+
stop_time = trip_update.stop_time_update.add()
|
|
54
|
+
stop_time.stop_id = "B08S"
|
|
55
|
+
future_time = datetime.now(timezone.utc).timestamp() + 300 # 5 minutes from now
|
|
56
|
+
stop_time.arrival.time = int(future_time)
|
|
57
|
+
|
|
58
|
+
# Mock response
|
|
59
|
+
mock_response = Mock()
|
|
60
|
+
mock_response.content = feed_message.SerializeToString()
|
|
61
|
+
mock_response.raise_for_status = Mock()
|
|
62
|
+
mock_get.return_value = mock_response
|
|
63
|
+
|
|
64
|
+
# Test
|
|
65
|
+
feed = SubwayFeed(feed_id="N")
|
|
66
|
+
arrivals = feed.get_arrivals(route_id="Q", stop_id="B08S")
|
|
67
|
+
|
|
68
|
+
assert len(arrivals) == 1
|
|
69
|
+
assert arrivals[0].route_id == "Q"
|
|
70
|
+
assert arrivals[0].stop_id == "B08S"
|
|
71
|
+
assert arrivals[0].destination == "Coney Island - Stillwell Av"
|
|
72
|
+
assert isinstance(arrivals[0].arrival_time, datetime)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@patch("pymta.requests.get")
|
|
76
|
+
def test_get_arrivals_filters_past_arrivals(mock_get):
|
|
77
|
+
"""Test that past arrivals are filtered out."""
|
|
78
|
+
# Create a GTFS-RT FeedMessage
|
|
79
|
+
feed_message = gtfs_realtime_pb2.FeedMessage()
|
|
80
|
+
|
|
81
|
+
# Create trip with past arrival
|
|
82
|
+
entity = feed_message.entity.add()
|
|
83
|
+
entity.id = "trip1"
|
|
84
|
+
trip_update = entity.trip_update
|
|
85
|
+
trip_update.trip.route_id = "Q"
|
|
86
|
+
trip_update.trip.trip_headsign = "Coney Island"
|
|
87
|
+
|
|
88
|
+
stop_time = trip_update.stop_time_update.add()
|
|
89
|
+
stop_time.stop_id = "B08S"
|
|
90
|
+
past_time = datetime.now(timezone.utc).timestamp() - 300 # 5 minutes ago
|
|
91
|
+
stop_time.arrival.time = int(past_time)
|
|
92
|
+
|
|
93
|
+
# Mock response
|
|
94
|
+
mock_response = Mock()
|
|
95
|
+
mock_response.content = feed_message.SerializeToString()
|
|
96
|
+
mock_response.raise_for_status = Mock()
|
|
97
|
+
mock_get.return_value = mock_response
|
|
98
|
+
|
|
99
|
+
# Test
|
|
100
|
+
feed = SubwayFeed(feed_id="N")
|
|
101
|
+
arrivals = feed.get_arrivals(route_id="Q", stop_id="B08S")
|
|
102
|
+
|
|
103
|
+
assert len(arrivals) == 0
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@patch("pymta.requests.get")
|
|
107
|
+
def test_get_arrivals_max_arrivals(mock_get):
|
|
108
|
+
"""Test max_arrivals parameter."""
|
|
109
|
+
# Create a GTFS-RT FeedMessage with 5 arrivals
|
|
110
|
+
feed_message = gtfs_realtime_pb2.FeedMessage()
|
|
111
|
+
|
|
112
|
+
for i in range(5):
|
|
113
|
+
entity = feed_message.entity.add()
|
|
114
|
+
entity.id = f"trip{i}"
|
|
115
|
+
trip_update = entity.trip_update
|
|
116
|
+
trip_update.trip.route_id = "Q"
|
|
117
|
+
trip_update.trip.trip_headsign = "Coney Island"
|
|
118
|
+
|
|
119
|
+
stop_time = trip_update.stop_time_update.add()
|
|
120
|
+
stop_time.stop_id = "B08S"
|
|
121
|
+
future_time = datetime.now(timezone.utc).timestamp() + (i + 1) * 60
|
|
122
|
+
stop_time.arrival.time = int(future_time)
|
|
123
|
+
|
|
124
|
+
# Mock response
|
|
125
|
+
mock_response = Mock()
|
|
126
|
+
mock_response.content = feed_message.SerializeToString()
|
|
127
|
+
mock_response.raise_for_status = Mock()
|
|
128
|
+
mock_get.return_value = mock_response
|
|
129
|
+
|
|
130
|
+
# Test
|
|
131
|
+
feed = SubwayFeed(feed_id="N")
|
|
132
|
+
arrivals = feed.get_arrivals(route_id="Q", stop_id="B08S", max_arrivals=3)
|
|
133
|
+
|
|
134
|
+
assert len(arrivals) == 3
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@patch("pymta.requests.get")
|
|
138
|
+
def test_get_arrivals_network_error(mock_get):
|
|
139
|
+
"""Test handling of network errors."""
|
|
140
|
+
mock_get.side_effect = Exception("Network error")
|
|
141
|
+
|
|
142
|
+
feed = SubwayFeed(feed_id="N")
|
|
143
|
+
with pytest.raises(MTAFeedError, match="Error fetching GTFS-RT feed"):
|
|
144
|
+
feed.get_arrivals(route_id="Q", stop_id="B08S")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_arrival_sorting():
|
|
148
|
+
"""Test that Arrival objects can be sorted by time."""
|
|
149
|
+
now = datetime.now(timezone.utc)
|
|
150
|
+
arrival1 = Arrival(
|
|
151
|
+
arrival_time=now,
|
|
152
|
+
route_id="Q",
|
|
153
|
+
stop_id="B08S",
|
|
154
|
+
destination="Coney Island",
|
|
155
|
+
)
|
|
156
|
+
arrival2 = Arrival(
|
|
157
|
+
arrival_time=now.replace(minute=now.minute + 5),
|
|
158
|
+
route_id="Q",
|
|
159
|
+
stop_id="B08S",
|
|
160
|
+
destination="Coney Island",
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
arrivals = [arrival2, arrival1]
|
|
164
|
+
arrivals.sort()
|
|
165
|
+
|
|
166
|
+
assert arrivals[0] == arrival1
|
|
167
|
+
assert arrivals[1] == arrival2
|