Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update dependencies and modernize codebase #37

Merged
merged 6 commits into from
Nov 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 8 additions & 17 deletions .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,25 @@ on: push

jobs:
build:
runs-on: ${{matrix.os}}
runs-on: ubuntu-latest
strategy:
matrix:
os: [ubuntu-latest]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@v3

- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Set up Cache
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: |
${{ format('pip-{0}-{1}', matrix.python-version, hashFiles('setup.py')) }}
cache: 'pip'
cache-dependency-path: setup.py

- name: Install Dependencies
run: |
pip install pip==23.*
pip install pip==24.*
pip install .[dev]

- name: Black
run: black src test --check --verbose

- name: Pytest
run: pytest --verbose
- name: Test
run: make lint-test
16 changes: 16 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.PHONY:
lint:
@ ruff check
@ ruff format --check

.PHONY:
format:
@ ruff check --select I --fix
@ ruff format

.PHONY:
pytest:
pytest test --verbose

.PHONY:
lint-test: lint pytest
10 changes: 0 additions & 10 deletions codecov.yml

This file was deleted.

14 changes: 14 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[tool.ruff]
include = ["pyproject.toml", "src/**/*.py", "tests/**/*.py"]
line-length = 100

[tool.ruff.lint]
select = [
"E", # pycodestyle
"F", # Pyflakes
"UP", # pyupgrade
"B", # flake8-bugbear
"SIM", # flake8-simplify
"I", # isort
"RUF", # ruff
]
8 changes: 3 additions & 5 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,13 @@
"protobuf>=3.19.6,<=3.20.3",
"protobuf3-to-dict==0.1.*",
"click>=7,<9",
"pydantic~=1.9.2",
"pydantic==2.*",
"pytz>=2019.2",
]

DEV_REQUIRES = [
"pytest==7.*",
"tox==4.*",
"black==23.*",
"ruff==0.7.* ",
"requests-mock==1.*",
]

Expand All @@ -38,12 +37,11 @@
author_email="nolanbconaway@gmail.com",
url="https://github.com/nolanbconaway/underground",
classifiers=[
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
],
keywords=["nyc", "transit", "subway", "command-line", "cli"],
license="MIT",
Expand Down
1 change: 1 addition & 0 deletions src/underground/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""A realtime MTA module."""

from pathlib import Path

from .models import SubwayFeed
Expand Down
1 change: 1 addition & 0 deletions src/underground/cli/__main__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Command line tools for the MTA module."""

from . import cli

if __name__ == "__main__":
Expand Down
5 changes: 1 addition & 4 deletions src/underground/cli/stops.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,7 @@ def main(route, fmt, retries, api_key, timezone, stalled_timeout):
)

# figure out how to format it
if fmt == "epoch":
format_fun = datetime_to_epoch
else:
format_fun = lambda x: x.strftime(fmt)
format_fun = datetime_to_epoch if fmt == "epoch" else lambda x: x.strftime(fmt)

# echo the result
for stop_id, departures in stops.items():
Expand Down
4 changes: 2 additions & 2 deletions src/underground/feed.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def load_protobuf(protobuf_bytes: bytes) -> dict:
return feed_dict


def request(route_or_url: str, api_key: str = None) -> bytes:
def request(route_or_url: str, api_key: typing.Optional[str] = None) -> bytes:
"""Send a HTTP GET request to the MTA for realtime feed data.

Occassionally a feed is requested as the MTA is writing updated data to the file,
Expand Down Expand Up @@ -80,7 +80,7 @@ def request(route_or_url: str, api_key: str = None) -> bytes:
def request_robust(
route_or_url: str,
retries: int = 100,
api_key: str = None,
api_key: typing.Optional[str] = None,
return_dict: bool = False,
) -> typing.Union[dict, bytes]:
"""Request feed data with validations and retries.
Expand Down
2 changes: 1 addition & 1 deletion src/underground/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,6 @@ def resolve_url(route_or_url: str) -> str:
return route_or_url

if route_or_url not in ROUTE_REMAP:
raise ValueError("Unknown route or url: %s" % route_or_url)
raise ValueError(f"Unknown route or url: {route_or_url}")

return ROUTE_FEED_MAP[ROUTE_REMAP[route_or_url]]
57 changes: 27 additions & 30 deletions src/underground/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
class UnixTimestamp(pydantic.BaseModel):
"""A unix timestamp model."""

time: datetime.datetime = None
time: typing.Optional[datetime.datetime] = None

@property
def timestamp_nyc(self):
def timestamp_nyc(self) -> typing.Optional[datetime.datetime]:
"""Return the NYC datetime."""
if not self.time:
return None
Expand All @@ -29,7 +29,7 @@ class FeedHeader(pydantic.BaseModel):
timestamp: datetime.datetime

@property
def timestamp_nyc(self):
def timestamp_nyc(self) -> datetime.datetime:
"""Return the NYC datetime of the header."""
return self.timestamp.astimezone(pytz.timezone(metadata.DEFAULT_TIMEZONE))

Expand All @@ -38,39 +38,38 @@ class Trip(pydantic.BaseModel):
"""Model describing a train trip."""

trip_id: str
start_time: datetime.time = None
start_time: typing.Optional[datetime.time] = None
start_date: int
route_id: str

@pydantic.validator("start_date")
@pydantic.field_validator("start_date")
def check_start_date(cls, start_date):
"""Start_date is an int, so check it conforms to date expectations."""
if start_date < 19000101:
raise ValueError("Probably not a date.")

return start_date

@pydantic.validator("route_id")
@pydantic.field_validator("route_id")
def check_route(cls, route_id):
"""Check for a valid route ID value."""
if route_id not in metadata.ROUTE_REMAP:
raise ValueError(
"Invalid route (%s). Must be one of %s."
% (route_id, str(set(metadata.ROUTE_REMAP.keys())))
f"Invalid route ({route_id}). Must be one of {set(metadata.ROUTE_REMAP.keys())}."
)

return route_id

@property
def route_id_mapped(self):
def route_id_mapped(self) -> str:
"""Find the parent route ID.

This is helpful for grabbing the, e.g., 5 Train when you might have a 5X.
"""
return metadata.ROUTE_REMAP[self.route_id]

@property
def route_is_assigned(self):
def route_is_assigned(self) -> bool:
"""Return a flag indicating that there is a route."""
return self.route_id != ""

Expand All @@ -93,11 +92,11 @@ class StopTimeUpdate(pydantic.BaseModel):
"""

stop_id: str
arrival: UnixTimestamp = None
departure: UnixTimestamp = None
arrival: typing.Optional[UnixTimestamp] = None
departure: typing.Optional[UnixTimestamp] = None

@property
def depart_or_arrive(self) -> UnixTimestamp:
def depart_or_arrive(self) -> typing.Optional[UnixTimestamp]:
"""Return the departure or arrival time if either are specified.

This OR should usually be called because the MTA is inconsistent about when
Expand All @@ -123,7 +122,7 @@ class TripUpdate(pydantic.BaseModel):
"""

trip: Trip
stop_time_update: typing.List[StopTimeUpdate] = None
stop_time_update: typing.Optional[list[StopTimeUpdate]] = None


class Vehicle(pydantic.BaseModel):
Expand Down Expand Up @@ -153,9 +152,9 @@ class Vehicle(pydantic.BaseModel):
"""

trip: Trip
timestamp: datetime.datetime = None
current_stop_sequence: int = None
stop_id: str = None
timestamp: typing.Optional[datetime.datetime] = None
current_stop_sequence: typing.Optional[int] = None
stop_id: typing.Optional[str] = None


class Entity(pydantic.BaseModel):
Expand All @@ -166,8 +165,8 @@ class Entity(pydantic.BaseModel):
"""

id: str
vehicle: Vehicle = None
trip_update: TripUpdate = None
vehicle: typing.Optional[Vehicle] = None
trip_update: typing.Optional[TripUpdate] = None


class SubwayFeed(pydantic.BaseModel):
Expand All @@ -177,10 +176,12 @@ class SubwayFeed(pydantic.BaseModel):
"""

header: FeedHeader
entity: typing.List[Entity]
entity: list[Entity]

@staticmethod
def get(route_or_url: str, retries: int = 100, api_key: str = None) -> "SubwayFeed":
def get(
route_or_url: str, retries: int = 100, api_key: typing.Optional[str] = None
) -> "SubwayFeed":
"""Request feed data from the MTA.

Parameters
Expand Down Expand Up @@ -213,7 +214,7 @@ def get(route_or_url: str, retries: int = 100, api_key: str = None) -> "SubwayFe

def extract_stop_dict(
self, timezone: str = metadata.DEFAULT_TIMEZONE, stalled_timeout: int = 90
) -> dict:
) -> dict[str, dict[str, list[datetime.datetime]]]:
"""Get the departure times for all stops in the feed.

Parameters
Expand All @@ -234,11 +235,7 @@ def extract_stop_dict(
"""

trip_updates = (x.trip_update for x in self.entity if x.trip_update is not None)
vehicles = {
e.vehicle.trip.trip_id: e.vehicle
for e in self.entity
if e.vehicle is not None
}
vehicles = {e.vehicle.trip.trip_id: e.vehicle for e in self.entity if e.vehicle is not None}

def is_trip_active(update: TripUpdate) -> bool:
has_route = update.trip.route_is_assigned
Expand All @@ -249,9 +246,9 @@ def is_trip_active(update: TripUpdate) -> bool:
return has_route and has_stops

# as recommended by the MTA, we use these timestamps to determine if a train is stalled
train_stalled = (
self.header.timestamp - vehicle.timestamp
) > datetime.timedelta(seconds=stalled_timeout)
train_stalled = (self.header.timestamp - vehicle.timestamp) > datetime.timedelta(
seconds=stalled_timeout
)
return has_route and has_stops and not train_stalled

# grab the updates with routes and stop times
Expand Down
12 changes: 0 additions & 12 deletions tox.ini

This file was deleted.

Loading