├── .github └── workflows │ └── tests.yaml ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── .gitignore ├── Makefile ├── _static │ └── usage │ │ └── images │ │ └── examples_3_journeys_map.jpg ├── changelog.rst ├── conf.py ├── development │ ├── api.rst │ ├── api │ │ ├── profiles │ │ │ ├── base.rst │ │ │ ├── db.rst │ │ │ ├── interfaces.rst │ │ │ └── vsn.rst │ │ └── types │ │ │ ├── hafas_response.rst │ │ │ └── station_board_request.rst │ ├── code_structure.rst │ ├── introduction.rst │ └── profiles.rst ├── glossary.rst ├── index.rst ├── make.bat └── usage │ ├── client.rst │ ├── examples.rst │ ├── exceptions.rst │ ├── fptf.rst │ ├── get_started.rst │ └── profiles.rst ├── example.py ├── exampleDB.py ├── example_rejseplanen.py ├── generateSalt.py ├── pyhafas ├── __init__.py ├── client.py ├── profile │ ├── __init__.py │ ├── base │ │ ├── __init__.py │ │ ├── helper │ │ │ ├── __init__.py │ │ │ ├── date_time.py │ │ │ ├── format_products_filter.py │ │ │ ├── parse_leg.py │ │ │ ├── parse_lid.py │ │ │ ├── parse_remark.py │ │ │ └── request.py │ │ ├── mappings │ │ │ ├── __init__.py │ │ │ └── error_codes.py │ │ └── requests │ │ │ ├── __init__.py │ │ │ ├── journey.py │ │ │ ├── journeys.py │ │ │ ├── location.py │ │ │ ├── nearby.py │ │ │ ├── station_board.py │ │ │ └── trip.py │ ├── db │ │ └── __init__.py │ ├── interfaces │ │ ├── __init__.py │ │ ├── helper │ │ │ ├── __init__.py │ │ │ ├── date_time.py │ │ │ ├── format_products_filter.py │ │ │ ├── parse_leg.py │ │ │ ├── parse_lid.py │ │ │ ├── parse_remark.py │ │ │ └── request.py │ │ ├── mappings │ │ │ ├── __init__.py │ │ │ └── error_codes.py │ │ └── requests │ │ │ ├── __init__.py │ │ │ ├── journey.py │ │ │ ├── journeys.py │ │ │ ├── location.py │ │ │ ├── nearby.py │ │ │ ├── station_board.py │ │ │ └── trip.py │ ├── kvb │ │ ├── __init__.py │ │ └── requests │ │ │ ├── __init__.py │ │ │ ├── journey.py │ │ │ └── journeys.py │ ├── nasa │ │ ├── __init__.py │ │ └── requests │ │ │ ├── __init__.py │ │ │ ├── journey.py │ │ │ └── journeys.py │ ├── nvv │ │ ├── __init__.py │ │ └── requests │ │ │ ├── __init__.py │ │ │ ├── journey.py │ │ │ └── journeys.py │ ├── rkrp │ │ └── __init__.py │ ├── vsn │ │ ├── __init__.py │ │ └── requests │ │ │ ├── __init__.py │ │ │ └── journey.py │ └── vvv │ │ ├── __init__.py │ │ └── requests │ │ ├── __init__.py │ │ ├── journey.py │ │ └── journeys.py └── types │ ├── __init__.py │ ├── exceptions.py │ ├── fptf.py │ ├── hafas_response.py │ ├── nearby.py │ └── station_board_request.py ├── pytest.ini ├── requirements.txt ├── setup.py ├── tests ├── __init__.py ├── base │ ├── __init__.py │ └── products_filter_test.py ├── db │ ├── __init__.py │ ├── parsing │ │ ├── __init__.py │ │ ├── departures_raw.json │ │ ├── departures_test.py │ │ ├── journey_raw.json │ │ ├── journey_test.py │ │ ├── journeys_raw.json │ │ ├── journeys_test.py │ │ ├── locations_raw.json │ │ ├── locations_test.py │ │ ├── trip_raw.json │ │ └── trip_test.py │ └── request │ │ ├── __init__.py │ │ ├── arrivals_test.py │ │ ├── departures_test.py │ │ ├── journey_test.py │ │ ├── locations_test.py │ │ └── nearby_test.py ├── distance.py ├── kvb │ ├── __init__.py │ ├── parsing │ │ ├── __init__.py │ │ ├── departures_raw.json │ │ └── departures_test.py │ └── request │ │ ├── __init__.py │ │ ├── arrivals_test.py │ │ ├── departures_test.py │ │ ├── journey_test.py │ │ ├── locations_test.py │ │ └── nearby_test.py ├── nasa │ ├── __init__.py │ └── requests │ │ ├── __init__.py │ │ ├── arrivals_test.py │ │ ├── departures_test.py │ │ ├── journey_test.py │ │ ├── locations_test.py │ │ └── nearby_test.py ├── nvv │ ├── __init__.py │ ├── parsing │ │ ├── __init__.py │ │ ├── departures_raw.json │ │ └── departures_test.py │ └── request │ │ ├── __init__.py │ │ ├── arrivals_test.py │ │ ├── departures_test.py │ │ ├── journey_test.py │ │ ├── locations_test.py │ │ └── nearby_test.py ├── types.py ├── vsn │ ├── __init__.py │ ├── parsing │ │ ├── __init__.py │ │ ├── departures_raw.json │ │ ├── departures_test.py │ │ ├── journeys_raw.json │ │ └── journeys_test.py │ └── request │ │ ├── __init__.py │ │ ├── arrivals_test.py │ │ ├── departures_test.py │ │ ├── journey_test.py │ │ ├── locations_test.py │ │ └── nearby_test.py └── vvv │ ├── __init__.py │ ├── parsing │ ├── __init__.py │ ├── departures_raw.json │ └── departures_test.py │ └── request │ ├── __init__.py │ ├── arrivals_test.py │ ├── departures_test.py │ ├── journey_test.py │ ├── locations_test.py │ └── nearby_test.py └── tox.ini /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | type: [ "opened", "reopened", "synchronize" ] 9 | schedule: 10 | - cron: '0 12 * * *' # run once a week on Sunday 11 | # Allow to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | jobs: 15 | tests: 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | python-version: ["3.9", "3.10", "3.11", "3.12"] 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v5 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | pip install -r requirements.txt 31 | - name: Run pytest 32 | run: pytest --junitxml=junit/test-results.xml -vv 33 | - name: Upload pytest test results 34 | uses: actions/upload-artifact@v4 35 | with: 36 | name: pytest-results-${{ matrix.python-version }} 37 | path: junit/test-results.xml 38 | # Use always() to always run this step to publish test results when there are test failures 39 | if: ${{ always() }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | .venv 4 | build 5 | dist 6 | *.egg* 7 | .idea 8 | .mypy_cache 9 | .coverage 10 | .tox 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 3.6 4 | - 3.7 5 | - 3.8 6 | 7 | env: 8 | - REQUESTS_VERSION="latest" PYTZ_VERSION="latest" 9 | - REQUESTS_VERSION="latest" PYTZ_VERSION="oldest" 10 | - REQUESTS_VERSION="oldest" PYTZ_VERSION="latest" 11 | - REQUESTS_VERSION="oldest" PYTZ_VERSION="oldest" 12 | 13 | install: 14 | - python setup.py install 15 | - pip install tox-travis 16 | 17 | script: tox 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2020-2024 Ember Keske , Leona Maroni 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # generated with check-manifest 2 | include *.py 3 | include *.txt 4 | include LICENSE 5 | include pytest.ini 6 | include tox.ini 7 | recursive-include docs *.bat 8 | recursive-include docs *.jpg 9 | recursive-include docs *.py 10 | recursive-include docs *.rst 11 | recursive-include docs Makefile 12 | recursive-include tests *.json 13 | recursive-include tests *.py 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyHaFAS 2 | **A python client for HaFAS public transport APIs**. 3 | 4 | [![](https://img.shields.io/pypi/v/pyhafas.svg)](https://pypi.python.org/pypi/pyhafas) 5 | [![](https://readthedocs.org/projects/pyhafas/badge/?version=latest)](https://pyhafas.readthedocs.io/) 6 | [![](https://api.travis-ci.com/n0emis/pyhafas.svg?branch=main)](https://travis-ci.com/github/n0emis/pyhafas) 7 | [![#pyhafas on matrix.org](https://img.shields.io/matrix/pyhafas:matrix.org?logo=matrix&server_fqdn=matrix.org)](https://riot.im/app/#/room/#pyhafas:matrix.org) 8 | 9 | ## Installation 10 | You only need to install the pyhafas package, for example using pip: 11 | 12 | ```bash 13 | $ pip install pyhafas 14 | ``` 15 | 16 | That’s it! 17 | 18 | ## Development setup 19 | For development is **recommended** to use a ``venv``. 20 | 21 | ```bash 22 | $ python3 -m venv .venv 23 | $ source .venv/bin/activate 24 | $ python setup.py develop 25 | ``` 26 | 27 | ## Background 28 | There's [a company called HaCon](https://hacon.de) that sells [a public transport management system called HAFAS](https://de.wikipedia.org/wiki/HAFAS). It is [used by companies all over Europe](https://gist.github.com/derhuerst/2b7ed83bfa5f115125a5) to serve routing and departure information for apps. All those endpoints are similar, with the same terms and API routes, but have slightly different options, filters and sets of enabled features. 29 | 30 | ## Related 31 | - [`hafas-client`](https://github.com/public-transport/hafas-client) – JavaScript client for the HAFAS API. 32 | - [`public-transport-enabler`](https://github.com/schildbach/public-transport-enabler) – Unleash public transport data in your Java project. 33 | - [*Friendly Public Transport Format*](https://github.com/public-transport/friendly-public-transport-format#friendly-public-transport-format-fptf) – A format for APIs, libraries and datasets containing and working with public transport data. 34 | - [`db-hafas`](https://github.com/derhuerst/db-hafas#db-hafas) – JavaScript client for the DB HAFAS API. 35 | 36 | ## Contributing 37 | If you **have a question**, **found a bug** or want to **propose a feature**, have a look at [the issues page](https://github.com/n0emis/pyhafas/issues). 38 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _build/ 2 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_static/usage/images/examples_3_journeys_map.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FahrplanDatenGarten/pyhafas/4e91ffd80fe214ea6c0db2954160eb2813a2e215/docs/_static/usage/images/examples_3_journeys_map.jpg -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | .. contents:: 5 | 6 | v0.6.1 7 | ------ 8 | * fix: regional trains type in KVB profile 9 | 10 | Internal changes: 11 | 12 | * fix: disable DB requests tests 13 | DB disabled their HaFAS 14 | 15 | v0.6.0 16 | ------ 17 | * feat: implement nearby request 18 | * fix: parsing station board results without a direction in base profile 19 | * fix: support additional TRSF leg type equivalent to walking in base profile 20 | This fixes VVV profile. 21 | * fix: fix journeys request in nasa and base profile 22 | 23 | Internal changes: 24 | 25 | * fix(testing): fix parsing test execution on windows 26 | * fix(testing): long-term fix for journey request tests. 27 | * feat(testing): execute tests in GitHub CI 28 | 29 | v0.5.0 30 | ------ 31 | * [feature] Add VVV Profile 32 | * [feature] Add NVV Profile 33 | * [fix] Typo in KVB Profile 34 | 35 | v0.4.0 36 | ------ 37 | * [feature] Parse leg remarks 38 | * [feature] Add KVB Profile 39 | * [feature] Add optional retry parameter for requests. You might activate retries with the `activate_retry()` function on your profile 40 | 41 | v0.3.1 42 | ------ 43 | * [BUG] Fix setting of default user agent 44 | 45 | v0.3.0 46 | ------ 47 | * [FEATURE] Add timezone awareness to all datetime time objects 48 | * [FEATURE] Allow filtering at :func:`pyhafas.client.HafasClient.departures` and :func:`pyhafas.client.HafasClient.arrivals` for direction station 49 | * [FEATURE] Add option to :func:`pyhafas.client.HafasClient.journeys` request to allow setting a maximum number of returned journeys 50 | * [BUG] Fix bug with some HaFAS versions in platform parsing 51 | * [BUG] Changed coordinate type from int to float 52 | * Better dependency requirements (less specific versions) 53 | * Add tests 54 | 55 | v0.2.0 56 | ------ 57 | * [BREAKING] Changed return format of :func:`pyhafas.client.HafasClient.arrivals` and :func:`pyhafas.client.HafasClient.departures` methods 58 | * [BREAKING] Removed deprecated parameter `max_journeys` in :func:`pyhafas.client.HafasClient.arrivals` and :func:`pyhafas.client.HafasClient.departures` methods 59 | * [BUG] Fixed :func:`pyhafas.client.HafasClient.journey` request in VSN-Profile 60 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | 16 | sys.path.insert(0, os.path.abspath('..')) 17 | 18 | import pyhafas 19 | 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = 'pyHaFAS' 24 | copyright = '2020 n0emis, em0lar' 25 | author = 'n0emis, em0lar' 26 | 27 | # The full version, including alpha/beta/rc tags 28 | release = '0.3.1' 29 | 30 | 31 | # -- General configuration --------------------------------------------------- 32 | 33 | master_doc = 'index' 34 | 35 | # Add any Sphinx extension module names here, as strings. They can be 36 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 37 | # ones. 38 | extensions = [ 39 | 'sphinx.ext.autodoc', 40 | 'sphinx.ext.intersphinx', 41 | 'sphinx.ext.todo', 42 | 'sphinx.ext.doctest', 43 | 'sphinx.ext.napoleon', 44 | 'sphinx_autodoc_typehints', 45 | 'sphinx.ext.viewcode', 46 | "sphinx_rtd_theme", 47 | ] 48 | 49 | intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} 50 | 51 | # Add any paths that contain templates here, relative to this directory. 52 | #templates_path = ['_templates'] 53 | 54 | # List of patterns, relative to source directory, that match files and 55 | # directories to ignore when looking for source files. 56 | # This pattern also affects html_static_path and html_extra_path. 57 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 58 | 59 | 60 | # -- Options for HTML output ------------------------------------------------- 61 | 62 | # The theme to use for HTML and HTML Help pages. See the documentation for 63 | # a list of builtin themes. 64 | # 65 | pygments_style = 'sphinx' 66 | html_theme = 'sphinx_rtd_theme' 67 | 68 | # Add any paths that contain custom static files (such as style sheets) here, 69 | # relative to this directory. They are copied after the builtin static files, 70 | # so a file named "default.css" will overwrite the builtin "default.css". 71 | html_static_path = ['_static'] 72 | 73 | 74 | autoclass_content = "class" 75 | -------------------------------------------------------------------------------- /docs/development/api.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | .. toctree:: 5 | :glob: 6 | :maxdepth: 2 7 | 8 | /development/api/profiles/* 9 | /development/api/types/* 10 | /usage/client 11 | /usage/fptf 12 | /usage/exceptions 13 | -------------------------------------------------------------------------------- /docs/development/api/profiles/base.rst: -------------------------------------------------------------------------------- 1 | BaseProfile 2 | =========== 3 | 4 | .. contents:: 5 | 6 | For a documentation of the variables, please look at the documentation of :class:`ProfileInterface ` 7 | 8 | .. automodule:: pyhafas.profile.base 9 | :members: 10 | :undoc-members: 11 | :show-inheritance: 12 | 13 | Helper 14 | ------ 15 | format_products_filter 16 | ^^^^^^^^^^^^^^^^^^^^^^ 17 | .. automodule:: pyhafas.profile.base.helper.format_products_filter 18 | :members: 19 | :undoc-members: 20 | :show-inheritance: 21 | 22 | date_time 23 | ^^^^^^^^^^^^^^^ 24 | .. automodule:: pyhafas.profile.base.helper.date_time 25 | :members: 26 | :undoc-members: 27 | :show-inheritance: 28 | 29 | parse_leg 30 | ^^^^^^^^^ 31 | .. automodule:: pyhafas.profile.base.helper.parse_leg 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | parse_lid 37 | ^^^^^^^^^ 38 | .. automodule:: pyhafas.profile.base.helper.parse_lid 39 | :members: 40 | :undoc-members: 41 | :show-inheritance: 42 | 43 | request 44 | ^^^^^^^^^ 45 | .. automodule:: pyhafas.profile.base.helper.request 46 | :members: 47 | :undoc-members: 48 | :show-inheritance: 49 | 50 | Mappings 51 | -------- 52 | 53 | error_codes 54 | ^^^^^^^^^^^ 55 | .. automodule:: pyhafas.profile.base.mappings.error_codes 56 | :members: 57 | :undoc-members: 58 | :show-inheritance: 59 | 60 | Requests 61 | -------- 62 | 63 | journey 64 | ^^^^^^^ 65 | .. automodule:: pyhafas.profile.base.requests.journey 66 | :members: 67 | :undoc-members: 68 | :show-inheritance: 69 | 70 | journeys 71 | ^^^^^^^^ 72 | .. automodule:: pyhafas.profile.base.requests.journeys 73 | :members: 74 | :undoc-members: 75 | :show-inheritance: 76 | 77 | location 78 | ^^^^^^^^ 79 | .. automodule:: pyhafas.profile.base.requests.location 80 | :members: 81 | :undoc-members: 82 | :show-inheritance: 83 | 84 | station_board 85 | ^^^^^^^^^^^^^ 86 | .. automodule:: pyhafas.profile.base.requests.station_board 87 | :members: 88 | :undoc-members: 89 | :show-inheritance: 90 | 91 | trip 92 | ^^^^ 93 | .. automodule:: pyhafas.profile.base.requests.trip 94 | :members: 95 | :undoc-members: 96 | :show-inheritance: 97 | -------------------------------------------------------------------------------- /docs/development/api/profiles/db.rst: -------------------------------------------------------------------------------- 1 | DBProfile 2 | ========= 3 | 4 | .. contents:: 5 | 6 | For a documentation of the variables, please look at the documentation of :class:`ProfileInterface ` 7 | 8 | .. automodule:: pyhafas.profile.db 9 | :members: 10 | :undoc-members: 11 | :show-inheritance: 12 | -------------------------------------------------------------------------------- /docs/development/api/profiles/interfaces.rst: -------------------------------------------------------------------------------- 1 | ProfileInterface 2 | ================ 3 | 4 | .. contents:: 5 | 6 | .. automodule:: pyhafas.profile.interfaces 7 | :members: 8 | :undoc-members: 9 | :show-inheritance: 10 | 11 | Helper 12 | ------ 13 | format_products_filter 14 | ^^^^^^^^^^^^^^^^^^^^^^ 15 | .. automodule:: pyhafas.profile.interfaces.helper.format_products_filter 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | date_time 21 | ^^^^^^^^^^^^^^^ 22 | .. automodule:: pyhafas.profile.interfaces.helper.date_time 23 | :members: 24 | :undoc-members: 25 | :show-inheritance: 26 | 27 | parse_leg 28 | ^^^^^^^^^ 29 | .. automodule:: pyhafas.profile.interfaces.helper.parse_leg 30 | :members: 31 | :undoc-members: 32 | :show-inheritance: 33 | 34 | parse_lid 35 | ^^^^^^^^^ 36 | .. automodule:: pyhafas.profile.interfaces.helper.parse_lid 37 | :members: 38 | :undoc-members: 39 | :show-inheritance: 40 | 41 | request 42 | ^^^^^^^^^ 43 | .. automodule:: pyhafas.profile.interfaces.helper.request 44 | :members: 45 | :undoc-members: 46 | :show-inheritance: 47 | 48 | Mappings 49 | -------- 50 | 51 | error_codes 52 | ^^^^^^^^^^^ 53 | .. automodule:: pyhafas.profile.interfaces.mappings.error_codes 54 | :members: 55 | :undoc-members: 56 | :show-inheritance: 57 | 58 | Requests 59 | -------- 60 | 61 | journey 62 | ^^^^^^^ 63 | .. automodule:: pyhafas.profile.interfaces.requests.journey 64 | :members: 65 | :undoc-members: 66 | :show-inheritance: 67 | 68 | journeys 69 | ^^^^^^^^ 70 | .. automodule:: pyhafas.profile.interfaces.requests.journeys 71 | :members: 72 | :undoc-members: 73 | :show-inheritance: 74 | 75 | location 76 | ^^^^^^^^ 77 | .. automodule:: pyhafas.profile.interfaces.requests.location 78 | :members: 79 | :undoc-members: 80 | :show-inheritance: 81 | 82 | station_board 83 | ^^^^^^^^^^^^^ 84 | .. automodule:: pyhafas.profile.interfaces.requests.station_board 85 | :members: 86 | :undoc-members: 87 | :show-inheritance: 88 | 89 | trip 90 | ^^^^ 91 | .. automodule:: pyhafas.profile.interfaces.requests.trip 92 | :members: 93 | :undoc-members: 94 | :show-inheritance: 95 | -------------------------------------------------------------------------------- /docs/development/api/profiles/vsn.rst: -------------------------------------------------------------------------------- 1 | VSNProfile 2 | ========== 3 | 4 | .. contents:: 5 | 6 | For a documentation of the variables, please look at the documentation of :class:`ProfileInterface ` 7 | 8 | .. automodule:: pyhafas.profile.vsn 9 | :members: 10 | :undoc-members: 11 | :show-inheritance: 12 | -------------------------------------------------------------------------------- /docs/development/api/types/hafas_response.rst: -------------------------------------------------------------------------------- 1 | HafasResponse 2 | ============= 3 | 4 | .. automodule:: pyhafas.types.hafas_response 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/development/api/types/station_board_request.rst: -------------------------------------------------------------------------------- 1 | StationBoardRequestType 2 | ======================= 3 | 4 | .. automodule:: pyhafas.types.station_board_request 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/development/code_structure.rst: -------------------------------------------------------------------------------- 1 | Code Structure 2 | ============== 3 | 4 | Classes and methods 5 | ------------------- 6 | pyHaFAS is built object-orientated with a lot of classes. You may know already the :class:`HafasClient ` and the :term:`profile`:superscript:`G` classes but there a lot more classes for internal use only. 7 | For example the :class:`BaseProfile ` class consists among others of :class:`BaseRequestHelper `, :class:`BaseJourneyRequest ` and very important the :class:`ProfileInterface ` 8 | 9 | Every class in a :term:`profile`:superscript:`G` has an `interface` that defines abstract methods that the profile class must implement. 10 | 11 | A more detailed view of the construction of a :term:`profile`:superscript:`G` is given on the page :doc:`profiles`. 12 | 13 | File Structure 14 | -------------- 15 | pyHaFAS's code is split in multiple files. These files are sorted as shown in the structure below. 16 | 17 | * **/pyhafas** - base directory of source code 18 | 19 | * **profile** - contains the profiles (every subdirectory should have the same structure as `base`) 20 | 21 | * **interfaces** - contains all abstract classes 22 | * **base** - Base profile - contains the default handling classes and methods 23 | 24 | * **__init__.py** - contains the Profile 25 | * **helper** - contains helper functions that are used by multiple requests 26 | * **mappings** - contains mapping Enum classes 27 | * **requests** - contains code responsible for requests (there is a file for each request-type containing all methods belonging to it) 28 | 29 | * **PROFILENAME** - contains the files for the profile (only files and directories with changes) 30 | 31 | * **types** - Base directory of all types valid for all profiles 32 | 33 | * **exceptions.py** - exceptions pyHaFAS can raise 34 | * **fptf.py** - most of the types important for pyHaFAS 35 | * **hafas_response.py** - contains the :class:`HafasRepsonse ` class 36 | 37 | * **client.py** - contains the :class:`HafasClient ` 38 | -------------------------------------------------------------------------------- /docs/development/introduction.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | Thank you for to interest in contributing to pyHaFAS. In general, we manage the project via the GitHub `issues page `_. 4 | When you are interested in one of these issues, please write a comment, and then we eagerly await your pull request. 5 | In special we are happy if you help us with the issues having the `help wanted `_ label. 6 | 7 | We want to give you an easy start with coding on pyHaFAS, so please read the pages in the category "for developers". 8 | 9 | Most of pyHaFAS code is documented with docstrings in the code directly. Most of these docstrings are also rendered in this HTML docs on the page :doc:`/development/api`. 10 | 11 | If you need help you can always write to us. Good ways for that are on the GitHub Issue or if the question is general, join our matrix-room `#pyhafas:matrix.org `_ 12 | 13 | Testing 14 | ------- 15 | Before you submit your pull request please run the tests. They are also automatically run when you create the pull request. 16 | You can execute the tests with the following command in the root directory of the project: 17 | 18 | .. code:: console 19 | 20 | $ pytest 21 | -------------------------------------------------------------------------------- /docs/development/profiles.rst: -------------------------------------------------------------------------------- 1 | Profile (Developer) 2 | =================== 3 | Since every HaFAS deployment is different we have the concept of :term:`profiles `:superscript:`G` for managing the differences between them. 4 | 5 | For a "standard" HaFAS we have the :class:`BaseProfile ` which contains code working for most HaFAS deployments. 6 | This profile inherits the methods of all classes living in the folder of the profile. That classes all have an interface that defines the abstract methods of the class. 7 | 8 | Most methods are instance methods, so they have a `self` parameter. Since the class using the method is the profile we use as type hint for `self` :class:`ProfileInterface `. 9 | This interface defines the variables and methods a profile has. 10 | 11 | How to build a profile? 12 | ----------------------- 13 | Requirements 14 | ^^^^^^^^^^^^ 15 | 16 | * You need to know how to authenticate to the HaFAS deployment. In most cases, you need to know if the authentication is via mic-mac or checksum+salt and you need the salt. The salt can get out of the source code of official apps of the transport provider or you can look if other HaFAS libraries already support the transport provider and get the salt from them 17 | * You need the general requests body. You can get this best with mitmproxy on the official app of the transport provider or of other libraries as in the other requirement 18 | 19 | One good source for both requirements is `hafas-client `_ in the specific profile folder. 20 | 21 | Steps 22 | ^^^^^ 23 | 1. Read the API of a profile. See :ref:`below `. 24 | 2. Have a look at the already existing profiles and the :class:`ProfileInterface ` to get the structure of a profile. 25 | 3. Create a new product folder with an `__init__.py` file containing the profile class 26 | 4. Fill the required variables 27 | 5. Add your profile to `/pyhafas/profile/__init__.py` 28 | 6. Test if everything works 29 | 7. Yes? Perfect, add your profile in the documentation and create a pull request! No? Go on with step 8. 30 | 8. Log the request pyHaFAS sends and compare this request with the one the official app sends. Is the difference only in one specific request type or in all? If it is only in one go on stop step 8a otherwise step 8b. 31 | 32 | a. You could create a new class overwriting the `format`-method of the request. An example where this is done is in the VSNProfile the :func:`format_journey_request` method in the :class:`pyhafas.profile.vsn.requests.journey.VSNJourneyRequest` class. 33 | b. Please make sure the `requestBody` in your profile is correct. If it is, please :doc:`contact ` us or try to find the method causing the error yourself. 34 | 35 | If you need help with any of that you can :doc:`contact ` us always! 36 | 37 | .. _api: 38 | 39 | API 40 | ^^^ 41 | Here are the minimum required variables of a profile (generated from :class:`ProfileInterface `): 42 | 43 | .. autoclass:: pyhafas.profile.interfaces.ProfileInterface 44 | :members: 45 | :undoc-members: 46 | :noindex: 47 | -------------------------------------------------------------------------------- /docs/glossary.rst: -------------------------------------------------------------------------------- 1 | Glossary 2 | ----------- 3 | 4 | .. glossary:: 5 | 6 | profile 7 | Customization for each HaFAS deployment 8 | - Contains the endpoint, tokens and possible changes for the deployment 9 | 10 | FPTF 11 | Abbreviation for `Friendly Public Transport Format `_ 12 | - Used as the basis for returned data 13 | 14 | Station Board 15 | Generalization for :func:`arrivals ` and :func:`departures ` requests 16 | 17 | product 18 | A product is the generalization of all means of transport. When this term is used, all types of transport are meant (e.g. busses, regional trains, ferries). 19 | 20 | journey 21 | A journey is a computed set of directions to get from A to B at a specific time. It would typically be the result of a route planning algorithm. 22 | 23 | leg 24 | A leg or also named trip is most times part of a journey and defines a journey with only one specific vehicle from A to B. 25 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | pyHaFAS - General Information 2 | =================================== 3 | 4 | .. image:: https://img.shields.io/pypi/v/pyhafas.svg 5 | :target: https://pypi.python.org/pypi/pyhafas 6 | 7 | pyHaFAS is a client for the API of HaFAS public transport management system. 8 | 9 | HaFAS is a software sold by the company "`HaCon `_" and is used by a lot of public transport providers for routing and providing departure information to their customers. 10 | 11 | Every public transport providers using HaFAS has their own deployment. 12 | In general all of them have the same API but there are some small differences between them. To cover this we have a :term:`profile`:superscript:`G` for each HaFAS deployment. 13 | 14 | .. WARNING:: pyHaFAS is still in beta. 15 | The interface might change, so please read the changelog carefully before you update. 16 | 17 | Contributing 18 | ------------ 19 | If you have a question, found a bug or want to propose a feature, have a look at `the issues page `_. 20 | Even better than creating an issue is creating a pull request. If you want to do that please read the :doc:`development introduction `. 21 | 22 | 23 | .. toctree:: 24 | :caption: Installation and usage 25 | :maxdepth: 2 26 | 27 | usage/get_started 28 | usage/profiles 29 | usage/client 30 | usage/fptf 31 | usage/exceptions 32 | usage/examples 33 | 34 | .. toctree:: 35 | :caption: For developers 36 | :maxdepth: 2 37 | 38 | development/introduction 39 | development/code_structure 40 | development/profiles 41 | development/api 42 | 43 | .. toctree:: 44 | :caption: General 45 | 46 | glossary 47 | changelog 48 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/usage/client.rst: -------------------------------------------------------------------------------- 1 | HafasClient 2 | =========== 3 | The `HafasClient` is the interface between your program and pyhafas internal code. 4 | You need it when you're using pyHaFAS. Below you can find the API of the client. 5 | 6 | .. automodule:: pyhafas.client 7 | :members: 8 | :undoc-members: 9 | -------------------------------------------------------------------------------- /docs/usage/examples.rst: -------------------------------------------------------------------------------- 1 | Usage Examples 2 | ============== 3 | Below you can find usage examples for each method available in :doc:`HafasClient `. 4 | 5 | General Information 6 | ------------------- 7 | In the following code blocks, we only use :func:`departures ` but not :func:`arrivals `. 8 | Those methods are pretty same, so every time we use :func:`departures ` you can exchange this with :func:`arrivals `. 9 | 10 | We also only use some of the supported clients. The client can be exchanged, if not specified otherwise. 11 | 12 | .. _example1: 13 | 14 | 1. locations + departures 15 | ------------------------- 16 | 17 | The below code gets the departing long-distance trains at the station with the best similarity when searching for "Siegburg/Bonn". 18 | Let's get to the parts of the code: 19 | 20 | 1. The required classes are imported, a :class:`HafasClient ` is created with the :class:`DBProfile ` 21 | 2. **Location-Search** 22 | 23 | 1. The :class:`HafasClient ` searches for locations with the term "Siegburg/Bonn". 24 | 2. The best location is chosen from the list (the first object in the list is that with the highest similarity) 25 | 26 | 3. The :class:`HafasClient ` searches for maximum 2 :term:`trips `:superscript:`G` with the following criteria: 27 | 28 | * departing now 29 | * at the best location (from step 2) 30 | * with the products in the categories `long_distance_express` or `long_distance`. `long_distance` is enabled per default and so per default in the list of enabled products and all others (except `long_distance_express`) are disabled. 31 | 32 | *long_distance_express is also enabled per default but it can be in the list with True as value to guarantee it's enabled, if it wouldn't be enabled by default, it would be enabled now* 33 | 34 | .. code:: python 35 | 36 | import datetime 37 | from typing import List 38 | 39 | # Part 1 40 | from pyhafas import HafasClient 41 | from pyhafas.profile import DBProfile 42 | from pyhafas.types.fptf import Leg 43 | 44 | client = HafasClient(DBProfile()) 45 | 46 | # Part 2 47 | locations = client.locations("Siegburg/Bonn") 48 | best_found_location = locations[0] 49 | print(best_found_location) # ({'id': '008005556', 'name': 'Siegburg/Bonn', 'latitude': 50.794051, 'longitude': 7.202616}) 50 | 51 | # Part 3 52 | departures: List[Leg] = client.departures( 53 | station=best_found_location.id, 54 | date=datetime.datetime.now(), 55 | max_trips=2, 56 | products={ 57 | 'long_distance_express': True, 58 | 'regional_express': False, 59 | 'regional': False, 60 | 'suburban': False, 61 | 'bus': False, 62 | 'ferry': False, 63 | 'subway': False, 64 | 'tram': False, 65 | 'taxi': False 66 | } 67 | ) 68 | print(departures) # [({...}), ({...})] 69 | 70 | .. _example2: 71 | 72 | 2. departures + trip 73 | -------------------- 74 | The below code get the next departing :term:`trip `:superscript:`G` at the station "Siegburg/Bonn" (with the id `008005556`) and gets after that detailed information with the :func:`trip ` method. 75 | 76 | Currently, the :func:`trip ` method gives the same data as :func:`departures `, but in future versions, there will be more data available in :func:`trip `. 77 | 78 | Using the :func:`trip ` method is also useful to refresh the data about a specific :term:`trip `:superscript:`G` by its ID. 79 | 80 | .. code:: python 81 | 82 | import datetime 83 | 84 | # Part 1 85 | from pyhafas import HafasClient 86 | from pyhafas.profile import DBProfile 87 | from pyhafas.types.fptf import Leg 88 | 89 | client = HafasClient(DBProfile()) 90 | 91 | # Part 2 92 | departure: Leg = client.departures( 93 | station="008005556", 94 | date=datetime.datetime.now(), 95 | max_trips=1 96 | )[0] 97 | print(departure) # ({'id': '1|236759|0|80|26072020', ...}) 98 | 99 | # Part 3 100 | trip: Leg = client.trip(departure.id) 101 | print(trip) # ({'id': '1|236759|0|80|26072020', ...}) 102 | 103 | .. _example3: 104 | 105 | 3. locations + journeys + journey 106 | --------------------------------- 107 | In the code block below we create search for possible :term:`journeys `:superscript:`G` between the stations "Göttingen Bahnhof/ZOB" and "Góttingen Campus" via "Göttingen Angerstraße". 108 | 109 | For an explanation of the first and second part please look at :ref:`example 1 `. After the code, there is also a visualization of a journey HaFAS returns for this request. 110 | 111 | In part 3 the HafasClient searches for :term:`journeys `:superscript:`G` with the following criteria: 112 | 113 | * origin station is "Göttingen Bahnhof/ZOB" 114 | * destination station is "Göttingen Campus" 115 | * the :term:`journey`:superscript:`G` must be via "Göttingen Angerstraße" 116 | * the :term:`journey`:superscript:`G` may have a maximum of 1 transfer 117 | * each transfer must have at least a time of 15 minutes 118 | 119 | In part 4 the :term:`journey`:superscript:`G` data of the first :term:`journey`:superscript:`G` found in part 3 is refreshed. 120 | 121 | .. code:: python 122 | 123 | import datetime 124 | 125 | # Part 1 126 | from pyhafas import HafasClient 127 | from pyhafas.profile import VSNProfile 128 | from pyhafas.types.fptf import Leg 129 | 130 | client = HafasClient(VSNProfile()) 131 | 132 | # Part 2 133 | location_goe_bf = client.locations("Göttingen Bahnhof/ZOB")[0] 134 | location_goe_ang = client.locations("Göttingen Angerstraße")[0] 135 | location_goe_campus = client.locations("Göttingen Campus")[0] 136 | 137 | # Part 3 138 | journeys = client.journeys( 139 | origin=location_goe_bf, 140 | via=[location_goe_ang], 141 | destination=location_goe_campus, 142 | date=datetime.datetime.now(), 143 | max_changes=1, 144 | min_change_time=15 145 | ) 146 | print(journeys) # [({...}), ({...}), ({...}), ...]})] 147 | 148 | # Part 4 149 | journey = client.journey(journeys[0].id) 150 | 151 | print(journey) # ({...}) 152 | 153 | *As short-form for Göttingen, we use GOE* 154 | 155 | Here is a table with the :term:`journey`:superscript:`G` in the variable `journey` of the code example above. 156 | Here some explanation on the routing algorithm of HaFAS: 157 | 158 | * You might see that the walk leg is exactly 15 minutes. This is because we set a minimum change time of 15 minutes. A normal walking time would be about 5 minutes. 159 | * A walk leg does not count in the number of changes between legs. The maximum number of changes only specifies how many vehicles you change. 160 | * You might think that there's a bug because the via station (GOE Angerstraße, 2) is not in the table below. That's correct. For HaFAS it's enough when a vehicle stops at the via station. In this example, the first and second bus both stops at "GOE Angerstraße". 161 | 162 | ===================== ===================== ============== ============ ================= 163 | origin station destination station departure time arrival time mode of transport 164 | ===================== ===================== ============== ============ ================= 165 | GOE Bahnhof (1) GOE Neues Rathaus (3) 11:40 11:44 BUS 166 | GOE Neues Rathaus (3) GOE Bürgerstraße (4) 11:44 11:59 WALKING 167 | GOE Bürgerstraße (4) GOE Campus (5) 12:00 12:13 BUS 168 | ===================== ===================== ============== ============ ================= 169 | 170 | .. figure:: /_static/usage/images/examples_3_journeys_map.jpg 171 | 172 | .. centered:: map showing the stations, © OpenStreetMap contributors. Tiles courtesy of MeMoMaps. 173 | 174 | 175 | -------------------------------------------------------------------------------- /docs/usage/exceptions.rst: -------------------------------------------------------------------------------- 1 | Exceptions 2 | ========== 3 | 4 | pyHaFAS can raise multiple exceptions. In the message of the exception you can always find the complete text error message HaFAS returned. 5 | 6 | .. automodule:: pyhafas.types.exceptions 7 | :members: 8 | :undoc-members: 9 | -------------------------------------------------------------------------------- /docs/usage/fptf.rst: -------------------------------------------------------------------------------- 1 | Friendly Public Transport Format (FPTF) 2 | ======================================= 3 | 4 | Most types used in pyHaFAS are specified in the `Friendly Public Transport Format `_. 5 | With this specification, we build python classes in the module `pyhafas.types.fptf`. You can find the reference for those classes below. 6 | 7 | .. automodule:: pyhafas.types.fptf 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | -------------------------------------------------------------------------------- /docs/usage/get_started.rst: -------------------------------------------------------------------------------- 1 | Get started 2 | =========== 3 | 4 | Terminology 5 | ----------- 6 | In pyHaFAS, we often use the following terms. Please read them, so you can understand the documentation and project better. 7 | 8 | Most other pyHaFAS-specific words are defined in the :doc:`/glossary`. 9 | If one of these words is used in this documentation it's marked as a link with a superscript G as follows: :term:`profile`:superscript:`G` 10 | 11 | 12 | ============= ======= 13 | Term Meaning 14 | ============= ======= 15 | profile Customization for each HaFAS deployment - Contains the endpoint, tokens and possible changes for the deployment 16 | FPTF Abbreviation for `Friendly Public Transport Format `_ - Used as the basis for returned data 17 | Station Board Generalization for :func:`arrivals ` and :func:`departures ` requests 18 | ============= ======= 19 | 20 | Installation 21 | ------------ 22 | You only need to install the pyhafas package, for example using pip: 23 | 24 | .. code:: console 25 | 26 | $ pip install pyhafas 27 | 28 | Sample Starter Code 29 | ------------------- 30 | Below is a sample code for easy usage. It has multiple parts: 31 | 32 | 1. It imports the :class:`HafasClient ` and the :class:`DBProfile ` of pyHaFAS and creates the :class:`HafasClient ` with the :term:`profile`:superscript:`G`. The :class:`DBProfile ` is the :term:`profile`:superscript:`G` belonging to the HaFAS deployment of Deutsche Bahn. 33 | 34 | 2. It searches for locations (stations) with the term "Berlin" and prints the result 35 | 36 | 3. It searches for departing trains at Berlin Central Station. Every station is identified by an ID, which (in this case `8011160`) can be obtained by a `location`-request with pyhafas. 37 | 38 | .. code:: python 39 | 40 | import datetime 41 | from pyhafas import HafasClient 42 | from pyhafas.profile import DBProfile 43 | 44 | client = HafasClient(DBProfile()) 45 | 46 | print(client.locations("Berlin")) 47 | 48 | print(client.departures( 49 | station='8011160', 50 | date=datetime.datetime.now(), 51 | max_trips=5 52 | )) 53 | 54 | What's next? 55 | ------------ 56 | 57 | For a good start with pyHaFAS you should go on reading the documentation. Especially the pages :doc:`/usage/examples` and :doc:`profiles` are a good start. 58 | -------------------------------------------------------------------------------- /docs/usage/profiles.rst: -------------------------------------------------------------------------------- 1 | Profiles 2 | ======== 3 | Here's a list of all HaFAS deployments pyHaFAS supports. 4 | If the :term:`profile`:superscript:`G` has any differences, they will be mentioned here. Also, available and default :term:`products `:superscript:`G` are defined. 5 | 6 | Deutsche Bahn (DB) 7 | ------------------ 8 | Usage 9 | ^^^^^^ 10 | .. code:: python 11 | 12 | from pyhafas.profile import DBProfile 13 | client = HafasClient(DBProfile()) 14 | 15 | Available Products 16 | ^^^^^^^^^^^^^^^^^^ 17 | 18 | ===================== ================== 19 | pyHaFAS Internal Name Example Train Type 20 | ===================== ================== 21 | long_distance_express ICE/ECE 22 | long_distance IC/EC 23 | regional_express RE/IRE 24 | regional RB 25 | suburban S 26 | bus BUS 27 | ferry F 28 | subway U 29 | tram STR/T 30 | taxi Group Taxi 31 | ===================== ================== 32 | 33 | Default Products 34 | ^^^^^^^^^^^^^^^^ 35 | All available products specified above are enabled by default. 36 | 37 | Other interesting Stuff 38 | ^^^^^^^^^^^^^^^^^^^^^^^ 39 | * Mapping list with station IDs exists: ``_ 40 | 41 | Verkehrsverbund Süd-Niedersachsen (VSN) 42 | --------------------------------------- 43 | Usage 44 | ^^^^^^ 45 | .. code:: python 46 | 47 | from pyhafas.profile import VSNProfile 48 | client = HafasClient(VSNProfile()) 49 | 50 | Available Products 51 | ^^^^^^^^^^^^^^^^^^ 52 | 53 | ===================== ================== 54 | pyHaFAS Internal Name Example Train Type 55 | ===================== ================== 56 | long_distance_express ICE/ECE 57 | long_distance IC/EC/CNL 58 | regional_express RE/IRE 59 | regional NV (e.g. RB) 60 | suburban S 61 | bus BUS 62 | ferry F 63 | subway U 64 | tram STR/T 65 | anruf_sammel_taxi Group Taxi 66 | ===================== ================== 67 | 68 | Default Products 69 | ^^^^^^^^^^^^^^^^ 70 | All available products specified above are enabled by default. 71 | 72 | Specialities 73 | ^^^^^^^^^^^^ 74 | 75 | * The `max_trips` filter in station board (departures/arrival) requests seems not to work 76 | 77 | 78 | Nahverkehr Sachsen-Anhalt (NASA) 79 | --------------------------------------- 80 | Usage 81 | ^^^^^^ 82 | .. code:: python 83 | 84 | from pyhafas.profile import NASAProfile 85 | client = HafasClient(NASAProfile()) 86 | 87 | Available Products 88 | ^^^^^^^^^^^^^^^^^^ 89 | 90 | ===================== ================== 91 | pyHaFAS Internal Name Example Train Type 92 | ===================== ================== 93 | long_distance_express ICE/ECE 94 | long_distance IC/EC/CNL 95 | regional RE / RB 96 | suburban S 97 | bus BUS 98 | tram STR/T 99 | tourism_train TT 100 | ===================== ================== 101 | 102 | Default Products 103 | ^^^^^^^^^^^^^^^^ 104 | All available products specified above are enabled by default. 105 | 106 | Specialities 107 | ^^^^^^^^^^^^ 108 | 109 | Part of NASA are tourism trains, for example the 'Harzer Schmalspurbahnen' (Light railway of Harz) which climbs the Brocken mountain (1141m). 110 | 111 | 112 | 113 | Kölner Verkehrsbetriebe (KVB) 114 | ----------------------------- 115 | Usage 116 | ^^^^^^ 117 | .. code:: python 118 | 119 | from pyhafas.profile import KVBProfile 120 | client = HafasClient(KVBProfile()) 121 | 122 | Available Products 123 | ^^^^^^^^^^^^^^^^^^ 124 | 125 | ===================== ================== 126 | pyHaFAS Internal Name Example Train Type 127 | ===================== ================== 128 | s-bahn S 129 | stadtbahn U 130 | bus BUS 131 | fernverkehr ICE/ECE/IC/EC 132 | regionalverkehr RE/IRE 133 | taxibus Group Taxi 134 | ===================== ================== 135 | 136 | Default Products 137 | ^^^^^^^^^^^^^^^^ 138 | All available products specified above are enabled by default. 139 | 140 | 141 | Verkehrsverbund Vorarlberg (VVV) 142 | ----------------------------- 143 | Usage 144 | ^^^^^^ 145 | .. code:: python 146 | 147 | from pyhafas.profile import VVVProfile 148 | client = HafasClient(VVVProfile()) 149 | 150 | Available Products 151 | ^^^^^^^^^^^^^^^^^^ 152 | 153 | ===================== ================== 154 | pyHaFAS Internal Name Train Type 155 | ===================== ================== 156 | train-and-s-bahn Bahn & S-Bahn 157 | u-bahn U-Bahn 158 | tram Straßenbahn 159 | long-distance-bus Fernbus 160 | regional-bus Regionalbus 161 | city-bus Stadtbus 162 | aerial-lift Seil-/Zahnradbahn 163 | ferry Schiff 164 | on-call Anrufsammeltaxi 165 | other-bus sonstige Busse 166 | ===================== ================== 167 | 168 | Default Products 169 | ^^^^^^^^^^^^^^^^ 170 | All available products specified above are enabled by default. -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from pyhafas import HafasClient 4 | from pyhafas.profile import DBProfile, VSNProfile 5 | 6 | client = HafasClient(DBProfile(), debug=True) 7 | 8 | print(client.departures( 9 | station='8000128', 10 | date=datetime.datetime.now(), 11 | max_trips=5 12 | )) 13 | 14 | print(client.arrivals( 15 | station='8005556', 16 | date=datetime.datetime.now(), 17 | max_trips=5 18 | )) 19 | print(client.journey('¶HKI¶T$A=1@O=Berlin Jungfernheide@L=8011167@a=128@$A=1@O=Berlin Hbf (tief)@L=8098160@a=128@$202002101544$202002101549$RB 18521$$1$§T$A=1@O=Berlin Hbf (tief)@L=8098160@a=128@$A=1@O=München Hbf@L=8000261@a=128@$202002101605$202002102002$ICE 1007$$1$')) 20 | print(client.journeys( 21 | destination="8000207", 22 | origin="8005556", 23 | date=datetime.datetime.now(), 24 | min_change_time=0, 25 | max_changes=-1 26 | )) 27 | print(client.locations("Köln Hbf")) 28 | 29 | print(client.trip("1|1372374|3|80|9062020")) 30 | 31 | print('='*20) 32 | vsn = HafasClient(VSNProfile()) 33 | print(vsn.departures( 34 | station='9034033', 35 | date=datetime.datetime.now(), 36 | max_journeys=5 37 | )) 38 | -------------------------------------------------------------------------------- /exampleDB.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from pyhafas import HafasClient 4 | from pyhafas.profile import DBProfile 5 | 6 | profile = DBProfile() 7 | profile.activate_retry() 8 | client = HafasClient(profile, debug=True) 9 | 10 | origin = 8096009 11 | destinations = [ 12 | 8011160, 13 | 8010060, 14 | 8000050, 15 | 8000019, 16 | 8071993, 17 | 8000351, 18 | 8005197, 19 | 8000096, 20 | 8010073, 21 | 8010093, 22 | 8011471, 23 | 8013479, 24 | 8013483, 25 | 8010215, 26 | 8013487, 27 | 8012666, 28 | 8010327, 29 | 597502, 30 | 8011044, 31 | 8010016, 32 | 8010139, 33 | 8010153, 34 | 8012963, 35 | 8012585, 36 | 8010241, 37 | 8012617, 38 | 8010304, 39 | 8010033, 40 | 8010324, 41 | 8010338, 42 | 8010381, 43 | 8010392, 44 | 8096022, 45 | 8001235, 46 | 8000310, 47 | 8003992, 48 | 8004004, 49 | 8000323, 50 | 8005247, 51 | 182006, 52 | 968306, 53 | ] 54 | 55 | 56 | for destination in destinations: 57 | journey = client.journeys( # type: ignore 58 | origin=origin, 59 | destination=destination, 60 | date=datetime.datetime.now(), 61 | max_journeys=1, 62 | products={ 63 | "long_distance_express": False, 64 | "long_distance": False, 65 | "ferry": False, 66 | "bus": False, 67 | "suburban": False, 68 | "subway": False, 69 | }, 70 | ) 71 | print(journey) 72 | -------------------------------------------------------------------------------- /example_rejseplanen.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from pyhafas import HafasClient 4 | from pyhafas.profile import RKRPProfile 5 | 6 | client = HafasClient(RKRPProfile(), debug=True) 7 | 8 | # Human input strings 9 | id_nordhavn = 'Nordhavn St.' 10 | id_kongebakken9 = 'Kongebakken 9, 2765 Smørum' 11 | 12 | # Resolve human string into Location-ID 13 | lids_kongebakken9 = client.locations(id_kongebakken9, rtype='ALL') 14 | lid_kongebakken9 = lids_kongebakken9[0].lid # First is good-enough 15 | assert('Kongebakken 9, 2765 Smørum, Egedal Kommune' in lid_kongebakken9) 16 | 17 | lids_nordhavn = client.locations(id_nordhavn, rtype='ALL') 18 | lid_nordhavn = lids_nordhavn[0].lid # First is good-enough 19 | assert('Nordhavn St' in lid_nordhavn) 20 | 21 | #print(client.departures( 22 | # station=lid_nordhavn, 23 | # date=datetime.datetime.now(), 24 | # max_trips=5 25 | #)) 26 | 27 | #print(client.arrivals( 28 | # station=lid_nordhavn, 29 | # date=datetime.datetime.now(), 30 | # max_trips=5 31 | #)) 32 | 33 | possibilities = client.journeys( 34 | origin = lid_nordhavn, 35 | destination = lid_kongebakken9, 36 | date = datetime.datetime.now(), 37 | min_change_time = 0, 38 | max_changes = -1 39 | ) 40 | 41 | 42 | for p in possibilities: 43 | print('--------') 44 | for l in p.legs: 45 | print(l.departure, l.origin.name, "--> " + str(l.name) + " -->" ) 46 | 47 | l = p.legs[-1] 48 | print(l.arrival, l.destination.name) 49 | print("Journey duration: ", p.duration) 50 | -------------------------------------------------------------------------------- /generateSalt.py: -------------------------------------------------------------------------------- 1 | from Crypto.Cipher import AES 2 | import base64 3 | 4 | aid = "rGhXPq+xAlvJd8T8cMnojdD0IoaOY53X7DPAbcXYe5g=" # from res/raw/haf_config.properties; HCI_CHECKSUM 5 | key = bytes([97, 72, 54, 70, 56, 122, 82, 117, 105, 66, 110, 109, 51, 51, 102, 85]) # from de/hafas/g/a/b.java of DBNavigator; probably static 6 | 7 | unpad = lambda s : s[:-ord(s[len(s)-1:])] # http://stackoverflow.com/a/12525165/3890934 8 | enc = base64.b64decode(aid) 9 | iv = bytes([00]*16) 10 | cipher = AES.new(key, AES.MODE_CBC, iv) 11 | salt = unpad(cipher.decrypt(enc).decode("utf-8")) 12 | 13 | print("Salt:", salt) 14 | -------------------------------------------------------------------------------- /pyhafas/__init__.py: -------------------------------------------------------------------------------- 1 | from .client import HafasClient 2 | from .types.exceptions import (AccessDeniedError, AuthenticationError, 3 | GeneralHafasError, 4 | JourneysArrivalDepartureTooNearError, 5 | JourneysTooManyTrainsError, 6 | LocationNotFoundError, TripDataNotFoundError) 7 | -------------------------------------------------------------------------------- /pyhafas/profile/__init__.py: -------------------------------------------------------------------------------- 1 | from .interfaces import ProfileInterface # isort:skip 2 | from .base import BaseProfile 3 | from .db import DBProfile 4 | from .vsn import VSNProfile 5 | from .rkrp import RKRPProfile 6 | from .nasa import NASAProfile 7 | from .kvb import KVBProfile 8 | from .nvv import NVVProfile 9 | from .vvv import VVVProfile 10 | -------------------------------------------------------------------------------- /pyhafas/profile/base/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List 2 | 3 | from pyhafas.profile.base.helper.date_time import BaseDateTimeHelper 4 | from pyhafas.profile.base.helper.format_products_filter import \ 5 | BaseFormatProductsFilterHelper 6 | from pyhafas.profile.base.helper.parse_leg import BaseParseLegHelper 7 | from pyhafas.profile.base.helper.parse_lid import BaseParseLidHelper 8 | from pyhafas.profile.base.helper.parse_remark import BaseParseRemarkHelper 9 | from pyhafas.profile.base.helper.request import BaseRequestHelper 10 | from pyhafas.profile.base.requests.journey import BaseJourneyRequest 11 | from pyhafas.profile.base.requests.journeys import BaseJourneysRequest 12 | from pyhafas.profile.base.requests.location import BaseLocationRequest 13 | from pyhafas.profile.base.requests.nearby import BaseNearbyRequest 14 | from pyhafas.profile.base.requests.station_board import BaseStationBoardRequest 15 | from pyhafas.profile.base.requests.trip import BaseTripRequest 16 | from pyhafas.profile.interfaces import ProfileInterface 17 | 18 | 19 | class BaseProfile( 20 | BaseRequestHelper, 21 | BaseFormatProductsFilterHelper, 22 | BaseParseLidHelper, 23 | BaseDateTimeHelper, 24 | BaseParseLegHelper, 25 | BaseParseRemarkHelper, 26 | BaseLocationRequest, 27 | BaseJourneyRequest, 28 | BaseJourneysRequest, 29 | BaseStationBoardRequest, 30 | BaseTripRequest, 31 | BaseNearbyRequest, 32 | ProfileInterface): 33 | """ 34 | Profile for a "normal" HaFAS. Only for other profiles usage as basis. 35 | """ 36 | baseUrl: str = "" 37 | defaultUserAgent: str = 'pyhafas' 38 | 39 | addMicMac: bool = False 40 | addChecksum: bool = False 41 | salt: str = "" 42 | 43 | requestBody: dict = {} 44 | 45 | availableProducts: Dict[str, List[int]] = {} 46 | defaultProducts: List[str] = [] 47 | 48 | def __init__(self, ua=None): 49 | if ua: 50 | self.userAgent = ua 51 | else: 52 | self.userAgent = self.defaultUserAgent 53 | -------------------------------------------------------------------------------- /pyhafas/profile/base/helper/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FahrplanDatenGarten/pyhafas/4e91ffd80fe214ea6c0db2954160eb2813a2e215/pyhafas/profile/base/helper/__init__.py -------------------------------------------------------------------------------- /pyhafas/profile/base/helper/date_time.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from pyhafas.profile import ProfileInterface 4 | from pyhafas.profile.interfaces.helper.date_time import DateTimeHelperInterface 5 | 6 | 7 | class BaseDateTimeHelper(DateTimeHelperInterface): 8 | def parse_datetime( 9 | self: ProfileInterface, 10 | time_string: str, 11 | date: datetime.date) -> datetime.datetime: 12 | """ 13 | Parses the time format HaFAS returns and combines it with a date 14 | 15 | :param time_string: Time string sent by HaFAS (multiple formats are supported. One example: 234000) 16 | :param date: Start day of the leg/journey 17 | :return: Parsed date and time as datetime object 18 | """ 19 | try: 20 | hour = int(time_string[-6:-4]) 21 | minute = int(time_string[-4:-2]) 22 | second = int(time_string[-2:]) 23 | except ValueError: 24 | raise ValueError( 25 | 'Time string "{}" has wrong format'.format(time_string)) 26 | 27 | dateOffset = int(time_string[:2]) if len(time_string) > 6 else 0 28 | return self.timezone.localize(datetime.datetime( 29 | date.year, 30 | date.month, 31 | date.day, 32 | hour, 33 | minute, 34 | second) + datetime.timedelta(days=dateOffset)) 35 | 36 | def parse_timedelta( 37 | self: ProfileInterface, 38 | time_string: str) -> datetime.timedelta: 39 | """ 40 | Parses the time HaFAS returns as timedelta object 41 | 42 | Example use case is when HaFAS returns a duration of a leg 43 | :param time_string: Time string sent by HaFAS (example for format is: 033200) 44 | :return: Parsed time as timedelta object 45 | """ 46 | try: 47 | hours = int(time_string[:2]) 48 | minutes = int(time_string[2:-2]) 49 | seconds = int(time_string[-2:]) 50 | except ValueError: 51 | raise ValueError( 52 | 'Time string "{}" has wrong format'.format(time_string)) 53 | 54 | return datetime.timedelta( 55 | hours=hours, 56 | minutes=minutes, 57 | seconds=seconds) 58 | 59 | def parse_date(self: ProfileInterface, date_string: str) -> datetime.date: 60 | """ 61 | Parses the date HaFAS returns 62 | 63 | :param date_string: Date sent by HaFAS 64 | :return: Parsed date object 65 | """ 66 | dt = datetime.datetime.strptime(date_string, '%Y%m%d') 67 | return dt.date() 68 | 69 | def transform_datetime_parameter_timezone( 70 | self: ProfileInterface, 71 | date_time: datetime.datetime) -> datetime.datetime: 72 | """ 73 | Transfers datetime parameters incoming by the user to the profile timezone 74 | 75 | :param date_time: datetime parameter incoming by user. Can be timezone aware or unaware 76 | :return: Timezone aware datetime object in profile timezone 77 | """ 78 | if date_time.tzinfo is not None and date_time.tzinfo.utcoffset( 79 | date_time) is not None: 80 | return date_time.astimezone(self.timezone) 81 | else: 82 | return self.timezone.localize(date_time) 83 | -------------------------------------------------------------------------------- /pyhafas/profile/base/helper/format_products_filter.py: -------------------------------------------------------------------------------- 1 | from pyhafas.profile import ProfileInterface 2 | from pyhafas.profile.interfaces.helper.format_products_filter import \ 3 | FormatProductsFilterHelperInterface 4 | from pyhafas.types.exceptions import ProductNotAvailableError 5 | 6 | 7 | class BaseFormatProductsFilterHelper(FormatProductsFilterHelperInterface): 8 | """Helper for creating the products filter """ 9 | 10 | def format_products_filter( 11 | self: ProfileInterface, 12 | requested_products: dict) -> dict: 13 | """ 14 | Create the products filter given to HaFAS 15 | 16 | :param requested_products: Mapping of Products to whether it's enabled or disabled 17 | :return: value for HaFAS "jnyFltrL" attribute 18 | """ 19 | products = self.defaultProducts 20 | for requested_product in requested_products: 21 | if requested_products[requested_product]: 22 | try: 23 | products.index(requested_product) 24 | except ValueError: 25 | products.append(requested_product) 26 | 27 | elif not requested_products[requested_product]: 28 | try: 29 | products.pop(products.index(requested_product)) 30 | except ValueError: 31 | pass 32 | bitmask_sum = 0 33 | for product in products: 34 | try: 35 | for product_bitmask in self.availableProducts[product]: 36 | bitmask_sum += product_bitmask 37 | except KeyError: 38 | raise ProductNotAvailableError( 39 | 'The product "{}" is not available in chosen profile.'.format(product)) 40 | return { 41 | 'type': 'PROD', 42 | 'mode': 'INC', 43 | 'value': str(bitmask_sum) 44 | } 45 | -------------------------------------------------------------------------------- /pyhafas/profile/base/helper/parse_leg.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import List 3 | 4 | from pyhafas.profile import ProfileInterface 5 | from pyhafas.profile.interfaces.helper.parse_leg import ParseLegHelperInterface 6 | from pyhafas.types.fptf import Leg, Mode, Stopover 7 | 8 | 9 | class BaseParseLegHelper(ParseLegHelperInterface): 10 | def parse_leg( 11 | self: ProfileInterface, 12 | journey: dict, 13 | common: dict, 14 | departure: dict, 15 | arrival: dict, 16 | date: datetime.date, 17 | jny_type: str = "JNY", 18 | gis=None) -> Leg: 19 | """ 20 | Parses Leg HaFAS returns into Leg object 21 | 22 | Different Types of HaFAS responses can be parsed into a leg object with the multiple variables 23 | 24 | :param journey: Journey object given back by HaFAS (Data of the Leg to parse) 25 | :param common: Common object given back by HaFAS 26 | :param departure: Departure object given back by HaFAS 27 | :param arrival: Arrival object given back by HaFAS 28 | :param date: Parsed date of Journey (Departing date) 29 | :param jny_type: HaFAS Journey type 30 | :param gis: GIS object given back by HaFAS. Currently only used by "WALK" journey type. 31 | :return: Parsed Leg object 32 | """ 33 | leg_origin = self.parse_lid_to_station( 34 | common['locL'][departure['locX']]['lid']) 35 | leg_destination = self.parse_lid_to_station( 36 | common['locL'][arrival['locX']]['lid']) 37 | if jny_type == "WALK" or jny_type == "TRSF": 38 | return Leg( 39 | id=gis['ctx'], 40 | origin=leg_origin, 41 | destination=leg_destination, 42 | departure=self.parse_datetime(departure['dTimeS'], date), 43 | arrival=self.parse_datetime(arrival['aTimeS'], date), 44 | mode=Mode.WALKING, 45 | name=None, 46 | distance=gis.get('dist') if gis is not None else None 47 | ) 48 | else: 49 | leg_stopovers: List[Stopover] = [] 50 | if 'stopL' in journey: 51 | for stopover in journey['stopL']: 52 | leg_stopovers.append( 53 | Stopover( 54 | stop=self.parse_lid_to_station( 55 | common['locL'][stopover['locX']]['lid'] 56 | ), 57 | cancelled=bool( 58 | stopover.get( 59 | 'dCncl', 60 | stopover.get( 61 | 'aCncl', 62 | False 63 | ))), 64 | departure=self.parse_datetime( 65 | stopover.get('dTimeS'), 66 | date) if stopover.get('dTimeS') is not None else None, 67 | departure_delay=self.parse_datetime( 68 | stopover['dTimeR'], 69 | date) - self.parse_datetime( 70 | stopover['dTimeS'], 71 | date) if stopover.get('dTimeR') is not None else None, 72 | departure_platform=stopover.get( 73 | 'dPlatfR', 74 | stopover.get('dPlatfS', stopover.get('dPltfR', stopover.get('dPltfS', {})).get('txt'))), 75 | arrival=self.parse_datetime( 76 | stopover['aTimeS'], 77 | date) if stopover.get('aTimeS') is not None else None, 78 | arrival_delay=self.parse_datetime( 79 | stopover['aTimeR'], 80 | date) - self.parse_datetime( 81 | stopover['aTimeS'], 82 | date) if stopover.get('aTimeR') is not None else None, 83 | arrival_platform=stopover.get( 84 | 'aPlatfR', 85 | stopover.get('aPlatfS', stopover.get('aPltfR', stopover.get('aPltfS', {})).get('txt'))), 86 | remarks=[self.parse_remark(common['remL'][msg['remX']], common) 87 | for msg in stopover.get('msgL', []) if msg.get('remX') is not None] 88 | )) 89 | 90 | return Leg( 91 | id=journey['jid'], 92 | name=common['prodL'][journey['prodX']]['name'], 93 | origin=leg_origin, 94 | destination=leg_destination, 95 | cancelled=bool(arrival.get('aCncl', False)), 96 | departure=self.parse_datetime( 97 | departure['dTimeS'], 98 | date), 99 | departure_delay=self.parse_datetime( 100 | departure['dTimeR'], 101 | date) - self.parse_datetime( 102 | departure['dTimeS'], 103 | date) if departure.get('dTimeR') is not None else None, 104 | departure_platform=departure.get( 105 | 'dPlatfR', 106 | departure.get('dPlatfS', departure.get('dPltfR', departure.get('dPltfS', {})).get('txt'))), 107 | arrival=self.parse_datetime( 108 | arrival['aTimeS'], 109 | date), 110 | arrival_delay=self.parse_datetime( 111 | arrival['aTimeR'], 112 | date) - self.parse_datetime( 113 | arrival['aTimeS'], 114 | date) if arrival.get('aTimeR') is not None else None, 115 | arrival_platform=arrival.get( 116 | 'aPlatfR', 117 | arrival.get('aPlatfS', arrival.get('aPltfR', arrival.get('aPltfS', {})).get('txt'))), 118 | stopovers=leg_stopovers, 119 | remarks=[self.parse_remark(common['remL'][msg['remX']], common) 120 | for msg in journey.get('msgL', {}) if msg.get('remX') is not None]) 121 | 122 | def parse_legs( 123 | self: ProfileInterface, 124 | jny: dict, 125 | common: dict, 126 | date: datetime.date) -> List[Leg]: 127 | """ 128 | Parses Legs (when multiple available) 129 | 130 | :param jny: Journeys object returned by HaFAS (contains secL list) 131 | :param common: Common object returned by HaFAS 132 | :param date: Parsed date of Journey (Departing date) 133 | :return: Parsed List of Leg objects 134 | """ 135 | legs: List[Leg] = [] 136 | 137 | for leg in jny['secL']: 138 | legs.append( 139 | self.parse_leg( 140 | leg.get( 141 | 'jny', 142 | None), 143 | common, 144 | leg['dep'], 145 | leg['arr'], 146 | date, 147 | leg['type'], 148 | leg.get('gis'))) 149 | 150 | return legs 151 | -------------------------------------------------------------------------------- /pyhafas/profile/base/helper/parse_lid.py: -------------------------------------------------------------------------------- 1 | from pyhafas.profile import ProfileInterface 2 | from pyhafas.profile.interfaces.helper.parse_lid import ParseLidHelperInterface 3 | from pyhafas.types.fptf import Station 4 | 5 | 6 | class BaseParseLidHelper(ParseLidHelperInterface): 7 | def parse_lid(self: ProfileInterface, lid: str) -> dict: 8 | """ 9 | Converts the LID given by HaFAS 10 | 11 | Splits the LID (e.g. A=1@O=Siegburg/Bonn) in multiple elements (e.g. A=1 and O=Siegburg/Bonn). 12 | These are converted into a dict where the part before the equal sign is the key and the part after the value. 13 | 14 | :param lid: Location identifier (given by HaFAS) 15 | :return: Dict of the elements of the dict 16 | """ 17 | parsedLid = {} 18 | for lidElementGroup in lid.split("@"): 19 | if lidElementGroup: 20 | parsedLid[lidElementGroup.split( 21 | "=")[0]] = lidElementGroup.split("=")[1] 22 | return parsedLid 23 | 24 | def parse_lid_to_station( 25 | self: ProfileInterface, 26 | lid: str, 27 | name: str = "", 28 | latitude: float = 0, 29 | longitude: float = 0) -> Station: 30 | """ 31 | Parses the LID given by HaFAS to a station object 32 | 33 | :param lid: Location identifier (given by HaFAS) 34 | :param name: Station name (optional, if not given, LID is used) 35 | :param latitude: Latitude of the station (optional, if not given, LID is used) 36 | :param longitude: Longitude of the station (optional, if not given, LID is used) 37 | :return: Parsed LID as station object 38 | """ 39 | parsedLid = self.parse_lid(lid) 40 | if latitude == 0 and longitude == 0 and parsedLid['X'] and parsedLid['Y']: 41 | latitude = float(float(parsedLid['Y']) / 1000000) 42 | longitude = float(float(parsedLid['X']) / 1000000) 43 | 44 | return Station( 45 | id=parsedLid.get('L') or parsedLid['b'], # key 'L' not always present; if not, 'b' should be 46 | lid=lid, 47 | name=name or parsedLid['O'], 48 | latitude=latitude, 49 | longitude=longitude 50 | ) 51 | -------------------------------------------------------------------------------- /pyhafas/profile/base/helper/parse_remark.py: -------------------------------------------------------------------------------- 1 | from pyhafas.profile import ProfileInterface 2 | from pyhafas.profile.interfaces.helper.parse_remark import ParseRemarkHelperInterface 3 | from pyhafas.types.fptf import Remark 4 | 5 | 6 | class BaseParseRemarkHelper(ParseRemarkHelperInterface): 7 | def parse_remark(self: ProfileInterface, remark: dict, common: dict) -> Remark: 8 | """ 9 | Parses Remark HaFAS returns into Remark object 10 | 11 | :param remark: Remark object given back by HaFAS 12 | :param common: Common object given back by HaFAS 13 | :return: Parsed Remark object 14 | """ 15 | 16 | rem = Remark( 17 | remark_type=remark.get('type'), 18 | code=remark.get('code') if remark.get('code') != "" else None, 19 | subject=remark.get('txtS') if remark.get('txtS') != "" else None, 20 | text=remark.get('txtN') if remark.get('txtN') != "" else None, 21 | priority=remark.get('prio'), 22 | trip_id=remark.get('jid'), 23 | ) 24 | # print(rem, remark) 25 | return rem 26 | -------------------------------------------------------------------------------- /pyhafas/profile/base/helper/request.py: -------------------------------------------------------------------------------- 1 | import json 2 | from hashlib import md5 3 | from typing import Tuple 4 | 5 | import requests 6 | from requests.adapters import HTTPAdapter 7 | from urllib3.util.retry import Retry 8 | 9 | from pyhafas.profile import ProfileInterface 10 | from pyhafas.profile.base.mappings.error_codes import BaseErrorCodesMapping 11 | from pyhafas.profile.interfaces.helper.request import RequestHelperInterface 12 | from pyhafas.types.hafas_response import HafasResponse 13 | 14 | 15 | class BaseRequestHelper(RequestHelperInterface): 16 | request_session = requests.session() 17 | 18 | def calculate_checksum(self: ProfileInterface, data: str) -> str: 19 | """ 20 | Calculates the checksum of the request (required for most profiles) 21 | 22 | :param data: Complete body as string 23 | :return: Checksum for the request 24 | """ 25 | return md5((data + self.salt).encode('utf-8')).hexdigest() 26 | 27 | def calculate_mic_mac( 28 | self: ProfileInterface, 29 | data: str) -> Tuple[str, str]: 30 | """ 31 | Calculates the mic-mac for the request (required for some profiles) 32 | 33 | :param data: Complete body as string 34 | :return: Mic and mac to be sent to HaFAS 35 | """ 36 | mic = md5(data.encode('utf-8')).hexdigest() 37 | mac = self.calculate_checksum(mic) 38 | return mic, mac 39 | 40 | def url_formatter(self: ProfileInterface, data: str) -> str: 41 | """ 42 | Formats the URL for HaFAS (adds the checksum or mic-mac) 43 | 44 | :param data: Complete body as string 45 | :return: Request-URL (maybe with checksum or mic-mac) 46 | """ 47 | url = self.baseUrl 48 | 49 | if self.addChecksum or self.addMicMac: 50 | parameters = [] 51 | if self.addChecksum: 52 | parameters.append( 53 | 'checksum={}'.format( 54 | self.calculate_checksum(data))) 55 | if self.addMicMac: 56 | parameters.append( 57 | 'mic={}&mac={}'.format( 58 | *self.calculate_mic_mac(data))) 59 | url += '?{}'.format('&'.join(parameters)) 60 | 61 | return url 62 | 63 | def activate_retry(self: ProfileInterface, retries: int = 4, backoff_factor: float = 1) -> None: 64 | self.request_session = requests.Session() 65 | 66 | retry = Retry( 67 | total=retries, 68 | read=retries, 69 | connect=retries, 70 | backoff_factor=backoff_factor, 71 | ) 72 | 73 | adapter = HTTPAdapter(max_retries=retry) 74 | self.request_session.mount("http://", adapter) 75 | self.request_session.mount("https://", adapter) 76 | 77 | def request(self: ProfileInterface, body) -> HafasResponse: 78 | """ 79 | Sends the request and does a basic parsing of the response and error handling 80 | 81 | :param body: Reqeust body as dict (without the `requestBody` of the profile) 82 | :return: HafasRespone object or Exception when HaFAS response returns an error 83 | """ 84 | data = { 85 | 'svcReqL': [body] 86 | } 87 | data.update(self.requestBody) 88 | data = json.dumps(data) 89 | 90 | res = self.request_session.post( 91 | self.url_formatter(data), 92 | data=data, 93 | headers={ 94 | 'User-Agent': self.userAgent, 95 | 'Content-Type': 'application/json'}) 96 | return HafasResponse(res, BaseErrorCodesMapping) 97 | -------------------------------------------------------------------------------- /pyhafas/profile/base/mappings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FahrplanDatenGarten/pyhafas/4e91ffd80fe214ea6c0db2954160eb2813a2e215/pyhafas/profile/base/mappings/__init__.py -------------------------------------------------------------------------------- /pyhafas/profile/base/mappings/error_codes.py: -------------------------------------------------------------------------------- 1 | from pyhafas.profile.interfaces.mappings.error_codes import \ 2 | ErrorCodesMappingInterface 3 | from pyhafas.types.exceptions import (AccessDeniedError, AuthenticationError, 4 | GeneralHafasError, 5 | JourneysArrivalDepartureTooNearError, 6 | JourneysTooManyTrainsError, 7 | LocationNotFoundError, 8 | TripDataNotFoundError) 9 | 10 | 11 | class BaseErrorCodesMapping(ErrorCodesMappingInterface): 12 | """ 13 | Mapping of the HaFAS error code to the exception class 14 | 15 | `default` defines the error when the error code cannot be found in the mapping 16 | """ 17 | default = GeneralHafasError 18 | AUTH = AuthenticationError 19 | R5000 = AccessDeniedError 20 | LOCATION = LocationNotFoundError 21 | H500 = JourneysTooManyTrainsError 22 | H890 = JourneysArrivalDepartureTooNearError 23 | SQ005 = TripDataNotFoundError 24 | TI001 = TripDataNotFoundError 25 | -------------------------------------------------------------------------------- /pyhafas/profile/base/requests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FahrplanDatenGarten/pyhafas/4e91ffd80fe214ea6c0db2954160eb2813a2e215/pyhafas/profile/base/requests/__init__.py -------------------------------------------------------------------------------- /pyhafas/profile/base/requests/journey.py: -------------------------------------------------------------------------------- 1 | from pyhafas.profile import ProfileInterface 2 | from pyhafas.profile.interfaces.requests.journey import JourneyRequestInterface 3 | from pyhafas.types.fptf import Journey 4 | from pyhafas.types.hafas_response import HafasResponse 5 | 6 | 7 | class BaseJourneyRequest(JourneyRequestInterface): 8 | def format_journey_request( 9 | self: ProfileInterface, 10 | journey: Journey) -> dict: 11 | """ 12 | Creates the HaFAS request body for a journey request 13 | 14 | :param journey: Id of the journey (ctxRecon) 15 | :return: Request body for HaFAS 16 | """ 17 | return { 18 | 'req': { 19 | 'ctxRecon': journey.id 20 | }, 21 | 'meth': 'Reconstruction' 22 | } 23 | 24 | def parse_journey_request( 25 | self: ProfileInterface, 26 | data: HafasResponse) -> Journey: 27 | """ 28 | Parses the HaFAS response for a journey request 29 | 30 | :param data: Formatted HaFAS response 31 | :return: List of Journey objects 32 | """ 33 | date = self.parse_date(data.res['outConL'][0]['date']) 34 | return Journey( 35 | data.res['outConL'][0]['ctxRecon'], 36 | date=date, 37 | duration=self.parse_timedelta( 38 | data.res['outConL'][0]['dur']), 39 | legs=self.parse_legs( 40 | data.res['outConL'][0], 41 | data.common, 42 | date)) 43 | -------------------------------------------------------------------------------- /pyhafas/profile/base/requests/journeys.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import Dict, List 3 | 4 | from pyhafas.profile import ProfileInterface 5 | from pyhafas.profile.interfaces.requests.journeys import \ 6 | JourneysRequestInterface 7 | from pyhafas.types.fptf import Journey, Station, Leg 8 | from pyhafas.types.hafas_response import HafasResponse 9 | 10 | 11 | class BaseJourneysRequest(JourneysRequestInterface): 12 | def format_journeys_request( 13 | self: ProfileInterface, 14 | origin: Station, 15 | destination: Station, 16 | via: List[Station], 17 | date: datetime.datetime, 18 | min_change_time: int, 19 | max_changes: int, 20 | products: Dict[str, bool], 21 | max_journeys: int 22 | ) -> dict: 23 | """ 24 | Creates the HaFAS request body for a journeys request 25 | 26 | :param origin: Origin station 27 | :param destination: Destionation station 28 | :param via: Via stations, maybe empty list) 29 | :param date: Date and time to search journeys for 30 | :param min_change_time: Minimum transfer/change time at each station 31 | :param max_changes: Maximum number of changes 32 | :param products: Allowed products (a product is a mean of transport like ICE,IC) 33 | :param max_journeys: Maximum number of returned journeys 34 | :return: Request body for HaFAS 35 | """ 36 | # TODO: find out, what commented-out values mean and implement options 37 | return { 38 | 'req': { 39 | 'arrLocL': [{ 40 | 'lid': 'A=1@L={}@'.format(destination.id) 41 | }], 42 | 'viaLocL': [{ 43 | 'loc': { 44 | 'lid': 'A=1@L={}@'.format(via_station.id), 45 | } 46 | } for via_station in via], 47 | 'depLocL': [{ 48 | 'lid': 'A=1@L={}@'.format(origin.id) 49 | }], 50 | 'outDate': date.strftime("%Y%m%d"), 51 | 'outTime': date.strftime("%H%M%S"), 52 | 'jnyFltrL': [ 53 | self.format_products_filter(products) 54 | ], 55 | 'minChgTime': min_change_time, 56 | 'maxChg': max_changes, 57 | 'numF': max_journeys, 58 | # 'getPasslist': False, 59 | # 'gisFltrL': [], 60 | # 'getTariff': False, 61 | # 'ushrp': True, 62 | # 'getPT': True, 63 | # 'getIV': False, 64 | # 'getPolyline': False, 65 | # 'outFrwd': True, 66 | # 'trfReq': { 67 | # 'jnyCl': 2, 68 | # 'cType': 'PK', 69 | # 'tvlrProf': [{ 70 | # 'type': 'E', 71 | # 'redtnCard': 4 72 | # }] 73 | # } 74 | }, 75 | # 'cfg': { 76 | # 'polyEnc': 'GPA', 77 | # 'rtMode': 'HYBRID' 78 | # }, 79 | 'meth': 'TripSearch' 80 | } 81 | 82 | def format_search_from_leg_request( 83 | self: ProfileInterface, 84 | origin: Leg, 85 | destination: Station, 86 | via: List[Station], 87 | min_change_time: int, 88 | max_changes: int, 89 | products: Dict[str, bool], 90 | ) -> dict: 91 | """ 92 | Creates the HaFAS request body for a journeys request 93 | 94 | :param origin: Origin leg 95 | :param destination: Destionation station 96 | :param via: Via stations, maybe empty list) 97 | :param min_change_time: Minimum transfer/change time at each station 98 | :param max_changes: Maximum number of changes 99 | :param products: Allowed products (a product is a mean of transport like ICE,IC) 100 | :return: Request body for HaFAS 101 | """ 102 | return { 103 | 'req': { 104 | 'arrLocL': [{ 105 | 'lid': 'A=1@L={}@'.format(destination.id) 106 | }], 107 | 'viaLocL': [{ 108 | 'loc': { 109 | 'lid': 'A=1@L={}@'.format(via_station.id) 110 | } 111 | } for via_station in via], 112 | 'locData': { 113 | 'loc': { 114 | 'lid': 'A=1@L={}@'.format(origin.origin.id) 115 | }, 116 | 'type': 'DEP', 117 | 'date': origin.departure.strftime("%Y%m%d"), 118 | 'time': origin.departure.strftime("%H%M%S") 119 | }, 120 | 'jnyFltrL': [ 121 | self.format_products_filter(products) 122 | ], 123 | 'minChgTime': min_change_time, 124 | 'maxChg': max_changes, 125 | 'jid': origin.id, 126 | 'sotMode': 'JI' 127 | }, 128 | 'meth': 'SearchOnTrip' 129 | } 130 | 131 | def parse_journeys_request( 132 | self: ProfileInterface, 133 | data: HafasResponse) -> List[Journey]: 134 | """ 135 | Parses the HaFAS response for a journeys request 136 | 137 | :param data: Formatted HaFAS response 138 | :return: List of Journey objects 139 | """ 140 | journeys = [] 141 | 142 | for jny in data.res['outConL']: 143 | # TODO: Add more data 144 | date = self.parse_date(jny['date']) 145 | journeys.append( 146 | Journey( 147 | jny['ctxRecon'], date=date, duration=self.parse_timedelta( 148 | jny['dur']), legs=self.parse_legs( 149 | jny, data.common, date))) 150 | return journeys 151 | -------------------------------------------------------------------------------- /pyhafas/profile/base/requests/location.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from pyhafas.profile import ProfileInterface 4 | from pyhafas.profile.interfaces import LocationRequestInterface 5 | from pyhafas.types.fptf import Station 6 | from pyhafas.types.hafas_response import HafasResponse 7 | 8 | 9 | class BaseLocationRequest(LocationRequestInterface): 10 | def format_location_request(self: ProfileInterface, term: str, rtype: str = 'S'): 11 | """ 12 | Creates the HaFAS request body for a location search request. 13 | 14 | :param term: Search term 15 | :param type: Result types. One of ['S' for stations, 'ALL' for addresses and stations] 16 | :return: Request body for HaFAS 17 | """ 18 | return { 19 | "req": { 20 | "input": { 21 | "field": "S", 22 | "loc": { 23 | "name": term, 24 | "type": rtype 25 | } 26 | } 27 | }, 28 | "meth": "LocMatch" 29 | } 30 | 31 | def parse_location_request( 32 | self: ProfileInterface, 33 | data: HafasResponse) -> List[Station]: 34 | """ 35 | Parses the HaFAS response for a location request 36 | 37 | :param data: Formatted HaFAS response 38 | :return: List of Station objects 39 | """ 40 | stations = [] 41 | for stn in data.res['match']['locL']: 42 | try: 43 | latitude: float = stn['crd']['y'] / 1000000 44 | longitude: float = stn['crd']['x'] / 1000000 45 | except KeyError: 46 | latitude: float = 0 47 | longitude: float = 0 48 | stations.append( 49 | self.parse_lid_to_station( 50 | stn['lid'], 51 | stn['name'], 52 | latitude, 53 | longitude)) 54 | return stations 55 | -------------------------------------------------------------------------------- /pyhafas/profile/base/requests/nearby.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from pyhafas.profile import ProfileInterface 4 | from pyhafas.profile.interfaces.requests.nearby import NearbyRequestInterface 5 | from pyhafas.types.fptf import Station 6 | from pyhafas.types.hafas_response import HafasResponse 7 | from pyhafas.types.nearby import LatLng 8 | 9 | 10 | class BaseNearbyRequest(NearbyRequestInterface): 11 | def format_nearby_request( 12 | self: ProfileInterface, 13 | location: LatLng, 14 | max_walking_distance: int, 15 | min_walking_distance: int, 16 | products: dict[str, bool], 17 | get_pois: bool, 18 | get_stops: bool, 19 | max_locations: int, 20 | ) -> dict: 21 | """ 22 | Creates the HaFAS request body for a nearby request. 23 | 24 | :param location: LatLng object containing latitude and longitude 25 | :param max_walking_distance: Maximum walking distance in meters 26 | :param min_walking_distance: Minimum walking distance in meters 27 | :param products: Dictionary of product names to products 28 | :param get_pois: If true, returns pois 29 | :param get_stops: If true, returns stops instead of locations 30 | :param max_locations: Maximum number of locations to return 31 | :return: Request body for HaFAS 32 | """ 33 | return { 34 | "cfg": { 35 | "polyEnc": "GPA" 36 | }, 37 | "meth": "LocGeoPos", 38 | "req": { 39 | "ring": { 40 | "cCrd": { 41 | "x": location.longitude_e6, 42 | "y": location.latitude_e6, 43 | }, 44 | "maxDist": max_walking_distance, 45 | "minDist": min_walking_distance, 46 | }, 47 | "locFltrL": [ 48 | self.format_products_filter(products) 49 | ], 50 | "getPOIs": get_pois, 51 | "getStops": get_stops, 52 | "maxLoc": max_locations 53 | 54 | } 55 | } 56 | 57 | def parse_nearby_response(self: ProfileInterface, data: HafasResponse) -> List[Station]: 58 | stations = [] 59 | 60 | for station in data.res["locL"]: 61 | try: 62 | latitude: float = station['crd']['y'] / 1E6 63 | longitude: float = station['crd']['x'] / 1E6 64 | except KeyError: 65 | latitude: float = 0 66 | longitude: float = 0 67 | stations.append( 68 | self.parse_lid_to_station( 69 | station['lid'], 70 | station['name'], 71 | latitude, 72 | longitude)) 73 | 74 | return stations 75 | -------------------------------------------------------------------------------- /pyhafas/profile/base/requests/station_board.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import Dict, List, Optional 3 | 4 | from pyhafas.profile import ProfileInterface 5 | from pyhafas.profile.interfaces.requests.station_board import \ 6 | StationBoardRequestInterface 7 | from pyhafas.types.fptf import Station, StationBoardLeg 8 | from pyhafas.types.hafas_response import HafasResponse 9 | from pyhafas.types.station_board_request import StationBoardRequestType 10 | 11 | 12 | class BaseStationBoardRequest(StationBoardRequestInterface): 13 | def format_station_board_request( 14 | self: ProfileInterface, 15 | station: Station, 16 | request_type: StationBoardRequestType, 17 | date: datetime.datetime, 18 | max_trips: int, 19 | duration: int, 20 | products: Dict[str, bool], 21 | direction: Optional[Station] 22 | ) -> dict: 23 | """ 24 | Creates the HaFAS request for a station board request (departure/arrival) 25 | 26 | :param station: Station to get departures/arrivals for 27 | :param request_type: ARRIVAL or DEPARTURE 28 | :param date: Date and time to get departures/arrival for 29 | :param max_trips: Maximum number of trips that can be returned 30 | :param products: Allowed products (e.g. ICE,IC) 31 | :param duration: Time in which trips are searched 32 | :param direction: Direction (end) station of the train. If none, filter will not be applied 33 | :return: Request body for HaFAS 34 | """ 35 | # TODO: More options 36 | return { 37 | 'req': { 38 | 'type': request_type.value, 39 | 'stbLoc': { 40 | 'lid': 'A=1@L={}@'.format(station.id) 41 | }, 42 | 'dirLoc': { 43 | 'lid': 'A=1@L={}@'.format(direction.id) 44 | } if direction is not None else None, 45 | 'maxJny': max_trips, 46 | 'date': date.strftime("%Y%m%d"), 47 | 'time': date.strftime("%H%M%S"), 48 | 'dur': duration, 49 | 'jnyFltrL': [ 50 | self.format_products_filter(products) 51 | ], 52 | }, 53 | 'meth': 'StationBoard' 54 | } 55 | 56 | def parse_station_board_request( 57 | self: ProfileInterface, 58 | data: HafasResponse, 59 | departure_arrival_prefix: str) -> List[StationBoardLeg]: 60 | """ 61 | Parses the HaFAS data for a station board request 62 | 63 | :param data: Formatted HaFAS response 64 | :param departure_arrival_prefix: Prefix for specifying whether its for arrival or departure (either a for arrival or d for departure) 65 | :return: List of StationBoardLeg objects 66 | """ 67 | legs = [] 68 | if not data.res.get('jnyL', False): 69 | return legs 70 | else: 71 | for raw_leg in data.res['jnyL']: 72 | date = self.parse_date(raw_leg['date']) 73 | 74 | try: 75 | platform = raw_leg['stbStop'][departure_arrival_prefix + 'PltfR']['txt'] if \ 76 | raw_leg['stbStop'].get(departure_arrival_prefix + 'PltfR') is not None else \ 77 | raw_leg['stbStop'][departure_arrival_prefix + 'PltfS']['txt'] 78 | except KeyError: 79 | platform = raw_leg['stbStop'].get( 80 | departure_arrival_prefix + 'PlatfR', 81 | raw_leg['stbStop'].get( 82 | departure_arrival_prefix + 'PlatfS', 83 | None)) 84 | 85 | legs.append(StationBoardLeg( 86 | id=raw_leg['jid'], 87 | name=data.common['prodL'][raw_leg['prodX']]['name'], 88 | direction=raw_leg.get('dirTxt'), 89 | date_time=self.parse_datetime( 90 | raw_leg['stbStop'][departure_arrival_prefix + 'TimeS'], 91 | date 92 | ), 93 | station=self.parse_lid_to_station(data.common['locL'][raw_leg['stbStop']['locX']]['lid']), 94 | platform=platform, 95 | delay=self.parse_datetime( 96 | raw_leg['stbStop'][departure_arrival_prefix + 'TimeR'], 97 | date) - self.parse_datetime( 98 | raw_leg['stbStop'][departure_arrival_prefix + 'TimeS'], 99 | date) if raw_leg['stbStop'].get(departure_arrival_prefix + 'TimeR') is not None else None, 100 | cancelled=bool(raw_leg['stbStop'].get(departure_arrival_prefix + 'Cncl', False)) 101 | )) 102 | return legs 103 | -------------------------------------------------------------------------------- /pyhafas/profile/base/requests/trip.py: -------------------------------------------------------------------------------- 1 | from pyhafas.profile import ProfileInterface 2 | from pyhafas.profile.interfaces.requests.trip import TripRequestInterface 3 | from pyhafas.types.fptf import Leg 4 | from pyhafas.types.hafas_response import HafasResponse 5 | 6 | 7 | class BaseTripRequest(TripRequestInterface): 8 | def format_trip_request(self: ProfileInterface, trip_id: str) -> dict: 9 | """ 10 | Creates the HaFAS request for a trip request 11 | 12 | :param trip_id: Id of the trip/leg 13 | :return: Request body for HaFAS 14 | """ 15 | return { 16 | 'req': { 17 | 'jid': trip_id 18 | }, 19 | 'meth': 'JourneyDetails' 20 | } 21 | 22 | def parse_trip_request(self: ProfileInterface, data: HafasResponse) -> Leg: 23 | """ 24 | Parses the HaFAS data for a trip request 25 | 26 | :param data: Formatted HaFAS response 27 | :return: Leg objects 28 | """ 29 | return self.parse_leg( 30 | data.res['journey'], 31 | data.common, 32 | data.res['journey']['stopL'][0], 33 | data.res['journey']['stopL'][-1], 34 | self.parse_date(data.res['journey']['date']) 35 | ) 36 | -------------------------------------------------------------------------------- /pyhafas/profile/db/__init__.py: -------------------------------------------------------------------------------- 1 | import pytz 2 | 3 | from pyhafas.profile.base import BaseProfile 4 | 5 | 6 | class DBProfile(BaseProfile): 7 | """ 8 | Profile of the HaFAS of Deutsche Bahn (DB) - German Railway - Regional and long-distance trains throughout Germany 9 | """ 10 | baseUrl = "https://reiseauskunft.bahn.de/bin/mgate.exe" 11 | defaultUserAgent = "DB Navigator/19.10.04 (iPhone; iOS 13.1.2; Scale/2.00)" 12 | 13 | salt = 'bdI8UVj40K5fvxwf' 14 | addChecksum = True 15 | 16 | locale = 'de-DE' 17 | timezone = pytz.timezone('Europe/Berlin') 18 | 19 | requestBody = { 20 | 'client': { 21 | 'id': 'DB', 22 | 'v': '20100000', 23 | 'type': 'IPH', 24 | 'name': 'DB Navigator' 25 | }, 26 | 'ext': 'DB.R21.12.a', 27 | 'ver': '1.15', 28 | 'auth': { 29 | 'type': 'AID', 30 | 'aid': 'n91dB8Z77MLdoR0K' 31 | } 32 | } 33 | 34 | availableProducts = { 35 | 'long_distance_express': [1], # ICE 36 | 'long_distance': [2], # IC/EC 37 | 'regional_express': [4], # RE/IR 38 | 'regional': [8], # RB 39 | 'suburban': [16], # S 40 | 'bus': [32], # BUS 41 | 'ferry': [64], # F 42 | 'subway': [128], # U 43 | 'tram': [256], # T 44 | 'taxi': [512] # Group Taxi 45 | } 46 | 47 | defaultProducts = [ 48 | 'long_distance_express', 49 | 'long_distance', 50 | 'regional_express', 51 | 'regional', 52 | 'suburban', 53 | 'bus', 54 | 'ferry', 55 | 'subway', 56 | 'tram', 57 | 'taxi' 58 | ] 59 | -------------------------------------------------------------------------------- /pyhafas/profile/interfaces/__init__.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from typing import Dict, List 3 | 4 | import pytz 5 | 6 | from pyhafas.profile.interfaces.helper.date_time import DateTimeHelperInterface 7 | from pyhafas.profile.interfaces.helper.format_products_filter import \ 8 | FormatProductsFilterHelperInterface 9 | from pyhafas.profile.interfaces.helper.parse_leg import ParseLegHelperInterface 10 | from pyhafas.profile.interfaces.helper.parse_lid import ParseLidHelperInterface 11 | from pyhafas.profile.interfaces.helper.parse_remark import ParseRemarkHelperInterface 12 | from pyhafas.profile.interfaces.helper.request import RequestHelperInterface 13 | from pyhafas.profile.interfaces.requests.journey import JourneyRequestInterface 14 | from pyhafas.profile.interfaces.requests.journeys import \ 15 | JourneysRequestInterface 16 | from pyhafas.profile.interfaces.requests.location import \ 17 | LocationRequestInterface 18 | from pyhafas.profile.interfaces.requests.nearby import NearbyRequestInterface 19 | from pyhafas.profile.interfaces.requests.station_board import \ 20 | StationBoardRequestInterface 21 | from pyhafas.profile.interfaces.requests.trip import TripRequestInterface 22 | 23 | 24 | class ProfileInterface( 25 | RequestHelperInterface, 26 | FormatProductsFilterHelperInterface, 27 | ParseLidHelperInterface, 28 | DateTimeHelperInterface, 29 | ParseLegHelperInterface, 30 | ParseRemarkHelperInterface, 31 | LocationRequestInterface, 32 | JourneyRequestInterface, 33 | JourneysRequestInterface, 34 | StationBoardRequestInterface, 35 | TripRequestInterface, 36 | NearbyRequestInterface, 37 | ABC): 38 | """ 39 | The profile interface is the abstract class of a profile. 40 | 41 | It inherits all interfaces, so it has all methods a normal profile has available as abstract methods. 42 | Therefore it can be used as type hint for `self` in methods which are inherited by profiles. 43 | """ 44 | baseUrl: str 45 | """Complete http(s) URL to mgate.exe of the HaFAS deployment. Other endpoints are (currently) incompatible with pyHaFAS""" 46 | addMicMac: bool 47 | """Whether the mic-mac authentication method should be activated. Exclusive with `addChecksum`""" 48 | addChecksum: bool 49 | """Whether the checksum authentication method should be activated. Exclusive with `addMicMac`""" 50 | salt: str 51 | """(required if `addMicMac` or `addChecksum` is true). The salt for calculating the checksum or mic-mac""" 52 | 53 | locale: str 54 | """(used in future) Locale used for i18n. Should be an IETF Language-Region Tag 55 | 56 | Examples: https://tools.ietf.org/html/bcp47#appendix-A 57 | """ 58 | timezone: pytz.tzinfo.tzinfo 59 | """Timezone HaFAS lives in. Should be a `pytz` `timezone` object""" 60 | 61 | requestBody: dict 62 | """Static part of the request body sent to HaFAS. Normally contains informations about the client and another authentication""" 63 | 64 | availableProducts: Dict[str, List[int]] 65 | """Should contain all products available in HaFAS. The key is the name the end-user will use, the value is a list of bitmasks (numbers) needed for the product. In most cases, this is only one number. This bitmasks will be add up to generate the final bitmask.""" 66 | defaultProducts: List[str] 67 | """List of products (item must be a key in `availableProducts`) which should be activated by default""" 68 | 69 | defaultUserAgent: str 70 | """(optional) User-Agent in header when connecting to HaFAS. Defaults to pyhafas. A good option would be the app ones. Can be overwritten by the user.""" 71 | userAgent: str 72 | """(optional) Do not change, unless you know what you're doing. Disallows the user to change the user agent. For usage in internal code to get the user-agent which should be used.""" 73 | -------------------------------------------------------------------------------- /pyhafas/profile/interfaces/helper/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FahrplanDatenGarten/pyhafas/4e91ffd80fe214ea6c0db2954160eb2813a2e215/pyhafas/profile/interfaces/helper/__init__.py -------------------------------------------------------------------------------- /pyhafas/profile/interfaces/helper/date_time.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import datetime 3 | 4 | 5 | class DateTimeHelperInterface(abc.ABC): 6 | @abc.abstractmethod 7 | def parse_datetime( 8 | self, 9 | time_string: str, 10 | date: datetime.date) -> datetime.datetime: 11 | """ 12 | Parses the time format HaFAS returns and combines it with a date 13 | 14 | :param time_string: Time string sent by HaFAS 15 | :param date: Start day of the leg/journey 16 | :return: Parsed date and time as datetime object 17 | """ 18 | pass 19 | 20 | @abc.abstractmethod 21 | def parse_timedelta(self, time_string: str) -> datetime.timedelta: 22 | """ 23 | Parses the time HaFAS returns as timedelta object 24 | 25 | Example use case is when HaFAS returns a duration of a leg 26 | :param time_string: Time string sent by HaFAS 27 | :return: Parsed time as timedelta object 28 | """ 29 | pass 30 | 31 | @abc.abstractmethod 32 | def parse_date(self, date_string: str) -> datetime.date: 33 | """ 34 | Parses the date HaFAS returns 35 | 36 | :param date_string: Date returned from HaFAS 37 | :return: Parsed date object 38 | """ 39 | pass 40 | 41 | @abc.abstractmethod 42 | def transform_datetime_parameter_timezone( 43 | self, 44 | date_time: datetime.datetime) -> datetime.datetime: 45 | """ 46 | Transfers datetime parameters incoming by the user to the profile timezone 47 | 48 | :param date_time: datetime parameter incoming by user. Can be timezone aware or unaware 49 | :return: Timezone aware datetime object in profile timezone 50 | """ 51 | pass 52 | -------------------------------------------------------------------------------- /pyhafas/profile/interfaces/helper/format_products_filter.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | 4 | class FormatProductsFilterHelperInterface(abc.ABC): 5 | @abc.abstractmethod 6 | def format_products_filter(self, requested_products: dict) -> dict: 7 | """ 8 | Create the products filter given to HaFAS 9 | 10 | :param requested_products: Mapping of Products to whether it's enabled or disabled 11 | :return: value for HaFAS "jnyFltrL" attribute 12 | """ 13 | pass 14 | -------------------------------------------------------------------------------- /pyhafas/profile/interfaces/helper/parse_leg.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import datetime 3 | from typing import List 4 | 5 | from pyhafas.types.fptf import Leg 6 | 7 | 8 | class ParseLegHelperInterface(abc.ABC): 9 | @abc.abstractmethod 10 | def parse_leg( 11 | self, 12 | journey: dict, 13 | common: dict, 14 | departure: dict, 15 | arrival: dict, 16 | date: datetime.date, 17 | jny_type: str = "JNY", 18 | gis=None) -> Leg: 19 | """ 20 | Parses Leg HaFAS returns into Leg object 21 | 22 | :param journey: Journey object given back by HaFAS (Data of the Leg to parse) 23 | :param common: Common object given back by HaFAS 24 | :param departure: Departure object given back by HaFAS 25 | :param arrival: Arrival object given back by HaFAS 26 | :param date: Parsed date of Journey (Departing date) 27 | :param jny_type: HaFAS Journey type 28 | :param gis: GIS object given back by HaFAS. 29 | :return: Parsed Leg object 30 | """ 31 | pass 32 | 33 | @abc.abstractmethod 34 | def parse_legs( 35 | self, 36 | jny: dict, 37 | common: dict, 38 | date: datetime.date) -> List[Leg]: 39 | """ 40 | Parses Legs (when multiple available) 41 | 42 | :param jny: Journeys object returned by HaFAS 43 | :param common: Common object returned by HaFAS 44 | :param date: Parsed date of Journey (Departing date) 45 | :return: Parsed List of Leg objects 46 | """ 47 | pass 48 | -------------------------------------------------------------------------------- /pyhafas/profile/interfaces/helper/parse_lid.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | from pyhafas.types.fptf import Station 4 | 5 | 6 | class ParseLidHelperInterface(abc.ABC): 7 | @abc.abstractmethod 8 | def parse_lid(self, lid: str) -> dict: 9 | """ 10 | Converts the LID given by HaFAS. Splits the LID in multiple elements 11 | 12 | :param lid: Location identifier (given by HaFAS) 13 | :return: Dict of the elements of the dict 14 | """ 15 | pass 16 | 17 | @abc.abstractmethod 18 | def parse_lid_to_station( 19 | self, 20 | lid: str, 21 | name: str = "", 22 | latitude: float = 0, 23 | longitude: float = 0) -> Station: 24 | """ 25 | Parses the LID given by HaFAS to a station object 26 | 27 | :param lid: Location identifier (given by HaFAS) 28 | :param name: Station name (optional) 29 | :param latitude: Latitude of the station (optional) 30 | :param longitude: Longitude of the station (optional) 31 | :return: Parsed LID as station object 32 | """ 33 | pass 34 | -------------------------------------------------------------------------------- /pyhafas/profile/interfaces/helper/parse_remark.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | from pyhafas.types.fptf import Remark 4 | 5 | 6 | class ParseRemarkHelperInterface(abc.ABC): 7 | @abc.abstractmethod 8 | def parse_remark( 9 | self, 10 | remark: dict, 11 | common: dict) -> Remark: 12 | """ 13 | Parses Remark HaFAS returns into Remark object 14 | 15 | :param remark: Remark object given back by HaFAS 16 | :param common: Common object given back by HaFAS 17 | :return: Parsed Remark object 18 | """ 19 | pass 20 | -------------------------------------------------------------------------------- /pyhafas/profile/interfaces/helper/request.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from typing import Tuple 3 | 4 | from pyhafas.types.hafas_response import HafasResponse 5 | 6 | 7 | class RequestHelperInterface(abc.ABC): 8 | @abc.abstractmethod 9 | def calculate_checksum(self, data: str) -> str: 10 | """ 11 | Calculates the checksum of the request (required for most profiles) 12 | 13 | :param data: Complete body as string 14 | :return: Checksum for the request 15 | """ 16 | pass 17 | 18 | @abc.abstractmethod 19 | def calculate_mic_mac(self, data: str) -> Tuple[str, str]: 20 | """ 21 | Calculates the mic-mac for the request (required for some profiles) 22 | 23 | :param data: Complete body as string 24 | :return: Mic and mac to be sent to HaFAS 25 | """ 26 | pass 27 | 28 | @abc.abstractmethod 29 | def url_formatter(self, data: str) -> str: 30 | """ 31 | Formats the URL for HaFAS (adds the checksum or mic-mac) 32 | 33 | :param data: Complete body as string 34 | :return: Request-URL (maybe with checksum or mic-mac) 35 | """ 36 | pass 37 | 38 | @abc.abstractmethod 39 | def request(self, body) -> HafasResponse: 40 | """ 41 | Sends the request and does a basic parsing of the response and error handling 42 | 43 | :param body: Reqeust body as dict (without the `requestBody` of the profile) 44 | :return: HafasRespone object or Exception when HaFAS response returns an error 45 | """ 46 | pass 47 | -------------------------------------------------------------------------------- /pyhafas/profile/interfaces/mappings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FahrplanDatenGarten/pyhafas/4e91ffd80fe214ea6c0db2954160eb2813a2e215/pyhafas/profile/interfaces/mappings/__init__.py -------------------------------------------------------------------------------- /pyhafas/profile/interfaces/mappings/error_codes.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class ErrorCodesMappingInterface(Enum): 5 | """ 6 | Mapping of the HaFAS error code to the exception class 7 | 8 | `default` defines the error when the error code cannot be found in the mapping 9 | """ 10 | default: Exception 11 | -------------------------------------------------------------------------------- /pyhafas/profile/interfaces/requests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FahrplanDatenGarten/pyhafas/4e91ffd80fe214ea6c0db2954160eb2813a2e215/pyhafas/profile/interfaces/requests/__init__.py -------------------------------------------------------------------------------- /pyhafas/profile/interfaces/requests/journey.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | from pyhafas.types.fptf import Journey 4 | from pyhafas.types.hafas_response import HafasResponse 5 | 6 | 7 | class JourneyRequestInterface(abc.ABC): 8 | @abc.abstractmethod 9 | def format_journey_request(self, journey: Journey) -> dict: 10 | """ 11 | Creates the HaFAS request body for a journey request 12 | 13 | :param journey: Id of the journey (ctxRecon) 14 | :return: Request body for HaFAS 15 | """ 16 | pass 17 | 18 | @abc.abstractmethod 19 | def parse_journey_request(self, data: HafasResponse) -> Journey: 20 | """ 21 | Parses the HaFAS response for a journey request 22 | 23 | :param data: Formatted HaFAS response 24 | :return: List of Journey objects 25 | """ 26 | pass 27 | -------------------------------------------------------------------------------- /pyhafas/profile/interfaces/requests/journeys.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import datetime 3 | from typing import Dict, List 4 | 5 | from pyhafas.types.fptf import Journey, Station, Leg 6 | from pyhafas.types.hafas_response import HafasResponse 7 | 8 | 9 | class JourneysRequestInterface(abc.ABC): 10 | @abc.abstractmethod 11 | def format_journeys_request( 12 | self, 13 | origin: Station, 14 | destination: Station, 15 | via: List[Station], 16 | date: datetime.datetime, 17 | min_change_time: int, 18 | max_changes: int, 19 | products: Dict[str, bool], 20 | max_journeys: int 21 | ) -> dict: 22 | """ 23 | Creates the HaFAS request body for a journeys request 24 | 25 | :param origin: Origin station 26 | :param destination: Destionation station 27 | :param via: Via stations, maybe empty list) 28 | :param date: Date and time to search journeys for 29 | :param min_change_time: Minimum transfer/change time at each station 30 | :param max_changes: Maximum number of changes 31 | :param products: Allowed products (a product is a mean of transport like ICE,IC) 32 | :param max_journeys: Maximum number of returned journeys 33 | :return: Request body for HaFAS 34 | """ 35 | pass 36 | 37 | @abc.abstractmethod 38 | def format_search_from_leg_request( 39 | self, 40 | origin: Leg, 41 | destination: Station, 42 | via: List[Station], 43 | min_change_time: int, 44 | max_changes: int, 45 | products: Dict[str, bool], 46 | ) -> dict: 47 | """ 48 | Creates the HaFAS request body for a search from leg request 49 | 50 | :param origin: Origin leg 51 | :param destination: Destionation station 52 | :param via: Via stations, maybe empty list) 53 | :param min_change_time: Minimum transfer/change time at each station 54 | :param max_changes: Maximum number of changes 55 | :param products: Allowed products (a product is a mean of transport like ICE,IC) 56 | :return: Request body for HaFAS 57 | """ 58 | pass 59 | 60 | @abc.abstractmethod 61 | def parse_journeys_request(self, data: HafasResponse) -> List[Journey]: 62 | """ 63 | Parses the HaFAS response for a journeys request 64 | 65 | :param data: Formatted HaFAS response 66 | :return: List of Journey objects 67 | """ 68 | pass 69 | -------------------------------------------------------------------------------- /pyhafas/profile/interfaces/requests/location.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from typing import List 3 | 4 | from pyhafas.types.fptf import Station 5 | from pyhafas.types.hafas_response import HafasResponse 6 | 7 | 8 | class LocationRequestInterface(abc.ABC): 9 | @abc.abstractmethod 10 | def format_location_request(self, term: str, rtype: str = 'S'): 11 | """ 12 | Creates the HaFAS request body for a location search request. 13 | 14 | :param term: Search term 15 | :param rtype: Result types. One of ['S' for stations, 'ALL' for addresses and stations] 16 | :return: Request body for HaFAS 17 | """ 18 | pass 19 | 20 | @abc.abstractmethod 21 | def parse_location_request(self, data: HafasResponse) -> List[Station]: 22 | """ 23 | Parses the HaFAS response for a location request 24 | 25 | :param data: Formatted HaFAS response 26 | :return: List of Station objects 27 | """ 28 | pass 29 | -------------------------------------------------------------------------------- /pyhafas/profile/interfaces/requests/nearby.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from typing import List 3 | 4 | from pyhafas.types.fptf import Station 5 | from pyhafas.types.hafas_response import HafasResponse 6 | from pyhafas.types.nearby import LatLng 7 | 8 | 9 | class NearbyRequestInterface(abc.ABC): 10 | @abc.abstractmethod 11 | def format_nearby_request(self, 12 | location: LatLng, 13 | max_walking_distance: int, 14 | min_walking_distance: int, 15 | products: dict[str, bool], 16 | get_pois: bool, 17 | get_stops: bool, 18 | max_locations: int) -> dict: 19 | """ 20 | Creates the HaFAS request body for a nearby request. 21 | 22 | :param location: LatLng object containing latitude and longitude 23 | :param max_walking_distance: Maximum walking distance in meters 24 | :param min_walking_distance: Minimum walking distance in meters 25 | :param products: Dictionary of product names to products 26 | :param get_pois: If true, returns pois 27 | :param get_stops: If true, returns stops instead of locations 28 | :param max_locations: Maximum number of locations to return 29 | :return: Request body for HaFAS 30 | """ 31 | pass 32 | 33 | @abc.abstractmethod 34 | def parse_nearby_response(self, data: HafasResponse) -> List[Station]: 35 | pass 36 | -------------------------------------------------------------------------------- /pyhafas/profile/interfaces/requests/station_board.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import datetime 3 | from typing import Dict, List, Optional 4 | 5 | from pyhafas.types.fptf import Station, StationBoardLeg 6 | from pyhafas.types.hafas_response import HafasResponse 7 | from pyhafas.types.station_board_request import StationBoardRequestType 8 | 9 | 10 | class StationBoardRequestInterface(abc.ABC): 11 | def format_station_board_request( 12 | self, 13 | station: Station, 14 | request_type: StationBoardRequestType, 15 | date: datetime.datetime, 16 | max_trips: int, 17 | duration: int, 18 | products: Dict[str, bool], 19 | direction: Optional[Station] 20 | ) -> dict: 21 | """ 22 | Creates the HaFAS request for Station Board (departure/arrival) 23 | 24 | :param station: Station to get departures/arrivals for 25 | :param request_type: ARRIVAL or DEPARTURE 26 | :param date: Date and time to get departures/arrival for 27 | :param max_trips: Maximum number of trips that can be returned 28 | :param products: Allowed products (a product is a mean of transport like ICE,IC) 29 | :param duration: Time in which trips are searched 30 | :param direction: Direction (end) station of the train. If none, filter will not be applied 31 | :return: Request body for HaFAS 32 | """ 33 | pass 34 | 35 | def parse_station_board_request( 36 | self, 37 | data: HafasResponse, 38 | departure_arrival_prefix: str) -> List[StationBoardLeg]: 39 | """ 40 | Parses the HaFAS data for a station board request 41 | 42 | :param data: Formatted HaFAS response 43 | :param departure_arrival_prefix: Prefix for specifying whether its for arrival or departure 44 | :return: List of StationBoardLeg objects 45 | """ 46 | pass 47 | -------------------------------------------------------------------------------- /pyhafas/profile/interfaces/requests/trip.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | from pyhafas.types.fptf import Leg 4 | from pyhafas.types.hafas_response import HafasResponse 5 | 6 | 7 | class TripRequestInterface(abc.ABC): 8 | @abc.abstractmethod 9 | def format_trip_request(self, trip_id: str) -> dict: 10 | """ 11 | Creates the HaFAS request for a trip request 12 | 13 | :param trip_id: Id of the trip/leg 14 | :return: Request body for HaFAS 15 | """ 16 | pass 17 | 18 | @abc.abstractmethod 19 | def parse_trip_request(self, data: HafasResponse) -> Leg: 20 | """ 21 | Parses the HaFAS data for a trip request 22 | 23 | :param data: Formatted HaFAS response 24 | :return: Leg objects 25 | """ 26 | pass 27 | -------------------------------------------------------------------------------- /pyhafas/profile/kvb/__init__.py: -------------------------------------------------------------------------------- 1 | import pytz 2 | 3 | from pyhafas.profile import BaseProfile 4 | from pyhafas.profile.kvb.requests.journeys import KVBJourneysRequest 5 | from pyhafas.profile.kvb.requests.journey import KVBJourneyRequest 6 | 7 | 8 | class KVBProfile(KVBJourneysRequest, KVBJourneyRequest, BaseProfile): 9 | """ 10 | Profile of the HaFAS of Kölner Verkehrsbetriebe (KVB) - Regional in Cologne 11 | """ 12 | baseUrl = "https://auskunft.kvb.koeln/gate" 13 | 14 | locale = 'de-DE' 15 | timezone = pytz.timezone('Europe/Berlin') 16 | 17 | requestBody = { 18 | 'client': { 19 | 'id': 'HAFAS', 20 | 'l': 'vs_webapp', 21 | 'v': '154', 22 | 'type': 'WEB', 23 | 'name': 'webapp' 24 | }, 25 | 'ext': 'DB.R21.12.a', 26 | 'ver': '1.58', 27 | 'lang': 'deu', 28 | 'auth': { 29 | 'type': 'AID', 30 | 'aid': 'Rt6foY5zcTTRXMQs' 31 | } 32 | } 33 | 34 | availableProducts = { 35 | 's-bahn': [1], 36 | 'stadtbahn': [2], 37 | 'bus': [8], 38 | 'regionalverkehr': [16], 39 | 'fernverkehr': [32], 40 | 'taxibus': [256] 41 | } 42 | 43 | defaultProducts = [ 44 | 's-bahn', 45 | 'stadtbahn', 46 | 'bus', 47 | 'fernverkehr', 48 | 'regionalverkehr', 49 | 'taxibus', 50 | ] 51 | -------------------------------------------------------------------------------- /pyhafas/profile/kvb/requests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FahrplanDatenGarten/pyhafas/4e91ffd80fe214ea6c0db2954160eb2813a2e215/pyhafas/profile/kvb/requests/__init__.py -------------------------------------------------------------------------------- /pyhafas/profile/kvb/requests/journey.py: -------------------------------------------------------------------------------- 1 | from pyhafas.profile import ProfileInterface 2 | from pyhafas.profile.base import BaseJourneyRequest 3 | from pyhafas.profile.interfaces.requests.journey import JourneyRequestInterface 4 | from pyhafas.types.fptf import Journey 5 | from pyhafas.types.hafas_response import HafasResponse 6 | 7 | 8 | class KVBJourneyRequest(BaseJourneyRequest, JourneyRequestInterface): 9 | def format_journey_request( 10 | self: ProfileInterface, 11 | journey: Journey) -> dict: 12 | """ 13 | Creates the HAFAS (KVB-deployment) request for refreshing journey details 14 | :param journey: Id of the journey (ctxRecon) 15 | :return: Request for HAFAS (KVB-deployment) 16 | """ 17 | return { 18 | 'req': { 19 | 'outReconL': [{ 20 | 'ctx': journey.id 21 | }] 22 | }, 23 | 'meth': 'Reconstruction' 24 | } 25 | 26 | def parse_journey_request(self: ProfileInterface, data: HafasResponse) -> Journey: 27 | """ 28 | Parses the HaFAS response for a journey request 29 | :param data: Formatted HaFAS response 30 | :return: List of Journey objects 31 | """ 32 | date = self.parse_date(data.res['outConL'][0]['date']) 33 | return Journey( 34 | data.res['outConL'][0]['recon']['ctx'], 35 | date=date, 36 | duration=self.parse_timedelta( 37 | data.res['outConL'][0]['dur']), 38 | legs=self.parse_legs( 39 | data.res['outConL'][0], 40 | data.common, 41 | date)) 42 | -------------------------------------------------------------------------------- /pyhafas/profile/kvb/requests/journeys.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from pyhafas.profile.base import BaseJourneysRequest 4 | from pyhafas.profile.interfaces.requests.journeys import JourneysRequestInterface 5 | from pyhafas.types.fptf import Journey 6 | from pyhafas.types.hafas_response import HafasResponse 7 | 8 | 9 | class KVBJourneysRequest(BaseJourneysRequest, JourneysRequestInterface): 10 | """ 11 | Class for the KVB Journeys requests, because the id of the journey is in jny['recon']['ctx] instead of 12 | jny['ctxRecon'] 13 | """ 14 | 15 | def parse_journeys_request(self, data: HafasResponse) -> List[Journey]: 16 | journeys = [] 17 | 18 | for jny in data.res['outConL']: 19 | date = self.parse_date(jny['date']) 20 | journeys.append( 21 | Journey( 22 | jny['recon']['ctx'], date=date, duration=self.parse_timedelta( 23 | jny['dur']), legs=self.parse_legs( 24 | jny, data.common, date))) 25 | return journeys 26 | -------------------------------------------------------------------------------- /pyhafas/profile/nasa/__init__.py: -------------------------------------------------------------------------------- 1 | import pytz 2 | 3 | from pyhafas.profile.base import BaseProfile 4 | from pyhafas.profile.nasa.requests.journeys import NASAJourneysRequest 5 | from pyhafas.profile.nasa.requests.journey import NASAJourneyRequest 6 | 7 | 8 | class NASAProfile(NASAJourneyRequest, NASAJourneysRequest, BaseProfile): 9 | """ 10 | Profile of the HaFAS of NASA (Nahverkehr Sachsen-Anhalt). 11 | """ 12 | 13 | baseUrl = "https://reiseauskunft.insa.de/bin/mgate.exe" 14 | defaultUserAgent = "nasa/6.4.3 (iPad; iOS 17.5.1; Scale/2.00)" 15 | 16 | addMicMac = False 17 | addChecksum = False 18 | 19 | locale = 'de-DE' 20 | timezone = pytz.timezone('Europe/Berlin') 21 | 22 | requestBody = { 23 | "client": { 24 | "id": "NASA", 25 | "type": "IPH", 26 | "name": "nasa", 27 | "v": "6040300", 28 | }, 29 | "ver": "1.57", 30 | "lang": "de", 31 | "auth": { 32 | "type": "AID", 33 | "aid": "nasa-apps", 34 | } 35 | } 36 | 37 | availableProducts = { 38 | 'long_distance_express': [1], # ICE 39 | 'long_distance': [2], # IC/EC/CNL 40 | 'regional': [8], # RE/RB 41 | 'suburban': [16], # S 42 | 'bus': [64, 128], # BUS 43 | 'tram': [32], # T 44 | 'tourism_train': [256], # TT 45 | } 46 | 47 | defaultProducts = [ 48 | 'long_distance_express', 49 | 'long_distance', 50 | 'regional', 51 | 'suburban', 52 | 'bus', 53 | 'tram', 54 | 'tourism_train', 55 | ] 56 | -------------------------------------------------------------------------------- /pyhafas/profile/nasa/requests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FahrplanDatenGarten/pyhafas/4e91ffd80fe214ea6c0db2954160eb2813a2e215/pyhafas/profile/nasa/requests/__init__.py -------------------------------------------------------------------------------- /pyhafas/profile/nasa/requests/journey.py: -------------------------------------------------------------------------------- 1 | from pyhafas.profile import ProfileInterface 2 | from pyhafas.profile.base import BaseJourneyRequest 3 | from pyhafas.profile.interfaces.requests.journey import JourneyRequestInterface 4 | from pyhafas.types.fptf import Journey 5 | from pyhafas.types.hafas_response import HafasResponse 6 | 7 | 8 | class NASAJourneyRequest(BaseJourneyRequest, JourneyRequestInterface): 9 | def format_journey_request( 10 | self: ProfileInterface, 11 | journey: Journey) -> dict: 12 | """ 13 | Creates the HaFAS ( Adapted for NASA ) request for refreshing journey details 14 | 15 | :param journey: Id of the journey (ctxRecon) 16 | :return: Request for HaFAS ( NASA-Adapted ) 17 | """ 18 | return { 19 | 'req': { 20 | 'outReconL': [{ 21 | 'ctx': journey.id 22 | }] 23 | }, 24 | 'meth': 'Reconstruction' 25 | } 26 | 27 | def parse_journey_request( 28 | self: ProfileInterface, 29 | data: HafasResponse) -> Journey: 30 | """ 31 | Parses the HaFAS response for a journey request ( Adapted for NASA ) 32 | 33 | :param data: Formatted HaFAS response 34 | :return: List of Journey objects 35 | """ 36 | date = self.parse_date(data.res['outConL'][0]['date']) 37 | return Journey( 38 | data.res['outConL'][0]['recon']['ctx'], 39 | date=date, 40 | duration=self.parse_timedelta( 41 | data.res['outConL'][0]['dur']), 42 | legs=self.parse_legs( 43 | data.res['outConL'][0], 44 | data.common, 45 | date)) -------------------------------------------------------------------------------- /pyhafas/profile/nasa/requests/journeys.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from pyhafas.profile.base import BaseJourneysRequest 4 | from pyhafas.profile.interfaces.requests.journeys import \ 5 | JourneysRequestInterface 6 | from pyhafas.types.fptf import Journey 7 | from pyhafas.types.hafas_response import HafasResponse 8 | 9 | 10 | class NASAJourneysRequest(BaseJourneysRequest, JourneysRequestInterface): 11 | ''' 12 | Class for the NASA Journeys requests, because the id of the journey is in jny['recon']['ctx] instead of jny['ctxRecon'] 13 | ''' 14 | 15 | def parse_journeys_request(self, data: HafasResponse) -> List[Journey]: 16 | journeys = [] 17 | 18 | for jny in data.res['outConL']: 19 | date = self.parse_date(jny['date']) 20 | journeys.append( 21 | Journey( 22 | jny['recon']['ctx'], date=date, duration=self.parse_timedelta( 23 | jny['dur']), legs=self.parse_legs( 24 | jny, data.common, date))) 25 | return journeys 26 | -------------------------------------------------------------------------------- /pyhafas/profile/nvv/__init__.py: -------------------------------------------------------------------------------- 1 | import pytz 2 | 3 | from pyhafas.profile.base import BaseProfile 4 | from pyhafas.profile.nvv.requests.journey import NVVJourneyRequest 5 | from pyhafas.profile.nvv.requests.journeys import NVVJourneysRequest 6 | 7 | 8 | class NVVProfile(NVVJourneyRequest, NVVJourneysRequest, BaseProfile): 9 | """ 10 | Profile for the HaFAs of "Nordhessischer Verkehrs Verbund" 11 | (NVV) - local transportation provider 12 | """ 13 | baseUrl = "https://auskunft.nvv.de/bin/mgate.exe" 14 | defaultUserAgent = "NVV Mobil/5.3.1 (iPhone; IOS 13.1.2; Scale/2.00)" 15 | 16 | addMicMac = False 17 | addChecksum = False 18 | 19 | locale = "de-DE" 20 | timezone = pytz.timezone("Europe/Berlin") 21 | 22 | requestBody = { 23 | "client": { 24 | "id": "NVV", 25 | "type": "WEB", 26 | "name": "webapp" 27 | }, 28 | "ver": "1.39", 29 | "lang": "deu", 30 | "auth":{ 31 | "type": "AID", 32 | "aid": "R7aKWQLVBRSoVRtY" 33 | } 34 | } 35 | 36 | availableProducts = { 37 | 'long_distance_express': [1], # ICE 38 | 'long_distance': [2], # IC/EC 39 | 'regional_express': [4], # RE/RB 40 | 'tram': [32], # Tram 41 | 'bus': [64, 128], # Bus 42 | 'anruf_sammel_taxi': [512], # Group Taxi 43 | 'regio_tram': [1024] # Tram / regional express hybrid 44 | } 45 | 46 | defaultProducts = [ 47 | 'long_distance_express', 48 | 'long_distance', 49 | 'regional_express', 50 | 'bus', 51 | 'tram', 52 | 'anruf_sammel_taxi', 53 | 'regio_tram' 54 | ] 55 | -------------------------------------------------------------------------------- /pyhafas/profile/nvv/requests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FahrplanDatenGarten/pyhafas/4e91ffd80fe214ea6c0db2954160eb2813a2e215/pyhafas/profile/nvv/requests/__init__.py -------------------------------------------------------------------------------- /pyhafas/profile/nvv/requests/journey.py: -------------------------------------------------------------------------------- 1 | from pyhafas.profile import ProfileInterface 2 | from pyhafas.profile.base import BaseJourneyRequest 3 | from pyhafas.profile.interfaces.requests.journey import JourneyRequestInterface 4 | from pyhafas.types.fptf import Journey 5 | from pyhafas.types.hafas_response import HafasResponse 6 | 7 | 8 | class NVVJourneyRequest(BaseJourneyRequest, JourneyRequestInterface): 9 | def format_journey_request( 10 | self: ProfileInterface, 11 | journey: Journey) -> dict: 12 | """ 13 | Creates the HAFAS (NVV-deployment) request for refreshing journey details 14 | 15 | :param journey: Id of the journey (ctxRecon) 16 | :return: Request for HAFAS (NVV-deployment) 17 | """ 18 | return { 19 | 'req': { 20 | 'outReconL': [{ 21 | 'ctx': journey.id 22 | }] 23 | }, 24 | 'meth': 'Reconstruction' 25 | } 26 | 27 | def parse_journey_request( 28 | self: ProfileInterface, 29 | data: HafasResponse) -> Journey: 30 | """ 31 | Parses the HaFAS response for a journey request 32 | 33 | :param data: Formatted HaFAS response 34 | :return: List of Journey objects 35 | """ 36 | date = self.parse_date(data.res['outConL'][0]['date']) 37 | return Journey( 38 | data.res['outConL'][0]['recon']['ctx'], 39 | date=date, 40 | duration=self.parse_timedelta( 41 | data.res['outConL'][0]['dur']), 42 | legs=self.parse_legs( 43 | data.res['outConL'][0], 44 | data.common, 45 | date)) 46 | -------------------------------------------------------------------------------- /pyhafas/profile/nvv/requests/journeys.py: -------------------------------------------------------------------------------- 1 | from pyhafas.profile import ProfileInterface 2 | from pyhafas.profile.interfaces.requests.journeys import JourneysRequestInterface 3 | from pyhafas.profile.base import BaseJourneysRequest 4 | from pyhafas.types.hafas_response import HafasResponse 5 | 6 | 7 | class NVVJourneysRequest(BaseJourneysRequest, JourneysRequestInterface): 8 | def parse_journeys_request(self:ProfileInterface, data: HafasResponse): 9 | for jny in data.res["outConL"]: 10 | jny["ctxRecon"] = jny["recon"]["ctx"] 11 | 12 | return super().parse_journeys_request(data) 13 | -------------------------------------------------------------------------------- /pyhafas/profile/rkrp/__init__.py: -------------------------------------------------------------------------------- 1 | import pytz 2 | 3 | from pyhafas.profile.base import BaseProfile 4 | 5 | 6 | class RKRPProfile(BaseProfile): 7 | """ 8 | Profile of the HaFAS of Rejsekort & Rejseplan (RKRP) - Danish {Railway, Bus, Metro, etc.}. 9 | https://www.rejsekort.dk/da/RKRP 10 | https://help.rejseplanen.dk/hc/da/articles/214174465-Rejseplanens-API 11 | """ 12 | 13 | # Alternative base URLs (mostly CNAMEs): 14 | # rejseplanen.dk 15 | # webapp.rejseplanen.dk 16 | # www.rejseplanen.dk 17 | # rkrp.hafas.cloud 18 | # rkrp-fat.hafas.cloud 19 | baseUrl = "https://www.rejseplanen.dk/bin/iphone.exe" 20 | defaultUserAgent = "Dalvik/2.1.0 (Linux; U; Android 11; Pixel 4a Build/RQ2A.210305.006)" 21 | 22 | addMicMac = False 23 | addChecksum = False 24 | 25 | locale = 'da-DK' 26 | timezone = pytz.timezone('Europe/Copenhagen') 27 | 28 | requestBody = { 29 | "client": { 30 | "id": "DK", 31 | "type": "WEB", 32 | "name": "rejseplanwebapp", 33 | "l": "vs_webapp" 34 | }, 35 | "ext": "DK.11", 36 | "ver": "1.24", 37 | "lang": "dan", 38 | "auth": { 39 | "type": "AID", 40 | "aid": "j1sa92pcj72ksh0-web" 41 | } 42 | } 43 | 44 | availableProducts = { 45 | 'long_distance_express': [1], # ICE 46 | 'long_distance': [2], # IC/EC 47 | 'regional_express': [4], # RE/IR 48 | 'regional': [8], # RB 49 | 'suburban': [16], # S 50 | 'bus': [32], # BUS 51 | 'ferry': [64], # F 52 | 'subway': [128], # U 53 | 'tram': [256], # T 54 | 'taxi': [512] # Group Taxi 55 | } 56 | 57 | defaultProducts = [ 58 | 'long_distance_express', 59 | 'long_distance', 60 | 'regional_express', 61 | 'regional', 62 | 'suburban', 63 | 'bus', 64 | 'ferry', 65 | 'subway', 66 | 'tram', 67 | 'taxi' 68 | ] 69 | -------------------------------------------------------------------------------- /pyhafas/profile/vsn/__init__.py: -------------------------------------------------------------------------------- 1 | import pytz 2 | 3 | from pyhafas.profile import BaseProfile 4 | from pyhafas.profile.vsn.requests.journey import VSNJourneyRequest 5 | 6 | 7 | class VSNProfile(VSNJourneyRequest, BaseProfile): 8 | """ 9 | Profile for the HaFAS of "Verkehrsverbund Süd-Niedersachsen" (VSN) - local transportation provider 10 | """ 11 | baseUrl = "https://fahrplaner.vsninfo.de/hafas/mgate.exe" 12 | defaultUserAgent = "vsn/5.3.1 (iPad; iOS 13.3; Scale/2.00)" 13 | 14 | salt = 'SP31mBufSyCLmNxp' 15 | addMicMac = True 16 | 17 | locale = 'de-DE' 18 | timezone = pytz.timezone('Europe/Berlin') 19 | 20 | requestBody = { 21 | 'client': { 22 | 'id': 'VSN', 23 | 'v': '5030100', 24 | 'type': 'IPA', 25 | 'name': 'vsn', 26 | 'os': 'iOS 13.3' 27 | }, 28 | 'ver': '1.24', 29 | 'lang': 'de', 30 | 'auth': { 31 | 'type': 'AID', 32 | 'aid': 'Mpf5UPC0DmzV8jkg' 33 | } 34 | } 35 | 36 | availableProducts = { 37 | 'long_distance_express': [1], # ICE 38 | 'long_distance': [2], # IC/EC/CNL 39 | 'regional_express': [4], # RE/IR 40 | 'regional': [8], # NV 41 | 'suburban': [16], # S 42 | 'bus': [32], # BUS 43 | 'ferry': [64], # F 44 | 'subway': [128], # U 45 | 'tram': [256], # T 46 | 'anruf_sammel_taxi': [512] # Group Taxi 47 | } 48 | 49 | defaultProducts = [ 50 | 'long_distance_express', 51 | 'long_distance', 52 | 'regional_express', 53 | 'regional', 54 | 'suburban', 55 | 'bus', 56 | 'ferry', 57 | 'subway', 58 | 'tram', 59 | 'anruf_sammel_taxi' 60 | ] 61 | -------------------------------------------------------------------------------- /pyhafas/profile/vsn/requests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FahrplanDatenGarten/pyhafas/4e91ffd80fe214ea6c0db2954160eb2813a2e215/pyhafas/profile/vsn/requests/__init__.py -------------------------------------------------------------------------------- /pyhafas/profile/vsn/requests/journey.py: -------------------------------------------------------------------------------- 1 | from pyhafas.profile import ProfileInterface 2 | from pyhafas.profile.base import BaseJourneyRequest 3 | from pyhafas.profile.interfaces.requests.journey import JourneyRequestInterface 4 | from pyhafas.types.fptf import Journey 5 | 6 | 7 | class VSNJourneyRequest(BaseJourneyRequest, JourneyRequestInterface): 8 | def format_journey_request( 9 | self: ProfileInterface, 10 | journey: Journey) -> dict: 11 | """ 12 | Creates the HaFAS (VSN-deployment) request for refreshing journey details 13 | 14 | :param journey: Id of the journey (ctxRecon) 15 | :return: Request for HaFAS (VSN-deployment) 16 | """ 17 | return { 18 | 'req': { 19 | 'outReconL': [{ 20 | 'ctx': journey.id 21 | }] 22 | }, 23 | 'meth': 'Reconstruction' 24 | } 25 | -------------------------------------------------------------------------------- /pyhafas/profile/vvv/__init__.py: -------------------------------------------------------------------------------- 1 | import pytz 2 | 3 | from pyhafas.profile.base import BaseProfile 4 | from pyhafas.profile.vvv.requests.journey import VVVJourneyRequest 5 | from pyhafas.profile.vvv.requests.journeys import VVVJourneysRequest 6 | 7 | 8 | class VVVProfile(VVVJourneyRequest, VVVJourneysRequest, BaseProfile): 9 | """ 10 | Profile of the HaFAS of Verkehrsverbund Vorarlberg (VVV) 11 | https://de.wikipedia.org/wiki/Verkehrsverbund_Vorarlberg 12 | """ 13 | 14 | baseUrl = "https://fahrplan.vmobil.at/bin/mgate.exe" 15 | 16 | addMicMac = True 17 | salt = '6633673735743766726667323938336A' 18 | 19 | locale = 'at-DE' 20 | timezone = pytz.timezone('Europe/Vienna') 21 | 22 | requestBody = { 23 | "client": { 24 | "id": "VAO", 25 | "l": "vs_vvv", 26 | "name": "webapp", 27 | "type": "WEB", 28 | "v": "20230901" 29 | }, 30 | "ext": "VAO.20", 31 | "ver": "1.59", 32 | "lang": "de", 33 | "auth": { 34 | "type": "AID", 35 | "aid": "wf7mcf9bv3nv8g5f" 36 | } 37 | } 38 | 39 | availableProducts = { 40 | 'train-and-s-bahn': [1, 2], # Bahn & S-Bahn 41 | 'u-bahn': [4], # U-Bahn 42 | 'tram': [16], # Straßenbahn 43 | 'long-distance-bus': [32], # Fernbus 44 | 'regional-bus': [64], # Regionalbus 45 | 'city-bus': [128], # Stadtbus 46 | 'aerial-lift': [256], # Seil-/Zahnradbahn 47 | 'ferry': [512], # Schiff 48 | 'on-call': [1024], # Anrufsammeltaxi 49 | 'other-bus': [2048] # sonstige Busse 50 | } 51 | 52 | defaultProducts = [ 53 | 'train-and-s-bahn', 54 | 'u-bahn', 55 | 'tram', 56 | 'long-distance-bus', 57 | 'regional-bus', 58 | 'city-bus', 59 | 'aerial-lift', 60 | 'ferry', 61 | 'on-call', 62 | 'other-bus' 63 | ] 64 | -------------------------------------------------------------------------------- /pyhafas/profile/vvv/requests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FahrplanDatenGarten/pyhafas/4e91ffd80fe214ea6c0db2954160eb2813a2e215/pyhafas/profile/vvv/requests/__init__.py -------------------------------------------------------------------------------- /pyhafas/profile/vvv/requests/journey.py: -------------------------------------------------------------------------------- 1 | from pyhafas.profile import ProfileInterface 2 | from pyhafas.profile.base import BaseJourneyRequest 3 | from pyhafas.profile.interfaces.requests.journey import JourneyRequestInterface 4 | from pyhafas.types.fptf import Journey 5 | from pyhafas.types.hafas_response import HafasResponse 6 | 7 | 8 | class VVVJourneyRequest(BaseJourneyRequest, JourneyRequestInterface): 9 | def format_journey_request( 10 | self: ProfileInterface, 11 | journey: Journey) -> dict: 12 | """ 13 | Creates the HAFAS (VVV-deployment) request for refreshing journey details 14 | 15 | :param journey: Id of the journey (ctxRecon) 16 | :return: Request for HAFAS (VVV-deployment) 17 | """ 18 | return { 19 | 'req': { 20 | 'outReconL': [{ 21 | 'ctx': journey.id 22 | }] 23 | }, 24 | 'meth': 'Reconstruction' 25 | } 26 | 27 | def parse_journey_request(self: ProfileInterface, data: HafasResponse) -> Journey: 28 | """ 29 | Parses the HaFAS response for a journey request 30 | :param data: Formatted HaFAS response 31 | :return: List of Journey objects 32 | """ 33 | date = self.parse_date(data.res['outConL'][0]['date']) 34 | return Journey( 35 | data.res['outConL'][0]['recon']['ctx'], 36 | date=date, 37 | duration=self.parse_timedelta( 38 | data.res['outConL'][0]['dur']), 39 | legs=self.parse_legs( 40 | data.res['outConL'][0], 41 | data.common, 42 | date)) 43 | -------------------------------------------------------------------------------- /pyhafas/profile/vvv/requests/journeys.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from pyhafas.profile.base import BaseJourneysRequest 4 | from pyhafas.profile.interfaces.requests.journeys import JourneysRequestInterface 5 | from pyhafas.types.fptf import Journey 6 | from pyhafas.types.hafas_response import HafasResponse 7 | 8 | 9 | class VVVJourneysRequest(BaseJourneysRequest, JourneysRequestInterface): 10 | def parse_journeys_request(self, data: HafasResponse) -> List[Journey]: 11 | journeys = [] 12 | 13 | for jny in data.res['outConL']: 14 | date = self.parse_date(jny['date']) 15 | 16 | # skip all 'TRSF' type journeys (propably better handling should be implemented) 17 | jny['secL'] = [s for s in jny['secL'] if s['type'] != 'TRSF'] 18 | 19 | journeys.append( 20 | Journey( 21 | jny['recon']['ctx'], date=date, duration=self.parse_timedelta( 22 | jny['dur']), legs=self.parse_legs( 23 | jny, data.common, date))) 24 | return journeys -------------------------------------------------------------------------------- /pyhafas/types/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FahrplanDatenGarten/pyhafas/4e91ffd80fe214ea6c0db2954160eb2813a2e215/pyhafas/types/__init__.py -------------------------------------------------------------------------------- /pyhafas/types/exceptions.py: -------------------------------------------------------------------------------- 1 | class ProductNotAvailableError(Exception): 2 | """Requested Product is not available in profile""" 3 | pass 4 | 5 | 6 | class GeneralHafasError(Exception): 7 | """HaFAS returned an general error""" 8 | pass 9 | 10 | 11 | class AuthenticationError(Exception): 12 | """Authentiction data missing or wrong""" 13 | pass 14 | 15 | 16 | class AccessDeniedError(Exception): 17 | """Access is denied""" 18 | pass 19 | 20 | 21 | class LocationNotFoundError(Exception): 22 | """Location/stop not found""" 23 | pass 24 | 25 | 26 | class JourneysTooManyTrainsError(Exception): 27 | """Journeys search: Too many trains, connection is not complete""" 28 | pass 29 | 30 | 31 | class JourneysArrivalDepartureTooNearError(Exception): 32 | """Journeys search: arrival and departure date are too near""" 33 | pass 34 | 35 | 36 | class NoDepartureArrivalDataError(Exception): 37 | """No departure/arrival data available""" 38 | pass 39 | 40 | 41 | class TripDataNotFoundError(Exception): 42 | """No trips found or trip info not available""" 43 | -------------------------------------------------------------------------------- /pyhafas/types/hafas_response.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import requests 4 | 5 | from pyhafas.profile.interfaces.mappings.error_codes import \ 6 | ErrorCodesMappingInterface 7 | 8 | 9 | class HafasResponse: 10 | """ 11 | The class HafasResponse handles the general parsing and error-checking of a raw HaFAS response 12 | 13 | :ivar raw_hafas_response: The raw response of HaFAS 14 | :vartype raw_hafas_response: requests.Response 15 | :ivar data: json parsed raw response of HaFAS 16 | :vartype data: dict 17 | """ 18 | 19 | def __init__(self, raw_hafas_response: requests.Response, 20 | mapping: ErrorCodesMappingInterface): 21 | """ 22 | Constructor of class HafasResponse 23 | 24 | :param raw_hafas_response: The raw response of HaFAS 25 | :param mapping: Error Mapping Enum (key is the HaFAS error code, value the error class) 26 | """ 27 | data = json.loads(raw_hafas_response.text) 28 | self.raw_hafas_response = raw_hafas_response 29 | self.data = data 30 | self.check_for_errors(mapping) 31 | 32 | def check_for_errors(self, mapping: ErrorCodesMappingInterface): 33 | """ 34 | Checks if HaFAS response has error messages and handles them 35 | 36 | :param mapping: Error Mapping Enum (key is the HaFAS error code, value the error class) 37 | """ 38 | error_not_found = False 39 | if self.data.get('err', "OK") != "OK": 40 | try: 41 | raise mapping[self.data['err']].value( 42 | self.data.get('errTxt', '')) 43 | except KeyError: 44 | error_not_found = True 45 | if error_not_found: 46 | raise mapping['default'].value(self.data.get('errTxt', '')) 47 | 48 | if not self.data.get('svcResL', False): 49 | raise mapping['default'].value("HaFAS response cannot be parsed") 50 | 51 | if self.data['svcResL'][0].get('err', "OK") != "OK": 52 | try: 53 | raise mapping[self.data['svcResL'][0]['err']].value( 54 | self.data['svcResL'][0].get('errTxt', '')) 55 | except KeyError: 56 | error_not_found = True 57 | if error_not_found: 58 | raise mapping['default'].value( 59 | self.data['svcResL'][0].get('errTxt', '')) 60 | 61 | @property 62 | def common(self): 63 | """ 64 | Returns the "common" data out of HaFAS data 65 | 66 | :return: dict with "common" data 67 | """ 68 | return self.data['svcResL'][0]['res']['common'] 69 | 70 | @property 71 | def res(self): 72 | """ 73 | Returns the "res" data out of HaFAS data 74 | 75 | :return: dict with "res" data 76 | """ 77 | return self.data['svcResL'][0]['res'] 78 | -------------------------------------------------------------------------------- /pyhafas/types/nearby.py: -------------------------------------------------------------------------------- 1 | class LatLng: 2 | latitude: float 3 | longitude: float 4 | 5 | def __init__(self, latitude: float, longitude: float): 6 | self.latitude = latitude 7 | self.longitude = longitude 8 | 9 | @property 10 | def latitude_e6(self) -> int: 11 | return round(self.latitude * 1E6) 12 | 13 | @property 14 | def longitude_e6(self) -> int: 15 | return round(self.longitude * 1E6) 16 | -------------------------------------------------------------------------------- /pyhafas/types/station_board_request.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class StationBoardRequestType(Enum): 5 | """ 6 | Mapping of StationBoard request from client to HaFAS 7 | """ 8 | DEPARTURE = 'DEP' 9 | ARRIVAL = 'ARR' 10 | 11 | def __repr__(self): 12 | return '<%s.%s>' % (self.__class__.__name__, self.name) 13 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = 3 | tests 4 | norecursedirs=dist build .tox scripts 5 | addopts = 6 | -r a 7 | -v 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests~=2.9 2 | pytz>=2013.6 3 | 4 | pycryptodome~=3.9.8 5 | pytest~=6.2.5 6 | setuptools==47.1.0 7 | sphinx-rtd-theme~=0.5.0 8 | sphinx_autodoc_typehints~=1.11.0 9 | Sphinx~=3.2.1 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Setup file for the HAFAS client.""" 3 | import os 4 | import sys 5 | 6 | 7 | from setuptools import setup,find_packages 8 | 9 | here = os.path.abspath(os.path.dirname(__file__)) 10 | 11 | with open(os.path.join(here, 'README.md'), encoding='utf-8') as readme: 12 | long_description = readme.read() 13 | 14 | if sys.argv[-1] == 'publish': 15 | os.system('python3 setup.py sdist upload') 16 | sys.exit() 17 | 18 | setup( 19 | name='pyhafas', 20 | version='0.6.1', 21 | description='Python client for HAFAS public transport APIs', 22 | long_description=long_description, 23 | long_description_content_type="text/markdown", 24 | url='https://github.com/FahrplanDatenGarten/pyhafas', 25 | download_url='https://github.com/FahrplanDatenGarten/pyhafas/releases', 26 | author='Ember Keske, Leona Maroni', 27 | author_email='dev@n0emis.eu, dev@leona.is', 28 | license='MIT', 29 | install_requires=['requests~=2.9', 30 | 'pytz>=2013.6'], 31 | packages=find_packages(include=['pyhafas', 'pyhafas.*']), 32 | zip_safe=True, 33 | classifiers=[ 34 | 'Development Status :: 4 - Beta', 35 | 'Intended Audience :: Developers', 36 | 'License :: OSI Approved :: MIT License', 37 | 'Operating System :: OS Independent', 38 | 'Programming Language :: Python :: 3.6', 39 | 'Programming Language :: Python :: 3.7', 40 | 'Programming Language :: Python :: 3.8', 41 | 'Topic :: Utilities', 42 | ], 43 | ) 44 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytest.importorskip("types") 4 | -------------------------------------------------------------------------------- /tests/base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FahrplanDatenGarten/pyhafas/4e91ffd80fe214ea6c0db2954160eb2813a2e215/tests/base/__init__.py -------------------------------------------------------------------------------- /tests/base/products_filter_test.py: -------------------------------------------------------------------------------- 1 | from pyhafas.profile.base import BaseFormatProductsFilterHelper 2 | 3 | 4 | class ProductsFilterTestProfile(BaseFormatProductsFilterHelper): 5 | availableProducts = { 6 | 'long_distance_express': [1, 2], 7 | 'long_distance': [4], 8 | 'regional_express': [8, 16], 9 | } 10 | 11 | defaultProducts = [ 12 | 'long_distance_express', 13 | 'long_distance', 14 | ] 15 | 16 | 17 | def test_base_products_filter_without_customization(): 18 | products_filter = ProductsFilterTestProfile().format_products_filter({}) 19 | assert products_filter == { 20 | 'type': 'PROD', 21 | 'mode': 'INC', 22 | 'value': "7" 23 | } 24 | 25 | 26 | def test_base_products_filter_with_customization(): 27 | products_filter = ProductsFilterTestProfile().format_products_filter({ 28 | "long_distance_express": False, 29 | "regional_express": True 30 | }) 31 | assert products_filter == { 32 | 'type': 'PROD', 33 | 'mode': 'INC', 34 | 'value': "28" 35 | } 36 | -------------------------------------------------------------------------------- /tests/db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FahrplanDatenGarten/pyhafas/4e91ffd80fe214ea6c0db2954160eb2813a2e215/tests/db/__init__.py -------------------------------------------------------------------------------- /tests/db/parsing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FahrplanDatenGarten/pyhafas/4e91ffd80fe214ea6c0db2954160eb2813a2e215/tests/db/parsing/__init__.py -------------------------------------------------------------------------------- /tests/db/parsing/departures_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | 4 | from pyhafas.profile import DBProfile 5 | from pyhafas.types.fptf import Station, StationBoardLeg 6 | 7 | from tests.types import PyTestHafasResponse 8 | 9 | 10 | def test_db_departures_parsing(): 11 | directory = os.path.dirname(os.path.realpath(__file__)) 12 | raw_hafas_json_file = open(directory + "/departures_raw.json", "r", encoding="utf8") 13 | hafas_response = PyTestHafasResponse(raw_hafas_json_file.read()) 14 | raw_hafas_json_file.close() 15 | correct_station_board_legs = [StationBoardLeg( 16 | id='1|200921|0|80|5082020', 17 | name='IC 2055', 18 | direction='Stralsund Hbf', 19 | station=Station( 20 | id='8098160', 21 | lid='A=1@O=Berlin Hbf (tief)@X=13369549@Y=52525589@U=80@L=8098160@', 22 | name='Berlin Hbf (tief)', 23 | latitude=52.525589, 24 | longitude=13.369549), 25 | date_time=DBProfile().timezone.localize(datetime.datetime(2020, 8, 5, 18, 16)), 26 | cancelled=False, 27 | delay=datetime.timedelta( 28 | seconds=0), 29 | platform='6')] 30 | assert DBProfile().parse_station_board_request( 31 | hafas_response, "d") == correct_station_board_legs 32 | -------------------------------------------------------------------------------- /tests/db/parsing/journey_raw.json: -------------------------------------------------------------------------------- 1 | {"ver":"1.16","ext":"DB.R19.04.a","lang":"deu","id":"hs2efpp2k6ipp6cc","cInfo":{"code":"OK","url":"","msg":""},"svcResL":[{"meth":"Reconstruction","err":"OK","res":{"common":{"locL":[{"lid":"A=1@O=Siegburg/Bonn@X=7203029@Y=50793916@U=80@L=8005556@","type":"S","name":"Siegburg/Bonn","icoX":0,"extId":"8005556","state":"F","crd":{"x":7202616,"y":50794051,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":825},{"lid":"A=1@O=Troisdorf@X=7150892@Y=50813926@U=80@L=8000135@","type":"S","name":"Troisdorf","icoX":3,"extId":"8000135","state":"F","crd":{"x":7150712,"y":50813720,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":56}],"prodL":[{"name":"S 19","nameS":"19","number":"19","icoX":1,"cls":16,"oprX":0,"prodCtx":{"name":"S 19","num":"33301","line":"19","matchId":"19","catOut":"S","catOutS":"s","catOutL":"S-Bahn","catIn":"s","catCode":"4","admin":"8003S_"}},{"name":"S 19","nameS":"19","number":"19","icoX":1,"cls":16,"oprX":0,"prodCtx":{"name":"S 19","num":"33301","line":"19","lineId":"4_8003S__19","matchId":"19","catOut":"S","catOutS":"s","catOutL":"S-Bahn","catIn":"s","catCode":"4","admin":"8003S_"}},{"name":"S 12","nameS":"12","number":"12","icoX":1,"cls":16,"oprX":0,"prodCtx":{"name":"S 12","num":"33207","line":"12","lineId":"4_8003S__12","matchId":"12","catOut":"S","catOutS":"s","catOutL":"S-Bahn","catIn":"s","catCode":"4","admin":"8003S_"}},{"name":"S 19","nameS":"19","number":"19","icoX":1,"cls":16,"oprX":0,"prodCtx":{"name":"S 19","num":"33303","line":"19","lineId":"4_8003S__19","matchId":"19","catOut":"S","catOutS":"s","catOutL":"S-Bahn","catIn":"s","catCode":"4","admin":"8003S_"}},{"name":"S 19","nameS":"19","number":"19","icoX":1,"cls":16,"oprX":0,"prodCtx":{"name":"S 19","num":"33307","line":"19","lineId":"4_8003S__19","matchId":"19","catOut":"S","catOutS":"s","catOutL":"S-Bahn","catIn":"s","catCode":"4","admin":"8003S_"}},{"name":"S 12","nameS":"12","number":"12","icoX":1,"cls":16,"oprX":0,"prodCtx":{"name":"S 12","num":"33217","line":"12","lineId":"4_8003S__12","matchId":"12","catOut":"S","catOutS":"s","catOutL":"S-Bahn","catIn":"s","catCode":"4","admin":"8003S_"}},{"name":"S 19","nameS":"19","number":"19","icoX":1,"cls":16,"oprX":0,"prodCtx":{"name":"S 19","num":"33309","line":"19","lineId":"4_8003S__19","matchId":"19","catOut":"S","catOutS":"s","catOutL":"S-Bahn","catIn":"s","catCode":"4","admin":"8003S_"}}],"polyL":[{"delta":true,"dim":3,"type":"WGS84","crdEncYX":"yu_uHkw}j@LV}[~h@qFbLoEjMiHjZyfAdaFVR","crdEncZ":"????????","crdEncS":"NNNMNLNN","crdEncF":"????????","ppLocRefL":[{"ppIdx":0,"locX":0},{"ppIdx":7,"locX":1}]}],"layerL":[{"id":"standard","name":"standard","index":0,"annoCnt":0}],"crdSysL":[{"id":"standard","index":0,"type":"WGS84","dim":3}],"opL":[{"name":"DB Regio AG NRW","icoX":2}],"remL":[{"type":"A","code":"FB","prio":260,"icoX":4,"txtN":"Fahrradmitnahme begrenzt möglich"},{"type":"A","code":"K2","prio":300,"icoX":5,"txtN":"nur 2. Klasse"},{"type":"A","code":"EH","prio":560,"icoX":6,"txtN":"Fahrzeuggebundene Einstiegshilfe vorhanden"}],"icoL":[{"res":"ICE"},{"res":"S"},{"res":"RE","txt":"DB Regio AG NRW"},{"res":"Bus"},{"res":"attr_bike"},{"res":"attr_2nd"},{"res":"attr_info"}]},"outConL":[{"cid":"DirectConReq","date":"20200808","dur":"000500","chg":0,"sDays":{"sDaysR":"fährt täglich ","sDaysB":"FFFFFFFFFFFFFFFFFFFFFFFFA040858207EFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0"},"dep":{"locX":0,"idx":1,"dProdX":0,"dPlatfS":"1","dInR":true,"dTimeS":"150700","dTimeR":"151100","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},"arr":{"locX":1,"idx":2,"aPlatfS":"1","aOutR":true,"aTimeS":"151200","aTimeR":"151600","aProgType":"PROGNOSED","aTZOffset":120,"type":"N"},"secL":[{"type":"JNY","icoX":1,"dep":{"locX":0,"idx":1,"dProdX":0,"dPlatfS":"1","dInR":true,"dTimeS":"150700","dTimeR":"151100","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},"arr":{"locX":1,"idx":2,"aPlatfS":"1","aOutR":true,"aTimeS":"151200","aTimeR":"151600","aProgType":"PROGNOSED","aTZOffset":120,"type":"N"},"jny":{"jid":"1|227361|0|80|8082020","prodX":1,"dirTxt":"Sindorf","status":"P","isRchbl":true,"stopL":[{"locX":0,"idx":1,"dProdX":1,"dPlatfS":"1","dInR":true,"dTimeS":"150700","dTimeR":"151100","dProgType":"PROGNOSED","dDirTxt":"Sindorf","dTZOffset":120,"type":"N"},{"locX":1,"idx":2,"aProdX":1,"aPlatfS":"1","aOutR":true,"aTimeS":"151200","aTimeR":"151600","aProgType":"PROGNOSED","aTZOffset":120,"type":"N"}],"polyG":{"polyXL":[0],"layerX":0,"crdSysX":0},"freq":{"minC":10,"maxC":20,"numC":11,"jnyL":[{"jid":"1|226237|3|80|8082020","prodX":2,"dirTxt":"Köln-Ehrenfeld","stopL":[{"locX":0,"idx":1,"dProdX":2,"dPlatfS":"1","dTimeS":"151700","dDirTxt":"Köln-Ehrenfeld","dTZOffset":120,"type":"N"},{"locX":1,"idx":2,"aProdX":2,"aPlatfS":"1","aTimeS":"152200","aTZOffset":120,"type":"N"}],"ctxRecon":"T$A=1@O=Siegburg/Bonn@L=8005556@a=128@$A=1@O=Troisdorf@L=8000135@a=128@$202008081517$202008081522$S 12$$1$$$","msgL":[{"type":"REM","remX":0,"fLocX":0,"tLocX":1,"fIdx":1,"tIdx":2,"tagL":["RES_JNY_DTL_L"]},{"type":"REM","remX":1,"fLocX":0,"tLocX":1,"fIdx":1,"tIdx":2,"tagL":["RES_JNY_DTL_L"]}],"subscr":"F"},{"jid":"1|227069|6|80|8082020","prodX":3,"dirTxt":"Düren","stopL":[{"locX":0,"idx":9,"dProdX":3,"dPlatfS":"1","dTimeS":"152700","dDirTxt":"Düren","dTZOffset":120,"type":"N"},{"locX":1,"idx":10,"aProdX":3,"aPlatfS":"1","aTimeS":"153200","aTZOffset":120,"type":"N"}],"ctxRecon":"T$A=1@O=Siegburg/Bonn@L=8005556@a=128@$A=1@O=Troisdorf@L=8000135@a=128@$202008081527$202008081532$S 19$$1$$$","msgL":[{"type":"REM","remX":0,"fLocX":0,"tLocX":1,"fIdx":9,"tIdx":10,"tagL":["RES_JNY_DTL_L"]},{"type":"REM","remX":1,"fLocX":0,"tLocX":1,"fIdx":9,"tIdx":10,"tagL":["RES_JNY_DTL_L"]},{"type":"REM","remX":2,"fLocX":0,"tLocX":1,"fIdx":9,"tIdx":10,"tagL":["RES_JNY_DTL"]}],"subscr":"F"},{"jid":"1|227363|0|80|8082020","prodX":4,"dirTxt":"Düren","stopL":[{"locX":0,"idx":3,"dProdX":4,"dPlatfS":"1","dTimeS":"154700","dDirTxt":"Düren","dTZOffset":120,"type":"N"},{"locX":1,"idx":4,"aProdX":4,"aPlatfS":"1","aTimeS":"155200","aTZOffset":120,"type":"N"}],"ctxRecon":"T$A=1@O=Siegburg/Bonn@L=8005556@a=128@$A=1@O=Troisdorf@L=8000135@a=128@$202008081547$202008081552$S 19$$1$$$","msgL":[{"type":"REM","remX":0,"fLocX":0,"tLocX":1,"fIdx":3,"tIdx":4,"tagL":["RES_JNY_DTL_L"]},{"type":"REM","remX":1,"fLocX":0,"tLocX":1,"fIdx":3,"tIdx":4,"tagL":["RES_JNY_DTL_L"]},{"type":"REM","remX":2,"fLocX":0,"tLocX":1,"fIdx":3,"tIdx":4,"tagL":["RES_JNY_DTL"]}],"subscr":"F"},{"jid":"1|226236|4|80|8082020","prodX":5,"dirTxt":"Köln-Ehrenfeld","stopL":[{"locX":0,"idx":10,"dProdX":5,"dPlatfS":"1","dTimeS":"155700","dDirTxt":"Köln-Ehrenfeld","dTZOffset":120,"type":"N"},{"locX":1,"idx":11,"aProdX":5,"aPlatfS":"1","aTimeS":"160200","aTZOffset":120,"type":"N"}],"ctxRecon":"T$A=1@O=Siegburg/Bonn@L=8005556@a=128@$A=1@O=Troisdorf@L=8000135@a=128@$202008081557$202008081602$S 12$$1$$$","msgL":[{"type":"REM","remX":0,"fLocX":0,"tLocX":1,"fIdx":10,"tIdx":11,"tagL":["RES_JNY_DTL_L"]},{"type":"REM","remX":1,"fLocX":0,"tLocX":1,"fIdx":10,"tIdx":11,"tagL":["RES_JNY_DTL_L"]}],"subscr":"F"},{"jid":"1|227361|1|80|8082020","prodX":6,"dirTxt":"Sindorf","stopL":[{"locX":0,"idx":1,"dProdX":6,"dPlatfS":"1","dTimeS":"160700","dDirTxt":"Sindorf","dTZOffset":120,"type":"N"},{"locX":1,"idx":2,"aProdX":6,"aPlatfS":"1","aTimeS":"161200","aTZOffset":120,"type":"N"}],"ctxRecon":"T$A=1@O=Siegburg/Bonn@L=8005556@a=128@$A=1@O=Troisdorf@L=8000135@a=128@$202008081607$202008081612$S 19$$1$$$","msgL":[{"type":"REM","remX":0,"fLocX":0,"tLocX":1,"fIdx":1,"tIdx":2,"tagL":["RES_JNY_DTL_L"]},{"type":"REM","remX":1,"fLocX":0,"tLocX":1,"fIdx":1,"tIdx":2,"tagL":["RES_JNY_DTL_L"]},{"type":"REM","remX":2,"fLocX":0,"tLocX":1,"fIdx":1,"tIdx":2,"tagL":["RES_JNY_DTL"]}],"subscr":"F"}]},"ctxRecon":"T$A=1@O=Siegburg/Bonn@L=8005556@a=128@$A=1@O=Troisdorf@L=8000135@a=128@$202008081507$202008081512$S 19$$1$$$","msgL":[{"type":"REM","remX":0,"fLocX":0,"tLocX":1,"fIdx":1,"tIdx":2,"tagL":["RES_JNY_DTL_L"]},{"type":"REM","remX":1,"fLocX":0,"tLocX":1,"fIdx":1,"tIdx":2,"tagL":["RES_JNY_DTL_L"]},{"type":"REM","remX":2,"fLocX":0,"tLocX":1,"fIdx":1,"tIdx":2,"tagL":["RES_JNY_DTL"]}],"subscr":"F"},"resState":"N","resRecommendation":"N"}],"ctxRecon":"¶HKI¶T$A=1@O=Siegburg/Bonn@L=8005556@a=128@$A=1@O=Troisdorf@L=8000135@a=128@$202008081507$202008081512$S 19$$1$$$","freq":{"minC":10},"conSubscr":"N","resState":"N","resRecommendation":"N","recState":"C","sotRating":0,"isSotCon":false,"showARSLink":false,"sotCtxt":{"cnLocX":0,"calcDate":"20200808","jid":"1|227361|0|80|-1","locMode":"FROM_START","pLocX":0,"reqMode":"UNKNOWN","sectX":0,"calcTime":"150450"},"cksum":"d539411f_3"}],"fpB":"20191215","fpE":"20201212","bfATS":-1,"bfIOSTS":-1,"planrtTS":"1596891755"}}]} 2 | -------------------------------------------------------------------------------- /tests/db/parsing/journey_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | 4 | from pyhafas.profile import DBProfile 5 | from pyhafas.types.fptf import Journey, Leg, Mode, Station, Stopover, Remark 6 | 7 | from tests.types import PyTestHafasResponse 8 | 9 | 10 | def test_db_journey_parsing(): 11 | directory = os.path.dirname(os.path.realpath(__file__)) 12 | raw_hafas_json_file = open(directory + "/journey_raw.json", "r", encoding="utf8") 13 | hafas_response = PyTestHafasResponse(raw_hafas_json_file.read()) 14 | raw_hafas_json_file.close() 15 | correct_journey = Journey( 16 | id='¶HKI¶T$A=1@O=Siegburg/Bonn@L=8005556@a=128@$A=1@O=Troisdorf@L=8000135@a=128@$202008081507$202008081512$S 19$$1$$$', 17 | date=datetime.date(2020, 8, 8), 18 | duration=datetime.timedelta(seconds=300), 19 | legs=[Leg( 20 | id='1|227361|0|80|8082020', 21 | origin=Station( 22 | id='8005556', 23 | lid='A=1@O=Siegburg/Bonn@X=7203029@Y=50793916@U=80@L=8005556@', 24 | name='Siegburg/Bonn', 25 | latitude=50.793916, 26 | longitude=7.203029), 27 | destination=Station( 28 | id='8000135', 29 | lid='A=1@O=Troisdorf@X=7150892@Y=50813926@U=80@L=8000135@', 30 | name='Troisdorf', 31 | latitude=50.813926, 32 | longitude=7.150892), 33 | departure=DBProfile().timezone.localize(datetime.datetime(2020, 8, 8, 15, 7)), 34 | arrival=DBProfile().timezone.localize(datetime.datetime(2020, 8, 8, 15, 12)), 35 | mode=Mode.TRAIN, 36 | name='S 19', 37 | cancelled=False, distance=None, 38 | departure_delay=datetime.timedelta(seconds=240), 39 | departure_platform='1', 40 | arrival_delay=datetime.timedelta(seconds=240), 41 | arrival_platform='1', 42 | stopovers=[Stopover( 43 | stop=Station( 44 | id='8005556', 45 | lid='A=1@O=Siegburg/Bonn@X=7203029@Y=50793916@U=80@L=8005556@', 46 | name='Siegburg/Bonn', 47 | latitude=50.793916, 48 | longitude=7.203029 49 | ), 50 | cancelled=False, 51 | arrival=None, 52 | arrival_delay=None, 53 | arrival_platform=None, 54 | departure=DBProfile().timezone.localize(datetime.datetime(2020, 8, 8, 15, 7)), 55 | departure_delay=datetime.timedelta(seconds=240), 56 | departure_platform='1', 57 | remarks=[], 58 | ), Stopover( 59 | stop=Station( 60 | id='8000135', 61 | lid='A=1@O=Troisdorf@X=7150892@Y=50813926@U=80@L=8000135@', 62 | name='Troisdorf', 63 | latitude=50.813926, 64 | longitude=7.150892 65 | ), 66 | cancelled=False, 67 | arrival=DBProfile().timezone.localize(datetime.datetime(2020, 8, 8, 15, 12)), 68 | arrival_delay=datetime.timedelta(seconds=240), 69 | arrival_platform='1', 70 | departure=None, 71 | departure_delay=None, 72 | departure_platform=None, 73 | remarks=[], 74 | ), 75 | ], 76 | remarks=[ 77 | Remark( 78 | remark_type='A', 79 | code='FB', 80 | subject=None, 81 | text='Fahrradmitnahme begrenzt möglich', 82 | priority=260, 83 | trip_id=None 84 | ), 85 | Remark( 86 | remark_type='A', 87 | code='K2', 88 | subject=None, 89 | text='nur 2. Klasse', 90 | priority=300, 91 | trip_id=None 92 | ), 93 | Remark( 94 | remark_type='A', 95 | code='EH', 96 | subject=None, 97 | text='Fahrzeuggebundene Einstiegshilfe vorhanden', 98 | priority=560, 99 | trip_id=None 100 | ) 101 | ])] 102 | ) 103 | assert DBProfile().parse_journey_request(hafas_response) == correct_journey 104 | -------------------------------------------------------------------------------- /tests/db/parsing/journeys_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | 4 | from pyhafas.profile import DBProfile 5 | from pyhafas.types.fptf import Journey, Leg, Mode, Station, Stopover, Remark 6 | 7 | from tests.types import PyTestHafasResponse 8 | 9 | 10 | def test_db_journeys_parsing(): 11 | directory = os.path.dirname(os.path.realpath(__file__)) 12 | raw_hafas_json_file = open(directory + "/journeys_raw.json", "r", encoding="utf8") 13 | hafas_response = PyTestHafasResponse(raw_hafas_json_file.read()) 14 | raw_hafas_json_file.close() 15 | correct_journeys = [Journey( 16 | id='¶HKI¶T$A=1@O=Siegburg/Bonn@L=8005556@a=128@$A=1@O=Troisdorf@L=8000135@a=128@$202008061207$202008061212$S 19$$1$$$', 17 | date=datetime.date(2020, 8, 6), 18 | duration=datetime.timedelta(seconds=300), 19 | legs=[Leg( 20 | id='1|226393|0|80|6082020', 21 | origin=Station( 22 | id='8005556', 23 | lid='A=1@O=Siegburg/Bonn@X=7203029@Y=50793916@U=80@L=8005556@', 24 | name='Siegburg/Bonn', 25 | latitude=50.793916, 26 | longitude=7.203029), 27 | destination=Station( 28 | id='8000135', 29 | lid='A=1@O=Troisdorf@X=7150892@Y=50813926@U=80@L=8000135@', 30 | name='Troisdorf', 31 | latitude=50.813926, 32 | longitude=7.150892), 33 | departure=DBProfile().timezone.localize(datetime.datetime(2020, 8, 6, 12, 7)), 34 | arrival=DBProfile().timezone.localize(datetime.datetime(2020, 8, 6, 12, 12)), 35 | mode=Mode.TRAIN, 36 | name='S 19', 37 | cancelled=False, 38 | distance=None, 39 | departure_delay=None, 40 | departure_platform='1', 41 | arrival_delay=datetime.timedelta(0), 42 | arrival_platform='1', 43 | stopovers=[Stopover( 44 | stop=Station( 45 | id='8005556', 46 | lid='A=1@O=Siegburg/Bonn@X=7203029@Y=50793916@U=80@L=8005556@', 47 | name='Siegburg/Bonn', 48 | latitude=50.793916, 49 | longitude=7.203029), 50 | cancelled=False, 51 | arrival=None, 52 | arrival_delay=None, 53 | arrival_platform=None, 54 | departure=DBProfile().timezone.localize(datetime.datetime(2020, 8, 6, 12, 7)), 55 | departure_delay=None, 56 | departure_platform='1', 57 | remarks=[]), 58 | Stopover( 59 | stop=Station( 60 | id='8000135', 61 | lid='A=1@O=Troisdorf@X=7150892@Y=50813926@U=80@L=8000135@', 62 | name='Troisdorf', 63 | latitude=50.813926, 64 | longitude=7.150892), 65 | cancelled=False, 66 | arrival=DBProfile().timezone.localize(datetime.datetime(2020, 8, 6, 12, 12)), 67 | arrival_delay=datetime.timedelta(0), 68 | arrival_platform='1', 69 | departure=None, 70 | departure_delay=None, 71 | departure_platform=None, 72 | remarks=[], 73 | ) 74 | ], 75 | remarks=[ 76 | Remark( 77 | remark_type='A', 78 | code='FB', 79 | subject=None, 80 | text='Fahrradmitnahme begrenzt möglich', 81 | priority=260, 82 | trip_id=None 83 | ), 84 | Remark( 85 | remark_type='A', 86 | code='K2', 87 | subject=None, 88 | text='nur 2. Klasse', 89 | priority=300, 90 | trip_id=None 91 | ), 92 | Remark( 93 | remark_type='A', 94 | code='EH', 95 | subject=None, 96 | text='Fahrzeuggebundene Einstiegshilfe vorhanden', 97 | priority=560, 98 | trip_id=None 99 | ) 100 | ] 101 | )], 102 | )] 103 | assert DBProfile().parse_journeys_request(hafas_response) == correct_journeys 104 | -------------------------------------------------------------------------------- /tests/db/parsing/locations_raw.json: -------------------------------------------------------------------------------- 1 | {"ver":"1.16","ext":"DB.R19.04.a","lang":"deu","id":"ks28mrfg42q8g4w8","cInfo":{"code":"OK","url":"","msg":""},"svcResL":[{"meth":"LocMatch","err":"OK","res":{"common":{"locL":[],"prodL":[{"name":"ICE","icoX":0,"cls":1},{"name":"THA","icoX":1,"cls":1},{"name":"EC","icoX":2,"cls":2},{"name":"IC","icoX":3,"cls":2},{"name":"NJ","icoX":4,"cls":2},{"name":"BTE","icoX":5,"cls":4},{"name":"FLX","icoX":5,"cls":4},{"name":"MSM","icoX":5,"cls":4},{"name":"UEX","icoX":5,"cls":4},{"name":"Bus SEV","nameS":"SEV","icoX":6,"cls":8,"prodCtx":{"lineId":"3_B2_____SEV!!774120!!5667129"}},{"name":"RB","icoX":7,"cls":8},{"name":"RB","icoX":8,"cls":8},{"name":"RE","icoX":7,"cls":8},{"name":"RE","icoX":9,"cls":8},{"name":"S 6","nameS":"6","icoX":10,"cls":16,"prodCtx":{"lineId":"4_800337_6"}},{"name":"S 11","nameS":"11","icoX":10,"cls":16,"prodCtx":{"lineId":"4_8003S__11"}},{"name":"S 12","nameS":"12","icoX":10,"cls":16,"prodCtx":{"lineId":"4_8003S__12"}},{"name":"S 13","nameS":"13","icoX":10,"cls":16,"prodCtx":{"lineId":"4_8003S__13"}},{"name":"S 19","nameS":"19","icoX":10,"cls":16,"prodCtx":{"lineId":"4_8003S__19"}},{"name":"Bus 172","nameS":"172","icoX":6,"cls":32,"prodCtx":{"lineId":"5_vrsKVB_172"}},{"name":"Bus 173","nameS":"173","icoX":6,"cls":32,"prodCtx":{"lineId":"5_vrsKVB_173"}},{"name":"STB 5","nameS":"5","icoX":11,"cls":256,"prodCtx":{"lineId":"8_B2_____5"}},{"name":"STR 5","nameS":"5","icoX":12,"cls":256,"prodCtx":{"lineId":"8_vrsKVB_5"}},{"name":"STR 16","nameS":"16","icoX":12,"cls":256,"prodCtx":{"lineId":"8_vrsKVB_16"}},{"name":"STR 18","nameS":"18","icoX":12,"cls":256,"prodCtx":{"lineId":"8_vrsKVB_18"}},{"name":"NX","icoX":7,"cls":8},{"name":"Bus 124","nameS":"124","icoX":6,"cls":32,"prodCtx":{"lineId":"5_vrsKVB_124"}},{"name":"Bus 132","nameS":"132","icoX":6,"cls":32,"prodCtx":{"lineId":"5_vrsKVB_132"}},{"name":"Bus 133","nameS":"133","icoX":6,"cls":32,"prodCtx":{"lineId":"5_vrsKVB_133"}},{"name":"Bus 171","nameS":"171","icoX":6,"cls":32,"prodCtx":{"lineId":"5_vrsKVB_171"}},{"name":"Bus 250","nameS":"250","icoX":6,"cls":32,"prodCtx":{"lineId":"5_vrs26__250"}},{"name":"Bus 260","nameS":"260","icoX":6,"cls":32,"prodCtx":{"lineId":"5_vrsRVW_260"}},{"name":"Bus 978","nameS":"978","icoX":6,"cls":32,"prodCtx":{"lineId":"5_vrsREV_978"}},{"name":"Bus N26","nameS":"N26","icoX":6,"cls":32,"prodCtx":{"lineId":"5_vrsRVW_N26"}},{"name":"Bus SB40","nameS":"SB40","icoX":6,"cls":32,"prodCtx":{"lineId":"5_vrsRVB_SB40"}}],"polyL":[],"layerL":[{"id":"standard","name":"standard","index":0,"annoCnt":0}],"crdSysL":[{"id":"standard","index":0,"type":"WGS84","dim":3}],"opL":[],"remL":[],"icoL":[{"res":"ICE"},{"res":"THA"},{"res":"EC"},{"res":"IC"},{"res":"NJ"},{"res":"DPF"},{"res":"Bus"},{"res":"DPN"},{"res":"RB"},{"res":"RE"},{"res":"S"},{"res":"STB"},{"res":"STR"}]},"match":{"field":"S","state":"L","locL":[{"lid":"A=1@O=Köln Hbf@X=6958730@Y=50943029@U=80@L=008000207@B=1@p=1596188796@","type":"S","name":"Köln Hbf","icoX":0,"extId":"008000207","state":"F","crd":{"x":6959197,"y":50942823,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":319,"pRefL":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24],"wt":30770},{"lid":"A=1@O=KÖLN@X=6967206@Y=50941312@U=80@L=008096022@B=1@p=1596188796@","type":"S","name":"KÖLN","icoX":0,"extId":"008096022","state":"F","crd":{"x":6967206,"y":50941312,"type":"WGS84","layerX":0,"crdSysX":0},"meta":true,"pCls":319,"pRefL":[0,1,2,3,4,5,6,7,8,9,25,10,11,12,13,14,15,16,17,18,26,27,28,29,19,20,30,31,32,33,34,21,22,23,24],"wt":30770}]}}}]} 2 | -------------------------------------------------------------------------------- /tests/db/parsing/locations_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pyhafas.profile import DBProfile 4 | from pyhafas.types.fptf import Station 5 | 6 | from tests.types import PyTestHafasResponse 7 | 8 | 9 | def test_db_locations_parsing(): 10 | directory = os.path.dirname(os.path.realpath(__file__)) 11 | raw_hafas_json_file = open(directory + "/locations_raw.json", "r", encoding="utf8") 12 | hafas_response = PyTestHafasResponse(raw_hafas_json_file.read()) 13 | raw_hafas_json_file.close() 14 | correct_locations = [ 15 | Station( 16 | id='008000207', 17 | lid='A=1@O=Köln Hbf@X=6958730@Y=50943029@U=80@L=008000207@B=1@p=1596188796@', 18 | name='Köln Hbf', 19 | latitude=50.942823, 20 | longitude=6.959197 21 | ), Station( 22 | id="008096022", 23 | lid='A=1@O=KÖLN@X=6967206@Y=50941312@U=80@L=008096022@B=1@p=1596188796@', 24 | name='KÖLN', 25 | latitude=50.941312, 26 | longitude=6.967206) 27 | ] 28 | assert DBProfile().parse_location_request(hafas_response) == correct_locations 29 | -------------------------------------------------------------------------------- /tests/db/request/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FahrplanDatenGarten/pyhafas/4e91ffd80fe214ea6c0db2954160eb2813a2e215/tests/db/request/__init__.py -------------------------------------------------------------------------------- /tests/db/request/arrivals_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import pytest 3 | 4 | from pyhafas import HafasClient 5 | from pyhafas.profile import DBProfile 6 | 7 | 8 | @pytest.mark.skip() 9 | def test_db_arrivals_request(): 10 | client = HafasClient(DBProfile()) 11 | arrivals = client.arrivals( 12 | station="8011160", 13 | date=datetime.datetime.now(), 14 | max_trips=2 15 | ) 16 | assert len(arrivals) <= 2 17 | -------------------------------------------------------------------------------- /tests/db/request/departures_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import pytest 3 | 4 | from pyhafas import HafasClient 5 | from pyhafas.profile import DBProfile 6 | 7 | 8 | @pytest.mark.skip() 9 | def test_db_departures_request(): 10 | client = HafasClient(DBProfile()) 11 | departures = client.departures( 12 | station="8011160", 13 | date=datetime.datetime.now(), 14 | max_trips=2 15 | ) 16 | assert len(departures) <= 2 17 | -------------------------------------------------------------------------------- /tests/db/request/journey_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import pytest 3 | 4 | from pyhafas import HafasClient 5 | from pyhafas.profile import DBProfile 6 | from pyhafas.types.fptf import Journey 7 | 8 | 9 | @pytest.mark.skip() 10 | def test_db_journey_request(): 11 | profile = DBProfile() 12 | client = HafasClient(profile) 13 | journeys = client.journeys( 14 | destination="8011306", 15 | origin="8011160", 16 | date=datetime.datetime.now(), 17 | min_change_time=0, 18 | max_changes=-1 19 | ) 20 | assert journeys 21 | 22 | journey = client.journey(journey=journeys[0].id) 23 | assert isinstance(journey, Journey) 24 | -------------------------------------------------------------------------------- /tests/db/request/locations_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pyhafas import HafasClient 4 | from pyhafas.profile import DBProfile 5 | 6 | 7 | @pytest.mark.skip() 8 | def test_db_locations_request(): 9 | client = HafasClient(DBProfile()) 10 | locations = client.locations(term="Köln Messe/Deutz") 11 | assert len(locations) >= 1 12 | -------------------------------------------------------------------------------- /tests/db/request/nearby_test.py: -------------------------------------------------------------------------------- 1 | from pyhafas import HafasClient 2 | from pyhafas.profile import DBProfile 3 | from pyhafas.types.nearby import LatLng 4 | from tests.distance import calculate_distance_in_meters 5 | import pytest 6 | 7 | 8 | @pytest.mark.skip() 9 | def test_db_nearby_request(): 10 | pos = LatLng(52.523765, 13.369948) 11 | client = HafasClient(DBProfile()) 12 | stations = client.nearby(pos) 13 | assert stations 14 | assert calculate_distance_in_meters(pos.latitude, pos.longitude, stations[0].latitude, stations[0].longitude) < 200 15 | -------------------------------------------------------------------------------- /tests/distance.py: -------------------------------------------------------------------------------- 1 | from math import radians, sin, cos, sqrt, atan2 2 | 3 | 4 | def calculate_distance_in_meters(ilat1: float, ilon1: float, ilat2: float, ilon2: float) -> float: 5 | # credits to https://stackoverflow.com/a/19412565/237312 6 | # used to not import external libs 7 | 8 | # Approximate radius of earth in meters 9 | r = 6373000.0 10 | 11 | lat1 = radians(ilat1) 12 | lon1 = radians(ilon1) 13 | lat2 = radians(ilat2) 14 | lon2 = radians(ilon2) 15 | 16 | dlon = lon2 - lon1 17 | dlat = lat2 - lat1 18 | 19 | a = sin(dlat / 2)**2 + cos(lat1) * cos(lat2) * sin(dlon / 2)**2 20 | c = 2 * atan2(sqrt(a), sqrt(1 - a)) 21 | 22 | distance = r * c 23 | 24 | return distance 25 | -------------------------------------------------------------------------------- /tests/kvb/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FahrplanDatenGarten/pyhafas/4e91ffd80fe214ea6c0db2954160eb2813a2e215/tests/kvb/__init__.py -------------------------------------------------------------------------------- /tests/kvb/parsing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FahrplanDatenGarten/pyhafas/4e91ffd80fe214ea6c0db2954160eb2813a2e215/tests/kvb/parsing/__init__.py -------------------------------------------------------------------------------- /tests/kvb/parsing/departures_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | 4 | from pyhafas.profile.kvb import KVBProfile 5 | from pyhafas.types.fptf import Station, StationBoardLeg 6 | 7 | from tests.types import PyTestHafasResponse 8 | 9 | 10 | def test_kvb_departures_parsing(): 11 | directory = os.path.dirname(os.path.realpath(__file__)) 12 | raw_hafas_json_file = open(directory + "/departures_raw.json", "r", encoding="utf8") 13 | hafas_response = PyTestHafasResponse(raw_hafas_json_file.read()) 14 | raw_hafas_json_file.close() 15 | correct_station_board_legs = [StationBoardLeg( 16 | id='1|119823|0|1|24052023', 17 | name='609', 18 | direction='Bonn Brüser Berg Hardthöhe/Südwache', 19 | station=Station( 20 | id='300068712', 21 | lid='A=1@O=Bonn Hbf@X=7099420@Y=50731810@U=1@L=300068712@', 22 | name='Bonn Hbf', 23 | latitude=50.731810, 24 | longitude=7.099420), 25 | date_time=KVBProfile().timezone.localize(datetime.datetime(2023, 5, 24, 15, 57)), 26 | cancelled=False, 27 | delay=datetime.timedelta( 28 | seconds=600), 29 | platform='D2')] 30 | assert KVBProfile().parse_station_board_request( 31 | hafas_response, "d") == correct_station_board_legs 32 | -------------------------------------------------------------------------------- /tests/kvb/request/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FahrplanDatenGarten/pyhafas/4e91ffd80fe214ea6c0db2954160eb2813a2e215/tests/kvb/request/__init__.py -------------------------------------------------------------------------------- /tests/kvb/request/arrivals_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from pyhafas import HafasClient 4 | from pyhafas.profile import KVBProfile 5 | 6 | 7 | def test_kvb_arrivals_request(): 8 | client = HafasClient(KVBProfile()) 9 | arrivals = client.arrivals( 10 | station="300068712", 11 | date=datetime.datetime.now(), 12 | max_trips=5 13 | ) 14 | assert arrivals 15 | -------------------------------------------------------------------------------- /tests/kvb/request/departures_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from pyhafas import HafasClient 4 | from pyhafas.profile import KVBProfile 5 | 6 | 7 | def test_kvb_departures_request(): 8 | client = HafasClient(KVBProfile()) 9 | departures = client.departures( 10 | station="300068712", 11 | date=datetime.datetime.now(), 12 | max_trips=5 13 | ) 14 | assert departures 15 | -------------------------------------------------------------------------------- /tests/kvb/request/journey_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from pyhafas import HafasClient 4 | from pyhafas.profile import KVBProfile 5 | from pyhafas.types.fptf import Journey 6 | 7 | 8 | def test_kvb_journey_request(): 9 | client = HafasClient(KVBProfile()) 10 | journeys = client.journeys( 11 | destination="900000687", 12 | origin="900000008", 13 | date=datetime.datetime.now(), 14 | min_change_time=0, 15 | max_changes=-1 16 | ) 17 | 18 | journey = client.journey( 19 | journey=journeys[0].id 20 | ) 21 | assert isinstance(journey, Journey) 22 | -------------------------------------------------------------------------------- /tests/kvb/request/locations_test.py: -------------------------------------------------------------------------------- 1 | from pyhafas import HafasClient 2 | from pyhafas.profile import KVBProfile 3 | 4 | 5 | def test_kvb_locations_request(): 6 | client = HafasClient(KVBProfile()) 7 | locations = client.locations(term="Bonn Hbf") 8 | assert len(locations) >= 1 9 | -------------------------------------------------------------------------------- /tests/kvb/request/nearby_test.py: -------------------------------------------------------------------------------- 1 | from pyhafas import HafasClient 2 | from pyhafas.profile import KVBProfile 3 | from pyhafas.types.nearby import LatLng 4 | from tests.distance import calculate_distance_in_meters 5 | 6 | 7 | def test_kvb_nearby_request(): 8 | pos = LatLng(50.940614, 6.958120) 9 | client = HafasClient(KVBProfile()) 10 | stations = client.nearby(pos) 11 | assert stations 12 | assert calculate_distance_in_meters(pos.latitude, pos.longitude, stations[0].latitude, stations[0].longitude) < 200 13 | -------------------------------------------------------------------------------- /tests/nasa/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FahrplanDatenGarten/pyhafas/4e91ffd80fe214ea6c0db2954160eb2813a2e215/tests/nasa/__init__.py -------------------------------------------------------------------------------- /tests/nasa/requests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FahrplanDatenGarten/pyhafas/4e91ffd80fe214ea6c0db2954160eb2813a2e215/tests/nasa/requests/__init__.py -------------------------------------------------------------------------------- /tests/nasa/requests/arrivals_test.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from pyhafas import HafasClient 4 | from pyhafas.profile import NASAProfile 5 | 6 | 7 | # Test arrivals request for NASAProfile of station 'Universitätsbibliothek' with id '7482' 8 | 9 | def test_nasa_arrivals_request(): 10 | client = HafasClient(NASAProfile()) 11 | arrivals = client.arrivals( 12 | station="7482", 13 | date=datetime.now(), 14 | max_trips=5 15 | ) 16 | assert arrivals 17 | -------------------------------------------------------------------------------- /tests/nasa/requests/departures_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from pyhafas import HafasClient 4 | from pyhafas.profile import NASAProfile 5 | 6 | 7 | def test_nasa_departures_request(): 8 | client = HafasClient(NASAProfile()) 9 | departures = client.departures( 10 | station="7482", 11 | date=datetime.datetime.now(), 12 | max_trips=5 13 | ) 14 | assert departures 15 | -------------------------------------------------------------------------------- /tests/nasa/requests/journey_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from pyhafas import HafasClient 4 | from pyhafas.profile import NASAProfile 5 | from pyhafas.types.fptf import Journey 6 | 7 | 8 | # Test journey request with NASA profile 9 | def test_nasa_journey_request(): 10 | profile = NASAProfile() 11 | client = HafasClient(profile) 12 | journeys = client.journeys( 13 | origin='90053', 14 | destination='5232', 15 | date=datetime.datetime.now(), 16 | min_change_time=0, 17 | max_changes=-1 18 | ) 19 | assert journeys 20 | journey = client.journey(journey=journeys[0].id) 21 | assert isinstance(journey, Journey) 22 | -------------------------------------------------------------------------------- /tests/nasa/requests/locations_test.py: -------------------------------------------------------------------------------- 1 | from pyhafas import HafasClient 2 | from pyhafas.profile import NASAProfile 3 | 4 | 5 | # Test Location request of NASA by searching for 'Universtitätsbibliothek, Magdeburg', assert that at least one result is returned 6 | def test_locations_request_nasa(): 7 | client = HafasClient(NASAProfile()) 8 | result = client.locations('Halle (Saale) Hbf') 9 | assert len(result) > 0 10 | -------------------------------------------------------------------------------- /tests/nasa/requests/nearby_test.py: -------------------------------------------------------------------------------- 1 | from pyhafas import HafasClient 2 | from pyhafas.profile import NASAProfile 3 | from pyhafas.types.nearby import LatLng 4 | from tests.distance import calculate_distance_in_meters 5 | 6 | 7 | def test_nasa_nearby_request(): 8 | pos = LatLng(52.131019, 11.622818) 9 | client = HafasClient(NASAProfile()) 10 | stations = client.nearby(pos) 11 | assert stations 12 | assert calculate_distance_in_meters(pos.latitude, pos.longitude, stations[0].latitude, stations[0].longitude) < 200 13 | -------------------------------------------------------------------------------- /tests/nvv/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FahrplanDatenGarten/pyhafas/4e91ffd80fe214ea6c0db2954160eb2813a2e215/tests/nvv/__init__.py -------------------------------------------------------------------------------- /tests/nvv/parsing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FahrplanDatenGarten/pyhafas/4e91ffd80fe214ea6c0db2954160eb2813a2e215/tests/nvv/parsing/__init__.py -------------------------------------------------------------------------------- /tests/nvv/parsing/departures_raw.json: -------------------------------------------------------------------------------- 1 | {"ver":"1.39","ext":"NVV.6.0","lang":"deu","id":"nm6x8zq226xbw24k","err":"OK","graph":{"id":"standard","index":0},"subGraph":{"id":"global","index":0},"view":{"id":"standard","index":0,"type":"WGS84"},"svcResL":[{"id":"1|7|","meth":"StationBoard","err":"OK","res":{"common":{"locL":[{"lid":"A=1@O=Kassel Keilsbergstraße@X=9455942@Y=51279199@U=80@L=2200048@","type":"S","name":"Kassel Keilsbergstraße","icoX":6,"extId":"2200048","state":"F","crd":{"x":9455942,"y":51279199,"floor":0},"pCls":1636,"pRefL":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15],"msgL":[{"type":"REM","remX":0,"sty":"I","dspl":"U","tagL":["RES_LOC_L"],"sort":684195840}],"gidL":["A×de:06611:200048"],"chgTime":"000500"},{"lid":"A=1@O=Baunatal-Großenritte Bahnhof@X=9391130@Y=51251890@U=80@L=2203069@","type":"S","name":"Baunatal-Großenritte Bahnhof","icoX":6,"extId":"2203069","state":"F","crd":{"x":9391130,"y":51251890,"floor":0},"pCls":672,"msgL":[{"type":"REM","remX":1,"sty":"I","dspl":"U","tagL":["RES_LOC_L"],"sort":684195840}],"gidL":["A×de:06633:203069"],"chgTime":"000500"}],"prodL":[{"pid":"L::5::Tram::B3736316743::de:nvv:Tram|5:::*","name":"Tram 5","nameS":"5","number":"5","icoX":0,"cls":32,"oprX":0,"prodCtx":{"name":"Tram 5 ","num":"677","line":"5","lineId":"de:nvv:Tram|5:","matchId":"5","catOut":"Tram ","catOutS":"T","catOutL":"Straßenbahn","catIn":"T","catCode":"5","admin":"KTR---"},"himIdL":["HIM_FREETEXT_7223","HIM_FREETEXT_7229"]},{"name":"RB5","nameS":"RB5","icoX":3,"cls":4,"prodCtx":{"name":"RB5","line":"RB5","lineId":"de:nvv:RB|RB5","catOut":"RB ","catOutS":"RBU","catOutL":"RB"}},{"name":"RB38","nameS":"RB38","icoX":3,"cls":4,"prodCtx":{"name":"RB38","line":"RB38","lineId":"de:nvv:RB|RB38","catOut":"RB ","catOutS":"DDB","catOutL":"RB"}},{"name":"RB39","nameS":"RB39","icoX":3,"cls":4,"prodCtx":{"name":"RB39","line":"RB39","lineId":"de:nvv:RB|RB39","catOut":"RB ","catOutS":"DDB","catOutL":"RB"}},{"name":"RE5","nameS":"RE5","icoX":3,"cls":4,"prodCtx":{"name":"RE5","line":"RE5","lineId":"de:nvv:RE|RE5","catOut":"RE ","catOutS":"DOE","catOutL":"RE"}},{"name":"RE30","nameS":"RE30","icoX":3,"cls":4,"prodCtx":{"name":"RE30","line":"RE30","lineId":"de:rmv:00001298:","catOut":"RE ","catOutS":"DOE","catOutL":"RE"}},{"name":"RE98","nameS":"RE98","icoX":3,"cls":4,"prodCtx":{"name":"RE98","line":"RE98","lineId":"de:nvv:RE|RE98","catOut":"RB ","catOutS":"FHB","catOutL":"RB"}},{"name":"RT5","nameS":"RT5","icoX":0,"cls":1024,"prodCtx":{"name":"RT5","line":"RT5","lineId":"de:nvv:RT|RT5:","catOut":"RT ","catOutS":"RT","catOutL":"RegioTram"}},{"name":"Tram 5","nameS":"5","icoX":0,"cls":32,"prodCtx":{"name":"Tram 5","line":"5","lineId":"de:nvv:Tram|5:","catOut":"Tram ","catOutS":"T","catOutL":"Straßenbahn"}},{"name":"Tram 6","nameS":"6","icoX":0,"cls":32,"prodCtx":{"name":"Tram 6","line":"6","lineId":"de:nvv:Tram|6:","catOut":"Tram ","catOutS":"T","catOutL":"Straßenbahn"}},{"name":"Bus 91","nameS":"91","icoX":4,"cls":64,"prodCtx":{"name":"Bus 91","line":"91","lineId":"de:nvv:AST|91:","catOut":"Bus ","catOutS":"NF","catOutL":"Niederflurbus"}},{"name":"Bus 92","nameS":"92","icoX":4,"cls":64,"prodCtx":{"name":"Bus 92","line":"92","lineId":"de:nvv:AST|92:","catOut":"Bus ","catOutS":"NF","catOutL":"Niederflurbus"}},{"name":"Bus EV","nameS":"EV","icoX":4,"cls":64,"prodCtx":{"name":"Bus EV","line":"EV","lineId":"SEVK4__EV","catOut":"Bus ","catOutS":"1aE","catOutL":"Niederflurbus"}},{"name":"BusEVRT5","nameS":"EVRT5","icoX":4,"cls":64,"prodCtx":{"name":"BusEVRT5","line":"EVRT5","lineId":"de:nvv:RT|RT5:SEV","catOut":"Bus ","catOutS":"NF","catOutL":"Niederflurbus"}},{"name":"AST 91","nameS":"91","icoX":5,"cls":512,"prodCtx":{"name":"AST 91","line":"91","lineId":"de:nvv:AST|91:","catOut":"AST ","catOutS":"AST","catOutL":"AST"}},{"name":"AST 92","nameS":"92","icoX":5,"cls":512,"prodCtx":{"name":"AST 92","line":"92","lineId":"de:nvv:AST|92:","catOut":"AST ","catOutS":"AST","catOutL":"AST"}}],"opL":[{"name":"KVG Tram","icoX":1,"id":"711"}],"remL":[{"type":"I","code":"KT","icoX":2,"txtN":"https://haltestellen.nvv.de/hms-nvv/detailspage?dhid=de:06611:200048"},{"type":"I","code":"KT","icoX":2,"txtN":"https://haltestellen.nvv.de/hms-nvv/detailspage?dhid=de:06633:203069"}],"icoL":[{"res":"","fg":{"r":255,"g":255,"b":255},"bg":{"r":193,"g":0,"b":31},"shp":"R","sty":"B"},{"res":"KVG","txt":"KVG Tram"},{"res":"NVV_HMS"},{"res":"","fg":{"r":255,"g":255,"b":255},"bg":{"r":86,"g":147,"b":135},"shp":"R","sty":"B"},{"res":"","fg":{"r":255,"g":255,"b":255},"bg":{"r":117,"g":22,"b":115},"shp":"C","sty":"B"},{"res":"","fg":{"r":241,"g":209,"b":29},"bg":{"r":26,"g":23,"b":27},"shp":"C","sty":"B"},{"res":"STA_HCIRCLE"},{"res":"rt_ont","txtA":"pünktlich"},{"res":"rt_cnf"}],"lDrawStyleL":[{"sIcoX":0,"type":"SOLID","bg":{"r":193,"g":0,"b":31}},{"type":"SOLID","bg":{"r":193,"g":0,"b":31}}]},"type":"DEP","jnyL":[{"jid":"2|#VN#1#ST#1688407889#PI#0#ZI#16504#TA#2#DA#50723#1S#2200012#1T#808#LS#2203069#LT#854#PU#80#RT#1#CA#T#ZE#5#ZB#Tram 5 #PC#5#FR#2200012#FT#808#TO#2203069#TT#854#","date":"20230705","prodX":0,"dirTxt":"Baunatal","dirFlg":"2","status":"P","isRchbl":true,"stbStop":{"locX":0,"idx":18,"dProdX":0,"dTimeS":"083700","dTimeR":"083700","dProgType":"PROGNOSED","type":"N"},"pos":{"x":9455861,"y":51279415},"subscr":"F","prodL":[{"prodX":0,"fLocX":0,"tLocX":1,"fIdx":18,"tIdx":30}],"sumLDrawStyleX":0,"resLDrawStyleX":1,"trainStartDate":"20230705"}],"fpB":"20230406","fpE":"20231209","planrtTS":"1688538967","sD":"20230705","sT":"083642","locRefL":[0]}}]} -------------------------------------------------------------------------------- /tests/nvv/parsing/departures_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | 4 | from pyhafas.profile import NVVProfile 5 | from pyhafas.types.fptf import Station, StationBoardLeg 6 | 7 | from tests.types import PyTestHafasResponse 8 | 9 | 10 | def test_nvv_departures_parsing(): 11 | directory = os.path.dirname(os.path.realpath(__file__)) 12 | raw_hafas_json_file = open(directory + "/departures_raw.json", "r", encoding="utf8") 13 | hafas_response = PyTestHafasResponse(raw_hafas_json_file.read()) 14 | raw_hafas_json_file.close() 15 | correct_station_board_legs = [StationBoardLeg( 16 | id='2|#VN#1#ST#1688407889#PI#0#ZI#16504#TA#2#DA#50723#1S#2200012#1T#808#LS#2203069#LT#854#PU#80#RT#1#CA#T#ZE#5#ZB#Tram 5 #PC#5#FR#2200012#FT#808#TO#2203069#TT#854#', 17 | name='Tram 5', 18 | direction='Baunatal', 19 | station=Station( 20 | id='2200048', 21 | lid='A=1@O=Kassel Keilsbergstraße@X=9455942@Y=51279199@U=80@L=2200048@', 22 | name='Kassel Keilsbergstraße', 23 | latitude=51.279199, 24 | longitude=9.455942), 25 | date_time=NVVProfile().timezone.localize(datetime.datetime(2023, 7, 5, 8, 37)), 26 | cancelled=False, 27 | delay=datetime.timedelta( 28 | seconds=0))] 29 | assert NVVProfile().parse_station_board_request( 30 | hafas_response, "d") == correct_station_board_legs 31 | -------------------------------------------------------------------------------- /tests/nvv/request/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FahrplanDatenGarten/pyhafas/4e91ffd80fe214ea6c0db2954160eb2813a2e215/tests/nvv/request/__init__.py -------------------------------------------------------------------------------- /tests/nvv/request/arrivals_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from pyhafas import HafasClient 4 | from pyhafas.profile import NVVProfile 5 | 6 | 7 | def test_kvb_arrivals_request(): 8 | client = HafasClient(NVVProfile()) 9 | arrivals = client.arrivals( 10 | station="2200001", 11 | date=datetime.datetime.now(), 12 | max_trips=5 13 | ) 14 | assert arrivals 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/nvv/request/departures_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from pyhafas import HafasClient 4 | from pyhafas.profile import NVVProfile 5 | 6 | 7 | def test_kvb_departures_request(): 8 | client = HafasClient(NVVProfile()) 9 | departures = client.departures( 10 | station="2200001", 11 | date=datetime.datetime.now(), 12 | max_trips=5 13 | ) 14 | assert departures 15 | -------------------------------------------------------------------------------- /tests/nvv/request/journey_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from pyhafas import HafasClient 4 | from pyhafas.profile import NVVProfile 5 | from pyhafas.types.fptf import Journey 6 | 7 | 8 | def test_nvv_journey_request(): 9 | client = HafasClient(NVVProfile()) 10 | journeys = client.journeys( 11 | destination="2202508", 12 | origin="2200001", 13 | date=datetime.datetime.now(), 14 | min_change_time=0, 15 | max_changes=-1 16 | ) 17 | assert journeys 18 | journey = client.journey( 19 | journey=journeys[0].id 20 | ) 21 | assert isinstance(journey, Journey) 22 | -------------------------------------------------------------------------------- /tests/nvv/request/locations_test.py: -------------------------------------------------------------------------------- 1 | from pyhafas import HafasClient 2 | from pyhafas.profile import NVVProfile 3 | 4 | 5 | def test_kvb_locations_request(): 6 | client = HafasClient(NVVProfile()) 7 | locations = client.locations(term="Kassel Hbf") 8 | assert len(locations) >= 1 9 | -------------------------------------------------------------------------------- /tests/nvv/request/nearby_test.py: -------------------------------------------------------------------------------- 1 | from pyhafas import HafasClient 2 | from pyhafas.profile import NVVProfile 3 | from pyhafas.types.nearby import LatLng 4 | from tests.distance import calculate_distance_in_meters 5 | 6 | 7 | def test_nvv_nearby_request(): 8 | pos = LatLng(51.317862, 9.490816) 9 | client = HafasClient(NVVProfile()) 10 | stations = client.nearby(pos) 11 | assert stations 12 | assert calculate_distance_in_meters(pos.latitude, pos.longitude, stations[0].latitude, stations[0].longitude) < 200 13 | -------------------------------------------------------------------------------- /tests/types.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | class PyTestHafasResponse: 5 | def __init__(self, raw_hafas_response: str): 6 | self.raw_hafas_response = raw_hafas_response 7 | data = json.loads(raw_hafas_response) 8 | self.data = data 9 | 10 | @property 11 | def common(self): 12 | return self.data['svcResL'][0]['res']['common'] 13 | 14 | @property 15 | def res(self): 16 | return self.data['svcResL'][0]['res'] 17 | -------------------------------------------------------------------------------- /tests/vsn/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FahrplanDatenGarten/pyhafas/4e91ffd80fe214ea6c0db2954160eb2813a2e215/tests/vsn/__init__.py -------------------------------------------------------------------------------- /tests/vsn/parsing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FahrplanDatenGarten/pyhafas/4e91ffd80fe214ea6c0db2954160eb2813a2e215/tests/vsn/parsing/__init__.py -------------------------------------------------------------------------------- /tests/vsn/parsing/departures_raw.json: -------------------------------------------------------------------------------- 1 | {"ver":"1.24","lang":"deu","err":"OK","svcResL":[{"meth":"StationBoard","err":"OK","res":{"common":{"locL":[{"lid":"A=1@O=Bremen Hbf@X=8813833@Y=53083478@U=80@L=8000050@","type":"S","name":"Bremen Hbf","icoX":2,"extId":"8000050","state":"F","crd":{"x":8813833,"y":53083478,"layerX":0,"crdSysX":0},"pCls":63,"pRefL":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16],"entry":true,"mMastLocX":1},{"lid":"A=1@O=Bremen Hbf@X=8813833@Y=53083478@U=80@L=9013927@","type":"S","name":"Bremen Hbf","icoX":2,"extId":"9013927","state":"F","crd":{"x":8813833,"y":53083478,"layerX":0,"crdSysX":0},"meta":true,"pCls":831,"isMainMast":true},{"lid":"A=1@O=Frankfurt(Main) Hbf@X=8663785@Y=50107149@U=80@L=8000105@","type":"S","name":"Frankfurt(Main) Hbf","icoX":2,"extId":"8000105","state":"F","crd":{"x":8663785,"y":50107149,"layerX":0,"crdSysX":0},"pCls":59}],"prodL":[{"pid":"L::1::IC::B3150904150::IC_3150904150::*","name":"IC 2315","nameS":"30","number":"2315","icoX":0,"cls":2,"oprX":0,"prodCtx":{"name":"IC 2315 ","num":"2315","line":"30","lineId":"80_____30","matchId":"2315","catOut":"IC ","catOutS":"IC","catOutL":"InterCity","catIn":"IC","catCode":"1","admin":"80____"}},{"name":"ICE","icoX":2,"cls":1,"prodCtx":{"name":"ICE ","line":"","catOut":"ICE ","catOutS":"ICE","catOutL":"InterCityExpress"}},{"name":"NJ","icoX":2,"cls":1,"prodCtx":{"name":"NJ ","line":"","catOut":"NJ ","catOutS":"NJ","catOutL":"Nightjet"}},{"name":"EC","icoX":0,"cls":2,"prodCtx":{"name":"EC ","line":"","catOut":"EC ","catOutS":"EC","catOutL":"EuroCity"}},{"name":"IC","icoX":0,"cls":2,"prodCtx":{"name":"IC ","line":"","catOut":"IC ","catOutS":"IC","catOutL":"InterCity"}},{"name":"DPF","nameS":"DPF","icoX":3,"cls":4,"prodCtx":{"name":"DPF","line":"DPF","lineId":"NY_____DPF","catOut":"DPF ","catOutS":"DPF","catOutL":"Fernreisezug"}},{"name":"UEX","nameS":"UEX","icoX":3,"cls":4,"prodCtx":{"name":"UEX","line":"UEX","lineId":"NYUEX__UEX","catOut":"DPF ","catOutS":"DPF","catOutL":"Fernreisezug"}},{"name":"MEX","nameS":"MEX","icoX":4,"cls":8,"prodCtx":{"name":"MEX","line":"MEX","lineId":"E0_____MEX","catOut":"Zug ","catOutS":"DPN","catOutL":"Nahreisezug"}},{"name":"RB37","nameS":"RB37","icoX":4,"cls":8,"prodCtx":{"name":"RB37","line":"RB37","lineId":"X1_____RB37","catOut":"Zug ","catOutS":"DPN","catOutL":"Nahreisezug"}},{"name":"RB41","nameS":"RB41","icoX":4,"cls":8,"prodCtx":{"name":"RB41","line":"RB41","lineId":"R1_____RB41","catOut":"Zug ","catOutS":"DPN","catOutL":"Nahreisezug"}},{"name":"RB58","nameS":"RB58","icoX":4,"cls":8,"prodCtx":{"name":"RB58","line":"RB58","lineId":"N1_____RB58","catOut":"Zug ","catOutS":"DPN","catOutL":"Nahreisezug"}},{"name":"RE1","nameS":"RE1","icoX":4,"cls":8,"prodCtx":{"name":"RE1","line":"RE1","lineId":"de:LNVG:RE1:","catOut":"RE ","catOutS":"RE","catOutL":"Regional-Express"}},{"name":"RS1","nameS":"RS1","icoX":5,"cls":16,"prodCtx":{"name":"RS1","line":"RS1","lineId":"de:VBN:RS1:","catOut":"S ","catOutS":"DPS","catOutL":"S-Bahn"}},{"name":"RS2","nameS":"RS2","icoX":5,"cls":16,"prodCtx":{"name":"RS2","line":"RS2","lineId":"de:VBN:RS2:","catOut":"S ","catOutS":"DPS","catOutL":"S-Bahn"}},{"name":"RS3","nameS":"RS3","icoX":5,"cls":16,"prodCtx":{"name":"RS3","line":"RS3","lineId":"de:VBN:RS3:","catOut":"S ","catOutS":"DPS","catOutL":"S-Bahn"}},{"name":"RS4","nameS":"RS4","icoX":5,"cls":16,"prodCtx":{"name":"RS4","line":"RS4","lineId":"de:VBN:RS4:","catOut":"S ","catOutS":"DPS","catOutL":"S-Bahn"}},{"name":"SEV","nameS":"SEV","icoX":6,"cls":32,"prodCtx":{"name":"SEV","line":"SEV","lineId":"R1_____SEV","catOut":"SEV ","catOutS":"Bsv","catOutL":"Schienenersatzverkehr"}}],"polyL":[],"layerL":[{"id":"standard","name":"standard","index":0,"annoCnt":0}],"crdSysL":[{"id":"standard","index":0,"type":"WGS84"}],"opL":[{"name":"DB Fernverkehr","icoX":1,"id":"1175"}],"remL":[{"type":"A","code":"FR","prio":260,"icoX":7,"txtN":"Fahrradmitnahme reservierungspflichtig"},{"type":"A","code":"FK","prio":260,"icoX":8,"txtN":"Fahrradmitnahme begrenzt möglich"}],"himL":[{"hid":"15528","act":true,"head":"Corona-Präventionsmaßnahme","text":"Derzeit gilt die Mund-Nasen-Bedeckungspflicht im ÖPNV. Danke, dass Sie sich und andere Fahrgäste schützen und zur Sicherheit des Bus- und Bahnfahrens beitragen!","icoX":9,"prio":1,"prod":65535,"lModDate":"20200602","lModTime":"153245","sDate":"20200424","sTime":"150000","eDate":"20201231","eTime":"235900","sDaily":"000000","eDaily":"235900","comp":"VBN","catRefL":[0],"pubChL":[{"name":"timetable","fDate":"20200424","fTime":"150000","tDate":"20201231","tTime":"235900"}]}],"icoL":[{"res":"prod_ic","fg":{"r":255,"g":255,"b":255},"bg":{"r":239,"g":119,"b":28}},{"res":"DB","txt":"DB Fernverkehr"},{"res":"prod_ice","fg":{"r":255,"g":255,"b":255},"bg":{"r":150,"g":25,"b":29}},{"res":"prod_ir","fg":{"r":255,"g":255,"b":255},"bg":{"r":62,"g":134,"b":144}},{"res":"prod_reg","fg":{"r":255,"g":255,"b":255},"bg":{"r":82,"g":38,"b":125}},{"res":"prod_comm","fg":{"r":255,"g":255,"b":255},"bg":{"r":19,"g":148,"b":69}},{"res":"prod_bus","fg":{"r":255,"g":255,"b":255},"bg":{"r":0,"g":92,"b":169}},{"res":"attr_bike_r"},{"res":"attr_bike"},{"res":"HIM2"}],"himMsgCatL":[{"id":2}],"lDrawStyleL":[{"sIcoX":0,"type":"SOLID","bg":{"r":239,"g":119,"b":28}},{"type":"SOLID","bg":{"r":239,"g":119,"b":28}}]},"type":"DEP","jnyL":[{"jid":"1|265947|0|80|1082020","date":"20200801","prodX":0,"dirTxt":"Frankfurt(Main) Hbf","dirFlg":"1","status":"P","isRchbl":true,"stbStop":{"locX":0,"idx":8,"dProdX":0,"dPltfS":{"type":"PL","txt":"8"},"dPltfR":{"type":"U","txt":"8"},"dTimeS":"174400","dTimeR":"184200","dProgType":"PROGNOSED","type":"N"},"msgL":[{"type":"REM","remX":0,"sty":"I","dspl":"U","fLocX":0,"tLocX":2,"tagL":["RES_JNY_DTL"],"sort":1141899264,"persist":false},{"type":"REM","remX":1,"sty":"I","dspl":"U","fLocX":0,"tLocX":2,"tagL":["RES_JNY_DTL"],"sort":1141899264,"persist":false},{"type":"HIM","himX":0,"sty":"M","fLocX":0,"tagL":["RES_GLB_HDR_H3","SUM_GLB_HDR_H3"],"sort":807680508}],"subscr":"F","prodL":[{"prodX":0,"fLocX":0,"tLocX":2,"fIdx":8,"tIdx":20}],"sumLDrawStyleX":0,"resLDrawStyleX":1}],"fpB":"20191215","fpE":"20201212","planrtTS":"1596298820","sD":"20200801","sT":"182119","locRefL":[0]}}]} 2 | -------------------------------------------------------------------------------- /tests/vsn/parsing/departures_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | 4 | from pyhafas.profile.vsn import VSNProfile 5 | from pyhafas.types.fptf import Station, StationBoardLeg 6 | 7 | from tests.types import PyTestHafasResponse 8 | 9 | 10 | def test_vsn_departures_parsing(): 11 | directory = os.path.dirname(os.path.realpath(__file__)) 12 | raw_hafas_json_file = open(directory + "/departures_raw.json", "r", encoding="utf8") 13 | hafas_response = PyTestHafasResponse(raw_hafas_json_file.read()) 14 | raw_hafas_json_file.close() 15 | correct_station_board_legs = [StationBoardLeg( 16 | id='1|265947|0|80|1082020', 17 | name='IC 2315', 18 | direction='Frankfurt(Main) Hbf', 19 | station=Station( 20 | id='8000050', 21 | lid='A=1@O=Bremen Hbf@X=8813833@Y=53083478@U=80@L=8000050@', 22 | name='Bremen Hbf', 23 | latitude=53.083478, 24 | longitude=8.813833), 25 | date_time=VSNProfile().timezone.localize(datetime.datetime(2020, 8, 1, 17, 44)), 26 | cancelled=False, 27 | delay=datetime.timedelta( 28 | seconds=3480), 29 | platform='8')] 30 | assert VSNProfile().parse_station_board_request( 31 | hafas_response, "d") == correct_station_board_legs 32 | -------------------------------------------------------------------------------- /tests/vsn/parsing/journeys_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | 4 | from pyhafas.profile.vsn import VSNProfile 5 | from pyhafas.types.fptf import (Journey, Leg, Mode, Station, StationBoardLeg, 6 | Stopover, Remark) 7 | 8 | from tests.types import PyTestHafasResponse 9 | 10 | 11 | def test_vsn_journeys_parsing(): 12 | directory = os.path.dirname(os.path.realpath(__file__)) 13 | raw_hafas_json_file = open(directory + "/journeys_raw.json", "r", encoding="utf8") 14 | hafas_response = PyTestHafasResponse(raw_hafas_json_file.read()) 15 | raw_hafas_json_file.close() 16 | correct_journeys = [ 17 | Journey( 18 | id='¶HKI¶T$A=1@O=Göttingen@L=8000128@a=128@$A=1@O=Lenglern@L=8003644@a=128@$202008090710$202008090719$ RB85$$1$$$', 19 | date=datetime.date( 20 | 2020, 21 | 8, 22 | 9), 23 | duration=datetime.timedelta( 24 | seconds=540), 25 | legs=[ 26 | Leg( 27 | id='1|147532|0|80|9082020', 28 | origin=Station( 29 | id='8000128', 30 | lid='A=1@O=Göttingen@X=9926069@Y=51536812@U=80@L=8000128@', 31 | name='Göttingen', 32 | latitude=51.536812, 33 | longitude=9.926069 34 | ), 35 | destination=Station( 36 | id='8003644', 37 | lid='A=1@O=Lenglern@X=9871199@Y=51588428@U=80@L=8003644@', 38 | name='Lenglern', 39 | latitude=51.588428, 40 | longitude=9.871199 41 | ), 42 | departure=VSNProfile().timezone.localize(datetime.datetime( 43 | 2020, 44 | 8, 45 | 9, 46 | 7, 47 | 10 48 | )), 49 | arrival=VSNProfile().timezone.localize(datetime.datetime( 50 | 2020, 51 | 8, 52 | 9, 53 | 7, 54 | 19 55 | )), 56 | mode=Mode.TRAIN, 57 | name='RB85', 58 | cancelled=False, 59 | distance=None, 60 | departure_delay=datetime.timedelta(seconds=0), 61 | departure_platform='4', 62 | arrival_delay=datetime.timedelta(seconds=60), 63 | arrival_platform='1', 64 | stopovers=[ 65 | Stopover( 66 | stop=Station( 67 | id='8000128', 68 | lid='A=1@O=Göttingen@X=9926069@Y=51536812@U=80@L=8000128@', 69 | name='Göttingen', 70 | latitude=51.536812, 71 | longitude=9.926069 72 | ), 73 | cancelled=False, 74 | arrival=None, 75 | arrival_delay=None, 76 | arrival_platform=None, 77 | departure=VSNProfile().timezone.localize(datetime.datetime( 78 | 2020, 79 | 8, 80 | 9, 81 | 7, 82 | 10)), 83 | departure_delay=datetime.timedelta(seconds=0), 84 | departure_platform=None 85 | ), 86 | Stopover( 87 | stop=Station( 88 | id='8003644', 89 | lid='A=1@O=Lenglern@X=9871199@Y=51588428@U=80@L=8003644@', 90 | name='Lenglern', 91 | latitude=51.588428, 92 | longitude=9.871199 93 | ), 94 | cancelled=False, 95 | arrival=VSNProfile().timezone.localize(datetime.datetime( 96 | 2020, 97 | 8, 98 | 9, 99 | 7, 100 | 19)), 101 | arrival_delay=datetime.timedelta(seconds=60), 102 | arrival_platform=None, 103 | departure=None, 104 | departure_delay=None, 105 | departure_platform=None 106 | ) 107 | ], 108 | remarks=[ 109 | Remark( 110 | remark_type='A', 111 | code='PB', 112 | subject=None, 113 | text='Pflicht zur Bedeckung von Mund und Nase', 114 | priority=200, 115 | trip_id=None 116 | ), 117 | Remark( 118 | remark_type='A', 119 | code='FK', 120 | subject=None, 121 | text='Fahrradmitnahme begrenzt möglich', 122 | priority=260, 123 | trip_id=None 124 | ), 125 | Remark( 126 | remark_type='A', 127 | code='NW', 128 | subject=None, 129 | text='Linie der NordWestBahn, Info unter 01806 600161', 130 | priority=899, 131 | trip_id=None 132 | ) 133 | ] 134 | ) 135 | ] 136 | ) 137 | ] 138 | assert VSNProfile().parse_journeys_request(hafas_response) == correct_journeys 139 | -------------------------------------------------------------------------------- /tests/vsn/request/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FahrplanDatenGarten/pyhafas/4e91ffd80fe214ea6c0db2954160eb2813a2e215/tests/vsn/request/__init__.py -------------------------------------------------------------------------------- /tests/vsn/request/arrivals_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from pyhafas import HafasClient 4 | from pyhafas.profile import VSNProfile 5 | 6 | 7 | def test_vsn_arrivals_request(): 8 | client = HafasClient(VSNProfile()) 9 | arrivals = client.arrivals( 10 | station="009033817", 11 | date=datetime.datetime.now(), 12 | max_trips=5 13 | ) 14 | assert arrivals 15 | -------------------------------------------------------------------------------- /tests/vsn/request/departures_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from pyhafas import HafasClient 4 | from pyhafas.profile import VSNProfile 5 | 6 | 7 | def test_vsn_departures_request(): 8 | client = HafasClient(VSNProfile()) 9 | departures = client.departures( 10 | station="009033817", 11 | date=datetime.datetime.now(), 12 | max_trips=5 13 | ) 14 | assert departures 15 | -------------------------------------------------------------------------------- /tests/vsn/request/journey_test.py: -------------------------------------------------------------------------------- 1 | from pyhafas import HafasClient 2 | from pyhafas.profile import VSNProfile 3 | from pyhafas.types.fptf import Journey 4 | import datetime 5 | 6 | 7 | def test_vsn_journey_request(): 8 | client = HafasClient(VSNProfile()) 9 | journeys = client.journeys( 10 | destination="9068991", 11 | origin="9033817", 12 | date=datetime.datetime.now(), 13 | min_change_time=0, 14 | max_changes=-1 15 | ) 16 | 17 | journey = client.journey( 18 | journey=journeys[0].id 19 | ) 20 | assert isinstance(journey, Journey) 21 | -------------------------------------------------------------------------------- /tests/vsn/request/locations_test.py: -------------------------------------------------------------------------------- 1 | from pyhafas import HafasClient 2 | from pyhafas.profile import VSNProfile 3 | 4 | 5 | def test_vsn_locations_request(): 6 | client = HafasClient(VSNProfile()) 7 | locations = client.locations(term="Göttingen Bahnhof/ZOB") 8 | assert len(locations) >= 1 9 | -------------------------------------------------------------------------------- /tests/vsn/request/nearby_test.py: -------------------------------------------------------------------------------- 1 | from pyhafas import HafasClient 2 | from pyhafas.profile import VSNProfile 3 | from pyhafas.types.nearby import LatLng 4 | from tests.distance import calculate_distance_in_meters 5 | 6 | 7 | def test_vsn_nearby_request(): 8 | pos = LatLng(51.536403, 9.927406) 9 | client = HafasClient(VSNProfile()) 10 | stations = client.nearby(pos) 11 | assert stations 12 | assert calculate_distance_in_meters(pos.latitude, pos.longitude, stations[0].latitude, stations[0].longitude) < 200 13 | -------------------------------------------------------------------------------- /tests/vvv/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FahrplanDatenGarten/pyhafas/4e91ffd80fe214ea6c0db2954160eb2813a2e215/tests/vvv/__init__.py -------------------------------------------------------------------------------- /tests/vvv/parsing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FahrplanDatenGarten/pyhafas/4e91ffd80fe214ea6c0db2954160eb2813a2e215/tests/vvv/parsing/__init__.py -------------------------------------------------------------------------------- /tests/vvv/parsing/departures_raw.json: -------------------------------------------------------------------------------- 1 | {"ver":"1.59","ext":"VAO.20","lang":"deu","err":"OK","svcResL":[{"id":"1|81|","meth":"StationBoard","err":"OK","res":{"common":{"locL":[{"lid":"A=1@O=Ebnit Heumöser@X=9742491@Y=47345429@U=81@L=480077901@i=A×at:48:779:0:1@","type":"S","name":"Ebnit Heumöser","icoX":0,"extId":"480077901","state":"F","crd":{"x":9742491,"y":47345429,"floor":0},"pCls":64,"pRefL":[1],"entry":true,"mMastLocX":1,"globalIdL":[{"id":"at:48:779:0:1","type":"A"}],"chgTime":"000200"}],"prodL":[{"pid":"L::6::::B2836125902::vvv-14-177-E-j23-1::*","name":"Landbus 177","nameS":"177","number":"177","icoX":0,"cls":64,"oprX":0,"prodCtx":{"name":" 177","num":"500","line":"177","lineId":"vvv-14-177-E-j23-1","matchId":"vvv-14-177-E#16","catOutS":"V00","catOutL":"Landbus","catIn":"V00","catCode":"6","admin":"V52RTB"}}]},"type":"DEP","jnyL":[{"jid":"2|#VN#1#ST#1698712882#PI#0#ZI#257543#TA#0#DA#11123#1S#480077901#1T#832#LS#480057101#LT#926#PU#81#RT#1#CA#V00#ZE#177#ZB# 177#PC#6#FR#480077901#FT#832#TO#480057101#TT#926#","date":"20231101","prodX":0,"dirTxt":"Dornbirn Bahnhof","dirFlg":"R","status":"P","isRchbl":true,"stbStop":{"locX":0,"idx":0,"dProdX":0,"dTimeS":"083200","dTimeFS":{"styleX":0},"dTimeFC":{"styleX":1},"type":"N"},"stopL":[{"locX":0,"idx":0,"dTimeS":"083200","dTimeFS":{"styleX":0},"dTimeFC":{"styleX":1},"isImp":true,"type":"N"}],"subscr":"F","prodL":[{"prodX":0,"fLocX":0,"tLocX":2,"fIdx":0,"tIdx":33}],"sumLDrawStyleX":0,"resLDrawStyleX":1,"trainStartDate":"20231101"}],"fpB":"20230830","fpE":"20240531","planrtTS":"1698789074","sD":"20231031","sT":"225103","locRefL":[0]}}]} -------------------------------------------------------------------------------- /tests/vvv/parsing/departures_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | 4 | from pyhafas.profile import VVVProfile 5 | from pyhafas.types.fptf import Station, StationBoardLeg 6 | 7 | from tests.types import PyTestHafasResponse 8 | 9 | 10 | def test_vvv_departures_parsing(): 11 | directory = os.path.dirname(os.path.realpath(__file__)) 12 | raw_hafas_json_file = open(directory + "/departures_raw.json", "r", encoding="utf8") 13 | hafas_response = PyTestHafasResponse(raw_hafas_json_file.read()) 14 | raw_hafas_json_file.close() 15 | correct_station_board_legs = [StationBoardLeg( 16 | id='2|#VN#1#ST#1698712882#PI#0#ZI#257543#TA#0#DA#11123#1S#480077901#1T#832#LS#480057101#LT#926#PU#81#RT#1#CA#V00#ZE#177#ZB# 177#PC#6#FR#480077901#FT#832#TO#480057101#TT#926#', 17 | name='Landbus 177', 18 | direction='Dornbirn Bahnhof', 19 | station=Station( 20 | id='480077901', 21 | lid='A=1@O=Ebnit Heumöser@X=9742491@Y=47345429@U=81@L=480077901@i=A×at:48:779:0:1@', 22 | name='Ebnit Heumöser', 23 | latitude=47.345429, 24 | longitude=9.742491), 25 | date_time=VVVProfile().timezone.localize(datetime.datetime(2023, 11, 1, 8, 32)), 26 | cancelled=False, 27 | delay=None)] 28 | assert VVVProfile().parse_station_board_request( 29 | hafas_response, "d") == correct_station_board_legs 30 | -------------------------------------------------------------------------------- /tests/vvv/request/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FahrplanDatenGarten/pyhafas/4e91ffd80fe214ea6c0db2954160eb2813a2e215/tests/vvv/request/__init__.py -------------------------------------------------------------------------------- /tests/vvv/request/arrivals_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from pyhafas import HafasClient 4 | from pyhafas.profile import VVVProfile 5 | 6 | 7 | def test_vvv_arrivals_request(): 8 | client = HafasClient(VVVProfile()) 9 | arrivals = client.arrivals( 10 | station="480063000", 11 | date=datetime.datetime.now(), 12 | max_trips=5 13 | ) 14 | assert arrivals 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/vvv/request/departures_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from pyhafas import HafasClient 4 | from pyhafas.profile import VVVProfile 5 | 6 | 7 | def test_vvv_departures_request(): 8 | client = HafasClient(VVVProfile()) 9 | departures = client.departures( 10 | station="480063000", 11 | date=datetime.datetime.now(), 12 | max_trips=5 13 | ) 14 | assert departures 15 | -------------------------------------------------------------------------------- /tests/vvv/request/journey_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from pyhafas import HafasClient 4 | from pyhafas.profile import VVVProfile 5 | from pyhafas.types.fptf import Journey 6 | 7 | 8 | def test_vvv_journey_request(): 9 | client = HafasClient(VVVProfile()) 10 | journeys = client.journeys( 11 | destination="480045200", 12 | origin="480058500", 13 | date=datetime.datetime.now(), 14 | min_change_time=0, 15 | max_changes=-1 16 | ) 17 | assert journeys 18 | journey = client.journey(journey=journeys[0].id) 19 | assert isinstance(journey, Journey) 20 | -------------------------------------------------------------------------------- /tests/vvv/request/locations_test.py: -------------------------------------------------------------------------------- 1 | from pyhafas import HafasClient 2 | from pyhafas.profile import VVVProfile 3 | 4 | 5 | def test_vvv_locations_request(): 6 | client = HafasClient(VVVProfile()) 7 | locations = client.locations(term="Rankweil Bifangstr.") 8 | assert len(locations) >= 1 9 | -------------------------------------------------------------------------------- /tests/vvv/request/nearby_test.py: -------------------------------------------------------------------------------- 1 | from pyhafas import HafasClient 2 | from pyhafas.profile import VVVProfile 3 | from pyhafas.types.nearby import LatLng 4 | from tests.distance import calculate_distance_in_meters 5 | 6 | 7 | def test_vvv_nearby_request(): 8 | pos = LatLng(47.272502, 9.638032) 9 | client = HafasClient(VVVProfile()) 10 | stations = client.nearby(pos) 11 | assert stations 12 | assert calculate_distance_in_meters(pos.latitude, pos.longitude, stations[0].latitude, stations[0].longitude) < 200 13 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{36,37,38}-requests{oldest,latest}-pytz{oldest,latest} 3 | 4 | [travis] 5 | python = 6 | 3.6: py36 7 | 3.7: py37 8 | 3.8: py38 9 | 10 | [travis:env] 11 | REQUESTS_VERSION = 12 | oldest: requestsoldest 13 | latest: requestslatest 14 | 15 | PYTZ_VERSION = 16 | oldest: pytzoldest 17 | latest: pytzlatest 18 | 19 | [testenv] 20 | deps = 21 | pytest 22 | requestsoldest: requests==2.9.0 23 | requestslatest: requests~=2.9 24 | pytzoldest: pytz==2013.6 25 | pytzlatest: pytz>=2013.6 26 | commands = pytest 27 | --------------------------------------------------------------------------------