Skip to content

Commit

Permalink
Update dependencies and modernize codebase (#37)
Browse files Browse the repository at this point in the history
* update dependencies

- drop py3.7, add py3.13
- pydantic 2
- black -> ruff. ruff format.
- push CI actions versions

* cleaner push yaml

* use makefile

* ruff fixes

* modernize pydantic validator. fix makefile

* drop 3.8
  • Loading branch information
nolanbconaway authored Nov 9, 2024
1 parent 3addcef commit 032996e
Show file tree
Hide file tree
Showing 12 changed files with 74 additions and 81 deletions.
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.

0 comments on commit 032996e

Please sign in to comment.