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 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.
@@ -0,0 +1,3 @@
1
+ include README.md
2
+ include LICENSE
3
+ include pymta/py.typed
@@ -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,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,2 @@
1
+ gtfs-realtime-bindings>=1.0.0
2
+ requests>=2.31.0
@@ -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*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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