pax_global_header00006660000000000000000000000064146761002020014510gustar00rootroot0000000000000052 comment=3d62d7abd733f34b1e8f868d7f7ee1583eca8d0a python-opendata-transport-0.5.0/000077500000000000000000000000001467610020200166565ustar00rootroot00000000000000python-opendata-transport-0.5.0/.github/000077500000000000000000000000001467610020200202165ustar00rootroot00000000000000python-opendata-transport-0.5.0/.github/workflows/000077500000000000000000000000001467610020200222535ustar00rootroot00000000000000python-opendata-transport-0.5.0/.github/workflows/python.yml000066400000000000000000000011631467610020200243200ustar00rootroot00000000000000name: Python package and lint on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: [ "3.11", "3.12" ] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip python setup.py install - name: Black Code Formatter uses: jpetrucciani/black-check@22.12.0 python-opendata-transport-0.5.0/.gitignore000066400000000000000000000022261467610020200206500ustar00rootroot00000000000000# 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 # SageMath parsed files *.sage.py # dotenv .env # virtualenv .venv venv/ ENV/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ # PyCharm .idea python-opendata-transport-0.5.0/CHANGES.txt000066400000000000000000000023061467610020200204700ustar00rootroot00000000000000Changes ======= 20240929 - 0.5.0 ---------------- - Chain errors to be more transparent to the error source (thanks @miaucl) - Add KeyError exception (thanks @miaucl) - Add line numbers (thanks @polgarc) 20231120 - 0.4.0 ---------------- - Add support locations API (thanks @miaucl) - Add missing connections parameters (thanks @miaucl) - Add missing stationboard parameters (thanks @miaucl) - Add "True" flag for correct formatting of lists (ex. via parameter `via[]=foo1&via[]=foo2`) (thanks @miaucl) 20211124 - 0.3.0 ---------------- - Don't use async timeout (thanks @agners) - Remove loop 20210317 - 0.2.2 ---------------- - Only allow aiohttp >= 3.7.4 to avoid CVE-2021-21330 20200101 - 0.2.1 ---------------- - Fix lib name 20191231 - 0.2.0 ---------------- - Stationboard support (Thanks @agners) - Various improvements (Thanks @agners) - Adding delay to response (Thanks @cwilhelm) 20180917 - 0.1.4 ---------------- - Client errors are not reported (Thanks @vikramgorla) 20180609 - 0.1.3 ---------------- - Modernize setup.py and support long description for PyPI 20180609 - 0.1.1 ---------------- - Update the README 20180527 - 0.1.0 ---------------- - Support for async syntax (Python 3.5+) python-opendata-transport-0.5.0/LICENSE000066400000000000000000000021261467610020200176640ustar00rootroot00000000000000MIT License Copyright (c) 2017-2024 Fabian Affolter 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. python-opendata-transport-0.5.0/MANIFEST.in000066400000000000000000000000201467610020200204040ustar00rootroot00000000000000include LICENSE python-opendata-transport-0.5.0/README.rst000066400000000000000000000021731467610020200203500ustar00rootroot00000000000000python-opendata-transport ========================= A Python client for interacting with `transport.opendata.ch `_. This module is the base for the integration into `Home Assistant `_ and is simply retrieving the details about a given connection between two stations. This module is not official, developed, supported or endorsed by opendata.ch. Installation ------------ The module is available from the `Python Package Index `_. .. code:: bash $ pip3 install python_opendata_transport On a Fedora-based system or on a CentOS/RHEL 8 machine with has EPEL enabled. .. code:: bash $ sudo dnf -y install python3-opendata-transport For Nix or NixOS users is a package available. Keep in mind that the lastest releases might only be present in the ``unstable`` channel. .. code:: bash $ nix-env -iA nixos.python3Packages.python-opendata-transport Usage ----- The file ``example.py`` contains an example about how to use this module. License ------- ``python-opendata-transport`` is licensed under MIT, for more details check LICENSE. python-opendata-transport-0.5.0/example.py000066400000000000000000000035031467610020200206640ustar00rootroot00000000000000"""Example to get the details for a connection or a station.""" import asyncio import aiohttp from opendata_transport import OpendataTransport from opendata_transport import OpendataTransportStationboard from opendata_transport import OpendataTransportLocation async def main(): """Example for getting the data.""" async with aiohttp.ClientSession() as session: # Search a station by query locations = OpendataTransportLocation(session, query="Stettb") await locations.async_get_data() # Print the locations data print(locations.locations) # Print as list print(list(map(lambda x: x["name"], locations.locations))) # Search a station by coordinates locations = OpendataTransportLocation(session, x=47.2, y=8.7) await locations.async_get_data() # Print the locations data print(locations.locations) # Print as list print(list(map(lambda x: x["name"], locations.locations))) print() # Get the connection for a defined route connection = OpendataTransport( "Zürich, Blumenfeldstrasse", "Zürich Oerlikon, Bahnhof", session, 4 ) await connection.async_get_data() # Print the start and the destination name print("Train connections:", connection.from_name, "->", connection.to_name) # Print the next three connections print(connection.connections) # Print the details of the next connection print(connection.connections[0]) print() # Get all connections of a station stationboard = OpendataTransportStationboard("8591355", session, 4) await stationboard.async_get_data() # Print the journey data print(stationboard.journeys) if __name__ == "__main__": asyncio.run(main()) python-opendata-transport-0.5.0/opendata_transport/000077500000000000000000000000001467610020200225655ustar00rootroot00000000000000python-opendata-transport-0.5.0/opendata_transport/__init__.py000066400000000000000000000250711467610020200247030ustar00rootroot00000000000000"""Wrapper to get connection details from Opendata Transport.""" import asyncio import logging import aiohttp import urllib.parse from . import exceptions _LOGGER = logging.getLogger(__name__) _RESOURCE_URL = "http://transport.opendata.ch/v1/" class OpendataTransportBase(object): """Representation of the Opendata Transport base class""" def __init__(self, session): self._session = session @staticmethod def get_url(resource, params): """Generate the URL for the request.""" param = urllib.parse.urlencode(params, True) url = "{resource_url}{resource}?{param}".format( resource_url=_RESOURCE_URL, resource=resource, param=param ) print(url) return url class OpendataTransportLocation(OpendataTransportBase): """A class for handling locations from Opendata Transport.""" def __init__(self, session, query=None, x=None, y=None, type_="all", fields=None): """Initialize the location.""" super().__init__(session) self.query = query self.x = x self.y = y self.type = type_ self.fields = ( fields if fields is not None and isinstance(fields, list) else None ) self.from_name = self.from_id = self.to_name = self.to_id = None self.locations = [] @staticmethod def get_station(station): """Get the station details.""" return { "name": station["name"], "score": station["score"], "coordinate_type": station["coordinate"]["type"], "x": station["coordinate"]["x"], "y": station["coordinate"]["y"], "distance": station["distance"], } async def async_get_data(self): """Retrieve the data for the location.""" params = {} if self.query is not None: params["query"] = self.query else: params["x"] = self.x params["y"] = self.y if self.fields: params["fields"] = self.fields url = self.get_url("locations", params) try: response = await self._session.get(url, raise_for_status=True) _LOGGER.debug("Response from transport.opendata.ch: %s", response.status) data = await response.json() _LOGGER.debug(data) except asyncio.TimeoutError as e: _LOGGER.error("Can not load data from transport.opendata.ch") raise exceptions.OpendataTransportConnectionError() from e except aiohttp.ClientError as aiohttpClientError: _LOGGER.error("Response from transport.opendata.ch: %s", aiohttpClientError) raise exceptions.OpendataTransportConnectionError() from aiohttpClientError try: for station in data["stations"]: self.locations.append(self.get_station(station)) except (KeyError, TypeError, IndexError) as e: raise exceptions.OpendataTransportError() from e class OpendataTransportStationboard(OpendataTransportBase): """A class for handling stationsboards from Opendata Transport.""" def __init__( self, station, session, limit=5, transportations=None, datetime=None, type_="departure", fields=None, ): """Initialize the journey.""" super().__init__(session) self.station = station self.limit = limit self.datetime = datetime self.transportations = ( transportations if transportations is not None and isinstance(transportations, list) else None ) self.type = type_ self.fields = ( fields if fields is not None and isinstance(fields, list) else None ) self.from_name = self.from_id = self.to_name = self.to_id = None self.journeys = [] @staticmethod def get_journey(journey): """Get the journey details.""" return { "departure": journey["stop"]["departure"], "delay": journey["stop"]["delay"], "platform": journey["stop"]["platform"], "name": journey["name"], "category": journey["category"], "number": journey["number"], "to": journey["to"], } async def __async_get_data(self, station): """Retrieve the data for the station.""" params = { "limit": self.limit, "type": self.type, } if str.isdigit(station): params["id"] = station else: params["station"] = station if self.datetime: params["datetime"] = self.date if self.transportations: params["transportations"] = self.transportations if self.fields: params["fields"] = self.fields url = self.get_url("stationboard", params) try: response = await self._session.get(url, raise_for_status=True) _LOGGER.debug("Response from transport.opendata.ch: %s", response.status) data = await response.json() _LOGGER.debug(data) except asyncio.TimeoutError as e: _LOGGER.error("Can not load data from transport.opendata.ch") raise exceptions.OpendataTransportConnectionError() from e except aiohttp.ClientError as aiohttpClientError: _LOGGER.error("Response from transport.opendata.ch: %s", aiohttpClientError) raise exceptions.OpendataTransportConnectionError() from aiohttpClientError try: for journey in data["stationboard"]: self.journeys.append(self.get_journey(journey)) except (KeyError, TypeError, IndexError) as e: raise exceptions.OpendataTransportError() from e async def async_get_data(self): """Retrieve the data for the given station.""" self.journeys = [] if isinstance(self.station, list): for station in self.station: await self.__async_get_data(station) list.sort(self.journeys, key=lambda journey: journey["departure"]) else: await self.__async_get_data(self.station) class OpendataTransport(OpendataTransportBase): """A class for handling connections from Opendata Transport.""" def __init__( self, start, destination, session, limit=3, page=0, date=None, time=None, isArrivalTime=False, transportations=None, direct=False, sleeper=False, couchette=False, bike=False, accessibility=None, via=None, fields=None, ): """Initialize the connection.""" super().__init__(session) self.limit = limit self.page = page self.start = start self.destination = destination self.via = via[:5] if via is not None and isinstance(via, list) else None self.date = date self.time = time self.isArrivalTime = 1 if isArrivalTime else 0 self.transportations = ( transportations if transportations is not None and isinstance(transportations, list) else None ) self.direct = 1 if direct else 0 self.sleeper = 1 if sleeper else 0 self.couchette = 1 if couchette else 0 self.bike = 1 if bike else 0 self.accessibility = accessibility self.fields = ( fields if fields is not None and isinstance(fields, list) else None ) self.from_name = self.from_id = self.to_name = self.to_id = None self.connections = dict() @staticmethod def get_connection(connection): """Get the connection details.""" connection_info = dict() connection_info["departure"] = connection["from"]["departure"] connection_info["duration"] = connection["duration"] connection_info["delay"] = connection["from"]["delay"] connection_info["transfers"] = connection["transfers"] # Sections journey can be null if there is a walking section at first connection_info["number"] = "" for section in connection["sections"]: if section["journey"] is not None: journey = section["journey"] connection_info["number"] = journey["name"] connection_info["line"] = "".join( filter(None, [journey["category"], journey["number"]]) ) break connection_info["platform"] = connection["from"]["platform"] return connection_info async def async_get_data(self): """Retrieve the data for the connection.""" params = { "from": self.start, "to": self.destination, "limit": self.limit, "page": self.page, "isArrivalTime": self.isArrivalTime, "direct": self.direct, "sleeper": self.sleeper, "couchette": self.couchette, "bike": self.bike, } if self.via: params["via"] = self.via if self.time: params["time"] = self.time if self.date: params["date"] = self.date if self.transportations: params["transportations"] = self.transportations if self.accessibility: params["accessibility"] = self.accessibility if self.fields: params["fields"] = self.fields url = self.get_url("connections", params) try: response = await self._session.get(url, raise_for_status=True) _LOGGER.debug("Response from transport.opendata.ch: %s", response.status) data = await response.json() _LOGGER.debug(data) except asyncio.TimeoutError as e: _LOGGER.error("Can not load data from transport.opendata.ch") raise exceptions.OpendataTransportConnectionError() from e except aiohttp.ClientError as aiohttpClientError: _LOGGER.error("Response from transport.opendata.ch: %s", aiohttpClientError) raise exceptions.OpendataTransportConnectionError() from aiohttpClientError try: self.from_id = data["from"]["id"] self.from_name = data["from"]["name"] self.to_id = data["to"]["id"] self.to_name = data["to"]["name"] index = 0 for connection in data["connections"]: self.connections[index] = self.get_connection(connection) index = index + 1 except (KeyError, TypeError, IndexError) as e: raise exceptions.OpendataTransportError() from e python-opendata-transport-0.5.0/opendata_transport/exceptions.py000066400000000000000000000004331467610020200253200ustar00rootroot00000000000000"""Exceptions for OpenData transport API client.""" class OpendataTransportError(Exception): """General OpenDataError exception occurred.""" pass class OpendataTransportConnectionError(OpendataTransportError): """When a connection error is encountered.""" pass python-opendata-transport-0.5.0/setup.cfg000066400000000000000000000000511467610020200204730ustar00rootroot00000000000000[metadata] description-file = README.rst python-opendata-transport-0.5.0/setup.py000066400000000000000000000025321467610020200203720ustar00rootroot00000000000000#!/usr/bin/env python3 """Setup for the Transport OpenData wrapper.""" import os from setuptools import setup, find_packages here = os.path.abspath(os.path.dirname(__file__)) with open(os.path.join(here, "README.rst"), encoding="utf-8") as readme: long_description = readme.read() setup( name="python_opendata_transport", version="0.5.0", description="Python API for interacting with transport.opendata.ch.", long_description=long_description, url="https://github.com/home-assistant-ecosystem/python-opendata-transport", download_url="https://github.com/home-assistant-ecosystem/python-opendata-transport/releases", author="Fabian Affolter", author_email="fabian@affolter-engineering.ch", license="MIT", install_requires=[ "aiohttp>=3.8.5,<4", "urllib3", ], packages=find_packages(), python_requires=">=3.11", zip_safe=True, classifiers=[ "Development Status :: 3 - Alpha", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: Utilities", ], )