pax_global_header00006660000000000000000000000064146607444770014535gustar00rootroot0000000000000052 comment=8bc2d90ca880af1563b6ce700a75648b945d8df0 py_nextbusnext-2.0.5/000077500000000000000000000000001466074447700146405ustar00rootroot00000000000000py_nextbusnext-2.0.5/.gitattributes000066400000000000000000000005721466074447700175370ustar00rootroot00000000000000# Auto detect text files and perform LF normalization * text=auto # Custom for Visual Studio *.cs diff=csharp # Standard to msysgit *.doc diff=astextplain *.DOC diff=astextplain *.docx diff=astextplain *.DOCX diff=astextplain *.dot diff=astextplain *.DOT diff=astextplain *.pdf diff=astextplain *.PDF diff=astextplain *.rtf diff=astextplain *.RTF diff=astextplain py_nextbusnext-2.0.5/.github/000077500000000000000000000000001466074447700162005ustar00rootroot00000000000000py_nextbusnext-2.0.5/.github/workflows/000077500000000000000000000000001466074447700202355ustar00rootroot00000000000000py_nextbusnext-2.0.5/.github/workflows/publish.yaml000066400000000000000000000006761466074447700226000ustar00rootroot00000000000000--- name: Publish Python Package on: release: types: [published] permissions: contents: read jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v3 with: python-version: '3.x' - name: Upload env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | make upload py_nextbusnext-2.0.5/.github/workflows/test.yaml000066400000000000000000000005401466074447700220770ustar00rootroot00000000000000--- name: Tests on: push: branches: [master] pull_request: branches: [master] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v3 with: python-version: '3.x' - name: Run tests run: | make test py_nextbusnext-2.0.5/.gitignore000066400000000000000000000026061466074447700166340ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # dotenv .env # virtualenv .venv venv/ ENV/ # Spyder project settings .spyderproject # Rope project settings .ropeproject # ========================= # Operating System Files # ========================= # Windows # ========================= # Windows thumbnail cache files Thumbs.db ehthumbs.db ehthumbs_vista.db # Folder config file Desktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Windows Installer files *.cab *.msi *.msm *.msp # Windows shortcuts *.lnk py_nextbusnext-2.0.5/.pre-commit-config.yaml000066400000000000000000000013651466074447700211260ustar00rootroot00000000000000repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: debug-statements language_version: python3 - id: check-merge-conflict - id: name-tests-test exclude: tests/(common.py|util.py|(helpers|integration/factories)/(.+).py) - repo: https://github.com/asottile/reorder_python_imports rev: v3.13.0 hooks: - id: reorder-python-imports args: - --py3-plus - repo: https://github.com/psf/black rev: 24.4.2 hooks: - id: black - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.10.1 hooks: - id: mypy additional_dependencies: - types-requests py_nextbusnext-2.0.5/LICENSE000066400000000000000000000020551466074447700156470ustar00rootroot00000000000000MIT License Copyright (c) 2018 Pierre Maris Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. py_nextbusnext-2.0.5/Makefile000066400000000000000000000036661466074447700163130ustar00rootroot00000000000000ENV := env .PHONY: default default: test # Creates virtualenv $(ENV): python3 -m venv $(ENV) $(ENV)/bin/pip install -e . # Install wheel for building packages $(ENV)/bin/wheel: $(ENV) $(ENV)/bin/pip install wheel setuptools # Install twine for uploading packages $(ENV)/bin/twine: $(ENV) $(ENV)/bin/pip install twine setuptools # Install pre-commit and other devenv items $(ENV)/bin/pre-commit: $(ENV) $(ENV)/bin/pip install -r ./requirements-dev.txt # Installs dev requirements to virtualenv .PHONY: devenv devenv: $(ENV)/bin/pre-commit # Generates a small build env for building and uploading dists .PHONY: build-env build-env: $(ENV)/bin/twine $(ENV)/bin/wheel # Runs tests .PHONY: test test: $(ENV) $(ENV)/bin/pre-commit $(ENV)/bin/tox $(ENV)/bin/pre-commit run --all-files # Builds wheel for package to upload .PHONY: build build: $(ENV)/bin/wheel $(ENV)/bin/python setup.py sdist $(ENV)/bin/python setup.py bdist_wheel # Verify that the python version matches the git tag so we don't push bad shas .PHONY: verify-tag-version verify-tag-version: $(ENV)/bin/wheel $(eval TAG_NAME = $(shell [ -n "$(DRONE_TAG)" ] && echo $(DRONE_TAG) || git describe --tags --exact-match)) test "v$(shell $(ENV)/bin/python setup.py -V)" = "$(TAG_NAME)" # Uses twine to upload to pypi .PHONY: upload upload: verify-tag-version build $(ENV)/bin/twine $(ENV)/bin/twine upload dist/* # Uses twine to upload to test pypi .PHONY: upload-test upload-test: verify-tag-version build $(ENV)/bin/twine $(ENV)/bin/twine upload --repository-url https://test.pypi.org/legacy/ dist/* # Cleans all build, runtime, and test artifacts .PHONY: clean clean: rm -fr ./build ./py_nextbus.egg-info find . -name '*.pyc' -delete find . -name '__pycache__' -delete # Cleans dist and env .PHONY: dist-clean dist-clean: clean rm -fr ./dist $(ENV) # Install pre-commit hooks .PHONY: install-hooks install-hooks: $(ENV)/bin/pre-commit $(ENV)/bin/pre-commit install -f --install-hooks py_nextbusnext-2.0.5/README.md000066400000000000000000000006031466074447700161160ustar00rootroot00000000000000# py_nextbusnext ## This is forked from py_nextbus for maintanence _It is no longer API compatible to the upstream_ A minimalistic Python 3 client to get routes and predictions from the NextBus API. Installation --- Install with pip: `pip install py-nextbusnext` Usage --- ``` >>> import py_nextbus >>> client = py_nextbus.NextBusClient() >>> agencies = client.get_agencies() ``` py_nextbusnext-2.0.5/gen_mock.py000066400000000000000000000011351466074447700167740ustar00rootroot00000000000000from py_nextbus import NextBusClient from tests.mock_responses import TEST_AGENCY_ID from tests.mock_responses import TEST_ROUTE_ID from tests.mock_responses import TEST_STOP_ID client = NextBusClient() agencies = client.agencies() print("Agencies:") print(agencies) routes = client.routes(TEST_AGENCY_ID) print("\nRoutes:") print(routes) route_details = client.route_details(TEST_ROUTE_ID, TEST_AGENCY_ID) print("\nRoute Details:") print(route_details) predictions = client.predictions_for_stop( TEST_STOP_ID, TEST_ROUTE_ID, agency_id=TEST_AGENCY_ID ) print("\nPredictions:") print(predictions) py_nextbusnext-2.0.5/py_nextbus/000077500000000000000000000000001466074447700170405ustar00rootroot00000000000000py_nextbusnext-2.0.5/py_nextbus/__init__.py000066400000000000000000000000771466074447700211550ustar00rootroot00000000000000from .client import NextBusClient # NOQA name = "py_nextbus" py_nextbusnext-2.0.5/py_nextbus/client.py000066400000000000000000000126011466074447700206700ustar00rootroot00000000000000from __future__ import annotations import json import logging import re from time import time from typing import Any from typing import cast from typing import NamedTuple import requests from requests.exceptions import HTTPError LOG = logging.getLogger() API_KEY_RE = re.compile(r"api_key.*key=([a-z0-9]+)") class NextBusError(Exception): pass class NextBusHTTPError(HTTPError, NextBusError): def __init__(self, message: str, http_err: HTTPError): self.__dict__.update(http_err.__dict__) self.message = message class NextBusValidationError(ValueError, NextBusError): """Error with missing fields for a NextBus request.""" class NextBusFormatError(ValueError, NextBusError): """Error with parsing a NextBus response.""" class NextBusAuthError(NextBusError): """Error with authentication to the NextBus API.""" class RouteStop(NamedTuple): route_tag: str stop_tag: str | int def __str__(self) -> str: return f"{self.route_tag}|{self.stop_tag}" @classmethod def from_dict(cls, legacy_dict: dict[str, str]) -> RouteStop: return cls(legacy_dict["route_tag"], legacy_dict["stop_tag"]) class NextBusClient: referer = "https://retro.umoiq.com/" base_url = "https://retro.umoiq.com/api/pub/v1" def __init__( self, agency_id: str | None = None, ) -> None: self.agency_id = agency_id self.api_key: str | None = None self.headers = { "Accept": "application/json, text/javascript, */*; q=0.01", "Referer": self.referer, } def agencies(self) -> list[dict[str, Any]]: result = self._get("agencies") return cast(list[dict[str, Any]], result) def routes(self, agency_id: str | None = None) -> list[dict[str, Any]]: if not agency_id: agency_id = self.agency_id result = self._get(f"agencies/{agency_id}/routes") return cast(list[dict[str, Any]], result) def route_details( self, route_id: str, agency_id: str | None = None ) -> dict[str, Any] | str: """Includes stops and directions.""" agency_id = agency_id or self.agency_id if not agency_id: raise NextBusValidationError("Agency ID is required") result = self._get(f"agencies/{agency_id}/routes/{route_id}") return cast(dict[str, Any], result) def predictions_for_stop( self, stop_id: str | int, route_id: str | None = None, direction_id: str | None = None, agency_id: str | None = None, ) -> list[dict[str, Any]]: agency_id = agency_id or self.agency_id if not agency_id: raise NextBusValidationError("Agency ID is required") params: dict[str, Any] = {"coincident": True} if direction_id: if not route_id: raise NextBusValidationError("Direction ID provided without route ID") params["direction"] = direction_id route_component = "" if route_id: route_component = f"routes/{route_id}/" result = self._get( f"agencies/{agency_id}/{route_component}stops/{stop_id}/predictions", params, ) predictions = cast(list[dict[str, Any]], result) # If route not provided, return all predictions as the API returned them if not route_id: return predictions # HACK: Filter predictions based on stop and route because the API seems to ignore the route predictions = [ prediction_result for prediction_result in predictions if ( prediction_result["stop"]["id"] == stop_id and prediction_result["route"]["id"] == route_id ) ] # HACK: Filter predictions based on direction in case the API returns extra predictions if direction_id: for prediction_result in predictions: prediction_result["values"] = [ prediction for prediction in prediction_result["values"] if prediction["direction"]["id"] == direction_id ] return predictions def _fetch_api_key(self) -> str: response = requests.get(self.referer) response.raise_for_status() key_search = API_KEY_RE.search(response.text) if not key_search: raise NextBusValidationError("Could not find API key on page") api_key = key_search.group(1) return api_key def _get( self, endpoint: str, params: dict[str, Any] | None = None ) -> dict[str, Any] | list[dict[str, Any]]: if params is None: params = {} if not self.api_key: self.api_key = self._fetch_api_key() params["key"] = self.api_key params["timestamp"] = int(time() * 1000) try: url = f"{self.base_url}/{endpoint}" LOG.debug("GET %s", url) response = requests.get(url, params=params, headers=self.headers) response.raise_for_status() return response.json() except HTTPError as exc: if exc.response.status_code == 401: self.api_key = None raise NextBusHTTPError("Error from the NextBus API", exc) from exc except json.decoder.JSONDecodeError as exc: raise NextBusFormatError("Failed to parse JSON from request") from exc py_nextbusnext-2.0.5/requirements-dev.txt000066400000000000000000000000521466074447700206750ustar00rootroot00000000000000tox pre-commit mypy==1.10.1 black==24.4.2 py_nextbusnext-2.0.5/setup.py000066400000000000000000000017671466074447700163650ustar00rootroot00000000000000from setuptools import find_packages from setuptools import setup with open("README.md", "r") as readme: long_description = readme.read() setup( name="py_nextbusnext", version="2.0.5", author="ViViDboarder", description="Minimalistic Python client for the NextBus public API for real-time transit " "arrival data", long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/vividboarder/py_nextbus", packages=find_packages( exclude=[ "build", "dist", "tests", ] ), python_requires=">=3.9", install_requires=[ "requests", ], classifiers=[ "Programming Language :: Python :: 3.9 ", "Programming Language :: Python :: 3.10 ", "Programming Language :: Python :: 3.11 ", "Programming Language :: Python :: 3.12 ", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ], ) py_nextbusnext-2.0.5/tests/000077500000000000000000000000001466074447700160025ustar00rootroot00000000000000py_nextbusnext-2.0.5/tests/__init__.py000066400000000000000000000000001466074447700201010ustar00rootroot00000000000000py_nextbusnext-2.0.5/tests/client_test.py000066400000000000000000000043531466074447700206760ustar00rootroot00000000000000from __future__ import annotations import unittest.mock from py_nextbus.client import NextBusClient from tests.helpers.mock_responses import MOCK_PREDICTIONS_RESPONSE_NO_ROUTE from tests.helpers.mock_responses import MOCK_PREDICTIONS_RESPONSE_WITH_ROUTE from tests.helpers.mock_responses import TEST_AGENCY_ID from tests.helpers.mock_responses import TEST_DIRECTION_ID from tests.helpers.mock_responses import TEST_ROUTE_ID from tests.helpers.mock_responses import TEST_STOP_ID class TestNextBusClient(unittest.TestCase): def setUp(self): self.client = NextBusClient() @unittest.mock.patch("py_nextbus.client.NextBusClient._get") def test_predictions_for_stop_no_route(self, mock_get): mock_get.return_value = MOCK_PREDICTIONS_RESPONSE_NO_ROUTE result = self.client.predictions_for_stop( TEST_STOP_ID, agency_id=TEST_AGENCY_ID ) self.assertEqual({r["stop"]["id"] for r in result}, {TEST_STOP_ID}) self.assertEqual(len(result), 3) # Results include all routes mock_get.assert_called_once() mock_get.assert_called_with( f"agencies/{TEST_AGENCY_ID}/stops/{TEST_STOP_ID}/predictions", {"coincident": True}, ) @unittest.mock.patch("py_nextbus.client.NextBusClient._get") def test_predictions_for_stop_with_route(self, mock_get): mock_get.return_value = MOCK_PREDICTIONS_RESPONSE_WITH_ROUTE result = self.client.predictions_for_stop( TEST_STOP_ID, TEST_ROUTE_ID, agency_id=TEST_AGENCY_ID, direction_id=TEST_DIRECTION_ID, ) # Assert all predictions are for the correct stop self.assertEqual({r["stop"]["id"] for r in result}, {TEST_STOP_ID}) self.assertEqual({r["route"]["id"] for r in result}, {TEST_ROUTE_ID}) self.assertEqual( {p["direction"]["id"] for r in result for p in r["values"]}, {TEST_DIRECTION_ID}, ) mock_get.assert_called_once() mock_get.assert_called_with( f"agencies/{TEST_AGENCY_ID}/routes/{TEST_ROUTE_ID}/stops/{TEST_STOP_ID}/predictions", {"coincident": True, "direction": TEST_DIRECTION_ID}, ) if __name__ == "__main__": unittest.main() py_nextbusnext-2.0.5/tests/helpers/000077500000000000000000000000001466074447700174445ustar00rootroot00000000000000py_nextbusnext-2.0.5/tests/helpers/mock_responses.py000066400000000000000000000237241466074447700230600ustar00rootroot00000000000000TEST_AGENCY_ID = "sfmta-cis" TEST_ROUTE_ID = "F" TEST_STOP_ID = "5184" TEST_DIRECTION_ID = "F_0_var0" MOCK_AGENCY_LIST_RESPONSE = [ { "id": "sfmta-cis", "name": "San Francisco Muni CIS", "shortName": "SF Muni CIS", "region": "California-Northern", "website": "http://www.sfmta.com", "logo": "/logos/muniLogoSmall.gif", "nxbs2RedirectUrl": "", }, ] MOCK_ROUTE_LIST_RESPONSE = [ { "id": "F", "rev": 1057, "title": "F Market & Wharves", "description": "7am-10pm daily", "color": "b49a36", "textColor": "000000", "hidden": False, "timestamp": "2024-06-23T03:06:58Z", }, ] MOCK_ROUTE_DETAILS_RESPONSE = { "id": "F", "rev": 1057, "title": "F Market & Wharves", "description": "7am-10pm daily", "color": "b49a36", "textColor": "000000", "hidden": False, "boundingBox": { "latMin": 37.7625799, "latMax": 37.8085899, "lonMin": -122.43498, "lonMax": -122.39344, }, "stops": [ { "id": "5184", "lat": 37.8071299, "lon": -122.41732, "name": "Jones St & Beach St", "code": "15184", "hidden": False, "showDestinationSelector": True, "directions": ["F_0_var1", "F_0_var0"], }, ], "directions": [ { "id": "F_0_var1", "shortName": "Castro + Market", "name": "Castro + Market", "useForUi": False, "stops": [ "5184", ], }, { "id": "F_0_var0", "shortName": "Castro", "name": "Castro", "useForUi": True, "stops": [ "5184", ], }, { "id": "F_1_var0", "shortName": "Fisherman`s Wharf", "name": "Fisherman`s Wharf", "useForUi": True, "stops": [ "5184_ar", ], }, ], "paths": [ { "id": "F_1_var0_16_4530_5184_ar", "points": [ {"lat": 37.80835, "lon": -122.41029}, {"lat": 37.80829, "lon": -122.41032}, {"lat": 37.80833, "lon": -122.4105}, {"lat": 37.80862, "lon": -122.4124}, {"lat": 37.80862, "lon": -122.41253}, {"lat": 37.80859, "lon": -122.41336}, {"lat": 37.8085, "lon": -122.41337}, {"lat": 37.80842, "lon": -122.41417}, {"lat": 37.80832, "lon": -122.41551}, {"lat": 37.80801, "lon": -122.41745}, {"lat": 37.80724, "lon": -122.41732}, {"lat": 37.80713, "lon": -122.41732}, ], }, ], "timestamp": "2024-06-23T03:06:58Z", } MOCK_PREDICTIONS_RESPONSE_NO_ROUTE = [ { "serverTimestamp": 1724038210798, "nxbs2RedirectUrl": "", "route": { "id": "LOWL", "title": "Lowl Owl Taraval", "description": "10pm-5am nightly", "color": "666666", "textColor": "ffffff", "hidden": False, }, "stop": { "id": "5184", "lat": 37.8071299, "lon": -122.41732, "name": "Jones St & Beach St", "code": "15184", "hidden": False, "showDestinationSelector": True, "route": "LOWL", }, "values": [], }, { "serverTimestamp": 1724038210798, "nxbs2RedirectUrl": "", "route": { "id": "FBUS", "title": "Fbus Market & Wharves", "description": "", "color": "b49a36", "textColor": "000000", "hidden": False, }, "stop": { "id": "5184", "lat": 37.8071299, "lon": -122.41732, "name": "Jones St & Beach St", "code": "15184", "hidden": False, "showDestinationSelector": True, "route": "FBUS", }, "values": [], }, { "serverTimestamp": 1724038210798, "nxbs2RedirectUrl": "", "route": { "id": "F", "title": "F Market & Wharves", "description": "7am-10pm daily", "color": "b49a36", "textColor": "000000", "hidden": False, }, "stop": { "id": "5184", "lat": 37.8071299, "lon": -122.41732, "name": "Jones St & Beach St", "code": "15184", "hidden": False, "showDestinationSelector": True, "route": "F", }, "values": [ { "timestamp": 1724038309178, "minutes": 1, "affectedByLayover": True, "isDeparture": True, "occupancyStatus": -1, "occupancyDescription": "Unknown", "vehiclesInConsist": 1, "linkedVehicleIds": "1078", "vehicleId": "1078", "vehicleType": "Historic Street Car_VC1", "direction": { "id": "F_0_var1", "name": "Castro + Market", "destinationName": "Castro + Market", }, "tripId": "11593249_M13", "delay": 0, "predUsingNavigationTm": False, "departure": True, }, { "timestamp": 1724039160000, "minutes": 15, "affectedByLayover": True, "isDeparture": True, "occupancyStatus": -1, "occupancyDescription": "Unknown", "vehiclesInConsist": 1, "linkedVehicleIds": "1080", "vehicleId": "1080", "vehicleType": "Historic Street Car_VC1", "direction": { "id": "F_0_var0", "name": "Castro", "destinationName": "Castro", }, "tripId": "11593252_M13", "delay": 0, "predUsingNavigationTm": False, "departure": True, }, { "timestamp": 1724041320000, "minutes": 51, "affectedByLayover": True, "isDeparture": True, "occupancyStatus": -1, "occupancyDescription": "Unknown", "vehiclesInConsist": 1, "linkedVehicleIds": "1056", "vehicleId": "1056", "vehicleType": "Historic Street Car_VC1", "direction": { "id": "F_0_var1", "name": "Castro + Market", "destinationName": "Castro + Market", }, "tripId": "11593256_M13", "delay": 0, "predUsingNavigationTm": False, "departure": True, }, ], }, ] MOCK_PREDICTIONS_RESPONSE_WITH_ROUTE = [ { "serverTimestamp": 1720034290432, "nxbs2RedirectUrl": "", "route": { "id": "F", "title": "F Market & Wharves", "description": "7am-10pm daily", "color": "b49a36", "textColor": "000000", "hidden": False, }, "stop": { "id": "5184", "lat": 37.8071299, "lon": -122.41732, "name": "Jones St & Beach St", "code": "15184", "hidden": False, "showDestinationSelector": True, "route": "F", }, "values": [ { "timestamp": 1720034640000, "minutes": 5, "affectedByLayover": True, "isDeparture": True, "occupancyStatus": 0, "occupancyDescription": "Empty", "vehiclesInConsist": 1, "linkedVehicleIds": "1080", "vehicleId": "1080", "vehicleType": None, "direction": { "id": "F_0_var0", "name": "Castro", "destinationName": "Castro", }, "tripId": "11618999_M31", "delay": 0, "predUsingNavigationTm": False, "departure": True, }, { "timestamp": 1720035360000, "minutes": 17, "affectedByLayover": True, "isDeparture": True, "occupancyStatus": -1, "occupancyDescription": "Unknown", "vehiclesInConsist": 1, "linkedVehicleIds": "1070", "vehicleId": "1070", "vehicleType": None, "direction": { "id": "F_0_var0", "name": "Castro", "destinationName": "Castro", }, "tripId": "11619000_M31", "delay": 0, "predUsingNavigationTm": False, "departure": True, }, { "timestamp": 1720036080000, "minutes": 29, "affectedByLayover": True, "isDeparture": True, "occupancyStatus": -1, "occupancyDescription": "Unknown", "vehiclesInConsist": 1, "linkedVehicleIds": "1079", "vehicleId": "1079", "vehicleType": None, "direction": { "id": "F_0_var0", "name": "Castro", "destinationName": "Castro", }, "tripId": "11619001_M31", "delay": 0, "predUsingNavigationTm": False, "departure": True, }, ], } ] py_nextbusnext-2.0.5/tox.ini000066400000000000000000000003231466074447700161510ustar00rootroot00000000000000[tox] envlist = py{39, 310, 311, 312, 3} ; envlist = py38,py39,py310,py311,py312} skip_missing_interpreters = true [testenv] description = Run unit tests commands = python -m unittest discover -p "*_test.py"